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); }