mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 11:24:49 +08:00
fix(pascal): the receiver calls of a chained Delphi call are indexed (#2787)
A Pascal call whose receiver is itself a call recorded only its outer
method: in `MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey)` the
receiver call `MD5FileSized` and the `TMeta.ExtendedPath` in its
arguments were dropped, as were paren'd calls deeper in a chain
(`A(1).B.C`) or in an index (`List[GetIndex(x)].Free`). The extractor
read a receiver call only for a `TFoo.GetInstance()` factory chain.
The extractor now walks a call's receiver chain and records each paren'd
call in it, with the calls in its arguments and indexes. A paren-less
link stays a read. A link inside a chain is recorded only when it can be
named precisely (`Obj.Method`, a bare routine, a `TFoo.Factory().X`
chain), never as the bare method name a chain like
`Client.New.BaseURL(…)` leaves, which matched namesakes. The calls main
already read keep their names.
Receivers are very often typecasts (`TButton(Sender).Caption`), which
read like calls. Delphi builds objects only through a constructor
(`TFoo.Create`), so a bare-name Pascal call that resolves to a type is a
cast: resolution now gives it a `references` edge to the type instead of
`instantiates` or `calls`. That also corrects the edges main already
made for value casts (`F := TFoo(Obj)`).
Validation:
- New __tests__/pascal-chained-calls.test.ts (7 tests, the issue's unit
included): all 7 fail with main's sources. Without only the resolver
rule the typecast test fails; without only the chain-link gate the
fluent-chain test fails.
- A/B against main 88ebefa1, both arms built from their own sources,
diffed by natural keys:
- Every removed edge is a cast changing kind at the same site, with
the same source and target (SynEdit 491, HeidiSQL 1,262, mORMot2
4,832, DWScript 3,115, DelphiMVCFramework 2,761), apart from 10
function references that moved to an earlier use in the same
routine.
- New edges: SynEdit 55, HeidiSQL 249, mORMot2 1,196, DWScript 690,
DelphiMVCFramework 2,727. Mostly casts read as references to their
type (14 of 14 sampled right) and receiver calls.
- 178 new call edges sampled: 140 right. instance-method 30/30,
qualified-name 10/10, bare functions 26/30, bare methods 70/92,
calls landing on a property, field or variable 0/12. The wrong ones
follow rules main applies to every call: the TypeInfo intrinsic
(main already links 1,709 such calls to a project method), calls
through local procedural variables, and calls now read inside a
receiver's arguments, named as main names them.
- Controls leveldb (C++, kernel) and SDWebImage (Objective-C, wasm) are
byte-identical in dump-graph.
- Cost within noise: index time medians of 3 interleaved rounds,
HeidiSQL 7.10 to 7.30 s, mORMot2 18.80 to 18.90 s, DWScript 20.00 to
19.40 s wall.
Fixes #2547
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7180fd0666
commit
0953084bc5
@@ -27,6 +27,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In VB.NET, every member of a `Structure` is now indexed, including its fields, properties, methods, constructors and nested enums. Before, only the first member was, so the rest could not be found and their callers looked empty.
|
||||
- In Delphi, a routine with an attribute on one of its parameters, like `const [ref] AValue: Integer` or DelphiMVCFramework's `const [MVCFromBody] Person: TPerson`, is now indexed with its calls, and so is every class member declared after it. Before, the routine was lost or its calls were credited to the file, the parameter showed up as a field of the class, and the members after it were missing. Thanks @stalexteam for the report. (#2545)
|
||||
- In Delphi, a `for … in` loop that declares its variable inline, like `for var lItem in AList do` or `for var lHandle: THandle in lHandles do`, now has its calls indexed, and a call on a typed loop variable reaches that type's method. Before, the whole loop was skipped, so a routine calling a method only from such a loop was missing from that method's callers. Thanks @stalexteam for the report. (#2546)
|
||||
- In Delphi, a call made on the result of another call, like `MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey)` or `List[IndexOf(Name)].Free`, now has the inner calls indexed too, so `MD5FileSized` and `TMeta.ExtendedPath` list that routine among their callers. A typecast like `TButton(Sender).Caption` or `F := TFoo(Obj)` now counts as a use of the type instead of creating one. Thanks @stalexteam for the report. (#2547)
|
||||
- In VB.NET and C#, what a property's `Get` and `Set` code calls, creates and reads now belongs to that property, as do C#'s `get => …` accessors and `=> …` property bodies. Before, it was dropped, so a method used only from a property looked unused.
|
||||
- In VB.NET, a field or property initializer like `= Compute()` or `As New List(Of Order)` now links what it calls and creates, and so do a `Custom Event`'s `AddHandler`, `RemoveHandler` and `RaiseEvent` blocks. Re-index VB.NET and C# projects after upgrading.
|
||||
- In C#, a field or property initializer like `private readonly ILogger _log = LogManager.GetLogger(typeof(X));` or `public List<Foo> Items { get; } = new();` now links what it calls, creates and reads, including inside a lambda, and a target-typed `new()` there counts as creating the declared type. Before, initializers were skipped, so a method or class used only from one — like a converter created in a static list — looked unused, and a method passed as a value there was credited to the whole class instead of the field or property.
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* The receiver of a chained Delphi call (#2547): in
|
||||
* `MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey)` only `IsSame` was
|
||||
* recorded. The receiver call `MD5FileSized` and the `TMeta.ExtendedPath` in
|
||||
* its arguments were dropped, and so were the paren'd calls deeper in a chain
|
||||
* (`A(1).B.C`) or in an index (`List[GetIndex(x)].Free`). The extractor now
|
||||
* reads every paren'd call in a receiver chain. A paren-less link (`.B`) may
|
||||
* read a field or property, so it is still not a call, and a typecast like
|
||||
* `TButton(Sender)` creates nothing, so it no longer reads as a construction.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { CodeGraph } from '../src';
|
||||
import { extractFromSource } from '../src/extraction';
|
||||
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
|
||||
|
||||
beforeAll(async () => {
|
||||
await initGrammars();
|
||||
await loadGrammarsForLanguages(['pascal']);
|
||||
});
|
||||
|
||||
// The issue's own unit.
|
||||
const CHAIN_PAS = `unit Chain;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TKey = record
|
||||
function IsSame(const AOther: TKey): Boolean;
|
||||
procedure Reset;
|
||||
end;
|
||||
|
||||
TMeta = class
|
||||
class function ExtendedPath(const APath: string): string; static;
|
||||
end;
|
||||
|
||||
function MD5FileSized(const APath: string): TKey;
|
||||
function TargetIsTheSame(const APath: string; const AKey: TKey): Boolean;
|
||||
procedure Stmt(const APath: string);
|
||||
|
||||
implementation
|
||||
|
||||
function TKey.IsSame(const AOther: TKey): Boolean;
|
||||
begin
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure TKey.Reset;
|
||||
begin
|
||||
end;
|
||||
|
||||
class function TMeta.ExtendedPath(const APath: string): string;
|
||||
begin
|
||||
Result := APath;
|
||||
end;
|
||||
|
||||
function MD5FileSized(const APath: string): TKey;
|
||||
begin
|
||||
end;
|
||||
|
||||
function TargetIsTheSame(const APath: string; const AKey: TKey): Boolean;
|
||||
begin
|
||||
Result := MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey);
|
||||
end;
|
||||
|
||||
procedure Stmt(const APath: string);
|
||||
begin
|
||||
MD5FileSized(TMeta.ExtendedPath(APath)).Reset;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
|
||||
function refsFrom(code: string, qualifiedName: string, kind = 'calls'): string[] {
|
||||
const result = extractFromSource('Unit1.pas', code);
|
||||
const from = result.nodes.find((n) => n.qualifiedName === qualifiedName);
|
||||
return result.unresolvedReferences
|
||||
.filter((r) => r.referenceKind === kind && r.fromNodeId === from?.id)
|
||||
.map((r) => `${r.referenceName}:${r.line}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
async function withIndex(code: string, check: (cg: CodeGraph) => void): Promise<void> {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-pascal-chain-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, 'Unit1.pas'), code);
|
||||
const cg = await CodeGraph.init(dir, { index: true });
|
||||
try {
|
||||
check(cg);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function edgesFrom(cg: CodeGraph, sourceName: string): string[] {
|
||||
const source = cg.getNodesByKind('function').concat(cg.getNodesByKind('method')).find((n) => n.name === sourceName);
|
||||
if (!source) return [];
|
||||
return cg
|
||||
.getOutgoingEdges(source.id)
|
||||
.filter((e) => e.kind !== 'contains')
|
||||
.map((e) => `${e.kind} ${cg.getNode(e.target)?.qualifiedName ?? '?'}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
describe('Delphi chained calls (#2547)', () => {
|
||||
it('records the receiver call and the calls in its arguments', () => {
|
||||
expect(refsFrom(CHAIN_PAS, 'TargetIsTheSame')).toEqual(['IsSame:41', 'MD5FileSized:41', 'TMeta.ExtendedPath:41']);
|
||||
expect(refsFrom(CHAIN_PAS, 'Stmt')).toEqual(['MD5FileSized:46', 'Reset:46', 'TMeta.ExtendedPath:46']);
|
||||
});
|
||||
|
||||
it('links each routine to all three callees the issue names', async () => {
|
||||
await withIndex(CHAIN_PAS, (cg) => {
|
||||
expect(edgesFrom(cg, 'TargetIsTheSame')).toEqual([
|
||||
'calls MD5FileSized',
|
||||
'calls TKey::IsSame',
|
||||
'calls TMeta::ExtendedPath',
|
||||
]);
|
||||
expect(edgesFrom(cg, 'Stmt')).toEqual(['calls MD5FileSized', 'calls TKey::Reset', 'calls TMeta::ExtendedPath']);
|
||||
});
|
||||
});
|
||||
|
||||
it('reads paren\'d calls anywhere in a receiver chain, in every context', () => {
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
implementation
|
||||
|
||||
procedure Run;
|
||||
begin
|
||||
A(1).B.C(2);
|
||||
X := A(3).B.C;
|
||||
if F(4).Ready then Exit;
|
||||
List[GetIndex(5)].Free;
|
||||
Y := Items[Find(6)].Value;
|
||||
GetHandler(7)(8);
|
||||
Z := Obj.Prop.Method(9);
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
// `B`, `C` (line 10), `Ready`, `Value` and `Prop` are paren-less links: reads, not calls.
|
||||
expect(refsFrom(code, 'Run')).toEqual([
|
||||
'A:10',
|
||||
'A:9',
|
||||
'C:9',
|
||||
'F:11',
|
||||
'Find:13',
|
||||
'Free:12',
|
||||
'GetHandler:14',
|
||||
'GetIndex:12',
|
||||
'Method:15',
|
||||
]);
|
||||
});
|
||||
|
||||
it('names a fluent chain\'s inner links only where they can be named precisely', () => {
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
implementation
|
||||
|
||||
procedure Run;
|
||||
begin
|
||||
Client.New.BaseURL('x').Accept('y').Get('/');
|
||||
RESTClient.HandleRedirects(False).Accept('z').Get('/');
|
||||
Builder.WithName(Name).Build;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
// The outer `Get` keeps the name a call has always had. `BaseURL` and
|
||||
// `Accept` would be bare names on a chain whose type is unknown, which
|
||||
// match namesakes, so they are left out; `RESTClient.HandleRedirects`
|
||||
// and `Builder.WithName` name their receiver.
|
||||
expect(refsFrom(code, 'Run')).toEqual(['Build:11', 'Builder.WithName:11', 'Get:10', 'Get:9', 'RESTClient.HandleRedirects:10']);
|
||||
});
|
||||
|
||||
it('gives a factory its caller and still resolves the chain on what it returns', async () => {
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TBar = class
|
||||
procedure DoIt;
|
||||
end;
|
||||
TDecoy = class
|
||||
procedure DoIt;
|
||||
end;
|
||||
TFoo = class
|
||||
class function GetInstance: TBar;
|
||||
end;
|
||||
|
||||
procedure Run;
|
||||
|
||||
implementation
|
||||
|
||||
procedure TBar.DoIt; begin end;
|
||||
procedure TDecoy.DoIt; begin end;
|
||||
class function TFoo.GetInstance: TBar; begin Result := nil; end;
|
||||
|
||||
procedure Run;
|
||||
begin
|
||||
TFoo.GetInstance().DoIt();
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex(code, (cg) => {
|
||||
expect(edgesFrom(cg, 'Run')).toEqual(['calls TBar::DoIt', 'calls TFoo::GetInstance']);
|
||||
});
|
||||
});
|
||||
|
||||
it('reads a typecast as a reference to its type, never a construction, wherever it is written', async () => {
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TFoo = class
|
||||
public
|
||||
procedure DoIt;
|
||||
function Ready: Boolean;
|
||||
end;
|
||||
|
||||
TDecoy = class
|
||||
public
|
||||
procedure DoIt;
|
||||
end;
|
||||
|
||||
NullableInt = record
|
||||
procedure SetNull;
|
||||
end;
|
||||
|
||||
TPair = record
|
||||
Lo, Hi: Integer;
|
||||
end;
|
||||
PPair = ^TPair;
|
||||
|
||||
function GetObj(N: Integer): TObject;
|
||||
procedure Run(Sender: TObject; var V; P: Pointer);
|
||||
|
||||
implementation
|
||||
|
||||
procedure TFoo.DoIt; begin end;
|
||||
function TFoo.Ready: Boolean; begin Result := True; end;
|
||||
procedure TDecoy.DoIt; begin end;
|
||||
procedure NullableInt.SetNull; begin end;
|
||||
function GetObj(N: Integer): TObject; begin Result := nil; end;
|
||||
|
||||
procedure Run(Sender: TObject; var V; P: Pointer);
|
||||
var
|
||||
F: TFoo;
|
||||
begin
|
||||
TFoo(Sender).DoIt;
|
||||
TFoo(GetObj(1)).DoIt();
|
||||
if TFoo(Sender).Ready then Exit;
|
||||
F := TFoo(Sender);
|
||||
NullableInt(V).SetNull;
|
||||
PPair(P)^.Lo := 1;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex(code, (cg) => {
|
||||
expect(edgesFrom(cg, 'Run')).toEqual([
|
||||
'calls GetObj',
|
||||
'calls NullableInt::SetNull',
|
||||
'calls TFoo::DoIt',
|
||||
'calls TFoo::DoIt',
|
||||
'references NullableInt',
|
||||
'references PPair',
|
||||
'references TFoo',
|
||||
'references TFoo',
|
||||
'references TFoo',
|
||||
'references TFoo',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('captures a routine passed by name to a receiver call, as for any call', async () => {
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TJob = class
|
||||
procedure Start;
|
||||
end;
|
||||
|
||||
function Schedule(Proc: TProc): TJob;
|
||||
procedure Worker;
|
||||
procedure Run;
|
||||
|
||||
implementation
|
||||
|
||||
procedure TJob.Start; begin end;
|
||||
function Schedule(Proc: TProc): TJob; begin Result := nil; end;
|
||||
procedure Worker; begin end;
|
||||
|
||||
procedure Run;
|
||||
begin
|
||||
Schedule(Worker).Start;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex(code, (cg) => {
|
||||
expect(edgesFrom(cg, 'Run')).toEqual(['calls Schedule', 'calls TJob::Start', 'references Worker']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8560,9 +8560,13 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract function calls from a Pascal expression
|
||||
* Extract function calls from a Pascal expression. A `receiverLink` call is
|
||||
* a link inside another call's receiver chain (#2547): it is recorded only
|
||||
* when it can be named precisely — `Obj.Method`, a bare routine, or a
|
||||
* `TFoo.Factory().Method` chain — never as the bare method name a longer
|
||||
* chain like `Client.New.BaseURL(…)` would leave, which matches namesakes.
|
||||
*/
|
||||
private extractPascalCall(node: SyntaxNode): void {
|
||||
private extractPascalCall(node: SyntaxNode, receiverLink = false): void {
|
||||
if (this.nodeStack.length === 0) return;
|
||||
const callerId = this.nodeStack[this.nodeStack.length - 1];
|
||||
if (!callerId) return;
|
||||
@@ -8613,7 +8617,8 @@ export class TreeSitterExtractor {
|
||||
calleeName = getNodeText(firstChild, this.source);
|
||||
}
|
||||
|
||||
if (calleeName) {
|
||||
const vagueLink = receiverLink && firstChild.type === 'exprDot' && !calleeName.includes('.');
|
||||
if (calleeName && !vagueLink) {
|
||||
this.unresolvedReferences.push({
|
||||
fromNodeId: callerId,
|
||||
referenceName: calleeName,
|
||||
@@ -8623,6 +8628,12 @@ export class TreeSitterExtractor {
|
||||
});
|
||||
}
|
||||
|
||||
// The calls in the callee itself: a chained call's receiver, like
|
||||
// `MD5FileSized(…)` in `MD5FileSized(…).IsSame(AKey)` (#2547), or the call
|
||||
// whose result is called in `GetHandler()(X)`.
|
||||
if (firstChild.type === 'exprDot') this.extractPascalReceiverCalls(firstChild.namedChild(0));
|
||||
else if (firstChild.type !== 'identifier') this.extractPascalReceiverCalls(firstChild);
|
||||
|
||||
// Also visit arguments for nested calls
|
||||
const args = node.namedChildren.find(
|
||||
(c: SyntaxNode) => c.type === 'exprArgs'
|
||||
@@ -8632,6 +8643,46 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The calls inside the receiver of a Pascal call or member access, which
|
||||
* the outer reference doesn't cover (#2547): `MD5FileSized(…)` and the
|
||||
* `TMeta.ExtendedPath(APath)` in its arguments for
|
||||
* `MD5FileSized(TMeta.ExtendedPath(APath)).IsSame(AKey)`, `A(1)` in
|
||||
* `A(1).B.C`, `GetIndex(x)` in `List[GetIndex(x)].Free`. A paren'd call
|
||||
* there is a call like any other. A paren-less link (`.B`) may read a field
|
||||
* or property, so it is not one. A typecast (`TButton(Sender)`) is written
|
||||
* like a call; resolution tells it apart by what the name resolves to.
|
||||
*/
|
||||
private extractPascalReceiverCalls(node: SyntaxNode | null): void {
|
||||
if (!node) return;
|
||||
switch (node.type) {
|
||||
case 'identifier':
|
||||
return;
|
||||
case 'exprCall':
|
||||
this.extractPascalReceiverCall(node, true);
|
||||
return;
|
||||
case 'exprDot':
|
||||
case 'exprTpl':
|
||||
this.extractPascalReceiverCalls(node.namedChild(0));
|
||||
return;
|
||||
case 'exprSubscript': {
|
||||
this.extractPascalReceiverCalls(node.namedChild(0));
|
||||
const index = node.namedChildren.find((c: SyntaxNode) => c.type === 'exprArgs');
|
||||
if (index) this.visitPascalBlock(index);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
this.visitPascalBlock(node);
|
||||
}
|
||||
}
|
||||
|
||||
/** A receiver call, read as any call is, its arguments' routine values included (#756). */
|
||||
private extractPascalReceiverCall(call: SyntaxNode, receiverLink: boolean): void {
|
||||
this.extractPascalCall(call, receiverLink);
|
||||
const args = call.namedChildren.find((c: SyntaxNode) => c.type === 'exprArgs');
|
||||
if (args) this.maybeCaptureFnRefs(args, 'exprArgs');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a PAREN-LESS Pascal method/procedure call (`Obj.Method;`,
|
||||
* `TFoo.GetInstance.DoIt;`). Pascal lets a no-arg method drop its parens, so it
|
||||
@@ -8671,10 +8722,11 @@ export class TreeSitterExtractor {
|
||||
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);
|
||||
if (receiver.type === 'exprCall') this.extractPascalReceiverCall(receiver, false);
|
||||
else this.extractPascalParenlessCall(receiver);
|
||||
} else {
|
||||
calleeName = method; // non-class receiver: a bare method ref (no field-access ref)
|
||||
this.extractPascalReceiverCalls(receiver); // `MD5FileSized(…).Reset;` (#2547)
|
||||
}
|
||||
} else {
|
||||
// Simple: `Obj.Method` → the dotted name (resolves via the receiver / bare name).
|
||||
@@ -8682,6 +8734,7 @@ export class TreeSitterExtractor {
|
||||
.filter((c: SyntaxNode) => c.type === 'identifier')
|
||||
.map((id: SyntaxNode) => getNodeText(id, this.source))
|
||||
.join('.');
|
||||
this.extractPascalReceiverCalls(receiver); // `List[GetIndex(x)].Free;`
|
||||
}
|
||||
|
||||
if (calleeName) {
|
||||
@@ -8716,16 +8769,15 @@ export class TreeSitterExtractor {
|
||||
// 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.
|
||||
// so there we only read the paren'd calls in its receiver chain
|
||||
// (`X := A(1).B.C` reads `A(1)`); a call right before the read is
|
||||
// named as any call is.
|
||||
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);
|
||||
}
|
||||
}
|
||||
const receiver = child.namedChild(0);
|
||||
if (receiver?.type === 'exprCall') this.extractPascalReceiverCall(receiver, false);
|
||||
else this.extractPascalReceiverCalls(receiver);
|
||||
}
|
||||
} else {
|
||||
this.visitPascalBlock(child);
|
||||
|
||||
@@ -153,6 +153,9 @@ const GO_BUILT_INS = new Set([
|
||||
'string', 'bool', 'byte', 'rune', 'any',
|
||||
]);
|
||||
|
||||
/** What a Pascal typecast's name resolves to: `TFoo(x)` casts, it doesn't call (see createEdges). */
|
||||
const PASCAL_CAST_TARGET_KINDS: ReadonlySet<string> = new Set(['class', 'struct', 'interface', 'enum', 'type_alias']);
|
||||
|
||||
const PASCAL_UNIT_PREFIXES = [
|
||||
'System.', 'Winapi.', 'Vcl.', 'Fmx.', 'Data.', 'Datasnap.',
|
||||
'Soap.', 'Xml.', 'Web.', 'REST.', 'FireDAC.', 'IBX.',
|
||||
@@ -1762,6 +1765,19 @@ export class ReferenceResolver {
|
||||
if (targetNode && GO_TYPE_KINDS.has(targetNode.kind)) kind = 'references';
|
||||
}
|
||||
|
||||
// A Pascal call written with a bare name that resolves to a type is a
|
||||
// typecast: `TButton(Sender).Caption`, `PInt64Rec(P)^.Lo`, `F :=
|
||||
// TFoo(Obj)`. Delphi builds an object only through a constructor
|
||||
// (`TFoo.Create`), so the code refers to the type and creates nothing.
|
||||
if (
|
||||
kind === 'calls' &&
|
||||
ref.original.language === 'pascal' &&
|
||||
!ref.original.referenceName.includes('.') &&
|
||||
PASCAL_CAST_TARGET_KINDS.has(this.nodeById(ref.targetNodeId)?.kind ?? '')
|
||||
) {
|
||||
kind = 'references';
|
||||
}
|
||||
|
||||
// Promote "calls" to "instantiates" when the resolved target is a
|
||||
// class/struct/union. Languages without a `new` keyword (Python, Ruby)
|
||||
// express instantiation as `Foo()` — extraction can't tell that
|
||||
|
||||
Reference in New Issue
Block a user