diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index 637b4a9d0..04966f2bf 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -4818,6 +4818,65 @@ end. expect(isCalled('TFoo::Reset')).toBe(true); }); + it('extracts UNQUALIFIED paren-less calls (`Reset;`, the dominant Delphi idiom)', async () => { + fs.writeFileSync( + path.join(tempDir, 'main.pas'), + `unit Main; +interface +type + TFoo = class + procedure DoThing; + procedure Reset; + procedure Refresh; + end; +implementation +procedure TFoo.Reset; begin end; +procedure TFoo.Refresh; begin end; +procedure TFoo.DoThing; +var + Total: Integer; +begin + Reset; + Refresh; + Total := 1; +end; +end. +` + ); + cg = await CodeGraph.init(tempDir, { index: true }); + expect(isCalled('TFoo::Reset')).toBe(true); + expect(isCalled('TFoo::Refresh')).toBe(true); + }); + + it('does not turn a bare identifier that is NOT a whole statement into a call', async () => { + fs.writeFileSync( + path.join(tempDir, 'main.pas'), + `unit Main; +interface +type + TFoo = class + procedure DoThing; + procedure Total; + end; +implementation +procedure TFoo.Total; begin end; +procedure TFoo.DoThing; +var + Total, Other: Integer; +begin + Other := Total; + if Total > 0 then Other := 0; +end; +end. +` + ); + cg = await CodeGraph.init(tempDir, { index: true }); + // `Total` here is a variable read on an assignment RHS and inside a + // condition, never a statement of its own: without type info the two are + // indistinguishable, so only the statement form counts as a call. + expect(isCalled('TFoo::Total')).toBe(false); + }); + it('resolves a PAREN-LESS chained factory call TFoo.GetInstance.DoIt via the return type', async () => { fs.writeFileSync( path.join(tempDir, 'main.pas'), diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 8d71d7f18..a4f086883 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -6581,6 +6581,30 @@ export class TreeSitterExtractor { * exprDot in assignment LHS/RHS or a condition is left alone — there it really * can be a field/property read.) */ + /** + * A statement that is nothing but an identifier: `DoWork;` — an unqualified + * call to a parameterless routine. The parenthesised form `DoWork();` is + * legal Pascal but the paren-less one is the convention, so without this the + * majority of intra-unit calls in a Delphi codebase are invisible to + * callers/callees/impact. + */ + private extractPascalUnqualifiedParenlessCall(node: SyntaxNode): void { + if (this.nodeStack.length === 0) return; + const callerId = this.nodeStack[this.nodeStack.length - 1]; + if (!callerId) return; + + const calleeName = getNodeText(node, this.source).trim(); + if (!calleeName) return; + + this.unresolvedReferences.push({ + fromNodeId: callerId, + referenceName: calleeName, + referenceKind: 'calls', + line: node.startPosition.row + 1, + column: node.startPosition.column, + }); + } + private extractPascalParenlessCall(node: SyntaxNode): void { if (this.nodeStack.length === 0) return; const callerId = this.nodeStack[this.nodeStack.length - 1]; @@ -6666,6 +6690,18 @@ export class TreeSitterExtractor { } } } + } else if ( + child.type === 'identifier' && + node.type === 'statement' && + node.namedChildCount === 1 + ) { + // An UNQUALIFIED paren-less call (`Initialize;`, `inherited Create;`'s + // sibling idiom): the statement is nothing but an identifier. Same + // convention as the exprDot case above, one step less qualified — and + // the dominant one in Delphi, where a no-arg routine is normally called + // without parens. Gated on the identifier BEING the whole statement: + // anywhere else a bare identifier is a variable read, not a call. + this.extractPascalUnqualifiedParenlessCall(child); } else { this.visitPascalBlock(child); }