mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 19:30:10 +08:00
A class's data members are read from its own lines, skipping those of
the functions and classes nested in it. The index keeps an anonymous
union or struct as a nested `<anonymous>` node, so its lines were skipped
too, though C++ looks its members up as the enclosing class's
([class.union.anon]).
Every generated protobuf message keeps its fields in `union { Impl_
_impl_; };`, so a call through `_impl_` was guessed by the method's name:
- `this_._impl_.type_url_.Destroy()` reached `Arena::Destroy`;
- `_impl_._cached_size_.Get()` reached `FieldArenaRep::Get`;
- `_impl_._has_bits_.Clear()` reached the message's own `Clear`.
fmt's `value` keeps its argument in a union of `string`, `custom` and the
rest in the same way.
cppNestedSpans now leaves out an anonymous union or struct with no name
and no declarator (isCppAnonymousMembers):
- a bare `union {` or `struct {` head;
- `};` after the closing brace.
A union with a declarator, `union { … } u;`, keeps its members to itself,
and so does a partial specialization, which the index also keeps as
`<anonymous>` but whose head names its template.
A member-chain diagnostic on main 81fd0078 found the anonymous union
behind most of protobuf's untyped chains. Kit A/B against 81fd0078:
| repo | removed | added | retargeted | same target, now typed |
|---|---|---|---|---|
| protobuf | 2 | 54 | 349 | 231 |
| fmt | 0 | 0 | 1 | 1 |
| rocksdb | 0 | 0 | 2 | 0 |
| leveldb, redis | identical | | | |
- Retargets go from bare-name guesses to the member's class:
ArenaStringPtr (Set, Get, IsDefault, Destroy), CachedSize::Get,
HasBits::Clear, RepeatedPtrField, SourceContext and MessageLite.
- The 2 removals are `ptr.string_value->clear()` on a `std::string*`, which
had reached `Map::clear`.
- fmt's and rocksdb's retargets pick the overload their argument fits now
that a union member has a type: `map(value_.uint128_value)` takes
`native_uint128`, and `Add(…, props.oldest_ancestor_time)` the `uint64_t`
overload.
- About 45 changed sites read by hand were all right.
Mutations: dropping the nested-span exclusion, the declarator check or
the head check each fail the new tests. Dropping the name check survives,
because the head check covers named types; it stays as a cheap early exit.
A matching change to the caller's-declaration check left the kit A/B
byte-identical, so it was not kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
172 lines
5.9 KiB
TypeScript
172 lines
5.9 KiB
TypeScript
/**
|
|
* The members of a C++ anonymous union are the members of the class it is
|
|
* declared in: protobuf's generated messages hold their fields in `union {
|
|
* Impl_ _impl_; };`, and fmt's `value` its argument in a union of `string`,
|
|
* `custom` and the rest. The union's lines were read as a nested class's, so
|
|
* a call through `_impl_.type_url_` or `value_.string` was guessed by the
|
|
* method's name: `this_._impl_.type_url_.Destroy()` reached `Arena::Destroy`.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } 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 lines = (...l: string[]): string => [...l, ''].join('\n');
|
|
|
|
describe('a C++ call through a member of an anonymous union', () => {
|
|
let root: string;
|
|
let cg: CodeGraph;
|
|
|
|
beforeAll(async () => {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-anonymous-union-'));
|
|
const write = (file: string, text: string): void => {
|
|
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
|
fs.writeFileSync(path.join(root, file), text);
|
|
};
|
|
// Each namesake sits where a call matched by its bare name lands on it.
|
|
write('src/arenastring.h', lines(
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'class StringNamesake {',
|
|
' public:',
|
|
' void ClearToEmpty() {}',
|
|
'};',
|
|
'class Arena {',
|
|
' public:',
|
|
' static void Destroy() {}',
|
|
'};',
|
|
'class SizeNamesake {',
|
|
' public:',
|
|
' int Get() const { return 0; }',
|
|
'};',
|
|
'namespace internal {',
|
|
'class ArenaStringPtr {',
|
|
' public:',
|
|
' void ClearToEmpty() {}',
|
|
' void Destroy() {}',
|
|
'};',
|
|
'class CachedSize {',
|
|
' public:',
|
|
' int Get() const { return 1; }',
|
|
'};',
|
|
'} // namespace internal',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
));
|
|
write('src/any.pb.h', lines(
|
|
'#include "src/arenastring.h"',
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'class Any final {',
|
|
' public:',
|
|
' void clear_type_url();',
|
|
' int GetCachedSize() const { return _impl_._cached_size_.Get(); }',
|
|
' static void SharedDtor(Any& this_);',
|
|
' private:',
|
|
' struct Impl_ {',
|
|
' ::google::protobuf::internal::ArenaStringPtr type_url_;',
|
|
' ::google::protobuf::internal::CachedSize _cached_size_;',
|
|
' };',
|
|
' union { Impl_ _impl_; };',
|
|
'};',
|
|
'inline void Any::clear_type_url() { _impl_.type_url_.ClearToEmpty(); }',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
));
|
|
write('src/any.pb.cc', lines(
|
|
'#include "src/any.pb.h"',
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'void Any::SharedDtor(Any& this_) { this_._impl_.type_url_.Destroy(); }',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
));
|
|
write('src/value.h', lines(
|
|
'class Inner {',
|
|
' public:',
|
|
' void Flush() {}',
|
|
'};',
|
|
'class Other {',
|
|
' public:',
|
|
' void Flush() {}',
|
|
'};',
|
|
'class string_value {',
|
|
' public:',
|
|
' const char* data() const { return ""; }',
|
|
'};',
|
|
'class StrNamesake {',
|
|
' public:',
|
|
' const char* data() const { return ""; }',
|
|
'};',
|
|
'class value {',
|
|
' public:',
|
|
' union {',
|
|
' int int_value;',
|
|
' string_value text;',
|
|
' };',
|
|
'};',
|
|
'struct Arg {',
|
|
' value value_;',
|
|
' const char* Data() { return value_.text.data(); }',
|
|
'};',
|
|
'struct Tagged {',
|
|
' union { Inner a; } u;',
|
|
' Other a;',
|
|
'};',
|
|
'void UseTagged(Tagged& t) { t.a.Flush(); }',
|
|
'struct Outer {',
|
|
' struct Named { Inner b; } named;',
|
|
' Other b;',
|
|
'};',
|
|
'void UseOuter(Outer& o) { o.b.Flush(); }',
|
|
// The index keeps a partial specialization as `<anonymous>` too.
|
|
'struct Box {',
|
|
' template <typename T> struct Helper { Other h; };',
|
|
' template <typename T> struct Helper<T*> {',
|
|
' Inner h;',
|
|
' };',
|
|
' Other h;',
|
|
'};',
|
|
'void UseBox(Box& b) { b.h.Flush(); }',
|
|
));
|
|
cg = await CodeGraph.init(root, { index: true });
|
|
}, 120_000);
|
|
|
|
afterAll(() => {
|
|
cg?.close();
|
|
if (root) fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
/** `calls` callees of the function or method `qualifiedName`, as `qualifiedName`. */
|
|
const calls = (qualifiedName: string): string[] => {
|
|
const caller = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')]
|
|
.find((n: Node) => n.qualifiedName === qualifiedName);
|
|
expect(caller, qualifiedName).toBeDefined();
|
|
return cg.getCallees(caller!.id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.qualifiedName).sort();
|
|
};
|
|
|
|
it("a method defined outside its class reaches the class's union member", () => {
|
|
expect(calls('google::protobuf::Any::clear_type_url')).toEqual(['google::protobuf::internal::ArenaStringPtr::ClearToEmpty']);
|
|
});
|
|
|
|
it('a method inside the class reaches a union member declared below it', () => {
|
|
expect(calls('google::protobuf::Any::GetCachedSize')).toEqual(['google::protobuf::internal::CachedSize::Get']);
|
|
});
|
|
|
|
it("a chain through an object's union member", () => {
|
|
expect(calls('google::protobuf::Any::SharedDtor')).toEqual(['google::protobuf::internal::ArenaStringPtr::Destroy']);
|
|
});
|
|
|
|
it("a union member of a class-typed member of the caller's class", () => {
|
|
expect(calls('Arg::Data')).toEqual(['string_value::data']);
|
|
});
|
|
|
|
it('a union with a declarator, a named nested struct or a specialization keeps its members to itself', () => {
|
|
expect(calls('UseTagged')).toEqual(['Other::Flush']);
|
|
expect(calls('UseOuter')).toEqual(['Other::Flush']);
|
|
expect(calls('UseBox')).toEqual(['Other::Flush']);
|
|
});
|
|
});
|