fix(cpp): a call through a namespace links no class's method by its name (#2730)

A C++ call written through a namespace, `x::m()`, reached matchMethodCall's
name strategies when no declaration of `x::m` was found by qualified name.
They can only return a method of some class, which a namespace never holds:
the capitalized-receiver class (`internal` → a class `Internal`), the one
method of the name in the project, or the best receiver-word guess. So
protocolbuffers/protobuf's `absl::StripPrefix(name, …)` linked to its Rust
generator's `MultiCasePrefixStripper::StripPrefix` and `absl::Status(…)` to
`upb::Status`'s constructor. fmtlib/fmt's `fmt::ptr(p)` linked to a test
scanner's `ptr()` method, and `fmt::day(…)` to a test's `date::day`, ahead of
the step that reads the namespaces a macro opens and finds fmt's own `ptr`
function and `day` alias in `FMT_BEGIN_NAMESPACE`.

Those strategies now return nothing when the qualifier is a namespace
(isCppNamespaceQualifier): a lowercase name that no C or C++ type or alias
node has, and that the calling file declares as neither a template parameter
nor an alias (cppNamesBroughtIn, now split into type names and namespace
names, since a namespace the file opens confirms it is one). rocksdb's
`hash_table::Insert(…)` through `typedef HashTable<…> hash_table;` keeps its
edge. So does the constructor of a class the call names in that namespace
(isCppConstructorThrough): protobuf's `internal::SourceWrapper<absl::Cord>(&data)`
constructs `struct SourceWrapper<absl::Cord>`, a specialization the index
holds without its name, whose template is only declared. A method of
another template's specialization is no constructor: fmt's
`fmt::scan<int>(…)` is fmt's `scan` function, not `scanner<num>::scan`.
The check runs only where one of those strategies would return.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 05:56:24 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 009e4377ff
commit 81fd007850
3 changed files with 442 additions and 10 deletions
+1
View File
@@ -257,6 +257,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In TypeScript, an interface that extends other interfaces, like `interface UserRepo extends Repo<User>, Named`, is now linked to each of them: the type hierarchy shows them above the interface and above every class that implements it, a change to an interface reaches the interfaces built on it and their implementations in impact results, and a call on a value typed by such an interface, like `hass.formatEntityName(…)` on a `HomeAssistant` that extends seven interfaces, links to the method the interface inherits instead of a same-named method elsewhere. Before, only a class's `extends` and `implements` were read, so every interface stood alone. A type from a package, like `ButtonProps` from `@mui/material`, or one of TypeScript's own, like `Omit<…>`, still links to nothing rather than to a type of the same name elsewhere in your project. Re-index TypeScript projects after upgrading.
- In TypeScript and JavaScript, a type, class, constant or other declaration that a `.d.ts` file declares inside `declare module '…'` now links only from files that use that module: files that import it, the module's other declarations, and files that import it through a file of yours that re-exports it. Before, any file using the same name linked to it: Node's `import { readFileSync } from 'fs'` linked to the `fs` that VS Code's `vscode.d.ts` declares, and a `ProcessEnv`, or an LSP `TextDocument` imported through a barrel, to the `vscode` module's types of those names. Re-index TypeScript projects after upgrading.
- In C++, a call written through a class the index doesn't hold, like protocolbuffers/protobuf's `FeatureSet::descriptor()` or `OneofOptions::default_instance()` (both classes are declared in `descriptor.pb.h`, a file too large to index), no longer links to another class's method of that name whose class only shares a word with it, like `JavaFeatures_NestInFileClassFeature::descriptor` or `CppFileOptions::default_instance`. A class named like the call's would have been found first, so one that only shares a word is another class. A call through a template parameter, through an alias, including one the calling function or a macro it expands declares, or through a namespace the file opens, aliases or uses links as before, and so does a call of a method only one class in the project declares. Re-index C++ projects after upgrading.
- In C++, a call written through a namespace, like protocolbuffers/protobuf's `absl::StripPrefix(…)` or fmtlib/fmt's `fmt::ptr(p)`, no longer links to a class's method that only shares the function's name, like protobuf's `MultiCasePrefixStripper::StripPrefix` or a test scanner's `ptr()`: a namespace holds functions and types, not methods. A call through a namespace a macro opens now links to what that namespace declares, so fmt's `fmt::ptr(p)` reaches fmt's own `ptr` function and `fmt::day(…)` its `day` type, while a call into a library outside your project, like abseil, links to nothing. A call through a lowercase class, or an alias or typedef of one, links as before. Re-index C++ projects after upgrading.
## [1.6.2] - 2026-10-03
+373
View File
@@ -0,0 +1,373 @@
/**
* A C++ call through a namespace — protobuf's `absl::StripPrefix(…)`, fmt's
* `fmt::ptr(p)` — links no class's method, which is all the name strategies
* of matchMethodCall can find: a namespace holds functions and types.
*
* protocolbuffers/protobuf's `absl::StripPrefix(name, …)` went to its Rust
* generator's `MultiCasePrefixStripper::StripPrefix`, the one method of that
* name, and `absl::Status(…)` to `upb::Status`'s constructor. fmtlib/fmt's
* `fmt::ptr(p)` went to a scanner's `ptr()` method; fmt's own `ptr` function,
* which `FMT_BEGIN_NAMESPACE` puts in `fmt`, is found by the step after those
* strategies, and is now what the call links.
*
* A lowercase qualifier is a namespace when no C or C++ type or alias has its
* name and the calling file declares it as neither a template parameter nor
* an alias. rocksdb's `hash_table::Insert(…)`, through `typedef HashTable<…>
* hash_table;`, still reaches `HashTable::Insert`.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import type { Node } from '../src/types';
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});
async function indexed(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-namespace-'));
roots.push(root);
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);
}
return CodeGraph.init(root, { index: true });
}
function callable(cg: CodeGraph, qualifiedName: string): Node {
const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName);
if (!node) throw new Error(`no function or method ${qualifiedName}`);
return node;
}
/** `calls` callees of a function or method, as `kind qualifiedName (file)`. */
function calls(cg: CodeGraph, caller: string): string[] {
return cg
.getCallees(callable(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => `${r.node.kind} ${r.node.qualifiedName} (${r.node.filePath})`)
.sort();
}
const lines = (...l: string[]): string => [...l, ''].join('\n');
/** fmt's namespace macros, and the scanner whose `ptr()` main took for `fmt::ptr`. */
const FMT = {
'include/fmt/base.h': lines(
'#define FMT_BEGIN_NAMESPACE namespace fmt { inline namespace v11 {',
'#define FMT_END_NAMESPACE } }',
),
'include/fmt/format.h': lines(
'#include "fmt/base.h"',
'FMT_BEGIN_NAMESPACE',
'template <typename T> const void* ptr(T p) { return p; }',
'FMT_END_NAMESPACE',
),
'include/fmt/chrono.h': lines(
'#include <chrono>',
'#include "fmt/base.h"',
'FMT_BEGIN_NAMESPACE',
'using day = std::chrono::day;',
'FMT_END_NAMESPACE',
),
'test/scan.h': lines(
'#include "fmt/base.h"',
'FMT_BEGIN_NAMESPACE',
'namespace detail {',
'class scan_buffer {',
' public:',
' const char* ptr() const { return ptr_; }',
' private:',
' const char* ptr_;',
'};',
'} // namespace detail',
'FMT_END_NAMESPACE',
),
'test/util.h': lines(
'namespace date {',
'class day {',
' public:',
' explicit day(unsigned d) : d_(d) {}',
' private:',
' unsigned d_;',
'};',
'} // namespace date',
),
};
describe('a C++ call through a namespace', () => {
it('reproduction: `fmt::ptr(p)` is fmt\'s own `ptr`, not a scanner\'s `ptr()` method', async () => {
const cg = await indexed({
...FMT,
'test/format-test.cc': lines(
'#include "fmt/format.h"',
'#include "scan.h"',
'const void* Pointer(int* p) { return fmt::ptr(p); }',
),
});
try {
expect(calls(cg, 'Pointer')).toEqual(['function ptr (include/fmt/format.h)']);
} finally {
cg.close();
}
});
it('a type a namespace macro puts in fmt: `fmt::day(1)` is fmt\'s alias, not a test\'s `date::day`', async () => {
const cg = await indexed({
...FMT,
'test/chrono-test.cc': lines(
'#include "fmt/chrono.h"',
'#include "util.h"',
'void Days() { auto d = fmt::day(1); }',
),
});
try {
expect(calls(cg, 'Days')).toEqual(['type_alias day (include/fmt/chrono.h)']);
} finally {
cg.close();
}
});
it('an outside library\'s namespace: `absl::StripPrefix` and `absl::Status` link nothing', async () => {
const cg = await indexed({
'src/google/protobuf/compiler/rust/naming.h': lines(
'#include <string>',
'namespace google {',
'namespace protobuf {',
'namespace compiler {',
'namespace rust {',
'class MultiCasePrefixStripper {',
' public:',
' std::string StripPrefix(std::string name) const { return name; }',
'};',
'} // namespace rust',
'} // namespace compiler',
'} // namespace protobuf',
'} // namespace google',
),
'hpb/status.h': lines(
'namespace upb {',
'class Status {',
' public:',
' Status() : ok_(true) {}',
' private:',
' bool ok_;',
'};',
'} // namespace upb',
),
'src/google/protobuf/compiler/python/generator.cc': lines(
'#include <string>',
'#include "absl/status/status.h"',
'#include "absl/strings/strip.h"',
'namespace google {',
'namespace protobuf {',
'namespace compiler {',
'namespace python {',
'std::string ModuleName(std::string name) {',
' auto status = absl::Status();',
' return absl::StripPrefix(name, "google.protobuf.");',
'}',
'} // namespace python',
'} // namespace compiler',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
expect(calls(cg, 'google::protobuf::compiler::python::ModuleName')).toEqual([]);
} finally {
cg.close();
}
});
});
describe('every name strategy steps aside for a namespace', () => {
it('the class named like the namespace, the best word guess, and a namespace the calling file opens', async () => {
const cg = await indexed({
'src/internal.h': lines(
'namespace leveldb {',
'class Internal {',
' public:',
' static void Fill(int n) {}',
'};',
'class LogWriter {',
' public:',
' void Flush() {}',
'};',
'class TableBuilder {',
' public:',
' void Flush() {}',
'};',
'class PyGenerator {',
' public:',
' static int Escape(int n) { return n; }',
'};',
'} // namespace leveldb',
),
'src/use.cc': lines(
'#include "src/internal.h"',
'namespace leveldb {',
'namespace python {',
'void Use(int n) {',
' internal::Fill(n);',
' log::Flush();',
' python::Escape(n);',
'}',
'} // namespace python',
'} // namespace leveldb',
),
});
try {
expect(calls(cg, 'leveldb::python::Use')).toEqual([]);
} finally {
cg.close();
}
});
});
describe('the constructor of a class a call names in its namespace', () => {
it('is kept: protobuf\'s `internal::SourceWrapper<absl::Cord>(&data)` constructs the specialization', async () => {
const cg = await indexed({
'src/google/protobuf/message_lite.h': lines(
'namespace google {',
'namespace protobuf {',
'namespace internal {',
'template <typename T>',
'struct SourceWrapper;',
'} // namespace internal',
'} // namespace protobuf',
'} // namespace google',
),
'src/google/protobuf/message_lite.cc': lines(
'#include "google/protobuf/message_lite.h"',
'#include "absl/strings/cord.h"',
'namespace google {',
'namespace protobuf {',
'namespace internal {',
'template <>',
'struct SourceWrapper<absl::Cord> {',
' explicit SourceWrapper(const absl::Cord* c) : cord(c) {}',
' const absl::Cord* cord;',
'};',
'} // namespace internal',
'int ParseCord(const absl::Cord& data) {',
' auto source = internal::SourceWrapper<absl::Cord>(&data);',
' return source.cord == nullptr;',
'}',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
const constructors = cg.getNodesByKind('method').filter((n) => n.name === 'SourceWrapper');
expect(constructors).toHaveLength(1);
expect(calls(cg, 'google::protobuf::ParseCord')).toEqual([`method ${constructors[0]!.qualifiedName} (src/google/protobuf/message_lite.cc)`]);
} finally {
cg.close();
}
});
it('but not a method of another template\'s specialization: `fmt::scan<int>(…)` is no `scanner<num>::scan`', async () => {
const cg = await indexed({
'include/fmt/base.h': FMT['include/fmt/base.h'],
'test/scan.h': lines(
'#include "fmt/base.h"',
'FMT_BEGIN_NAMESPACE',
'template <typename T> struct scanner;',
'template <typename... T> int scan(const char* input) { return 0; }',
'FMT_END_NAMESPACE',
),
'test/scan-test.cc': lines(
'#include "scan.h"',
'struct num {',
' int value;',
'};',
'namespace fmt {',
'template <> struct scanner<num> {',
' int scan(num& n) const { return n.value; }',
'};',
'} // namespace fmt',
'int Scan() { return fmt::scan<int>("42"); }',
),
});
try {
expect(calls(cg, 'Scan')).toEqual(['function scan (test/scan.h)']);
} finally {
cg.close();
}
});
});
describe('a lowercase qualifier that is a type is untouched', () => {
it('a typedef the class declares: rocksdb\'s `hash_table::Insert` is HashTable::Insert', async () => {
const cg = await indexed({
'utilities/persistent_cache/hash_table_evictable.h': lines(
'namespace rocksdb {',
'template <class T, class Hash, class Equal>',
'class HashTable {',
' public:',
' bool Insert(const T& t) { return true; }',
'};',
'template <class T, class Hash, class Equal>',
'class EvictableHashTable : private HashTable<T*, Hash, Equal> {',
' public:',
' typedef HashTable<T*, Hash, Equal> hash_table;',
' bool Insert(T* t) { return hash_table::Insert(t); }',
'};',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::EvictableHashTable::Insert')).toEqual([
'method rocksdb::HashTable::Insert (utilities/persistent_cache/hash_table_evictable.h)',
]);
} finally {
cg.close();
}
});
it('an alias the calling function declares, a template parameter\'s default, and a class', async () => {
const cg = await indexed({
'util/ring.h': lines(
'namespace rocksdb {',
'class RingBuffer {',
' public:',
' static int Capacity() { return 8; }',
'};',
'struct default_policy {',
' static int apply() { return 1; }',
'};',
'class string_buffer {',
' public:',
' static int initial_size() { return 500; }',
'};',
'using ring_buffer = RingBuffer;',
'} // namespace rocksdb',
),
'util/ring.cc': lines(
'#include "util/ring.h"',
'namespace rocksdb {',
'int Capacity() {',
' using ring = RingBuffer;',
' return ring::Capacity();',
'}',
'int HeaderCapacity() { return ring_buffer::Capacity(); }',
'template <typename policy = default_policy>',
'int Apply() { return policy::apply(); }',
'int InitialSize() { return string_buffer::initial_size(); }',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::Capacity')).toEqual(['method rocksdb::RingBuffer::Capacity (util/ring.h)']);
expect(calls(cg, 'rocksdb::HeaderCapacity')).toEqual(['method rocksdb::RingBuffer::Capacity (util/ring.h)']);
expect(calls(cg, 'rocksdb::Apply')).toEqual(['method rocksdb::default_policy::apply (util/ring.h)']);
expect(calls(cg, 'rocksdb::InitialSize')).toEqual(['method rocksdb::string_buffer::initial_size (util/ring.h)']);
} finally {
cg.close();
}
});
});
+68 -10
View File
@@ -16163,13 +16163,55 @@ function isUndeclaredTypeName(typeName: string, ref: UnresolvedRef, context: Res
* (cppNamesBroughtIn).
*/
function isCppUndeclaredQualifier(qualifier: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
return isUndeclaredTypeName(qualifier, ref, context) &&
cppTypesNamed(qualifier, context).length === 0 &&
!context.getNodesByName(qualifier).some((n) => (n.language === 'cpp' || n.language === 'c') && (n.kind === 'namespace' || n.kind === 'module')) &&
!cppNamesBroughtIn(ref.filePath, context).has(qualifier);
if (!isUndeclaredTypeName(qualifier, ref, context) || cppTypesNamed(qualifier, context).length > 0 ||
context.getNodesByName(qualifier).some((n) => (n.language === 'cpp' || n.language === 'c') && (n.kind === 'namespace' || n.kind === 'module'))) return false;
const names = cppNamesBroughtIn(ref.filePath, context);
return !names.types.has(qualifier) && !names.namespaces.has(qualifier);
}
const CPP_FILE_TYPE_NAMES = new WeakMap<ResolutionContext, Map<string, ReadonlySet<string>>>();
/**
* Is the qualifier of a C++ `x::m()` call a namespace, `absl` in
* `absl::StripPrefix(…)` or `fmt` in `fmt::ptr(p)`, and not a type? A
* lowercase name that no C or C++ type or alias node has, and that the
* calling file doesn't declare as a template parameter or an alias
* (cppNamesBroughtIn). A namespace holds functions and types, never a
* class's method.
*/
function isCppNamespaceQualifier(qualifier: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
return /^[a-z_]/.test(qualifier) && cppTypesNamed(qualifier, context).length === 0 &&
!cppNamesBroughtIn(ref.filePath, context).types.has(qualifier);
}
/**
* Is `method` the constructor of the class a C++ call `ns::m(…)` names
* through its namespace? Its owner, right inside `ns`, is `m` itself or a
* specialization of the class template `m`, which the index holds without
* a name: protobuf's `internal::SourceWrapper<absl::Cord>(&data)` constructs
* the `struct SourceWrapper<absl::Cord>` in message_lite.cc, whose template
* is only declared. fmt's `template <> struct scanner<num>` is no `scan`.
*/
function isCppConstructorThrough(namespace: string, method: Node, context: ResolutionContext): boolean {
const parts = method.qualifiedName.split('::');
if (parts.length < 3 || parts[parts.length - 3] !== namespace) return false;
const owner = parts[parts.length - 2]!;
if (owner === method.name) return true;
if (owner !== '<anonymous>') return false;
const holder = context.getNodesByQualifiedName(parts.slice(0, -1).join('::')).find((n) =>
n.filePath === method.filePath && n.startLine <= method.startLine && (n.endLine ?? n.startLine) >= method.startLine);
const lines = holder ? context.getFileLines?.(holder.filePath) ?? context.readFile(holder.filePath)?.split(/\r?\n/) : undefined;
const head = holder && lines ? lines.slice(holder.startLine - 1, holder.startLine + 1).join(' ') : '';
return new RegExp(String.raw`\b(?:class|struct|union)\s+(?:[A-Za-z_]\w*\s+)*${method.name}\s*<`).test(head);
}
/** The names a C++ file makes something of itself, which the index holds no node for (see cppNamesBroughtIn). */
interface CppFileNames {
/** Template parameters and aliases. */
types: ReadonlySet<string>;
/** The namespaces it opens, aliases or uses. */
namespaces: ReadonlySet<string>;
}
const CPP_FILE_TYPE_NAMES = new WeakMap<ResolutionContext, Map<string, CppFileNames>>();
const CPP_MACRO_TYPE_NAMES = new WeakMap<ResolutionContext, Map<string, ReadonlySet<string>>>();
/**
@@ -16180,20 +16222,22 @@ const CPP_MACRO_TYPE_NAMES = new WeakMap<ResolutionContext, Map<string, Readonly
* names: opened, aliased or used (`using namespace …;`). googletest's typed
* tests declare `TypeParam` and `TestFixture`.
*/
function cppNamesBroughtIn(file: string, context: ResolutionContext): ReadonlySet<string> {
function cppNamesBroughtIn(file: string, context: ResolutionContext): CppFileNames {
let memo = CPP_FILE_TYPE_NAMES.get(context);
if (!memo) CPP_FILE_TYPE_NAMES.set(context, (memo = new Map()));
const hit = memo.get(file);
if (hit) return hit;
const text = stripCommentsForRegex((context.readFile(file) ?? '').replace(/\\\r?\n/g, ' '), 'cpp');
const names = cppTypeNamesDeclared(text);
const types = cppTypeNamesDeclared(text);
const namespaces = new Set<string>();
for (const m of text.matchAll(/\bnamespace\s+([^;{}()]{1,200}?)\s*[;{]/g)) {
for (const name of m[1]!.match(/[A-Za-z_]\w*/g) ?? []) names.add(name);
for (const name of m[1]!.match(/[A-Za-z_]\w*/g) ?? []) namespaces.add(name);
}
for (const [macro, declared] of cppMacroTypeNames(context)) {
if (hasCppWord(text, macro)) for (const name of declared) names.add(name);
if (hasCppWord(text, macro)) for (const name of declared) types.add(name);
}
if (/\bTYPED_TEST(?:_P)?\s*\(/.test(text)) names.add('TypeParam').add('TestFixture');
if (/\bTYPED_TEST(?:_P)?\s*\(/.test(text)) types.add('TypeParam').add('TestFixture');
const names = { types, namespaces };
memo.set(file, names);
return names;
}
@@ -19165,6 +19209,17 @@ export function matchMethodCall(
return null;
}
// A C++ name through a namespace, `absl::StripPrefix(…)` or `fmt::ptr(p)`,
// is a function or type of that namespace, never a class's method, which
// is all the strategies below find: protobuf's `absl::StripPrefix` went to
// `rust::MultiCasePrefixStripper::StripPrefix`, and fmt's `fmt::ptr` to a
// scanner's `ptr` method, past fmt's own `ptr`, which a namespace macro
// declares and is read after them. The one exception is the constructor of
// a class the call names in that namespace (isCppConstructorThrough).
// Asked only of a guess about to be made.
const throughCppNamespace = (): boolean =>
ref.language === 'cpp' && match === colonMatch && isCppNamespaceQualifier(objectOrClass!, ref, context);
// Strategy 2: Instance variable receiver - try capitalized form to find class
// e.g., "permissionEngine" → look for a class named "PermissionEngine"
const capitalizedReceiver = objectOrClass!.charAt(0).toUpperCase() + objectOrClass!.slice(1);
@@ -19186,6 +19241,7 @@ export function matchMethodCall(
const methodNode = classStrategyMethod(classNode, methodName!, context);
if (methodNode) {
if (throughCppNamespace()) return null;
return {
original: ref,
targetNodeId: methodNode.id,
@@ -19299,6 +19355,7 @@ export function matchMethodCall(
!sharesReceiverWord(objectOrClass!, targetMethods[0]!) &&
!(ref.language === 'objc' && objcReceiverReaches(objectOrClass!, targetMethods[0]!, context)) &&
!(ref.language === 'php' && phpReceiverReaches(objectOrClass!, targetMethods[0]!, context)))) {
if (throughCppNamespace() && !isCppConstructorThrough(objectOrClass!, targetMethods[0]!, context)) return null;
return {
original: ref,
targetNodeId: targetMethods[0]!.id,
@@ -19382,6 +19439,7 @@ export function matchMethodCall(
// `JavaFeatures_NestInFileClassFeature::descriptor`, its Python
// extension's `OneofOptions::default_instance()` to `CppFileOptions`'.
if (ref.language === 'cpp' && match === colonMatch && isCppUndeclaredQualifier(objectOrClass!, ref, context)) return null;
if (throughCppNamespace() && !isCppConstructorThrough(objectOrClass!, bestMatch, context)) return null;
return {
original: ref,
targetNodeId: bestMatch.id,