mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 12:00:02 +08:00
A Pascal call to a name the compiler or the System unit gives meaning
to (SizeOf, TypeInfo, High, Low, Length, Inc, Dec, Assigned, Ord, Exit,
Include, Move, Assert, typecasts like Pointer(P)) went to name
matching, which linked any project member of that name: mORMot's
TRttiProp.TypeInfo, a record's length or high field, a property named
Pointer. The built-in name check compared case, so length(S) and dec(N)
got through too. And a call that resolved to a field, property or
variable linked whatever data had the name.
Now a bare call to a System name links only to a declaration its scope
reaches: the class and its ancestors, the unit, the units it uses last
first, skipping a unit's stand-in declared under {$IFDEF} or {$IF}. A
call resolved to data stands only when the data's declared type is a
routine type (written out, a project alias to one, generic or on two
lines, or an RTL procedural type by name), retargeted to what the scope
declares. A name the calling routine declares itself links nothing; a
name the scope doesn't declare at all (a with object's member, a
declaration the parse misplaced) links to the one routine-holding field
or variable of that name. Built-in names compare without case; the RTL
namespaces keep their spelling, except system. Scope lookups now see a
unit whose unit header the parser lost, by its file name (its indented,
lost class members aren't taken for its routines), and an include file
shares its unit's scope and uses clauses. A with body's bare call to a
System name is not extracted, since it may be the with object's.
Validation, edges removed/added against origin/main: SynEdit -243/+50,
HeidiSQL -858/+296, mORMot2 -9039/+987, DelphiMVCFramework -2420/+242,
DWScript -705/+222; leveldb and SDWebImage byte-identical. Sampled
precision of calls to data, before and after: 20/50 and 50/50; of calls
to System names: 2/50 and 47/47. Sampled removed edges: 40/40 wrong.
Follow-up to #2514.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
988 lines
44 KiB
TypeScript
988 lines
44 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.
|
|
*
|
|
* Written without its arguments, a routine that needs some is a method
|
|
* pointer too, in any of them.
|
|
*
|
|
* 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';
|
|
interface Found {
|
|
node: Node;
|
|
resolvedBy: ResolvedRef['resolvedBy'];
|
|
confidence: number;
|
|
}
|
|
|
|
const PARENLESS = /^\{(call|read|ref|recv)(?:@([^}]+))?\}\.(.+)$/;
|
|
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;
|
|
const mode = m[1] as Mode;
|
|
const path = m[3]!.split('.');
|
|
// Most such names read data, often the framework's: a name no Pascal
|
|
// routine has is settled here, before any lookup in scope.
|
|
if (routineNames(context)?.has(path[path.length - 1]!.toLowerCase()) === false) return null;
|
|
let cls: Node | null = null;
|
|
if (m[2]) {
|
|
cls = classByQualifiedName(m[2], 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);
|
|
if (!found) return null;
|
|
// 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;
|
|
const 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 };
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* 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 gatePascalDataCall(result: ResolvedRef, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
|
const target = context.getNodeById?.(result.targetNodeId);
|
|
if (!target || !DATA_KINDS.has(target.kind)) return result;
|
|
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 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.
|
|
*/
|
|
function declarationInScope(lower: string, cls: Node | null, ref: UnresolvedRef, context: ResolutionContext): Found | null {
|
|
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. */
|
|
function memberThroughPath(path: string[], cls: Node | null, ref: UnresolvedRef, context: ResolutionContext): Found | null {
|
|
const head = headOf(path[0]!, cls, ref, context);
|
|
if (!head) return null;
|
|
let type: Node | null;
|
|
let rest = path.slice(1);
|
|
if (head.unitFile) {
|
|
// `UnitName.Routine`, `UnitName.GlobalVar.Method`.
|
|
const decl = unitDeclaration(head.unitFile, rest[0]!.toLowerCase(), context, ref.filePath!);
|
|
if (!decl) return null;
|
|
if (rest.length === 1) return isRoutine(decl) ? { node: decl, resolvedBy: 'import', confidence: 0.9 } : null;
|
|
type = typeOfMember(decl, context);
|
|
rest = rest.slice(1);
|
|
} else {
|
|
type = head.type;
|
|
}
|
|
for (const link of rest.slice(0, -1)) {
|
|
const member = type ? memberInHierarchy(type, link.toLowerCase(), context) : null;
|
|
if (!member) return null;
|
|
type = typeOfMember(member, context);
|
|
}
|
|
const member = type ? memberInHierarchy(type, rest[rest.length - 1]!.toLowerCase(), context) : null;
|
|
return member && isRoutine(member) ? { node: member, resolvedBy: 'instance-method', confidence: 0.9 } : 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, or a unit.
|
|
*/
|
|
function headOf(name: string, cls: Node | null, ref: UnresolvedRef, context: ResolutionContext): { type: Node | null; 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 { type: CLASS_KINDS.has(decl.kind) ? decl : typeOfMember(decl, context) };
|
|
const unitFile = unitFileNamed(lower, ref.filePath!, context);
|
|
return unitFile ? { type: null, unitFile } : null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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[]>;
|
|
}
|
|
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(),
|
|
};
|
|
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;`.
|
|
for (const alias of visible(ALIAS_KINDS)) {
|
|
const line = (fileLines(alias.filePath, context)[alias.startLine - 1] ?? '').replace(/\/\/.*$|\{[^}]*\}/g, '');
|
|
const target = /=\s*([A-Za-z_][\w.]*)\s*;/.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 {
|
|
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 false;
|
|
const types = typesNamed(typeName, context);
|
|
if (types.length === 0) return RTL_PROCEDURAL_TYPES.has(typeName) || /^t\w*(?:event|proc|method|callback)$/.test(typeName);
|
|
return types.some((t) => t.kind === 'type_alias' && isProceduralTypeText(aliasedTypeText(t, context), context, depth + 1));
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|