fix(cpp): an explicit specialization, like protobuf's WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>, is named after its template (#2609)

An explicit specialization of a function or member function template kept
its template arguments in its name: protobuf's `template <> bool
WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>(…)` was
named `TYPE_INT32>` and qualified `…::ReadPrimitive::TYPE_INT32>`, the
qualified id split at the `::` inside its arguments; rocksdb's `template <>
int64_t Deserialize<int64_t>(…)` was `Deserialize<int64_t>`. A call writes
the bare name, so nothing linked to them.

Both extractors (wasm and the native kernel) now name a specialization
after its template, from the template-id's own name, and take its receiver
from the text before the declarator's name. That text must end in a real
`::`: tree-sitter recovers Metal's `vertex VertexOut vertexShader(…)` with
a missing, zero-width one.

A specialization now meets its template among a name's candidates, so the
resolver chooses between them wherever it picks among C++ candidates (a
class's methods, the implicit-this walk and its bases, exact, qualified,
global, fuzzy and macro-namespace matching):
 - a call that writes template arguments reaches the specialization
   written with the same ones (spacing aside, or qualified further on one
   side), else the template itself or an ordinary overload, else nothing:
   fmt's `num_bits<unsigned short>()` stays on `num_bits<T>()` instead of
   the nearer `num_bits<uint128>()`; protobuf's `Arena::Create<T>` keeps
   calling `DefaultConstruct<T>`, not the tests' `DefaultConstruct<
   DispatcherTestProto>` hook; a dependent `ReadPrimitive<CType,
   DeclaredType>(…)` links nowhere rather than to the first specialization.
 - a call that writes none, or `<>`, reaches a specialization only when
   nothing else has the name.
 - a class's own member template, specialized but none for these
   arguments, ends the lookup there instead of falling to a base's
   namesake.
The `cpp-override` pass leaves specializations out on both sides: a member
template is never virtual.

Validated on the cgstd kit against main (f7951a13): leveldb and redis
byte-identical; fmt: 8 specializations renamed, every call target as on
main; rocksdb: the cassandra `Serialize<T>`/`Deserialize<T>` calls (25 in
format.cc, 75 in its tests, which main sent to `ColumnBase` methods) each
reach the specialization their argument names (100 of 100 agree);
protobuf: `MergeFrom<MessageLite>` (3 calls), `ReadPrimitive<int,
TYPE_ENUM>` (2) and a test's `ReadPackedPrimitive<uint32_t, TYPE_FIXED32>`
reach their specializations; 5 dependent calls like
`GetRepeatedFieldInternal<T>`, and one `MutableRepeatedFieldInternal<int>`
(the macro writes `<int32_t>`), no longer link to the first specialization
a macro defines. Kernel and wasm dumps identical (but for protobuf's known
Swift line). Full suite: 7179 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-16 02:40:57 +00:00
committed by GitHub
parent f7951a13db
commit 627018eb25
6 changed files with 724 additions and 30 deletions
+1
View File
@@ -100,6 +100,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In C++, a call on the variable of a range-based `for` loop over `auto` elements, like `cfd->Ref()` inside facebook/rocksdb's `for (auto cfd : cfds)`, now links to the method of the type the loop goes over. That type is read from what the loop iterates: a `std::vector`, `std::set`, `absl::Span` or similar container, protocolbuffers/protobuf's `RepeatedPtrField` or RocksDB's `autovector`, whether a variable, parameter or member holds it, an `auto` variable is set to it or a function or method returns it, and a project class you loop over through its `begin()`, like RocksDB's `ColumnFamilySet`. Before, the call was matched by the variable's name, or by an earlier variable of that name: in RocksDB, `blob_file.GetBlobFileNumber()` over a list of `BlobFileGarbage` reached `BlobFileAddition::GetBlobFileNumber`, and in protobuf's Objective-C generator, calls over a list of `std::unique_ptr<MessageGenerator>` reached other generators' methods. A call on an element of a standard-library type, like `file.find(…)` over a `std::vector<std::string>`, no longer links to a project method, and `->` on an element held in a `std::unique_ptr` or `std::shared_ptr` reaches the type it holds. Re-index C++ projects after upgrading.
- In C++, a call on a variable, parameter or member declared as one of your classes, like `iter->Valid()` on an `Iterator* iter` or `it.Valid()` on a `const Iterator& it`, now links to that method of the class C++ itself would find for the type where the call is written, or of a class it inherits from. Before, the method was picked by the type's last name, so a class of the same name nested elsewhere could win: in facebook/rocksdb, calls on an `Iterator` reached `MemTableRep::Iterator`, which declares `Valid` and `Next` itself while the `Iterator` the code means inherits them, google/leveldb's reached `SkipList::Iterator`, and in protocolbuffers/protobuf the Java generator's `context->options()` reached the hpb generator's `Context` and `text_parser.Parse(…)` on a `TextFormat::Parser` reached the `.proto` parser. When that class and the classes it derives from have no method of that name, the call no longer links to an unrelated class's method that shares it, as protobuf's `field->full_name()` on a `FieldDescriptor*` did. A type is also found through a `using` declaration the file writes, like protobuf's `using google::protobuf::Descriptor;`, before a class of the same name declared at global scope elsewhere in the repository; this also applies to base classes and to the type a smart pointer holds. A type written from the global scope, like `::upb::Arena& arena`, is looked up there, so a namespace's own `upb::Arena` is not taken for it, nor is an alias of that name the calling function declares. A type written as a member of a class template, like `SkipList<Key, Comparator>::Iterator iter` in leveldb's and RocksDB's skip-list tests, is now read whole, so its calls reach the skip list's own iterator rather than the general `Iterator`, and RocksDB's skip-list memtable iterator no longer links its calls on the skip list's iterator to its own methods; a standard container's `iterator` written this way is the library's, never a class of yours named `iterator`. Re-index C++ projects after upgrading.
- In C++, a method defined by a macro of the same file is now indexed where the file invokes the macro, so calls to it link. protocolbuffers/protobuf's descriptor classes only declare most of their accessors and define them further down with macros like `PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, number, int)`, and its reflection defines `GetInt32`, `SetInt32` and the like by pasting names together in a macro. Those methods had no node at all, so calls like `field->number()`, `descriptor->full_name()` or `reflection->GetInt32(…)` linked to nothing. Calls through what such a method returns, like `field->containing_type()->full_name()`, now link too. A macro defined in another file, and a method declared but defined nowhere in the repository, are still not indexed. Re-index C++ projects after upgrading.
- In C++, an explicit specialization of a function or member function template, like protocolbuffers/protobuf's `template <> bool WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>(…)` or facebook/rocksdb's `template <> int64_t Deserialize<int64_t>(…)`, is now indexed under its template's name, so search finds it and calls link to it. Before, it kept its template arguments in its name, and one with a `::` among them was named after the last part, like `TYPE_INT32>`. A call that writes template arguments, like `Deserialize<int64_t>(src, offset)`, links to the specialization written with the same ones, and otherwise to the template itself, as fmt's `num_bits<unsigned short>()` does; a call that writes none links to a specialization only when nothing else has that name. Re-index C++ projects after upgrading.
- In C++, text inside a string literal is no longer taken for a variable's declaration. That covers the C code protocolbuffers/protobuf's upb generator writes in a raw string (`R"cc( … upb_MiniTableField field = $field_init$; … )cc"`) and log messages like facebook/rocksdb's `"… in WriteBatch handler; …"`. Before, a later call like `field.ctype()` on the function's own `upb::FieldDefPtr field` parameter, or `handler->PutCF(…)` on a `WriteBatch::Handler* handler`, was matched against the type that text seemed to name, so it linked to nothing or to another class's method. Re-index C++ projects after upgrading.
- In C++, a call on an `auto` variable now links to the method of the class C++ finds for the type its initializer gives it, as a call on a variable declared with that type does. The type is read from a cast or smart pointer written out, like facebook/rocksdb's `auto iter = std::unique_ptr<Iterator>(…)` or `auto* mutex = static_cast<InstrumentedMutex*>(arg)`, from `new` or `std::make_unique` of a class, and from what a function or method is declared to return, like the `Iterator*` of `txn->GetIterator(…)`, read to the end of an initializer that goes on to the next line. Before, such a variable took the type of an earlier variable of the same name, or none: in RocksDB, `iter->SeekToFirst()` reached `MemTableRep::Iterator` and `mutex->Lock()` reached `port::Mutex`. A chained call like `cfd->current()->storage_info()` now looks up the class the inner call is declared to return the same way, through a smart pointer too, so protocolbuffers/protobuf's Java generator no longer reaches the C# generator's `EnumGenerator`. A type from outside the project no longer links to a class of the project that shares its name: protobuf's `status.ok()` on an `absl::Status` reached `upb::Status::ok`, and fmtlib/fmt's `os.flush()` on a `std::ostream&` reached fmt's own `ostream::flush`. The same holds for a variable declared in a macro body, or typed by what a function in another file returns, and for a class the index doesn't hold whose name only another class shares: protobuf's `_status.ok()` in its `RETURN_IF_ERROR` macro reached `upb::Status::ok`, and `range.options()` on its generated `DescriptorProto::ExtensionRange` reached `Descriptor::ExtensionRange::options`. A namespace a macro stands for is the project's under either name: facebook/rocksdb declares its classes in `namespace ROCKSDB_NAMESPACE` with `#define ROCKSDB_NAMESPACE rocksdb`, so `db->Put(…)` on a `rocksdb::DB*` links to RocksDB's own `DB::Put`. Re-index C++ projects after upgrading.
- In C++, a call on a data member of the calling method's class, like `versions_->GetColumnFamilySet()` in facebook/rocksdb's `db_impl_write.cc`, now links to the method of the member's type wherever the class is declared. Before, only the call's own file and the header of the same name were read for the member, and RocksDB declares `DBImpl` in `db_impl.h` while its methods are spread over `db_impl_write.cc`, `db_impl_compaction_flush.cc`, `db_impl_open.cc` and more, so such calls were guessed by the member's name. A member declared below an inline method that uses it, one a base class declares, and, in a gtest `TEST_F` or `TEST_P` body, one of the test's fixture, like RocksDB's `db_`, are found the same way, and a member of a class nested in the caller's class is no longer taken for the caller's own. A call on a member of a standard-library type, like `std::atomic<bool> shutting_down_`, no longer links to a project method that only shares the method's name. Re-index C++ projects after upgrading.
+465
View File
@@ -0,0 +1,465 @@
/**
* A C++ explicit specialization of a function or member function template is
* named after its template, without its template arguments:
*
* template <> bool WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>(…) { … }
* template <> int WireFormatLite::get<int>() const { … }
* template <> void Swap<Foo>(Foo& a, Foo& b) { … }
*
* The first was named `TYPE_INT32>` and qualified
* `…::WireFormatLite::ReadPrimitive::TYPE_INT32>`: the qualified id was split
* at every `::`, the one inside its template arguments too. The others were
* named `get<int>` and `Swap<Foo>`. A call writes the bare name, so none
* linked to them. Extraction runs against the native kernel (when built)
* and the wasm extractor, which must agree.
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import CodeGraph from '../src/index';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel';
import type { ExtractionResult, Node } from '../src/types';
const KERNEL_PATH = path.join(
__dirname,
'..',
'codegraph-kernel',
'prebuilds',
`${process.platform}-${process.arch}`,
'codegraph-kernel.node'
);
const kernelAvailable = fs.existsSync(KERNEL_PATH) || process.env.CODEGRAPH_KERNEL_EXPECT === '1';
const lines = (...l: string[]): string => [...l, ''].join('\n');
/** protobuf's wire_format_lite.h, in small, with a free function template and a class template. */
const WIRE_H = lines(
'namespace pb {',
'class WireFormatLite {',
' public:',
' enum FieldType { TYPE_INT32 = 1, TYPE_INT64 = 2 };',
' template <typename CType, enum FieldType DeclaredType>',
' static bool ReadPrimitive(int* input, CType* value);',
' template <typename T> T get() const;',
' bool operator<(const WireFormatLite& other) const;',
'};',
'',
'template <>',
'inline bool WireFormatLite::ReadPrimitive<int, WireFormatLite::TYPE_INT32>(int* input, int* value) {',
' return true;',
'}',
'template <>',
'inline bool WireFormatLite::ReadPrimitive<long, WireFormatLite::TYPE_INT64>(int* input, long* value) {',
' return false;',
'}',
'template <>',
'inline int WireFormatLite::get<int>() const { return 1; }',
'inline bool WireFormatLite::operator<(const WireFormatLite& other) const { return false; }',
'',
'template <typename T> void Swap(T& a, T& b) {}',
'template <> void Swap<WireFormatLite>(WireFormatLite& a, WireFormatLite& b) {}',
'template <typename T> T* Make() { return nullptr; }',
'template <> WireFormatLite* Make<WireFormatLite>() { return nullptr; }',
'',
'template <typename T> struct Box { T get() const; };',
'template <> int Box<int>::get() const { return 0; }',
'template <typename T> T Box<T>::get() const { return T(); }',
'} // namespace pb',
);
const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const;
/** `<kind> <name> <qualified name> <line>` of every function and method, sorted. */
function callables(result: ExtractionResult): string[] {
return result.nodes
.filter((n) => n.kind === 'method' || n.kind === 'function')
.map((n) => `${n.kind} ${n.name} ${n.qualifiedName} ${n.startLine}`)
.sort();
}
describe('a C++ explicit specialization is named after its template', () => {
let savedEnv: Record<string, string | undefined> = {};
beforeAll(async () => {
await initGrammars();
await loadGrammarsForLanguages(['cpp']);
});
beforeEach(() => {
savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
resetKernelForTests();
});
afterEach(() => {
for (const k of ENV_KEYS) {
if (savedEnv[k] === undefined) delete process.env[k];
else process.env[k] = savedEnv[k];
}
resetKernelForTests();
});
function extract(backend: 'kernel' | 'wasm', source: string): ExtractionResult {
if (backend === 'wasm') {
process.env.CODEGRAPH_KERNEL = '0';
return extractFromSource('src/wire.h', source, 'cpp');
}
delete process.env.CODEGRAPH_KERNEL;
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
const result = tryKernelExtract('src/wire.h', source, 'cpp');
expect(result, 'kernel extraction of src/wire.h').not.toBeNull();
return result!;
}
const backends = kernelAvailable ? (['kernel', 'wasm'] as const) : (['wasm'] as const);
for (const crlf of [false, true]) {
const source = crlf ? WIRE_H.replace(/\n/g, '\r\n') : WIRE_H;
const label = crlf ? ' (CRLF)' : '';
it.each(backends)(`member and function specializations, beside what was already right: %s${label}`, (backend) => {
const result = extract(backend, source);
expect(callables(result)).toEqual([
'function Make pb::Make 25',
'function Make pb::Make 26',
'function Swap pb::Swap 23',
'function Swap pb::Swap 24',
'method get pb::Box::get 29',
'method get pb::Box::get 30',
'method get pb::WireFormatLite::get 20',
'method operator< pb::WireFormatLite::operator< 21',
'method ReadPrimitive pb::WireFormatLite::ReadPrimitive 12',
'method ReadPrimitive pb::WireFormatLite::ReadPrimitive 16',
].sort());
// The class the receiver names holds them, as it holds any out-of-line definition.
const byId = new Map(result.nodes.map((n) => [n.id, n]));
const held = result.edges
.filter((e) => e.kind === 'contains' && byId.get(e.source)?.name === 'WireFormatLite')
.map((e) => `${byId.get(e.target)?.name} ${byId.get(e.target)?.startLine}`)
.sort();
expect(held).toEqual(expect.arrayContaining(['ReadPrimitive 12', 'ReadPrimitive 16', 'get 20']));
});
}
});
/**
* A specialization now shares its template's name, so a call meets both
* among that name's candidates. The template arguments a call writes pick
* between them: the specialization written with the same ones, else the
* template itself or an ordinary overload, else nothing. A call that writes
* none reaches a specialization only when there is nothing else.
*/
describe('a C++ call reaches the specialization its template arguments name', () => {
let root: string;
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-specialization-calls-'));
const write = (file: string, text: string): void => fs.writeFileSync(path.join(root, 'src', file), text);
fs.mkdirSync(path.join(root, 'src'));
write('wire.h', WIRE_H);
write('use.cc', lines(
'#include "src/wire.h"',
'namespace pb {',
'bool Read(int* in, int* v) { return WireFormatLite::ReadPrimitive<int, WireFormatLite::TYPE_INT32>(in, v); }',
'bool ReadLong(int* in, long* v) { return WireFormatLite::ReadPrimitive<long, WireFormatLite::TYPE_INT64>(in, v); }',
'bool ReadLongQualified(int* in, long* v) { return WireFormatLite::ReadPrimitive<long, pb::WireFormatLite::TYPE_INT64>(in, v); }',
'int Get(const WireFormatLite& w) { return w.get<int>(); }',
'void SwapWire(WireFormatLite& a, WireFormatLite& b) { Swap<WireFormatLite>(a, b); }',
'void SwapInts(int& x, int& y) { Swap<int>(x, y); }',
'void SwapDeduced(int& x, int& y) { Swap(x, y); }',
'int* MakeInt() { return Make<int>(); }',
'WireFormatLite* MakeWire() { return Make<WireFormatLite>(); }',
'} // namespace pb',
));
// Inside the class: a call written with no receiver, its arguments
// unqualified; one in a template, whose arguments name its parameters.
write('reader.h', lines(
'#include "src/wire.h"',
'namespace pb {',
'class Reader : public WireFormatLite {',
' public:',
' bool ReadLong(int* in) { return ReadPrimitive<long, TYPE_INT64>(in, nullptr); }',
' template <typename C, FieldType D> bool ReadAny(int* in, C* v) { return ReadPrimitive<C, D>(in, v); }',
'};',
'} // namespace pb',
));
// rocksdb's cassandra serialize.h: the template is declared, its specializations defined.
write('serialize.h', lines(
'namespace ns {',
'template <typename T> void Serialize(T val, char* dest);',
'template <> inline void Serialize<char>(char t, char* dest) {}',
'template <> inline void Serialize<long>(long t, char* dest) {}',
'template <> inline void Serialize<const char*>(const char* t, char* dest) {}',
'inline void WriteLocal(char* dest) { ns::Serialize<long>(1, dest); }',
'} // namespace ns',
));
write('format.cc', lines(
'#include "src/serialize.h"',
'namespace ns {',
'void Write(char* dest) { ns::Serialize<long>(1, dest); }',
'void WriteShort(char* dest) { ns::Serialize<short>(1, dest); }',
"void WriteDeduced(char* dest) { ns::Serialize('c', dest); }",
"void WriteBrackets(char* dest) { ns::Serialize<>('c', dest); }",
'void WriteText(char* dest) { ns::Serialize<const char *>("x", dest); }',
'void WriteGlobal(char* dest) { ::ns::Serialize<long>(1, dest); }',
'} // namespace ns',
));
// Only a specialization, of a template declared without a body.
write('parse.h', lines(
'namespace fmt {',
'template <typename T> T parse(const char* text);',
'template <> int parse<int>(const char* text) { return 0; }',
'inline long parse_long(const char* t) { return parse<long>(t); }',
'} // namespace fmt',
));
// fmt's namespace, opened by a macro.
write('digits.h', lines(
'#define MY_BEGIN_NAMESPACE namespace mylib {',
'#define MY_END_NAMESPACE }',
'MY_BEGIN_NAMESPACE',
'template <typename T> constexpr int digits() { return 1; }',
'template <> constexpr int digits<long>() { return 19; }',
'MY_END_NAMESPACE',
));
write('digits.cc', lines(
'#include "src/digits.h"',
'int long_digits() { return mylib::digits<long>(); }',
'int char_digits() { return mylib::digits<char>(); }',
));
// gmock's converting constructor, which names its class template in its parameter.
write('action.h', lines(
'namespace gm {',
'template <typename F> class ActionInterface {};',
'template <typename F> class Action {',
' public:',
' explicit Action(ActionInterface<F>* impl) {}',
' template <typename Func> explicit Action(const Action<Func>& action) {}',
'};',
'template <typename F> Action<F> Convert(const Action<long>& from) { return Action<F>(from); }',
'} // namespace gm',
));
// A specialization whose return type begins with its name.
write('get.h', lines(
'namespace kv {',
'struct GetResult {};',
'template <typename T> GetResult Get(int key) { return GetResult(); }',
'template <> GetResult Get<long>(int key) { return GetResult(); }',
'inline GetResult GetInt() { return Get<int>(1); }',
'} // namespace kv',
));
// protobuf's arena.h, whose tests specialize `DefaultConstruct` to hook
// it — with a parameter the call's argument fits as well as the template's.
write('arena.h', lines(
'namespace pb {',
'class Arena;',
'typedef Arena* ArenaPtr;',
'class Arena {',
' public:',
' template <typename T> static void* DefaultConstruct(ArenaPtr arena);',
' template <typename T> static T* Create(Arena* arena) { return static_cast<T*>(DefaultConstruct<T>(arena)); }',
'};',
'template <typename T> void* Arena::DefaultConstruct(ArenaPtr arena) { return nullptr; }',
'} // namespace pb',
));
// A specialization whose parameter is written through a typedef: the
// call's argument fits the template's parameter better.
write('registry.h', lines(
'namespace pb {',
'class Handle;',
'typedef Handle* HandlePtr;',
'class Registry {',
' public:',
' template <typename T> void Add(Handle* h) {}',
'};',
'template <> inline void Registry::Add<int>(HandlePtr h) {}',
'class Pool : public Registry {',
' public:',
' void AddInt(Handle* h) { Add<int>(h); }',
'};',
'inline void Register(Registry& r, Handle* h) { r.Add<int>(h); }',
'} // namespace pb',
));
// fmt's format.h: a call beside the specializations, nearer them than the template.
write('bits.h', lines(
'namespace fmt {',
'template <typename T> constexpr int num_bits() { return 8; }',
'template <> constexpr int num_bits<long>() { return 64; }',
'template <> constexpr int num_bits<short>() { return 16; }',
'inline int char_bits() { return num_bits<char>(); }',
'inline int long_bits() { return num_bits<long>(); }',
'template <typename T> int width(T value) { return 1; }',
'template <> int width<long>(long value) { return 8; }',
'inline int int_width(int v) { return width(v); }',
'inline int named_long_bits() { const char* unit = "größe"; return num_bits<long>(); }',
'template <typename T, bool zigzag = false> T decode(T v) { return v; }',
'template <> int decode<int, true>(int v) { return -v; }',
'inline int plain(int v) { return decode<int>(v); }',
'template <typename T> int scale(T v);',
'template <> int scale<long>(long v) { return 2; } template <typename T> int scale(T v) { return 1; }',
'inline int int_scale() { return scale<int>(1); }',
'template <> long decode<long>(long v) { return 2 * v; }',
'inline long zigzag(long v) { return decode<long, true>(v); }',
'} // namespace fmt',
));
// A member template's specialization, and one called through another call.
write('codec.h', lines(
'#include "src/wire.h"',
'namespace pb {',
'class Codec {',
' public:',
' template <typename T> void Put(T v) {}',
'};',
'class IntCodec : public Codec {',
' public:',
' template <typename T> void Put(T v);',
' WireFormatLite& target() { return w_; }',
' WireFormatLite w_;',
'};',
'template <> inline void IntCodec::Put<int>(int v) {}',
'inline void PutInt(IntCodec& c) { c.Put<int>(1); }',
'inline void PutLong(IntCodec& c) { c.Put<long>(1L); }',
'inline long LongFromCodec(IntCodec& codec) { return codec.target().get<long>(); }',
'} // namespace pb',
));
// protobuf's generated_message_reflection.cc: a macro defines the specializations.
write('reflection.h', lines(
'namespace pb {',
'class Reflection {',
' public:',
' template <typename T> const T& GetField(int index) const;',
'};',
'template <typename T> const T& Reflection::GetField(int index) const { static T t; return t; }',
'#define DEFINE_FIELD_GETTER(TYPE) \\',
' template <> const TYPE& Reflection::GetField<TYPE>(int index) const { static TYPE t; return t; }',
'DEFINE_FIELD_GETTER(int)',
'DEFINE_FIELD_GETTER(long)',
'inline long GetLong(const Reflection& r) { return r.GetField<long>(0); }',
'inline double GetDouble(const Reflection& r) { return r.GetField<double>(0); }',
'} // namespace pb',
));
write('arena_test.cc', lines(
'#include "src/arena.h"',
'namespace pb {',
'struct Hooked {};',
'template <> void* Arena::DefaultConstruct<Hooked>(Arena* arena) { return nullptr; }',
'void* MakeHooked(Arena* a) { return Arena::DefaultConstruct<Hooked>(a); }',
'} // namespace pb',
));
// A base class's member template, specialized, beside a derived class's namesake.
write('repeated.h', lines(
'namespace pb {',
'class FieldBase {',
' public:',
' template <typename T> void MergeFrom(const FieldBase& from) {}',
' virtual void Visit(int value) {}',
'};',
'template <> inline void FieldBase::MergeFrom<int>(const FieldBase& from) {}',
'class Field : public FieldBase {',
' public:',
' void MergeFrom(const Field& other) {}',
' template <typename T> void Visit(T value) {}',
'};',
'template <> inline void Field::Visit<int>(int value) {}',
'} // namespace pb',
));
cg = await CodeGraph.init(root, { index: true });
}, 120_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/** `<qualified name>@<line>` of what the function or method `name` (at `line`, if given) calls. */
const calls = (name: string, line?: number): string[] => {
const caller = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')]
.find((n: Node) => n.qualifiedName === name && (line === undefined || n.startLine === line))!;
expect(caller, name).toBeDefined();
return cg.getCallees(caller.id).filter((r) => r.edge.kind === 'calls').map((r) => `${r.node.qualifiedName}@${r.node.startLine}`);
};
it('the specialization the call writes the arguments of', () => {
expect(calls('pb::Read')).toEqual(['pb::WireFormatLite::ReadPrimitive@12']);
expect(calls('pb::ReadLong')).toEqual(['pb::WireFormatLite::ReadPrimitive@16']);
expect(calls('pb::Get')).toEqual(['pb::WireFormatLite::get@20']);
expect(calls('pb::SwapWire')).toEqual(['pb::Swap@24']);
expect(calls('pb::MakeWire')).toEqual(['pb::Make@26']);
expect(calls('ns::Write')).toEqual(['ns::Serialize@4']);
expect(calls('pb::MakeHooked')).toEqual(['pb::Arena::DefaultConstruct@4']);
expect(calls('fmt::long_bits')).toEqual(['fmt::num_bits@3']);
expect(calls('pb::Register')).toEqual(['pb::Registry::Add@8']);
expect(calls('pb::Pool::AddInt')).toEqual(['pb::Registry::Add@8']);
expect(calls('pb::PutInt')).toEqual(['pb::IntCodec::Put@13']);
// The call's column counts the bytes of the wider characters before it.
expect(calls('fmt::named_long_bits')).toEqual(['fmt::num_bits@3']);
// An argument qualified further than the specialization writes it, or spaced otherwise.
expect(calls('pb::ReadLongQualified')).toEqual(['pb::WireFormatLite::ReadPrimitive@16']);
expect(calls('ns::WriteText')).toEqual(['ns::Serialize@5']);
// From the global scope, and beside the specializations.
expect(calls('ns::WriteGlobal')).toEqual(['ns::Serialize@4']);
expect(calls('ns::WriteLocal')).toEqual(['ns::Serialize@4']);
// In a namespace a macro opens.
expect(calls('long_digits')).toEqual(['digits@5']);
});
it('the specializations a macro defines', () => {
expect(calls('pb::GetLong')).toEqual(['pb::Reflection::GetField@10']);
expect(calls('pb::GetDouble')).toEqual(['pb::Reflection::GetField@6']);
});
it('a specialization when nothing else is there', () => {
expect(calls('ns::WriteDeduced')).toEqual(['ns::Serialize@3']);
expect(calls('ns::WriteBrackets')).toEqual(['ns::Serialize@3']);
});
it('the template itself for arguments no specialization writes', () => {
expect(calls('pb::SwapInts')).toEqual(['pb::Swap@23']);
expect(calls('pb::MakeInt')).toEqual(['pb::Make@25']);
expect(calls('fmt::char_bits')).toEqual(['fmt::num_bits@2']);
// protobuf's `Arena::Create<T>` calls `DefaultConstruct<T>`, not the tests' hook.
expect(calls('pb::Arena::Create')).toEqual(['pb::Arena::DefaultConstruct@9']);
// `decode<int>` leaves `zigzag` at its default; `decode<long>` is `decode<long, false>`.
expect(calls('fmt::plain')).toEqual(['fmt::decode@11']);
expect(calls('fmt::zigzag')).toEqual(['fmt::decode@11']);
// The template defined after its specialization on one line.
expect(calls('fmt::int_scale')).toEqual(['fmt::scale@15']);
expect(calls('char_digits')).toEqual(['digits@4']);
expect(calls('kv::GetInt')).toEqual(['kv::Get@3']);
});
it('a constructor is no specialization', () => {
expect(calls('gm::Convert')).toEqual(['gm::Action::Action@6']);
});
it('the template itself for a call that writes no arguments', () => {
expect(calls('pb::SwapDeduced')).toEqual(['pb::Swap@23']);
expect(calls('fmt::int_width')).toEqual(['fmt::width@7']);
});
it('a call inside the class, its arguments written unqualified', () => {
expect(calls('pb::Reader::ReadLong')).toEqual(['pb::WireFormatLite::ReadPrimitive@16']);
});
it('nothing when only specializations are there and none is the one called', () => {
expect(calls('pb::Reader::ReadAny')).toEqual([]);
expect(calls('ns::WriteShort')).toEqual([]);
// `IntCodec::Put` hides `Codec::Put`.
expect(calls('pb::PutLong')).toEqual([]);
expect(calls('pb::LongFromCodec')).toEqual(['pb::IntCodec::target@10']);
expect(calls('fmt::parse_long')).toEqual([]);
});
it("a derived class's namesake overrides the template, not its specialization", () => {
const overriders = (name: string, line: number): string[] => {
const method = cg.getNodesByKind('method').find((n: Node) => n.qualifiedName === name && n.startLine === line)!;
expect(method, `${name}@${line}`).toBeDefined();
return cg.getCallers(method.id).filter((r) => r.edge.kind === 'calls').map((r) => `${r.node.qualifiedName}@${r.node.startLine}`);
};
expect(overriders('pb::Field::MergeFrom', 10)).toEqual(['pb::FieldBase::MergeFrom@4']);
expect(overriders('pb::Field::Visit', 11)).toEqual(['pb::FieldBase::Visit@5']);
expect(overriders('pb::Field::Visit', 13)).toEqual([]);
});
});
+69 -6
View File
@@ -659,6 +659,9 @@ impl<'t> Walker<'t> {
if let Some(mut parts) = self.cpp_written_class_parts(node) {
return parts.pop().unwrap_or_default();
}
if let Some(name) = self.cpp_specialized_function_name(node) {
return name;
}
}
if let Some(name_node) = node.child_by_field_name("declarator") {
let mut resolved = name_node;
@@ -764,18 +767,52 @@ impl<'t> Walker<'t> {
Some(self.text(identifier).to_string())
}
/// extractCppQualifiedMethodName (languages/c-cpp.ts:75).
/// extractCppQualifiedMethodName (languages/c-cpp.ts): the name the
/// declarator's qualified id ends in, an explicit specialization's without
/// its template arguments (cppDefinedName).
fn extract_cpp_qualified_method_name(&self, node: Node) -> Option<String> {
if let Some(n) = self.recover_cpp_macro_defined_name(node) {
return Some(n);
}
let declarator = node.child_by_field_name("declarator")?;
let qid = find_declarator_qualified_id(declarator)?;
if let Some(name) = cpp_qualified_id_name(qid) {
return Some(self.cpp_defined_name(name));
}
let text = self.text(qid).trim();
let parts: Vec<&str> = text.split("::").filter(|p| !p.is_empty()).collect();
parts.last().map(|s| s.to_string())
}
/// cppDefinedName (languages/c-cpp.ts): a definition's name node as
/// named, a `template_function`'s without its template arguments
/// (`ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>` is `ReadPrimitive`).
fn cpp_defined_name(&self, name: Node) -> String {
let own = if name.kind() == "template_function" { name.child_by_field_name("name") } else { None };
self.text(own.unwrap_or(name)).trim().to_string()
}
/// cppSpecializedFunctionName (languages/c-cpp.ts): the name of a
/// function an explicit specialization defines, without its template
/// arguments (`Swap` for `template <> void Swap<Foo>(Foo& a, Foo& b)`).
fn cpp_specialized_function_name(&self, node: Node) -> Option<String> {
if node.kind() != "function_definition" {
return None;
}
let mut declarator = node.child_by_field_name("declarator")?;
while matches!(declarator.kind(), "pointer_declarator" | "reference_declarator") {
declarator = declarator.child_by_field_name("declarator").or_else(|| declarator.named_child(0))?;
}
if declarator.kind() != "function_declarator" {
return None;
}
let inner = declarator.child_by_field_name("declarator")?;
if inner.kind() != "template_function" {
return None;
}
Some(self.cpp_defined_name(inner))
}
/// recoverSingleArgMacroDefinedName: only a local macro definition proves
/// that its argument is the function name, in either parser shape (#1373).
fn recover_single_arg_macro_defined_name(&self, node: Node) -> Option<String> {
@@ -899,16 +936,32 @@ impl<'t> Walker<'t> {
Some(name.to_string())
}
/// extractCppReceiverType (languages/c-cpp.ts:86).
/// extractCppReceiverType (languages/c-cpp.ts): the scopes are what the
/// qualified id writes before its name, so a specialization's arguments
/// (`ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>`) are no scope.
fn receiver_type_of(&self, node: Node) -> Option<String> {
let declarator = node.child_by_field_name("declarator")?;
let qid = find_declarator_qualified_id(declarator)?;
let text = self.text(qid).trim();
let parts: Vec<&str> = text.split("::").filter(|p| !p.is_empty()).collect();
if parts.len() <= 1 {
let scopes = match cpp_qualified_id_name(qid) {
Some(name) => {
let written = self.src[qid.start_byte()..name.start_byte()].trim_end();
// A `::` the parser supplied in recovery writes nothing: Metal's
// `vertex VertexOut vertexShader(…)` reads as `VertexOut::vertexShader`.
if !written.ends_with("::") {
return None;
}
written.to_string()
}
None => {
let parts: Vec<&str> = self.text(qid).trim().split("::").filter(|p| !p.is_empty()).collect();
parts[..parts.len().saturating_sub(1)].join("::")
}
};
let parts: Vec<&str> = scopes.split("::").filter(|p| !p.is_empty()).collect();
if parts.is_empty() {
return None;
}
let receiver = strip_cpp_template_args(&parts[..parts.len() - 1].join("::"));
let receiver = strip_cpp_template_args(&parts.join("::"));
if receiver.is_empty() {
None
} else {
@@ -2391,6 +2444,16 @@ impl<'t> Walker<'t> {
// --- free helpers ------------------------------------------------------------
/// cppQualifiedIdName (languages/c-cpp.ts): the name a `qualified_identifier`
/// ends in, past its scopes. None when the tree lacks one.
fn cpp_qualified_id_name(qid: Node) -> Option<Node> {
let mut name = qid;
while name.kind() == "qualified_identifier" {
name = name.child_by_field_name("name")?;
}
Some(name)
}
/// findDeclaratorQualifiedId (languages/c-cpp.ts:13): BFS for the declarator's
/// `qualified_identifier`, skipping parameter_list + trailing_return_type so a
/// qualified PARAMETER type can't be mistaken for the method name.
+57 -4
View File
@@ -217,6 +217,29 @@ function cppWrittenClassParts(node: SyntaxNode, source: string): string[] | unde
return parts.length > 0 && parts.every((p) => /^[A-Za-z_]\w*$/.test(p)) ? parts : undefined;
}
/**
* The name a C++ `qualified_identifier` ends in, past its scopes: the
* `template_function` `ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>` for
* `WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>`. Null
* when the tree lacks one.
*/
function cppQualifiedIdName(qid: SyntaxNode): SyntaxNode | null {
let name: SyntaxNode | null = qid;
while (name?.type === 'qualified_identifier') name = getChildByField(name, 'name');
return name;
}
/**
* A C++ definition's name node, as named: an explicit specialization's
* (`template_function`) without its template arguments, so
* `ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>` is `ReadPrimitive`,
* the name every call to it writes. Mirrored in the kernel (ccpp/mod.rs).
*/
function cppDefinedName(name: SyntaxNode, source: string): string {
const own = name.type === 'template_function' ? getChildByField(name, 'name') : null;
return getNodeText(own ?? name, source).trim();
}
function extractCppQualifiedMethodName(node: SyntaxNode, source: string): string | undefined {
const macroDefined = recoverSingleArgMacroDefinedName(node, source) ?? recoverCppMacroDefinedName(node, source);
if (macroDefined) return macroDefined;
@@ -224,24 +247,53 @@ function extractCppQualifiedMethodName(node: SyntaxNode, source: string): string
if (!declarator) return undefined;
const qid = findDeclaratorQualifiedId(declarator);
if (!qid) return undefined;
const name = cppQualifiedIdName(qid);
if (name) return cppDefinedName(name, source);
const parts = getNodeText(qid, source).trim().split('::').filter(Boolean);
return parts[parts.length - 1];
}
/**
* The name of a C++ function an explicit specialization defines, without its
* template arguments: `Swap` for `template <> void Swap<Foo>(Foo& a, Foo& b)`.
* Undefined for any other definition. Mirrored in the kernel (ccpp/mod.rs).
*/
function cppSpecializedFunctionName(node: SyntaxNode, source: string): string | undefined {
if (node.type !== 'function_definition') return undefined;
let declarator = getChildByField(node, 'declarator');
while (declarator && (declarator.type === 'pointer_declarator' || declarator.type === 'reference_declarator')) {
declarator = getChildByField(declarator, 'declarator') || declarator.namedChild(0);
}
if (declarator?.type !== 'function_declarator') return undefined;
const inner = getChildByField(declarator, 'declarator');
return inner?.type === 'template_function' ? cppDefinedName(inner, source) : undefined;
}
function extractCppReceiverType(node: SyntaxNode, source: string): string | undefined {
const declarator = getChildByField(node, 'declarator');
if (!declarator) return undefined;
const qid = findDeclaratorQualifiedId(declarator);
if (!qid) return undefined;
const parts = getNodeText(qid, source).trim().split('::').filter(Boolean);
if (parts.length <= 1) return undefined;
// The scopes are what the qualified id writes before its name: a
// specialization's arguments (`ReadPrimitive<int32_t,
// WireFormatLite::TYPE_INT32>`) are no scope, though they may write `::`.
const name = cppQualifiedIdName(qid);
let scopes = getNodeText(qid, source).trim().split('::').filter(Boolean).slice(0, -1).join('::');
if (name) {
scopes = source.slice(qid.startIndex, name.startIndex).trimEnd();
// A `::` the parser supplied in recovery writes nothing: Metal's `vertex
// VertexOut vertexShader(…)` reads as `VertexOut::vertexShader`.
if (!scopes.endsWith('::')) return undefined;
}
const parts = scopes.split('::').filter(Boolean);
if (parts.length === 0) return undefined;
// An out-of-line template method definition carries the class's template
// parameter list in the qualifier (`template<typename T> T Box<T>::get()`),
// but the class node is indexed as bare `Box` — strip `<…>` so the receiver
// matches it, the same normalization #1043 applies to base-class refs.
// Multi-line parameter lists otherwise leak whole `<…>` blocks (newlines
// included) into qualified_name, which can exceed NAME_MAX (#1286).
const receiver = stripCppTemplateArgs(parts.slice(0, -1).join('::'));
const receiver = stripCppTemplateArgs(parts.join('::'));
return receiver || undefined;
}
@@ -2425,7 +2477,8 @@ export const cppExtractor: LanguageExtractor = {
bodyField: 'body',
paramsField: 'parameters',
resolveName: (node, source) =>
extractCppQualifiedMethodName(node, source) ?? unnamedTypeVariableName(node, source) ?? cppWrittenClassParts(node, source)?.at(-1),
extractCppQualifiedMethodName(node, source) ?? unnamedTypeVariableName(node, source) ?? cppWrittenClassParts(node, source)?.at(-1) ??
cppSpecializedFunctionName(node, source),
getDeclarationWrapper: (node) => definingDeclaration(node) ?? undefined,
getReceiverType: extractCppReceiverType,
getClassScope: (node, source) => cppWrittenClassParts(node, source)?.slice(0, -1).join('::') || undefined,
+8 -5
View File
@@ -39,7 +39,7 @@ import { createYielder, type MaybeYield } from './cooperative-yield';
import { crossTierEdges, hasCrossTierPattern, hasTestRequestPattern, testRequestEdges } from './tier-synthesizer';
import { enclosingFn, makeLineAt } from './synth-utils';
import { resolveImportPath } from './import-resolver';
import { crossesCodeBoundary, jsCodeBindsName } from './name-matcher';
import { cppSpecializationArguments, crossesCodeBoundary, jsCodeBindsName } from './name-matcher';
import { declaredComponent, exportedComponent, lazyValueModules } from './frameworks/react';
import { jsxCode } from './jsx-code';
@@ -747,8 +747,10 @@ async function arkuiRouterEdges(ctx: ResolutionContext, onYield: MaybeYield): Pr
* (the override), so trace/callees from the interface method reach the
* implementation(s). Over-approximation accepted (reachability-correct); capped
* per class and gated to C++ to avoid touching other languages' dispatch.
* An explicit specialization of a member template, which shares the
* template's name, overrides nothing and is overridden by nothing.
*/
async function cppOverrideEdges(queries: QueryBuilder, onYield: MaybeYield): Promise<Edge[]> {
async function cppOverrideEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
let scanned255 = 0;
const edges: Edge[] = [];
const seen = new Set<string>();
@@ -757,6 +759,7 @@ async function cppOverrideEdges(queries: QueryBuilder, onYield: MaybeYield): Pro
.getOutgoingEdges(classId, ['contains'])
.map((e) => queries.getNodeById(e.target))
.filter((n): n is Node => !!n && n.kind === 'method');
const overridable = (m: Node): boolean => cppSpecializationArguments(m, ctx) === null;
for (const cls of queries.iterateNodesByKind('class')) {
if ((++scanned255 & 63) === 0) await onYield();
const subMethods = methodsOf(cls.id).filter((n) => n.language === 'cpp');
@@ -764,12 +767,12 @@ async function cppOverrideEdges(queries: QueryBuilder, onYield: MaybeYield): Pro
for (const ext of queries.getOutgoingEdges(cls.id, ['extends'])) {
const base = queries.getNodeById(ext.target);
if (!base || base.language !== 'cpp' || base.id === cls.id) continue;
const baseMethods = new Map(methodsOf(base.id).map((m) => [m.name, m]));
const baseMethods = new Map(methodsOf(base.id).filter(overridable).map((m) => [m.name, m]));
let added = 0;
for (const m of subMethods) {
if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
const bm = baseMethods.get(m.name);
if (!bm || bm.id === m.id) continue;
if (!bm || bm.id === m.id || !overridable(m)) continue;
const key = `${bm.id}>${m.id}`;
if (seen.has(key)) continue;
seen.add(key);
@@ -4539,7 +4542,7 @@ export const SYNTH_PASSES: SynthPassDef[] = [
{ name: 'arkuiStateEdges', gate: (has) => has('arkts'), run: (q, c, y) => arkuiStateBuildEdges(q, c, y) },
{ name: 'arkuiEmitter', gate: (has) => has('arkts'), run: (_q, c, y) => arkuiEmitterEdges(c, y) },
{ name: 'arkuiRoutes', gate: (has) => has('arkts'), run: (_q, c, y) => arkuiRouterEdges(c, y) },
{ name: 'cppEdges', gate: (has) => has('cpp'), run: (q, _c, y) => cppOverrideEdges(q, y) },
{ name: 'cppEdges', gate: (has) => has('cpp'), run: (q, c, y) => cppOverrideEdges(q, c, y) },
{
name: 'ifaceEdges',
gate: (has) => has('java', 'kotlin', 'csharp', 'swift', 'scala', 'go', 'rust', 'arkts', ...JS_FAMILY),
+124 -15
View File
@@ -7961,7 +7961,7 @@ export function matchByExactName(
const candidates = dartBare ? nearestDartMembers(preferOwnDartLibrary(filtered, ref, context), ref, context)
: dartTypeRef && dartPrefix === null ? preferOwnDartLibrary(filtered, ref, context)
: swiftShape && swiftShape.shape !== 'chained' ? nearestSwiftMembers(filtered, ref, context)
: kotlinBare ? lexicalKotlinMembers(filtered, ref, context) : filtered;
: kotlinBare ? lexicalKotlinMembers(filtered, ref, context) : cppSpecializationPool(filtered, ref, context);
if (candidates.length === 0) {
return null;
@@ -8038,9 +8038,9 @@ export function matchByQualifiedName(
// inside a namespace a macro opens, which only looks global in the index
// (fmt's `struct pipe`); step 5 of matchReference reads those.
if ((ref.language === 'cpp' || ref.language === 'c') && ref.referenceName.startsWith('::')) {
const global = keepForRef(context.getNodesByQualifiedName(ref.referenceName.slice(2))).filter((n) =>
const global = cppSpecializationPool(keepForRef(context.getNodesByQualifiedName(ref.referenceName.slice(2))).filter((n) =>
(n.language === 'cpp' || n.language === 'c') &&
!cppMacroNamespaceFrames(n.filePath, context).some((f) => f.start <= n.startLine && f.end >= n.startLine));
!cppMacroNamespaceFrames(n.filePath, context).some((f) => f.start <= n.startLine && f.end >= n.startLine)), ref, context);
const chosen = preferCallSiteFile(global, ref.filePath)[0];
if (!chosen) return null;
return {
@@ -8051,7 +8051,7 @@ export function matchByQualifiedName(
};
}
let candidates = keepForRef(context.getNodesByQualifiedName(ref.referenceName));
let candidates = cppSpecializationPool(keepForRef(context.getNodesByQualifiedName(ref.referenceName)), ref, context);
// A C# `using X.Y;` names a namespace: one the project declares, else it is
// the file's own (external) using — never another file's using of that name.
// That own using ends the lookup here, and resolveOne drops it (an import
@@ -8121,8 +8121,8 @@ export function matchByQualifiedName(
const parts = ref.referenceName.split(/[:.]/);
const lastName = parts[parts.length - 1];
if (lastName) {
const partialCandidates = keepForRef(context.getNodesByName(lastName))
.filter((candidate) => candidate.qualifiedName.endsWith(ref.referenceName));
const partialCandidates = cppSpecializationPool(keepForRef(context.getNodesByName(lastName))
.filter((candidate) => candidate.qualifiedName.endsWith(ref.referenceName)), ref, context);
const chosen = preferCallSiteFile(partialCandidates, ref.filePath)[0];
if (chosen) {
return {
@@ -10758,8 +10758,11 @@ function cppBasesKnown(cls: Node, context: ResolutionContext, depth = 0, seen =
/**
* C++ class `cls`'s method `name`: its own — of its overloads, the one the
* call's arguments fit (cppOverloadFor) — else the nearest one of a class
* it derives from, through the base edges every declaration of `cls` has.
* call's arguments fit (cppOverloadFor), and of a member template's explicit
* specializations, the one its template arguments name (cppSpecializationPool;
* null when it names none of them and they are all `cls` has) — else the
* nearest one of a class it derives from, through the base edges every
* declaration of `cls` has.
* When other classes share `cls`'s qualified name, its own are the ones
* defined in its body, else where it is: the local classes of gtest bodies
* in one namespace are each `ns::TEST_F::Listener`, and leveldb's
@@ -10772,7 +10775,11 @@ function cppMethodOf(cls: Node, name: string, ref: UnresolvedRef, context: Resol
const want = `${cls.qualifiedName}::${name}`;
const named = context.getMethodMatches?.(cls.name, name, ref.language) ??
context.getNodesByName(name).filter((n) => n.kind === 'method' && sameLanguageFamily(n.language, ref.language));
const own = named.filter((n) => n.qualifiedName === want);
const family = named.filter((n) => n.qualifiedName === want);
const own = cppSpecializationPool(family, ref, context);
// Specializations of the class's own member template, none the call's:
// what C++ finds is the template, which has no body here.
if (own.length === 0 && family.length > 0) return null;
if (own.length > 0) {
const ordered = preferCallSiteFile(cppVisibleFirst(own, ref, context), ref.filePath);
if (ref.referenceName.includes('().')) return ordered[0]!;
@@ -11111,12 +11118,12 @@ function cppScopeDeclarations(qualifiedName: string, decls: Node[], inner: Node,
* through a macro, which no node shows: lookup would stop there.
*/
function cppScopeMethod(scope: CppClassScope, name: string, receiver: 'bare' | 'this', ref: UnresolvedRef, context: ResolutionContext): Node | null | undefined {
const own = context.getNodesByQualifiedName(`${scope.qualifiedName}::${name}`).filter((n) =>
const own = cppSpecializationPool(context.getNodesByQualifiedName(`${scope.qualifiedName}::${name}`).filter((n) =>
n.kind === 'method' && (n.language === 'cpp' || n.language === 'c') &&
// A namesake class's members are defined where it is: leveldb's
// fault-injection test declares a `FileState` of its own, with no
// `Truncate`, beside memenv.cc's.
(!scope.narrowed || scope.decls.some((d) => d.filePath === n.filePath || cppIncluders(d.filePath, context).has(n.filePath))));
(!scope.narrowed || scope.decls.some((d) => d.filePath === n.filePath || cppIncluders(d.filePath, context).has(n.filePath)))), ref, context);
const holder = scope.holds ? scope.decls[0]! : null;
const inline = holder ? own.filter((n) => n.filePath === holder.filePath && rangeWithin(n, holder)) : [];
// A class with no name (a partial specialization, an unnamed struct)
@@ -11138,7 +11145,7 @@ function cppScopeMethod(scope: CppClassScope, name: string, receiver: 'bare' | '
// two-parameter template beside it).
const siblings = context.getNodesByQualifiedName(inherited.qualifiedName).filter((n) =>
n.id !== inherited.id && n.kind === 'method' && (n.language === 'cpp' || n.language === 'c'));
return cppOverloadFor([inherited, ...preferCallSiteFile(siblings, ref.filePath)], name, ref, context);
return cppOverloadFor(cppSpecializationPool([inherited, ...preferCallSiteFile(siblings, ref.filePath)], ref, context), name, ref, context);
}
}
return null;
@@ -11178,6 +11185,107 @@ function cppOverloadFor(candidates: Node[], name: string, ref: UnresolvedRef, co
return best;
}
/**
* Of the C++ functions `candidates` a call may reach, the ones the template
* arguments it writes allow. An explicit specialization is named like the
* template it specializes, so a call meets both among that name's candidates:
* - a call that writes template arguments is to the specialization written
* with the same ones — rocksdb's `Deserialize<int64_t>(src, offset)` is
* `template <> int64_t Deserialize<int64_t>(…)` — else to the template
* itself or an ordinary overload: fmt's `num_bits<unsigned short>()` is
* `num_bits<T>()`, not the `num_bits<uint128>()` beside the call. None is
* left when specializations are all there is: protobuf's template
* `ReadPrimitive<CType, DeclaredType>(…)` calls whichever one its
* arguments become.
* - a call that writes none, or another reference, reaches a
* specialization only when there is nothing else.
* The candidates as they are when none is a specialization.
*/
function cppSpecializationPool(candidates: Node[], ref: UnresolvedRef, context: ResolutionContext): Node[] {
if (ref.language !== 'cpp') return candidates;
const written = candidates.map((n) => cppSpecializationArguments(n, context));
if (written.every((w) => w === null)) return candidates;
const others = candidates.filter((_, i) => written[i] === null);
const args = cppCallTemplateArguments(ref, context);
if (args === null) return others.length > 0 ? others : candidates;
const named = candidates.filter((_, i) => written[i] !== null && cppTemplateArgumentsMatch(written[i]!, args));
return named.length > 0 ? named : others;
}
/** Per context: a C++ function's id → the template arguments it is an explicit specialization for (null: none). */
const CPP_SPECIALIZATION_ARGUMENTS = new WeakMap<ResolutionContext, Map<string, string[] | null>>();
/**
* The template arguments the C++ function or method `n` is an explicit
* specialization for, as its definition writes them after its name:
* `int32_t` and `WireFormatLite::TYPE_INT32` for protobuf's `template <>
* inline bool WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>(…)`.
* A method a macro defines is read from its expansion. Null for a template's
* own definition, any other function, and a specialization that leaves its
* arguments to be deduced.
*/
export function cppSpecializationArguments(n: Node, context: ResolutionContext): string[] | null {
// A constructor, whose signature is its parameter list, is never specialized
// by name: gmock's `template <typename Func> Action(const Action<Func>& action)`
// writes `Action<Func>` for a parameter.
if (n.language !== 'cpp' || (n.kind !== 'function' && n.kind !== 'method') || n.signature?.startsWith('(')) return null;
let memo = CPP_SPECIALIZATION_ARGUMENTS.get(context);
if (!memo) CPP_SPECIALIZATION_ARGUMENTS.set(context, (memo = new Map()));
const hit = memo.get(n.id);
if (hit !== undefined) return hit;
// (A method a macro defines carries its expansion as its signature.)
const lines = n.signature ? [] : context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? [];
const head = n.signature || lines.slice(n.startLine - 1, n.startLine + 11).join('\n').slice(n.startColumn);
const args = cppTemplateArgumentsAfter(head, n.name, 0);
memo.set(n.id, args);
return args;
}
/**
* The template arguments the C++ call at `ref` writes after the name it
* calls — `unsigned short` for fmt's `num_bits<unsigned short>()` — or null
* when it writes none, leaving them to be deduced.
*/
function cppCallTemplateArguments(ref: UnresolvedRef, context: ResolutionContext): string[] | null {
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? [];
const text = lines.slice(ref.line - 1, ref.line + 3).join('\n');
return cppTemplateArgumentsAfter(text, ref.referenceName.split(/::|\.|->/).pop()!, Math.max(0, ref.column));
}
/**
* The template arguments `text` writes right after `name`, the first time
* from `from` on it is written as a word of its own, as they compare
* (cppTemplateArgumentKey). Null when it writes none there, or `<>`.
*/
function cppTemplateArgumentsAfter(text: string, name: string, from: number): string[] | null {
for (let at = text.indexOf(name, from); at >= 0; at = text.indexOf(name, at + 1)) {
const open = at + name.length;
if (/[\w$]/.test(text[at - 1] ?? '') || /[\w$]/.test(text[open] ?? '')) continue;
const end = text[open] === '<' ? cppTemplateArgumentsEnd(text, open) : -1;
const args = end < 0 ? [] : splitCppTopLevel(text.slice(open + 1, end)).map(cppTemplateArgumentKey);
return args.length > 0 ? args : null;
}
return null;
}
/** A C++ template argument as it compares: spaced only between words. */
function cppTemplateArgumentKey(arg: string): string {
return arg.replace(/\s*([^\w\s])\s*/g, '$1').replace(/\s+/g, ' ');
}
/**
* Whether a call's template arguments `call` are the ones a specialization
* writes, `written`: each the same, or the same name qualified further on
* one side (`TYPE_INT64` inside the class, `WireFormatLite::TYPE_INT64`
* outside it).
*/
function cppTemplateArgumentsMatch(written: string[], call: string[]): boolean {
return written.length === call.length && written.every((w, i) => {
const c = call[i]!;
return w === c || w.endsWith(`::${c}`) || c.endsWith(`::${w}`);
});
}
const CPP_DELETED_FUNCTIONS = new WeakMap<ResolutionContext, Map<string, boolean>>();
/**
@@ -16136,7 +16244,7 @@ export function matchFuzzy(
// Prefer same-language matches
const sameLanguageCandidates = callableCandidates.filter(n => n.language === ref.language);
const languageCandidates = sameLanguageCandidates.length > 0 ? sameLanguageCandidates : callableCandidates;
const languageCandidates = cppSpecializationPool(sameLanguageCandidates.length > 0 ? sameLanguageCandidates : callableCandidates, ref, context);
const finalCandidates = dartBare || (dartTypeRef && dartPrefix === null) ? preferOwnDartLibrary(languageCandidates, ref, context) : languageCandidates;
// Both post-pipeline visibility guards (#1745 language-local + #1719 sealed
@@ -16803,13 +16911,14 @@ function matchCppMacroNamespaced(ref: UnresolvedRef, context: ResolutionContext)
const effective = prefix.length > 0 ? `${prefix.join('::')}::${n.qualifiedName}` : alias ? n.qualifiedName : '';
if (effective === target) matches.push(n);
}
const pool = cppSpecializationPool(matches, ref, context);
// A declaration outside the tests over one in them; among an overload set,
// the one the call's arguments fit: `fmt::format("{}", v)` is format.h's
// `format(format_string, T&&...)`, not color.h's `format(const text_style&, …)`.
const args = matches.length > 1 && ref.referenceKind === 'calls' ? cppCallArguments(ref, name, context) : null;
const args = pool.length > 1 && ref.referenceKind === 'calls' ? cppCallArguments(ref, name, context) : null;
let best: Node | null = null;
let bestScore = -Infinity;
for (const n of matches) {
for (const n of pool) {
const score = (isTestPath(n.filePath) ? -10 : 0) + (args ? cppOverloadFit(n, name, args, context) : 0);
if (score > bestScore) { best = n; bestScore = score; }
}