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