mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-22 01:07:18 +08:00
79cd737bec
#2493 resolved calls on declared pointers, values and references by C++ lookup. Calls whose receiver type came from an initializer or a return type still went by the type's last name: - An `auto` local now has the type its initializer has, as written where that is declared: a cast or smart pointer written out (`std::unique_ptr<Iterator>(…)`, `static_cast<InstrumentedMutex*>(arg)`), `new` / `std::make_unique` / a construction of a class, or what a function or method is declared to return (`txn->GetIterator(…)`), with the initializer read to its `;` across lines. Its calls then go through matchCppDeclaredTypeCall from that site. A type that depends on a template parameter (`T*`, `std::shared_ptr<T>`) is left to the old path. - A chained call `a->b()->c()` looks `c` up on the class C++ finds for the written return type of `b`, through a smart pointer too (matchCppChainOnWrittenType), before the last-name path. - A type from outside the project gets no project method: a std/abseil library type (#2413's gate, now applied before the last-name match, so a namesake class like fmt's own `ostream` no longer wins), and a capitalized type under a namespace the project declares no type in (`absl::Status`, which reached `upb::Status::ok`). - A bare call in a method types an `auto` local from the class's own or inherited method; a `template <…>` header followed by macros or specifiers still names its parameters; a trailing annotation macro (`T* PROTOBUF_NONNULL`) is no part of a return type; and memoized `auto` types are worked out with a fresh depth budget, so a deep chain that reads a local first no longer leaves it untyped for its other calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
696 lines
30 KiB
TypeScript
696 lines
30 KiB
TypeScript
/**
|
|
* C++ type aliases in a declared type: `typedef X Y;` and `using Y = X;`.
|
|
*
|
|
* Both extractors index an alias as a `type_alias` node and record nothing
|
|
* about the type it names, so a receiver declared through one used to be
|
|
* looked up by the alias's own name. google/leveldb's `MemTable::Add` calls
|
|
* `table_.Insert(buf)` on a member `Table table_;`, with
|
|
* `typedef SkipList<const char*, KeyComparator> Table;` declared beside it in
|
|
* the class. No class `Table` has an `Insert` (the one class `Table` is the
|
|
* unrelated `leveldb::Table`), so the call fell through to a guess by the
|
|
* receiver's name and reached `HandleTable::Insert`.
|
|
*
|
|
* A name is looked up the way C++ looks it up: from the innermost scope of
|
|
* the code that uses it outwards. That is an alias declared earlier in the
|
|
* calling function, then the caller's class and that class's bases, then the
|
|
* classes and namespaces around it, and the global scope. The first scope
|
|
* that declares the name decides; a class there is the type itself. An alias
|
|
* there is read from its own declaration and followed from the alias's own
|
|
* scope, for a few hops at most. A qualified name is resolved a segment at a
|
|
* time, so `Table::Iterator` is `SkipList::Iterator` and `MemTable::Table`
|
|
* is `SkipList`. Template arguments are dropped. An alias of what only a
|
|
* template argument decides (`using Type = GenericType;` in the template
|
|
* that declares `GenericType`, `typename Traits::Field`) or of a
|
|
* `decltype(…)` names no particular class, and is reported as such.
|
|
*/
|
|
import type { Node } from '../types';
|
|
import type { ResolutionContext, UnresolvedRef } from './types';
|
|
|
|
/** Kinds that declare a type name in a scope, besides an alias. */
|
|
const DECLARING_KINDS: ReadonlySet<string> = new Set([
|
|
'class', 'struct', 'union', 'enum', 'interface', 'namespace', 'module',
|
|
]);
|
|
const CLASS_KINDS: ReadonlySet<string> = new Set(['class', 'struct', 'union']);
|
|
|
|
/** How many aliases one name is followed through (`A` → `B` → `C`). */
|
|
const MAX_HOPS = 4;
|
|
/** How many base classes deep a member is looked for. */
|
|
const MAX_BASE_DEPTH = 4;
|
|
/** How far past a class's first line its base clause may run. */
|
|
const HEAD_LINES = 12;
|
|
|
|
/**
|
|
* What a type named through an alias resolved to: the aliased type as written
|
|
* in `scope` (`['SkipList']`), then the rest of the name that followed the
|
|
* alias (`['Iterator']` for `Table::Iterator`).
|
|
*/
|
|
export interface CppAliasedType {
|
|
target: string[];
|
|
rest: string[];
|
|
/** The qualified scope `target` is written in (`''` for the global scope). */
|
|
scope: string;
|
|
/** An alias on the way named a pointer (`using Field = const FieldDescriptor*;`). */
|
|
pointer: boolean;
|
|
/**
|
|
* The type is a class template of the project (or a class nested in one),
|
|
* whose specializations can declare members the primary template doesn't.
|
|
*/
|
|
classTemplate: boolean;
|
|
}
|
|
|
|
/** A written type's `::` segments, each with whether template arguments followed it. */
|
|
interface TypeName {
|
|
names: string[];
|
|
templated: boolean[];
|
|
}
|
|
|
|
/** `SkipList<const char*, Cmp<int>>::Iterator` → `SkipList::Iterator`. */
|
|
export function stripCppTemplateArguments(text: string): string {
|
|
let out = '';
|
|
let depth = 0;
|
|
for (const c of text) {
|
|
if (c === '<') depth++;
|
|
else if (c === '>') depth = Math.max(0, depth - 1);
|
|
else if (depth === 0) out += c;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** The `::` segments of a written type, or null when it is not a plain (possibly qualified) type name. */
|
|
function cppTypeName(raw: string): TypeName | null {
|
|
const parts: string[] = [];
|
|
const templated: boolean[] = [];
|
|
let depth = 0;
|
|
let part = '';
|
|
let args = false;
|
|
const text = raw.replace(/\b(?:const|volatile|mutable|typename|template|class|struct|union|enum)\b/g, ' ').replace(/[&*]+/g, ' ');
|
|
for (let i = 0; i < text.length; i++) {
|
|
const c = text[i]!;
|
|
if (c === '<') {
|
|
depth++;
|
|
args = true;
|
|
} else if (c === '>') depth = Math.max(0, depth - 1);
|
|
else if (depth > 0) continue;
|
|
else if (c === ':' && text[i + 1] === ':') {
|
|
parts.push(part.trim());
|
|
templated.push(args);
|
|
part = '';
|
|
args = false;
|
|
i++;
|
|
} else part += c;
|
|
}
|
|
parts.push(part.trim());
|
|
templated.push(args);
|
|
if (parts[0] === '') {
|
|
// A leading `::` names the global scope.
|
|
parts.shift();
|
|
templated.shift();
|
|
}
|
|
if (parts.length === 0 || !parts.every((p) => /^[A-Za-z_]\w*$/.test(p))) return null;
|
|
return { names: parts, templated };
|
|
}
|
|
|
|
/** Is a written type a pointer (`Table*`, `const Foo* const`), not counting its template arguments? */
|
|
export function isCppPointerType(raw: string): boolean {
|
|
return stripCppTemplateArguments(raw).includes('*');
|
|
}
|
|
|
|
/**
|
|
* The `::` segments of a written type: `const ns::Foo<T>&` → `['ns', 'Foo']`.
|
|
* Null when what is written is not a plain (possibly qualified) type name —
|
|
* `unsigned int`, `decltype(…)`, a function type.
|
|
*/
|
|
export function cppTypeSegments(raw: string): string[] | null {
|
|
return cppTypeName(raw)?.names ?? null;
|
|
}
|
|
|
|
/**
|
|
* The top-level arguments of the first template argument list in a written
|
|
* type: `['Iterator']` for `const std::unique_ptr<Iterator>&`, `['Foo',
|
|
* 'Deleter<Foo>']` for `std::unique_ptr<Foo, Deleter<Foo>>`. Null when it has
|
|
* none, or the list is not closed.
|
|
*/
|
|
export function cppTemplateArguments(raw: string): string[] | null {
|
|
const open = raw.indexOf('<');
|
|
if (open < 0) return null;
|
|
let depth = 0;
|
|
for (let i = open; i < raw.length; i++) {
|
|
if (raw[i] === '<') depth++;
|
|
else if (raw[i] === '>' && --depth === 0) return splitTopLevel(raw.slice(open + 1, i)).map((a) => a.trim());
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** C and C++ declare the names a C++ file can see (a header may be either). */
|
|
function isCFamily(n: Node): boolean {
|
|
return n.language === 'cpp' || n.language === 'c';
|
|
}
|
|
|
|
/** Scopes visible from inside the scope `qualified`, innermost first, ending with the global scope. */
|
|
function scopesWithin(qualified: string): string[] {
|
|
const parts = qualified ? qualified.split('::') : [];
|
|
const scopes: string[] = [];
|
|
for (let i = parts.length; i >= 0; i--) scopes.push(parts.slice(0, i).join('::'));
|
|
return scopes;
|
|
}
|
|
|
|
/** The scope a declaration sits in: `leveldb::MemTable` for `leveldb::MemTable::Table`. */
|
|
function parentScope(qualified: string): string {
|
|
const cut = qualified.lastIndexOf('::');
|
|
return cut < 0 ? '' : qualified.slice(0, cut);
|
|
}
|
|
|
|
/** The scope the code that makes `ref` is written in: its class for a method, itself for a class. */
|
|
function callerScope(caller: Node | null | undefined): string {
|
|
if (!caller || caller.kind === 'file') return '';
|
|
return DECLARING_KINDS.has(caller.kind) ? caller.qualifiedName : parentScope(caller.qualifiedName);
|
|
}
|
|
|
|
function linesOf(filePath: string, context: ResolutionContext): string[] | null {
|
|
return context.getFileLines?.(filePath) ?? context.readFile(filePath)?.split(/\r?\n/) ?? null;
|
|
}
|
|
|
|
const escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
|
/** The text of `node`'s declaration, cut to its columns when they fit the lines. */
|
|
function declarationText(node: Node, context: ResolutionContext): string | null {
|
|
const lines = linesOf(node.filePath, context);
|
|
if (!lines) return null;
|
|
const span = lines.slice(node.startLine - 1, node.endLine ?? node.startLine);
|
|
if (span.length === 0) return null;
|
|
const whole = span.join('\n');
|
|
if (span.length === 1) {
|
|
span[0] = span[0]!.slice(node.startColumn, node.endColumn);
|
|
} else {
|
|
span[0] = span[0]!.slice(node.startColumn);
|
|
span[span.length - 1] = span[span.length - 1]!.slice(0, node.endColumn);
|
|
}
|
|
const cut = span.join('\n');
|
|
return /\b(?:typedef|using)\b/.test(cut) ? cut : whole;
|
|
}
|
|
|
|
/** Split `text` on commas outside `<>`, `()`, `[]` and `{}`. */
|
|
function splitTopLevel(text: string): string[] {
|
|
const parts: string[] = [];
|
|
let depth = 0;
|
|
let start = 0;
|
|
for (let i = 0; i < text.length; i++) {
|
|
const c = text[i]!;
|
|
if ('<([{'.includes(c)) depth++;
|
|
else if ('>)]}'.includes(c)) depth = Math.max(0, depth - 1);
|
|
else if (c === ',' && depth === 0) {
|
|
parts.push(text.slice(start, i));
|
|
start = i + 1;
|
|
}
|
|
}
|
|
parts.push(text.slice(start));
|
|
return parts;
|
|
}
|
|
|
|
/**
|
|
* What an alias of `name` declares in `text`, as written: `SkipList<const
|
|
* char*, KeyComparator>` for `typedef SkipList<const char*, KeyComparator>
|
|
* Table;` and for `using Table = SkipList<const char*, KeyComparator>;`.
|
|
* Null when `text` declares no alias of that name.
|
|
*/
|
|
function aliasedTypeIn(text: string, name: string): string | null {
|
|
const escaped = escapeRegExp(name);
|
|
const using = new RegExp(String.raw`\busing\s+${escaped}\s*(?:\[\[[^\]]*\]\]\s*)?=\s*([^;]+)`).exec(text);
|
|
if (using) return using[1]!.trim();
|
|
const typedef = /\btypedef\b([^;]+)/.exec(text);
|
|
if (!typedef) return null;
|
|
// `typedef Foo* FooPtr, FooObj;` indexes its first declarator; the type is
|
|
// what precedes that declarator's name.
|
|
const declared = new RegExp(String.raw`^([\s\S]*?)\b${escaped}\s*$`).exec(splitTopLevel(typedef[1]!)[0]!);
|
|
return declared?.[1]?.trim() || null;
|
|
}
|
|
|
|
/** Is `alias` an alias template (`template <typename T> using Vec = std::vector<T>;`)? */
|
|
function isAliasTemplate(alias: Node, context: ResolutionContext): boolean {
|
|
const lines = linesOf(alias.filePath, context);
|
|
if (!lines) return false;
|
|
const before = [
|
|
...lines.slice(Math.max(0, alias.startLine - 4), alias.startLine - 1),
|
|
(lines[alias.startLine - 1] ?? '').slice(0, alias.startColumn),
|
|
].join(' ');
|
|
return /\btemplate\s*<[^;{}]*>\s*$/.test(before);
|
|
}
|
|
|
|
const LOCAL_ALIAS_LINES = new WeakMap<ResolutionContext, Map<string, number[]>>();
|
|
|
|
/** The lines of a function's body that declare an alias (`typedef …;`, `using X = …;`), last first. */
|
|
function localAliasLines(caller: Node, lines: readonly string[], context: ResolutionContext): number[] {
|
|
let memo = LOCAL_ALIAS_LINES.get(context);
|
|
if (!memo) LOCAL_ALIAS_LINES.set(context, (memo = new Map()));
|
|
let found = memo.get(caller.id);
|
|
if (!found) {
|
|
found = [];
|
|
const last = Math.min(caller.endLine ?? caller.startLine, lines.length);
|
|
for (let i = last - 1; i >= caller.startLine - 1 && i >= 0; i--) {
|
|
if (/\b(?:typedef\b|using\s+\w+\s*=)/.test(lines[i]!)) found.push(i);
|
|
}
|
|
memo.set(caller.id, found);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/**
|
|
* The type an alias declared earlier in the calling function names, as
|
|
* written. Those aliases are not indexed, so the function's own lines are
|
|
* read, from the call back to the function's first line.
|
|
*/
|
|
function localAliasedType(name: string, caller: Node | null | undefined, ref: UnresolvedRef, context: ResolutionContext): string | null | undefined {
|
|
if (!caller || (caller.kind !== 'function' && caller.kind !== 'method') || caller.filePath !== ref.filePath) return undefined;
|
|
const lines = linesOf(ref.filePath, context);
|
|
if (!lines) return undefined;
|
|
const declares = new RegExp(String.raw`\b(?:using\s+${escapeRegExp(name)}\s*=|typedef\b[^;]*\b${escapeRegExp(name)}\s*[;,])`);
|
|
for (const i of localAliasLines(caller, lines, context)) {
|
|
if (i > ref.line - 1) continue;
|
|
const line = lines[i]!;
|
|
if (line.includes(name) && declares.test(line)) return aliasedTypeIn(line, name);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** What a scope declares under a name. */
|
|
type Declared =
|
|
| { alias: Node }
|
|
/** A class, struct or union, by its qualified name. */
|
|
| { cls: string }
|
|
/** An enum, namespace or other non-class type. */
|
|
| 'other'
|
|
/**
|
|
* Aliases naming different types, none of them the calling file's: rocksdb's
|
|
* `using DBWithTTLImplBase = …;` in each arm of an `#if USE_COROUTINES`.
|
|
*/
|
|
| { ambiguous: Node[] }
|
|
| undefined;
|
|
|
|
/** Which declarations a lookup can see; every one of them unless a caller narrows it. */
|
|
type Visible = (n: Node) => boolean;
|
|
|
|
/** C and C++ declarations named `name` that can stand for a type or scope. */
|
|
function typeDeclarations(name: string, context: ResolutionContext, visible?: Visible): Node[] {
|
|
return context.getNodesByName(name).filter((n) =>
|
|
isCFamily(n) && (n.kind === 'type_alias' || DECLARING_KINDS.has(n.kind)) && (!visible || visible(n)));
|
|
}
|
|
|
|
function declaredIn(
|
|
scope: string,
|
|
name: string,
|
|
named: readonly Node[],
|
|
templated: boolean,
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext,
|
|
): Declared {
|
|
const qualified = scope ? `${scope}::${name}` : name;
|
|
let here = named.filter((n) => n.qualifiedName === qualified);
|
|
// An alias template is only ever named with template arguments.
|
|
if (!templated) here = here.filter((n) => n.kind !== 'type_alias' || !isAliasTemplate(n, context));
|
|
if (here.length === 0) return undefined;
|
|
// `typedef struct Node Node;` declares the struct as well.
|
|
if (here.some((n) => CLASS_KINDS.has(n.kind))) return { cls: qualified };
|
|
if (here.some((n) => n.kind !== 'type_alias')) return 'other';
|
|
const alias = pickAlias(here, ref, context);
|
|
return alias ? { alias } : { ambiguous: here };
|
|
}
|
|
|
|
/**
|
|
* One of several aliases a scope declares under one name (a translation unit
|
|
* sees one of them): the one in the calling file, then in its header, then
|
|
* any of them when they all name the same type.
|
|
*/
|
|
function pickAlias(aliases: Node[], ref: UnresolvedRef, context: ResolutionContext): Node | null {
|
|
if (aliases.length === 1) return aliases[0]!;
|
|
const sameFile = aliases.filter((a) => a.filePath === ref.filePath);
|
|
if (sameFile.length === 1) return sameFile[0]!;
|
|
const stem = (p: string): string => p.replace(/\.[^./\\]+$/, '');
|
|
const header = aliases.filter((a) => stem(a.filePath) === stem(ref.filePath));
|
|
if (header.length === 1) return header[0]!;
|
|
const named = new Set(aliases.map((a) => {
|
|
const text = declarationText(a, context);
|
|
const type = text ? aliasedTypeIn(text, a.name) : null;
|
|
return type ? (cppTypeSegments(type)?.join('::') ?? type) : null;
|
|
}));
|
|
return named.size === 1 && !named.has(null) ? aliases[0]! : null;
|
|
}
|
|
|
|
const BASES = new WeakMap<ResolutionContext, Map<string, string[]>>();
|
|
const TEMPLATE_PARAMETERS = new WeakMap<ResolutionContext, Map<string, ReadonlySet<string>>>();
|
|
|
|
/** Drop the memos of class bases, template parameters and local aliases (see ReferenceResolver.clearCaches). */
|
|
export function clearCppTypeAliasMemos(context: ResolutionContext): void {
|
|
BASES.delete(context);
|
|
TEMPLATE_PARAMETERS.delete(context);
|
|
LOCAL_ALIAS_LINES.delete(context);
|
|
}
|
|
|
|
/** Add the parameter names of the `template <…>` header that ends right where a declaration starts. */
|
|
function collectTemplateHeader(lines: readonly string[], startLine: number, startColumn: number, names: Set<string>): void {
|
|
const before = [...lines.slice(Math.max(0, startLine - 4), startLine - 1), (lines[startLine - 1] ?? '').slice(0, startColumn)].join('\n');
|
|
const at = before.lastIndexOf('template');
|
|
// Specifiers, attributes and macros may come between the header and where
|
|
// the declaration starts: protobuf's `template <typename T, typename...
|
|
// Args>` / `PROTOBUF_NDEBUG_INLINE static T* Create(…)`.
|
|
const header = at < 0 ? null : /^template\s*<([\s\S]*)>(?:\s*(?:\[\[[^\]]*\]\]|(?:static|inline|constexpr|consteval|virtual|explicit|friend|extern)\b|[A-Z][A-Z0-9]*_[A-Z0-9_]*\b(?:\s*\([^()]*\))?))*\s*$/.exec(before.slice(at));
|
|
if (!header) return;
|
|
for (const item of splitTopLevel(header[1]!)) {
|
|
// `typename T = int`, `template <typename> class Policy`, `typename... Ts`, `int N`.
|
|
const declared = stripCppTemplateArguments(item).split('=')[0]!;
|
|
const name = /([A-Za-z_]\w*)\s*$/.exec(declared)?.[1];
|
|
if (name && name !== 'typename' && name !== 'class') names.add(name);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The template parameters in scope where `node` is declared: its own
|
|
* `template <…>` header and those of the class templates around it.
|
|
*/
|
|
function templateParametersAround(node: Node, context: ResolutionContext): ReadonlySet<string> {
|
|
let memo = TEMPLATE_PARAMETERS.get(context);
|
|
if (!memo) TEMPLATE_PARAMETERS.set(context, (memo = new Map()));
|
|
const hit = memo.get(node.id);
|
|
if (hit) return hit;
|
|
const names = new Set<string>();
|
|
const lines = linesOf(node.filePath, context);
|
|
if (lines) {
|
|
collectTemplateHeader(lines, node.startLine, node.startColumn, names);
|
|
for (let scope = parentScope(node.qualifiedName); scope; scope = parentScope(scope)) {
|
|
for (const cls of context.getNodesByQualifiedName(scope)) {
|
|
if (!isCFamily(cls) || !CLASS_KINDS.has(cls.kind) || cls.filePath !== node.filePath) continue;
|
|
if (node.startLine < cls.startLine || node.startLine > (cls.endLine ?? cls.startLine)) continue;
|
|
collectTemplateHeader(lines, cls.startLine, cls.startColumn, names);
|
|
}
|
|
}
|
|
}
|
|
memo.set(node.id, names);
|
|
return names;
|
|
}
|
|
|
|
/**
|
|
* The qualified names of the classes `cls` derives from, read from the base
|
|
* clause of its own declaration (`struct ParseProto2Descriptor :
|
|
* Proto2Descriptor {`) and looked up from the scope the class is declared in.
|
|
*/
|
|
function baseClassesOf(cls: string, context: ResolutionContext): string[] {
|
|
let memo = BASES.get(context);
|
|
if (!memo) BASES.set(context, (memo = new Map()));
|
|
const hit = memo.get(cls);
|
|
if (hit) return hit;
|
|
memo.set(cls, []);
|
|
const bases: string[] = [];
|
|
for (const decl of context.getNodesByQualifiedName(cls)) {
|
|
if (!isCFamily(decl) || !CLASS_KINDS.has(decl.kind)) continue;
|
|
const lines = linesOf(decl.filePath, context);
|
|
if (!lines) continue;
|
|
const head = lines.slice(decl.startLine - 1, decl.startLine - 1 + HEAD_LINES);
|
|
if (head.length === 0) continue;
|
|
head[0] = head[0]!.slice(decl.startColumn);
|
|
const text = head.join('\n');
|
|
const brace = text.indexOf('{');
|
|
// A forward declaration (`class Foo;`) has no base clause of its own.
|
|
if (brace < 0 || text.slice(0, brace).includes(';')) continue;
|
|
const colon = text.slice(0, brace).search(/(?<!:):(?!:)/);
|
|
if (colon < 0) continue;
|
|
for (const part of splitTopLevel(text.slice(colon + 1, brace))) {
|
|
const name = cppTypeName(part.replace(/\b(?:public|protected|private|virtual)\b/g, ' '));
|
|
if (!name) continue;
|
|
const base = classNamed(name.names, scopesWithin(parentScope(cls)), context);
|
|
if (base && base !== cls && !bases.includes(base)) bases.push(base);
|
|
}
|
|
}
|
|
memo.set(cls, bases);
|
|
return bases;
|
|
}
|
|
|
|
/** The qualified name of the class `names` spells, looked up from `scopes`. */
|
|
function classNamed(names: readonly string[], scopes: readonly string[], context: ResolutionContext): string | undefined {
|
|
const spelled = names.join('::');
|
|
for (const scope of scopes) {
|
|
const qualified = scope ? `${scope}::${spelled}` : spelled;
|
|
if (context.getNodesByQualifiedName(qualified).some((n) => isCFamily(n) && CLASS_KINDS.has(n.kind))) return qualified;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** `name` declared in class `cls` or one of its bases. */
|
|
function memberOf(
|
|
cls: string,
|
|
name: string,
|
|
named: readonly Node[],
|
|
templated: boolean,
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext,
|
|
depth = 0,
|
|
seen: Set<string> = new Set(),
|
|
): Declared {
|
|
const own = declaredIn(cls, name, named, templated, ref, context);
|
|
if (own !== undefined || depth >= MAX_BASE_DEPTH) return own;
|
|
seen.add(cls);
|
|
for (const base of baseClassesOf(cls, context)) {
|
|
if (seen.has(base)) continue;
|
|
const inherited = memberOf(base, name, named, templated, ref, context, depth + 1, seen);
|
|
if (inherited !== undefined) return inherited;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** `name` looked up unqualified from `scopes`, innermost first; a class scope includes its bases. */
|
|
function lookupUnqualified(
|
|
name: string,
|
|
templated: boolean,
|
|
scopes: readonly string[],
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext,
|
|
visible?: Visible,
|
|
): Declared {
|
|
const named = typeDeclarations(name, context, visible);
|
|
if (named.length === 0) return undefined;
|
|
for (const scope of scopes) {
|
|
const found = scope ? memberOf(scope, name, named, templated, ref, context) : declaredIn('', name, named, templated, ref, context);
|
|
if (found !== undefined) return found;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* The type a C++ declared type names once the aliases in it are followed,
|
|
* looked up from the code that makes `ref`.
|
|
* - `undefined`: the type is not an alias; the caller keeps its own reading.
|
|
* - `null`: an alias that names no particular class: a template
|
|
* parameter's type (`using Type = GenericType;`, `typename
|
|
* Traits::Field`), a `decltype(…)` or a function type, or a chain of
|
|
* aliases too long to follow.
|
|
*/
|
|
export function resolveCppAliasedType(rawType: string, ref: UnresolvedRef, context: ResolutionContext): CppAliasedType | null | undefined {
|
|
const name = cppTypeName(rawType);
|
|
if (!name) return undefined;
|
|
const caller = context.getNodeById?.(ref.fromNodeId);
|
|
const scope = callerScope(caller);
|
|
const local = localAliasedType(name.names[0]!, caller, ref, context);
|
|
const resolved = local !== undefined
|
|
? expand(local, name.names.slice(1), name.templated.slice(1), scope, ref, context, 1,
|
|
caller ? templateParametersAround(caller, context) : undefined)
|
|
: follow(name, scope, ref, context, 0);
|
|
// `Table* table` is a pointer whatever `Table` names.
|
|
return resolved && isCppPointerType(rawType) ? { ...resolved, pointer: true } : resolved;
|
|
}
|
|
|
|
/**
|
|
* The name receiver inference gives a type resolved through aliases: the
|
|
* aliased type's last segment (`SkipList`), as for any declared type, with
|
|
* the nested part of a name reached through the alias kept on it
|
|
* (`SkipList::Iterator`), since that owner is what names the nested type.
|
|
*/
|
|
export function cppAliasedTypeName(resolved: CppAliasedType): string {
|
|
return [resolved.target[resolved.target.length - 1]!, ...resolved.rest].join('::');
|
|
}
|
|
|
|
/** The qualified name of the project class a type resolved through aliases is, if it is one: `leveldb::SkipList::Iterator`. */
|
|
export function cppAliasedClass(resolved: CppAliasedType, context: ResolutionContext): string | undefined {
|
|
return classNamed([...resolved.target, ...resolved.rest], scopesWithin(resolved.scope), context);
|
|
}
|
|
|
|
/** What a C++ type name names when it is looked up for a class (see cppClassNamed). */
|
|
export type CppNamedClass =
|
|
/** A class, by its qualified name. */
|
|
| { cls: string }
|
|
/**
|
|
* An alias that leads to none of the project's classes: one of a type
|
|
* outside the project (`template <bool B> using bool_constant =
|
|
* std::integral_constant<bool, B>;`), of what a template argument decides,
|
|
* or one each `#if` arm declares differently (the first declaration).
|
|
*/
|
|
| { alias: Node }
|
|
/** No particular class: an enum, a member of a type outside the project. */
|
|
| null
|
|
/** Nothing in the scopes looked in declares it. */
|
|
| undefined;
|
|
|
|
/**
|
|
* The class a C++ type written as `written` names, looked up from `scopes`
|
|
* (innermost first) as a declared type is — a class scope with its bases, a
|
|
* qualified name a segment at a time — and followed through an alias it
|
|
* reaches: rocksdb's `InternalIterator` under `using InternalIterator =
|
|
* InternalIteratorBase<Slice>;` names `InternalIteratorBase`. `visible`
|
|
* narrows the declarations the lookup can see.
|
|
*/
|
|
export function cppClassNamed(
|
|
written: string,
|
|
scopes: readonly string[],
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext,
|
|
visible?: Visible,
|
|
): CppNamedClass {
|
|
const name = cppTypeName(written);
|
|
if (!name) return null;
|
|
const { found, next } = lookupSegments(name, scopes, ref, context, visible);
|
|
if (found === undefined) return undefined;
|
|
if (typeof found !== 'object') return null;
|
|
if ('cls' in found) return found;
|
|
if ('ambiguous' in found) {
|
|
// An alias each `#if` arm declares differently: the first declaration.
|
|
const [first] = [...found.ambiguous].sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine);
|
|
return next === name.names.length ? { alias: first! } : null;
|
|
}
|
|
const alias = found.alias;
|
|
const text = declarationText(alias, context);
|
|
// The aliased type is written in the alias's own scope.
|
|
const aliased = expand(text ? aliasedTypeIn(text, alias.name) : null, name.names.slice(next), name.templated.slice(next),
|
|
parentScope(alias.qualifiedName), ref, context, 1, templateParametersAround(alias, context));
|
|
const cls = aliased && !aliased.pointer && classNamed([...aliased.target, ...aliased.rest], scopesWithin(aliased.scope), context);
|
|
if (cls) return { cls };
|
|
// A member of a type outside the project (`Alias::Inner`) is not the alias.
|
|
return next === name.names.length ? { alias } : null;
|
|
}
|
|
|
|
/**
|
|
* The type the C++ alias written as `written` names, as the alias's
|
|
* declaration writes it, template arguments and all — `std::vector<
|
|
* std::shared_ptr<BlobFileMetaData>>` for rocksdb's `BlobFiles` under `using
|
|
* BlobFiles = std::vector<…>;` — with the alias. The alias is looked up from
|
|
* `scopes` (innermost first), as cppClassNamed looks a name up, and only its
|
|
* own text is read: what that names is the caller's to look up in turn,
|
|
* from where the alias is declared.
|
|
* - `undefined`: `written` names no alias the lookup finds.
|
|
* - `null`: an alias whose type can't be read: one each `#if` arm declares
|
|
* differently, a member of the aliased type (`Table::Iterator`), an alias
|
|
* template, or a `decltype(…)` or `typename …` type.
|
|
*/
|
|
export function cppAliasedTypeWritten(
|
|
written: string,
|
|
scopes: readonly string[],
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext,
|
|
): { text: string; alias: Node } | null | undefined {
|
|
const name = cppTypeName(written);
|
|
if (!name) return undefined;
|
|
const { found, next } = lookupSegments(name, scopes, ref, context);
|
|
if (!found || typeof found !== 'object' || 'cls' in found) return undefined;
|
|
if ('ambiguous' in found || next !== name.names.length || isAliasTemplate(found.alias, context)) return null;
|
|
const text = declarationText(found.alias, context);
|
|
const aliased = text ? aliasedTypeIn(text, found.alias.name) : null;
|
|
if (!aliased || /\b(?:typename|decltype)\b/.test(aliased)) return null;
|
|
return { text: aliased, alias: found.alias };
|
|
}
|
|
|
|
/** The scopes visible from inside the scope `qualified`, innermost first, ending with the global scope. */
|
|
export function cppScopesWithin(qualified: string): string[] {
|
|
return scopesWithin(qualified);
|
|
}
|
|
|
|
/** The scope a C++ declaration is declared in: `leveldb::MemTable` for `leveldb::MemTable::Table`. */
|
|
export function cppParentScope(qualified: string): string {
|
|
return parentScope(qualified);
|
|
}
|
|
|
|
/** The template parameters in scope where `node` is declared: its own `template <…>` header and its class templates'. */
|
|
export function cppTemplateParameters(node: Node, context: ResolutionContext): ReadonlySet<string> {
|
|
return templateParametersAround(node, context);
|
|
}
|
|
|
|
/**
|
|
* What a written name declares, looked up from `scopes` (innermost first), and
|
|
* how many of its segments that took: a class's member is looked up in the
|
|
* class, and an alias ends the walk (`next` is where the rest of the name
|
|
* starts).
|
|
*/
|
|
function lookupSegments(
|
|
name: TypeName,
|
|
scopes: readonly string[],
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext,
|
|
visible?: Visible,
|
|
): { found: Declared; next: number } {
|
|
let found = lookupUnqualified(name.names[0]!, name.templated[0]!, scopes, ref, context, visible);
|
|
let next = 1;
|
|
if (found === undefined && name.names.length > 1) {
|
|
// Namespaces are not indexed: `detail::buffer_t` from inside `fmt` is
|
|
// found by its spelling in an enclosing scope, longest prefix first.
|
|
for (let i = name.names.length; i >= 2 && found === undefined; i--) {
|
|
const spelled = name.names.slice(0, i);
|
|
const named = typeDeclarations(spelled[spelled.length - 1]!, context, visible);
|
|
for (const s of scopes) {
|
|
found = declaredIn(s ? `${s}::${spelled.slice(0, -1).join('::')}` : spelled.slice(0, -1).join('::'),
|
|
spelled[spelled.length - 1]!, named, name.templated[i - 1]!, ref, context);
|
|
if (found !== undefined) break;
|
|
}
|
|
next = i;
|
|
}
|
|
}
|
|
// `MemTable::Table`, `Table::Iterator`: one segment at a time.
|
|
while (found && typeof found === 'object' && 'cls' in found && next < name.names.length) {
|
|
found = memberOf(found.cls, name.names[next]!, typeDeclarations(name.names[next]!, context, visible), name.templated[next]!, ref, context);
|
|
next++;
|
|
}
|
|
return { found, next };
|
|
}
|
|
|
|
function follow(
|
|
name: TypeName,
|
|
scope: string,
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext,
|
|
hops: number,
|
|
): CppAliasedType | null | undefined {
|
|
const { found, next } = lookupSegments(name, scopesWithin(scope), ref, context);
|
|
if (!found || typeof found !== 'object' || !('alias' in found)) return undefined;
|
|
const alias = found.alias;
|
|
if (hops >= MAX_HOPS) return null;
|
|
const text = declarationText(alias, context);
|
|
// The aliased type is written in the alias's own scope.
|
|
return expand(text ? aliasedTypeIn(text, alias.name) : null, name.names.slice(next), name.templated.slice(next),
|
|
parentScope(alias.qualifiedName), ref, context, hops + 1, templateParametersAround(alias, context));
|
|
}
|
|
|
|
/**
|
|
* Continue from the type an alias names, as written (`rest` followed the
|
|
* alias in the name; `parameters` are the template parameters where it is
|
|
* written).
|
|
*/
|
|
function expand(
|
|
written: string | null,
|
|
rest: readonly string[],
|
|
restTemplated: readonly boolean[],
|
|
scope: string,
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext,
|
|
hops: number,
|
|
parameters?: ReadonlySet<string>,
|
|
): CppAliasedType | null {
|
|
// `typename Traits::Field`, `PolicyTemplate<const T>` inside the template
|
|
// that declares `PolicyTemplate`: whatever a template argument makes it.
|
|
if (!written || /\btypename\b/.test(written)) return null;
|
|
const target = cppTypeName(written);
|
|
if (!target || parameters?.has(target.names[0]!)) return null;
|
|
const pointer = isCppPointerType(written);
|
|
const names = [...target.names, ...rest];
|
|
const further = follow({ names, templated: [...target.templated, ...restTemplated] }, scope, ref, context, hops);
|
|
if (further === null) return null;
|
|
if (further) return { ...further, pointer: further.pointer || pointer };
|
|
const cls = classNamed(names, scopesWithin(scope), context);
|
|
const classTemplate = !!cls && context.getNodesByQualifiedName(cls)
|
|
.some((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && templateParametersAround(n, context).size > 0);
|
|
return { target: target.names, rest: [...rest], scope, pointer, classTemplate };
|
|
}
|