mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 18:23:25 +08:00
A C++ name is looked up from where it is written, and a class that a source file defines belongs to that file's translation unit, so only that file sees it (#2538). A header that declares `struct Rep;` and `std::unique_ptr<Rep> rep_;` writes the name for a `Table::Rep` that its source file defines, and code in that file reads the member through it. The lookup ran from the header, saw no source file's classes and found nothing. The call was then guessed by the method's name, often onto the calling class's own method: - leveldb's `rep_->index_block->NewIterator(…)` reached `Table::NewIterator`; - rocksdb's `rep->write_batch.GetDataSize()` reached `WriteBatchWithIndex::GetDataSize`; - rocksdb's `level_iters_[i]->status()` reached `ForwardIterator::status`. cppClassWritten's lookup now also sees the classes of the file the reference is in, the translation unit that includes the header (visibleFrom(file, unit)). classNode filters by that same visibility instead of recomputing the header's. A member-chain diagnostic on main81fd0078found 99 rocksdb and 7 leveldb chain calls through a `Rep` whose class couldn't be placed. Kit A/B against main5994edb3: | repo | removed | added | retargeted | same target, now typed | |---|---|---|---|---| | rocksdb | 34 | 40 | 43 | 56 | | fmt | 0 | 27 | 0 | 1 | | protobuf | 25 | 1 | 2 | 2 | | leveldb | 0 | 1 | 3 | 3 | | redis | identical | | | | - Retargeted: from the calling class's own method or a namesake to the member's class: - rocksdb: WriteBatch, ForwardLevelIterator, IndexBuilder and PartitionedIndexBuilder, RangeDelBlockBuilder, Env, MemFile; - leveldb: Block::NewIterator; - protobuf: DescriptorPool::Tables::FindSymbol. - Removed: calls on library members reached this way, which had linked to namesakes: - `std::stack::pop` (BinaryHeap::pop); - `std::vector::size` (Block::size); - `std::thread::join` (WindowsThread::join); - absl::flat_hash_set's `clear` (Map::clear); - `std::optional`'s `has_value` and `reset` (Option, ScopedPythonPtr). - Added: - WriteBatch's Put, Delete, Merge and the rest, through WriteBatchWithIndex's `rep->write_batch`; - gtest's UnitTestImpl through `impl()`, in fmt's and rocksdb's vendored gtest; - leveldb's FilterPolicy::Name. - About 35 changed sites read by hand were all right. Mutations: dropping the unit from visibleFrom, or from the lookup's call, fails the new tests. classNode's own filter only chooses among several declarations of one class, and nothing on the kit or in the tests has two, so dropping it survives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
387 lines
18 KiB
TypeScript
387 lines
18 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 outermost namespace; 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;`). These come before the global scope: they put the
|
|
* name in a namespace around the code (`namespace upb::generator { using
|
|
* google::protobuf::Descriptor; }`), and a global declaration of it is one
|
|
* the file can't see at the same time (protobuf's PHP extension declares a
|
|
* C struct `Descriptor`);
|
|
* - then the global scope, and 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`) — and so does a name a header
|
|
* writes, read for code in that file, which includes the header: a
|
|
* pimpl's `std::unique_ptr<Rep> rep_`, whose `Table::Rep` the source file
|
|
* calling through it defines (leveldb's `Table`, rocksdb's
|
|
* `BlockBasedTableBuilder`);
|
|
* - 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.
|
|
*
|
|
* The same lookup (cppClassWritten) finds the class a `std::unique_ptr`,
|
|
* `std::shared_ptr` or `std::optional` holds, for a `->` call on it, looked
|
|
* up from the calling function (name-matcher's matchCppHeldTypeCall).
|
|
*/
|
|
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 './cpp-namespaces';
|
|
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 at `site` can see (visibleFrom), read for code in
|
|
* the file `unit`, where a class local to a function counts only in that
|
|
* function's lines: the index names it after the function, and every gtest
|
|
* `TEST_F` body in a namespace is `ns::TEST_F`, so one body's local
|
|
* `Listener` passed for any other's — in the body itself, and in the methods
|
|
* of a local class of its own.
|
|
*/
|
|
function seenFrom(site: CppNameSite, context: ResolutionContext, unit?: string): (n: Node) => boolean {
|
|
const visible = visibleFrom(site.file, unit);
|
|
return (n) => {
|
|
if (!visible(n)) return false;
|
|
const fn = cppFunctionAround(n, context);
|
|
return !fn || (fn.filePath === site.file && site.line >= fn.startLine && site.line <= fn.endLine);
|
|
};
|
|
}
|
|
|
|
const LOCAL_TO = new WeakMap<ResolutionContext, Map<string, Node | null>>();
|
|
|
|
/**
|
|
* The function a C or C++ class is local to: the one of the scope its
|
|
* qualified name is in whose lines hold it. Null for a class of a namespace
|
|
* or of another class.
|
|
*/
|
|
function cppFunctionAround(cls: Node, context: ResolutionContext): Node | null {
|
|
let memo = LOCAL_TO.get(context);
|
|
if (!memo) LOCAL_TO.set(context, (memo = new Map()));
|
|
const hit = memo.get(cls.id);
|
|
if (hit !== undefined) return hit;
|
|
const scope = cppParentScope(cls.qualifiedName);
|
|
const fn = (scope ? context.getNodesByQualifiedName(scope) : []).find((f) =>
|
|
(f.kind === 'function' || f.kind === 'method') && f.filePath === cls.filePath &&
|
|
f.startLine <= cls.startLine && f.endLine >= cls.startLine) ?? null;
|
|
memo.set(cls.id, fn);
|
|
return fn;
|
|
}
|
|
|
|
/**
|
|
* The declarations code in `file` can see: a header's, and the file's own —
|
|
* and, read for code in the file `unit` (one that includes `file`), `unit`'s.
|
|
*/
|
|
function visibleFrom(file: string, unit?: string): (n: Node) => boolean {
|
|
return (n) => n.filePath === file || n.filePath === unit || !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 found = cppClassWritten(writtenBase(ref, context), {
|
|
scope: cppParentScope(derived.qualifiedName),
|
|
file: derived.filePath,
|
|
line: ref.line,
|
|
parameters: cppTemplateParameters(derived, context),
|
|
self: derived,
|
|
}, ref, context);
|
|
if (!found) return null;
|
|
return found.exact
|
|
? { original: ref, targetNodeId: found.node.id, confidence: 0.9, resolvedBy: 'qualified-name' }
|
|
: { original: ref, targetNodeId: found.node.id, confidence: 0.7, resolvedBy: 'exact-match' };
|
|
}
|
|
|
|
/** Where a C or C++ type name is written, which is where its lookup starts. */
|
|
export interface CppNameSite {
|
|
/**
|
|
* The scope it is looked up from: a class's enclosing scope for its base
|
|
* clause, the class of a method for code in the method.
|
|
*/
|
|
scope: string;
|
|
/** The file that writes it: its translation unit decides which classes it sees, and its `using`s before `line` apply. */
|
|
file: string;
|
|
line: number;
|
|
/** The template parameters in scope there; a name that starts with one names no particular class. */
|
|
parameters: ReadonlySet<string>;
|
|
/** A class the name cannot be: the deriving class, in its own base clause. */
|
|
self?: Node;
|
|
/**
|
|
* A class looked in right after `scope` itself, its bases included: a gtest
|
|
* `TEST_F` body is a member function of a class derived from its fixture,
|
|
* so the fixture's nested types are what its unqualified names find
|
|
* before the enclosing namespaces'.
|
|
*/
|
|
classScope?: string;
|
|
}
|
|
|
|
/** The class a C or C++ type name names. */
|
|
export interface CppWrittenClass {
|
|
/** The class, or an alias of a type that is none of the project's classes. */
|
|
node: Node;
|
|
/** Found by C++ name lookup, rather than as the only class of that name the file can see. */
|
|
exact: boolean;
|
|
}
|
|
|
|
/**
|
|
* The class a C or C++ type name written at `site` names, looked up as the
|
|
* comment at the top of this file says. Null when the name names no
|
|
* particular class (a template parameter, an enum, a member of a type outside
|
|
* the project); undefined when nothing the file can see is named so, or
|
|
* several classes could be.
|
|
*/
|
|
export function cppClassWritten(written: string, site: CppNameSite, ref: UnresolvedRef, context: ResolutionContext): CppWrittenClass | null | undefined {
|
|
const segments = cppTypeSegments(written);
|
|
if (!segments) return null;
|
|
const global = written.startsWith('::');
|
|
if (!global && site.parameters.has(segments[0]!)) return null;
|
|
|
|
// (For code in the reference's own file: the file it is in includes the one that writes the name.)
|
|
const visible = seenFrom(site, context, ref.filePath);
|
|
const scopes = cppScopesWithin(site.scope);
|
|
if (site.classScope) scopes.splice(1, 0, site.classScope);
|
|
const resolved = (node: Node): CppWrittenClass => ({ node, exact: true });
|
|
|
|
// The scopes around the name short of the global one, or only the global one for `::Base`.
|
|
const lexical = classNode(cppClassNamed(written, global ? [''] : scopes.slice(0, -1), ref, context, visible), site, context, visible);
|
|
if (lexical !== undefined) return lexical && resolved(lexical);
|
|
|
|
// What the file writes before the name, 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(site.file, context).filter((u) => u.line < site.line).reverse();
|
|
const lookUp = (spelled: string): Node | undefined => {
|
|
const absolute = spelled.startsWith('::');
|
|
return classNode(cppClassNamed(spelled, absolute ? [''] : scopes, ref, context, visible), site, context, visible) ?? 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);
|
|
}
|
|
// The global scope.
|
|
const outermost = classNode(cppClassNamed(written, [''], ref, context, visible), site, context, visible);
|
|
if (outermost !== undefined) return outermost && resolved(outermost);
|
|
}
|
|
// `fmt::detail::buffer`, where a macro opens `namespace fmt` around `detail::buffer`.
|
|
if (segments.length > 1) {
|
|
const hit = pick(namedClasses(segments, site, context, visible)
|
|
.filter((n) => macroQualifiedName(n, context) === path), site.file);
|
|
if (hit) return resolved(hit);
|
|
}
|
|
// The only class of that name the file can see, by its qualified tail.
|
|
const tail = namedClasses(segments, site, context, visible)
|
|
.filter((n) => n.qualifiedName === path || n.qualifiedName.endsWith(`::${path}`));
|
|
const own = tail.filter((n) => n.filePath === site.file);
|
|
const sole = own.length > 0 ? own : tail;
|
|
return sole.length === 1 ? { node: sole[0]!, exact: false } : undefined;
|
|
}
|
|
|
|
/**
|
|
* 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, among the declarations the lookup can
|
|
* see (`visible`; null: the name names no particular class; undefined: keep
|
|
* looking).
|
|
*/
|
|
function classNode(named: CppNamedClass, site: CppNameSite, context: ResolutionContext, visible: (n: Node) => boolean): 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 !== site.self?.id);
|
|
// A lookup that ended on the class itself (its namespace lost) found nothing.
|
|
if (classes.length === 0) return undefined;
|
|
const shown = classes.filter(visible);
|
|
return pick(shown.length > 0 ? shown : classes, site.file);
|
|
}
|
|
|
|
/**
|
|
* One of several declarations of a class (separate translation units,
|
|
* vendored copies): the one in `file` itself, then in its header, then the
|
|
* nearest by path.
|
|
*/
|
|
function pick(classes: Node[], file: string): Node | undefined {
|
|
if (classes.length <= 1) return classes[0];
|
|
const own = classes.find((n) => n.filePath === file);
|
|
if (own) return own;
|
|
const stem = (p: string): string => p.replace(/\.[^./\\]+$/, '');
|
|
const header = classes.find((n) => stem(n.filePath) === stem(file));
|
|
if (header) return header;
|
|
const shared = (p: string): number => {
|
|
const a = p.split('/');
|
|
const b = file.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 the file at `site` can see. */
|
|
function namedClasses(segments: readonly string[], site: CppNameSite, 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 !== site.self?.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 and the local-class memo (see ReferenceResolver.clearCaches). */
|
|
export function clearCppSupertypeMemos(context: ResolutionContext): void {
|
|
USINGS.delete(context);
|
|
LOCAL_TO.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;
|
|
}
|