feat(pascal): extract paren-less method calls (Obj.Free; / TFoo.GetInstance.DoIt;) (#793)
Pascal/Delphi lets a no-arg method or procedure drop its parens, so the call parses as a bare `exprDot` (not an `exprCall`) and was never recorded as a call — callers/impact/trace missed all of them (e.g. `Obj.Free`, `List.Clear`, the paren-less factory chain `TFoo.GetInstance.DoIt`). extractPascalParenlessCall handles these, wired into visitPascalBlock scoped to STATEMENT position only: a bare `Obj.Field;` statement is a no-op, so a statement-level dot expression is a call — but a dot in assignment LHS/RHS or a condition is left alone, since there it's genuinely ambiguous with a field/property access. The chained paren-less form reuses the #750 chain encoding (gated on the Delphi `TFoo`/`IFoo` type convention) and resolves the same way. PascalCoin A/B: +1131 / -1 — purely additive, and all 1131 new edges resolve to METHOD nodes (zero field/property false positives, confirming the statement-level gate). 3 new synthetic tests (paren-less call, paren-less chained factory, and the property-write/read non-extraction guard). EXTRACTION_VERSION 16->17. Full suite green. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4c35b72136
commit
35dce04e1f
@@ -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 = 16;
|
||||
export const EXTRACTION_VERSION = 17;
|
||||
|
||||
@@ -4371,6 +4371,69 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a PAREN-LESS Pascal method/procedure call (`Obj.Method;`,
|
||||
* `TFoo.GetInstance.DoIt;`). Pascal lets a no-arg method drop its parens, so it
|
||||
* parses as a bare `exprDot` (not an `exprCall`). A bare `exprDot` is
|
||||
* syntactically identical to a field/property access, so this is only ever
|
||||
* called for a STATEMENT-level exprDot (caller-gated): a bare `Obj.Field;`
|
||||
* statement is a no-op, so a statement-level dot expression is a call. (An
|
||||
* exprDot in assignment LHS/RHS or a condition is left alone — there it really
|
||||
* can be a field/property read.)
|
||||
*/
|
||||
private extractPascalParenlessCall(node: SyntaxNode): void {
|
||||
if (this.nodeStack.length === 0) return;
|
||||
const callerId = this.nodeStack[this.nodeStack.length - 1];
|
||||
if (!callerId) return;
|
||||
|
||||
const receiver = node.namedChild(0);
|
||||
const outerId = node.namedChildren.filter((c: SyntaxNode) => c.type === 'identifier').pop();
|
||||
const method = outerId ? getNodeText(outerId, this.source) : '';
|
||||
if (!method) return;
|
||||
|
||||
let calleeName = '';
|
||||
// Chained: the receiver is itself a call — a paren-less `TFoo.GetInstance` (an
|
||||
// inner exprDot) or a paren'd `TFoo.GetInstance()` (an exprCall). Encode the
|
||||
// chain `TFoo.GetInstance().DoIt` so resolution infers DoIt's class from what
|
||||
// the factory RETURNS (#645/#608), gated on the Delphi `TFoo`/`IFoo` type
|
||||
// convention; a capitalized VARIABLE chain stays a bare method name.
|
||||
if ((receiver?.type === 'exprDot' || receiver?.type === 'exprCall') && /^\w+$/.test(method)) {
|
||||
const innerCalleeNode = receiver.type === 'exprCall' ? receiver.namedChild(0) : receiver;
|
||||
const innerCallee = !innerCalleeNode
|
||||
? ''
|
||||
: innerCalleeNode.type === 'identifier'
|
||||
? getNodeText(innerCalleeNode, this.source)
|
||||
: innerCalleeNode.namedChildren
|
||||
.filter((c: SyntaxNode) => c.type === 'identifier')
|
||||
.map((id: SyntaxNode) => getNodeText(id, this.source))
|
||||
.join('.');
|
||||
if (innerCallee && /^[TI][A-Z]/.test(innerCallee)) {
|
||||
calleeName = `${innerCallee}().${method}`;
|
||||
// The T/I-prefixed inner is itself a real call — record it too.
|
||||
if (receiver.type === 'exprCall') this.extractPascalCall(receiver);
|
||||
else this.extractPascalParenlessCall(receiver);
|
||||
} else {
|
||||
calleeName = method; // non-class receiver: a bare method ref (no field-access ref)
|
||||
}
|
||||
} else {
|
||||
// Simple: `Obj.Method` → the dotted name (resolves via the receiver / bare name).
|
||||
calleeName = node.namedChildren
|
||||
.filter((c: SyntaxNode) => c.type === 'identifier')
|
||||
.map((id: SyntaxNode) => getNodeText(id, this.source))
|
||||
.join('.');
|
||||
}
|
||||
|
||||
if (calleeName) {
|
||||
this.unresolvedReferences.push({
|
||||
fromNodeId: callerId,
|
||||
referenceName: calleeName,
|
||||
referenceKind: 'calls',
|
||||
line: node.startPosition.row + 1,
|
||||
column: node.startPosition.column,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively visit a Pascal block/statement tree for call expressions
|
||||
*/
|
||||
@@ -4381,11 +4444,18 @@ export class TreeSitterExtractor {
|
||||
if (child.type === 'exprCall') {
|
||||
this.extractPascalCall(child);
|
||||
} else if (child.type === 'exprDot') {
|
||||
// Check if exprDot contains an exprCall
|
||||
for (let j = 0; j < child.namedChildCount; j++) {
|
||||
const grandchild = child.namedChild(j);
|
||||
if (grandchild?.type === 'exprCall') {
|
||||
this.extractPascalCall(grandchild);
|
||||
// A STATEMENT-level bare exprDot is a paren-less call (`Obj.Free;`,
|
||||
// `TFoo.GetInstance.DoIt;`). Anywhere else (assignment side, condition,
|
||||
// expression) a bare exprDot is ambiguous with a field/property access,
|
||||
// so there we only descend for paren'd inner calls.
|
||||
if (node.type === 'statement') {
|
||||
this.extractPascalParenlessCall(child);
|
||||
} else {
|
||||
for (let j = 0; j < child.namedChildCount; j++) {
|
||||
const grandchild = child.namedChild(j);
|
||||
if (grandchild?.type === 'exprCall') {
|
||||
this.extractPascalCall(grandchild);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user