Просмотр исходного кода

fix(impact): resolve chained method calls (a.b.Method()) — C# extension methods

matchMethodCall's pattern only accepted a single-segment receiver
(`^(\w+)\.(\w+)$`), so a call through a property/member chain
(`builder.Services.AddCoreServices(...)`) matched nothing and never reached the
method-name fallback (Strategy 3). Broadened the receiver to allow dots so the
last segment is taken as the method name and resolved by name (with Strategy 3's
existing single-candidate / receiver-overlap guards). This is the common C#
extension-method shape — ASP.NET DI registration (`builder.Services.AddX()`),
Guard clauses (`Guard.Against.X()`), LINQ-style chains.

Note: C# constructor DI / interface->implementation dispatch ALREADY works
(`csharp` is in IFACE_OVERRIDE_LANGS; service/repo impls were already covered) —
the gap was these chained extension calls, not DI dispatch.

FAIR coverage: ASP.NET eShopOnWeb 62.3% -> 65.3% (DI-registration + Guard
extension classes now covered). No regression: cs-mediatr 90.7%, cs-polly 80.7%,
gin/requests/fastapi unchanged. Full suite 1172; 1 regression test.

ASP.NET's remaining ~70 zeros are convention/reflection ENTRIES (MVC controllers,
Razor Pages, API endpoint classes, Blazor .razor-driven code-behind, EF
IEntityTypeConfiguration registered by reflection) with no static in-repo caller —
a genuine static-analysis frontier; literal 95% needs framework-entry metric
exclusions or per-convention modeling (a separate decision).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby McHenry 2 недель назад
Родитель
Сommit
4c144134b9
3 измененных файлов с 48 добавлено и 1 удалено
  1. 1 0
      CHANGELOG.md
  2. 41 0
      __tests__/extraction.test.ts
  3. 6 1
      src/resolution/name-matcher.ts

+ 1 - 0
CHANGELOG.md

@@ -40,6 +40,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Python now also links a whole-module **absolute** import (`import conduit.apps.signals`) to that module's file, not just `from x import y`. A module imported by its dotted path — common in package setup and side-effect imports — is no longer reported as having no dependents. Standard-library imports (`import os`) correctly create no edge. (Python)
 - Python `from package import submodule` now links to that submodule's file, resolved through the import's package so it lands on the right one when same-named modules exist in sibling packages (the FastAPI / Django router-aggregator pattern: `from app.api.routes import authentication` with an unrelated `authentication.py` elsewhere). So a route/handler module pulled in only by an aggregator is no longer reported as having no dependents. (Python)
 - Django `include('app.urls')` now records a dependency from the project URLconf onto the included app's `urls.py`, so an app's routes module is no longer reported as having no dependents and editing it surfaces the project that mounts it. (Django)
+- A chained method call (`builder.Services.AddCoreServices(...)`) now resolves to its definition. Previously only a single-segment receiver (`obj.method()`) resolved, so a call through a property/member chain — very common for C# extension methods like ASP.NET dependency-injection registration (`AddCoreServices`/`AddWebServices`) and Guard clauses — found no definition. (C#, and any language with chained calls)
 - The background file watcher no longer exhausts your machine's file-descriptor budget. On macOS it previously kept **one open file handle per watched file**, so on a large project the running MCP server could pile up tens of thousands of handles and blow past the system-wide limit — at which point *unrelated* apps (your shell, editor, Docker, browser) started failing with "too many open files" until the codegraph process was killed. The watcher now uses a single recursive watch on macOS and Windows, and bounded per-directory watches on Linux, so its cost stays flat no matter how large the project is. (#644, #496, #555, #628, #579)
 - Indexing a project with very symbol-dense files (tens of thousands of functions or methods in a single file) no longer runs out of memory. The step that links dynamic call relationships used to load every function and method into memory at once, which could exhaust the heap and abort indexing with "JavaScript heap out of memory" on large or generated codebases; it now streams them, so memory stays flat no matter how many symbols the project has. (#610)
 - Indexing a very large repository no longer aborts during its first sync with a "too many SQL variables" error. (#540)

+ 41 - 0
__tests__/extraction.test.ts

@@ -3953,6 +3953,47 @@ describe('Python absolute module import resolution', () => {
   });
 });
 
+describe('Chained method-call resolution (C# extension methods)', () => {
+  let tempDir: string;
+  let cg: CodeGraph;
+
+  beforeEach(() => {
+    tempDir = createTempDir();
+  });
+
+  afterEach(() => {
+    if (cg) cg.close();
+    if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
+  });
+
+  it('resolves a chained extension-method call (a.b.Method()) to its definition', async () => {
+    // ASP.NET DI registration: `builder.Services.AddCoreServices(...)` calls a
+    // static extension method elsewhere. A multi-dot receiver chain matched no
+    // method-call pattern before, so the extension method had no caller.
+    fs.mkdirSync(path.join(tempDir, 'cfg'), { recursive: true });
+    fs.writeFileSync(
+      path.join(tempDir, 'cfg/Ext.cs'),
+      `namespace App {\n  public static class Ext {\n    public static object AddCoreServices(this object services, int x) { return services; }\n  }\n}\n`
+    );
+    fs.writeFileSync(
+      path.join(tempDir, 'Program.cs'),
+      `namespace App {\n  public class Program {\n    public void Run(object builder) {\n      builder.Services.AddCoreServices(1);\n    }\n  }\n}\n`
+    );
+
+    cg = CodeGraph.initSync(tempDir);
+    await cg.indexAll();
+    cg.resolveReferences();
+
+    const ext = cg
+      .getNodesByKind('method')
+      .find((n) => n.name === 'AddCoreServices')
+      ?? cg.getNodesByKind('function').find((n) => n.name === 'AddCoreServices');
+    expect(ext, 'AddCoreServices defined').toBeDefined();
+    const callers = [...cg.getImpactRadius(ext!.id, 2).nodes.values()].map((n) => n.filePath ?? '');
+    expect(callers.some((p) => p.endsWith('Program.cs')), 'chained extension call resolves to its definition').toBe(true);
+  });
+});
+
 describe('Same-directory include + KMP import resolution', () => {
   let tempDir: string;
   let cg: CodeGraph;

+ 6 - 1
src/resolution/name-matcher.ts

@@ -463,7 +463,12 @@ export function matchMethodCall(
   // part allows trailing `:` keywords so Objective-C selectors resolve
   // (`SDImageCache.storeImage:`, `obj.setX:y:`); colons never appear in other
   // languages' method refs, so this is a no-op for them.
-  const dotMatch = ref.referenceName.match(/^(\w+)\.(\w+:?(?:\w+:)*)$/);
+  // The receiver allows dots (`builder.Services.AddCoreServices`) so a CHAINED
+  // call resolves by its last segment — Strategy 3 below name-matches the method
+  // (with its existing single-candidate / receiver-overlap guards). Without this
+  // a multi-dot extension-method call (C# DI `builder.Services.AddCoreServices()`,
+  // `Guard.Against.X()`) matched no pattern and never resolved.
+  const dotMatch = ref.referenceName.match(/^([\w.]+)\.(\w+:?(?:\w+:)*)$/);
   const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
 
   const match = dotMatch || colonMatch;