mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 04:21:31 +08:00
normalizeCppTypeName cut each `<…>` at its first `>`, so a declared type with template arguments nested in its template arguments kept debris: `autovector<std::pair<int, FileMetaData*>>` read as `autovector >` and `Striped<CacheAlignedWrapper<port::Mutex>>` as `Striped >`. No class has such a name, so the call was never resolved on the receiver's class. A capitalized debris name made isUndeclaredTypeName take the type for an outside one and silence the call (rocksdb's `mutex_.Get(key)`), and a lowercase one fell to a guess by the receiver's words (rocksdb's `files_marked_for_compaction_.clear()` reached `CompactionInputFiles::clear`). Template arguments are now stripped with their nesting by cpp-type-aliases' depth-tracking helper, exported as stripCppTemplateArguments; cpp-supertypes' identical private copy now imports it. A `>` that closes no `<` is the end of a declaration begun on an earlier line (`std::unique_ptr<BlobContents>>>& blob_reqs` under `autovector<std::pair<…,`): a depth strip alone would read the argument's type there, so such text names no type. The scan back to the receiver's declaration then marks it shadowed (#2413), as it does for a declaration whose type it can't read, so a member or earlier variable of the same name found further up doesn't decide the call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
277 lines
13 KiB
TypeScript
277 lines
13 KiB
TypeScript
/**
|
|
* A C or C++ base class — `class DynamicMessage final : public Message` — is
|
|
* the class C++ name lookup finds for it.
|
|
*
|
|
* The base used to be matched by its name alone, so one named like a class of
|
|
* another namespace, or of another language, bound to whichever namesake
|
|
* ranked first: protobuf's `google::protobuf::DynamicMessage` derived from
|
|
* `json_internal::ResolverPool::Message`, and before #2397 from the PHP
|
|
* extension's C struct `Message`; leveldb's iterators from
|
|
* `SkipList::Iterator`. A wrong supertype reaches everything that walks the
|
|
* hierarchy: the supertype walk that finds an inherited method, the
|
|
* cpp-override edges, the type hierarchy `codegraph_explore` prints.
|
|
*
|
|
* The name is looked up as C++ looks it up:
|
|
* - from the scope the class is declared in (its own members are not in
|
|
* scope in its base clause), outwards to the global scope; a class scope
|
|
* includes its bases, a qualified name is resolved a segment at a time and
|
|
* an alias is followed to the class it names (cpp-type-aliases.ts);
|
|
* `::Base` is the global one;
|
|
* - then through what the file writes before the class: a namespace alias
|
|
* (`namespace _pbi = ::google::protobuf::internal;`), a using-declaration
|
|
* (`using leveldb::FilterPolicy;`), a using-directive (`using namespace
|
|
* ROCKSDB_NAMESPACE;`); and through a namespace a macro opens
|
|
* (`fmt::detail::buffer` under fmt's FMT_BEGIN_NAMESPACE);
|
|
* - a class a source file (`.cc`, `.cpp`, `.c`) defines belongs to that
|
|
* translation unit, so only that file sees it (leveldb's skiplist test
|
|
* declares its own `leveldb::Comparator`);
|
|
* - a template parameter (`template <class Base> class X : public Base`) or
|
|
* a member of one (`public T::Base`) is whatever a template argument makes
|
|
* it: no edge.
|
|
* When none of that finds the class, a class of that name that is the only
|
|
* one the file can see is taken: the index loses a class's namespace in a
|
|
* few shapes (`class PROTOBUF_EXPORT FieldDescriptor`, the classes after a
|
|
* partial specialization), and the lookup from the wrong scope finds nothing.
|
|
* Several such classes would be a guess, so there is no edge.
|
|
*
|
|
* Only C and C++ declarations are candidates, and no other strategy (a
|
|
* framework's, the import resolver, name matching) sees these references.
|
|
*/
|
|
import type { Node } from '../types';
|
|
import type { ResolvedRef, ResolutionContext, UnresolvedRef } from './types';
|
|
import {
|
|
cppClassNamed,
|
|
cppParentScope,
|
|
cppScopesWithin,
|
|
cppTemplateParameters,
|
|
cppTypeSegments,
|
|
stripCppTemplateArguments,
|
|
type CppNamedClass,
|
|
} from './cpp-type-aliases';
|
|
import { cppMacroNamespaceFrames, cppNamespaceAliases } from './name-matcher';
|
|
import { stripCommentsForRegex } from './strip-comments';
|
|
|
|
const CLASS_KINDS: ReadonlySet<string> = new Set(['class', 'struct', 'union']);
|
|
|
|
/** A C or C++ source file, whose definitions belong to its own translation unit. */
|
|
const SOURCE_FILE = /\.(?:c|cc|cpp|cxx|c\+\+|cu|metal)$/i;
|
|
|
|
/** Does `ref` name a C or C++ class's base? */
|
|
export function isCppSupertypeRef(ref: UnresolvedRef): boolean {
|
|
return (ref.language === 'cpp' || ref.language === 'c') &&
|
|
(ref.referenceKind === 'extends' || ref.referenceKind === 'implements');
|
|
}
|
|
|
|
function isCFamily(n: Node): boolean {
|
|
return n.language === 'cpp' || n.language === 'c';
|
|
}
|
|
|
|
/** The declarations code in `file` can see: a header's, and the file's own. */
|
|
function visibleFrom(file: string): (n: Node) => boolean {
|
|
return (n) => n.filePath === file || !SOURCE_FILE.test(n.filePath);
|
|
}
|
|
|
|
export function matchCppSupertype(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
|
const derived = context.getNodeById?.(ref.fromNodeId);
|
|
if (!derived || !isCFamily(derived) || !CLASS_KINDS.has(derived.kind)) return null;
|
|
const written = writtenBase(ref, context);
|
|
const segments = cppTypeSegments(written);
|
|
if (!segments) return null;
|
|
const global = written.startsWith('::');
|
|
if (!global && cppTemplateParameters(derived, context).has(segments[0]!)) return null;
|
|
|
|
const visible = visibleFrom(derived.filePath);
|
|
const scopes = cppScopesWithin(cppParentScope(derived.qualifiedName));
|
|
const resolved = (target: Node | null | undefined): ResolvedRef | null =>
|
|
target ? { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'qualified-name' } : null;
|
|
|
|
// The scopes around the class, or only the global one for `::Base`.
|
|
const lexical = classNode(cppClassNamed(written, global ? [''] : scopes, ref, context, visible), derived, context);
|
|
if (lexical !== undefined) return resolved(lexical);
|
|
|
|
// What the file writes before the class, the nearest first: each stands for
|
|
// the name's first segment, and the rest stays as written.
|
|
const path = segments.join('::');
|
|
const first = /^\s*(?:::\s*)?[A-Za-z_]\w*/.exec(written)?.[0] ?? '';
|
|
const after = written.slice(first.length);
|
|
const usings = usingsIn(derived.filePath, context).filter((u) => u.line < ref.line).reverse();
|
|
const lookUp = (spelled: string): Node | undefined => {
|
|
const absolute = spelled.startsWith('::');
|
|
return classNode(cppClassNamed(spelled, absolute ? [''] : scopes, ref, context, visible), derived, context) ?? undefined;
|
|
};
|
|
// `::_pbi::MessageGlobalsBase` under `namespace _pbi = ::google::protobuf::internal;`.
|
|
if (segments.length > 1) {
|
|
const local = usings.find((u) => u.kind === 'alias' && u.name === segments[0]);
|
|
const target = local?.path ?? cppNamespaceAliases(context).get(segments[0]!);
|
|
const hit = target ? lookUp(target + after) : undefined;
|
|
if (hit) return resolved(hit);
|
|
}
|
|
if (!global) {
|
|
// `using leveldb::FilterPolicy;` … `public FilterPolicy`.
|
|
for (const u of usings) {
|
|
if (u.kind !== 'declaration' || u.name !== segments[0]) continue;
|
|
const hit = lookUp(u.path + after);
|
|
if (hit) return resolved(hit);
|
|
}
|
|
// `using namespace ROCKSDB_NAMESPACE;` … `public EventListener`.
|
|
for (const u of usings) {
|
|
if (u.kind !== 'directive') continue;
|
|
const hit = lookUp(`${u.path}::${written}`);
|
|
if (hit) return resolved(hit);
|
|
}
|
|
}
|
|
// `fmt::detail::buffer`, where a macro opens `namespace fmt` around `detail::buffer`.
|
|
if (segments.length > 1) {
|
|
const hit = pick(namedClasses(segments, derived, context, visible)
|
|
.filter((n) => macroQualifiedName(n, context) === path), derived);
|
|
if (hit) return resolved(hit);
|
|
}
|
|
// The only class of that name the file can see, by its qualified tail.
|
|
const tail = namedClasses(segments, derived, context, visible)
|
|
.filter((n) => n.qualifiedName === path || n.qualifiedName.endsWith(`::${path}`));
|
|
const own = tail.filter((n) => n.filePath === derived.filePath);
|
|
const sole = own.length > 0 ? own : tail;
|
|
return sole.length === 1 ? { original: ref, targetNodeId: sole[0]!.id, confidence: 0.7, resolvedBy: 'exact-match' } : null;
|
|
}
|
|
|
|
/**
|
|
* The base as the base clause writes it, template arguments included
|
|
* (`bool_constant<is_utf8_enabled>`): the reference carries the name without
|
|
* them, and an alias template is only ever named with them. The reference's
|
|
* own name when the source there reads otherwise.
|
|
*/
|
|
function writtenBase(ref: UnresolvedRef, context: ResolutionContext): string {
|
|
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/);
|
|
const text = lines?.slice(ref.line - 1, ref.line + 11).join('\n').slice(Math.max(0, ref.column)) ?? '';
|
|
let depth = 0;
|
|
let end = text.length;
|
|
for (let i = 0; i < text.length; i++) {
|
|
const c = text[i]!;
|
|
// A template's `<` follows its name; `sizeof(T) <= 8` and `1 << n` compare and shift.
|
|
const opens = c === '(' || (c === '<' && /\w/.test(text[i - 1] ?? '') && text[i + 1] !== '=' && text[i + 1] !== '<');
|
|
const closes = c === ')' || (c === '>' && text[i - 1] !== '-' && text[i + 1] !== '=');
|
|
if (opens) depth++;
|
|
else if (closes) {
|
|
if (depth === 0) {
|
|
end = i;
|
|
break;
|
|
}
|
|
depth--;
|
|
} else if (depth === 0 && (c === ',' || c === '{' || c === ';')) {
|
|
end = i;
|
|
break;
|
|
}
|
|
}
|
|
const written = text.slice(0, end).trim();
|
|
const squash = (s: string): string => s.replace(/\s+/g, '');
|
|
return squash(stripCppTemplateArguments(written)) === squash(ref.referenceName) ? written : ref.referenceName.trim();
|
|
}
|
|
|
|
/**
|
|
* The node of the class a lookup found, as the deriving class's file sees it
|
|
* (null: the name names no particular class; undefined: keep looking).
|
|
*/
|
|
function classNode(named: CppNamedClass, derived: Node, context: ResolutionContext): Node | null | undefined {
|
|
if (named === null || named === undefined) return named;
|
|
if ('alias' in named) return named.alias;
|
|
const classes = context
|
|
.getNodesByQualifiedName(named.cls)
|
|
.filter((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && n.id !== derived.id);
|
|
// A lookup that ended on the class itself (its namespace lost) found nothing.
|
|
if (classes.length === 0) return undefined;
|
|
const visible = classes.filter(visibleFrom(derived.filePath));
|
|
return pick(visible.length > 0 ? visible : classes, derived);
|
|
}
|
|
|
|
/**
|
|
* One of several declarations of a class (separate translation units,
|
|
* vendored copies): the deriving class's own file's, then its header's, then
|
|
* the nearest by path.
|
|
*/
|
|
function pick(classes: Node[], derived: Node): Node | undefined {
|
|
if (classes.length <= 1) return classes[0];
|
|
const own = classes.find((n) => n.filePath === derived.filePath);
|
|
if (own) return own;
|
|
const stem = (p: string): string => p.replace(/\.[^./\\]+$/, '');
|
|
const header = classes.find((n) => stem(n.filePath) === stem(derived.filePath));
|
|
if (header) return header;
|
|
const shared = (p: string): number => {
|
|
const a = p.split('/');
|
|
const b = derived.filePath.split('/');
|
|
let i = 0;
|
|
while (i < a.length - 1 && i < b.length - 1 && a[i] === b[i]) i++;
|
|
return i;
|
|
};
|
|
return classes.reduce((best, n) => (shared(n.filePath) > shared(best.filePath) ? n : best));
|
|
}
|
|
|
|
/** C and C++ classes named like the last segment of `segments` that `derived`'s file can see. */
|
|
function namedClasses(segments: readonly string[], derived: Node, context: ResolutionContext, visible: (n: Node) => boolean): Node[] {
|
|
return context
|
|
.getNodesByName(segments[segments.length - 1]!)
|
|
.filter((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && n.id !== derived.id && visible(n));
|
|
}
|
|
|
|
/** A declaration's qualified name with the namespaces macros open around it: `fmt::detail::buffer` for `detail::buffer`. */
|
|
function macroQualifiedName(n: Node, context: ResolutionContext): string {
|
|
const prefix = cppMacroNamespaceFrames(n.filePath, context)
|
|
.filter((f) => f.start <= n.startLine && f.end >= n.startLine)
|
|
.sort((a, b) => a.start - b.start)
|
|
.flatMap((f) => f.path);
|
|
return prefix.length > 0 ? `${prefix.join('::')}::${n.qualifiedName}` : n.qualifiedName;
|
|
}
|
|
|
|
/** A `using` declaration or directive, or a namespace alias, a C or C++ file writes. */
|
|
interface CppUsing {
|
|
line: number;
|
|
kind: 'declaration' | 'directive' | 'alias';
|
|
/** The name it brings in: the declared name, or the alias. */
|
|
name: string;
|
|
/** What it names, as written: `leveldb::FilterPolicy`, `ROCKSDB_NAMESPACE`, `::google::protobuf::internal`. */
|
|
path: string;
|
|
}
|
|
|
|
const USINGS = new WeakMap<ResolutionContext, Map<string, CppUsing[]>>();
|
|
|
|
/** Drop the per-file `using` memo (see ReferenceResolver.clearCaches). */
|
|
export function clearCppSupertypeMemos(context: ResolutionContext): void {
|
|
USINGS.delete(context);
|
|
}
|
|
|
|
const PATH = String.raw`(?:::\s*)?[A-Za-z_]\w*(?:\s*::\s*[A-Za-z_]\w*)*`;
|
|
const USING = new RegExp(
|
|
String.raw`\busing\s+namespace\s+(${PATH})\s*;` +
|
|
String.raw`|\busing\s+(${PATH}\s*::\s*[A-Za-z_]\w*)\s*;` +
|
|
String.raw`|\bnamespace\s+([A-Za-z_]\w*)\s*=\s*(${PATH})\s*;`,
|
|
'g',
|
|
);
|
|
|
|
/** The `using` declarations and directives and namespace aliases of a file, in order. */
|
|
function usingsIn(file: string, context: ResolutionContext): CppUsing[] {
|
|
let memo = USINGS.get(context);
|
|
if (!memo) USINGS.set(context, (memo = new Map()));
|
|
const hit = memo.get(file);
|
|
if (hit) return hit;
|
|
const found: CppUsing[] = [];
|
|
const source = context.readFile(file);
|
|
if (source && (source.includes('using') || source.includes('namespace'))) {
|
|
const text = stripCommentsForRegex(source, 'cpp');
|
|
let line = 1;
|
|
let at = 0;
|
|
for (const m of text.matchAll(USING)) {
|
|
for (; at < m.index; at++) if (text.charCodeAt(at) === 10) line++;
|
|
const squash = (s: string): string => s.replace(/\s+/g, '');
|
|
if (m[1]) {
|
|
found.push({ line, kind: 'directive', name: '', path: squash(m[1]) });
|
|
} else if (m[2]) {
|
|
const path = squash(m[2]);
|
|
found.push({ line, kind: 'declaration', name: path.slice(path.lastIndexOf('::') + 2), path });
|
|
} else if (m[3] && m[4]) {
|
|
found.push({ line, kind: 'alias', name: m[3], path: squash(m[4]) });
|
|
}
|
|
}
|
|
}
|
|
memo.set(file, found);
|
|
return found;
|
|
}
|