From f3fdba2955fb2979c4c4f09e61d300f9f2fd277e Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Thu, 22 Oct 2026 17:50:35 +0000 Subject: [PATCH] fix(pascal): calls inside nested Delphi routines are indexed, each under its routine (#2810) A Pascal routine may declare routines of its own between its header and its begin. The extractor walked only a routine's body block, so a nested routine's code was never read: what it called was missing, and a call to it from the routine that declares it went by name to any routine elsewhere with that name (in mORMot2's tests, Test, Check and One calls landed on other classes' methods). A nested routine is now a function node qualified under the routine it is nested in (TFoo::Bar::Inner), contained by it and walked with itself as the caller. It sees the parameters and locals of the routines around it that are declared above it. Its name means it in the routine that declares it, in the routines nested there after it and in its own body, ahead of the class and the unit. A call resolved to a nested routine from anywhere else is what the call's own scope declares, or nothing. A nested routine's bare calls reach the class of the method it is nested in first. Where a routine's tree has errors and its parse swallowed the unit's routines after it (column 0, or after the body), those are walked as the unit's own; of two bodies under {$IFDEF} the first is walked, as before. Validation against origin/main 7b50ca83, nodes and edges removed/added: SynEdit +239, -6/+1127; HeidiSQL +404, -178/+2352; mORMot2 +574, -956/+9565; DelphiMVCFramework +134, -279/+1049; DWScript +434, -346/+3351; leveldb and SDWebImage byte-identical. Sampled: calls to nested routines 38/38 right, calls made in them 44/59 (ordinary name matching's misses), removed edges 30/30 wrong links. Index time within about 5%, in proportion to the code now walked. Covers the nested-routine part of #2550. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/pascal-nested-routines.test.ts | 363 +++++++++++++++++++++++ __tests__/pascal-system-calls.test.ts | 2 +- src/extraction/tree-sitter.ts | 108 ++++--- src/resolution/index.ts | 15 +- src/resolution/pascal-scope.ts | 137 ++++++++- 6 files changed, 580 insertions(+), 46 deletions(-) create mode 100644 __tests__/pascal-nested-routines.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b3e62ce0..cf15b332b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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 Delphi, a call to something the compiler provides, like `SizeOf(X)`, `TypeInfo(TFoo)`, `High(A)`, `Inc(N)`, `Assert(…)` or a typecast like `Pointer(P)`, written in any letter case, no longer shows up as a call to a project method, field or variable that happens to share its name, unless the calling code's own class or units declare one, like a list class's own `Delete`. A call through a field, property or variable now counts only when it holds a routine, like an `OnChange` event or a `reference to function` variable, and calls in `{$I}` include files and into units the parser could only partly read now find the routines those units declare. Thanks @OleksiyPenkov for the report. (#2514) +- In Delphi, a routine declared inside another, between its header and its `begin`, is now indexed as a routine of its own under the one it belongs to (`TFoo.Bar`'s `Inner` is `TFoo::Bar::Inner`), with the calls its body makes. Before, its body was skipped, so what it called was missing from callers and impact, and a call to it from its own routine went to any routine of the same name elsewhere. Thanks @OleksiyPenkov for the pull request this follows. (#2550) - 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 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. diff --git a/__tests__/pascal-nested-routines.test.ts b/__tests__/pascal-nested-routines.test.ts new file mode 100644 index 000000000..7c594db95 --- /dev/null +++ b/__tests__/pascal-nested-routines.test.ts @@ -0,0 +1,363 @@ +/** + * A Pascal routine may declare routines of its own between its header and + * its `begin`. Their bodies were never walked, so whatever they called was + * missing from the graph, and a call to one from the routine that declares + * it went by name to any routine elsewhere that shares it. Now a nested + * routine is a function qualified under the routine it is nested in + * (`THost::Fire::Inner`), its calls are its own, and its name means it + * wherever it is in scope: in the routine that declares it, in the routines + * nested after it there, and in its own body. Its locals and parameters are + * in scope for its body and for the routines nested inside it. + */ +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, check: (cg: CodeGraph) => void): Promise { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-pascal-nested-')); + 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 }); + } +} + +function routine(cg: CodeGraph, qualifiedName: string) { + const node = cg.getNodesByKind('method').concat(cg.getNodesByKind('function')).find((n) => n.qualifiedName === qualifiedName); + expect(node, qualifiedName).toBeDefined(); + return node!; +} + +/** What a routine calls, as `file:Target`, sorted. */ +function callsFrom(cg: CodeGraph, qualifiedName: string): string[] { + return [...new Set(cg.getOutgoingEdges(routine(cg, qualifiedName).id) + .filter((e) => e.kind === 'calls') + .map((e) => cg.getNode(e.target)!) + .map((n) => `${path.basename(n.filePath)}:${n.qualifiedName}`))].sort(); +} + +// The routines elsewhere a nested routine's name could be taken for. +const LIB = `unit Lib; + +interface + +procedure Inner(N: Integer); +procedure Helper(N: Integer); +procedure Log(const S: string); + +implementation + +procedure Inner(N: Integer); begin end; +procedure Helper(N: Integer); begin end; +procedure Log(const S: string); begin end; + +end. +`; + +function host(implementation: string): string { + return `unit Host; + +interface + +uses Lib; + +type + THost = class + public + procedure Read; + procedure Fire; + function Count(O: TObject): Integer; + end; + + TOther = class + public + function Count(O: TObject): Integer; + end; + +implementation + +function THost.Count(O: TObject): Integer; begin Result := 0; end; +procedure THost.Read; begin end; +function TOther.Count(O: TObject): Integer; begin Result := 1; end; + +${implementation} + +end. +`; +} + +describe('Pascal nested routines', () => { + it('indexes a nested routine as a function of its own, called by the routine that declares it', async () => { + const main = host(`procedure THost.Fire; +var + Count: Integer; + + procedure Inner(N: Integer); + var + K: Integer; + begin + K := N + Count; + Helper(K); + end; + +begin + Inner(1); +end;`); + await withIndex({ 'Lib.pas': LIB, 'Host.pas': main }, (cg) => { + const inner = routine(cg, 'THost::Fire::Inner'); + expect(inner.kind).toBe('function'); + const contains = cg.getOutgoingEdges(routine(cg, 'THost::Fire').id).filter((e) => e.kind === 'contains'); + expect(contains.map((e) => e.target)).toContain(inner.id); + // Its call is its own, and the routine's call to `Inner` is to it, not to Lib's. + expect(callsFrom(cg, 'THost::Fire::Inner')).toEqual(['Lib.pas:Helper']); + expect(callsFrom(cg, 'THost::Fire')).toEqual(['Host.pas:THost::Fire::Inner']); + }); + }); + + it('follows calls through nesting levels, siblings above and the routine itself', async () => { + const main = host(`procedure THost.Fire; + + procedure First; + begin + Log('first'); + end; + + procedure Again(N: Integer); + begin + if N > 0 then + Again(N - 1); + First; + end; + + procedure Level1; + + procedure Level2; + begin + Again(2); + Level1; + end; + + begin + Level2; + end; + +begin + Level1; +end;`); + await withIndex({ 'Lib.pas': LIB, 'Host.pas': main }, (cg) => { + expect(callsFrom(cg, 'THost::Fire')).toEqual(['Host.pas:THost::Fire::Level1']); + expect(callsFrom(cg, 'THost::Fire::Level1')).toEqual(['Host.pas:THost::Fire::Level1::Level2']); + // A deeper routine reaches a routine nested further out, and the one it is in. + expect(callsFrom(cg, 'THost::Fire::Level1::Level2')).toEqual([ + 'Host.pas:THost::Fire::Again', + 'Host.pas:THost::Fire::Level1', + ]); + // A routine calls itself, and an earlier sibling, with or without parentheses. + expect(callsFrom(cg, 'THost::Fire::Again')).toEqual(['Host.pas:THost::Fire::Again', 'Host.pas:THost::Fire::First']); + expect(callsFrom(cg, 'THost::Fire::First')).toEqual(['Lib.pas:Log']); + }); + }); + + it('keeps a nested routine to the routine it is nested in', async () => { + const main = host(`procedure THost.Fire; + + procedure Outer; + + procedure Hidden(N: Integer); + begin + end; + + begin + Hidden(1); + end; + + procedure Peer; + begin + Hidden(2); + Helper(3); + end; + +begin + Outer; + Peer; +end; + +procedure Tidy; + + procedure Helper(N: Integer); + begin + end; + +begin + Helper(4); +end;`); + await withIndex({ 'Lib.pas': LIB, 'Host.pas': main }, (cg) => { + expect(callsFrom(cg, 'THost::Fire::Outer')).toEqual(['Host.pas:THost::Fire::Outer::Hidden']); + // Neither another routine's nested `Hidden` nor `Tidy`'s nested `Helper` is in scope here. + expect(callsFrom(cg, 'THost::Fire::Peer')).toEqual(['Lib.pas:Helper']); + expect(callsFrom(cg, 'Tidy')).toEqual(['Host.pas:Tidy::Helper']); + }); + }); + + it('keeps a later sibling out of scope, and a nested routine out of another unit', async () => { + const main = host(`procedure THost.Fire; + + procedure Early; + begin + Inner(1); + end; + + procedure Inner(N: Integer); + begin + end; + +begin + Early; + Inner(2); +end;`); + const other = `unit Other; + +interface + +uses Lib, Host; + +procedure Run; + +implementation + +procedure Run; +begin + Inner(3); +end; + +end. +`; + await withIndex({ 'Lib.pas': LIB, 'Host.pas': main, 'Other.pas': other }, (cg) => { + // Declared after it, the sibling isn't one `Early` can call: Lib's `Inner` is. + expect(callsFrom(cg, 'THost::Fire::Early')).toEqual(['Lib.pas:Inner']); + expect(callsFrom(cg, 'THost::Fire')).toEqual(['Host.pas:THost::Fire::Early', 'Host.pas:THost::Fire::Inner']); + expect(callsFrom(cg, 'Run')).toEqual(['Lib.pas:Inner']); + }); + }); + + it('gives a nested routine the class and the locals of the routines around it', async () => { + const main = host(`procedure THost.Fire; +var + Before: TObject; + + procedure Inner; + begin + Read; + Before.Free; + Helper(Count(Before)); + end; + +var + Log: Integer; + + procedure Second(Helper: Integer); + begin + Log := Helper + 1; + end; + +begin + Inner; + Second(1); +end;`); + await withIndex({ 'Lib.pas': LIB, 'Host.pas': main }, (cg) => { + // The class's `Read` and `Count` are in scope there, not `TOther`'s + // `Count`, and `Before` is the routine's local. + expect(callsFrom(cg, 'THost::Fire::Inner')).toEqual(['Host.pas:THost::Count', 'Host.pas:THost::Read', 'Lib.pas:Helper']); + // A parameter and a local of the routine around it hide Lib's routines. + expect(callsFrom(cg, 'THost::Fire::Second')).toEqual([]); + expect(callsFrom(cg, 'THost::Fire')).toEqual(['Host.pas:THost::Fire::Inner', 'Host.pas:THost::Fire::Second']); + }); + }); + + it('takes a routine a broken parse nests, at the start of its line, for one of the unit', async () => { + // The missing `;` after `procedure First` leaves `Second` inside it in the tree. + const script = `procedure First +begin +end; + +function Second : Integer +var + i : Integer; +begin + i := 1; + Result := Helper(i); +end; + +type + TThing = class + method Run : String; + method Stop; + end; + +method TThing.Run : String +begin + Result := ''; +end; + +method TThing.Stop +const + c = 2; +begin +end; +`; + const lib = `unit Lib; + +interface + +function Helper(N: Integer): Integer; + +implementation + +function Helper(N: Integer): Integer; begin Result := N; end; + +end. +`; + await withIndex({ 'Lib.pas': lib, 'Script.pas': script }, (cg) => { + const names = cg.getNodesByKind('function').filter((n) => n.filePath === 'Script.pas').map((n) => n.qualifiedName); + expect(names).toContain('Second'); + expect(names).not.toContain('First::Second'); + expect(callsFrom(cg, 'Second')).toEqual(['Lib.pas:Helper']); + }); + }); + + it('takes a nested function\'s own name for its result, and a nested name like a System routine\'s for the nested one', async () => { + const main = host(`procedure THost.Fire; + + function Calc: Integer; + begin + Calc := 5; + end; + + procedure Delete(Index: Integer); + begin + end; + +begin + if Calc > 0 then + Delete(1); +end;`); + await withIndex({ 'Lib.pas': LIB, 'Host.pas': main }, (cg) => { + expect(callsFrom(cg, 'THost::Fire::Calc')).toEqual([]); + expect(callsFrom(cg, 'THost::Fire')).toEqual(['Host.pas:THost::Fire::Calc', 'Host.pas:THost::Fire::Delete']); + }); + }); +}); diff --git a/__tests__/pascal-system-calls.test.ts b/__tests__/pascal-system-calls.test.ts index b26e43c24..6714ee974 100644 --- a/__tests__/pascal-system-calls.test.ts +++ b/__tests__/pascal-system-calls.test.ts @@ -378,7 +378,7 @@ end. expect(callsFrom(cg, 'TConn::Connect')).toEqual(['24: TOciLib::HandleAlloc']); // A parameter and a nested routine, not the properties named so. expect(callsFrom(cg, 'Hash')).toEqual([]); - expect(callsFrom(cg, 'Test')).toEqual([]); + expect(callsFrom(cg, 'Test')).toEqual(['39: Test::Direct']); }); }); diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 95c39a0e7..94453c64e 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -8670,61 +8670,103 @@ export class TreeSitterExtractor { if (!parentId) parentId = this.nodeStack[this.nodeStack.length - 1]; if (!parentId) return; - // Visit the block for calls - const block = node.namedChildren.find( - (c: SyntaxNode) => c.type === 'block' - ); - if (block) { - // `TList.Add` belongs to `TList`, whose node may spell its parameters - // with their constraints (`TList`). - 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(); + // `TList.Add` belongs to `TList`, whose node may spell its parameters + // with their constraints (`TList`). + const owner = fullName.includes('.') ? fullName.replace(/<[^>]*>/g, '').split('.').slice(0, -1).join('::') : null; + const routineId = parentId; + // What a routine nested in this one is qualified under: the routine's + // node, or its name as written when the implementation matched none. + const qualifiedName = (): string => { + const routine = this.nodes.find((n) => n.id === routineId); + return routine && routine.kind !== 'file' ? routine.qualifiedName : fullName.replace(/<[^>]*>/g, '').split('.').join('::'); + }; + this.walkPascalRoutine(node, routineId, owner, shortNameKey.replace(/<.*$/, ''), qualifiedName); + } + + /** + * A routine's declarations and body, walked for its calls with the routine + * as their caller. A routine nested in it (`procedure Inner;` between its + * header and `begin`) is a function of its own, qualified under it + * (`TFoo::Bar::Inner`), whose calls are its own; it sees the names the + * routine declares above it, as the routine's body sees them all. + */ + private walkPascalRoutine(node: SyntaxNode, routineId: string, classQn: string | null, routine: string, qualifiedName: () => string): void { + const locals = this.pascalRoutineLocals(node, true); + this.pascalScopes.push({ locals, classQn, routine, startLine: node.startPosition.row + 1 }); + this.nodeStack.push(routineId); + let block: SyntaxNode | null = null; + const misplaced: SyntaxNode[] = []; + const broken = node.hasError; + for (const c of node.namedChildren) { + if (c.type === 'declVars' || c.type === 'declConsts') { + for (const decl of c.namedChildren) this.declarePascalLocal(locals, decl); + } else if (c.type === 'defProc') { + // A nested routine comes before the body. Where the routine's tree + // has errors, its parse can swallow the unit's routines after it, + // which start their lines where it does: those are the unit's. + if (block || (broken && c.startPosition.column <= node.startPosition.column)) misplaced.push(c); + else this.extractPascalNestedRoutine(c, classQn, qualifiedName()); + } else if (c.type === 'block' && !block) { + // The first: `{$IFDEF}`s around two bodies leave a second. + block = c; + } } + if (block) this.visitPascalBlock(block); + this.nodeStack.pop(); + this.pascalScopes.pop(); + for (const c of misplaced) this.extractPascalDefProc(c); + } + + /** A routine nested in another (see walkPascalRoutine), under the qualified name of the one it is nested in. */ + private extractPascalNestedRoutine(node: SyntaxNode, classQn: string | null, outerQualifiedName: string): void { + const declProc = node.namedChildren.find((c: SyntaxNode) => c.type === 'declProc'); + const nameNode = declProc ? getChildByField(declProc, 'name') : null; + if (!declProc || nameNode?.type !== 'identifier') return; + const name = getNodeText(nameNode, this.source); + const qualifiedName = `${outerQualifiedName}::${name}`; + const fn = this.createNode('function', name, node, { + qualifiedName, + signature: this.extractor?.getSignature?.(declProc, this.signatureSource), + }); + if (!fn) return; + this.walkPascalRoutine(node, fn.id, classQn, name.toLowerCase(), () => qualifiedName); } /** * 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 + * parameters, `var`/`const` sections and `Result` (with `headerOnly`, the + * parameters and `Result`; walkPascalRoutine adds the sections in order). + * 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. + * never a call. A nested routine's name is not: it is a routine to call. */ - private pascalRoutineLocals(routine: SyntaxNode): Map { + private pascalRoutineLocals(routine: SyntaxNode, headerOnly = false): Map { const names = new Map(); 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); + if (arg.type === 'declArg') this.declarePascalLocal(names, arg); } + if (headerOnly) return names; 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); + for (const decl of c.namedChildren) this.declarePascalLocal(names, decl); } } return names; } + /** The names a parameter, `var` or `const` declaration declares, with the type it gives them (see pascalRoutineLocals). */ + private declarePascalLocal(names: Map, 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); + } + } + /** * 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 diff --git a/src/resolution/index.ts b/src/resolution/index.ts index b088bef40..6a381191d 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -29,7 +29,8 @@ import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformance import { clearDartLibraryMemos } from './dart-libraries'; import { clearVbnetReceiverMemos, isVbMemberRead, isVbPathCall, matchVbMemberRead, matchVbPathCall } from './vbnet-receivers'; import { - clearPascalScopeMemos, gatePascalDataCall, isPascalParenlessRef, isPascalSystemCall, matchPascalParenlessRef, matchPascalSystemCall, + clearPascalScopeMemos, gatePascalCall, isPascalParenlessRef, isPascalSystemCall, matchPascalNestedCall, + matchPascalParenlessRef, matchPascalSystemCall, } from './pascal-scope'; import { gateTypeParameter, clearTypeParameterMemos } from './type-parameters'; import { gateDartLocal, clearDartLocalScopeMemos } from './dart-local-scope'; @@ -1424,9 +1425,10 @@ export class ReferenceResolver { !reachesAmbientModule(ref, specifier!, declaringFile, this.context)) return null; } if (!resolved || ref.referenceKind !== 'calls') return resolved; - // A Pascal call to data runs only a routine the data holds, in scope (see - // gatePascalDataCall). - const call = ref.language === 'pascal' ? gatePascalDataCall(resolved, ref, this.context) : resolved; + // A Pascal call to data runs only a routine the data holds, in scope, and + // one to a routine nested in another comes only from inside that one (see + // gatePascalCall). + const call = ref.language === 'pascal' ? gatePascalCall(resolved, ref, this.context) : resolved; if (!call) return null; const target = this.nodeById(call.targetNodeId); @@ -1462,6 +1464,11 @@ export class ReferenceResolver { // meaning to (`SizeOf`, `TypeInfo`, `length(S)`, `Pointer(P)`), whatever // its case: only a declaration in the call's scope shadows it. if (isPascalSystemCall(ref, this.context)) return matchPascalSystemCall(ref, this.context); + // A bare Pascal call to a routine nested where it is made (`Inner(1)` in + // the routine that declares `procedure Inner(N: Integer);`) is that one, + // and one made in a nested routine is what its scope declares first. + const nested = matchPascalNestedCall(ref, this.context); + if (nested) return nested; // 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); diff --git a/src/resolution/pascal-scope.ts b/src/resolution/pascal-scope.ts index a128a0c94..9f74e9812 100644 --- a/src/resolution/pascal-scope.ts +++ b/src/resolution/pascal-scope.ts @@ -144,8 +144,11 @@ export function matchPascalSystemCall(ref: UnresolvedRef, context: ResolutionCon } /** - * A Pascal call another strategy resolved to data — a field, a property, a - * variable, a constant — runs what the data holds, so it stands only for data + * What a Pascal call another strategy resolved stands for, when it landed on + * data or on a routine nested in another (see gatePascalNestedTarget). + * + * A call resolved to data — a field, a property, a variable, a constant — + * runs what the data holds, so it stands only for data * whose declared type is a routine's. A bare name is what its scope declares * (`FOnChange(Self)` in the class that declares its own `FOnChange`): the * routine or the routine-holding data there, whichever namesake the strategy @@ -157,9 +160,16 @@ export function matchPascalSystemCall(ref: UnresolvedRef, context: ResolutionCon * or a declaration the parse put in the wrong place: it stands for the one * routine-holding field or variable the project declares by that name. */ -export function gatePascalDataCall(result: ResolvedRef, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { +export function gatePascalCall(result: ResolvedRef, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { const target = context.getNodeById?.(result.targetNodeId); - if (!target || !DATA_KINDS.has(target.kind)) return result; + if (!target) return result; + if (DATA_KINDS.has(target.kind)) return gatePascalDataCall(target, result, ref, context); + if (isNestedRoutine(target, context)) return gatePascalNestedTarget(result, ref, context); + return result; +} + +/** See gatePascalCall: a call resolved to data. */ +function gatePascalDataCall(target: Node, result: ResolvedRef, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { if (!isBareCall(ref, context)) return target.language === 'pascal' && isProceduralData(target, context) ? result : null; const lower = ref.referenceName.toLowerCase(); if (declaredInRoutine(lower, ref, context)) return null; @@ -309,14 +319,121 @@ function nameInScope(name: string, cls: Node | null, ref: UnresolvedRef, context return decl && isRoutine(decl.node) ? decl : null; } +/** + * The routine nested in the calling routine, or in one around it, that a + * bare name there names (`procedure Inner;` between `procedure TFoo.Bar;` + * and its `begin`, `TFoo::Bar::Inner`): the innermost one declared above the + * call, the calling routine itself among them. It comes before anything of + * the class or the unit. Null when none is. + */ +function nestedRoutineInScope(lower: string, ref: UnresolvedRef, context: ResolutionContext): Found | null { + // Most names no nested routine has: settled before any other lookup. + const named = pascalNamed(lower, context); + if (!named.some((n) => n.kind === 'function' && n.qualifiedName.includes('::'))) return null; + const memo = memos(context).nestedInScope; + const key = `${ref.fromNodeId}|${lower}`; + if (memo.has(key)) return memo.get(key)!; + let found: Found | null = null; + const caller = routineNode(ref.fromNodeId, context); + if (caller && caller.language === 'pascal') { + const chain = caller.qualifiedName.toLowerCase(); + let best: Node | null = null; + let depth = -1; + for (const n of named) { + if (n.filePath !== caller.filePath || n.startLine > ref.line || !isNestedRoutine(n, context)) continue; + const outer = ownerOf(n); + // Nested in the caller, or in a routine the caller is nested in. + if (chain !== outer && !chain.startsWith(`${outer}::`)) continue; + if (outer.length > depth || (outer.length === depth && n.startLine < best!.startLine)) { + best = n; + depth = outer.length; + } + } + if (best) found = { node: best, resolvedBy: 'exact-match', confidence: 0.95 }; + } + memo.set(key, found); + return found; +} + +/** + * Whether a node is a routine nested in another (see nestedRoutineInScope): + * a Pascal function qualified under a routine, which no code outside the one + * it is nested in can call. + */ +function isNestedRoutine(node: Node, context: ResolutionContext): boolean { + if (node.kind !== 'function' || node.language !== 'pascal') return false; + const cut = node.qualifiedName.lastIndexOf('::'); + if (cut < 0) return false; + const outer = node.qualifiedName.slice(0, cut); + const memo = memos(context).routineQns; + let routine = memo.get(outer); + if (routine === undefined) { + routine = context.getNodesByQualifiedName(outer).some((n) => n.language === 'pascal' && isRoutine(n)); + memo.set(outer, routine); + } + return routine; +} + +/** The routine a reference comes from, or null when it comes from a file or anything else. */ +function routineNode(id: string, context: ResolutionContext): Node | null { + const memo = memos(context).routineNodes; + if (memo.has(id)) return memo.get(id)!; + const node = context.getNodeById?.(id) ?? null; + const routine = node && isRoutine(node) ? node : null; + memo.set(id, routine); + return routine; +} + +/** + * A bare Pascal call (`Inner(1)`) to a routine nested where the call is (see + * nestedRoutineInScope). Made inside a nested routine, it reaches the rest of + * that routine's scope the same way: the class of the method it is nested in + * (`GetNext(Run)` in a tree method's nested routine is the tree's), its unit, + * the units it uses. Undefined when that scope holds no routine of the name, + * for the other strategies to resolve. + */ +export function matchPascalNestedCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | undefined { + if (ref.language !== 'pascal' || ref.referenceKind !== 'calls' || !isBareCall(ref, context)) return undefined; + const lower = ref.referenceName.toLowerCase(); + let found = nestedRoutineInScope(lower, ref, context); + if (!found) { + const caller = routineNode(ref.fromNodeId, context); + if (!caller || !isNestedRoutine(caller, context)) return undefined; + const decl = declarationInScope(lower, classOfCall(ref, context), ref, context); + if (!decl || !(isRoutine(decl.node) || (DATA_KINDS.has(decl.node.kind) && isProceduralData(decl.node, context)))) return undefined; + found = decl; + } + context.rememberNode?.(found.node); + return { original: ref, targetNodeId: found.node.id, confidence: found.confidence, resolvedBy: found.resolvedBy }; +} + +/** + * A Pascal call another strategy resolved to a routine nested in some other + * routine, which nothing outside that routine can call: a bare name is then + * what the call's scope declares, a routine or data holding one (mORMot's + * `StrLen` variable), and any other call links nothing. + */ +function gatePascalNestedTarget(result: ResolvedRef, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { + if (!isBareCall(ref, context)) return null; + const lower = ref.referenceName.toLowerCase(); + const decl = declarationInScope(lower, classOfCall(ref, context), ref, context); + if (!decl || (!isRoutine(decl.node) && !(DATA_KINDS.has(decl.node.kind) && isProceduralData(decl.node, context)))) return null; + if (decl.node.id === result.targetNodeId) return result; + context.rememberNode?.(decl.node); + return { ...result, targetNodeId: decl.node.id, confidence: decl.confidence, resolvedBy: decl.resolvedBy }; +} + /** * The first declaration a bare name reaches from a routine of class `cls` - * (null: a free routine), whatever it declares: a member of the class or its - * ancestors, then a declaration of the unit, then of the units it uses, last - * first. Null when the project declares none of them, as for the System - * unit's names. + * (null: a free routine), whatever it declares: a routine nested in the + * calling routine or one around it, a member of the class or its ancestors, + * then a declaration of the unit, then of the units it uses, last first. + * Null when the project declares none of them, as for the System unit's + * names. */ function declarationInScope(lower: string, cls: Node | null, ref: UnresolvedRef, context: ResolutionContext): Found | null { + const nested = nestedRoutineInScope(lower, ref, context); + if (nested) return nested; const memo = memos(context).inScope; const key = `${cls?.id ?? ''}|${ref.filePath}|${lower}`; if (memo.has(key)) return memo.get(key)!; @@ -413,6 +530,9 @@ interface Memos { headerClasses: Map>; outlines: Map; conditionDepths: Map; + nestedInScope: Map; + routineNodes: Map; + routineQns: Map; } const MEMOS = new WeakMap(); @@ -424,6 +544,7 @@ function memos(context: ResolutionContext): Memos { ancestors: new Map(), usedFiles: new Map(), classByName: new Map(), unitFiles: new Map(), hierarchy: new Map(), memberTypes: new Map(), keywords: new Map(), procedural: new Map(), headerClasses: new Map(), outlines: new Map(), conditionDepths: new Map(), unitFileSets: new Map(), includes: new Map(), includers: new Map(), ownFiles: new Map(), + nestedInScope: new Map(), routineNodes: new Map(), routineQns: new Map(), }; MEMOS.set(context, m); }