Ver código fonte

fix(pascal): resolve chained factory calls TFoo.GetInstance().DoIt() (#750) (#791)

Ports the #645/#608 chained-receiver mechanism to Pascal/Delphi — which I'd
previously mis-scoped as blocked. The paren'd chained form extracts fine; it just
hit the chained-call gap like the others (with a decoy, `TFoo.GetInstance().DoIt()`
mis-resolved to a same-named method on an unrelated class).

- pascal.ts: getReturnType reads the method's `typeref` (a `function GetInstance:
  TBar` returns TBar; an interface return `IFoo` is captured too).
- tree-sitter.ts: extractPascalCall now re-encodes a chained call `TFoo.GetInstance().DoIt`
  (the exprDot's receiver is an exprCall) instead of collapsing it to bare `DoIt`.
  Gated on the Delphi type-naming convention (`TFoo`/`IFoo`) so a capitalized
  VARIABLE chain (Pascal capitalizes locals too — `Curve.X().Y()`, `Self.X().Y()`)
  stays bare and keeps its existing bare-name resolution.
- name-matcher.ts: `pascal` joins the dotted-chain gate + CHAIN_LANGUAGES +
  CONSTRUCTS_VIA_BARE_CALL (a `TFoo(x)` typecast yields a TFoo). When the factory's
  return type wasn't captured (a `constructor Create` has no `: TBar` but returns
  its class), resolve the method on the factory class itself. resolveMethodOnType
  validates, so a wrong inference yields no edge.

Validation: 4 synthetic tests (factory+decoy, constructor chain, typecast chain,
absent-method safety). Real-repo A/B on PascalCoin (772 files): +19 / -18 — 15 of
the -18 are correct class→interface retargets (`GetInstance(): IAsn1OctetString`
resolves `.GetOctets` on the declared interface, not baseline's concrete-class
guess); 3 are negligible drops (0.02%). EXTRACTION_VERSION 15->16. Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby Mchenry 1 semana atrás
pai
commit
af56f3539d

+ 1 - 0
CHANGELOG.md

@@ -34,6 +34,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Rust method calls made through a chained associated function now resolve to the correct type. A call like `Foo::new().bar()` or `Foo::with(cfg).build()` used to drop the receiver, so the chained method silently attached to a same-named method on an unrelated type — or didn't resolve. CodeGraph now captures Rust return types (`-> Self` resolves to the implementing type), infers the chained receiver's type from what the associated function returns, and resolves the method on it — including methods provided by a trait the type implements (via the new `impl Trait for Type` relationships) — creating the edge only when the type or one of its traits genuinely has the method. Existing Rust indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Rust)
 - Dart method calls made through a static factory, a factory or named constructor, or a fluent chain now resolve to the correct type. A call like `Foo.create().bar()` used to drop the receiver, so the chained method silently attached to a same-named method on an unrelated type — most often mis-attributing a standard-library `Option` / `Iterator` `.map` / `.where` onto your own same-named class. CodeGraph now indexes Dart **factory and named constructors** (`factory Foo.create()`, `Foo.named()`) as first-class members so calls to them resolve, captures Dart return types (a generic `List<Foo>` resolves to its container `List`), infers the chained receiver's type from what the inner call returns or constructs, and resolves the method on it — including methods inherited from a superclass or mixin — creating the edge only when that type genuinely has the method. Plain construction (`Foo(...)`) is still recorded as instantiation. Existing Dart indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Dart)
 - Objective-C methods called through a chained message send now resolve to the correct class. A call like `[[Foo create] doIt]` used to drop the receiver, so `doIt` silently attached to a same-named method on an unrelated class — most often a test helper or stdlib class. CodeGraph now captures Objective-C method return types and infers the chained receiver's type from what the inner message returns. For the ubiquitous `[[X alloc] init]` and singleton (`[[X sharedInstance] …]`) patterns — where the factory returns `instancetype` — the receiver is the class `X` itself, so the chained method resolves on `X` (including methods inherited from a superclass), creating the edge only when the class genuinely has the method. Existing Objective-C indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Objective-C)
+- Pascal/Delphi methods called through a chained factory call now resolve to the correct class. A call like `TFoo.GetInstance().DoIt()` used to drop the receiver, so `DoIt` silently attached to a same-named method on an unrelated class. CodeGraph now captures Pascal return types and infers the chained receiver's type from what the factory function returns — resolving to the declared type (including an interface return like `IFoo`), and for a constructor (`TFoo.Create().…`) or a typecast (`TFoo(x).…`) to the class `TFoo` itself, since both yield a `TFoo`. The edge is created only when that type genuinely has the method (so a wrong inference produces no edge). Existing Pascal/Delphi indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Pascal/Delphi)
 - Chained method calls now resolve when the chained method is **inherited from a superclass or declared on an interface/protocol** the receiver's type conforms to — for example a call on a sealed-subclass instance (`Either.Right(x).combine(...)`) that invokes a method defined on its parent type. Previously these chains found no caller edge even though the factory's type was known, so the call was invisible to callers, impact, and trace. CodeGraph now walks the type's supertypes (its `extends` / `implements` relationships) to find the method, creating the edge only when a supertype genuinely declares it (so a wrong inference still produces no edge). This makes Java, Kotlin, and C# factory and fluent chains more complete. Existing indexes should be re-indexed (`codegraph index -f`) to benefit. (#750)
 - Swift method calls made through a static factory, fluent chain, or constructor now resolve to the correct class. A call like `Foo.make().draw()` or `Foo().draw()` used to drop the receiver, so the chained method silently attached to a same-named method on an unrelated class — or didn't resolve at all. CodeGraph now captures Swift return types and infers the chained receiver's type from what the inner call returns (or the constructed type), creating the edge only when that class genuinely has the method (so a wrong inference produces no edge instead of a misleading one). Existing Swift indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (Swift)
 - C# method calls made through a static factory or fluent chain now resolve to the correct class. A call like `Foo.Create().Bar()` or `JObject.Parse(s).Property(...)` used to lose the receiver's type, so the chained method didn't resolve and the call was invisible to callers/impact/trace. CodeGraph now captures C# return types and infers the chained receiver's type from what the inner call returns, creating the edge only when that class genuinely has the method (so a wrong inference produces no edge). Existing C# indexes should be re-indexed (`codegraph index -f`) to benefit. (#750) (C#)

+ 135 - 0
__tests__/resolution.test.ts

@@ -3131,4 +3131,139 @@ void run() {
       expect(callerNamesOf('Decoy::clearAll')).toEqual([]);
     });
   });
+
+  describe('Pascal/Delphi chained static-factory call resolution (#645/#608 mechanism)', () => {
+    function callerNamesOf(qualifiedName: string): string[] {
+      const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
+      if (!target) return [];
+      const names = cg
+        .getIncomingEdges(target.id)
+        .filter((e) => e.kind === 'calls')
+        .map((e) => cg.getNode(e.source)?.name)
+        .filter((n): n is string => !!n);
+      return [...new Set(names)].sort();
+    }
+    function isCalled(qn: string): boolean {
+      const t = cg.getNodesByKind('method').find((n) => n.qualifiedName === qn);
+      return !!t && cg.getIncomingEdges(t.id).some((e) => e.kind === 'calls');
+    }
+
+    it('resolves a chained factory call TFoo.GetInstance().DoIt() via the return type, never a same-named decoy', async () => {
+      fs.writeFileSync(
+        path.join(tempDir, 'main.pas'),
+        `unit Main;
+interface
+type
+  TBar = class
+    procedure DoIt;
+  end;
+  TDecoy = class
+    procedure DoIt;
+  end;
+  TFoo = class
+    class function GetInstance: TBar;
+  end;
+implementation
+procedure TBar.DoIt; begin end;
+procedure TDecoy.DoIt; begin end;
+class function TFoo.GetInstance: TBar; begin Result := nil; end;
+procedure Run;
+begin
+  TFoo.GetInstance().DoIt();
+end;
+end.
+`
+      );
+      cg = await CodeGraph.init(tempDir, { index: true });
+      expect(isCalled('TBar::DoIt')).toBe(true);
+      expect(isCalled('TDecoy::DoIt')).toBe(false);
+    });
+
+    it('resolves a constructor chain TFoo.Create().Configure() on the constructed class', async () => {
+      fs.writeFileSync(
+        path.join(tempDir, 'main.pas'),
+        `unit Main;
+interface
+type
+  TFoo = class
+    constructor Create;
+    procedure Configure;
+  end;
+  TDecoy = class
+    procedure Configure;
+  end;
+implementation
+constructor TFoo.Create; begin end;
+procedure TFoo.Configure; begin end;
+procedure TDecoy.Configure; begin end;
+procedure Run;
+begin
+  TFoo.Create().Configure();
+end;
+end.
+`
+      );
+      cg = await CodeGraph.init(tempDir, { index: true });
+      // A constructor returns its own class (no `: TBar` annotation), so Configure
+      // resolves on TFoo, not the same-named decoy.
+      expect(isCalled('TFoo::Configure')).toBe(true);
+      expect(isCalled('TDecoy::Configure')).toBe(false);
+    });
+
+    it('resolves a typecast chain TFoo(x).DoIt() on the cast type', async () => {
+      fs.writeFileSync(
+        path.join(tempDir, 'main.pas'),
+        `unit Main;
+interface
+type
+  TFoo = class
+    procedure DoIt;
+  end;
+  TDecoy = class
+    procedure DoIt;
+  end;
+implementation
+procedure TFoo.DoIt; begin end;
+procedure TDecoy.DoIt; begin end;
+procedure Run(obj: TObject);
+begin
+  TFoo(obj).DoIt();
+end;
+end.
+`
+      );
+      cg = await CodeGraph.init(tempDir, { index: true });
+      expect(isCalled('TFoo::DoIt')).toBe(true);
+      expect(isCalled('TDecoy::DoIt')).toBe(false);
+    });
+
+    it('creates NO edge when the factory return type lacks the method (silent miss)', async () => {
+      fs.writeFileSync(
+        path.join(tempDir, 'main.pas'),
+        `unit Main;
+interface
+type
+  TBar = class
+  end;
+  TOther = class
+    procedure OnlyOther;
+  end;
+  TFoo = class
+    class function GetInstance: TBar;
+  end;
+implementation
+procedure TOther.OnlyOther; begin end;
+class function TFoo.GetInstance: TBar; begin Result := nil; end;
+procedure Run;
+begin
+  TFoo.GetInstance().OnlyOther();
+end;
+end.
+`
+      );
+      cg = await CodeGraph.init(tempDir, { index: true });
+      // TBar has no OnlyOther — must not mis-attach to the same-named TOther::OnlyOther.
+      expect(isCalled('TOther::OnlyOther')).toBe(false);
+    });
+  });
 });

+ 1 - 1
src/extraction/extraction-version.ts

@@ -21,4 +21,4 @@
  * turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
  * in the product is load-bearing").
  */
-export const EXTRACTION_VERSION = 15;
+export const EXTRACTION_VERSION = 16;

+ 10 - 0
src/extraction/languages/pascal.ts

@@ -17,6 +17,16 @@ export const pascalExtractor: LanguageExtractor = {
   bodyField: 'body',
   paramsField: 'args',
   returnField: 'type',
+  // Pascal/Delphi `function GetInstance: TBar` — the return type is a `typeref`
+  // child. Capture its bare class name for the chained static-factory call
+  // mechanism (#750). A procedure (no return) has no typeref → undefined.
+  getReturnType: (node, source) => {
+    const typeref = node.namedChildren.find((c: SyntaxNode) => c.type === 'typeref');
+    if (!typeref) return undefined;
+    const id = typeref.namedChildren.find((c: SyntaxNode) => c.type === 'identifier') ?? typeref;
+    const name = getNodeText(id, source).trim();
+    return /^[A-Za-z_]\w*$/.test(name) ? name : undefined;
+  },
   getSignature: (node, source) => {
     const args = getChildByField(node, 'args');
     const returnType = node.namedChildren.find(

+ 35 - 6
src/extraction/tree-sitter.ts

@@ -4312,12 +4312,41 @@ export class TreeSitterExtractor {
 
     let calleeName = '';
     if (firstChild.type === 'exprDot') {
-      // Qualified call: Obj.Method(...)
-      const identifiers = firstChild.namedChildren.filter(
-        (c: SyntaxNode) => c.type === 'identifier'
-      );
-      if (identifiers.length > 0) {
-        calleeName = identifiers.map((id: SyntaxNode) => getNodeText(id, this.source)).join('.');
+      // Chained static-factory call: `TFoo.GetInstance().DoIt()` — the exprDot's
+      // receiver is itself an `exprCall`, so the bare identifier list would
+      // collapse to just `DoIt` and mis-resolve to a same-named method on an
+      // unrelated class. Encode `TFoo.GetInstance().DoIt` so resolution infers
+      // DoIt's class from what `TFoo.GetInstance` RETURNS (#645/#608). Only a
+      // capitalized class-factory chain; a unary outer method.
+      const innerCall = firstChild.namedChildren.find((c: SyntaxNode) => c.type === 'exprCall');
+      const outerId = firstChild.namedChildren.filter((c: SyntaxNode) => c.type === 'identifier').pop();
+      const method = outerId ? getNodeText(outerId, this.source) : '';
+      if (innerCall && method && /^\w+$/.test(method)) {
+        const innerFirst = innerCall.namedChild(0);
+        let innerCallee = '';
+        if (innerFirst?.type === 'exprDot') {
+          innerCallee = innerFirst.namedChildren
+            .filter((c: SyntaxNode) => c.type === 'identifier')
+            .map((id: SyntaxNode) => getNodeText(id, this.source))
+            .join('.');
+        } else if (innerFirst?.type === 'identifier') {
+          innerCallee = getNodeText(innerFirst, this.source);
+        }
+        // Gate on the Delphi type-naming convention — `TFoo` classes / `IFoo`
+        // interfaces — so a class-factory chain re-encodes but a capitalized
+        // VARIABLE/parameter chain (Pascal capitalizes locals too: `Curve.X().Y()`,
+        // `Self.X().Y()`) stays bare and keeps its existing bare-name resolution.
+        calleeName = innerCallee && /^[TI][A-Z]/.test(innerCallee)
+          ? `${innerCallee}().${method}`
+          : method;
+      } else {
+        // Qualified call: Obj.Method(...)
+        const identifiers = firstChild.namedChildren.filter(
+          (c: SyntaxNode) => c.type === 'identifier'
+        );
+        if (identifiers.length > 0) {
+          calleeName = identifiers.map((id: SyntaxNode) => getNodeText(id, this.source)).join('.');
+        }
       }
     } else if (firstChild.type === 'identifier') {
       calleeName = getNodeText(firstChild, this.source);

+ 1 - 1
src/resolution/index.ts

@@ -37,7 +37,7 @@ const SUPERTYPE_BEARING_KINDS = new Set<Node['kind']>([
  * second pass. Dotted-receiver languages resolve via matchDottedCallChain; the
  * `::`-receiver ones (Rust) via matchScopedCallChain.
  */
-const CHAIN_LANGUAGES = new Set(['java', 'kotlin', 'csharp', 'swift', 'rust', 'go', 'scala', 'dart', 'objc']);
+const CHAIN_LANGUAGES = new Set(['java', 'kotlin', 'csharp', 'swift', 'rust', 'go', 'scala', 'dart', 'objc', 'pascal']);
 const SCOPED_CHAIN_LANGUAGES = new Set(['rust']);
 
 /** The extractor's chained-receiver encoding: `<inner>().<method>`. */

+ 18 - 3
src/resolution/name-matcher.ts

@@ -603,9 +603,11 @@ export function matchScopedCallChain(
  * so a bare `Foo()` there is a method call, not construction — excluded. Scala's
  * `Foo(args)` is a case-class / companion `apply`, which conventionally returns
  * `Foo` — and resolveMethodOnType validates, so a non-conventional `apply` that
- * returns another type simply yields no edge rather than a wrong one.
+ * returns another type simply yields no edge rather than a wrong one. Pascal/Delphi:
+ * a `TFoo(x)` is a TYPECAST whose result is a `TFoo`, so `TFoo(x).method()` resolves
+ * the method on `TFoo` — same shape, same validation.
  */
-const CONSTRUCTS_VIA_BARE_CALL = new Set(['kotlin', 'swift', 'scala', 'dart']);
+const CONSTRUCTS_VIA_BARE_CALL = new Set(['kotlin', 'swift', 'scala', 'dart', 'pascal']);
 
 /**
  * Resolve a dotted chained call whose receiver is a static factory / fluent call —
@@ -688,6 +690,18 @@ export function matchDottedCallChain(
     if (ref.language === 'objc' && /^[A-Z]/.test(factoryClass)) {
       return resolveMethodOnType(factoryClass, method, ref, context, 0.8, 'instance-method', importedFqnOf(factoryClass, ref, context));
     }
+    // Pascal/Delphi: the extractor only re-encodes a `TFoo`/`IFoo`-prefixed chain
+    // (the type-naming convention), so `factoryClass` is always a real class here.
+    // A factory whose return type wasn't captured is a CONSTRUCTOR
+    // (`TFileMem.Create().SetCachePerformance` — `constructor Create` has no `:
+    // TBar` annotation but returns its own class) or an unannotated function. In
+    // both cases the receiver's type is the class itself, so resolve the method on
+    // `factoryClass`. resolveMethodOnType validates against it (and its
+    // supertypes), so a wrong inference yields no edge — and this never fires when
+    // a return type WAS captured but lacks the method (absent-method safety above).
+    if (ref.language === 'pascal' && /^[TI]/.test(factoryClass)) {
+      return resolveMethodOnType(factoryClass, method, ref, context, 0.8, 'instance-method', importedFqnOf(factoryClass, ref, context));
+    }
     return null;
   }
   return resolveMethodOnType(ret, method, ref, context, 0.85, 'instance-method', importedFqnOf(ret, ref, context));
@@ -1153,7 +1167,8 @@ export function matchReference(
     ref.language === 'go' ||
     ref.language === 'scala' ||
     ref.language === 'dart' ||
-    ref.language === 'objc'
+    ref.language === 'objc' ||
+    ref.language === 'pascal'
   ) {
     result = matchDottedCallChain(ref, context);
     if (result) return result;