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 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-22 17:50:35 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 80c3680068
commit f3fdba2955
6 changed files with 580 additions and 46 deletions
+75 -33
View File
@@ -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<T>.Add` belongs to `TList`, whose node may spell its parameters
// with their constraints (`TList<T: class>`).
const owner = fullName.includes('.') ? fullName.replace(/<[^>]*>/g, '').split('.').slice(0, -1).join('::') : null;
this.pascalScopes.push({
locals: this.pascalRoutineLocals(node),
classQn: owner,
routine: shortNameKey.replace(/<.*$/, ''),
startLine: node.startPosition.row + 1,
});
this.nodeStack.push(parentId);
this.visitPascalBlock(block);
this.nodeStack.pop();
this.pascalScopes.pop();
// `TList<T>.Add` belongs to `TList`, whose node may spell its parameters
// with their constraints (`TList<T: class>`).
const owner = fullName.includes('.') ? fullName.replace(/<[^>]*>/g, '').split('.').slice(0, -1).join('::') : null;
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<string, string | null> {
private pascalRoutineLocals(routine: SyntaxNode, headerOnly = false): Map<string, string | null> {
const names = new Map<string, string | null>();
const header = routine.type === 'lambda' ? routine : routine.namedChildren.find((c: SyntaxNode) => c.type === 'declProc');
names.set('result', this.pascalTypeName(header?.namedChildren.find((c: SyntaxNode) => c.type === 'typeref')));
const declare = (decl: SyntaxNode): void => {
const type = this.pascalTypeName(decl.namedChildren.find((c: SyntaxNode) => c.type === 'type' || c.type === 'typeref'));
for (const c of decl.namedChildren) {
if (c.type === 'identifier') names.set(getNodeText(c, this.source).toLowerCase(), type);
}
};
const args = header?.namedChildren.find((c: SyntaxNode) => c.type === 'declArgs');
for (const arg of args?.namedChildren ?? []) {
if (arg.type === 'declArg') declare(arg);
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<string, string | null>, 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
+11 -4
View File
@@ -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);
+129 -8
View File
@@ -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<string, Array<{ line: number; classQn: string | null; routine: boolean }>>;
outlines: Map<string, { classes: Node[]; imports: Node[]; routineColumn: number }>;
conditionDepths: Map<string, number[]>;
nestedInScope: Map<string, Found | null>;
routineNodes: Map<string, Node | null>;
routineQns: Map<string, boolean>;
}
const MEMOS = new WeakMap<ResolutionContext, Memos>();
@@ -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);
}