fix(cpp): an alias naming a class through a derived class leads to the class it inherits (#2549)

protobuf's `using ConformanceRequestSetting =
BinaryAndJsonConformanceSuite::ConformanceRequestSetting;` names a class the
suite inherits from `ConformanceTestSuite`. An alias was followed only to a
class declared under exactly the name it spells, so constructions through it
stopped at the alias. Its target is now looked up a segment at a time through
each class's bases, as C++ does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-14 06:31:04 +00:00
committed by GitHub
parent 6df4827625
commit 754cc45ad7
4 changed files with 178 additions and 5 deletions
+1
View File
@@ -109,6 +109,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In C++, a call to an overloaded method now links to the overload its arguments fit: by how many there are, and then by their types. That covers a call on a variable or member of a known class, like facebook/rocksdb's `db->Get(ro, key, &value)` and `db->Put(write_opts, key, value)`, as well as a call written without a receiver, like `Insert(ch)` on a `char ch`. Before, a call on a variable always linked to the first overload declared, so RocksDB's convenience overloads of `DB::Get`, `DB::Put` and `DB::NewIterator` and protocolbuffers/protobuf's `io::Printer::Print`, `Emit` and `Annotate` had almost no callers, and a call without a receiver picked by argument count alone. A literal, a variable the calling function declares, `&variable`, `container[i]` and the result of a call are read for their types; a parameter's default value is read from the method's declaration in its class, and comments in argument lists are skipped. Re-index C++ projects after upgrading.
- In C++, a nested class defined outside the class it belongs to, like google/leveldb's `struct SkipList<Key, Comparator>::Node { … }` or facebook/rocksdb's `struct BlockBasedTable::Rep { … }`, is now indexed under its own name, `SkipList::Node`, with its methods and members. Before, it was indexed as an unnamed class, so it was missing from search and `codegraph_explore`, its methods belonged to no class you could name, and a call like `x->Next(level)` on a `Node*` was matched by the method's name alone. Creating an object, as in `new log::Writer(…)` or `Writer w(…)`, now links to the class C++ itself finds for the type as written there: leveldb's `new log::Writer(…)` in `DBImpl` methods no longer links to `DBImpl`'s own `Writer`, RocksDB's tests' `Random rnd(301)` links to RocksDB's `Random` instead of googletest's, an object created through an alias links to the class the alias names, and one links to its class rather than to the class's constructor. An object of a template parameter's type, or of a type the function names with its own `using`, like protocolbuffers/protobuf's `using Map = std::map<…>`, no longer links to a project class that only shares the name. Re-index C++ projects after upgrading.
- In C++, a call whose receiver's type can't be read no longer links to a method of a class nested in the class the receiver's name suggests, or of a class whose name only contains that name. Before, such a guess took any method of the call's name whose qualified name contained the class's name: protocolbuffers/protobuf's `descriptor->file()` in a template reached `Descriptor::ExtensionRange::file`, and facebook/rocksdb's `CompositeEnv::IsInstanceOf(name)` reached `CompositeEnvWrapper::IsInstanceOf`. A call written through a class, like `PessimisticTransaction::PopSavePoint()` in an override, now links to the method that class inherits, here `TransactionBaseImpl::PopSavePoint`. Re-index C++ projects after upgrading.
- In C++, an alias that names a class through a class derived from the one that declares it, like protocolbuffers/protobuf's `using ConformanceRequestSetting = BinaryAndJsonConformanceSuite::ConformanceRequestSetting;`, now leads to that class, `ConformanceTestSuite::ConformanceRequestSetting`. Before, an alias was followed only to a class declared under exactly the name it spells, so objects created through this one linked to the alias rather than the class. Re-index C++ projects after upgrading.
- Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading.
- In a repository that mixes languages, a web framework's naming conventions now link only code written in that framework's own languages. Before, Express's also linked Go and Python code: in etcd a Go method's `*zap.Logger` result type linked to the method itself, and in Harbor a Python `self.jaeger.validate()` call linked to another class's `validate`. Links that cross languages on purpose, like React Native and Expo native modules, Swift and Objective-C, Drupal's `routing.yml`, SvelteKit's `$lib/…` imports in `.ts` files and a Razor page's `@model`, are unchanged.
- In C and C++, a call written from the global scope, like `::leveldb::RepairDB(…)` or `::hpb::CreateMessage(…)`, now links to the function of exactly that name, and one through a namespace alias, like protobuf's `::_pbi::…` calls, to the function in the aliased namespace. Before, these calls were dropped unless the repository also held Swift and Objective-C files, and even then a name spelled out in full, like `::google::protobuf::internal::TSanWrite(…)`, linked to nothing, while `::operator delete(…)` linked to one class's own `operator delete`. A global call never links to a function or method of the same name inside a namespace or class, such as a wrapper `open()` that calls the C library's `::open(…)`, and `::std::…` and `::testing::…` calls stay unlinked unless that library's source is part of your project. Re-index C and C++ projects after upgrading.
@@ -0,0 +1,151 @@
/**
* A C++ alias can name a class one of a class's bases declares:
* protocolbuffers/protobuf's conformance suites write `using
* ConformanceRequestSetting = BinaryAndJsonConformanceSuite::ConformanceRequestSetting;`,
* a class their base `ConformanceTestSuite` declares. Following the alias
* looked the class up only by the qualified name as spelled, which no class
* has, so constructions through the alias stopped at the alias and calls on
* such a variable lost their class. The aliased type is now looked up the way
* a declared type is, a class's members through its bases too.
*/
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';
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-alias-base-'));
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 });
}
const lines = (...text: string[]): string => [...text, ''].join('\n');
/** What a function's or method's `kind` edges reach, as `qualifiedName @confidence`. */
function targets(cg: CodeGraph, qualifiedName: string, kind: 'calls' | 'instantiates'): string[] {
const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!;
return cg.getOutgoingEdgesFrom([from.id], [kind])
.map((e) => `${cg.getNode(e.target)?.qualifiedName} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`)
.sort();
}
const NS = 'google::protobuf::conformance';
const SUITE = lines(
'namespace google {',
'namespace protobuf {',
'namespace conformance {',
'class ConformanceTestSuite {',
' public:',
' class ConformanceRequestSetting {',
' public:',
' ConformanceRequestSetting(int level, const char* name) {}',
' const char* GetTestName() const { return ""; }',
' };',
'};',
'} // namespace conformance',
'} // namespace protobuf',
'} // namespace google',
);
describe('C++ aliases of a class a base declares', () => {
it('reach the class through an alias spelled through the derived class', async () => {
const cg = await indexed({
'conformance/conformance_test.h': SUITE,
'conformance/binary_json_conformance_suite.h': lines(
'#include "conformance/conformance_test.h"',
'namespace google {',
'namespace protobuf {',
'namespace conformance {',
'class BinaryAndJsonConformanceSuite : public ConformanceTestSuite {};',
'template <typename MessageType>',
'class BinaryAndJsonConformanceSuiteImpl {',
' public:',
' void RunTest();',
' private:',
' using ConformanceRequestSetting =',
' BinaryAndJsonConformanceSuite::ConformanceRequestSetting;',
'};',
'} // namespace conformance',
'} // namespace protobuf',
'} // namespace google',
),
'conformance/binary_json_conformance_suite.cc': lines(
'#include "conformance/binary_json_conformance_suite.h"',
'namespace google {',
'namespace protobuf {',
'namespace conformance {',
'template <typename MessageType>',
'void BinaryAndJsonConformanceSuiteImpl<MessageType>::RunTest() {',
' ConformanceRequestSetting setting(1, "x");',
' setting.GetTestName();',
'}',
'// An alias the function declares itself is not indexed: read from the function.',
'void RunLocal() {',
' using ConformanceRequestSetting = BinaryAndJsonConformanceSuite::ConformanceRequestSetting;',
' ConformanceRequestSetting local(3, "w");',
' local.GetTestName();',
'}',
'} // namespace conformance',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
const run = `${NS}::BinaryAndJsonConformanceSuiteImpl::RunTest`;
expect(targets(cg, run, 'instantiates')).toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting @0.9`]);
expect(targets(cg, run, 'calls').filter((t) => t.includes('GetTestName')))
.toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting::GetTestName @0.9`]);
expect(targets(cg, `${NS}::RunLocal`, 'instantiates')).toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting @0.9`]);
expect(targets(cg, `${NS}::RunLocal`, 'calls').filter((t) => t.includes('GetTestName')))
.toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting::GetTestName @0.9`]);
} finally {
cg.close();
}
});
it("reach the class through an alias a derived class writes with the base's name alone", async () => {
const cg = await indexed({
'conformance/conformance_test.h': SUITE,
'conformance/text_format_suite.h': lines(
'#include <string>',
'#include "conformance/conformance_test.h"',
'namespace google {',
'namespace protobuf {',
'namespace conformance {',
'class TextFormatSuite : public ConformanceTestSuite {',
' public:',
' using Setting = ConformanceRequestSetting;',
' using Name = std::string;',
' void Run() {',
' Setting setting(2, "y");',
' setting.GetTestName();',
' Name name("z");',
' }',
'};',
'} // namespace conformance',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
const run = `${NS}::TextFormatSuite::Run`;
// (An alias of a type from outside the project still makes the alias.)
expect(targets(cg, run, 'instantiates'))
.toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting @0.9`, `${NS}::TextFormatSuite::Name @0.9`]);
expect(targets(cg, run, 'calls').filter((t) => t.includes('GetTestName')))
.toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting::GetTestName @0.9`]);
} finally {
cg.close();
}
});
});
+25 -4
View File
@@ -438,6 +438,22 @@ function baseClassesOf(cls: string, context: ResolutionContext): string[] {
return bases;
}
/**
* The qualified name of the class `names` spells, looked up from `scope`:
* spelled out in a scope around it (classNamed), else a segment at a time as
* a declared type is, so a class one of the named class's bases declares is
* found. protobuf's conformance suites alias `using ConformanceRequestSetting =
* BinaryAndJsonConformanceSuite::ConformanceRequestSetting;`, a class their
* base `ConformanceTestSuite` declares.
*/
function classReached(names: readonly string[], scope: string, ref: UnresolvedRef, context: ResolutionContext): string | undefined {
const spelled = classNamed(names, scopesWithin(scope), context);
if (spelled) return spelled;
// (The walk stops on a class only once every segment is taken.)
const { found } = lookupSegments({ names: [...names], templated: names.map(() => false) }, scopesWithin(scope), ref, context);
return found && typeof found === 'object' && 'cls' in found ? found.cls : undefined;
}
/** The qualified name of the class `names` spells, looked up from `scopes`. */
function classNamed(names: readonly string[], scopes: readonly string[], context: ResolutionContext): string | undefined {
const spelled = names.join('::');
@@ -521,9 +537,14 @@ export function cppAliasedTypeName(resolved: CppAliasedType): string {
return [resolved.target[resolved.target.length - 1]!, ...resolved.rest].join('::');
}
/** The qualified name of the project class a type resolved through aliases is, if it is one: `leveldb::SkipList::Iterator`. */
export function cppAliasedClass(resolved: CppAliasedType, context: ResolutionContext): string | undefined {
return classNamed([...resolved.target, ...resolved.rest], scopesWithin(resolved.scope), context);
/**
* The qualified name of the project class a type resolved through aliases is,
* if it is one: `leveldb::SkipList::Iterator`. With the ref it is resolved
* for, a class a base of the named class declares counts (classReached).
*/
export function cppAliasedClass(resolved: CppAliasedType, context: ResolutionContext, ref?: UnresolvedRef): string | undefined {
const names = [...resolved.target, ...resolved.rest];
return ref ? classReached(names, resolved.scope, ref, context) : classNamed(names, scopesWithin(resolved.scope), context);
}
/** What a C++ type name names when it is looked up for a class (see cppClassNamed). */
@@ -573,7 +594,7 @@ export function cppClassNamed(
// The aliased type is written in the alias's own scope.
const aliased = expand(text ? aliasedTypeIn(text, alias.name) : null, name.names.slice(next), name.templated.slice(next),
parentScope(alias.qualifiedName), ref, context, 1, templateParametersAround(alias, context));
const cls = aliased && !aliased.pointer && classNamed([...aliased.target, ...aliased.rest], scopesWithin(aliased.scope), context);
const cls = aliased && !aliased.pointer && classReached([...aliased.target, ...aliased.rest], aliased.scope, ref, context);
if (cls) return { cls };
// A member of a type outside the project (`Alias::Inner`) is not the alias.
return next === name.names.length ? { alias } : null;
+1 -1
View File
@@ -11405,7 +11405,7 @@ function matchCppConstruction(ref: UnresolvedRef, context: ResolutionContext): R
: { original: ref, targetNodeId: node.id, confidence: 0.7, resolvedBy: 'exact-match' };
if (cppDeclaresLocalAlias(written, ref, context)) {
const aliased = resolveCppAliasedType(written, ref, context);
const named = aliased && !aliased.pointer ? cppAliasedClass(aliased, context) : undefined;
const named = aliased && !aliased.pointer ? cppAliasedClass(aliased, context, ref) : undefined;
const cls = named ? context.getNodesByQualifiedName(named).find((n) => CPP_CLASS_KINDS.has(n.kind)) : undefined;
return cls ? made(cls) : null;
}