fix(cpp): a qualified name reaches a declaration in a macro-opened namespace (#2176)

fmt opens its namespace with FMT_BEGIN_NAMESPACE, pybind11 with
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE), rapidjson/spdlog with
…_NAMESPACE_BEGIN — the extractor sees a bare macro line, so every
declaration inside is indexed without its namespace and fmt::format(…)
(1,728 calls on fmt) resolved to nothing.

As a last resort for a qualified C/C++ name, read the project's
namespace macros (object-like and single-parameter function-like
openers, through identifier aliases, inline namespaces transparent;
closers by brace count), the ranges each opens per file, and namespace
aliases (namespace py = pybind11;), and match the declaration whose
macro prefix + qualified name is the name called. fmt +2480, pybind11
+1446, spdlog +264; tokio/serde/gin/excalidraw/Newtonsoft unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-30 16:38:59 +00:00
committed by GitHub
parent c0a38a8758
commit 5844eb8986
3 changed files with 229 additions and 0 deletions
+1
View File
@@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Fixes
- In C and C++, a qualified name like `fmt::format` now reaches a declaration inside a namespace the library opens with a macro, like fmt's `FMT_BEGIN_NAMESPACE`, pybind11's `PYBIND11_NAMESPACE_BEGIN(…)` or rapidjson's and spdlog's `…_NAMESPACE_BEGIN`. It also works through a namespace alias like `namespace py = pybind11;`. On fmt, 1,728 `fmt::format(…)` calls used to link to nothing, and so did about 1,400 `py::…` calls on pybind11.
- A method guessed from a receiver's name no longer lands on a request handler the web framework dispatches to, such as a Django or DRF view's `post` / `get` or a controller's `update` / `destroy`. It also no longer lands on a test double the calling file never mentions. On django-allauth, tests' `client.post(…)` used to land on a `ClientRegistrationView` 425 times. On mealie, `response.json()` used to land on a test's `_FakeHTTPResponse` 424 times. In Python, `site = Site.objects.create(…)` now makes `site` a `Site`, so `site.save()` reaches the model's own `save`. Re-index after upgrading.
- Kotlin infix calls, like `Users.id eq id1` or koin's `single { … } bind MyApi::class`, are now recorded as calls to the infix function. Before, they had no callers at all. On Exposed, `eq` now shows over a thousand callers. Bit operations on numbers (`x and 0xff`, `h shr 8`) are left to Kotlin's own operators, even where the project defines a same-named extension. Re-index after upgrading.
- Kotlin code using an infix function whose name starts with `e`, like Exposed's `Users.id eq id1`, now parses cleanly. Before, the rest of such a line was a parse error, and in a file with many of them the whole class around it could be lost. On Exposed, files with a parse error dropped from 229 to 71. Re-index after upgrading.
+90
View File
@@ -0,0 +1,90 @@
/**
* A C++ library that opens its namespace with a macro — fmt's
* `FMT_BEGIN_NAMESPACE` (`namespace fmt { inline namespace v12 {`),
* pybind11's `PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)` — has its
* declarations indexed without that namespace, so `fmt::format(…)` found
* nothing: 1,728 calls on fmt. A qualified name now reaches a declaration the
* project's own namespace macros put there, through a namespace alias
* (`namespace py = pybind11;`) too.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-macro-ns-'));
const files: Record<string, string> = {
'include/fmt/base.h': `#pragma once
#define FMT_BEGIN_NAMESPACE \\
namespace fmt { \\
inline namespace v12 {
#define FMT_END_NAMESPACE \\
} \\
}
FMT_BEGIN_NAMESPACE
namespace detail {
inline int to_unsigned(int value) { return value; }
}
inline int format(const char* spec) { return detail::to_unsigned(0); }
FMT_END_NAMESPACE
`,
'include/pybind11/detail/macros.h': `#pragma once
#define PYBIND11_NAMESPACE pybind11
#define PYBIND11_NAMESPACE_BEGIN(name) \\
namespace name { \\
PYBIND11_WARNING_PUSH
#define PYBIND11_NAMESPACE_END(name) \\
PYBIND11_WARNING_POP \\
}
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
inline void print(const char* text) {}
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)
`,
'test/format-test.cc': `#include "fmt/base.h"
#include "pybind11/detail/macros.h"
namespace py = pybind11;
int main() {
fmt::format("{}");
fmt::detail::to_unsigned(1);
py::print("hi");
return 0;
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
describe('C++ namespaces opened by a macro', () => {
it('put their declarations under the qualified names callers use', () => {
const ids = cg.getNodesInFile('test/format-test.cc').map((n) => n.id);
const targets = cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'calls')
.map((e) => `${cg.getNode(e.target)!.filePath}:${cg.getNode(e.target)!.name}`)
.sort();
expect(targets).toEqual([
'include/fmt/base.h:format',
'include/fmt/base.h:to_unsigned',
'include/pybind11/detail/macros.h:print',
]);
});
});
+138
View File
@@ -4547,6 +4547,9 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
KOTLIN_RECEIVER_TYPES.delete(context);
KOTLIN_HIERARCHIES.delete(context);
KOTLIN_FRAMES.delete(context);
CPP_NS_MACROS.delete(context);
CPP_NS_FRAMES.delete(context);
CPP_NS_ALIASES.delete(context);
MEMBER_TYPE_MEMO.delete(context);
CSHARP_ALIASES.delete(context);
SWIFT_HIERARCHIES.delete(context);
@@ -7315,5 +7318,140 @@ function matchReferenceInner(
result = nmTimed('fuzzy', ref, () => matchFuzzy(ref, context));
if (result) return result;
// 5. A C / C++ name qualified by a namespace the project opens with a macro
// (`fmt::format` — `FMT_BEGIN_NAMESPACE`), which the index cannot see.
if ((ref.language === 'cpp' || ref.language === 'c') && ref.referenceName.includes('::')) {
return matchCppMacroNamespaced(ref, context);
}
return null;
}
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;`. */
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 line ranges of a C / C++ file each namespace macro opens, with the namespace path it opens. */
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;
}
/**
* Resolve `fmt::format` / `fmt::detail::to_unsigned` to the declaration a
* namespace macro puts there: a node named `format` (qualified `format` or
* `detail::to_unsigned` in the index, which cannot see the macro) inside a
* `FMT_BEGIN_NAMESPACE` … `FMT_END_NAMESPACE` range. On fmt, 1,728
* `fmt::format(…)` calls resolved to nothing.
*/
function matchCppMacroNamespaced(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
let target = ref.referenceName.replace(/^::/, '');
const head = target.slice(0, target.indexOf('::'));
const alias = cppNamespaceAliases(context).get(head);
// `py::str` under `namespace py = pybind11;` is `pybind11::str`.
if (alias) target = alias + target.slice(head.length);
const name = target.slice(target.lastIndexOf('::') + 2);
if (!/^[A-Za-z_~]\w*$/.test(name)) return null;
let best: Node | null = null;
for (const n of context.getNodesByName(name)) {
if (n.language !== 'cpp' && n.language !== 'c') continue;
if (!['function', 'method', 'class', 'struct', 'enum', 'type_alias', 'union', 'variable', 'constant'].includes(n.kind)) continue;
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);
const effective = prefix.length > 0 ? `${prefix.join('::')}::${n.qualifiedName}` : alias ? n.qualifiedName : '';
if (effective !== target) continue;
// One of an overload set is as good as another; a declaration outside the tests over one in them.
if (!best || (isTestPath(best.filePath) && !isTestPath(n.filePath))) best = n;
}
return best ? { original: ref, targetNodeId: best.id, confidence: 0.8, resolvedBy: 'qualified-name' } : null;
}