Files
codegraph/src/resolution/cpp-namespaces.ts
T
Colby MchenryandClaude Opus 5.5 00d7430ebb fix(cpp): a qualified receiver type never reaches a class that only shares its last name, and rocksdb::DB is RocksDB's own (#2567)
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>
2026-10-15 03:29:12 +00:00

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);
}