Files
codegraph/__tests__/pascal-reference-boundaries.test.ts
T
Colby MchenryandClaude Opus 5.5 063708ba53 fix(pascal): Delphi chained calls and method pointers follow declared types (#3065)
Four Delphi bugs that PR 2550 by @OleksiyPenkov found, reimplemented on
main's Pascal scope code rather than as a second resolver:

- A call through a chain of members and methods recorded only the outer
  method's bare name, which name matching gave to any namesake: SynEdit's
  Canvas.Font.Assign(Font) went to TProposalColumn::Assign, and
  TLogger2.Create.Write('y') linked nothing, a bare Write being a System
  name. The extractor now writes such a call as the typed chain
  pascal-scope already resolves for paren-less names ({call} for a
  statement, a new {args} mode for a last link written with its
  arguments). Each link is typed by its declaration: field and property
  types, function results, a local's declared type, through pointer and
  class reference types (PVirtualNode = ^TVirtualNode, TSynLogClass =
  class of TSynLog) and explicit dereferences (a^.Copy). A constructor
  called on a class gives an instance of it, a unit name, dotted ones
  too, qualifies its declarations, and a chain called with arguments may
  end at data that holds a routine. There is no bare fallback: a chain
  through a type from outside the project links nothing, and in a with
  body only a chain from Self or a typed local is read.
- A method given to a target was a method pointer only when the target
  was named On.../FOn...: Callback := FProvider.Value with Callback: TGet
  recorded a call, and FOnGet := @FProvider.Value nothing. A value given
  to an assignment now carries its target, whose declared type decides
  (routine types written out, of object, reference to, project aliases,
  the RTL's routine type names; a class, a record or a System value type
  holds no routine); the name decides only where the declaration can't
  be read. A function that returns a routine type is called for it, and
  @Obj.Method is a reference anywhere.
- A nested routine's locals leaked into the routine around it: the name
  matcher's Pascal receiver scan read every declaration above the call.
  It now steps over routines nested elsewhere, reads the calling
  routine's own name lists (Doc, Doc2: TDocVariantData), which main typed
  only by luck through a nested routine's parameter, and follows pointer
  and class reference types. Where the tree has errors, the source-text
  scopes give the outer routine its own names back after a nested one.
- A nested routine declared forward is in scope from that declaration.

Validation:
- New __tests__/pascal-reference-boundaries.test.ts, 41 tests, 24 of
  them ported from PR 2550: 22 fail on main's sources, the other 19 pin
  what main already got right. pascal-chained-calls and pascal-for-in-var
  pinned the bare outer-method names and now expect the typed chains
  (4 tests, failing on main). Twenty mutations, one per rule, each fail
  a test.
- A/B against main 80f6b505, both arms built from their own sources and
  diffed by natural keys, edges removed/added: SynEdit 195/66, HeidiSQL
  789/320, mORMot2 2553/4164, DelphiMVCFramework 2394/1850, DWScript
  without its Test folder of scripts (519 files) 864/801. leveldb and
  SDWebImage byte-identical.
- Removed edges, hand-triaged: SynEdit all 195, 193 namesakes or parse
  artifacts and 2 right ones lost (Get(i).LoadFromStream in a with
  body); samples of HeidiSQL 20/20, DelphiMVCFramework 30/30, DWScript
  12/12 and mORMot2 23/25 namesakes (of the other two, one now links the
  unit's own CompareMem, and one right edge is lost, through a field
  declared after the first name of a name list, which the extractor
  gives no node).
- Added edges sampled right: typed chains 186 of 187 (the wrong one
  reaches the FMX variant of a conditional ancestor), receiver calls
  40/40, method pointers, @-references and kind changes 57/57.
- CPU, median of 3 interleaved: SynEdit +6.6%, HeidiSQL +0.6%, mORMot2
  -2.5%, DWScript +4.7%, while the identical controls moved 0.0% and
  -5.9%. DelphiMVCFramework, timed before the scan's memo was split per
  routine (which only made it cheaper): -6.6%.

Reimplements part of PR 2550 by @OleksiyPenkov.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:29:03 +00:00

1007 lines
30 KiB
TypeScript

/**
* Where a Delphi call or method pointer stops being what its spelling says
* (#2550): a call through a chain of members and functions
* (`FPanel.ActiveSeries.BeginUpdate;`, `TLogger.Create.Write('x')`) is a
* method of what the chain's links are declared to be, never a namesake; a
* method given to a field, a local or a property whose declared type is a
* routine type is a method pointer whatever the target is called, and so is
* `@Obj.Method`; a routine nested in another keeps its locals to itself; and
* a nested routine declared `forward` is in scope from that declaration.
*
* The cases in the first two blocks are ported from @OleksiyPenkov's tests
* in #2550, which found these bugs.
*/
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { CodeGraph } from '../src';
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
beforeAll(async () => {
await initGrammars();
await loadGrammarsForLanguages(['pascal']);
});
let directory: string;
let graph: CodeGraph | undefined;
beforeEach(() => {
directory = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-pascal-boundaries-'));
});
afterEach(() => {
graph?.close();
graph = undefined;
fs.rmSync(directory, { recursive: true, force: true, maxRetries: 5 });
});
async function index(files: Record<string, string>): Promise<CodeGraph> {
for (const [name, text] of Object.entries(files)) fs.writeFileSync(path.join(directory, name), text);
graph = await CodeGraph.init(directory, { index: true });
return graph;
}
function routine(qualifiedName: string) {
const node = graph!.getNodesByKind('method').concat(graph!.getNodesByKind('function')).find((n) => n.qualifiedName === qualifiedName);
expect(node, qualifiedName).toBeDefined();
return node!;
}
/** The calls and references a routine makes, as `kind:Target`, once each. */
function dependencies(qualifiedName: string): string[] {
return [...new Set(graph!.getOutgoingEdges(routine(qualifiedName).id)
.filter((e) => e.kind === 'calls' || e.kind === 'references')
.map((e) => `${e.kind}:${graph!.getNode(e.target)!.qualifiedName}`))].sort();
}
/** The routines that call a method, by name, once each. */
function callerNamesOf(qualifiedName: string): string[] {
const target = graph!.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
if (!target) return [];
return [...new Set(graph!.getIncomingEdges(target.id).filter((e) => e.kind === 'calls')
.map((e) => graph!.getNode(e.source)?.name).filter((n): n is string => !!n))].sort();
}
describe('Pascal reference boundaries (#2550)', () => {
it('keeps calls to overloaded methods on their declared receiver', async () => {
await index({ 'Main.pas': `unit Main;
interface
type
TLog = class
procedure Add(const S: string); overload;
procedure Add(I: Integer); overload;
end;
implementation
procedure TLog.Add(const S: string); begin end;
procedure TLog.Add(I: Integer); begin end;
procedure Run(Log: TLog); begin Log.Add('x'); end;
end.` });
expect(dependencies('Run')).toEqual(['calls:TLog::Add']);
});
it.each([
['argument constructor', 'constructor Create(I: Integer);', 'constructor TBuilder.Create(I: Integer); begin end;', 'TBuilder.Create(1).Build;'],
['inherited constructor', '', '', 'TBuilder.Create().Build;'],
])('keeps the constructed receiver type for an %s', async (_name, declaration, implementation, call) => {
await index({ 'Main.pas': `unit Main;
interface
type
TBuilder = class(TObject)
${declaration}
procedure Build;
end;
TDecoy = class procedure Build; end;
implementation
${implementation}
procedure TBuilder.Build; begin end;
procedure TDecoy.Build; begin end;
procedure Run; begin ${call} end;
end.` });
expect(dependencies('Run')).toContain('calls:TBuilder::Build');
expect(dependencies('Run')).not.toContain('calls:TDecoy::Build');
});
it.each([
['plain', 'FPanel.Series.GetCount'],
['address-of', '@FPanel.Series.GetCount'],
])('evaluates intermediate receivers in a %s method-pointer assignment', async (_name, rhs) => {
await index({ 'Main.pas': `unit Main;
interface
type
TGet = function: Integer of object;
TSeries = class function GetCount: Integer; end;
TPanel = class function Series: TSeries; end;
TConsumer = class
FPanel: TPanel;
Callback: TGet;
procedure Wire;
end;
implementation
function TSeries.GetCount: Integer; begin Result := 1; end;
function TPanel.Series: TSeries; begin Result := nil; end;
procedure TConsumer.Wire; begin Callback := ${rhs}; end;
end.` });
expect(dependencies('TConsumer::Wire')).toEqual(['calls:TPanel::Series', 'references:TSeries::GetCount']);
});
it('references procedure handlers even when the event type is external', async () => {
await index({ 'Main.pas': `unit Main;
interface
uses System.Classes;
type
TController = class procedure HandleClick(Sender: TObject); end;
TConsumer = class
FController: TController;
Callback: TNotifyEvent;
procedure Wire;
end;
implementation
procedure TController.HandleClick(Sender: TObject); begin end;
procedure TConsumer.Wire; begin Callback := FController.HandleClick; end;
end.` });
expect(dependencies('TConsumer::Wire')).toEqual(['references:TController::HandleClick']);
});
it('calls a factory that returns a callback instead of referencing the factory', async () => {
await index({ 'Main.pas': `unit Main;
interface
type
TGet = function: Integer of object;
TFactory = class function MakeHandler: TGet; end;
TConsumer = class
FFactory: TFactory;
Callback: TGet;
procedure Wire;
end;
implementation
function TFactory.MakeHandler: TGet; begin Result := nil; end;
procedure TConsumer.Wire; begin Callback := FFactory.MakeHandler; end;
end.` });
expect(dependencies('TConsumer::Wire')).toEqual(['calls:TFactory::MakeHandler']);
});
it.each([
['else', 'if Ready then Callback := FProvider.Value else Callback := nil;'],
['end', 'Callback := FProvider.Value'],
])('references a handler before %s without a semicolon', async (_name, body) => {
await index({ 'Main.pas': `unit Main;
interface
type
TGet = function: Integer of object;
TProvider = class function Value: Integer; end;
TConsumer = class
FProvider: TProvider;
Callback: TGet;
Ready: Boolean;
procedure Wire;
end;
implementation
function TProvider.Value: Integer; begin Result := 1; end;
procedure TConsumer.Wire; begin ${body} end;
end.` });
expect(dependencies('TConsumer::Wire')).toEqual(['references:TProvider::Value']);
});
it('resolves chains through a forward-declared class', async () => {
await index({ 'Main.pas': `unit Main;
interface
type
TModel = class;
TPanel = class Model: TModel; end;
TModel = class procedure Recalc; end;
implementation
procedure TModel.Recalc; begin end;
procedure Run(Panel: TPanel); begin Panel.Model.Recalc; end;
end.` });
expect(dependencies('Run')).toEqual(['calls:TModel::Recalc']);
});
it('records a parameterless Self method used as a root receiver', async () => {
await index({ 'Main.pas': `unit Main;
interface
type
TPanel = class procedure Refresh; end;
TConsumer = class
function ActivePanel: TPanel;
procedure Run;
end;
implementation
procedure TPanel.Refresh; begin end;
function TConsumer.ActivePanel: TPanel; begin Result := nil; end;
procedure TConsumer.Run; begin ActivePanel.Refresh; end;
end.` });
expect(dependencies('TConsumer::Run')).toEqual(['calls:TConsumer::ActivePanel', 'calls:TPanel::Refresh']);
});
it('resolves a global receiver imported from another unit', async () => {
await index({
'Globals.pas': `unit Globals;
interface
type
TPanel = class procedure Refresh; end;
TApplication = class Panel: TPanel; end;
var MainForm: TApplication;
implementation
procedure TPanel.Refresh; begin end;
end.`,
'Main.pas': `unit Main;
interface
uses Globals;
implementation
procedure Run; begin MainForm.Panel.Refresh; end;
end.`,
});
expect(dependencies('Run')).toEqual(['calls:TPanel::Refresh']);
});
it('resolves unit-qualified class receivers', async () => {
await index({
'Builders.pas': `unit Builders;
interface
type TBuilder = class procedure Build; end;
implementation
procedure TBuilder.Build; begin end;
end.`,
'Main.pas': `unit Main;
interface
uses Builders;
implementation
procedure Run; begin Builders.TBuilder.Create().Build; end;
end.`,
});
expect(dependencies('Run')).toEqual(['calls:TBuilder::Build']);
});
it('does not use a field type when an inline local shadows that field', async () => {
await index({ 'Main.pas': `unit Main;
interface
type
TField = class procedure Run; end;
TLocal = class procedure Run; end;
TConsumer = class
Receiver: TField;
procedure UseLocal;
end;
implementation
procedure TField.Run; begin end;
procedure TLocal.Run; begin end;
procedure TConsumer.UseLocal;
begin
var Receiver: TLocal;
Receiver.Run;
end;
end.` });
expect(dependencies('TConsumer::UseLocal')).toEqual(['calls:TLocal::Run']);
});
it('keeps typed as-cast receiver calls', async () => {
await index({ 'Main.pas': `unit Main;
interface
type TPanel = class procedure Refresh; end;
implementation
procedure TPanel.Refresh; begin end;
procedure Run(Sender: TObject); begin (Sender as TPanel).Refresh; end;
end.` });
expect(dependencies('Run')).toEqual(['calls:TPanel::Refresh']);
});
it('keeps calls on indexed receivers', async () => {
await index({ 'Main.pas': `unit Main;
interface
type TPanel = class procedure Refresh; end;
implementation
procedure TPanel.Refresh; begin end;
procedure Run; var Panels: array of TPanel; begin Panels[0].Refresh; end;
end.` });
expect(dependencies('Run')).toEqual(['calls:TPanel::Refresh']);
});
it('does not bind an external receiver to an unrelated same-named method', async () => {
await index({ 'Main.pas': `unit Main;
interface
uses System.Classes;
type TDecoy = class procedure Clear; end;
implementation
procedure TDecoy.Clear; begin end;
procedure Run(List: TStringList); begin List.Clear; end;
end.` });
expect(dependencies('Run')).toEqual([]);
});
it.each([
['after nested declaration', 'var Receiver: TLocal;', 'calls:TLocal::Run'],
['nested local does not escape', '', 'calls:TField::Run'],
])('scopes locals when a %s exists', async (_name, locals, expected) => {
await index({ 'Main.pas': `unit Main;
interface
type
TField = class procedure Run; end;
TLocal = class procedure Run; end;
TConsumer = class Receiver: TField; procedure UseLocal; end;
implementation
procedure TField.Run; begin end;
procedure TLocal.Run; begin end;
procedure TConsumer.UseLocal;
procedure Inner;
var Receiver: TLocal;
begin end;
${locals}
begin Receiver.Run; end;
end.` });
expect(dependencies('TConsumer::UseLocal')).toEqual([expected]);
});
it('does not infer field types from unrelated method parameters', async () => {
await index({ 'Main.pas': `unit Main;
interface
type
TGet = function: Integer of object;
TProvider = class function Value: Integer; end;
TDecoy = class function Value: Integer; end;
TConsumer = class
procedure Other(FProvider: TDecoy; Callback: TGet);
FProvider: TProvider;
Callback: Integer;
procedure Evaluate;
end;
implementation
function TProvider.Value: Integer; begin Result := 1; end;
function TDecoy.Value: Integer; begin Result := 2; end;
procedure TConsumer.Other(FProvider: TDecoy; Callback: TGet); begin end;
procedure TConsumer.Evaluate; begin Callback := FProvider.Value; end;
end.` });
expect(dependencies('TConsumer::Evaluate')).toEqual(['calls:TProvider::Value']);
});
it('does not turn a field hiding an inherited method into a call', async () => {
await index({ 'Main.pas': `unit Main;
interface
type
TBase = class function Visible: Boolean; end;
TSeries = class(TBase) Visible: Boolean; end;
TConsumer = class
Series: TSeries;
function Evaluate: Boolean;
end;
implementation
function TBase.Visible: Boolean; begin Result := True; end;
function TConsumer.Evaluate: Boolean; begin Result := Series.Visible; end;
end.` });
expect(dependencies('TConsumer::Evaluate')).toEqual([]);
});
});
describe('Pascal calls without parentheses, from #2550\'s resolution cases', () => {
it('extracts paren-less method calls inside expressions', async () => {
await index({ 'UnitA.pas': `unit UnitA;
interface
type
TProvider = class
public
function Value: Integer;
function ValueParens(): Integer;
public
Count: Integer;
property Total: Integer read Value;
end;
TConsumer = class
private
FProvider: TProvider;
public
function UseNoParens: Integer;
function UseParens: Integer;
function UseFieldRead: Integer;
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;
function TConsumer.UseFieldRead: Integer;
begin
Result := FProvider.Count + FProvider.Total;
end;
end.
` });
expect(callerNamesOf('TProvider::ValueParens')).toEqual(['UseParens']);
// Reading the field `Count` or the property `Total` draws no call.
expect(callerNamesOf('TProvider::Value')).toEqual(['UseNoParens']);
expect(graph!.getOutgoingEdges(routine('TConsumer::UseFieldRead').id).filter((e) => e.kind === 'calls')).toEqual([]);
});
it('records each method evaluated in an instance receiver chain', async () => {
await index({ 'Chain.pas': `unit Chain;
interface
type
TSeries = class
Visible: Boolean;
procedure BeginUpdate;
end;
TPanel = class
function ActiveModelSeries: TSeries;
end;
TConsumer = class
FProjectPanel: TPanel;
procedure UseStatement;
procedure UseExplicit;
function UseCondition: Boolean;
function UseAssignment: TSeries;
end;
TDecoy = class
function Visible: Boolean;
procedure BeginUpdate;
end;
implementation
procedure TSeries.BeginUpdate; begin end;
function TPanel.ActiveModelSeries: TSeries; begin Result := nil; end;
procedure TConsumer.UseStatement;
begin FProjectPanel.ActiveModelSeries.BeginUpdate; end;
procedure TConsumer.UseExplicit;
begin FProjectPanel.ActiveModelSeries().BeginUpdate(); end;
function TConsumer.UseCondition: Boolean;
begin
if FProjectPanel.ActiveModelSeries.Visible then Result := True;
end;
function TConsumer.UseAssignment: TSeries;
begin Result := FProjectPanel.ActiveModelSeries; end;
function TDecoy.Visible: Boolean; begin Result := False; end;
procedure TDecoy.BeginUpdate; begin end;
end.` });
expect(callerNamesOf('TPanel::ActiveModelSeries')).toEqual(['UseAssignment', 'UseCondition', 'UseExplicit', 'UseStatement']);
expect(callerNamesOf('TSeries::BeginUpdate')).toEqual(['UseExplicit', 'UseStatement']);
expect(callerNamesOf('TDecoy::BeginUpdate')).toEqual([]);
expect(callerNamesOf('TDecoy::Visible')).toEqual([]);
});
it('distinguishes method-pointer assignments from evaluated function results', async () => {
await index({
'Provider.pas': `unit Provider;
interface
type
TGetIntEvent = function: Integer of object;
TEventAlias = TGetIntEvent;
TProvider = class
function Value: Integer;
end;
TEvents = class
FOnGet: TEventAlias;
property OnGet: TEventAlias read FOnGet write FOnGet;
end;
implementation
function TProvider.Value: Integer; begin Result := 1; end;
end.`,
'Consumer.pas': `unit Consumer;
interface
uses Provider;
type
TConsumer = class
FProvider: TProvider;
FEvents: TEvents;
FOnGet: TGetIntEvent;
procedure Wire;
function Evaluate: Integer;
property OnGet: TGetIntEvent read FOnGet write FOnGet;
end;
implementation
procedure TConsumer.Wire;
var Callback: TEventAlias;
begin
OnGet :=
FProvider.Value;
FOnGet := FProvider.Value;
FEvents.OnGet := FProvider.Value;
Callback := FProvider.Value;
end;
function TConsumer.Evaluate: Integer;
var Number: Integer;
begin
Number := FProvider.Value;
Result := FProvider.Value;
end;
end.`,
});
const value = routine('TProvider::Value');
const wire = graph!.getOutgoingEdges(routine('TConsumer::Wire').id).filter((e) => e.target === value.id);
const evaluate = graph!.getOutgoingEdges(routine('TConsumer::Evaluate').id).filter((e) => e.target === value.id);
expect(wire.filter((e) => e.kind === 'calls')).toEqual([]);
expect(wire.filter((e) => e.kind === 'references').map((e) => e.line).sort((a, b) => a! - b!)).toEqual([18, 19, 20, 21]);
expect(evaluate.filter((e) => e.kind === 'calls').map((e) => e.line).sort((a, b) => a! - b!)).toEqual([26, 27]);
});
});
/** The edges a routine makes, as `line kind:Target`, in line order. */
function edgesByLine(qualifiedName: string): string[] {
return graph!.getOutgoingEdges(routine(qualifiedName).id)
.filter((e) => e.kind === 'calls' || e.kind === 'references')
.map((e) => `${e.line} ${e.kind}:${graph!.getNode(e.target)!.qualifiedName}`)
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10) || a.localeCompare(b));
}
describe('Delphi chains, method pointers, nested scopes and forward routines (#2550)', () => {
it('calls the method of what a constructor makes, the class\'s own or inherited', async () => {
await index({ 'Wire.pas': `unit Wire;
interface
type
TLogger2 = class procedure Emit(const S: string); procedure Write(const S: string); end;
TBuilder = class constructor Make(N: Integer); procedure Build; end;
TDecoy = class procedure Emit(const S: string); procedure Write(const S: string); procedure Build; end;
TWirer = class procedure CallEmit; end;
implementation
procedure TLogger2.Emit(const S: string); begin end;
procedure TLogger2.Write(const S: string); begin end;
constructor TBuilder.Make(N: Integer); begin end;
procedure TBuilder.Build; begin end;
procedure TDecoy.Emit(const S: string); begin end;
procedure TDecoy.Write(const S: string); begin end;
procedure TDecoy.Build; begin end;
procedure TWirer.CallEmit;
begin
TLogger2.Create.Emit('x');
TLogger2.Create.Write('y');
TBuilder.Make(1).Build;
end;
end.` });
expect(edgesByLine('TWirer::CallEmit')).toEqual([
'18 calls:TLogger2::Emit', '19 calls:TLogger2::Write', '20 calls:TBuilder::Build', '20 calls:TBuilder::Make',
]);
});
it('reads a class its unit\'s name qualifies, through its constructor', async () => {
await index({
'Logger.pas': `unit Logger;
interface
type TLogger = class procedure Write(const S: string); end;
implementation
procedure TLogger.Write(const S: string); begin end;
end.`,
'Scope.pas': `unit Scope;
interface
uses Logger;
implementation
procedure RunLog; begin Logger.TLogger.Create.Write('x'); end;
procedure RunLog2; begin Logger.TLogger.Create().Write('x'); end;
end.`,
});
expect(dependencies('RunLog')).toEqual(['calls:TLogger::Write']);
expect(dependencies('RunLog2')).toEqual(['calls:TLogger::Write']);
});
it('reads a routine a dotted unit name qualifies', async () => {
await index({
'My.Core.Utils.pas': `unit My.Core.Utils;
interface
function Helper(N: Integer): Integer;
procedure Reset;
implementation
function Helper(N: Integer): Integer; begin Result := N; end;
procedure Reset; begin end;
end.`,
'App.pas': `unit App;
interface
uses My.Core.Utils;
implementation
procedure Run;
var X: Integer;
begin
X := My.Core.Utils.Helper(1);
My.Core.Utils.Reset;
end;
end.`,
});
expect(dependencies('Run')).toEqual(['calls:Helper', 'calls:Reset']);
});
it('links no namesake for a chain through a type from outside the project', async () => {
await index({ 'Main.pas': `unit Main;
interface
uses Vcl.ExtCtrls;
type
TDecoy = class procedure Clear; procedure Refresh(N: Integer); end;
TOwner = class
FPanel: TPanel;
procedure Run;
end;
implementation
procedure TDecoy.Clear; begin end;
procedure TDecoy.Refresh(N: Integer); begin end;
procedure TOwner.Run;
begin
FPanel.Controls.Clear;
FPanel.Parent.Refresh(1);
end;
end.` });
expect(dependencies('TOwner::Run')).toEqual([]);
});
it('gives a method pointer to a local of a routine type, named, aliased or written out', async () => {
await index({ 'Wire.pas': `unit Wire;
interface
type
TGet = function: Integer of object;
TGetAlias = TGet;
TProvider = class function Value: Integer; end;
TWirer = class
FProvider: TProvider;
procedure Locals;
end;
implementation
function TProvider.Value: Integer; begin Result := 1; end;
procedure TWirer.Locals;
var
Named: TGet;
Aliased: TGetAlias;
Written: function: Integer of object;
Closure: reference to function: Integer;
Number: Integer;
begin
Named := FProvider.Value;
Aliased := FProvider.Value;
Written := FProvider.Value;
Closure := FProvider.Value;
Number := FProvider.Value;
end;
end.` });
expect(edgesByLine('TWirer::Locals')).toEqual([
'21 references:TProvider::Value', '22 references:TProvider::Value', '23 references:TProvider::Value',
'24 references:TProvider::Value', '25 calls:TProvider::Value',
]);
});
it('takes `@Obj.Method` for a method pointer, whatever it is given to', async () => {
await index({ 'Wire.pas': `unit Wire;
interface
type
TGet = function: Integer of object;
TProvider = class function Value: Integer; end;
TWirer = class
FProvider: TProvider;
Callback: TGet;
FOnGet: TGet;
function Own: Integer;
procedure AtOnNamed;
procedure AtCallback;
procedure AtBareOwn;
end;
implementation
function TProvider.Value: Integer; begin Result := 1; end;
function TWirer.Own: Integer; begin Result := 2; end;
procedure TWirer.AtOnNamed; begin FOnGet := @FProvider.Value; end;
procedure TWirer.AtCallback; begin Callback := @FProvider.Value; end;
procedure TWirer.AtBareOwn; begin Callback := @Own; end;
end.` });
expect(dependencies('TWirer::AtOnNamed')).toEqual(['references:TProvider::Value']);
expect(dependencies('TWirer::AtCallback')).toEqual(['references:TProvider::Value']);
expect(dependencies('TWirer::AtBareOwn')).toEqual(['references:TWirer::Own']);
});
it('calls a function given to a target named like an event that holds data', async () => {
await index({ 'Wire.pas': `unit Wire;
interface
type
TProvider = class function Value: Integer; function IsTop: Boolean; end;
TWirer = class
FProvider: TProvider;
FOnCount: Integer;
FOnTop: Boolean;
procedure Wire;
end;
implementation
function TProvider.Value: Integer; begin Result := 1; end;
function TProvider.IsTop: Boolean; begin Result := True; end;
procedure TWirer.Wire;
begin
FOnCount := FProvider.Value;
FOnTop := FProvider.IsTop;
end;
end.` });
expect(edgesByLine('TWirer::Wire')).toEqual(['16 calls:TProvider::Value', '17 calls:TProvider::IsTop']);
});
it('keeps a nested routine\'s locals to it, and the routine around it its own', async () => {
await index({ 'Scope.pas': `unit Scope;
interface
type
TField = class procedure Run; end;
TLocal = class procedure Run; end;
TScoped = class Receiver: TField; procedure UseLocalNested; end;
implementation
procedure TField.Run; begin end;
procedure TLocal.Run; begin end;
procedure TScoped.UseLocalNested;
procedure Inner;
var Receiver: TLocal;
begin Receiver.Run; end;
begin Receiver.Run; end;
end.` });
expect(dependencies('TScoped::UseLocalNested::Inner')).toEqual(['calls:TLocal::Run']);
expect(dependencies('TScoped::UseLocalNested')).toEqual(['calls:TField::Run']);
});
it('types a receiver by the routine\'s own name list, not a nested routine\'s parameter', async () => {
await index({ 'Docs.pas': `unit Docs;
interface
type
TDocData = class procedure Clear; end;
TOther = class procedure Clear; end;
TItem = class procedure Clear; end;
TTests = class procedure Run; end;
implementation
procedure TDocData.Clear; begin end;
procedure TOther.Clear; begin end;
procedure TItem.Clear; begin end;
procedure TTests.Run;
procedure Check(var Item: TOther);
begin
Item.Clear;
end;
var
Item, Spare: TDocData;
begin
Item.Clear;
Spare.Clear;
end;
end.` });
expect(dependencies('TTests::Run::Check')).toEqual(['calls:TOther::Clear']);
expect(edgesByLine('TTests::Run')).toEqual(['20 calls:TDocData::Clear', '21 calls:TDocData::Clear']);
});
it('calls a class method through a class reference type', async () => {
await index({ 'Meta.pas': `unit Meta;
interface
type
TFoo = class
class function Make(N: Integer): TFoo;
end;
TFooClass = class of TFoo;
TOther = class
class function Make(N: Integer): TOther;
end;
procedure Run(Cls: TFooClass);
implementation
class function TFoo.Make(N: Integer): TFoo; begin Result := nil; end;
class function TOther.Make(N: Integer): TOther; begin Result := nil; end;
procedure Run(Cls: TFooClass);
begin
Cls.Make(1);
end;
end.` });
expect(dependencies('Run')).toEqual(['calls:TFoo::Make']);
});
it('reads no other routine\'s name list for a receiver', async () => {
await index({ 'Mongo.pas': `unit Mongo;
interface
type
RawUtf8 = type AnsiString;
TMongoDatabase = class
procedure RunCommand(const Cmd: string);
end;
TMongoRequest = class
constructor Create(const Database, Collection: RawUtf8);
end;
TMongoCollection = class
function GetDatabase: TMongoDatabase;
property Database: TMongoDatabase read GetDatabase;
procedure Aggregate;
end;
implementation
procedure TMongoDatabase.RunCommand(const Cmd: string); begin end;
constructor TMongoRequest.Create(const Database, Collection: RawUtf8); begin end;
function TMongoCollection.GetDatabase: TMongoDatabase; begin Result := nil; end;
procedure TMongoCollection.Aggregate;
begin
Database.RunCommand('x');
end;
end.` });
expect(dependencies('TMongoCollection::Aggregate')).toEqual(['calls:TMongoDatabase::RunCommand']);
});
it('calls the record\'s method through a pointer to it, declared alone or in a name list', async () => {
await index({ 'Ptr.pas': `unit Ptr;
interface
type
PBigInt = ^TBigInt;
TBigInt = record
procedure RightShift(N: Integer);
end;
TOther = class
procedure RightShift(N: Integer);
end;
procedure Single(q: PBigInt);
procedure Listed;
implementation
procedure TBigInt.RightShift(N: Integer); begin end;
procedure TOther.RightShift(N: Integer); begin end;
procedure Single(q: PBigInt);
begin
q.RightShift(2);
end;
procedure Listed;
var
q, r1: PBigInt;
begin
q.RightShift(2);
end;
end.` });
expect(dependencies('Single')).toEqual(['calls:TBigInt::RightShift']);
expect(dependencies('Listed')).toEqual(['calls:TBigInt::RightShift']);
});
it('reads a chain through a class reference type and an explicit dereference', async () => {
await index({ 'Logs.pas': `unit Logs;
interface
type
TSynLog = class
class function Add: TSynLog;
procedure Log(const Msg: string);
end;
TSynLogClass = class of TSynLog;
ISynLogLike = interface
procedure Log(const Msg: string);
end;
PBigInt = ^TBigInt;
TBigInt = record
function Copy: PBigInt;
function MultiplyLow(Other: PBigInt): PBigInt;
end;
TDecoy = class
function MultiplyLow(Other: Pointer): Pointer;
end;
var
DBLog: TSynLogClass;
procedure Run(a, b: PBigInt);
implementation
class function TSynLog.Add: TSynLog; begin Result := nil; end;
procedure TSynLog.Log(const Msg: string); begin end;
function TBigInt.Copy: PBigInt; begin Result := @Self; end;
function TBigInt.MultiplyLow(Other: PBigInt): PBigInt; begin Result := @Self; end;
function TDecoy.MultiplyLow(Other: Pointer): Pointer; begin Result := nil; end;
procedure Run(a, b: PBigInt);
begin
DBLog.Add.Log('x');
a^.Copy.MultiplyLow(b);
end;
end.` });
const run = dependencies('Run');
expect(run).toEqual(expect.arrayContaining(['calls:TSynLog::Add', 'calls:TSynLog::Log', 'calls:TBigInt::MultiplyLow']));
expect(run).not.toContain('calls:ISynLogLike::Log');
expect(run).not.toContain('calls:TDecoy::MultiplyLow');
});
it('reads a chain in a with body only from a name the routine declares', async () => {
await index({ 'Emf.pas': `unit Emf;
interface
type
TCanvas2 = class procedure Stroke; end;
TEnum = class Canvas: TCanvas2; end;
TDecoy = class procedure Stroke; end;
TRec = record X: Integer; end;
PRec = ^TRec;
THost = class
FEnum: TEnum;
procedure Run(R: PRec; E: TEnum);
end;
implementation
procedure TCanvas2.Stroke; begin end;
procedure TDecoy.Stroke; begin end;
procedure THost.Run(R: PRec; E: TEnum);
begin
with R^ do
begin
E.Canvas.Stroke;
FEnum.Canvas.Stroke;
end;
end;
end.` });
// `FEnum` may be the with object's member, which only its type could say.
expect(edgesByLine('THost::Run')).toEqual(['20 calls:TCanvas2::Stroke']);
});
it('reads a chain through pointer types, which Delphi dereferences', async () => {
await index({ 'Tree.pas': `unit Tree;
interface
type
PNode = ^TNode;
TNode = record
LastChild: PNode;
procedure SetNextSibling(Value: PNode);
end;
TDecoy = class
procedure SetNextSibling(Value: Pointer);
end;
procedure Attach(Node, Child: PNode);
implementation
procedure TNode.SetNextSibling(Value: PNode); begin end;
procedure TDecoy.SetNextSibling(Value: Pointer); begin end;
procedure Attach(Node, Child: PNode);
begin
Node.LastChild.SetNextSibling(Child);
end;
end.` });
expect(dependencies('Attach')).toEqual(['calls:TNode::SetNextSibling']);
});
it('keeps a nested routine\'s locals to it where the unit\'s tree has errors', async () => {
// The old-style `begin … end.` initialization breaks the unit's parse,
// so the routines' names come from the source text.
await index({ 'Brk.pas': `unit Brk;
interface
type
TField = class
function Value: Integer;
end;
TLocal = class
function Value: Integer;
end;
TScoped = class
Receiver: TField;
function UseLocal: Integer;
end;
implementation
function TField.Value: Integer;
begin
Result := 1;
end;
function TLocal.Value: Integer;
begin
Result := 2;
end;
function TScoped.UseLocal: Integer;
procedure Inner;
var
Receiver: TLocal;
begin
Receiver := nil;
end;
var
N: Integer;
begin
N := Receiver.Value;
Result := N;
end;
begin
end.
` });
expect(dependencies('TScoped::UseLocal')).toEqual(['calls:TField::Value']);
});
it('calls a nested routine declared forward above the call', async () => {
await index({ 'Scope.pas': `unit Scope;
interface
type
TScoped = class procedure Fire; end;
implementation
procedure TScoped.Fire;
procedure Late; forward;
function Later(N: Integer;
M: Integer): Integer; forward;
procedure Early;
begin
Late;
Later(1, 2);
end;
procedure Late; begin end;
function Later(N: Integer; M: Integer): Integer; begin Result := N + M; end;
begin
Early;
end;
end.` });
expect(dependencies('TScoped::Fire::Early')).toEqual(['calls:TScoped::Fire::Late', 'calls:TScoped::Fire::Later']);
expect(dependencies('TScoped::Fire')).toEqual(['calls:TScoped::Fire::Early']);
});
});