mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-31 08:03:58 +08:00
fix(pascal): Delphi calls written without parentheses link to the routine (#2797)
Delphi calls a routine that takes no arguments without parentheses
(`Before;`, `X := FProvider.Value + 1`, `if Ready then`), and only a
statement-level `Obj.Method;` was recorded, so most call sites were
missing from callers, callees and impact. Bare names in arguments and
assignments were captured as function values and matched by name alone,
which linked locals and parameters to other classes' methods.
The extractor now records each name written without parentheses with
how it is used (a statement, a value, an event's method, a receiver),
the routine's class, and the declared type of a receiver the routine
declares. It leaves out what the routine declares, data the class and
the unit declare, names the class's code assigns or passes to Inc/Dec,
names Delphi's conventions give to data, and anything in a `with` body
but an event's method. A new resolver links such a name only to a method
or function Delphi's scope rules reach: the class and its ancestors, the
unit, then the units it uses, last first, or a method of a receiver's
declared type. Data of that name links nothing. A function runs (calls);
a procedure read as a value, an event's method, and a routine that needs
arguments written without them are method pointers (references). Where
the tree has errors, the class and declarations come from the source
text, and a routine's own header is no call. `class sealed (TBase)` and
`class abstract (TBase)` heads now name their ancestors. Pascal function
values capture only `@Handler`.
A/B against main 8073637d (SynEdit, HeidiSQL, mORMot2, DWScript,
DelphiMVCFramework): added edges 3011/4626/6501/5092/6776; main's
name-matched function values now linked by scope 2387/2727/948/726/712,
114/281/554/342/586 of them as calls; removed 85/139/802/333/707,
triaged as main's false positives (locals and parameters linked by
name, other classes' methods, routine headers misparsed in broken
trees) and own-class retargets, less deliberate losses (reads in `with`
bodies, lowercase or type-like names read as values). Sampled precision
of added edges: 196/196. leveldb and SDWebImage byte-identical. CPU,
median of 3 interleaved: +15.2%, +16.2%, +8.7%, +12.9%, and +0.8% for
DelphiMVCFramework (one run each).
Fixes #2514
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
63ca5681db
commit
407efb1c65
@@ -28,6 +28,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- 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 Delphi, a routine called without parentheses, the usual way to call one that takes no arguments, like `Before;`, `if Ready then`, `Result := FProvider.Value + 1` or `QueryTabs.ActiveTab.MemoFilename`, now counts as a call, so it shows up in callers, callees and impact, and a method given to an event, like `OnClick := ButtonClick`, is linked as a reference. A name is linked only to a method or function its code reaches, in its class and the classes it inherits from (`class sealed (TBase)` included), its unit, or the units it uses, never to a field, property or variable of that name. Thanks @OleksiyPenkov for the report, and @drakeo338 and @OleksiyPenkov for their pull requests. (#2514)
|
||||
- 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,749 @@
|
||||
/**
|
||||
* Delphi calls a parameterless routine without parentheses (#2514):
|
||||
* `Before;`, `Result := FProvider.Value + 1`, `if Ready then`, and a method
|
||||
* given to an event, `OnGet := FProvider.Value`. None of these was linked, so
|
||||
* callers, callees and impact missed most call sites in real Delphi code.
|
||||
* The same spelling reads a field, a property, a variable or a constant, so a
|
||||
* name is linked only when Delphi's scope rules reach a method or function of
|
||||
* that name: the routine's own declarations, its class and ancestors, the
|
||||
* unit, then the units it uses. Data of that name links nothing.
|
||||
*/
|
||||
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 { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
|
||||
|
||||
beforeAll(async () => {
|
||||
await initGrammars();
|
||||
await loadGrammarsForLanguages(['pascal']);
|
||||
});
|
||||
|
||||
async function withIndex(files: Record<string, string>, check: (cg: CodeGraph) => void): Promise<void> {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-pascal-parenless-'));
|
||||
try {
|
||||
for (const [name, code] of Object.entries(files)) fs.writeFileSync(path.join(dir, name), code);
|
||||
const cg = await CodeGraph.init(dir, { index: true });
|
||||
try {
|
||||
check(cg);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The calls and references a routine makes, as `kind Target::Name`. */
|
||||
function edgesFrom(cg: CodeGraph, qualifiedName: string): string[] {
|
||||
const source = cg.getNodesByKind('function').concat(cg.getNodesByKind('method')).find((n) => n.qualifiedName === qualifiedName);
|
||||
expect(source, qualifiedName).toBeDefined();
|
||||
return cg
|
||||
.getOutgoingEdges(source!.id)
|
||||
.filter((e) => e.kind === 'calls' || e.kind === 'references')
|
||||
.map((e) => `${e.kind} ${cg.getNode(e.target)?.qualifiedName ?? '?'}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
// The issue's own unit.
|
||||
const UNIT_A = `unit UnitA;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TGetIntEvent = function: Integer of object;
|
||||
|
||||
TProvider = class
|
||||
public
|
||||
function Value: Integer;
|
||||
function ValueParens(): Integer;
|
||||
end;
|
||||
|
||||
TConsumer = class
|
||||
private
|
||||
FProvider: TProvider;
|
||||
FOnGet: TGetIntEvent;
|
||||
public
|
||||
function UseNoParens: Integer;
|
||||
function UseParens: Integer;
|
||||
function UseEvent: Integer;
|
||||
procedure Wire;
|
||||
property OnGet: TGetIntEvent read FOnGet write FOnGet;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
function TProvider.Value: Integer;
|
||||
begin
|
||||
Result := 1;
|
||||
end;
|
||||
|
||||
function TProvider.ValueParens(): Integer;
|
||||
begin
|
||||
Result := 2;
|
||||
end;
|
||||
|
||||
function TConsumer.UseNoParens: Integer;
|
||||
begin
|
||||
Result := FProvider.Value + 1;
|
||||
end;
|
||||
|
||||
function TConsumer.UseParens: Integer;
|
||||
begin
|
||||
Result := FProvider.ValueParens() + 1;
|
||||
end;
|
||||
|
||||
procedure TConsumer.Wire;
|
||||
begin
|
||||
OnGet := FProvider.Value;
|
||||
end;
|
||||
|
||||
function TConsumer.UseEvent: Integer;
|
||||
begin
|
||||
Result := FOnGet;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
|
||||
describe('Delphi calls without parentheses (#2514)', () => {
|
||||
it('links the issue\'s paren-less call as a call and the event\'s method as a reference', async () => {
|
||||
await withIndex({ 'UnitA.pas': UNIT_A }, (cg) => {
|
||||
expect(edgesFrom(cg, 'TConsumer::UseNoParens')).toEqual(['calls TProvider::Value']);
|
||||
expect(edgesFrom(cg, 'TConsumer::UseParens')).toEqual(['calls TProvider::ValueParens']);
|
||||
expect(edgesFrom(cg, 'TConsumer::Wire')).toEqual(['references TProvider::Value']);
|
||||
// `FOnGet` is a field: reading it calls whatever was assigned, which the code doesn't say.
|
||||
expect(edgesFrom(cg, 'TConsumer::UseEvent')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('links a bare name to the class\'s method or the unit\'s routine, as a statement or for its value', async () => {
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TMin = class
|
||||
public
|
||||
procedure Before;
|
||||
procedure After;
|
||||
function Value: Integer;
|
||||
procedure UseValue;
|
||||
end;
|
||||
|
||||
procedure FreeProc;
|
||||
|
||||
implementation
|
||||
|
||||
procedure FreeProc;
|
||||
begin
|
||||
end;
|
||||
|
||||
procedure TMin.Before;
|
||||
begin
|
||||
end;
|
||||
|
||||
procedure TMin.After;
|
||||
begin
|
||||
Before;
|
||||
freeproc;
|
||||
end;
|
||||
|
||||
function TMin.Value: Integer;
|
||||
begin
|
||||
Result := 1;
|
||||
end;
|
||||
|
||||
procedure TMin.UseValue;
|
||||
var
|
||||
X: Integer;
|
||||
begin
|
||||
X := Value;
|
||||
if Value > 0 then
|
||||
Before;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex({ 'Unit1.pas': code }, (cg) => {
|
||||
// Pascal names are case-insensitive: `freeproc` is `FreeProc`.
|
||||
expect(edgesFrom(cg, 'TMin::After')).toEqual(['calls FreeProc', 'calls TMin::Before']);
|
||||
expect(edgesFrom(cg, 'TMin::UseValue')).toEqual(['calls TMin::Before', 'calls TMin::Value', 'calls TMin::Value']);
|
||||
});
|
||||
});
|
||||
|
||||
it('never links a name that reads a field, property, variable, constant or parameter', async () => {
|
||||
const base = `unit Base;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TBase = class
|
||||
protected
|
||||
FReady: Boolean;
|
||||
public
|
||||
property Ready: Boolean read FReady;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
end.
|
||||
`;
|
||||
const decoys = `unit Decoys;
|
||||
|
||||
interface
|
||||
|
||||
function Ready: Boolean;
|
||||
function Count: Integer;
|
||||
function Items: Integer;
|
||||
function Limit: Integer;
|
||||
function Total: Integer;
|
||||
function FItems: Integer;
|
||||
|
||||
implementation
|
||||
|
||||
function Ready: Boolean; begin Result := True; end;
|
||||
function Count: Integer; begin Result := 0; end;
|
||||
function Items: Integer; begin Result := 0; end;
|
||||
function Limit: Integer; begin Result := 0; end;
|
||||
function Total: Integer; begin Result := 0; end;
|
||||
function FItems: Integer; begin Result := 0; end;
|
||||
|
||||
end.
|
||||
`;
|
||||
const main = `unit Main;
|
||||
|
||||
interface
|
||||
|
||||
uses Decoys, Base;
|
||||
|
||||
type
|
||||
TList = class(TBase)
|
||||
private
|
||||
FItems: Integer;
|
||||
function GetCount: Integer;
|
||||
public
|
||||
property Count: Integer read GetCount;
|
||||
function Run(Total: Integer): Integer;
|
||||
end;
|
||||
|
||||
const
|
||||
Limit = 10;
|
||||
|
||||
var
|
||||
Items: Integer;
|
||||
|
||||
implementation
|
||||
|
||||
function TList.GetCount: Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
end;
|
||||
|
||||
function TList.Run(Total: Integer): Integer;
|
||||
begin
|
||||
Result := Count + Items + Limit + Total + FItems;
|
||||
if Ready then
|
||||
Result := 0;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex({ 'Base.pas': base, 'Decoys.pas': decoys, 'Main.pas': main }, (cg) => {
|
||||
// `Ready` is the inherited property, which the unit's own
|
||||
// `uses Decoys` doesn't reach past.
|
||||
expect(edgesFrom(cg, 'TList::Run')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('follows the class\'s ancestors and the units it uses, the last one first', async () => {
|
||||
const lib = `unit Lib;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TBase = class
|
||||
public
|
||||
procedure Setup;
|
||||
end;
|
||||
|
||||
procedure Helper;
|
||||
|
||||
implementation
|
||||
|
||||
procedure TBase.Setup; begin end;
|
||||
procedure Helper; begin end;
|
||||
|
||||
end.
|
||||
`;
|
||||
const other = `unit Other;
|
||||
|
||||
interface
|
||||
|
||||
procedure Helper;
|
||||
|
||||
implementation
|
||||
|
||||
procedure Helper; begin end;
|
||||
|
||||
end.
|
||||
`;
|
||||
const unused = `unit Unused;
|
||||
|
||||
interface
|
||||
|
||||
procedure Teardown;
|
||||
|
||||
implementation
|
||||
|
||||
procedure Teardown; begin end;
|
||||
|
||||
end.
|
||||
`;
|
||||
const main = `unit Main;
|
||||
|
||||
interface
|
||||
|
||||
uses Other, Lib;
|
||||
|
||||
type
|
||||
TChild = class(TBase)
|
||||
public
|
||||
procedure Run;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
procedure TChild.Run;
|
||||
begin
|
||||
Setup;
|
||||
Helper;
|
||||
Teardown;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex({ 'Lib.pas': lib, 'Other.pas': other, 'Unused.pas': unused, 'Main.pas': main }, (cg) => {
|
||||
const helpers = cg.getNodesByKind('function').filter((n) => n.name === 'Helper');
|
||||
const libHelper = helpers.find((n) => n.filePath.endsWith('Lib.pas'));
|
||||
const run = cg.getNodesByKind('method').find((n) => n.qualifiedName === 'TChild::Run')!;
|
||||
const targets = cg.getOutgoingEdges(run.id).filter((e) => e.kind === 'calls').map((e) => e.target);
|
||||
// `Lib` is used last, so its `Helper` hides `Other`'s; `Unused` isn't used at all.
|
||||
expect(targets).toContain(libHelper!.id);
|
||||
expect(edgesFrom(cg, 'TChild::Run')).toEqual(['calls Helper', 'calls TBase::Setup']);
|
||||
});
|
||||
});
|
||||
|
||||
it('links a function used as a receiver, through fields, parameters and locals of a declared type', async () => {
|
||||
// The issue's real project: `FProjectPanel.ActiveModelSeries.Visible`.
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TSeries = class
|
||||
public
|
||||
procedure BeginUpdate;
|
||||
function Visible: Boolean;
|
||||
end;
|
||||
|
||||
TPanel = class
|
||||
public
|
||||
function ActiveSeries: TSeries;
|
||||
end;
|
||||
|
||||
TForm1 = class
|
||||
private
|
||||
FPanel: TPanel;
|
||||
function CurrentPanel: TPanel;
|
||||
public
|
||||
procedure Run;
|
||||
procedure RunWith(APanel: TPanel);
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
procedure TSeries.BeginUpdate; begin end;
|
||||
function TSeries.Visible: Boolean; begin Result := True; end;
|
||||
function TPanel.ActiveSeries: TSeries; begin Result := nil; end;
|
||||
function TForm1.CurrentPanel: TPanel; begin Result := FPanel; end;
|
||||
|
||||
procedure TForm1.Run;
|
||||
begin
|
||||
FPanel.ActiveSeries.BeginUpdate;
|
||||
if CurrentPanel.ActiveSeries.Visible then
|
||||
Exit;
|
||||
end;
|
||||
|
||||
procedure TForm1.RunWith(APanel: TPanel);
|
||||
var
|
||||
S: TSeries;
|
||||
begin
|
||||
S := APanel.ActiveSeries;
|
||||
if S.Visible then
|
||||
Exit;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex({ 'Unit1.pas': code }, (cg) => {
|
||||
expect(edgesFrom(cg, 'TForm1::Run')).toEqual([
|
||||
'calls TForm1::CurrentPanel',
|
||||
'calls TPanel::ActiveSeries',
|
||||
'calls TPanel::ActiveSeries',
|
||||
'calls TSeries::BeginUpdate',
|
||||
'calls TSeries::Visible',
|
||||
]);
|
||||
expect(edgesFrom(cg, 'TForm1::RunWith')).toEqual(['calls TPanel::ActiveSeries', 'calls TSeries::Visible']);
|
||||
});
|
||||
});
|
||||
|
||||
it('gives a method to an event or an argument as a reference, a method pointer', async () => {
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TNotify = procedure of object;
|
||||
|
||||
TButton = class
|
||||
public
|
||||
OnClick: TNotify;
|
||||
procedure Click;
|
||||
end;
|
||||
|
||||
TItem = class
|
||||
public
|
||||
constructor Create(AOwner: TObject);
|
||||
end;
|
||||
|
||||
TForm1 = class
|
||||
private
|
||||
FButton: TButton;
|
||||
procedure ButtonClick;
|
||||
procedure Register(Handler: TNotify);
|
||||
function Title: string;
|
||||
function Compare(A, B: Integer): Integer;
|
||||
procedure Sort(Fn: TCompare);
|
||||
public
|
||||
procedure Setup;
|
||||
procedure SetupWith;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
constructor TItem.Create(AOwner: TObject); begin end;
|
||||
procedure TButton.Click; begin end;
|
||||
procedure TForm1.ButtonClick; begin end;
|
||||
procedure TForm1.Register(Handler: TNotify); begin end;
|
||||
function TForm1.Title: string; begin Result := ''; end;
|
||||
function TForm1.Compare(A, B: Integer): Integer; begin Result := A - B; end;
|
||||
procedure TForm1.Sort(Fn: TCompare); begin end;
|
||||
|
||||
procedure TForm1.Setup;
|
||||
var
|
||||
Item: TObject;
|
||||
begin
|
||||
FButton.OnClick := ButtonClick;
|
||||
Register(ButtonClick);
|
||||
// A function that needs arguments, written without them, is its pointer.
|
||||
Sort(Compare);
|
||||
// A constructor written without the arguments it needs is an inherited
|
||||
// one outside the project, TObject.Create.
|
||||
Item := TItem.Create;
|
||||
end;
|
||||
|
||||
procedure TForm1.SetupWith;
|
||||
var
|
||||
S: string;
|
||||
begin
|
||||
with FButton do
|
||||
begin
|
||||
Click;
|
||||
OnClick := ButtonClick;
|
||||
S := Title;
|
||||
end;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex({ 'Unit1.pas': code }, (cg) => {
|
||||
expect(edgesFrom(cg, 'TForm1::Setup')).toEqual([
|
||||
'calls TForm1::Register',
|
||||
'calls TForm1::Sort',
|
||||
'references TForm1::ButtonClick',
|
||||
'references TForm1::ButtonClick',
|
||||
'references TForm1::Compare',
|
||||
]);
|
||||
// In a `with` body a name may be the `with` object's member: `Click`
|
||||
// is the button's, and `Title` could be. Only the event's method is
|
||||
// linked there.
|
||||
expect(edgesFrom(cg, 'TForm1::SetupWith')).toEqual(['references TForm1::ButtonClick']);
|
||||
});
|
||||
});
|
||||
|
||||
it('finds the class of a generic class\'s method, whose node spells its constraints', async () => {
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TBox<T: class> = class
|
||||
public
|
||||
procedure Clear;
|
||||
procedure Reset;
|
||||
end;
|
||||
|
||||
TOther = class
|
||||
public
|
||||
procedure Clear;
|
||||
procedure Reset;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
procedure TOther.Clear; begin end;
|
||||
procedure TOther.Reset; begin end;
|
||||
procedure TBox<T>.Clear; begin end;
|
||||
|
||||
procedure TBox<T>.Reset;
|
||||
begin
|
||||
Clear;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex({ 'Unit1.pas': code }, (cg) => {
|
||||
const reset = cg.getNodesByKind('method').filter((n) => n.name === 'Reset');
|
||||
const targets = reset.flatMap((r) => cg.getOutgoingEdges(r.id).filter((e) => e.kind === 'calls'))
|
||||
.map((e) => cg.getNode(e.target)?.qualifiedName);
|
||||
expect(targets).toEqual(['TBox<T: class>::Clear']);
|
||||
});
|
||||
});
|
||||
|
||||
it('links a form\'s event handler named after its component, and a C library\'s function', async () => {
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TNotify = procedure(Sender: TObject) of object;
|
||||
|
||||
TButton = class
|
||||
public
|
||||
OnClick: TNotify;
|
||||
end;
|
||||
|
||||
TForm1 = class
|
||||
private
|
||||
btnOK: TButton;
|
||||
public
|
||||
procedure btnOKClick(Sender: TObject);
|
||||
procedure Setup;
|
||||
function Check: Integer;
|
||||
end;
|
||||
|
||||
function ERR_get_error: Cardinal; cdecl;
|
||||
function mach_absolute_time: UInt64; cdecl;
|
||||
|
||||
implementation
|
||||
|
||||
function ERR_get_error: Cardinal; cdecl;
|
||||
begin
|
||||
Result := 0;
|
||||
end;
|
||||
|
||||
function mach_absolute_time: UInt64; cdecl;
|
||||
begin
|
||||
Result := 0;
|
||||
end;
|
||||
|
||||
procedure TForm1.btnOKClick(Sender: TObject);
|
||||
begin
|
||||
end;
|
||||
|
||||
procedure TForm1.Setup;
|
||||
begin
|
||||
btnOK.OnClick := btnOKClick;
|
||||
end;
|
||||
|
||||
function TForm1.Check: Integer;
|
||||
begin
|
||||
Result := ERR_get_error + mach_absolute_time;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex({ 'Unit1.pas': code }, (cg) => {
|
||||
expect(edgesFrom(cg, 'TForm1::Setup')).toEqual(['references TForm1::btnOKClick']);
|
||||
expect(edgesFrom(cg, 'TForm1::Check')).toEqual(['calls ERR_get_error', 'calls mach_absolute_time']);
|
||||
});
|
||||
});
|
||||
|
||||
it('reads an ancestor through a class the unit declares ahead (`TBase = class;`)', async () => {
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TChild = class;
|
||||
|
||||
TBase = class
|
||||
public
|
||||
function Ready: Boolean;
|
||||
end;
|
||||
|
||||
TChild = class(TBase)
|
||||
public
|
||||
procedure Run;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
function TBase.Ready: Boolean;
|
||||
begin
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure TChild.Run;
|
||||
begin
|
||||
if Ready then
|
||||
Exit;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex({ 'Unit1.pas': code }, (cg) => {
|
||||
expect(edgesFrom(cg, 'TChild::Run')).toEqual(['calls TBase::Ready']);
|
||||
});
|
||||
});
|
||||
|
||||
it('links the calls of a unit\'s initialization and finalization sections', async () => {
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
procedure Setup;
|
||||
procedure Teardown;
|
||||
|
||||
implementation
|
||||
|
||||
procedure Setup;
|
||||
begin
|
||||
end;
|
||||
|
||||
procedure Teardown;
|
||||
begin
|
||||
end;
|
||||
|
||||
initialization
|
||||
Setup;
|
||||
|
||||
finalization
|
||||
Teardown;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex({ 'Unit1.pas': code }, (cg) => {
|
||||
const file = cg.getNodesByKind('file').find((n) => n.filePath.endsWith('Unit1.pas'))!;
|
||||
const targets = cg.getOutgoingEdges(file.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)?.name).sort();
|
||||
expect(targets).toEqual(['Setup', 'Teardown']);
|
||||
});
|
||||
});
|
||||
|
||||
it('takes a name the class\'s code assigns or passes to Inc for a variable, as Delphi does', async () => {
|
||||
// `Run` is a field of a base class the project doesn't have (a VCL
|
||||
// highlighter's, say): the unit's `Run` function is not what it reads.
|
||||
const helpers = `unit Helpers;
|
||||
|
||||
interface
|
||||
|
||||
function Run: Integer;
|
||||
function Limit: Integer;
|
||||
|
||||
implementation
|
||||
|
||||
function Run: Integer; begin Result := 0; end;
|
||||
function Limit: Integer; begin Result := 0; end;
|
||||
|
||||
end.
|
||||
`;
|
||||
const main = `unit Main;
|
||||
|
||||
interface
|
||||
|
||||
uses Helpers;
|
||||
|
||||
type
|
||||
TMyHighlighter = class(TSynCustomHighlighter)
|
||||
public
|
||||
procedure Next;
|
||||
procedure Reset;
|
||||
function Count: Integer;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
procedure TMyHighlighter.Next;
|
||||
var
|
||||
C: Char;
|
||||
begin
|
||||
Inc(Run);
|
||||
C := FLine[Run];
|
||||
if Limit > 0 then
|
||||
Exit;
|
||||
end;
|
||||
|
||||
procedure TMyHighlighter.Reset;
|
||||
begin
|
||||
Limit := 0;
|
||||
end;
|
||||
|
||||
function TMyHighlighter.Count: Integer;
|
||||
begin
|
||||
Count := Run;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex({ 'Helpers.pas': helpers, 'Main.pas': main }, (cg) => {
|
||||
expect(edgesFrom(cg, 'TMyHighlighter::Next')).toEqual([]);
|
||||
expect(edgesFrom(cg, 'TMyHighlighter::Count')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it('leaves out names a routine declares itself, even where a method shares the name', async () => {
|
||||
const code = `unit Unit1;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TMin = class
|
||||
public
|
||||
function Value: Integer;
|
||||
function Items: Integer;
|
||||
procedure Run(Value: Integer);
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
function TMin.Value: Integer; begin Result := 1; end;
|
||||
function TMin.Items: Integer; begin Result := 1; end;
|
||||
|
||||
procedure TMin.Run(Value: Integer);
|
||||
var
|
||||
X: Integer;
|
||||
begin
|
||||
X := Value;
|
||||
for var Items in [1, 2] do
|
||||
X := X + Items;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex({ 'Unit1.pas': code }, (cg) => {
|
||||
expect(edgesFrom(cg, 'TMin::Run')).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4023,6 +4023,15 @@ export class QueryBuilder {
|
||||
return rows.map((r) => r.name);
|
||||
}
|
||||
|
||||
/** The names of a language's nodes of the given kinds (repeats included), decoding nothing else. */
|
||||
getNodeNamesOfKinds(language: Language, kinds: readonly NodeKind[]): string[] {
|
||||
// `+language`: the kind index narrows further than the language one.
|
||||
const rows = this.db
|
||||
.prepare(`SELECT name FROM nodes WHERE kind IN (${kinds.map(() => '?').join(', ')}) AND +language = ?`)
|
||||
.all(...kinds, language) as Array<{ name: string }>;
|
||||
return rows.map((r) => r.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* The import node of every C / C++ `#include`, narrowed to the path it
|
||||
* spells and where it is written: the include graph reads nothing else, and
|
||||
|
||||
@@ -355,9 +355,13 @@ const PASCAL_SPEC: FnRefSpec = {
|
||||
idTypes: new Set(['identifier']),
|
||||
dispatch: new Map<string, CaptureRule>([
|
||||
['exprArgs', { mode: 'args' }],
|
||||
['assignment', { mode: 'rhs', field: 'rhs' }], // OnClick := Handler
|
||||
['assignment', { mode: 'rhs', field: 'rhs' }], // OnClick := @Handler
|
||||
]),
|
||||
unwrap: new Map<string, string | null>([['exprUnary', 'operand']]), // @Handler
|
||||
// A bare name (`OnClick := Handler`, `Register(Handler)`) is a paren-less
|
||||
// name the extractor records itself, linked by scope (#2514); only the
|
||||
// explicit `@Handler` is a function value here.
|
||||
addressOfOnly: true,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+506
-13
@@ -649,6 +649,32 @@ function keepsBareTsJsReceiver(node: SyntaxNode, source: string): boolean {
|
||||
*/
|
||||
const REACT_HANDLER_HOOKS = /^(?:React\.)?use(?:Callback|EffectEvent|Event)$/;
|
||||
|
||||
/** Bare Pascal names that are never a project routine's call (#2514), lowercased. */
|
||||
const PASCAL_NOT_ROUTINES: ReadonlySet<string> = new Set(['self', 'result', 'true', 'false', 'nil', 'exit', 'break', 'continue']);
|
||||
|
||||
/** A routine header of the Pascal source text, for a broken tree's regions (#2514). */
|
||||
interface PascalTextRoutine {
|
||||
line: number;
|
||||
classQn: string | null;
|
||||
routine: string | null;
|
||||
locals: Map<string, string | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* How a Pascal name written without parentheses may run a routine (#2514):
|
||||
* as a statement (`call`), for its value (`read`), as an event's method
|
||||
* (`ref`), or as a receiver or an indexed value (`recv`). See
|
||||
* resolution/pascal-scope.
|
||||
*/
|
||||
type PascalNameMode = 'call' | 'read' | 'ref' | 'recv';
|
||||
|
||||
/**
|
||||
* Names Delphi's conventions give to data, never to a routine: a field
|
||||
* (`FCount`), a type (`TList`, `EAbort`), a constant (`MAX_PATH`, `CRLF`).
|
||||
* A C library's functions keep their own case (`ERR_get_error`).
|
||||
*/
|
||||
const PASCAL_DATA_NAME = /^(?:F[A-Z][a-z]|[TE][A-Z][a-z]|[A-Z][A-Z0-9_]*$)/;
|
||||
|
||||
export class TreeSitterExtractor {
|
||||
/**
|
||||
* The node's prose, from either place it can live: a preceding comment
|
||||
@@ -728,6 +754,27 @@ export class TreeSitterExtractor {
|
||||
// the precision guard (the #932 table-dispatch philosophy scoped to locals).
|
||||
private cppLocalFnPtrs = new Map<string, Map<string, Set<string>>>();
|
||||
private methodIndex: Map<string, string> | null = null; // lookup key → node ID for Pascal defProc lookup
|
||||
// Paren-less Pascal names (#2514): the names each routine or anonymous
|
||||
// method being walked declares itself (innermost last), the class the
|
||||
// routine belongs to, how many `with` bodies the walk is inside (a bare name
|
||||
// there may be the `with` object's), and per class the names its own data
|
||||
// and the unit's data take (lowercased).
|
||||
private pascalScopes: Array<{
|
||||
locals: Map<string, string | null>;
|
||||
classQn: string | null;
|
||||
routine: string | null;
|
||||
/** The line of the routine's header (0: none), for a broken tree's regions. */
|
||||
startLine: number;
|
||||
}> = [];
|
||||
/** The routine headers of the source text, by line, for a broken tree's regions (#2514). */
|
||||
private pascalTextRoutines: PascalTextRoutine[] | null = null;
|
||||
private pascalSourceLines: string[] | null = null;
|
||||
private pascalWithDepth = 0;
|
||||
private pascalDataNameSets = new Map<string, Set<string>>();
|
||||
/** Bare names read in an expression, kept until the file shows which are variables (#2514). */
|
||||
private pascalReads: Array<{ ref: UnresolvedReference; scope: string; name: string }> = [];
|
||||
/** Per routine class (`''`: none), the names the file assigns to or passes to `Inc`/`Dec`. */
|
||||
private pascalAssigned = new Map<string, Set<string>>();
|
||||
// Function-as-value capture (#756): per-language spec + candidates collected
|
||||
// during the walk, gated & flushed into unresolvedReferences at end-of-file
|
||||
// (see flushFnRefCandidates).
|
||||
@@ -870,6 +917,7 @@ export class TreeSitterExtractor {
|
||||
// nodes and import refs are complete and the file node is still pushed.
|
||||
this.flushFnRefCandidates();
|
||||
this.flushValueRefs();
|
||||
this.flushPascalReads();
|
||||
|
||||
if (packageNodeId) this.nodeStack.pop();
|
||||
this.nodeStack.pop();
|
||||
@@ -8331,9 +8379,19 @@ export class TreeSitterExtractor {
|
||||
return true;
|
||||
}
|
||||
|
||||
// block (begin..end) → visit for calls
|
||||
if (nodeType === 'block') {
|
||||
this.visitPascalBlock(node);
|
||||
// block (begin..end) → visit for calls. Outside a routine — a program's
|
||||
// main block, `initialization` and `finalization` — the names in it are
|
||||
// the unit's (#2514). Where the tree has errors, a block outside a routine
|
||||
// (a `blockTr` there) may be the body of one whose header didn't parse;
|
||||
// the source text says whose it is.
|
||||
if (nodeType === 'block' || nodeType === 'blockTr' || nodeType === 'initialization' || nodeType === 'finalization') {
|
||||
const outside = this.pascalScopes.length === 0;
|
||||
if (outside) {
|
||||
this.pascalScopes.push({ locals: new Map([['result', null]]), classQn: null, routine: null, startLine: 0 });
|
||||
}
|
||||
// A `blockTr` may also hold declarations, which are walked as they always were.
|
||||
this.visitPascalBlock(node, nodeType === 'blockTr');
|
||||
if (outside) this.pascalScopes.pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8553,12 +8611,107 @@ export class TreeSitterExtractor {
|
||||
(c: SyntaxNode) => c.type === 'block'
|
||||
);
|
||||
if (block) {
|
||||
// `TList<T>.Add` belongs to `TList`, whose node may spell its parameters
|
||||
// with their constraints (`TList<T: class>`).
|
||||
const owner = fullName.includes('.') ? fullName.replace(/<[^>]*>/g, '').split('.').slice(0, -1).join('::') : null;
|
||||
this.pascalScopes.push({
|
||||
locals: this.pascalRoutineLocals(node),
|
||||
classQn: owner,
|
||||
routine: shortNameKey.replace(/<.*$/, ''),
|
||||
startLine: node.startPosition.row + 1,
|
||||
});
|
||||
this.nodeStack.push(parentId);
|
||||
this.visitPascalBlock(block);
|
||||
this.nodeStack.pop();
|
||||
this.pascalScopes.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The names a routine (a `defProc`) or an anonymous method (a `lambda`)
|
||||
* declares before its body, lowercased, with the class or record type each
|
||||
* is declared with (null when it has none, or none a name can stand for):
|
||||
* parameters, `var`/`const` sections, nested routines and `Result`. The
|
||||
* body's inline declarations join as the walk reaches them
|
||||
* (`declarePascalInline`). A bare name among them is the routine's own,
|
||||
* never a call.
|
||||
*/
|
||||
private pascalRoutineLocals(routine: SyntaxNode): Map<string, string | null> {
|
||||
const names = new Map<string, string | null>();
|
||||
const header = routine.type === 'lambda' ? routine : routine.namedChildren.find((c: SyntaxNode) => c.type === 'declProc');
|
||||
names.set('result', this.pascalTypeName(header?.namedChildren.find((c: SyntaxNode) => c.type === 'typeref')));
|
||||
const declare = (decl: SyntaxNode): void => {
|
||||
const type = this.pascalTypeName(decl.namedChildren.find((c: SyntaxNode) => c.type === 'type' || c.type === 'typeref'));
|
||||
for (const c of decl.namedChildren) {
|
||||
if (c.type === 'identifier') names.set(getNodeText(c, this.source).toLowerCase(), type);
|
||||
}
|
||||
};
|
||||
const args = header?.namedChildren.find((c: SyntaxNode) => c.type === 'declArgs');
|
||||
for (const arg of args?.namedChildren ?? []) {
|
||||
if (arg.type === 'declArg') declare(arg);
|
||||
}
|
||||
for (const c of routine.namedChildren) {
|
||||
if (c.type === 'declVars' || c.type === 'declConsts') {
|
||||
for (const decl of c.namedChildren) declare(decl);
|
||||
} else if (c.type === 'defProc') {
|
||||
const name = getChildByField(c.namedChildren.find((x: SyntaxNode) => x.type === 'declProc') ?? c, 'name');
|
||||
if (name?.type === 'identifier') names.set(getNodeText(name, this.source).toLowerCase(), null);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* A name a routine's body declares where it stands — an inline `var`, a
|
||||
* `for` or `for … in` loop's variable, an `on E: EFoo do` handler's — joins
|
||||
* the routine's locals before the code that uses it is walked.
|
||||
*/
|
||||
private declarePascalInline(node: SyntaxNode): void {
|
||||
const locals = this.pascalScopes[this.pascalScopes.length - 1]?.locals;
|
||||
if (!locals) return;
|
||||
const declare = (id: SyntaxNode | null | undefined, type: string | null): void => {
|
||||
if (id?.type !== 'identifier') return;
|
||||
const name = getNodeText(id, this.source).toLowerCase();
|
||||
// A loop over a variable the `var` section declares keeps its type.
|
||||
if (type !== null || !locals.has(name)) locals.set(name, type);
|
||||
};
|
||||
switch (node.type) {
|
||||
case 'varAssignDef':
|
||||
case 'varDef': {
|
||||
let type = this.pascalTypeName(node.namedChildren.find((c: SyntaxNode) => c.type === 'typeref' || c.type === 'type'));
|
||||
const value = node.parent?.type === 'assignment' ? node.parent.namedChild(node.parent.namedChildCount - 1) : null;
|
||||
if (!type && value?.type === 'exprDot') {
|
||||
// `var List := TStringList.Create;`
|
||||
const [cls, ctor] = [value.namedChild(0), value.namedChild(value.namedChildCount - 1)];
|
||||
if (cls?.type === 'identifier' && ctor?.type === 'identifier' && /^create$/i.test(getNodeText(ctor, this.source))) {
|
||||
type = getNodeText(cls, this.source);
|
||||
}
|
||||
}
|
||||
declare(node.namedChildren.find((c: SyntaxNode) => c.type === 'identifier'), type);
|
||||
return;
|
||||
}
|
||||
case 'foreach':
|
||||
declare(getChildByField(node, 'iterator'), null);
|
||||
return;
|
||||
case 'for':
|
||||
declare(node.namedChildren.find((c: SyntaxNode) => c.type === 'assignment')?.namedChild(0), null);
|
||||
return;
|
||||
case 'exceptionHandler':
|
||||
declare(getChildByField(node, 'variable'), this.pascalTypeName(node.namedChildren.find((c: SyntaxNode) => c.type === 'typeref')));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** The class or record name a declared type (`type`/`typeref`) stands for: `TFoo`, `Sys.TFoo` → `TFoo`, `TList<T>` → `TList`. */
|
||||
private pascalTypeName(type: SyntaxNode | null | undefined): string | null {
|
||||
let ref = type?.type === 'type' ? type.namedChild(0) : type;
|
||||
if (ref?.type !== 'typeref') return null;
|
||||
ref = ref.namedChild(0);
|
||||
if (ref?.type === 'typerefDot') ref = ref.namedChild(ref.namedChildCount - 1);
|
||||
else if (ref?.type === 'typerefTpl') ref = ref.namedChild(0);
|
||||
return ref?.type === 'identifier' ? getNodeText(ref, this.source) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract function calls from a Pascal expression. A `receiverLink` call is
|
||||
* a link inside another call's receiver chain (#2547): it is recorded only
|
||||
@@ -8649,24 +8802,31 @@ export class TreeSitterExtractor {
|
||||
* `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.
|
||||
* there is a call like any other. A paren-less link (`B` in `A.B.C`, `GetList`
|
||||
* in `GetList.Count`) is read for its value, which a function gives
|
||||
* (#2514): it is recorded as a paren-less name. 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 {
|
||||
private extractPascalReceiverCalls(node: SyntaxNode | null, names = true): void {
|
||||
if (!node) return;
|
||||
switch (node.type) {
|
||||
case 'identifier':
|
||||
if (names) this.extractPascalParenlessName(node, null);
|
||||
return;
|
||||
case 'exprCall':
|
||||
this.extractPascalReceiverCall(node, true);
|
||||
return;
|
||||
case 'exprDot':
|
||||
if (names) this.extractPascalParenlessName(node, null);
|
||||
this.extractPascalReceiverCalls(node.namedChild(0), names);
|
||||
return;
|
||||
case 'exprTpl':
|
||||
this.extractPascalReceiverCalls(node.namedChild(0));
|
||||
// `TList<Integer>.Create`: a generic type's name, not a value's.
|
||||
this.extractPascalReceiverCalls(node.namedChild(0), false);
|
||||
return;
|
||||
case 'exprSubscript': {
|
||||
this.extractPascalReceiverCalls(node.namedChild(0));
|
||||
this.extractPascalReceiverCalls(node.namedChild(0), names);
|
||||
const index = node.namedChildren.find((c: SyntaxNode) => c.type === 'exprArgs');
|
||||
if (index) this.visitPascalBlock(index);
|
||||
return;
|
||||
@@ -8749,16 +8909,23 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively visit a Pascal block/statement tree for call expressions
|
||||
* Recursively visit a Pascal block/statement tree for call expressions.
|
||||
* With `declarations`, a declaration in it is visited as a declaration.
|
||||
*/
|
||||
private visitPascalBlock(node: SyntaxNode): void {
|
||||
private visitPascalBlock(node: SyntaxNode, declarations = false): void {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (!child) continue;
|
||||
if (declarations && /^(?:decl|defProc$|interface$|implementation$)/.test(child.type)) {
|
||||
this.visitNode(child);
|
||||
continue;
|
||||
}
|
||||
// Function-as-value capture (#756): Pascal bodies are walked here, not
|
||||
// in visitNode/visitForCallsAndStructure, so the capture hook fires here
|
||||
// — assignment RHS is the Delphi event-wiring idiom (`OnFire := Handler`).
|
||||
this.maybeCaptureFnRefs(child, child.type);
|
||||
this.declarePascalInline(child);
|
||||
this.learnPascalVariable(child);
|
||||
if (child.type === 'exprCall') {
|
||||
this.extractPascalCall(child);
|
||||
// The walker doesn't descend into a call's arguments — dispatch the
|
||||
@@ -8773,17 +8940,343 @@ export class TreeSitterExtractor {
|
||||
// (`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);
|
||||
// Where the tree has errors, `procedure TFoo.Bar;` may parse as
|
||||
// one: a routine's header calls nothing (#2514).
|
||||
if (!this.isPascalHeaderName(child)) {
|
||||
this.extractPascalParenlessCall(child);
|
||||
}
|
||||
} else {
|
||||
const receiver = child.namedChild(0);
|
||||
if (receiver?.type === 'exprCall') this.extractPascalReceiverCall(receiver, false);
|
||||
else this.extractPascalReceiverCalls(receiver);
|
||||
this.extractPascalParenlessName(child, node);
|
||||
}
|
||||
} else if (child.type === 'identifier') {
|
||||
this.extractPascalParenlessName(child, node);
|
||||
} else if (child.type === 'with') {
|
||||
this.pascalWithDepth++;
|
||||
this.visitPascalBlock(child, declarations);
|
||||
this.pascalWithDepth--;
|
||||
} else if (child.type === 'lambda') {
|
||||
const outer = this.pascalScopes[this.pascalScopes.length - 1];
|
||||
this.pascalScopes.push({
|
||||
locals: this.pascalRoutineLocals(child),
|
||||
classQn: outer?.classQn ?? null,
|
||||
routine: null,
|
||||
startLine: outer?.startLine ?? 0,
|
||||
});
|
||||
this.visitPascalBlock(child, declarations);
|
||||
this.pascalScopes.pop();
|
||||
} else {
|
||||
this.visitPascalBlock(child);
|
||||
// A `begin … end` block in it holds statements only, as it always has.
|
||||
this.visitPascalBlock(child, declarations && child.type !== 'block');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A name written without parentheses where a parameterless routine may be
|
||||
* called (#2514): `Before;`, `Result := FProvider.Value + 1`, `if Ready
|
||||
* then`, `OnGet := Handler`, `GetList.Count`. Recorded as a `references`
|
||||
* ref named `{mode}.Name` or `{mode}.Receiver.Name`, with the routine's
|
||||
* class after an `@` (see resolution/pascal-scope), which links only a
|
||||
* routine the name reaches, or nothing. A null `parent` marks a receiver's
|
||||
* link. Names the routine declares itself and names the class's or unit's
|
||||
* own data takes are left out here; a receiver the routine declares is
|
||||
* written as its declared type (`{read}.:TProvider.Value`), or left out
|
||||
* when it has none.
|
||||
*/
|
||||
private extractPascalParenlessName(name: SyntaxNode, parent: SyntaxNode | null): void {
|
||||
const scope = this.pascalScopeAt(name.startPosition.row + 1);
|
||||
if (!scope) return;
|
||||
// Where the tree has errors, a routine's header may parse as statements
|
||||
// (`procedure Foo;`): the name it declares is no call.
|
||||
if (this.isPascalHeaderName(name)) return;
|
||||
const callerId = this.nodeStack[this.nodeStack.length - 1];
|
||||
if (!callerId) return;
|
||||
// Most bare names are the routine's own or the class's data: settled
|
||||
// before what the name's place in the tree says.
|
||||
if (name.type === 'identifier') {
|
||||
const lower = getNodeText(name, this.source).toLowerCase();
|
||||
if (PASCAL_NOT_ROUTINES.has(lower) || this.pascalLocalType(lower, scope.locals) !== undefined) return;
|
||||
if (this.pascalDataNames(scope.classQn).has(lower)) return;
|
||||
}
|
||||
const mode: PascalNameMode | null = parent ? this.pascalNameMode(name, parent) : 'recv';
|
||||
if (!mode) return;
|
||||
// In a `with` body a name may be the `with` object's member, which the
|
||||
// object's type would have to say. Only the method an event is given is
|
||||
// linked there (`OnClick := ButtonClick`), the form's, as it is written.
|
||||
if (this.pascalWithDepth > 0 && (mode !== 'ref' || name.type !== 'identifier')) return;
|
||||
let path: string[];
|
||||
if (name.type === 'identifier') {
|
||||
path = [getNodeText(name, this.source)];
|
||||
} else {
|
||||
const links = this.pascalIdentifierPath(name);
|
||||
if (!links) return;
|
||||
path = links;
|
||||
}
|
||||
const head = path[0]!;
|
||||
const lower = head.toLowerCase();
|
||||
const local = this.pascalLocalType(lower, scope.locals);
|
||||
// Read for its value, a name Delphi's conventions give to data is never a
|
||||
// routine's: lowercase words are locals, fields and enum values (`clRed`),
|
||||
// where a C library's functions aren't (`mach_absolute_time`), and see
|
||||
// PASCAL_DATA_NAME. A statement on its own runs a routine, and an event is
|
||||
// given one, whatever its name (`btnOKClick`).
|
||||
const read = mode !== 'call' && mode !== 'ref';
|
||||
if (read && PASCAL_DATA_NAME.test(path[path.length - 1]!)) return;
|
||||
if (path.length === 1) {
|
||||
if (PASCAL_NOT_ROUTINES.has(lower) || local !== undefined) return;
|
||||
if (read && /^[a-z][A-Za-z]*$/.test(head)) return;
|
||||
if (this.pascalDataNames(scope.classQn).has(lower)) return;
|
||||
} else if (local !== undefined) {
|
||||
// A local's declared class stands for it; a pointer type (`PNode`),
|
||||
// which no class node declares, can't. A local's member as a receiver
|
||||
// is left out: what it reaches is almost always data.
|
||||
if (local === null || mode === 'recv' || /^P[A-Z]/.test(local)) return;
|
||||
path[0] = `:${local}`;
|
||||
} else if (lower !== 'self' && PASCAL_NOT_ROUTINES.has(lower)) {
|
||||
return;
|
||||
}
|
||||
// The routine's class rides along: the reference's own node may be the
|
||||
// file when the implementation matched no declaration.
|
||||
const ref: UnresolvedReference = {
|
||||
fromNodeId: callerId,
|
||||
referenceName: `{${mode}${scope.classQn ? `@${scope.classQn}` : ''}}.${path.join('.')}`,
|
||||
referenceKind: 'references',
|
||||
line: name.startPosition.row + 1,
|
||||
column: name.startPosition.column,
|
||||
};
|
||||
if (path.length === 1 && mode !== 'call') this.pascalReads.push({ ref, scope: scope.classQn ?? '', name: lower });
|
||||
else this.unresolvedReferences.push(ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* A bare name the code assigns to (`Run := 0`) or passes to `Inc`/`Dec` is
|
||||
* a variable, a field or a property wherever the same class's code reads it
|
||||
* (#2514), unless the routine declares it, or it is the function's own name
|
||||
* (its result, in the old style), or a `with` object may hold it.
|
||||
*/
|
||||
private learnPascalVariable(node: SyntaxNode): void {
|
||||
if (this.pascalWithDepth > 0) return;
|
||||
let target: SyntaxNode | null = null;
|
||||
if (node.type === 'assignment') {
|
||||
target = node.namedChild(0);
|
||||
} else if (node.type === 'exprCall') {
|
||||
const callee = node.namedChild(0);
|
||||
if (callee?.type === 'identifier' && /^(?:inc|dec)$/i.test(getNodeText(callee, this.source))) {
|
||||
target = node.namedChildren.find((c: SyntaxNode) => c.type === 'exprArgs')?.namedChild(0) ?? null;
|
||||
}
|
||||
}
|
||||
if (target?.type !== 'identifier') return;
|
||||
const scope = this.pascalScopeAt(target.startPosition.row + 1);
|
||||
if (!scope) return;
|
||||
const name = getNodeText(target, this.source).toLowerCase();
|
||||
if (name === scope.routine || this.pascalLocalType(name, scope.locals) !== undefined) return;
|
||||
const key = scope.classQn ?? '';
|
||||
let names = this.pascalAssigned.get(key);
|
||||
if (!names) this.pascalAssigned.set(key, (names = new Set()));
|
||||
names.add(name);
|
||||
}
|
||||
|
||||
/** The bare names read in an expression, less those the file shows are variables of their class. */
|
||||
private flushPascalReads(): void {
|
||||
for (const { ref, scope, name } of this.pascalReads) {
|
||||
if (!this.pascalAssigned.get(scope)?.has(name)) this.unresolvedReferences.push(ref);
|
||||
}
|
||||
this.pascalReads = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a node is the name a routine's header declares (`TFoo.Bar` in
|
||||
* `procedure TFoo.Bar;`), which a tree with errors may parse as a statement.
|
||||
*/
|
||||
private isPascalHeaderName(node: SyntaxNode): boolean {
|
||||
if (!this.tree?.rootNode.hasError) return false;
|
||||
const line = (this.pascalSourceLines ??= this.source.split(/\r?\n/))[node.startPosition.row] ?? '';
|
||||
return /^\s*(?:class\s+)?(?:procedure|function|constructor|destructor|operator)\s+$/i.test(line.slice(0, node.startPosition.column));
|
||||
}
|
||||
|
||||
/**
|
||||
* The declared type of a name the enclosing routines declare (null: none it
|
||||
* can be named by), or undefined when none does. `textLocals` are the
|
||||
* declarations the source text gives a broken tree's routine.
|
||||
*/
|
||||
private pascalLocalType(lower: string, textLocals?: Map<string, string | null> | null): string | null | undefined {
|
||||
for (let i = this.pascalScopes.length - 1; i >= 0; i--) {
|
||||
const locals = this.pascalScopes[i]!.locals;
|
||||
if (locals.has(lower)) return locals.get(lower);
|
||||
}
|
||||
return textLocals?.has(lower) ? textLocals.get(lower) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The class and routine a name at `line` belongs to; null outside any code.
|
||||
* Where the tree has errors, a routine whose header didn't parse is a block
|
||||
* outside any routine, or part of the routine above it, so the source text
|
||||
* says: a header between the tree's routine and the name is the name's,
|
||||
* with the declarations the text gives it.
|
||||
*/
|
||||
private pascalScopeAt(line: number): { classQn: string | null; routine: string | null; locals: Map<string, string | null> | null } | null {
|
||||
const scope = this.pascalScopes[this.pascalScopes.length - 1];
|
||||
if (!scope) return null;
|
||||
const own = { classQn: scope.classQn, routine: scope.routine, locals: null };
|
||||
if (!this.tree?.rootNode.hasError) return own;
|
||||
const routines = (this.pascalTextRoutines ??= this.readPascalTextRoutines());
|
||||
let lo = 0;
|
||||
let hi = routines.length - 1;
|
||||
let hit = -1;
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (routines[mid]!.line <= line) {
|
||||
hit = mid;
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
const text = hit >= 0 ? routines[hit]! : null;
|
||||
// Inside a routine the tree parsed, only a later header overrides it.
|
||||
if (!text || text.line <= scope.startLine || (scope.startLine > 0 && text.routine === null)) return own;
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Each routine header of the source text (`procedure TFoo.Bar(…)`), with
|
||||
* its class, its name and what it declares before its body — parameters,
|
||||
* `Result`'s type, `var` and `const` sections — and where the unit's own
|
||||
* code resumes: after an `end;` that starts its line, which closes a
|
||||
* routine, and at `initialization` and `finalization`. An indented header
|
||||
* is a routine nested in the one above, whose class and names it shares.
|
||||
*/
|
||||
private readPascalTextRoutines(): PascalTextRoutine[] {
|
||||
const lines = (this.pascalSourceLines ??= this.source.split(/\r?\n/));
|
||||
const header = /^(\s*)(?:class\s+)?(?:procedure|function|constructor|destructor|operator)\s+([A-Za-z_]\w*(?:<[^>]*>)?(?:\.[A-Za-z_]\w*(?:<[^>]*>)?)*)/i;
|
||||
const routines: PascalTextRoutine[] = [];
|
||||
let outer: PascalTextRoutine | null = null;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const text = lines[i]!;
|
||||
if (/^(?:end;|\s*(?:initialization|finalization)\b)/i.test(text)) {
|
||||
routines.push({ line: i + 1, classQn: null, routine: null, locals: new Map() });
|
||||
outer = null;
|
||||
continue;
|
||||
}
|
||||
const m = header.exec(text);
|
||||
if (!m) continue;
|
||||
const nested: boolean = m[1] !== '' && outer !== null;
|
||||
const parts = m[2]!.replace(/<[^>]*>/g, '').split('.');
|
||||
const routine = parts.pop()!.toLowerCase();
|
||||
let decl = text.slice(m.index + m[0].length);
|
||||
for (let j = i + 1; j < lines.length && j <= i + 200 && !/^\s*(?:begin|asm)\b/i.test(lines[j]!) && !header.test(lines[j]!); j++) {
|
||||
decl += '\n' + lines[j];
|
||||
}
|
||||
decl = decl.replace(/\{[^}]*\}|\(\*[\s\S]*?\*\)|\/\/[^\n]*/g, ' ');
|
||||
const locals: Map<string, string | null> = new Map(nested ? outer!.locals : []);
|
||||
const result = /^\s*(?:\([^)]*\))?\s*:\s*([A-Za-z_][\w.]*)/.exec(decl);
|
||||
locals.set('result', result ? result[1]!.split('.').pop()! : null);
|
||||
for (const d of decl.matchAll(/([A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)\s*:(?!=)\s*([A-Za-z_][\w.]*)?/g)) {
|
||||
const type = d[2] ? d[2].split('.').pop()! : null;
|
||||
for (const name of d[1]!.split(',')) locals.set(name.trim().toLowerCase(), type);
|
||||
}
|
||||
if (nested) {
|
||||
// The outer routine's body follows its nested ones: it keeps the
|
||||
// outer routine's class and name, and sees the names of both.
|
||||
routines.push({ line: i + 1, classQn: outer!.classQn, routine: outer!.routine, locals });
|
||||
} else {
|
||||
outer = { line: i + 1, classQn: parts.length > 0 ? parts.join('::') : null, routine, locals };
|
||||
routines.push(outer);
|
||||
}
|
||||
}
|
||||
return routines;
|
||||
}
|
||||
|
||||
/**
|
||||
* How a name in `parent` may run a parameterless routine: `call` as a
|
||||
* statement of its own, `read` for its value in an expression or an
|
||||
* argument, `ref` as the method an event is given, `recv` as what is
|
||||
* indexed; null where it can't (an assignment's target, a case label, a
|
||||
* set literal, a type).
|
||||
*/
|
||||
private pascalNameMode(name: SyntaxNode, parent: SyntaxNode): PascalNameMode | null {
|
||||
const is = (field: string): boolean => parent.childForFieldName(field)?.id === name.id;
|
||||
switch (parent.type) {
|
||||
case 'statement':
|
||||
return parent.namedChildCount === 1 && name.type === 'identifier' ? 'call' : null;
|
||||
case 'exprSubscript':
|
||||
return parent.namedChild(0)?.id === name.id ? 'recv' : null;
|
||||
case 'assignment': {
|
||||
const target = parent.namedChild(0);
|
||||
if (!target || target.id === name.id) return null;
|
||||
const last = target.type === 'exprDot'
|
||||
? target.namedChildren.filter((c: SyntaxNode) => c.type === 'identifier').pop()
|
||||
: target;
|
||||
return last && /^F?On[A-Z]/.test(getNodeText(last, this.source)) ? 'ref' : 'read';
|
||||
}
|
||||
case 'exprArgs':
|
||||
return parent.parent?.type === 'exprCall' || parent.parent?.type === 'exprSubscript' || parent.parent?.type === 'inherited' ? 'read' : null;
|
||||
case 'exprBinary': {
|
||||
const op = parent.namedChildren.find((c: SyntaxNode) => c.id !== name.id && /^k[A-Z]/.test(c.type));
|
||||
return op && (op.type === 'kIs' || op.type === 'kAs') && parent.namedChild(0)?.id !== name.id ? null : 'read';
|
||||
}
|
||||
case 'exprUnary':
|
||||
return parent.child(0)?.type === 'kAt' ? null : 'read';
|
||||
case 'if':
|
||||
case 'ifElse':
|
||||
case 'while':
|
||||
case 'repeat':
|
||||
return is('condition') ? 'read' : null;
|
||||
case 'case':
|
||||
return 'read';
|
||||
case 'foreach':
|
||||
return is('iterable') ? 'read' : null;
|
||||
case 'for':
|
||||
return is('end') ? 'read' : null;
|
||||
case 'exprParens':
|
||||
return 'read';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** The identifiers of a plain dotted name (`FProvider.Value`, `Self.A.B`), or null. */
|
||||
private pascalIdentifierPath(dot: SyntaxNode): string[] | null {
|
||||
const links: string[] = [];
|
||||
let n: SyntaxNode | null = dot;
|
||||
while (n?.type === 'exprDot') {
|
||||
const last = n.namedChild(n.namedChildCount - 1);
|
||||
if (last?.type !== 'identifier') return null;
|
||||
links.unshift(getNodeText(last, this.source));
|
||||
n = n.namedChild(0);
|
||||
}
|
||||
if (n?.type !== 'identifier') return null;
|
||||
links.unshift(getNodeText(n, this.source));
|
||||
return links;
|
||||
}
|
||||
|
||||
/**
|
||||
* The names data takes for a routine of class `classQn`, lowercased: the
|
||||
* class's fields, properties and constants, and the unit's variables and
|
||||
* constants. A bare name among them reads that data.
|
||||
*/
|
||||
private pascalDataNames(classQn: string | null): Set<string> {
|
||||
const key = classQn ?? '';
|
||||
let names = this.pascalDataNameSets.get(key);
|
||||
if (names) return names;
|
||||
names = new Set();
|
||||
const prefix = classQn ? `${classQn.toLowerCase()}::` : null;
|
||||
for (const n of this.nodes) {
|
||||
if (n.kind === 'enum_member') {
|
||||
names.add(n.name.toLowerCase()); // enum values are visible unqualified
|
||||
continue;
|
||||
}
|
||||
if (n.kind !== 'field' && n.kind !== 'property' && n.kind !== 'constant' && n.kind !== 'variable') continue;
|
||||
const qn = n.qualifiedName.replace(/<[^>]*>/g, '').toLowerCase();
|
||||
const own = prefix !== null && qn.startsWith(prefix) && qn.indexOf('::', prefix.length) < 0;
|
||||
if (own || !qn.includes('::')) names.add(n.name.toLowerCase());
|
||||
}
|
||||
this.pascalDataNameSets.set(key, names);
|
||||
return names;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './
|
||||
import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility';
|
||||
import { clearDartLibraryMemos } from './dart-libraries';
|
||||
import { clearVbnetReceiverMemos, isVbMemberRead, isVbPathCall, matchVbMemberRead, matchVbPathCall } from './vbnet-receivers';
|
||||
import { clearPascalScopeMemos, isPascalParenlessRef, matchPascalParenlessRef } from './pascal-scope';
|
||||
import { gateTypeParameter, clearTypeParameterMemos } from './type-parameters';
|
||||
import { gateDartLocal, clearDartLocalScopeMemos } from './dart-local-scope';
|
||||
import { clearCppTypeAliasMemos } from './cpp-type-aliases';
|
||||
@@ -587,6 +588,7 @@ export class ReferenceResolver {
|
||||
clearSwiftTypeVisibility(this.context);
|
||||
clearDartLibraryMemos(this.context);
|
||||
clearVbnetReceiverMemos(this.context);
|
||||
clearPascalScopeMemos(this.context);
|
||||
clearTypeParameterMemos(this.context);
|
||||
clearDartLocalScopeMemos(this.context);
|
||||
clearCppTypeAliasMemos(this.context);
|
||||
@@ -909,6 +911,12 @@ export class ReferenceResolver {
|
||||
return result;
|
||||
},
|
||||
|
||||
getNodeNamesOfKinds: (language, kinds) => this.queries.getNodeNamesOfKinds(language, kinds),
|
||||
|
||||
rememberNode: (node: Node) => {
|
||||
if (!this.nodeByIdCache.has(node.id)) this.nodeByIdCache.set(node.id, node);
|
||||
},
|
||||
|
||||
getNodeById: (id: string) => {
|
||||
return this.nodeById(id);
|
||||
},
|
||||
@@ -1428,6 +1436,10 @@ export class ReferenceResolver {
|
||||
// ./cpp-supertypes).
|
||||
if (isCppSupertypeRef(ref)) return matchCppSupertype(ref, this.context);
|
||||
|
||||
// A Pascal name written without parentheses (#2514) links only to a
|
||||
// routine its scope reaches, or through a receiver to a method of the
|
||||
// receiver's declared type; data of that name links nothing.
|
||||
if (isPascalParenlessRef(ref)) return matchPascalParenlessRef(ref, this.context);
|
||||
// A Dart member read (`x.area`) links the getter the receiver's type
|
||||
// reaches, as a call, or nothing — never a guess by name (#2338).
|
||||
if (isDartMemberRead(ref)) return matchDartMemberRead(ref, this.context);
|
||||
|
||||
@@ -18233,7 +18233,7 @@ function headTypeArguments(cls: Node, sup: string, context: ResolutionContext):
|
||||
const DECLARED_SUPERS = new WeakMap<ResolutionContext, Map<string, string[]>>();
|
||||
const INHERITED_METHODS = new WeakMap<ResolutionContext, Map<string, Node | null>>();
|
||||
|
||||
function declaredSupertypes(cls: Node, context: ResolutionContext): string[] {
|
||||
export function declaredSupertypes(cls: Node, context: ResolutionContext): string[] {
|
||||
let memo = DECLARED_SUPERS.get(context);
|
||||
if (!memo) DECLARED_SUPERS.set(context, (memo = new Map()));
|
||||
const hit = memo.get(cls.id);
|
||||
@@ -18257,7 +18257,7 @@ function readDeclaredSupertypes(cls: Node, context: ResolutionContext): string[]
|
||||
const names = (text: string | undefined): string[] =>
|
||||
text ? [...text.matchAll(/([A-Za-z_][\w.:\\]*)/g)].map((m) => m[1]!.split(/::|\.|\\/).pop()!).filter((w) => !/^(?:metaclass|object)$/.test(w)) : [];
|
||||
switch (cls.language) {
|
||||
case 'pascal': return names(/=\s*class\s*\(([^)]*)\)/i.exec(head)?.[1]);
|
||||
case 'pascal': return names(/=\s*class\s*(?:sealed|abstract)?\s*\(([^)]*)\)/i.exec(head)?.[1]);
|
||||
// A base built by a class's factory — Django's `class Manager(
|
||||
// BaseManager.from_queryset(QuerySet))` — subclasses that class: the
|
||||
// qualifier is the supertype, not the method called on it.
|
||||
|
||||
@@ -0,0 +1,483 @@
|
||||
/**
|
||||
* Pascal names written without parentheses (#2514): a parameterless routine
|
||||
* is called that way in Delphi — `Before;`, `Result := FProvider.Value + 1`,
|
||||
* `if Ready then` — and the same spelling reads a field, a property, a
|
||||
* variable or a constant. The extractor records such a name as a
|
||||
* `references` ref named `{mode}.Name` or `{mode}.Receiver.Name`:
|
||||
*
|
||||
* - `{call}` — a statement on its own, which only a routine can be;
|
||||
* - `{read}` — a name in an expression or an argument, where a function
|
||||
* (or a constructor) is called for its value and a procedure, which has
|
||||
* none, can only be a method pointer;
|
||||
* - `{ref}` — the value given to an event (`OnClick := Handler`), a method
|
||||
* pointer;
|
||||
* - `{recv}` — a receiver (`GetList` in `GetList.Count`, `B` in `A.B.C`) or
|
||||
* an indexed value, which only a function can be.
|
||||
*
|
||||
* Written without its arguments, a routine that needs some is a method
|
||||
* pointer too, in any of them.
|
||||
*
|
||||
* The routine's class follows an `@` (`{call@TConsumer}.Before`), because the
|
||||
* reference's own node is the file when an implementation matched no
|
||||
* declaration. A receiver the routine declares itself is written as its
|
||||
* declared type after a `:` (`{read}.:TProvider.Value`).
|
||||
*
|
||||
* This module links such a ref only to a method or function the name reaches
|
||||
* by Delphi's scope rules — the routine's class and its ancestors, then the
|
||||
* unit, then the units it uses, last first — or, through a receiver, a method
|
||||
* of the receiver's declared type. The first declaration of the name in that
|
||||
* order decides: when it is data (a field, property, variable, constant, enum
|
||||
* value or type), nothing is linked. A bare name the routine declares itself
|
||||
* never reaches here (the extractor skips it).
|
||||
*/
|
||||
|
||||
import type { Node } from '../types';
|
||||
import type { ResolutionContext, ResolvedRef, UnresolvedRef } from './types';
|
||||
import { declaredSupertypes } from './name-matcher';
|
||||
|
||||
type Mode = 'call' | 'read' | 'ref' | 'recv';
|
||||
interface Found {
|
||||
node: Node;
|
||||
resolvedBy: ResolvedRef['resolvedBy'];
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
const PARENLESS = /^\{(call|read|ref|recv)(?:@([^}]+))?\}\.(.+)$/;
|
||||
const CLASS_KINDS: ReadonlySet<string> = new Set(['class', 'struct', 'interface']);
|
||||
/** What a unit declares by name at its top level. */
|
||||
const UNIT_KINDS: ReadonlySet<string> = new Set([
|
||||
'function', 'variable', 'constant', 'class', 'struct', 'interface', 'enum', 'type_alias', 'enum_member',
|
||||
]);
|
||||
/** What a class declares as its members. */
|
||||
const MEMBER_KINDS: ReadonlySet<string> = new Set(['method', 'field', 'property', 'constant', 'variable']);
|
||||
const MAX_HIERARCHY_DEPTH = 8;
|
||||
|
||||
/** Whether `ref` is a paren-less Pascal name (see the file comment), which only this module resolves. */
|
||||
export function isPascalParenlessRef(ref: UnresolvedRef): boolean {
|
||||
return ref.language === 'pascal' && ref.referenceKind === 'references' && PARENLESS.test(ref.referenceName);
|
||||
}
|
||||
|
||||
/** The routine a paren-less Pascal name reaches in its scope, or null (see the file comment). */
|
||||
export function matchPascalParenlessRef(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
const m = PARENLESS.exec(ref.referenceName);
|
||||
if (!m) return null;
|
||||
const mode = m[1] as Mode;
|
||||
const path = m[3]!.split('.');
|
||||
// Most such names read data, often the framework's: a name no Pascal
|
||||
// routine has is settled here, before any lookup in scope.
|
||||
if (routineNames(context)?.has(path[path.length - 1]!.toLowerCase()) === false) return null;
|
||||
let cls: Node | null = null;
|
||||
if (m[2]) {
|
||||
cls = classByQualifiedName(m[2], ref.filePath!, context);
|
||||
} else {
|
||||
const routine = context.getNodeById?.(ref.fromNodeId);
|
||||
if (routine) cls = classOfRoutine(routine, context);
|
||||
}
|
||||
const found = path.length === 1 ? nameInScope(path[0]!, cls, ref, context) : memberThroughPath(path, cls, ref, context);
|
||||
if (!found) return null;
|
||||
// Written without arguments, a routine that needs some can only be a
|
||||
// method pointer — save a constructor, whose name then means an inherited
|
||||
// one outside the project (`TObject.Create`) — and so is a procedure read
|
||||
// for a value, and an event's method; any other runs.
|
||||
const needsArgs = needsArguments(found.node);
|
||||
if (needsArgs && routineKeyword(found.node, context) === 'constructor') return null;
|
||||
const pointer = mode === 'ref' || needsArgs || (mode !== 'call' && !yieldsValue(found.node, context));
|
||||
// A statement runs its routine, and a receiver is a routine's value.
|
||||
if (pointer && (mode === 'call' || mode === 'recv')) return null;
|
||||
const edgeKind = pointer ? 'references' : 'calls';
|
||||
context.rememberNode?.(found.node);
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: found.node.id,
|
||||
confidence: found.confidence,
|
||||
resolvedBy: found.resolvedBy,
|
||||
edgeKind,
|
||||
// A method pointer is a function used as a value, as `@Handler` is (#756).
|
||||
...(edgeKind === 'references' ? { metadata: { fnRef: true } } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scope lookup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isRoutine(node: Node): boolean {
|
||||
return node.kind === 'method' || node.kind === 'function';
|
||||
}
|
||||
|
||||
/** A bare name, looked up the way Delphi looks it up from a routine of class `cls` (null: a free routine). */
|
||||
function nameInScope(name: string, cls: Node | null, ref: UnresolvedRef, context: ResolutionContext): Found | null {
|
||||
const lower = name.toLowerCase();
|
||||
const memo = memos(context).inScope;
|
||||
const key = `${cls?.id ?? ''}|${ref.filePath}|${lower}`;
|
||||
if (memo.has(key)) return memo.get(key)!;
|
||||
let found: Found | null = null;
|
||||
const member = cls ? memberInHierarchy(cls, lower, context) : null;
|
||||
if (member) {
|
||||
if (isRoutine(member)) found = { node: member, resolvedBy: 'instance-method', confidence: 0.9 };
|
||||
} else {
|
||||
const own = unitDeclaration(ref.filePath!, lower, context);
|
||||
if (own) {
|
||||
if (isRoutine(own)) found = { node: own, resolvedBy: 'exact-match', confidence: 0.9 };
|
||||
} else {
|
||||
const used = usedUnitFiles(ref.filePath!, context);
|
||||
for (let i = used.length - 1; i >= 0; i--) {
|
||||
const decl = unitDeclaration(used[i]!, lower, context);
|
||||
if (!decl) continue;
|
||||
if (isRoutine(decl)) found = { node: decl, resolvedBy: 'import', confidence: 0.85 };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
memo.set(key, found);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** A dotted name: the method its receiver's declared type has. */
|
||||
function memberThroughPath(path: string[], cls: Node | null, ref: UnresolvedRef, context: ResolutionContext): Found | null {
|
||||
const head = headOf(path[0]!, cls, ref, context);
|
||||
if (!head) return null;
|
||||
let type: Node | null;
|
||||
let rest = path.slice(1);
|
||||
if (head.unitFile) {
|
||||
// `UnitName.Routine`, `UnitName.GlobalVar.Method`.
|
||||
const decl = unitDeclaration(head.unitFile, rest[0]!.toLowerCase(), context);
|
||||
if (!decl) return null;
|
||||
if (rest.length === 1) return isRoutine(decl) ? { node: decl, resolvedBy: 'import', confidence: 0.9 } : null;
|
||||
type = typeOfMember(decl, context);
|
||||
rest = rest.slice(1);
|
||||
} else {
|
||||
type = head.type;
|
||||
}
|
||||
for (const link of rest.slice(0, -1)) {
|
||||
const member = type ? memberInHierarchy(type, link.toLowerCase(), context) : null;
|
||||
if (!member) return null;
|
||||
type = typeOfMember(member, context);
|
||||
}
|
||||
const member = type ? memberInHierarchy(type, rest[rest.length - 1]!.toLowerCase(), context) : null;
|
||||
return member && isRoutine(member) ? { node: member, resolvedBy: 'instance-method', confidence: 0.9 } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a dotted name's first link is: a value of a known class (a local's
|
||||
* declared type, written `:TFoo`), a class itself, or a unit.
|
||||
*/
|
||||
function headOf(name: string, cls: Node | null, ref: UnresolvedRef, context: ResolutionContext): { type: Node | null; unitFile?: string } | null {
|
||||
if (name.startsWith(':')) return { type: classNamed(name.slice(1), ref.filePath!, context) };
|
||||
const lower = name.toLowerCase();
|
||||
if (lower === 'self') return cls ? { type: cls } : null;
|
||||
if (cls) {
|
||||
const member = memberInHierarchy(cls, lower, context);
|
||||
if (member) return { type: typeOfMember(member, context) };
|
||||
}
|
||||
const decl = unitDeclaration(ref.filePath!, lower, context) ??
|
||||
usedUnitFiles(ref.filePath!, context).reduceRight<Node | undefined>((hit, file) => hit ?? unitDeclaration(file, lower, context), undefined);
|
||||
if (decl) return { type: CLASS_KINDS.has(decl.kind) ? decl : typeOfMember(decl, context) };
|
||||
const unitFile = unitFileNamed(lower, ref.filePath!, context);
|
||||
return unitFile ? { type: null, unitFile } : null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Classes, units and declarations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface Memos {
|
||||
routineNames?: Set<string> | null;
|
||||
named: Map<string, Node[]>;
|
||||
inScope: Map<string, Found | null>;
|
||||
classOfRoutine: Map<string, Node | null>;
|
||||
classByQn: Map<string, Node | null>;
|
||||
ancestors: Map<string, Node[]>;
|
||||
usedFiles: Map<string, string[]>;
|
||||
classByName: Map<string, Node | null>;
|
||||
unitsByName?: Map<string, string[]>;
|
||||
unitFiles: Map<string, string | null>;
|
||||
hierarchy: Map<string, Node | null>;
|
||||
memberTypes: Map<string, Node | null>;
|
||||
keywords: Map<string, string | null>;
|
||||
}
|
||||
const MEMOS = new WeakMap<ResolutionContext, Memos>();
|
||||
|
||||
function memos(context: ResolutionContext): Memos {
|
||||
let m = MEMOS.get(context);
|
||||
if (!m) {
|
||||
m = {
|
||||
named: new Map(), inScope: new Map(), classOfRoutine: new Map(), classByQn: new Map(),
|
||||
ancestors: new Map(), usedFiles: new Map(), classByName: new Map(), unitFiles: new Map(), hierarchy: new Map(),
|
||||
memberTypes: new Map(), keywords: new Map(),
|
||||
};
|
||||
MEMOS.set(context, m);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
/** Every Pascal method's and function's name, lowercased, or null when the context can't list them. */
|
||||
function routineNames(context: ResolutionContext): Set<string> | null {
|
||||
const memo = memos(context);
|
||||
if (memo.routineNames === undefined) {
|
||||
const names = context.getNodeNamesOfKinds?.('pascal', ['method', 'function']);
|
||||
memo.routineNames = names ? new Set(names.map((n) => n.toLowerCase())) : null;
|
||||
}
|
||||
return memo.routineNames;
|
||||
}
|
||||
|
||||
/** The Pascal nodes a name names, whatever their case. */
|
||||
function pascalNamed(lower: string, context: ResolutionContext): Node[] {
|
||||
const memo = memos(context).named;
|
||||
let nodes = memo.get(lower);
|
||||
if (!nodes) {
|
||||
nodes = context.getNodesByLowerName(lower).filter((n) => n.language === 'pascal');
|
||||
memo.set(lower, nodes);
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/** Drops this module's per-context memos (the resolver clears them between runs). */
|
||||
export function clearPascalScopeMemos(context: ResolutionContext): void {
|
||||
MEMOS.delete(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* The class (or record) an implementation header names (`TOuter::TInner`,
|
||||
* generic parameters dropped): the one `filePath` declares, whatever its case
|
||||
* or parameters, else one the file sees.
|
||||
*/
|
||||
function classByQualifiedName(qualifiedName: string, filePath: string, context: ResolutionContext): Node | null {
|
||||
const memo = memos(context).classByQn;
|
||||
const key = `${filePath}|${qualifiedName}`;
|
||||
if (memo.has(key)) return memo.get(key)!;
|
||||
const exact = context.getNodesByQualifiedName(qualifiedName).filter((n) => n.language === 'pascal' && CLASS_KINDS.has(n.kind));
|
||||
const lower = qualifiedName.toLowerCase();
|
||||
const cls = declarationOf(exact.filter((n) => n.filePath === filePath)) ??
|
||||
declarationOf(context.getNodesInFile(filePath).filter((n) =>
|
||||
CLASS_KINDS.has(n.kind) && n.qualifiedName.replace(/<[^>]*>/g, '').toLowerCase() === lower)) ??
|
||||
declarationOf(exact) ??
|
||||
classNamed(qualifiedName.split('::').pop()!, filePath, context);
|
||||
memo.set(key, cls);
|
||||
return cls;
|
||||
}
|
||||
|
||||
/**
|
||||
* The class declaration among same-named nodes, not a forward declaration
|
||||
* (`TFoo = class;`, which names no members or ancestors): the longest one.
|
||||
*/
|
||||
function declarationOf(classes: Node[]): Node | null {
|
||||
let best: Node | null = null;
|
||||
for (const c of classes) if (!best || c.endLine - c.startLine > best.endLine - best.startLine) best = c;
|
||||
return best;
|
||||
}
|
||||
|
||||
/** The class a method belongs to: its qualified name's owner, declared in the method's own file. */
|
||||
function classOfRoutine(routine: Node, context: ResolutionContext): Node | null {
|
||||
if (routine.kind !== 'method') return null;
|
||||
const memo = memos(context).classOfRoutine;
|
||||
if (memo.has(routine.id)) return memo.get(routine.id)!;
|
||||
const owner = routine.qualifiedName.slice(0, Math.max(0, routine.qualifiedName.lastIndexOf('::')));
|
||||
const candidates = owner ? context.getNodesByQualifiedName(owner).filter((n) => n.language === 'pascal' && CLASS_KINDS.has(n.kind)) : [];
|
||||
const cls = declarationOf(candidates.filter((n) => n.filePath === routine.filePath)) ?? declarationOf(candidates);
|
||||
memo.set(routine.id, cls);
|
||||
return cls;
|
||||
}
|
||||
|
||||
/** A class and its ancestors the project declares, nearest first. */
|
||||
function ancestorsOf(cls: Node, context: ResolutionContext): Node[] {
|
||||
const memo = memos(context).ancestors;
|
||||
const hit = memo.get(cls.id);
|
||||
if (hit) return hit;
|
||||
const order: Node[] = [];
|
||||
const seen = new Set<string>();
|
||||
let frontier = [cls];
|
||||
for (let depth = 0; depth < MAX_HIERARCHY_DEPTH && frontier.length > 0; depth++) {
|
||||
const next: Node[] = [];
|
||||
for (const c of frontier) {
|
||||
if (seen.has(c.id)) continue;
|
||||
seen.add(c.id);
|
||||
order.push(c);
|
||||
for (const name of declaredSupertypes(c, context)) {
|
||||
const sup = classNamed(name, c.filePath, context);
|
||||
if (sup) next.push(sup);
|
||||
}
|
||||
}
|
||||
frontier = next;
|
||||
}
|
||||
memo.set(cls.id, order);
|
||||
return order;
|
||||
}
|
||||
|
||||
/** The qualified name of what declares a node (`TFoo::Bar` → `tfoo`), lowercased. */
|
||||
function ownerOf(node: Node): string {
|
||||
return node.qualifiedName.slice(0, Math.max(0, node.qualifiedName.lastIndexOf('::'))).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* The member a name reaches in a class or, failing that, in its ancestors,
|
||||
* nearest first; a parameterless overload before the others. Null when no
|
||||
* class of the hierarchy declares it.
|
||||
*/
|
||||
function memberInHierarchy(cls: Node, lower: string, context: ResolutionContext): Node | null {
|
||||
const memo = memos(context).hierarchy;
|
||||
const key = `${cls.id}|${lower}`;
|
||||
if (memo.has(key)) return memo.get(key)!;
|
||||
let found: Node | null = null;
|
||||
const candidates = pascalNamed(lower, context).filter((n) => MEMBER_KINDS.has(n.kind));
|
||||
if (candidates.length > 0) {
|
||||
for (const c of ancestorsOf(cls, context)) {
|
||||
const owner = c.qualifiedName.toLowerCase();
|
||||
const members = candidates.filter((n) => n.filePath === c.filePath && ownerOf(n) === owner);
|
||||
if (members.length > 0) {
|
||||
found = members.find((n) => !needsArguments(n)) ?? members[0]!;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
memo.set(key, found);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** A unit's top-level declaration of a name (its enum values included). */
|
||||
function unitDeclaration(filePath: string, lower: string, context: ResolutionContext): Node | undefined {
|
||||
const decls = pascalNamed(lower, context).filter((n) =>
|
||||
n.filePath === filePath && UNIT_KINDS.has(n.kind) && (n.kind === 'enum_member' || !n.qualifiedName.includes('::')));
|
||||
// A routine declared in the interface and defined in the implementation is
|
||||
// one name; of overloads, one called without arguments first.
|
||||
return decls.find((n) => !isRoutine(n)) ?? decls.find((n) => !needsArguments(n)) ?? decls[0];
|
||||
}
|
||||
|
||||
/** The files of the units a file uses, in the order its `uses` clauses name them. */
|
||||
function usedUnitFiles(filePath: string, context: ResolutionContext): string[] {
|
||||
const memo = memos(context).usedFiles;
|
||||
const hit = memo.get(filePath);
|
||||
if (hit) return hit;
|
||||
const files: string[] = [];
|
||||
const imports = context.getNodesInFile(filePath)
|
||||
.filter((n) => n.kind === 'import' && n.language === 'pascal')
|
||||
.sort((a, b) => a.startLine - b.startLine || a.startColumn - b.startColumn);
|
||||
for (const imp of imports) {
|
||||
const file = unitFileNamed(imp.name.toLowerCase(), filePath, context);
|
||||
if (file && file !== filePath && !files.includes(file)) files.push(file);
|
||||
}
|
||||
memo.set(filePath, files);
|
||||
return files;
|
||||
}
|
||||
|
||||
/** The file of the unit a name names, nearest to `fromFile` when several units share it. */
|
||||
function unitFileNamed(lower: string, fromFile: string, context: ResolutionContext): string | null {
|
||||
const m = memos(context);
|
||||
const key = `${fromFile}|${lower}`;
|
||||
if (m.unitFiles.has(key)) return m.unitFiles.get(key)!;
|
||||
if (!m.unitsByName) {
|
||||
m.unitsByName = new Map();
|
||||
for (const n of context.getNodesByKind('module')) {
|
||||
if (n.language !== 'pascal') continue;
|
||||
const name = n.name.toLowerCase();
|
||||
const files = m.unitsByName.get(name);
|
||||
if (files) files.push(n.filePath);
|
||||
else m.unitsByName.set(name, [n.filePath]);
|
||||
}
|
||||
}
|
||||
const file = nearest(m.unitsByName.get(lower) ?? [], fromFile);
|
||||
m.unitFiles.set(key, file);
|
||||
return file;
|
||||
}
|
||||
|
||||
/** A class (or record or interface) a type name names, seen from `fromFile`. */
|
||||
function classNamed(typeName: string, fromFile: string, context: ResolutionContext): Node | null {
|
||||
const simple = typeName.split('.').pop()!.toLowerCase();
|
||||
const memo = memos(context).classByName;
|
||||
const key = `${fromFile}|${simple}`;
|
||||
if (memo.has(key)) return memo.get(key)!;
|
||||
const candidates = pascalNamed(simple, context).filter((n) => CLASS_KINDS.has(n.kind));
|
||||
let cls = declarationOf(candidates.filter((n) => n.filePath === fromFile));
|
||||
if (!cls && candidates.length > 0) {
|
||||
// The unit used last declares the name Delphi sees; else the one unit that does.
|
||||
const used = usedUnitFiles(fromFile, context);
|
||||
for (let i = used.length - 1; i >= 0 && !cls; i--) cls = declarationOf(candidates.filter((n) => n.filePath === used[i]));
|
||||
if (!cls && candidates.every((n) => n.filePath === candidates[0]!.filePath)) cls = declarationOf(candidates);
|
||||
}
|
||||
memo.set(key, cls);
|
||||
return cls;
|
||||
}
|
||||
|
||||
/** The file whose path shares the longest directory prefix with `from`. */
|
||||
function nearest(files: string[], from: string): string | null {
|
||||
if (files.length <= 1) return files[0] ?? null;
|
||||
const dirs = from.split('/');
|
||||
let best: string | null = null;
|
||||
let bestScore = -1;
|
||||
for (const f of files) {
|
||||
const parts = f.split('/');
|
||||
let score = 0;
|
||||
while (score < parts.length && score < dirs.length && parts[score] === dirs[score]) score++;
|
||||
if (score > bestScore) {
|
||||
best = f;
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Declared types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function fileLines(filePath: string, context: ResolutionContext): string[] {
|
||||
return context.getFileLines?.(filePath) ?? context.readFile(filePath)?.split(/\r?\n/) ?? [];
|
||||
}
|
||||
|
||||
function escapeRegExp(s: string): string {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/** The class a member's value has: a field's or variable's declared type, a property's, a function's result. */
|
||||
function typeOfMember(member: Node, context: ResolutionContext): Node | null {
|
||||
const memo = memos(context).memberTypes;
|
||||
if (memo.has(member.id)) return memo.get(member.id)!;
|
||||
let typeName: string | undefined;
|
||||
if (member.kind === 'method' || member.kind === 'function') {
|
||||
typeName = member.returnType ?? /:\s*([A-Za-z_][\w.]*)\s*;?\s*$/.exec(member.signature ?? '')?.[1];
|
||||
} else {
|
||||
const line = fileLines(member.filePath, context)[member.startLine - 1] ?? '';
|
||||
const name = escapeRegExp(member.name);
|
||||
const re = member.kind === 'property'
|
||||
? new RegExp(`\\bproperty\\s+${name}\\s*(?:\\[[^\\]]*\\])?\\s*:\\s*([A-Za-z_][\\w.]*)`, 'i')
|
||||
: new RegExp(`\\b${name}\\b(?:\\s*,\\s*[A-Za-z_]\\w*)*\\s*:\\s*([A-Za-z_][\\w.]*)`, 'i');
|
||||
typeName = re.exec(line)?.[1];
|
||||
}
|
||||
const type = typeName ? classNamed(typeName, member.filePath, context) : null;
|
||||
memo.set(member.id, type);
|
||||
return type;
|
||||
}
|
||||
|
||||
/** Whether a routine has a parameter without a default value, which a call must pass. */
|
||||
function needsArguments(routine: Node): boolean {
|
||||
const signature = routine.signature?.trim();
|
||||
if (!signature?.startsWith('(')) return false;
|
||||
let depth = 0;
|
||||
let end = signature.length;
|
||||
for (let i = 0; i < signature.length; i++) {
|
||||
if (signature[i] === '(') depth++;
|
||||
else if (signature[i] === ')' && --depth === 0) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return signature.slice(1, end).split(';').some((param) => param.trim() !== '' && !param.includes('='));
|
||||
}
|
||||
|
||||
/** Whether a routine gives a value — a function or a constructor — rather than a procedure. */
|
||||
function yieldsValue(routine: Node, context: ResolutionContext): boolean {
|
||||
if (routine.returnType) return true;
|
||||
const keyword = routineKeyword(routine, context);
|
||||
return keyword === 'function' || keyword === 'constructor';
|
||||
}
|
||||
|
||||
/** The keyword that declares a routine (`function`, `procedure`, `constructor`, …), lowercased, or null. */
|
||||
function routineKeyword(routine: Node, context: ResolutionContext): string | null {
|
||||
const memo = memos(context).keywords;
|
||||
const hit = memo.get(routine.id);
|
||||
if (hit !== undefined) return hit;
|
||||
const line = fileLines(routine.filePath, context)[routine.startLine - 1] ?? '';
|
||||
const keyword = /\b(function|procedure|constructor|destructor|operator)\s/i.exec(line)?.[1]!.toLowerCase() ?? null;
|
||||
memo.set(routine.id, keyword);
|
||||
return keyword;
|
||||
}
|
||||
@@ -146,6 +146,17 @@ export interface ResolutionContext {
|
||||
getAllFiles(): string[];
|
||||
/** Get nodes by lowercase name (O(1) lookup for fuzzy matching) */
|
||||
getNodesByLowerName(lowerName: string): Node[];
|
||||
/**
|
||||
* The names of a language's nodes of the given kinds, nothing else decoded:
|
||||
* a cheap pre-filter before lookups by name. Optional for external/test
|
||||
* contexts.
|
||||
*/
|
||||
getNodeNamesOfKinds?(language: Language, kinds: readonly Node['kind'][]): string[];
|
||||
/**
|
||||
* Hands the resolver a node a resolution found by other means, so the gates
|
||||
* after it don't fetch it again by id. Optional for external/test contexts.
|
||||
*/
|
||||
rememberNode?(node: Node): void;
|
||||
/**
|
||||
* Direct supertypes of the type named `typeName` (same language): the classes
|
||||
* it extends and the interfaces / protocols / traits it implements/conforms to,
|
||||
|
||||
Reference in New Issue
Block a user