diff --git a/CHANGELOG.md b/CHANGELOG.md index 2daa541e1..7c01e09cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a call made with `->` on an `absl::StatusOr`, an `absl::optional` or an `absl::NoDestructor`, like protocolbuffers/protobuf's `resolver->MergeFeatures(…)` on an `absl::StatusOr`, now links to the method of the class it holds, as one on a `std::unique_ptr` or `std::optional` does. A call written without a receiver in a class template now also finds a member of a base that depends on the template's parameters when the class brings it in with a using-declaration, like facebook/rocksdb's `using BasicTypedCacheHelperFns::DownCastValue;`, and so does a call on such a member. Re-index C++ projects after upgrading. - In C and C++, a call or a type name no longer links to something only another source file can see: a class, struct or alias a `.c` or `.cc` file declares for itself, its members, or anything inside that file's unnamed `namespace { … }`. In facebook/rocksdb, other test files' bare `Put(…)`, `Flush()` and `dbfull()` calls had linked to the methods of a test fixture declared in `column_family_test.cc`, and calls on an `Env*` had linked to a test-only environment class in `memenv.cc`. When a header and a source file each declare a class of the same name, like RocksDB's `SharedState`, a call now links to the header's. A source file's own declarations are still found from the file itself, from a file that `#include`s it, and from a header it includes below them. Re-index C and C++ projects after upgrading. - 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::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. - 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. diff --git a/__tests__/cpp-written-scope-class-names.test.ts b/__tests__/cpp-written-scope-class-names.test.ts new file mode 100644 index 000000000..b0125308e --- /dev/null +++ b/__tests__/cpp-written-scope-class-names.test.ts @@ -0,0 +1,498 @@ +/** + * A C++ class, struct or union whose declaration writes its name with a + * scope — a nested class defined outside the class it is nested in — is + * indexed by its own name, qualified under that scope. + * + * google/leveldb defines `template struct + * SkipList::Node { … }` below the class, and facebook/rocksdb + * `struct BlockBasedTable::Rep { … }` in a source file. Each was indexed as a + * class named ``, so its members belonged to no class anyone + * names, and a call on a `Node*` or `Rep*` couldn't reach them: leveldb's + * `x->Next(level)` was guessed by the method's name. Such a class is now + * `Node`, qualified `leveldb::SkipList::Node` the way an out-of-line method + * `SkipList::Node::Next` already was, and its members are + * qualified under it. A specialization (`struct fmt::formatter`) + * keeps out of its template's name. + * + * A construction is recorded by its type's last name, so with + * `DBImpl::Writer` named, leveldb's `new log::Writer(…)` in a `DBImpl` method + * went to it. A construction now makes the class C++ lookup finds for the + * type as written there. + */ +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): Promise { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-scoped-class-')); + 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'); + +/** Every class, struct and union node, as `kind qualifiedName`, with the members it contains. */ +function classes(cg: CodeGraph): Record { + const out: Record = {}; + for (const n of [...cg.getNodesByKind('class'), ...cg.getNodesByKind('struct'), ...cg.getNodesByKind('union')]) { + const members = cg.getOutgoingEdgesFrom([n.id], ['contains']) + .map((e) => cg.getNode(e.target)) + .filter((m): m is Node => m !== null && (m.kind === 'method' || m.kind === 'field' || m.kind === 'property')) + .map((m) => m.qualifiedName) + .sort(); + out[`${n.kind} ${n.qualifiedName}`] = members; + } + return out; +} + +const SKIPLIST = lines( + 'namespace leveldb {', + 'template ', + 'class SkipList {', + ' public:', + ' void Insert(const Key& key) {', + ' Node* x = head_;', + ' x->SetNext(0, x->Next(0));', + ' }', + ' private:', + ' struct Node;', + ' Node* head_;', + '};', + 'template ', + 'struct SkipList::Node {', + ' Node* Next(int n) { return nullptr; }', + ' void SetNext(int n, Node* x) {}', + '};', + 'class Table {', + ' public:', + ' int GlobalSeqno() const { return rep_->get_global_seqno(); }', + ' private:', + ' struct Rep;', + ' Rep* rep_;', + '};', + 'struct Table::Rep {', + ' int get_global_seqno() const { return 0; }', + '};', + '// Another class with methods of these names, so a guess by the name can miss.', + 'class MemTableRep {', + ' public:', + ' void* Next(int n) { return nullptr; }', + ' void SetNext(int n, void* x) {}', + ' int get_global_seqno() const { return 1; }', + '};', + '} // namespace leveldb', +); + +describe('C++ classes named with a scope or template arguments', () => { + it('names an out-of-line nested class, qualified with its members under its scope', async () => { + const cg = await indexed({ + 'db/skiplist.h': SKIPLIST, + // rocksdb's thread pool hides its state in a `class` defined in its source file. + 'util/threadpool.h': lines( + 'namespace rocksdb {', + 'class ThreadPoolImpl {', + ' public:', + ' int Size() const;', + ' private:', + ' class Impl;', + ' Impl* impl_;', + '};', + '} // namespace rocksdb', + ), + 'util/threadpool.cc': lines( + '#include "util/threadpool.h"', + 'namespace rocksdb {', + 'class ThreadPoolImpl::Impl {', + ' public:', + ' int GetQueueLen() const { return 0; }', + '};', + 'int ThreadPoolImpl::Size() const { return impl_->GetQueueLen(); }', + '} // namespace rocksdb', + ), + }); + try { + const all = classes(cg); + expect(all['struct leveldb::SkipList::Node']).toEqual(['leveldb::SkipList::Node::Next', 'leveldb::SkipList::Node::SetNext']); + expect(all['struct leveldb::Table::Rep']).toEqual(['leveldb::Table::Rep::get_global_seqno']); + expect(all['class rocksdb::ThreadPoolImpl::Impl']).toEqual(['rocksdb::ThreadPoolImpl::Impl::GetQueueLen']); + expect(Object.keys(all).filter((k) => k.includes(''))).toEqual([]); + } finally { + cg.close(); + } + }); + + it("leaves a specialization out of its template's name", async () => { + // Named `formatter`, fmt's specializations stood in for the template and + // for each other: `formatter : formatter` + // extended `formatter`. + const cg = await indexed({ + 'include/fmt/base.h': lines( + 'namespace fmt {', + 'template struct formatter {', + ' formatter() = delete;', + '};', + 'template <> struct formatter {', + ' auto format(int x) const { return x; }', + '};', + '} // namespace fmt', + ), + 'test/format-test.cc': lines( + '#include "include/fmt/base.h"', + 'struct Answer {};', + 'template <> struct fmt::formatter : fmt::formatter {};', + ), + }); + try { + const named = [...cg.getNodesByKind('struct'), ...cg.getNodesByKind('class')] + .filter((n) => n.name === 'formatter' || n.qualifiedName.includes('formatter')); + expect(named.map((n) => `${n.qualifiedName} ${n.filePath}:${n.startLine}`)).toEqual(['fmt::formatter include/fmt/base.h:2']); + } finally { + cg.close(); + } + }); + + it('leaves a class unnamed when its scope is no plain name', async () => { + const cg = await indexed({ + 'outer.h': lines( + 'namespace ns {', + 'template struct Outer { struct Inner; struct Other; };', + 'template struct Outer<(N > 1)>::Inner { int x; };', + 'struct decltype(Outer<1>())::Other { int y; };', + '} // namespace ns', + ), + }); + try { + expect(Object.keys(classes(cg)).sort()).toEqual(['struct ns::', 'struct ns::Outer']); + } finally { + cg.close(); + } + }); + + it("links a call on such a class's pointer to its method", async () => { + const cg = await indexed({ 'db/skiplist.h': SKIPLIST }); + try { + expect(targets(cg, 'leveldb::SkipList::Insert', 'calls')).toEqual(['leveldb::SkipList::Node::Next @0.9', 'leveldb::SkipList::Node::SetNext @0.9']); + expect(targets(cg, 'leveldb::Table::GlobalSeqno', 'calls')).toEqual(['leveldb::Table::Rep::get_global_seqno @0.9']); + } finally { + cg.close(); + } + }); +}); + +/** 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(); +} + +describe('C++ constructions of a type a class shares its name with', () => { + // leveldb's db_impl.cc constructs `log::Writer`s and, as `Writer w(…)`, the + // `DBImpl::Writer` it defines out of line: by the type's last name alone, + // the two were one. + const LEVELDB = { + 'db/log_writer.h': lines( + 'namespace leveldb {', + 'namespace log {', + 'class Writer {', + ' public:', + ' explicit Writer(int* dest) {}', + '};', + '} // namespace log', + '} // namespace leveldb', + ), + 'db/db_impl.h': lines( + '#include "db/log_writer.h"', + 'namespace leveldb {', + 'class DBImpl {', + ' public:', + ' void NewDB();', + ' void Write();', + ' private:', + ' struct Writer;', + ' log::Writer* log_;', + '};', + '} // namespace leveldb', + ), + 'db/db_impl.cc': lines( + '#include "db/db_impl.h"', + 'namespace leveldb {', + 'struct DBImpl::Writer {', + ' explicit Writer(int* mu) {}', + '};', + 'void DBImpl::NewDB() {', + ' int file = 0;', + ' log_ = new log::Writer(&file);', + ' const log::Writer scratch(&file);', + '}', + 'void DBImpl::Write() {', + ' int mu = 0;', + ' Writer w(&mu);', + '}', + '} // namespace leveldb', + ), + 'db/log_test.cc': lines( + '#include "db/log_writer.h"', + 'namespace leveldb {', + 'namespace log {', + 'class LogTest {', + ' public:', + ' void Reset() { writer_ = new Writer(nullptr); }', + ' private:', + ' Writer* writer_;', + '};', + '} // namespace log', + '} // namespace leveldb', + ), + }; + + it('makes the class C++ lookup finds for the type as written', async () => { + const cg = await indexed(LEVELDB); + try { + expect(targets(cg, 'leveldb::DBImpl::NewDB', 'instantiates')).toEqual(['leveldb::log::Writer @0.9', 'leveldb::log::Writer @0.9']); + expect(targets(cg, 'leveldb::DBImpl::Write', 'instantiates')).toEqual(['leveldb::DBImpl::Writer @0.9']); + expect(targets(cg, 'leveldb::log::LogTest::Reset', 'instantiates')).toEqual(['leveldb::log::Writer @0.9']); + } finally { + cg.close(); + } + }); + + it("tells apart two classes' nested classes of one name", async () => { + // rocksdb's `new BlockBasedTable::Rep(…)` reached `BlockBasedTableBuilder::Rep`. + const cg = await indexed({ + 'table/reader.h': lines( + 'namespace rocksdb {', + 'class BlockBasedTable {', + ' public:', + ' static void Open();', + ' struct Rep;', + '};', + 'struct BlockBasedTable::Rep {', + ' explicit Rep(int level) {}', + '};', + '} // namespace rocksdb', + ), + 'table/reader.cc': lines( + '#include "table/reader.h"', + 'namespace rocksdb {', + 'void BlockBasedTable::Open() {', + ' Rep* rep = new BlockBasedTable::Rep(0);', + '}', + '} // namespace rocksdb', + ), + 'table/builder.cc': lines( + '#include "table/reader.h"', + 'namespace rocksdb {', + 'class BlockBasedTableBuilder {', + ' public:', + ' void Finish();', + ' private:', + ' struct Rep;', + ' Rep* rep_;', + '};', + 'struct BlockBasedTableBuilder::Rep {', + ' explicit Rep(int level) {}', + '};', + 'void BlockBasedTableBuilder::Finish() {', + ' rep_ = new Rep(1);', + ' BlockBasedTable::Rep* other = new BlockBasedTable::Rep(2);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(targets(cg, 'rocksdb::BlockBasedTable::Open', 'instantiates')).toEqual(['rocksdb::BlockBasedTable::Rep @0.9']); + expect(targets(cg, 'rocksdb::BlockBasedTableBuilder::Finish', 'instantiates')) + .toEqual(['rocksdb::BlockBasedTable::Rep @0.9', 'rocksdb::BlockBasedTableBuilder::Rep @0.9']); + } finally { + cg.close(); + } + }); + + it('makes an enum, the class a local alias names, or nothing, where lookup finds no class', async () => { + // rocksdb declares a `Comparator` class, and its skip lists take one as a + // template parameter; protobuf's `CounterMap()` makes a `std::map` it + // calls `Map`, beside protobuf's own `Map`. + const cg = await indexed({ + 'util/counters.cc': lines( + '#include ', + '#include "util/comparator.h"', + 'namespace rocksdb {', + 'void* CounterMap() {', + ' using Bytes = std::map;', + ' return new Bytes{};', + '}', + 'void* Buffer() { return new Bytes(1); }', + 'void* Owned() {', + ' using Comparator = Bytes;', + ' return new Comparator(2);', + '}', + '} // namespace rocksdb', + ), + 'util/comparator.h': lines( + 'namespace rocksdb {', + 'class Comparator {', + ' public:', + ' explicit Comparator(int n) {}', + '};', + 'class Bytes {', + ' public:', + ' explicit Bytes(int n) {}', + '};', + 'class value_type {', + ' public:', + ' explicit value_type(char c) {}', + '};', + 'enum ValueType { kTypeValue, kTypeDeletion };', + '} // namespace rocksdb', + ), + 'memtable/skiplist.h': lines( + '#include ', + '#include "util/comparator.h"', + 'namespace rocksdb {', + 'namespace memtable {', + 'using Bytes = std::string;', + 'template ', + 'class SkipList {', + ' public:', + ' void Insert() {', + ' Comparator compare(1);', + ' Bytes key(8);', + " Bytes::value_type first('a');", + ' const ValueType types[] = {kTypeValue};', + ' }', + '};', + 'template ', + 'void Fill() { ValueType value(1); }', + '} // namespace memtable', + '} // namespace rocksdb', + ), + // An enum of the same name another source file declares for itself. + 'util/a.cc': lines( + 'namespace rocksdb {', + 'enum ValueType { kOther };', + '} // namespace rocksdb', + ), + }); + try { + expect(targets(cg, 'rocksdb::memtable::SkipList::Insert', 'instantiates')).toEqual(['rocksdb::ValueType @0.9', 'rocksdb::memtable::Bytes @0.9']); + const insert = cg.getNodesByKind('method').find((n) => n.qualifiedName === 'rocksdb::memtable::SkipList::Insert')!; + expect(cg.getOutgoingEdgesFrom([insert.id], ['instantiates']).map((e) => cg.getNode(e.target)!) + .filter((n) => n.kind === 'enum').map((n) => n.filePath)).toEqual(['util/comparator.h']); + expect(targets(cg, 'rocksdb::memtable::Fill', 'instantiates')).toEqual([]); + expect(targets(cg, 'rocksdb::CounterMap', 'instantiates')).toEqual([]); + expect(targets(cg, 'rocksdb::Buffer', 'instantiates')).toEqual(['rocksdb::Bytes @0.9']); + expect(targets(cg, 'rocksdb::Owned', 'instantiates')).toEqual(['rocksdb::Bytes @0.9']); + } finally { + cg.close(); + } + }); + + it('takes the class only the file sees at less than C++ lookup', async () => { + const cg = await indexed({ + 'widget.h': lines( + 'namespace ui {', + 'class Widget {', + ' public:', + ' explicit Widget(int n) {}', + '};', + '} // namespace ui', + ), + 'main.cc': lines( + '#include "widget.h"', + 'class App {', + ' public:', + ' void Run() { Widget w(1); }', + '};', + ), + }); + try { + expect(targets(cg, 'App::Run', 'instantiates')).toEqual(['ui::Widget @0.7']); + } finally { + cg.close(); + } + }); + + it("reads the type's template arguments, and tells same-named local classes apart by where they are", async () => { + const cg = await indexed({ + 'util/writer_test.cc': lines( + 'namespace rocksdb {', + 'template class Outer {', + ' public:', + ' class Inner {};', + '};', + 'class WriterTest {', + ' public:', + ' void Make() {', + ' auto* a = new Outer::Inner();', + ' auto* b = new Outer<(sizeof(int) > 2)>::Inner();', + ' }', + '};', + 'TEST_F(WriterTest, RangeSync) {', + ' class FakeWF {', + ' public:', + ' explicit FakeWF(int n) {}', + ' };', + ' FakeWF* wf = new FakeWF(1);', + '}', + 'TEST_F(WriterTest, IncrementalBuffer) {', + ' class FakeWF {', + ' public:', + ' explicit FakeWF(int n) {}', + ' };', + ' FakeWF* wf = new FakeWF(2);', + '}', + '} // namespace rocksdb', + ), + // googlemock's specializations, unnamed, each nest an `Impl` and an alias. + 'gmock/gmock.h': lines( + 'namespace testing {', + 'class Matchers {', + ' template class RefMatcher;', + ' template class RefMatcher {', + ' public:', + ' using Tuple = std::tuple;', + ' class Impl {};', + " // The next specialization's `Tuple` and `Impl` are nearer to", + ' // this `Make` than its own.', + ' //', + ' //', + ' //', + ' void* Make() { Tuple t(1); return new Impl(); }', + ' };', + ' template class RefMatcher {', + ' public:', + ' using Tuple = std::tuple;', + ' class Impl {};', + ' void* Make() { Tuple t(nullptr); return new Impl(); }', + ' };', + '};', + '} // namespace testing', + ), + }); + try { + // (Recorded by the name before its first `<`, `Outer`.) + expect(targets(cg, 'rocksdb::WriterTest::Make', 'instantiates')).toEqual(['rocksdb::Outer::Inner @0.9', 'rocksdb::Outer::Inner @0.9']); + const made = (callers: Node[]): string[][] => callers.sort((a, b) => a.startLine - b.startLine) + .map((fn) => cg.getOutgoingEdgesFrom([fn.id], ['instantiates']).map((e) => `${fn.startLine} -> ${cg.getNode(e.target)?.startLine}`).sort()); + expect(made(cg.getNodesByKind('function').filter((n) => n.name === 'TEST_F'))).toEqual([['13 -> 14'], ['20 -> 21']]); + expect(made(cg.getNodesByKind('method').filter((n) => n.filePath === 'gmock/gmock.h'))) + .toEqual([['13 -> 6', '13 -> 7'], ['19 -> 17', '19 -> 18']]); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/kernel-ccpp-parity.test.ts b/__tests__/kernel-ccpp-parity.test.ts index 33baf912e..bbb9a838d 100644 --- a/__tests__/kernel-ccpp-parity.test.ts +++ b/__tests__/kernel-ccpp-parity.test.ts @@ -122,6 +122,43 @@ describe.skipIf(!kernelBuilt)('kernel C/C++ extraction parity', () => { expect(names).not.toContain('registration'); }); + it.each(['\n', '\r\n'])('classes named with a scope or template arguments retain parity (%j)', (eol) => { + const source = [ + 'namespace leveldb {', + 'template ', + 'class SkipList { struct Node; Node* head_; };', + 'template ', + 'struct SkipList::Node {', + ' Node* Next(int n) { return nullptr; }', + ' struct Link { int hops() const { return 0; } };', + '};', + 'class Table { struct Rep; };', + 'struct Table::Rep { int seqno() const { return 0; } };', + 'namespace leveldb {', + 'struct leveldb::Table::Rep2 {};', + '}', + 'struct ::Top { int depth() const { return 0; } };', + '} // namespace leveldb', + 'template ', + 'class FunctionMocker { R Invoke(Args... args) { return R(); } };', + 'template <> struct fmt::formatter { int format(int x) const { return x; } };', + 'union ::Global::Bits { int all; };', + 'struct Tag;', + 'struct ::Tag { int bits() const { return 0; } };', + '', + ].join(eol); + assertParity('scoped-classes.h', source, 'cpp', 10); + const result = tryKernelExtract('scoped-classes.h', source, 'cpp')!; + const named = (kind: string) => result.nodes.filter((n) => n.kind === kind).map((n) => n.qualifiedName).sort(); + // A name written from the global scope alone has no scope to be qualified under. + expect(result.nodes.map((n) => n.qualifiedName).filter((q) => q.startsWith('::') || q.includes('::::'))).toEqual([]); + expect(named('struct')).toEqual(expect.arrayContaining(['leveldb::SkipList::Node', 'leveldb::SkipList::Node::Link', 'leveldb::Table::Rep', 'leveldb::Table::Rep2'])); + expect(named('union')).toEqual(['Global::Bits']); + expect(named('method')).toEqual(expect.arrayContaining(['leveldb::SkipList::Node::Next', 'leveldb::SkipList::Node::Link::hops', 'leveldb::Table::Rep::seqno'])); + // A specialization is not named after its template. + expect([...named('struct'), ...named('class')].filter((q) => /formatter|FunctionMocker/.test(q))).toEqual([]); + }); + it.each(['\n', '\r\n'])('COM interface declarations retain native/wasm parity (%j)', (eol) => { const source = [ '#define interface struct', diff --git a/codegraph-kernel/src/ccpp/mod.rs b/codegraph-kernel/src/ccpp/mod.rs index 415ca3bca..f6d5fa7d3 100644 --- a/codegraph-kernel/src/ccpp/mod.rs +++ b/codegraph-kernel/src/ccpp/mod.rs @@ -301,6 +301,10 @@ struct Scope { row: u32, kind: &'static str, name: String, + /// The qualified name its members are qualified under in place of the + /// scopes around it: a class written in a scope (objectPathOwners in + /// tree-sitter.ts, written_scope_qualified_name). + qualified: Option, } #[derive(Default)] @@ -464,7 +468,7 @@ pub fn extract(file_path: &str, source: &str, language: &str) -> Result Walker<'t> { let qualified = extra.qualified_name.unwrap_or_else(|| { let mut parts: Vec<&str> = self.namespace_prefix.iter().map(|s| s.as_str()).collect(); for s in &self.stack { - if s.kind != "file" { + if let Some(q) = &s.qualified { + parts.clear(); + parts.push(q); + } else if s.kind != "file" { parts.push(&s.name); } } @@ -648,6 +655,11 @@ impl<'t> Walker<'t> { if let Some(name) = self.unnamed_type_variable_name(node) { return name; } + if self.variant == Variant::Cpp { + if let Some(mut parts) = self.cpp_written_class_parts(node) { + return parts.pop().unwrap_or_default(); + } + } if let Some(name_node) = node.child_by_field_name("declarator") { let mut resolved = name_node; // Unwrap pointer/reference declarators (`int* f()`, `T& f()`). @@ -688,6 +700,55 @@ impl<'t> Walker<'t> { "".to_string() } + /// cppWrittenClassParts (languages/c-cpp.ts): the `::` segments of a class, + /// struct or union name written with a scope, the scope's template + /// arguments dropped (`["SkipList", "Node"]` for `struct SkipList::Node`). None for a plain name and for a specialization, + /// whose innermost name has template arguments. + fn cpp_written_class_parts(&self, node: Node) -> Option> { + if !matches!(node.kind(), "class_specifier" | "struct_specifier" | "union_specifier") { + return None; + } + let written = node.child_by_field_name("name")?; + if written.kind() != "qualified_identifier" { + return None; + } + let mut innermost = Some(written); + while let Some(n) = innermost.filter(|n| n.kind() == "qualified_identifier") { + innermost = n.child_by_field_name("name"); + } + if innermost.map(|n| n.kind()) != Some("type_identifier") { + return None; + } + let parts: Vec = strip_cpp_template_args(self.text(written)) + .split("::") + .map(|p| p.trim().to_string()) + .filter(|p| !p.is_empty()) + .collect(); + let identifier = |p: &String| { + let mut chars = p.chars(); + chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_') + && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') + }; + if parts.is_empty() || !parts.iter().all(identifier) { + return None; + } + Some(parts) + } + + /// writtenScopeQualifiedName (tree-sitter.ts): a class whose name is + /// written in a scope is qualified like an out-of-line method under it. + fn written_scope_qualified_name(&self, node: Node, name: &str) -> Option { + if self.variant != Variant::Cpp { + return None; + } + let parts = self.cpp_written_class_parts(node)?; + if parts.len() < 2 { + return None; + } + Some(self.compose_receiver_qualified_name(&parts[..parts.len() - 1].join("::"), name)) + } + /// unnamedTypeVariableName (languages/c-cpp.ts): an unnamed class, struct, /// union or enum defined in a declaration takes the name of the first /// variable the declaration declares (`static struct { … } SPT;` → `SPT`). @@ -1172,7 +1233,7 @@ impl<'t> Walker<'t> { // for c/cpp: not in TYPE_ANNOTATION_LANGUAGES, and the decorator node // kinds never appear as direct children/preceding siblings in these // grammars — `attribute` only occurs under attribute_declaration.) - self.stack.push(Scope { row, kind: "function", name }); + self.stack.push(Scope { row, kind: "function", name, qualified: None }); if let Some(body) = node.child_by_field_name("body") { self.visit_function_body(body); } @@ -1242,7 +1303,7 @@ impl<'t> Walker<'t> { } } - self.stack.push(Scope { row, kind: "method", name }); + self.stack.push(Scope { row, kind: "method", name, qualified: None }); if let Some(body) = node.child_by_field_name("body") { self.visit_function_body(body); } @@ -1260,14 +1321,16 @@ impl<'t> Walker<'t> { stack_guard!(); let Some(body) = node.child_by_field_name("body") else { return }; let name = self.extract_name(node); + let scoped = self.written_scope_qualified_name(node, &name); let extra = Extra { docstring: self.class_like_docstring(node), visibility: self.visibility_of(node), end: self.brace_body_end(body), + qualified_name: scoped.clone(), ..Extra::default() }; let Some(row) = self.create_node("class", &name, node, extra) else { return }; - let scope = Scope { row, kind: "class", name }; + let scope = Scope { row, kind: "class", name, qualified: scoped }; self.open_class_scope(&scope, body); self.extract_inheritance(node, row); self.stack.push(scope); @@ -1284,14 +1347,16 @@ impl<'t> Walker<'t> { stack_guard!(); let Some(body) = node.child_by_field_name("body") else { return }; let name = self.extract_name(node); + let scoped = self.written_scope_qualified_name(node, &name); let extra = Extra { docstring: self.class_like_docstring(node), visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None }, end: self.brace_body_end(body), + qualified_name: scoped.clone(), ..Extra::default() }; let Some(row) = self.create_node(kind, &name, node, extra) else { return }; - let scope = Scope { row, kind, name }; + let scope = Scope { row, kind, name, qualified: scoped }; self.open_class_scope(&scope, body); self.extract_inheritance(node, row); self.stack.push(scope); @@ -1314,7 +1379,7 @@ impl<'t> Walker<'t> { }; let Some(row) = self.create_node("enum", &name, node, extra) else { return }; self.extract_inheritance(node, row); - self.stack.push(Scope { row, kind: "enum", name }); + self.stack.push(Scope { row, kind: "enum", name, qualified: None }); for i in 0..body.named_child_count() { let Some(child) = body.named_child(i) else { continue }; if child.kind() == "enumerator" { @@ -1375,7 +1440,7 @@ impl<'t> Walker<'t> { ) else { return true; }; - self.stack.push(Scope { row, kind, name }); + self.stack.push(Scope { row, kind, name, qualified: None }); let type_child = node .child_by_field_name("type") .or_else(|| self.find_child_by_kind(node, "struct_specifier")) @@ -1402,7 +1467,7 @@ impl<'t> Walker<'t> { ) else { return true; }; - self.stack.push(Scope { row, kind: "enum", name }); + self.stack.push(Scope { row, kind: "enum", name, qualified: None }); if let Some(inner) = self.find_child_by_kind(node, "enum_specifier") { self.extract_inheritance(inner, row); if let Some(body) = inner.child_by_field_name("body") { diff --git a/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts index f2161c6fa..69d9a5c6c 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -192,6 +192,31 @@ function recoverCppMacroDefinedName(node: SyntaxNode, source: string): string | return name; } +const CPP_CLASS_SPECIFIERS: ReadonlySet = new Set(['class_specifier', 'struct_specifier', 'union_specifier']); + +/** + * The `::` segments of a C++ class, struct or union name written with a + * scope, the scope's template arguments dropped: `['SkipList', 'Node']` for + * an out-of-line nested class `struct SkipList::Node`. Such + * a class was indexed ``, so its members were no class's. + * Undefined for a plain name, and for a specialization (`class + * FunctionMocker`, `struct fmt::formatter`): named after its + * template, it would stand in for the template and every other + * specialization of it (fmt's `formatter : formatter` extended `formatter`). Mirrored in the kernel + * (ccpp/mod.rs). + */ +function cppWrittenClassParts(node: SyntaxNode, source: string): string[] | undefined { + if (!CPP_CLASS_SPECIFIERS.has(node.type)) return undefined; + const written = getChildByField(node, 'name'); + if (written?.type !== 'qualified_identifier') return undefined; + let innermost: SyntaxNode | null = written; + while (innermost?.type === 'qualified_identifier') innermost = getChildByField(innermost, 'name'); + if (innermost?.type !== 'type_identifier') return undefined; + const parts = stripCppTemplateArgs(getNodeText(written, source)).split('::').map((p) => p.trim()).filter(Boolean); + return parts.length > 0 && parts.every((p) => /^[A-Za-z_]\w*$/.test(p)) ? parts : undefined; +} + function extractCppQualifiedMethodName(node: SyntaxNode, source: string): string | undefined { const macroDefined = recoverSingleArgMacroDefinedName(node, source) ?? recoverCppMacroDefinedName(node, source); if (macroDefined) return macroDefined; @@ -2400,9 +2425,10 @@ export const cppExtractor: LanguageExtractor = { bodyField: 'body', paramsField: 'parameters', resolveName: (node, source) => - extractCppQualifiedMethodName(node, source) ?? unnamedTypeVariableName(node, source), + extractCppQualifiedMethodName(node, source) ?? unnamedTypeVariableName(node, source) ?? cppWrittenClassParts(node, source)?.at(-1), getDeclarationWrapper: (node) => definingDeclaration(node) ?? undefined, getReceiverType: extractCppReceiverType, + getClassScope: (node, source) => cppWrittenClassParts(node, source)?.slice(0, -1).join('::') || undefined, getReturnType: extractCppReturnType, // Constructors (definitions and class-body declarations) carry their // parameter list as the signature; a trailing semicolon marks a prototype, so a local `T obj(args)` can be matched diff --git a/src/extraction/tree-sitter-types.ts b/src/extraction/tree-sitter-types.ts index e665b7d3d..3a695d52d 100644 --- a/src/extraction/tree-sitter-types.ts +++ b/src/extraction/tree-sitter-types.ts @@ -314,6 +314,14 @@ export interface LanguageExtractor { */ getReceiverType?: (node: SyntaxNode, source: string) => string | undefined; + /** + * The scope a class, struct or union declaration writes its name in, when + * it does: C++'s `SkipList` for an out-of-line nested class `struct + * SkipList::Node`. The class is qualified under it the way + * an out-of-line method is under its receiver, and so are its members. + */ + getClassScope?: (node: SyntaxNode, source: string) => string | undefined; + /** * Extract a function/method's normalized return type name (bare class name, * smart-pointer pointee unwrapped), stored on the node as `returnType`. Used diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 165885eda..0422c44c5 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -1819,6 +1819,18 @@ export class TreeSitterExtractor { return prefix.length > 0 ? `${prefix.join('::')}::${base}` : base; } + /** + * The qualified name of a class, struct or union whose declaration writes + * its name in a scope (getClassScope): C++'s out-of-line nested `struct + * SkipList::Node` is `leveldb::SkipList::Node`, composed + * like an out-of-line method's. Its members are qualified under it + * (objectPathOwners). Undefined for any other. + */ + private writtenScopeQualifiedName(node: SyntaxNode, name: string): string | undefined { + const scope = this.extractor?.getClassScope?.(node, this.source); + return scope ? this.composeReceiverQualifiedName(scope, name) : undefined; + } + /** * Build qualified name from node stack */ @@ -2140,14 +2152,17 @@ export class TreeSitterExtractor { const docstring = this.docstringFor(node); const visibility = this.extractor.getVisibility?.(node); const isExported = this.extractor.isExported?.(node, this.source); + const scoped = this.writtenScopeQualifiedName(node, name); const classNode = this.createNode(kind, name, node, { docstring, visibility, isExported, ...this.cppBodyEnd(resolvedBody), + ...(scoped !== undefined ? { qualifiedName: scoped } : {}), }); if (!classNode) return; + if (scoped !== undefined) this.objectPathOwners.set(classNode.id, scoped); this.openCppClassScope(classNode, resolvedBody); // Extract extends/implements @@ -2356,14 +2371,17 @@ export class TreeSitterExtractor { const docstring = this.docstringFor(node); const visibility = this.extractor.getVisibility?.(node); const isExported = this.extractor.isExported?.(node, this.source); + const scoped = this.writtenScopeQualifiedName(node, name); const aggregateNode = this.createNode(kind, name, node, { docstring, visibility, isExported, ...this.cppBodyEnd(body), + ...(scoped !== undefined ? { qualifiedName: scoped } : {}), }); if (!aggregateNode) return; + if (scoped !== undefined) this.objectPathOwners.set(aggregateNode.id, scoped); this.openCppClassScope(aggregateNode, body); // Extract inheritance (e.g. Swift: struct HTTPMethod: RawRepresentable) diff --git a/src/resolution/cpp-type-aliases.ts b/src/resolution/cpp-type-aliases.ts index 3fbd1cc5c..f4bbbbbe4 100644 --- a/src/resolution/cpp-type-aliases.ts +++ b/src/resolution/cpp-type-aliases.ts @@ -81,12 +81,16 @@ function cppTypeName(raw: string): TypeName | null { const parts: string[] = []; const templated: boolean[] = []; let depth = 0; + let parens = 0; let part = ''; let args = false; const text = raw.replace(/\b(?:const|volatile|mutable|typename|template|class|struct|union|enum)\b/g, ' ').replace(/[&*]+/g, ' '); for (let i = 0; i < text.length; i++) { const c = text[i]!; - if (c === '<') { + // (In template arguments, a `<` or `>` in parentheses compares: `Bits<(N > 8)>`.) + if (depth > 0 && (c === '(' || c === ')')) parens = Math.max(0, parens + (c === '(' ? 1 : -1)); + else if (parens > 0) continue; + else if (c === '<') { depth++; args = true; } else if (c === '>') depth = Math.max(0, depth - 1); @@ -254,6 +258,17 @@ function localAliasLines(caller: Node, lines: readonly string[], context: Resolu return found; } +/** + * Does the function making `ref` declare, before it, an alias of the name a + * written type starts with — `using Map = std::map<…>;` for `Map`? Such an + * alias isn't indexed, so a lookup goes past it; resolveCppAliasedType reads it. + */ +export function cppDeclaresLocalAlias(written: string, ref: UnresolvedRef, context: ResolutionContext): boolean { + const name = cppTypeName(written); + return !!name && !written.trimStart().startsWith('::') && + localAliasedType(name.names[0]!, context.getNodeById?.(ref.fromNodeId), ref, context) !== undefined; +} + /** * The type an alias declared earlier in the calling function names, as * written. Those aliases are not indexed, so the function's own lines are diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index ed9ef9643..f95a076b5 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -14,7 +14,7 @@ import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-vis import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, inSameDartLibrary } from './dart-libraries'; import { isDartLocallyBound } from './dart-local-scope'; import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers'; -import { cppAliasedClass, cppAliasedTypeName, cppAliasedTypeWritten, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments, type CppAliasedType } from './cpp-type-aliases'; +import { cppAliasedClass, cppAliasedTypeName, cppAliasedTypeWritten, cppDeclaresLocalAlias, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments, type CppAliasedType } from './cpp-type-aliases'; import { clearCppNamespaceMemos, cppMacroNamespaceFrames, cppMacroNamespaceHeads, cppNamespaceAliases } from './cpp-namespaces'; import { clearCppRawStringMemos, cppLiteralsBlanked, cppRawStringLines } from './cpp-raw-strings'; import { cppClassWritten, type CppNameSite } from './cpp-supertypes'; @@ -11346,6 +11346,133 @@ function inferCppAutoInitializerType( return null; } +/** + * A C++ construction — `new log::Writer(…)`, `Writer w(…)`, which the + * extractor records by the type's last name — makes the class C++ lookup + * finds for the type as written at the ref (cppClassWritten), as a base + * class names one (matchCppSupertype): from the scopes around the call + * outward, its qualifier followed, an alias to the class it names. leveldb's + * db_impl.cc writes `new log::Writer(…)` in `DBImpl` methods, which also + * construct `DBImpl::Writer`s, the struct it defines out of line, as `Writer + * w(…)`; by its last name alone the two were one. + * - At 0.7 when lookup doesn't place the type and the class is only the one + * of that name the file can see. + * - The enum lookup finds instead of a class: rocksdb's `const ValueType + * types[] = {kTypeValue, …}`. + * - Through an alias the calling function declares itself, which isn't + * indexed, the class it names, if one of the project's. + * - null (no edge) when the type names no particular class: a template + * parameter, a member of a type from outside the project (rocksdb's + * `stl_wrappers::KVMap::value_type`), a function's own alias of one — + * protobuf's `using Map = std::map<…>; … new Map{}` is no + * `google::protobuf::Map`. + * - undefined, for the name strategies to decide as before, when nothing + * the file sees is named so. + * The type is read with its template arguments, so `new Outer::Inner(…)`, + * which the extractor records by the name before the first `<`, makes an + * `Outer::Inner`. Where the file declares several classes or aliases of the + * qualified name lookup found, which C++ tells apart and qualified names + * don't, the type is the one declared around the construction + * (cppNamesakeAround). + */ +function matchCppConstruction(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined { + const caller = context.getNodeById?.(ref.fromNodeId); + const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/); + const text = lines?.[ref.line - 1]?.slice(Math.max(0, ref.column)); + if (!caller || !text) return undefined; + // (A declaration's column is where its specifiers start: `const Slice key(…)`.) + const specifiers = /^(?:(?:new|const|volatile|static|thread_local|constexpr|constinit|mutable|struct|class|union|typename)\s+)*/.exec(text)![0]; + const written = cppTypeWrittenAt(text.slice(specifiers.length)); + if (!written) return undefined; + const made = (node: Node, exact = true): ResolvedRef => exact + ? { original: ref, targetNodeId: node.id, confidence: 0.9, resolvedBy: 'qualified-name' } + : { 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 cls = named ? context.getNodesByQualifiedName(named).find((n) => CPP_CLASS_KINDS.has(n.kind)) : undefined; + return cls ? made(cls) : null; + } + const site = cppCallSite(caller, ref, context); + const found = cppClassWritten(written, site, ref, context); + if (found === null) { + const named = cppEnumWritten(written, site, context); + return named ? made(named) : null; + } + if (!found) return undefined; + const namesakes = context.getNodesByQualifiedName(found.node.qualifiedName) + .filter((n) => n.filePath === ref.filePath && (CPP_CLASS_KINDS.has(n.kind) || n.kind === 'type_alias')); + const type = namesakes.length > 1 ? cppNamesakeAround(namesakes, ref, context) : found.node; + return type ? made(type, found.exact) : undefined; +} + +/** + * The enum a C++ type written at `site` names, which C++ lookup for a class + * passes by: the first scope around the site, innermost first, that declares + * one of that name the file can see. (A template parameter of that name is + * the parameter, which the resolver's type-parameter gate sees to.) + */ +function cppEnumWritten(written: string, site: CppNameSite, context: ResolutionContext): Node | undefined { + const global = written.trimStart().startsWith('::'); + const path = stripCppTemplateArguments(written).replace(/\s+/g, '').replace(/^::/, ''); + for (const scope of global ? [''] : cppScopesWithin(site.scope)) { + const enums = context.getNodesByQualifiedName(scope ? `${scope}::${path}` : path) + .filter((n) => n.kind === 'enum' && (n.filePath === site.file || !C_SOURCE_EXT.test(n.filePath))); + if (enums.length > 0) return enums.find((n) => n.filePath === site.file) ?? enums[0]; + } + return undefined; +} + +const CPP_HOLDER_KINDS: ReadonlySet = new Set(['function', 'method', 'class', 'struct', 'union']); + +/** + * Of several classes or aliases the ref's file declares under one qualified + * name — the local classes of rocksdb's gtest `TEST_F` bodies, each a + * `FakeWF`, or the `Impl`s and `ArgumentTuple`s of googlemock's unnamed + * specializations — the one whose holder (the function or class it is + * declared in, else the file) holds the ref. Undefined unless exactly one is. + */ +function cppNamesakeAround(namesakes: Node[], ref: UnresolvedRef, context: ResolutionContext): Node | undefined { + const holders = context.getNodesInFile(ref.filePath).filter((n) => CPP_HOLDER_KINDS.has(n.kind)); + const around = namesakes.filter((cls) => { + let holder: Node | undefined; + for (const n of holders) { + if (n.id === cls.id || n.startLine > cls.startLine || n.endLine < cls.endLine) continue; + if (!holder || n.endLine - n.startLine < holder.endLine - holder.startLine) holder = n; + } + return !holder || (holder.startLine <= ref.line && ref.line <= holder.endLine); + }); + return around.length === 1 ? around[0] : undefined; +} + +/** + * The type name `text` starts with, template arguments and all: `log::Writer` + * of `log::Writer(…)`, `Outer::Inner` of `Outer::Inner x(…)`. + * Undefined when it starts with none, or a template argument list goes on to + * the next line. + */ +function cppTypeWrittenAt(text: string): string | undefined { + let end = 0; + for (let segment = /^(?:::\s*)?[A-Za-z_]\w*/; ; segment = /^\s*::\s*[A-Za-z_]\w*/) { + const name = segment.exec(text.slice(end)); + if (!name) return end > 0 ? text.slice(0, end) : undefined; + end += name[0].length; + const open = /^\s*` inside parentheses compares: `Bits<(N > 8)>`.) + for (let angles = 1, parens = 0; angles > 0; end++) { + const c = text[end]; + if (c === undefined) return undefined; + if (c === '(') parens++; + else if (c === ')') parens--; + else if (parens > 0) continue; + else if (c === '<') angles++; + else if (c === '>') angles--; + } + } +} + /** * Resolve a C++ chained call whose receiver is itself a call — encoded by the * extractor as `().` (#645). The receiver's type is what @@ -15849,6 +15976,13 @@ function matchReferenceInner( if (result) return result; } + // 1b'. A C++ construction — `new log::Writer(…)`, `Writer w(…)` — makes the + // class C++ lookup finds for the type as written there, or nothing. + if (ref.language === 'cpp' && ref.referenceKind === 'instantiates') { + const constructed = nmTimedT('cppConstruct', ref, () => matchCppConstruction(ref, context)); + if (constructed !== undefined) return constructed; + } + // 1c. `::`-scoped factory chain — PHP `Cls::for($x)->method()` (#608) or Rust // `Foo::new().bar()`, both encoded as `Cls::factory().method`. The receiver's // type is the factory's `self` (PHP `: self`/`: static`, Rust `-> Self`) or