Files
codegraph/__tests__/cpp-global-alias-target.test.ts
Colby Mchenry 759acbbccb fix(cpp): an alias of a type written from the global scope names that global type (#2568)
protobuf's hpb declares `class Arena { … backend::Arena arena_; }`, where
`namespace backend = hpb::internal::backend::upb;` and that namespace says
`using Arena = ::upb::Arena;`. cppTypeName dropped the leading `::`, so the
alias's target was looked up as `upb::Arena` from the alias's own scope,
whose nearest `upb` is that very namespace: the alias named itself, the
lookup ended on no class, and `arena_.Fuse(other.arena_)` fell back to the
type's last name, linking `hpb::Arena::Fuse` (and `IsFused`, `RefArena`) to
itself instead of `upb::Arena::Fuse`.

TypeName now records a leading `::` (`global`), and a global name is looked
up from the global scope alone: when an alias's target is followed (follow,
and the scope expand looks the target up from and hands on), in a class's
base clause (baseClassesOf), and in cppAliasedTypeWritten, which #2532's
`auto` locals and chains walk alias by alias.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:21:12 +00:00

217 lines
7.1 KiB
TypeScript

/**
* A C++ type written from the global scope (`::upb::Arena`) is looked up there
* alone, also where an alias names it and where a class's base clause or a
* declaration writes it.
*
* protocolbuffers/protobuf's hpb declares `class Arena { … backend::Arena
* arena_; }`, where `namespace backend = hpb::internal::backend::upb;` and
* that namespace says `using Arena = ::upb::Arena;`. The alias was looked up
* as if written `upb::Arena`, from its own namespace, whose nearest `upb` is
* that namespace itself: the alias named itself, no class was found, and
* `arena_.Fuse(other.arena_)` fell back to the type's last name, landing on
* the calling method `hpb::Arena::Fuse`.
*/
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-global-alias-'));
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 `qualifiedName`. */
function calls(cg: CodeGraph, caller: string): string[] {
return cg
.getCallees(callable(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => r.node.qualifiedName)
.sort();
}
const lines = (...l: string[]): string => [...l, ''].join('\n');
const UPB_ARENA = lines(
'namespace upb {',
'class Arena {',
' public:',
' bool Fuse(Arena& other) { return true; }',
' bool IsFused(Arena& other) const { return false; }',
' void RefArena(const Arena& to) {}',
'};',
'} // namespace upb',
);
describe('a C++ alias of a type written from the global scope', () => {
it('reproduction: protobuf hpb\'s `backend::Arena` is `upb::Arena`, not the calling method\'s own class', async () => {
const cg = await indexed({
'upb/mem/arena.hpp': UPB_ARENA,
'hpb/multibackend.h': lines(
'#define HPB_INTERNAL_BACKEND_UPB 1',
'#define HPB_INTERNAL_BACKEND_CPP 2',
'namespace hpb {',
'namespace internal {',
'namespace backend {',
'namespace upb {}',
'namespace cpp {}',
'} // namespace backend',
'} // namespace internal',
'#if HPB_INTERNAL_BACKEND == HPB_INTERNAL_BACKEND_UPB',
'namespace backend = hpb::internal::backend::upb;',
'#elif HPB_INTERNAL_BACKEND == HPB_INTERNAL_BACKEND_CPP',
'namespace backend = hpb::internal::backend::cpp;',
'#endif',
'} // namespace hpb',
),
'hpb/backend/types.h': lines(
'#include "hpb/multibackend.h"',
'#include "upb/mem/arena.hpp"',
'namespace hpb {',
'namespace internal {',
'namespace backend {',
'#if HPB_INTERNAL_BACKEND == HPB_INTERNAL_BACKEND_UPB',
'namespace upb {',
'using Arena = ::upb::Arena;',
'}',
'#elif HPB_INTERNAL_BACKEND == HPB_INTERNAL_BACKEND_CPP',
'namespace cpp {',
'using Arena = google::protobuf::Arena;',
'}',
'#endif',
'} // namespace backend',
'} // namespace internal',
'} // namespace hpb',
),
'hpb/arena.h': lines(
'#include "hpb/backend/types.h"',
'namespace hpb {',
'class Arena {',
' public:',
' bool Fuse(Arena& other) { return arena_.Fuse(other.arena_); };',
' bool IsFused(Arena& other) { return arena_.IsFused(other.arena_); };',
' void RefArena(const Arena& to) { arena_.RefArena(to.arena_); }',
' private:',
' backend::Arena arena_;',
'};',
'} // namespace hpb',
),
});
try {
expect(calls(cg, 'hpb::Arena::Fuse')).toEqual(['upb::Arena::Fuse']);
expect(calls(cg, 'hpb::Arena::IsFused')).toEqual(['upb::Arena::IsFused']);
expect(calls(cg, 'hpb::Arena::RefArena')).toEqual(['upb::Arena::RefArena']);
} finally {
cg.close();
}
});
it('names the global class, not a namesake in the scope around the alias', async () => {
const cg = await indexed({
'upb/mem/arena.hpp': UPB_ARENA,
'hpb/arena.cc': lines(
'#include "upb/mem/arena.hpp"',
'namespace hpb {',
'namespace upb {',
'class Arena {',
' public:',
' bool Fuse(Arena& other) { return false; }',
'};',
'} // namespace upb',
'using GlobalArena = ::upb::Arena;',
'bool Join(GlobalArena& a, GlobalArena& b) {',
' return a.Fuse(b);',
'}',
'} // namespace hpb',
),
});
try {
expect(calls(cg, 'hpb::Join')).toEqual(['upb::Arena::Fuse']);
} finally {
cg.close();
}
});
it('through an `auto` variable the alias types', async () => {
const cg = await indexed({
'upb/mem/arena.hpp': UPB_ARENA,
'hpb/arena.cc': lines(
'#include "upb/mem/arena.hpp"',
'namespace hpb {',
'namespace upb {',
'using Arena = ::upb::Arena;',
'} // namespace upb',
'class Arena {',
' public:',
' bool Fuse(Arena& other) { return false; }',
'};',
'upb::Arena MakeArena() { return upb::Arena(); }',
'bool Join(upb::Arena& other) {',
' auto made = MakeArena();',
' return made.Fuse(other);',
'}',
'} // namespace hpb',
),
});
try {
expect(calls(cg, 'hpb::Join').filter((c) => c.endsWith('::Fuse'))).toEqual(['upb::Arena::Fuse']);
} finally {
cg.close();
}
});
});
describe('other C++ names written from the global scope', () => {
it('a base class: `::lib::Options`, not the namespace\'s own `lib::Options`', async () => {
const cg = await indexed({
'src/config.cc': lines(
'namespace app {',
'struct Inner {',
' void Run() {}',
'};',
'} // namespace app',
'namespace lib {',
'struct Options {',
' struct Inner {',
' void Run() {}',
' };',
'};',
'} // namespace lib',
'namespace app {',
'namespace lib {',
'struct Options {};',
'} // namespace lib',
'class Config : public ::lib::Options {};',
'void Use() {',
' Config::Inner inner;',
' inner.Run();',
'}',
'} // namespace app',
),
});
try {
expect(calls(cg, 'app::Use')).toEqual(['lib::Options::Inner::Run']);
} finally {
cg.close();
}
});
});