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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;