mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 20:12:11 +08:00
A C++ receiver declared with a namespace-qualified type is from outside the project only when no class of the project can be what that name names. Three cases read that wrong on main: - A namespace a macro stands for. RocksDB declares its classes in `namespace ROCKSDB_NAMESPACE` under `#define ROCKSDB_NAMESPACE rocksdb`, so the index names them `ROCKSDB_NAMESPACE::DB`, and #2532's outside rule (cppProjectScopeNames) never saw `rocksdb`: `rocksdb::DB* db; db->Put(…)` got no edge. cppProjectScopeNames now adds the name a single-name macro stands for, in both directions (cppNameMacros, the map cppNamespaceMacros already builds). - A lowercase outside type beside a project namesake. isCppLibraryType (#2413) took a non-std lowercase type as the library's only when no project type had its name at all, so `absl::flat_hash_set<int> seen; seen.insert(…)` reached a project's own `util::flat_hash_set::insert`. It is now also the library's when every project type of that name is a namesake (cppFindsOnlyNamesakes). - A capitalized type whose last name finds only namesakes, on the paths #2493's call-site rules leave to the last-name match: a declaration read outside the calling function (protobuf's `absl::Status _status` in its RETURN_IF_ERROR macro body reached `upb::Status::ok`; `auto bytes = ::hpb::Serialize(…); bytes.value()` reached `hpb::StatusOr::value`), and a namespace the project uses (its generated `DescriptorProto::ExtensionRange`, which the index doesn't hold, reached `Descriptor::ExtensionRange::options`). That call now gets no edge, as isUndeclaredTypeName rules where no class shares the name; `->` on a value still goes to the name strategies, and a template parameter's member is left alone. cppFindsOnlyNamesakes: every type of the last name, and every owner of a method of the called name (the index can hold a class's methods without the class), has a qualified name that ends unlike the written one, neither the other's tail; namespace aliases are expanded and macro-named namespaces read both ways. A name a project type qualifies, an unqualified name and an unbalanced `<>` are never judged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
6.4 KiB
TypeScript
128 lines
6.4 KiB
TypeScript
/**
|
|
* C and C++ namespaces the source spells in ways the index cannot see: a
|
|
* namespace alias (`namespace py = pybind11;`) and a macro that opens a
|
|
* namespace (`FMT_BEGIN_NAMESPACE`). Shared by name matching and the C++
|
|
* class lookup (cpp-supertypes.ts).
|
|
*/
|
|
import type { ResolutionContext } from './types';
|
|
import { stripCommentsForRegex } from './strip-comments';
|
|
|
|
const CPP_NS_MACROS = new WeakMap<ResolutionContext, { openers: Map<string, string[]>; openerFns: Set<string>; closers: Map<string, number>; aliases: Map<string, string> }>();
|
|
const CPP_NS_FRAMES = new WeakMap<ResolutionContext, Map<string, Array<{ start: number; end: number; path: string[] }>>>();
|
|
/** A closing macro's body: `}` / `} }`, maybe beside a pragma macro (`PYBIND11_WARNING_POP }`). */
|
|
const CPP_CLOSER_BODY = /^(?:[A-Za-z_]\w*\s+)*\}(?:\s*\})*\s*;?$/;
|
|
const CPP_NS_ALIASES = new WeakMap<ResolutionContext, Map<string, string>>();
|
|
|
|
/** The project's namespace aliases: `namespace py = pybind11;`. */
|
|
export function cppNamespaceAliases(context: ResolutionContext): Map<string, string> {
|
|
const hit = CPP_NS_ALIASES.get(context);
|
|
if (hit) return hit;
|
|
const aliases = new Map<string, string>();
|
|
for (const file of context.getAllFiles()) {
|
|
if (!/\.(?:h|hh|hpp|hxx|inl|c|cc|cpp|cxx)$/i.test(file)) continue;
|
|
const source = context.readFile(file);
|
|
if (!source || !source.includes('namespace')) continue;
|
|
for (const m of source.matchAll(/^[ \t]*namespace[ \t]+([A-Za-z_]\w*)[ \t]*=[ \t]*(?:::)?([A-Za-z_][\w:]*)[ \t]*;/gm)) {
|
|
if (!aliases.has(m[1]!)) aliases.set(m[1]!, m[2]!);
|
|
}
|
|
}
|
|
CPP_NS_ALIASES.set(context, aliases);
|
|
return aliases;
|
|
}
|
|
|
|
/**
|
|
* The project's namespace-opening macros — `#define FMT_BEGIN_NAMESPACE
|
|
* namespace fmt { inline namespace v12 {`, `#define RAPIDJSON_NAMESPACE_BEGIN
|
|
* namespace RAPIDJSON_NAMESPACE {` (through `#define RAPIDJSON_NAMESPACE
|
|
* rapidjson`) — as the namespace path each opens (inline namespaces are
|
|
* transparent), and the closing macros as how many scopes each closes.
|
|
*/
|
|
function cppNamespaceMacros(context: ResolutionContext): { openers: Map<string, string[]>; openerFns: Set<string>; closers: Map<string, number>; aliases: Map<string, string> } {
|
|
const hit = CPP_NS_MACROS.get(context);
|
|
if (hit) return hit;
|
|
const openers = new Map<string, string[]>();
|
|
// `#define PYBIND11_NAMESPACE_BEGIN(name) namespace name {`, used as `PYBIND11_NAMESPACE_BEGIN(detail)`.
|
|
const openerFns = new Set<string>();
|
|
const closers = new Map<string, number>();
|
|
const aliases = new Map<string, string>();
|
|
const bodies: Array<[string, string]> = [];
|
|
for (const file of context.getAllFiles()) {
|
|
if (!/\.(?:h|hh|hpp|hxx|h\+\+|inl|ipp|tcc)$/i.test(file)) continue;
|
|
const raw = context.readFile(file);
|
|
if (!raw || !raw.includes('#') || !raw.includes('define')) continue;
|
|
const source = stripCommentsForRegex(raw.replace(/\\\r?\n/g, ' '), 'cpp');
|
|
for (const m of source.matchAll(/^[ \t]*#[ \t]*define[ \t]+([A-Za-z_]\w*)(\(\s*([A-Za-z_]\w*)?\s*\))?[ \t]+([^\n]*)$/gm)) {
|
|
const body = m[4]!.trim();
|
|
if (m[2] !== undefined) {
|
|
// (a trailing pragma macro — `PYBIND11_WARNING_PUSH` — rides along)
|
|
if (m[3] && new RegExp(`^namespace\\s+${m[3]}\\s*\\{[\\w\\s]*$`).test(body)) openerFns.add(m[1]!);
|
|
else if (CPP_CLOSER_BODY.test(body)) closers.set(m[1]!, (body.match(/\}/g) ?? []).length);
|
|
continue;
|
|
}
|
|
if (/^[A-Za-z_]\w*$/.test(body)) aliases.set(m[1]!, body);
|
|
else if (CPP_CLOSER_BODY.test(body)) closers.set(m[1]!, (body.match(/\}/g) ?? []).length);
|
|
// An inline namespace (transparent, and often named by a macro call) is skipped.
|
|
else if (/^(?:inline\s+namespace\s+[^{}]*\{\s*|namespace\s+[A-Za-z_]\w*\s*\{\s*)+[\w\s]*$/.test(body)) bodies.push([m[1]!, body]);
|
|
}
|
|
}
|
|
for (const [name, body] of bodies) {
|
|
if (openers.has(name)) continue;
|
|
const path = [...body.replace(/inline\s+namespace\s+[^{}]*\{/g, '').matchAll(/namespace\s+([A-Za-z_]\w*)/g)]
|
|
.map((m) => aliases.get(m[1]!) ?? m[1]!);
|
|
if (path.length > 0) openers.set(name, path);
|
|
}
|
|
const macros = { openers, openerFns, closers, aliases };
|
|
CPP_NS_MACROS.set(context, macros);
|
|
return macros;
|
|
}
|
|
|
|
/** The outermost namespaces the project's namespace-opening macros open: `fmt` for `FMT_BEGIN_NAMESPACE`. */
|
|
export function cppMacroNamespaceHeads(context: ResolutionContext): string[] {
|
|
return [...cppNamespaceMacros(context).openers.values()].map((path) => path[0]!);
|
|
}
|
|
|
|
/**
|
|
* The project's macros that stand for a single name, by the macro's name:
|
|
* `#define ROCKSDB_NAMESPACE rocksdb`, so the namespace rocksdb declares as
|
|
* `namespace ROCKSDB_NAMESPACE` is `rocksdb` to code that names it.
|
|
*/
|
|
export function cppNameMacros(context: ResolutionContext): ReadonlyMap<string, string> {
|
|
return cppNamespaceMacros(context).aliases;
|
|
}
|
|
|
|
/** The line ranges of a C / C++ file each namespace macro opens, with the namespace path it opens. */
|
|
export function cppMacroNamespaceFrames(file: string, context: ResolutionContext): Array<{ start: number; end: number; path: string[] }> {
|
|
let memo = CPP_NS_FRAMES.get(context);
|
|
if (!memo) {
|
|
memo = new Map();
|
|
CPP_NS_FRAMES.set(context, memo);
|
|
}
|
|
const hit = memo.get(file);
|
|
if (hit) return hit;
|
|
const frames: Array<{ start: number; end: number; path: string[] }> = [];
|
|
const { openers, openerFns, closers, aliases } = cppNamespaceMacros(context);
|
|
if (openers.size > 0 || openerFns.size > 0) {
|
|
const lines = context.getFileLines?.(file) ?? context.readFile(file)?.split(/\r?\n/) ?? [];
|
|
const open: Array<{ start: number; path: string[] }> = [];
|
|
lines.forEach((text, i) => {
|
|
const m = /^[ \t]*([A-Z_][A-Z0-9_]*)(?:\(\s*([A-Za-z_]\w*)?\s*\))?[ \t]*;?[ \t]*(?:\/\/.*|\/\*.*\*\/[ \t]*)?\r?$/.exec(text);
|
|
const token = m?.[1];
|
|
if (!token) return;
|
|
const arg = m[2];
|
|
const path = arg !== undefined && openerFns.has(token) ? [aliases.get(arg) ?? arg] : arg === undefined ? openers.get(token) : undefined;
|
|
if (path) open.push({ start: i + 1, path });
|
|
else if (closers.has(token) && open.length > 0) frames.push({ ...open.pop()!, end: i + 1 });
|
|
});
|
|
for (const frame of open) frames.push({ ...frame, end: lines.length });
|
|
}
|
|
memo.set(file, frames);
|
|
return frames;
|
|
}
|
|
|
|
/** Drop the per-context namespace memos (see ReferenceResolver.clearCaches). */
|
|
export function clearCppNamespaceMemos(context: ResolutionContext): void {
|
|
CPP_NS_MACROS.delete(context);
|
|
CPP_NS_FRAMES.delete(context);
|
|
CPP_NS_ALIASES.delete(context);
|
|
}
|