Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions __tests__/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
36 changes: 36 additions & 0 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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);
}
Expand Down