From 8ebb7c5937d6f4a456971d9d433a074195de0828 Mon Sep 17 00:00:00 2001 From: glyco27 Date: Tue, 18 Aug 2026 21:58:00 +0100 Subject: [PATCH] fix(pascal): detect unqualified paren-less calls (`DoWork;`) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A statement that is nothing but an identifier is an unqualified call to a parameterless routine. Pascal allows dropping the parens on a no-arg call and Delphi codebases do so by convention, so `visitPascalBlock` was missing the majority of intra-unit calls: only `DoWork()` was recorded, never `DoWork;`. The qualified form (`Obj.Free;`) is already handled by `extractPascalParenlessCall`; this is the same idea one step less qualified. Gated on the identifier BEING the whole statement (`node.type === 'statement' && node.namedChildCount === 1`). Anywhere else — assignment RHS, condition, any expression — a bare identifier is indistinguishable from a variable read without type information, so those are left alone. Covered by a negative test. --- __tests__/resolution.test.ts | 59 +++++++++++++++++++++++++++++++++++ src/extraction/tree-sitter.ts | 36 +++++++++++++++++++++ 2 files changed, 95 insertions(+) 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); }