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>
This commit is contained in:
Colby Mchenry
2026-06-11 08:37:04 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent a4d19a5ed8
commit af56f3539d
7 changed files with 201 additions and 11 deletions
+135
View File
@@ -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);
});
});
});