mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 18:23:25 +08:00
Four Delphi bugs that PR 2550 by @OleksiyPenkov found, reimplemented on
main's Pascal scope code rather than as a second resolver:
- A call through a chain of members and methods recorded only the outer
method's bare name, which name matching gave to any namesake: SynEdit's
Canvas.Font.Assign(Font) went to TProposalColumn::Assign, and
TLogger2.Create.Write('y') linked nothing, a bare Write being a System
name. The extractor now writes such a call as the typed chain
pascal-scope already resolves for paren-less names ({call} for a
statement, a new {args} mode for a last link written with its
arguments). Each link is typed by its declaration: field and property
types, function results, a local's declared type, through pointer and
class reference types (PVirtualNode = ^TVirtualNode, TSynLogClass =
class of TSynLog) and explicit dereferences (a^.Copy). A constructor
called on a class gives an instance of it, a unit name, dotted ones
too, qualifies its declarations, and a chain called with arguments may
end at data that holds a routine. There is no bare fallback: a chain
through a type from outside the project links nothing, and in a with
body only a chain from Self or a typed local is read.
- A method given to a target was a method pointer only when the target
was named On.../FOn...: Callback := FProvider.Value with Callback: TGet
recorded a call, and FOnGet := @FProvider.Value nothing. A value given
to an assignment now carries its target, whose declared type decides
(routine types written out, of object, reference to, project aliases,
the RTL's routine type names; a class, a record or a System value type
holds no routine); the name decides only where the declaration can't
be read. A function that returns a routine type is called for it, and
@Obj.Method is a reference anywhere.
- A nested routine's locals leaked into the routine around it: the name
matcher's Pascal receiver scan read every declaration above the call.
It now steps over routines nested elsewhere, reads the calling
routine's own name lists (Doc, Doc2: TDocVariantData), which main typed
only by luck through a nested routine's parameter, and follows pointer
and class reference types. Where the tree has errors, the source-text
scopes give the outer routine its own names back after a nested one.
- A nested routine declared forward is in scope from that declaration.
Validation:
- New __tests__/pascal-reference-boundaries.test.ts, 41 tests, 24 of
them ported from PR 2550: 22 fail on main's sources, the other 19 pin
what main already got right. pascal-chained-calls and pascal-for-in-var
pinned the bare outer-method names and now expect the typed chains
(4 tests, failing on main). Twenty mutations, one per rule, each fail
a test.
- A/B against main 80f6b505, both arms built from their own sources and
diffed by natural keys, edges removed/added: SynEdit 195/66, HeidiSQL
789/320, mORMot2 2553/4164, DelphiMVCFramework 2394/1850, DWScript
without its Test folder of scripts (519 files) 864/801. leveldb and
SDWebImage byte-identical.
- Removed edges, hand-triaged: SynEdit all 195, 193 namesakes or parse
artifacts and 2 right ones lost (Get(i).LoadFromStream in a with
body); samples of HeidiSQL 20/20, DelphiMVCFramework 30/30, DWScript
12/12 and mORMot2 23/25 namesakes (of the other two, one now links the
unit's own CompareMem, and one right edge is lost, through a field
declared after the first name of a name list, which the extractor
gives no node).
- Added edges sampled right: typed chains 186 of 187 (the wrong one
reaches the FMX variant of a conditional ancestor), receiver calls
40/40, method pointers, @-references and kind changes 57/57.
- CPU, median of 3 interleaved: SynEdit +6.6%, HeidiSQL +0.6%, mORMot2
-2.5%, DWScript +4.7%, while the identical controls moved 0.0% and
-5.9%. DelphiMVCFramework, timed before the scan's memo was split per
routine (which only made it cheaper): -6.6%.
Reimplements part of PR 2550 by @OleksiyPenkov.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1247 lines
57 KiB
TypeScript
1247 lines
57 KiB
TypeScript
/**
|
|
* Pascal names written without parentheses (#2514): a parameterless routine
|
|
* is called that way in Delphi — `Before;`, `Result := FProvider.Value + 1`,
|
|
* `if Ready then` — and the same spelling reads a field, a property, a
|
|
* variable or a constant. The extractor records such a name as a
|
|
* `references` ref named `{mode}.Name` or `{mode}.Receiver.Name`:
|
|
*
|
|
* - `{call}` — a statement on its own, which only a routine can be;
|
|
* - `{read}` — a name in an expression or an argument, where a function
|
|
* (or a constructor) is called for its value and a procedure, which has
|
|
* none, can only be a method pointer;
|
|
* - `{ref}` — the value given to an event (`OnClick := Handler`), a method
|
|
* pointer;
|
|
* - `{recv}` — a receiver (`GetList` in `GetList.Count`, `B` in `A.B.C`) or
|
|
* an indexed value, which only a function can be;
|
|
* - `{args}` — the last link of a chain called with its arguments written
|
|
* (`FPanel.Series().Clear(1)`, `TLog.Create.Write('x')`), which runs it (#2550).
|
|
*
|
|
* Written without its arguments, a routine that needs some is a method
|
|
* pointer too, in any of them. A chain of more than one link (`{call}.A.B.C`)
|
|
* is the outer method of a call through members and functions, typed by what
|
|
* each link is declared to be, never a namesake by its name alone (#2550).
|
|
*
|
|
* A value assigned to a target carries the target after an `=`
|
|
* (`{read=Callback}.FProvider.Value`, `{ref=FOnGet}`, a local's declared type
|
|
* as `{read=:TGet}`): a target whose declared type is a routine type is given
|
|
* a method pointer, whatever it is called, and one of any other type a value;
|
|
* the mode, from the target's name (`OnClick`, `FOnGet`), decides only where
|
|
* its declaration doesn't say (#2550).
|
|
*
|
|
* The routine's class follows an `@` (`{call@TConsumer}.Before`), because the
|
|
* reference's own node is the file when an implementation matched no
|
|
* declaration. A receiver the routine declares itself is written as its
|
|
* declared type after a `:` (`{read}.:TProvider.Value`).
|
|
*
|
|
* This module links such a ref only to a method or function the name reaches
|
|
* by Delphi's scope rules — the routine's class and its ancestors, then the
|
|
* unit, then the units it uses, last first — or, through a receiver, a method
|
|
* of the receiver's declared type. The first declaration of the name in that
|
|
* order decides: when it is data (a field, property, variable, constant, enum
|
|
* value or type), nothing is linked. A bare name the routine declares itself
|
|
* never reaches here (the extractor skips it).
|
|
*/
|
|
|
|
import type { Node } from '../types';
|
|
import type { ResolutionContext, ResolvedRef, UnresolvedRef } from './types';
|
|
import { declaredSupertypes } from './name-matcher';
|
|
import { PASCAL_SYSTEM_NAMES } from '../extraction/languages/pascal-system';
|
|
|
|
type Mode = 'call' | 'read' | 'ref' | 'recv' | 'args';
|
|
interface Found {
|
|
node: Node;
|
|
resolvedBy: ResolvedRef['resolvedBy'];
|
|
confidence: number;
|
|
}
|
|
|
|
const PARENLESS = /^\{(call|read|ref|recv|args)(?:=([^@}]+))?(?:@([^}]+))?\}\.(.+)$/;
|
|
const CLASS_KINDS: ReadonlySet<string> = new Set(['class', 'struct', 'interface']);
|
|
/** What a unit declares by name at its top level. */
|
|
const UNIT_KINDS: ReadonlySet<string> = new Set([
|
|
'function', 'variable', 'constant', 'class', 'struct', 'interface', 'enum', 'type_alias', 'enum_member',
|
|
]);
|
|
/** What a class declares as its members. */
|
|
const MEMBER_KINDS: ReadonlySet<string> = new Set(['method', 'field', 'property', 'constant', 'variable']);
|
|
/** Declarations that hold a value rather than run code. */
|
|
const DATA_KINDS: ReadonlySet<string> = new Set(['field', 'property', 'variable', 'constant', 'enum_member']);
|
|
/**
|
|
* The RTL's routine types whose names don't say so (`TProc`, `TFunc<T>`),
|
|
* lowercased; the project's own are read from their declarations.
|
|
*/
|
|
const RTL_PROCEDURAL_TYPES: ReadonlySet<string> = new Set([
|
|
'tproc', 'tfunc', 'tpredicate', 'tcomparison', 'tthreadprocedure', 'tlistsortcompare', 'tstringlistsortcompare',
|
|
]);
|
|
const MAX_HIERARCHY_DEPTH = 8;
|
|
|
|
/** Whether `ref` is a paren-less Pascal name (see the file comment), which only this module resolves. */
|
|
export function isPascalParenlessRef(ref: UnresolvedRef): boolean {
|
|
return ref.language === 'pascal' && ref.referenceKind === 'references' && PARENLESS.test(ref.referenceName);
|
|
}
|
|
|
|
/** The routine a paren-less Pascal name reaches in its scope, or null (see the file comment). */
|
|
export function matchPascalParenlessRef(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
|
const m = PARENLESS.exec(ref.referenceName);
|
|
if (!m) return null;
|
|
let mode = m[1] as Mode;
|
|
const target = m[2];
|
|
const path = m[4]!.split('.');
|
|
// Most such names read data, often the framework's: a name no Pascal
|
|
// routine has is settled here, before any lookup in scope. Called with its
|
|
// arguments, a chain may end at data that holds a routine.
|
|
if (mode !== 'args' && routineNames(context)?.has(path[path.length - 1]!.toLowerCase()) === false) return null;
|
|
let cls: Node | null = null;
|
|
if (m[3]) {
|
|
cls = classByQualifiedName(m[3], ref.filePath!, context);
|
|
} else {
|
|
const routine = context.getNodeById?.(ref.fromNodeId);
|
|
if (routine) cls = classOfRoutine(routine, context);
|
|
}
|
|
const found = path.length === 1
|
|
? nameInScope(path[0]!, cls, ref, context)
|
|
: memberThroughPath(path, cls, ref, context, mode === 'args');
|
|
if (!found) return null;
|
|
let edgeKind: 'calls' | 'references' = 'calls';
|
|
// Written with its arguments, the routine runs, or the routine the data holds.
|
|
if (mode !== 'args') {
|
|
if (target !== undefined) {
|
|
// Given to a target, a method is a method pointer when the target's
|
|
// declared type is a routine type, and a value otherwise. A function
|
|
// whose result is a routine type is called for it.
|
|
const routineTyped = assignedRoutineType(target, cls, ref, context);
|
|
if (routineTyped !== undefined) mode = routineTyped ? 'ref' : 'read';
|
|
if (mode === 'ref' && returnsRoutineType(found.node, context)) mode = 'read';
|
|
}
|
|
// Written without arguments, a routine that needs some can only be a
|
|
// method pointer — save a constructor, whose name then means an inherited
|
|
// one outside the project (`TObject.Create`) — and so is a procedure read
|
|
// for a value, and an event's method; any other runs.
|
|
const needsArgs = needsArguments(found.node);
|
|
if (needsArgs && routineKeyword(found.node, context) === 'constructor') return null;
|
|
const pointer = mode === 'ref' || needsArgs || (mode !== 'call' && !yieldsValue(found.node, context));
|
|
// A statement runs its routine, and a receiver is a routine's value.
|
|
if (pointer && (mode === 'call' || mode === 'recv')) return null;
|
|
edgeKind = pointer ? 'references' : 'calls';
|
|
}
|
|
context.rememberNode?.(found.node);
|
|
return {
|
|
original: ref,
|
|
targetNodeId: found.node.id,
|
|
confidence: found.confidence,
|
|
resolvedBy: found.resolvedBy,
|
|
edgeKind,
|
|
// A method pointer is a function used as a value, as `@Handler` is (#756).
|
|
...(edgeKind === 'references' ? { metadata: { fnRef: true } } : {}),
|
|
};
|
|
}
|
|
|
|
/** Whether `ref` is a bare Pascal call to one of the System unit's names (see PASCAL_SYSTEM_NAMES). */
|
|
export function isPascalSystemCall(ref: UnresolvedRef, context: ResolutionContext): boolean {
|
|
return ref.language === 'pascal' && ref.referenceKind === 'calls' && PASCAL_SYSTEM_NAMES.has(ref.referenceName.toLowerCase()) &&
|
|
isBareCall(ref, context);
|
|
}
|
|
|
|
/**
|
|
* Whether a Pascal call named by a bare name is written bare: its expression
|
|
* starts with the name (`Reset(F)`). The last call of a chain is named bare
|
|
* too (`Reset` in `GetKey(P).Reset`), and its scope is its receiver's type.
|
|
*/
|
|
function isBareCall(ref: UnresolvedRef, context: ResolutionContext): boolean {
|
|
if (!/^[A-Za-z_]\w*$/.test(ref.referenceName)) return false;
|
|
const line = fileLines(ref.filePath!, context)[ref.line - 1] ?? '';
|
|
return /^[A-Za-z_]\w*/.exec(line.slice(ref.column ?? 0))?.[0].toLowerCase() === ref.referenceName.toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* A call to one of the System unit's names is the System unit's, which the
|
|
* project doesn't hold, unless a declaration in the call's scope shadows it:
|
|
* a member of the class (`Delete(I)` in a list class), a routine of the unit
|
|
* or the units it uses. A project type of that name is a typecast. Data of
|
|
* that name runs only when it holds a routine (see isProceduralData). A unit
|
|
* that declares one under a condition stands in for compilers without it
|
|
* (`{$IFNDEF UNICODE} UnicodeString = WideString; {$ENDIF}`): the code is
|
|
* most likely built with one that has it.
|
|
*/
|
|
export function matchPascalSystemCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
|
const decl = declarationInScope(ref.referenceName.toLowerCase(), classOfCall(ref, context), ref, context);
|
|
if (!decl) return null;
|
|
if (DATA_KINDS.has(decl.node.kind) && !isProceduralData(decl.node, context)) return null;
|
|
if (!decl.node.qualifiedName.includes('::') && declaredUnderCondition(decl.node, context)) return null;
|
|
context.rememberNode?.(decl.node);
|
|
return { original: ref, targetNodeId: decl.node.id, confidence: decl.confidence, resolvedBy: decl.resolvedBy };
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* chose. Any other is a namesake: `length(S)` was a record's `length` field,
|
|
* `Pointer(P)` a property named `Pointer`. A name the calling routine
|
|
* declares itself, as a parameter, a local or a nested routine, is that
|
|
* declaration, which the graph doesn't hold. A name its scope doesn't
|
|
* declare at all is a `with` object's member (`with OCI do HandleAlloc(...)`)
|
|
* 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 gatePascalCall(result: ResolvedRef, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
|
const target = context.getNodeById?.(result.targetNodeId);
|
|
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;
|
|
const decl = declarationInScope(lower, classOfCall(ref, context), ref, context);
|
|
if (!decl) {
|
|
const only = target.language === 'pascal' && pascalNamed(lower, context).every((n) => n.id === target.id || n.kind === 'import');
|
|
return only && isProceduralData(target, context) ? result : null;
|
|
}
|
|
if (!isRoutine(decl.node) && !(DATA_KINDS.has(decl.node.kind) && isProceduralData(decl.node, context))) return null;
|
|
if (decl.node.id === target.id) return result;
|
|
context.rememberNode?.(decl.node);
|
|
return { ...result, targetNodeId: decl.node.id, confidence: decl.confidence, resolvedBy: decl.resolvedBy };
|
|
}
|
|
|
|
/**
|
|
* Whether the routine making a call declares the called name itself, as a
|
|
* parameter, a local or a nested routine (`function Hash(Item: Pointer;
|
|
* Hasher: THasher)` calling `Hasher(0, Item, 4)`): from the routine's header
|
|
* down to the call.
|
|
*/
|
|
function declaredInRoutine(lower: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
|
const lines = fileLines(ref.filePath!, context);
|
|
// The header in the text: a method's node may start at its declaration in
|
|
// the class, above the fields and the unit's variables.
|
|
const from = routineHeaderAt(ref.filePath!, ref.line, context);
|
|
const name = escapeRegExp(lower);
|
|
const mentions = new RegExp(`\\b${name}\\b`, 'i');
|
|
const declares = new RegExp(`\\b(?:procedure|function)\\s+${name}\\b|\\b${name}\\s*(?:,\\s*[A-Za-z_]\\w*\\s*)*:(?!=)`, 'i');
|
|
for (let i = Math.max(1, from); i <= ref.line && i <= lines.length; i++) {
|
|
const text = lines[i - 1]!;
|
|
if (mentions.test(text) && declares.test(text.replace(/\/\/.*$|\{[^}]*\}|'[^']*'/g, ' '))) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** The line of the nearest routine header at column 0 at or above a line, else the line itself. */
|
|
function routineHeaderAt(filePath: string, line: number, context: ResolutionContext): number {
|
|
headerClassAt(filePath, line, context);
|
|
const headers = memos(context).headerClasses.get(filePath) ?? [];
|
|
let found = line;
|
|
for (const h of headers) {
|
|
if (h.line > line) break;
|
|
if (h.routine) found = h.line;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/**
|
|
* Whether a declaration is inside an `{$IFDEF}`, `{$IFNDEF}`, `{$IF}` or
|
|
* `{$IFOPT}` block, or its `{$ELSE}`.
|
|
*/
|
|
function declaredUnderCondition(node: Node, context: ResolutionContext): boolean {
|
|
const memo = memos(context).conditionDepths;
|
|
let depths = memo.get(node.filePath);
|
|
const lines = fileLines(node.filePath, context);
|
|
if (!depths) {
|
|
depths = [];
|
|
let depth = 0;
|
|
for (const text of lines) {
|
|
depths.push(depth);
|
|
if (text.includes('{$')) depth = conditionDepthAfter(text, depth);
|
|
}
|
|
memo.set(node.filePath, depths);
|
|
}
|
|
const line = lines[node.startLine - 1] ?? '';
|
|
return conditionDepthAfter(line.slice(0, node.startColumn), depths[node.startLine - 1] ?? 0) > 0;
|
|
}
|
|
|
|
/** The depth of conditional blocks after a line's directives (`{$IFDEF X}` opens one, `{$ENDIF}` closes it). */
|
|
function conditionDepthAfter(text: string, depth: number): number {
|
|
for (const m of text.matchAll(/\{\$(ifdef|ifndef|ifopt|ifend|endif|if)\b/gi)) {
|
|
const directive = m[1]!.toLowerCase();
|
|
depth = directive === 'endif' || directive === 'ifend' ? Math.max(0, depth - 1) : depth + 1;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
/**
|
|
* The class whose method makes a call: the one the nearest method header
|
|
* above the call names in the source text (`procedure TFoo.Bar;`), which holds
|
|
* where the tree had errors and gave the code to the file, or to a routine
|
|
* that swallowed the ones after it; else the reference's method's class.
|
|
*/
|
|
function classOfCall(ref: UnresolvedRef, context: ResolutionContext): Node | null {
|
|
const qn = headerClassAt(ref.filePath!, ref.line, context);
|
|
const cls = qn ? classByQualifiedName(qn, ref.filePath!, context) : null;
|
|
if (cls) return cls;
|
|
const routine = context.getNodeById?.(ref.fromNodeId);
|
|
return routine?.kind === 'method' ? classOfRoutine(routine, context) : null;
|
|
}
|
|
|
|
/**
|
|
* The class the nearest routine header at or above a line names
|
|
* (`procedure TFoo.Bar;` → `TFoo`), or null when it is a free routine's, or
|
|
* the unit's code resumes there: after an `end;` that starts its line, or at
|
|
* `initialization`.
|
|
*/
|
|
function headerClassAt(filePath: string, line: number, context: ResolutionContext): string | null {
|
|
const memo = memos(context).headerClasses;
|
|
let headers = memo.get(filePath);
|
|
if (!headers) {
|
|
headers = [];
|
|
const lines = fileLines(filePath, context);
|
|
const header = /^(?:class\s+)?(?:procedure|function|constructor|destructor|operator)\s+([A-Za-z_][\w.<>, ]*?)\s*[(:;]/i;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const text = lines[i]!;
|
|
// Each starts its line with one of c, d, e, f, i, o, p (either case).
|
|
const first = text.charCodeAt(0) | 0x20;
|
|
if (first !== 0x63 && first !== 0x64 && first !== 0x65 && first !== 0x66 && first !== 0x69 && first !== 0x6f && first !== 0x70) continue;
|
|
if (/^(?:end;|(?:initialization|finalization)\b)/i.test(text)) {
|
|
headers.push({ line: i + 1, classQn: null, routine: false });
|
|
continue;
|
|
}
|
|
const m = header.exec(text);
|
|
if (!m) continue;
|
|
const parts = m[1]!.replace(/<[^>]*>/g, '').replace(/\s+/g, '').split('.');
|
|
headers.push({ line: i + 1, classQn: parts.length > 1 ? parts.slice(0, -1).join('::') : null, routine: true });
|
|
}
|
|
memo.set(filePath, headers);
|
|
}
|
|
let lo = 0;
|
|
let hi = headers.length - 1;
|
|
let hit: string | null = null;
|
|
while (lo <= hi) {
|
|
const mid = (lo + hi) >> 1;
|
|
if (headers[mid]!.line <= line) {
|
|
hit = headers[mid]!.classQn;
|
|
lo = mid + 1;
|
|
} else {
|
|
hi = mid - 1;
|
|
}
|
|
}
|
|
return hit;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Scope lookup
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function isRoutine(node: Node): boolean {
|
|
return node.kind === 'method' || node.kind === 'function';
|
|
}
|
|
|
|
/** A bare name, looked up the way Delphi looks it up from a routine of class `cls` (null: a free routine): a routine, or null. */
|
|
function nameInScope(name: string, cls: Node | null, ref: UnresolvedRef, context: ResolutionContext): Found | null {
|
|
const decl = declarationInScope(name.toLowerCase(), cls, ref, 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, or declared `forward` above
|
|
* it (see declaredForward). 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 || !isNestedRoutine(n, context)) continue;
|
|
if (n.startLine > ref.line && !declaredForward(n, ref.line, 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 routine nested in another is declared `forward` above a line
|
|
* (`procedure Late; forward;`), which puts it in scope there ahead of its
|
|
* body (#2550): a header of its name, as indented as its own, that ends in
|
|
* `forward;`, between the line and the header indented less above it, the
|
|
* routine both are nested in.
|
|
*/
|
|
function declaredForward(routine: Node, line: number, context: ResolutionContext): boolean {
|
|
const lines = fileLines(routine.filePath, context);
|
|
const name = routine.name.toLowerCase();
|
|
for (let i = Math.min(line, lines.length) - 1; i >= 0; i--) {
|
|
const m = /^(\s*)(?:class\s+)?(?:procedure|function|constructor|destructor)\s+([A-Za-z_]\w*)/i.exec(lines[i]!);
|
|
if (!m) continue;
|
|
if (m[1]!.length < routine.startColumn) return false;
|
|
if (m[1]!.length !== routine.startColumn || m[2]!.toLowerCase() !== name) continue;
|
|
// Its parameters may go on over a few lines.
|
|
let text = lines[i]!.replace(/\/\/.*$|\{[^}]*\}|\(\*.*?\*\)/g, ' ');
|
|
for (let j = i + 1; j < lines.length && j <= i + 4 && (text.split('(').length > text.split(')').length); j++) {
|
|
text += ' ' + lines[j]!.replace(/\/\/.*$|\{[^}]*\}|\(\*.*?\*\)/g, ' ');
|
|
}
|
|
if (/\bforward\s*;/i.test(text)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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 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)!;
|
|
let found: Found | null = null;
|
|
const member = cls ? memberInHierarchy(cls, lower, context) : null;
|
|
if (member) {
|
|
found = { node: member, resolvedBy: 'instance-method', confidence: 0.9 };
|
|
} else {
|
|
const own = ownUnitDeclaration(ref.filePath!, lower, context);
|
|
if (own) {
|
|
found = { node: own, resolvedBy: 'exact-match', confidence: 0.9 };
|
|
} else {
|
|
const used = usedUnitFiles(ref.filePath!, context);
|
|
for (let i = used.length - 1; i >= 0 && !found; i--) {
|
|
const decl = unitDeclaration(used[i]!, lower, context, ref.filePath!);
|
|
if (decl) found = { node: decl, resolvedBy: 'import', confidence: 0.85 };
|
|
}
|
|
}
|
|
}
|
|
memo.set(key, found);
|
|
return found;
|
|
}
|
|
|
|
/**
|
|
* A dotted name: the method its receiver's declared type has; with
|
|
* `holdsRoutine`, data that holds a routine too (a chain called with its
|
|
* arguments, `Owner.Events.FOnChange(Self)`, see isProceduralData).
|
|
*/
|
|
function memberThroughPath(path: string[], cls: Node | null, ref: UnresolvedRef, context: ResolutionContext, holdsRoutine = false): Found | null {
|
|
const last = memberAtPath(path, cls, ref, context);
|
|
if (!last) return null;
|
|
if (!isRoutine(last.node) && !(holdsRoutine && DATA_KINDS.has(last.node.kind) && isProceduralData(last.node, context))) return null;
|
|
return { node: last.node, resolvedBy: last.unit ? 'import' : 'instance-method', confidence: 0.9 };
|
|
}
|
|
|
|
/**
|
|
* What a dotted name's last link is declared as: a member of what the links
|
|
* before it are declared to be — a field's or a property's type, a
|
|
* function's result — from its head (see headOf), or, with `unit`, the
|
|
* routine or variable a unit's name qualifies (`Utils.Helper`). A constructor
|
|
* called on a class gives an instance of it: one the class declares, or
|
|
* `Create`, which the project's classes inherit from outside it
|
|
* (`TLog.Create.Write('x')`, `Logs.TLog.Create().Write('x')`).
|
|
*/
|
|
function memberAtPath(path: string[], cls: Node | null, ref: UnresolvedRef, context: ResolutionContext): { node: Node; unit: boolean } | null {
|
|
let head = headOf(path[0]!, cls, ref, context);
|
|
let rest = path.slice(1);
|
|
// A dotted unit name, the longest the links make (`mormot.core.os.GetTickCount64`).
|
|
for (let k = path.length - 1; !head && k >= 2; k--) {
|
|
const unitFile = unitFileNamed(path.slice(0, k).join('.').toLowerCase(), ref.filePath!, context);
|
|
if (unitFile) {
|
|
head = { type: null, unitFile };
|
|
rest = path.slice(k);
|
|
}
|
|
}
|
|
if (!head) return null;
|
|
let type = head.type;
|
|
let classRef = head.classRef === true;
|
|
if (head.unitFile) {
|
|
// `UnitName.Routine`, `UnitName.GlobalVar.Method`, `UnitName.TFoo.Create.Method`.
|
|
const decl = unitDeclaration(head.unitFile, rest[0]!.toLowerCase(), context, ref.filePath!);
|
|
if (!decl) return null;
|
|
if (rest.length === 1) return { node: decl, unit: true };
|
|
classRef = CLASS_KINDS.has(decl.kind);
|
|
type = classRef ? decl : typeOfMember(decl, context);
|
|
rest = rest.slice(1);
|
|
}
|
|
for (let i = 0; i < rest.length && type; i++) {
|
|
const lower = rest[i]!.toLowerCase();
|
|
const member = memberInHierarchy(type, lower, context);
|
|
if (i === rest.length - 1) return member ? { node: member, unit: false } : null;
|
|
if (classRef && (member ? routineKeyword(member, context) === 'constructor' : lower === 'create')) {
|
|
classRef = false;
|
|
continue;
|
|
}
|
|
if (!member) return null;
|
|
type = typeOfMember(member, context);
|
|
classRef = false;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* What a dotted name's first link is: a value of a known class (a local's
|
|
* declared type, written `:TFoo`), a class itself (`classRef`), or a unit.
|
|
*/
|
|
function headOf(name: string, cls: Node | null, ref: UnresolvedRef, context: ResolutionContext): { type: Node | null; classRef?: boolean; unitFile?: string } | null {
|
|
if (name.startsWith(':')) return { type: classNamed(name.slice(1), ref.filePath!, context) };
|
|
const lower = name.toLowerCase();
|
|
if (lower === 'self') return cls ? { type: cls } : null;
|
|
if (cls) {
|
|
const member = memberInHierarchy(cls, lower, context);
|
|
if (member) return { type: typeOfMember(member, context) };
|
|
}
|
|
const decl = ownUnitDeclaration(ref.filePath!, lower, context) ??
|
|
usedUnitFiles(ref.filePath!, context).reduceRight<Node | undefined>(
|
|
(hit, file) => hit ?? unitDeclaration(file, lower, context, ref.filePath!), undefined);
|
|
if (decl) return CLASS_KINDS.has(decl.kind) ? { type: decl, classRef: true } : { type: typeOfMember(decl, context) };
|
|
const unitFile = unitFileNamed(lower, ref.filePath!, context);
|
|
return unitFile ? { type: null, unitFile } : null;
|
|
}
|
|
|
|
/**
|
|
* Whether the target a value is assigned to is declared with a routine type
|
|
* (see the file comment): the field, property or variable a name reaches in
|
|
* scope, or through a receiver (`FEvents.OnGet`, `Self.Callback`), or the
|
|
* declared type of a local (`:TGet`, `:procedure` for one written out).
|
|
* Undefined when the declaration, or what its type is, can't be read.
|
|
*/
|
|
function assignedRoutineType(target: string, cls: Node | null, ref: UnresolvedRef, context: ResolutionContext): boolean | undefined {
|
|
const links = target.split('.');
|
|
if (links.length === 1 && links[0]!.startsWith(':')) return routineTypeVerdict(links[0]!.slice(1), context, 0);
|
|
const node = links.length === 1
|
|
? declarationInScope(links[0]!.toLowerCase(), cls, ref, context)?.node
|
|
: memberAtPath(links, cls, ref, context)?.node;
|
|
if (!node || !DATA_KINDS.has(node.kind) || node.kind === 'enum_member') return undefined;
|
|
const text = declaredTypeText(node, context);
|
|
return text ? routineTypeVerdict(text, context, 0) : undefined;
|
|
}
|
|
|
|
/** Whether a function's result is of a routine type (`function MakeHandler: TNotifyEvent`). */
|
|
function returnsRoutineType(routine: Node, context: ResolutionContext): boolean {
|
|
if (!isRoutine(routine)) return false;
|
|
const typeName = routine.returnType ?? /:\s*([A-Za-z_][\w.]*)\s*;?\s*$/.exec(routine.signature ?? '')?.[1];
|
|
return !!typeName && routineTypeVerdict(typeName, context, 0) === true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Classes, units and declarations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface Memos {
|
|
routineNames?: Set<string> | null;
|
|
named: Map<string, Node[]>;
|
|
inScope: Map<string, Found | null>;
|
|
classOfRoutine: Map<string, Node | null>;
|
|
classByQn: Map<string, Node | null>;
|
|
ancestors: Map<string, Node[]>;
|
|
usedFiles: Map<string, string[]>;
|
|
classByName: Map<string, Node | null>;
|
|
unitsByName?: Map<string, string[]>;
|
|
genericTypes?: Map<string, Node[]>;
|
|
projectFiles?: { byName: Map<string, string[]>; pascal: string[] };
|
|
includes: Map<string, string[]>;
|
|
includers: Map<string, string | null>;
|
|
ownFiles: Map<string, string[]>;
|
|
unitFileSets: Map<string, string[]>;
|
|
unitFiles: Map<string, string | null>;
|
|
hierarchy: Map<string, Node | null>;
|
|
memberTypes: Map<string, Node | null>;
|
|
keywords: Map<string, string | null>;
|
|
procedural: Map<string, boolean>;
|
|
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>();
|
|
|
|
function memos(context: ResolutionContext): Memos {
|
|
let m = MEMOS.get(context);
|
|
if (!m) {
|
|
m = {
|
|
named: new Map(), inScope: new Map(), classOfRoutine: new Map(), classByQn: new Map(),
|
|
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);
|
|
}
|
|
return m;
|
|
}
|
|
|
|
/** Every Pascal method's and function's name, lowercased, or null when the context can't list them. */
|
|
function routineNames(context: ResolutionContext): Set<string> | null {
|
|
const memo = memos(context);
|
|
if (memo.routineNames === undefined) {
|
|
const names = context.getNodeNamesOfKinds?.('pascal', ['method', 'function']);
|
|
memo.routineNames = names ? new Set(names.map((n) => n.toLowerCase())) : null;
|
|
}
|
|
return memo.routineNames;
|
|
}
|
|
|
|
/** The Pascal nodes a name names, whatever their case. */
|
|
function pascalNamed(lower: string, context: ResolutionContext): Node[] {
|
|
const memo = memos(context).named;
|
|
let nodes = memo.get(lower);
|
|
if (!nodes) {
|
|
nodes = context.getNodesByLowerName(lower).filter((n) => n.language === 'pascal');
|
|
memo.set(lower, nodes);
|
|
}
|
|
return nodes;
|
|
}
|
|
|
|
/** Drops this module's per-context memos (the resolver clears them between runs). */
|
|
export function clearPascalScopeMemos(context: ResolutionContext): void {
|
|
MEMOS.delete(context);
|
|
}
|
|
|
|
/**
|
|
* The class (or record) an implementation header names (`TOuter::TInner`,
|
|
* generic parameters dropped): the one `filePath` declares, whatever its case
|
|
* or parameters, else one the file sees.
|
|
*/
|
|
function classByQualifiedName(qualifiedName: string, filePath: string, context: ResolutionContext): Node | null {
|
|
const memo = memos(context).classByQn;
|
|
const key = `${filePath}|${qualifiedName}`;
|
|
if (memo.has(key)) return memo.get(key)!;
|
|
const exact = context.getNodesByQualifiedName(qualifiedName).filter((n) => n.language === 'pascal' && CLASS_KINDS.has(n.kind));
|
|
const lower = qualifiedName.toLowerCase();
|
|
let cls: Node | null = null;
|
|
for (const file of ownUnitFiles(filePath, context)) {
|
|
cls = declarationOf(exact.filter((n) => n.filePath === file)) ??
|
|
declarationOf(outlineOf(file, context).classes.filter((n) => n.qualifiedName.replace(/<[^>]*>/g, '').toLowerCase() === lower));
|
|
if (cls) break;
|
|
}
|
|
cls ??= declarationOf(exact) ?? classNamed(qualifiedName.split('::').pop()!, filePath, context);
|
|
memo.set(key, cls);
|
|
return cls;
|
|
}
|
|
|
|
/**
|
|
* The class declaration among same-named nodes, not a forward declaration
|
|
* (`TFoo = class;`, which names no members or ancestors): the longest one.
|
|
*/
|
|
function declarationOf(classes: Node[]): Node | null {
|
|
let best: Node | null = null;
|
|
for (const c of classes) if (!best || c.endLine - c.startLine > best.endLine - best.startLine) best = c;
|
|
return best;
|
|
}
|
|
|
|
/** The class a method belongs to: its qualified name's owner, declared in the method's own file. */
|
|
function classOfRoutine(routine: Node, context: ResolutionContext): Node | null {
|
|
if (routine.kind !== 'method') return null;
|
|
const memo = memos(context).classOfRoutine;
|
|
if (memo.has(routine.id)) return memo.get(routine.id)!;
|
|
const owner = routine.qualifiedName.slice(0, Math.max(0, routine.qualifiedName.lastIndexOf('::')));
|
|
const candidates = owner ? context.getNodesByQualifiedName(owner).filter((n) => n.language === 'pascal' && CLASS_KINDS.has(n.kind)) : [];
|
|
const cls = declarationOf(candidates.filter((n) => n.filePath === routine.filePath)) ?? declarationOf(candidates);
|
|
memo.set(routine.id, cls);
|
|
return cls;
|
|
}
|
|
|
|
/** A class and its ancestors the project declares, nearest first. */
|
|
function ancestorsOf(cls: Node, context: ResolutionContext): Node[] {
|
|
const memo = memos(context).ancestors;
|
|
const hit = memo.get(cls.id);
|
|
if (hit) return hit;
|
|
const order: Node[] = [];
|
|
const seen = new Set<string>();
|
|
let frontier = [cls];
|
|
for (let depth = 0; depth < MAX_HIERARCHY_DEPTH && frontier.length > 0; depth++) {
|
|
const next: Node[] = [];
|
|
for (const c of frontier) {
|
|
if (seen.has(c.id)) continue;
|
|
seen.add(c.id);
|
|
order.push(c);
|
|
for (const name of declaredSupertypes(c, context)) {
|
|
const sup = classNamed(name, c.filePath, context);
|
|
if (sup) next.push(sup);
|
|
}
|
|
}
|
|
frontier = next;
|
|
}
|
|
memo.set(cls.id, order);
|
|
return order;
|
|
}
|
|
|
|
/** The qualified name of what declares a node (`TFoo::Bar` → `tfoo`), lowercased. */
|
|
function ownerOf(node: Node): string {
|
|
return node.qualifiedName.slice(0, Math.max(0, node.qualifiedName.lastIndexOf('::'))).toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* The member a name reaches in a class or, failing that, in its ancestors,
|
|
* nearest first; a parameterless overload before the others. Null when no
|
|
* class of the hierarchy declares it.
|
|
*/
|
|
function memberInHierarchy(cls: Node, lower: string, context: ResolutionContext): Node | null {
|
|
const memo = memos(context).hierarchy;
|
|
const key = `${cls.id}|${lower}`;
|
|
if (memo.has(key)) return memo.get(key)!;
|
|
let found: Node | null = null;
|
|
const candidates = pascalNamed(lower, context).filter((n) => MEMBER_KINDS.has(n.kind));
|
|
if (candidates.length > 0) {
|
|
for (const c of ancestorsOf(cls, context)) {
|
|
const owner = c.qualifiedName.toLowerCase();
|
|
const members = candidates.filter((n) => n.filePath === c.filePath && ownerOf(n) === owner);
|
|
if (members.length > 0) {
|
|
found = members.find((n) => !needsArguments(n)) ?? members[0]!;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
memo.set(key, found);
|
|
return found;
|
|
}
|
|
|
|
/**
|
|
* A unit's top-level declaration of a name (its enum values included), as
|
|
* code in `fromFile` sees it: another unit's member that its parse lost (see
|
|
* unitRoutineColumn) is not one, while in its own unit it is most likely the
|
|
* member the code calls.
|
|
*/
|
|
function unitDeclaration(filePath: string, lower: string, context: ResolutionContext, fromFile = filePath): Node | undefined {
|
|
const decls = pascalNamed(lower, context).filter((n) =>
|
|
n.filePath === filePath && UNIT_KINDS.has(n.kind) && (n.kind === 'enum_member' || !n.qualifiedName.includes('::')) &&
|
|
!(fromFile !== filePath && n.kind === 'function' && n.startColumn > unitRoutineColumn(filePath, context)));
|
|
// A routine declared in the interface and defined in the implementation is
|
|
// one name; of overloads, one called without arguments first.
|
|
return decls.find((n) => !isRoutine(n)) ?? decls.find((n) => !needsArguments(n)) ?? decls[0];
|
|
}
|
|
|
|
/**
|
|
* How far a unit's own routines are indented at most. In a unit whose
|
|
* `unit` header the parser lost, the members of a class it couldn't read
|
|
* come out as routines of the unit, indented deeper than the unit's own
|
|
* (`function Http: IHttpClient;` in a class body); elsewhere, any.
|
|
*/
|
|
function unitRoutineColumn(filePath: string, context: ResolutionContext): number {
|
|
return outlineOf(filePath, context).routineColumn;
|
|
}
|
|
|
|
/** What scope lookups need of a file's nodes, read once: its classes, its `uses` in order, unitRoutineColumn. */
|
|
function outlineOf(filePath: string, context: ResolutionContext): { classes: Node[]; imports: Node[]; routineColumn: number } {
|
|
const memo = memos(context).outlines;
|
|
let outline = memo.get(filePath);
|
|
if (!outline) {
|
|
const nodes = context.getNodesInFile(filePath);
|
|
let routineColumn = Infinity;
|
|
if (!nodes.some((n) => n.kind === 'module')) {
|
|
for (const n of nodes) {
|
|
if (n.kind === 'function' && !n.qualifiedName.includes('::')) routineColumn = Math.min(routineColumn, n.startColumn);
|
|
}
|
|
}
|
|
outline = {
|
|
classes: nodes.filter((n) => CLASS_KINDS.has(n.kind)),
|
|
imports: nodes.filter((n) => n.kind === 'import' && n.language === 'pascal')
|
|
.sort((a, b) => a.startLine - b.startLine || a.startColumn - b.startColumn),
|
|
routineColumn,
|
|
};
|
|
memo.set(filePath, outline);
|
|
}
|
|
return outline;
|
|
}
|
|
|
|
/** The files of the units a file uses, in the order its `uses` clauses name them. */
|
|
function usedUnitFiles(fromFile: string, context: ResolutionContext): string[] {
|
|
const memo = memos(context).usedFiles;
|
|
const hit = memo.get(fromFile);
|
|
if (hit) return hit;
|
|
// The unit's `uses` clauses, then those of the files it includes: mORMot's
|
|
// `.inc` files open the unit's implementation with one.
|
|
const unit = unitFilesOf(fromFile, context);
|
|
const files: string[] = [];
|
|
for (const own of unit) {
|
|
for (const imp of outlineOf(own, context).imports) {
|
|
const file = unitFileNamed(imp.name.toLowerCase(), own, context);
|
|
if (file && !unit.includes(file) && !files.includes(file)) files.push(file);
|
|
}
|
|
}
|
|
memo.set(fromFile, files);
|
|
return files;
|
|
}
|
|
|
|
/** The unit's own declaration of a name, seen from one of its files (see ownUnitFiles). */
|
|
function ownUnitDeclaration(fromFile: string, lower: string, context: ResolutionContext): Node | undefined {
|
|
for (const file of ownUnitFiles(fromFile, context)) {
|
|
const decl = unitDeclaration(file, lower, context);
|
|
if (decl) return decl;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* The files whose declarations are a file's unit's own, the file first: the
|
|
* unit is its own file and the include files it pulls in, so code in
|
|
* `mormot.core.os.pas` sees a variable `mormot.core.os.posix.inc` declares,
|
|
* and that file's code sees the unit's (see unitFilesOf).
|
|
*/
|
|
function ownUnitFiles(filePath: string, context: ResolutionContext): string[] {
|
|
const memo = memos(context).ownFiles;
|
|
let own = memo.get(filePath);
|
|
if (!own) {
|
|
const unit = unitFilesOf(filePath, context);
|
|
own = unit[0] === filePath ? unit : [filePath, ...unit.filter((f) => f !== filePath)];
|
|
memo.set(filePath, own);
|
|
}
|
|
return own;
|
|
}
|
|
|
|
/**
|
|
* The files one unit is made of: its own file, then the include files it
|
|
* pulls in (`{$I name.inc}`, `{$INCLUDE name}`), nested ones too. For an
|
|
* include file, the unit that includes it, nearest it when several do.
|
|
*/
|
|
function unitFilesOf(filePath: string, context: ResolutionContext): string[] {
|
|
const m = memos(context);
|
|
const hit = m.unitFileSets.get(filePath);
|
|
if (hit) return hit;
|
|
let root = filePath;
|
|
for (let i = 0; i < 3 && /\.inc$/i.test(root); i++) {
|
|
const up = includerOf(root, context);
|
|
if (!up || up === filePath) break;
|
|
root = up;
|
|
}
|
|
const files = [root];
|
|
for (let i = 0; i < files.length && files.length < 32; i++) {
|
|
for (const inc of includesOf(files[i]!, context)) if (!files.includes(inc)) files.push(inc);
|
|
}
|
|
if (!files.includes(filePath)) files.push(filePath);
|
|
m.unitFileSets.set(filePath, files);
|
|
return files;
|
|
}
|
|
|
|
/** The include files a Pascal file pulls in (`{$I name.inc}`, `{$INCLUDE name}`), each the one of that name nearest it. */
|
|
function includesOf(filePath: string, context: ResolutionContext): string[] {
|
|
const m = memos(context);
|
|
let includes = m.includes.get(filePath);
|
|
if (includes) return includes;
|
|
includes = [];
|
|
if (PASCAL_SOURCE.test(filePath)) {
|
|
for (const text of fileLines(filePath, context)) {
|
|
if (!text.includes('{$') || !/\{\$i(?:nclude)?\s/i.test(text)) continue;
|
|
for (const match of text.matchAll(/\{\$(?:I|INCLUDE)\s+['"]?([^'"}\s*]+)/gi)) {
|
|
let name = match[1]!.replace(/\\/g, '/');
|
|
name = name.slice(name.lastIndexOf('/') + 1).toLowerCase();
|
|
if (!name.includes('.')) name += '.inc';
|
|
const inc = nearest(projectFiles(context).byName.get(name) ?? [], filePath);
|
|
if (inc && inc !== filePath && !includes.includes(inc)) includes.push(inc);
|
|
}
|
|
}
|
|
}
|
|
m.includes.set(filePath, includes);
|
|
return includes;
|
|
}
|
|
|
|
/** The Pascal file that includes an include file: of the forty nearest it, the nearest whose text does. */
|
|
function includerOf(incFile: string, context: ResolutionContext): string | null {
|
|
const m = memos(context);
|
|
if (m.includers.has(incFile)) return m.includers.get(incFile)!;
|
|
// The files of its directory first, those named most like it first of
|
|
// them (`mormot.core.os.pas` for `mormot.core.os.posix.inc`): how many
|
|
// characters of its path a file shares, the directory counting first.
|
|
const shared = (f: string): number => {
|
|
let n = 0;
|
|
while (n < f.length && n < incFile.length && f.charCodeAt(n) === incFile.charCodeAt(n)) n++;
|
|
return f.lastIndexOf('/', n - 1) * 1000 + n;
|
|
};
|
|
const candidates = projectFiles(context).pascal.filter((f) => f !== incFile)
|
|
.map((f) => ({ f, n: shared(f) })).sort((a, b) => b.n - a.n).slice(0, 40);
|
|
const includer = candidates.find(({ f }) => includesOf(f, context).includes(incFile))?.f ?? null;
|
|
m.includers.set(incFile, includer);
|
|
return includer;
|
|
}
|
|
|
|
const PASCAL_SOURCE = /\.(?:pas|dpr|dpk|lpr|inc)$/i;
|
|
|
|
/** The project's files by lowercased name, and its Pascal sources. */
|
|
function projectFiles(context: ResolutionContext): { byName: Map<string, string[]>; pascal: string[] } {
|
|
const m = memos(context);
|
|
if (!m.projectFiles) {
|
|
const byName = new Map<string, string[]>();
|
|
const pascal: string[] = [];
|
|
for (const file of context.getAllFiles()) {
|
|
const base = file.slice(file.lastIndexOf('/') + 1).toLowerCase();
|
|
const same = byName.get(base);
|
|
if (same) same.push(file);
|
|
else byName.set(base, [file]);
|
|
if (PASCAL_SOURCE.test(file)) pascal.push(file);
|
|
}
|
|
m.projectFiles = { byName, pascal };
|
|
}
|
|
return m.projectFiles;
|
|
}
|
|
|
|
/** The file of the unit a name names, nearest to `fromFile` when several units share it. */
|
|
function unitFileNamed(lower: string, fromFile: string, context: ResolutionContext): string | null {
|
|
const m = memos(context);
|
|
const key = `${fromFile}|${lower}`;
|
|
if (m.unitFiles.has(key)) return m.unitFiles.get(key)!;
|
|
if (!m.unitsByName) {
|
|
const units = new Map<string, string[]>();
|
|
const add = (name: string, file: string): void => {
|
|
const files = units.get(name);
|
|
if (files) files.push(file);
|
|
else units.set(name, [file]);
|
|
};
|
|
const headed = new Set<string>();
|
|
for (const n of context.getNodesByKind('module')) {
|
|
if (n.language !== 'pascal') continue;
|
|
add(n.name.toLowerCase(), n.filePath);
|
|
headed.add(n.filePath);
|
|
}
|
|
// A unit's file is named after it, which Delphi enforces: one whose
|
|
// `unit` header the parser lost to an error is the unit its file names.
|
|
for (const file of context.getAllFiles()) {
|
|
if (headed.has(file) || !/\.pas$/i.test(file)) continue;
|
|
add(file.slice(file.lastIndexOf('/') + 1, -'.pas'.length).toLowerCase(), file);
|
|
}
|
|
m.unitsByName = units;
|
|
}
|
|
const file = nearest(m.unitsByName.get(lower) ?? [], fromFile);
|
|
m.unitFiles.set(key, file);
|
|
return file;
|
|
}
|
|
|
|
/** A class (or record or interface) a type name names, seen from `fromFile`. */
|
|
function classNamed(typeName: string, fromFile: string, context: ResolutionContext, depth = 0): Node | null {
|
|
const simple = typeName.split('.').pop()!.toLowerCase();
|
|
const memo = memos(context).classByName;
|
|
const key = `${fromFile}|${simple}`;
|
|
if (memo.has(key)) return memo.get(key)!;
|
|
const named = pascalNamed(simple, context);
|
|
const visible = (kinds: ReadonlySet<string>): Node[] => {
|
|
// In the unit's own files, then in the unit used last, which declares the
|
|
// name Delphi sees; else in the one unit that does.
|
|
const candidates = named.filter((n) => kinds.has(n.kind));
|
|
for (const file of ownUnitFiles(fromFile, context)) {
|
|
const own = candidates.filter((n) => n.filePath === file);
|
|
if (own.length > 0) return own;
|
|
}
|
|
const used = usedUnitFiles(fromFile, context);
|
|
for (let i = used.length - 1; i >= 0; i--) {
|
|
const there = candidates.filter((n) => n.filePath === used[i]);
|
|
if (there.length > 0) return there;
|
|
}
|
|
return candidates.length > 0 && candidates.every((n) => n.filePath === candidates[0]!.filePath) ? candidates : [];
|
|
};
|
|
let cls = declarationOf(visible(CLASS_KINDS));
|
|
if (!cls && depth < 4) {
|
|
// A type alias names the class it stands for: `TVTAncestor = TVTAncestorVcl;`,
|
|
// and so do a pointer type and a class reference type, whose values reach
|
|
// its members (`PVirtualNode = ^TVirtualNode;`, `TSynLogClass = class of
|
|
// TSynLog;`, #2550).
|
|
for (const alias of visible(ALIAS_KINDS)) {
|
|
const line = (fileLines(alias.filePath, context)[alias.startLine - 1] ?? '').replace(/\/\/.*$|\{[^}]*\}/g, '');
|
|
const target = /=\s*(?:\^|class\s+of\b)?\s*([A-Za-z_][\w.]*)\s*;/i.exec(line)?.[1];
|
|
if (target && (cls = classNamed(target, alias.filePath, context, depth + 1))) break;
|
|
}
|
|
}
|
|
memo.set(key, cls);
|
|
return cls;
|
|
}
|
|
|
|
const ALIAS_KINDS: ReadonlySet<string> = new Set(['type_alias']);
|
|
|
|
/** The file whose path shares the longest directory prefix with `from`. */
|
|
function nearest(files: string[], from: string): string | null {
|
|
if (files.length <= 1) return files[0] ?? null;
|
|
const dirs = from.split('/');
|
|
let best: string | null = null;
|
|
let bestScore = -1;
|
|
for (const f of files) {
|
|
const parts = f.split('/');
|
|
let score = 0;
|
|
while (score < parts.length && score < dirs.length && parts[score] === dirs[score]) score++;
|
|
if (score > bestScore) {
|
|
best = f;
|
|
bestScore = score;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Declared types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function fileLines(filePath: string, context: ResolutionContext): string[] {
|
|
return context.getFileLines?.(filePath) ?? context.readFile(filePath)?.split(/\r?\n/) ?? [];
|
|
}
|
|
|
|
function escapeRegExp(s: string): string {
|
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
/** The class a member's value has: a field's or variable's declared type, a property's, a function's result. */
|
|
function typeOfMember(member: Node, context: ResolutionContext): Node | null {
|
|
const memo = memos(context).memberTypes;
|
|
if (memo.has(member.id)) return memo.get(member.id)!;
|
|
let typeName: string | undefined;
|
|
if (member.kind === 'method' || member.kind === 'function') {
|
|
typeName = member.returnType ?? /:\s*([A-Za-z_][\w.]*)\s*;?\s*$/.exec(member.signature ?? '')?.[1];
|
|
} else {
|
|
typeName = /^([A-Za-z_][\w.]*)/.exec(declaredTypeText(member, context))?.[1];
|
|
}
|
|
const type = typeName ? classNamed(typeName, member.filePath, context) : null;
|
|
memo.set(member.id, type);
|
|
return type;
|
|
}
|
|
|
|
/**
|
|
* The text of the type a field, variable, constant or property declares,
|
|
* from its declaration on: `TFoo; // the foo` for `FFoo: TFoo; // the foo`.
|
|
* A name in a comma list (`FOnLoad,` above `FOnSave: TSaveEvent;`) takes the
|
|
* type the list ends with, a few lines down. Empty when none is written, as
|
|
* for a published `property OnClick;`.
|
|
*/
|
|
function declaredTypeText(node: Node, context: ResolutionContext): string {
|
|
const lines = fileLines(node.filePath, context);
|
|
const name = escapeRegExp(node.name);
|
|
const re = node.kind === 'property'
|
|
? new RegExp(`\\bproperty\\s+${name}\\s*(?:\\[[^\\]]*\\])?\\s*:\\s*(.*)$`, 'i')
|
|
: new RegExp(`\\b${name}\\b(?:\\s*,\\s*[A-Za-z_]\\w*)*\\s*,?\\s*:(?!=)\\s*(.*)$`, 'i');
|
|
let text = '';
|
|
for (let i = node.startLine - 1; i < node.startLine + 5 && i < lines.length; i++) {
|
|
text += ' ' + lines[i]!.replace(/\/\/.*$|\{[^}]*\}|\(\*.*?\*\)/g, ' ');
|
|
const m = re.exec(text);
|
|
if (m) return m[1]!.trim();
|
|
// A list goes on only after a comma.
|
|
if (node.kind === 'property' || !/,\s*$/.test(text)) break;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Whether data holds a routine, by its declared type: one written in place
|
|
* (`FProc: procedure of object`, `F: reference to function: Integer`), a
|
|
* project type declared as one (`TOnLog = procedure(...) of object;`), or a
|
|
* type the project doesn't declare that the RTL's names say is one
|
|
* (`TNotifyEvent`, `TWndMethod`, `TProc<T>`).
|
|
*/
|
|
function isProceduralData(node: Node, context: ResolutionContext): boolean {
|
|
const memo = memos(context).procedural;
|
|
const hit = memo.get(node.id);
|
|
if (hit !== undefined) return hit;
|
|
const procedural = isProceduralTypeText(declaredTypeText(node, context), context, 0);
|
|
memo.set(node.id, procedural);
|
|
return procedural;
|
|
}
|
|
|
|
/**
|
|
* Whether a written type is a routine's: written out
|
|
* (`reference to function: Integer`), or named, with the name's declaration
|
|
* saying so, through an alias to another such name
|
|
* (`TFormatter = TFunc<Exception, string>;`). A name the project doesn't
|
|
* declare is a routine type when the RTL's names say so: `TProc`, `TFunc<T>`,
|
|
* and the names Delphi gives them (`TNotifyEvent`, `TWndMethod`).
|
|
*/
|
|
function isProceduralTypeText(text: string, context: ResolutionContext, depth: number): boolean {
|
|
return routineTypeVerdict(text, context, depth) === true;
|
|
}
|
|
|
|
/**
|
|
* Whether a written type is a routine's (see isProceduralTypeText): false
|
|
* for one of the System unit's value types, and undefined when nothing says,
|
|
* for a name neither the project declares nor the RTL's names say is a
|
|
* routine type (`TStringList`) or text that names no type (`array of TFoo`).
|
|
*/
|
|
function routineTypeVerdict(text: string, context: ResolutionContext, depth: number): boolean | undefined {
|
|
if (PROCEDURAL_TYPE.test(text)) return true;
|
|
const typeName = /^(?:type\s+)?([A-Za-z_][\w.]*)/i.exec(text)?.[1]?.split('.').pop()?.toLowerCase();
|
|
if (!typeName || depth > 3) return undefined;
|
|
const types = typesNamed(typeName, context);
|
|
if (types.length === 0) {
|
|
if (RTL_PROCEDURAL_TYPES.has(typeName) || /^t\w*(?:event|proc|method|callback)$/.test(typeName)) return true;
|
|
return SYSTEM_VALUE_TYPES.has(typeName) ? false : undefined;
|
|
}
|
|
return types.some((t) => t.kind === 'type_alias' && isProceduralTypeText(aliasedTypeText(t, context), context, depth + 1));
|
|
}
|
|
|
|
/** The System unit's value types, lowercased, which hold no routine. */
|
|
const SYSTEM_VALUE_TYPES: ReadonlySet<string> = new Set([
|
|
'boolean', 'bytebool', 'wordbool', 'longbool', 'integer', 'cardinal', 'shortint', 'smallint', 'longint', 'longword',
|
|
'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64', 'byte', 'word', 'dword', 'nativeint',
|
|
'nativeuint', 'single', 'double', 'extended', 'real', 'currency', 'comp', 'char', 'ansichar', 'widechar', 'string',
|
|
'ansistring', 'widestring', 'unicodestring', 'shortstring', 'rawbytestring', 'utf8string', 'pchar', 'pansichar',
|
|
'pwidechar', 'pointer', 'variant', 'olevariant', 'tdatetime', 'tdate', 'ttime',
|
|
]);
|
|
|
|
/**
|
|
* The project's Pascal types a name names, generic ones by the name before
|
|
* their parameters: `TSetParams<T: class>` for `TSetParams`.
|
|
*/
|
|
function typesNamed(lower: string, context: ResolutionContext): Node[] {
|
|
const m = memos(context);
|
|
if (!m.genericTypes) {
|
|
const generic = new Map<string, Node[]>();
|
|
const add = (n: Node): void => {
|
|
const open = n.name.indexOf('<');
|
|
if (n.language !== 'pascal' || open <= 0 || !(n.kind === 'type_alias' || CLASS_KINDS.has(n.kind))) return;
|
|
const base = n.name.slice(0, open).trim().toLowerCase();
|
|
const list = generic.get(base);
|
|
if (list) list.push(n);
|
|
else generic.set(base, [n]);
|
|
};
|
|
const kinds = ['type_alias', 'class', 'struct', 'interface'] as const;
|
|
// The names alone first, where the index can tell them, to fetch only
|
|
// the generic types.
|
|
const names = context.getNodeNamesOfKinds?.('pascal', kinds);
|
|
if (names) {
|
|
for (const name of new Set(names)) if (name.includes('<')) context.getNodesByName(name).forEach(add);
|
|
} else {
|
|
for (const kind of kinds) for (const n of context.iterateNodesByKind?.(kind) ?? context.getNodesByKind(kind)) add(n);
|
|
}
|
|
m.genericTypes = generic;
|
|
}
|
|
return pascalNamed(lower, context).filter((t) => t.kind === 'type_alias' || CLASS_KINDS.has(t.kind))
|
|
.concat(m.genericTypes.get(lower) ?? []);
|
|
}
|
|
|
|
/**
|
|
* What a type declaration says after its `=`, which may start on the next
|
|
* line: `procedure(...) of object` for `TOnHelper =` above it.
|
|
*/
|
|
function aliasedTypeText(alias: Node, context: ResolutionContext): string {
|
|
const lines = fileLines(alias.filePath, context);
|
|
let text = '';
|
|
for (let i = alias.startLine - 1; i < alias.startLine + 2 && i < lines.length; i++) {
|
|
text += ' ' + lines[i]!.replace(/\/\/.*$|\{[^}]*\}|\(\*.*?\*\)/g, ' ');
|
|
const eq = text.indexOf('=');
|
|
if (eq >= 0 && text.slice(eq + 1).trim()) return text.slice(eq + 1).trim();
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/** A routine type written out: `procedure(...) of object`, `function: T`, `reference to procedure`. */
|
|
const PROCEDURAL_TYPE = /^(?:reference\s+to\s+)?(?:procedure|function)\b/i;
|
|
|
|
/** Whether a routine has a parameter without a default value, which a call must pass. */
|
|
function needsArguments(routine: Node): boolean {
|
|
const signature = routine.signature?.trim();
|
|
if (!signature?.startsWith('(')) return false;
|
|
let depth = 0;
|
|
let end = signature.length;
|
|
for (let i = 0; i < signature.length; i++) {
|
|
if (signature[i] === '(') depth++;
|
|
else if (signature[i] === ')' && --depth === 0) {
|
|
end = i;
|
|
break;
|
|
}
|
|
}
|
|
return signature.slice(1, end).split(';').some((param) => param.trim() !== '' && !param.includes('='));
|
|
}
|
|
|
|
/** Whether a routine gives a value — a function or a constructor — rather than a procedure. */
|
|
function yieldsValue(routine: Node, context: ResolutionContext): boolean {
|
|
if (routine.returnType) return true;
|
|
const keyword = routineKeyword(routine, context);
|
|
return keyword === 'function' || keyword === 'constructor';
|
|
}
|
|
|
|
/** The keyword that declares a routine (`function`, `procedure`, `constructor`, …), lowercased, or null. */
|
|
function routineKeyword(routine: Node, context: ResolutionContext): string | null {
|
|
const memo = memos(context).keywords;
|
|
const hit = memo.get(routine.id);
|
|
if (hit !== undefined) return hit;
|
|
const line = fileLines(routine.filePath, context)[routine.startLine - 1] ?? '';
|
|
const keyword = /\b(function|procedure|constructor|destructor|operator)\s/i.exec(line)?.[1]!.toLowerCase() ?? null;
|
|
memo.set(routine.id, keyword);
|
|
return keyword;
|
|
}
|