From b0ed652c0b1f6a56b8b294588fcd3449b21ecb2c Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Mon, 12 Oct 2026 22:49:17 -0500 Subject: [PATCH 1/2] fix(cpp): a bare call in a gtest TEST_F or TEST_P body reaches the fixture's method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The index holds a gtest `TEST_F(Fixture, Name)` / `TEST_P` body as a function named `TEST_F`; gtest makes it a method of a class derived from `Fixture`. #2533 gave such a body the fixture's data members (cppTestFixture), not its methods. rocksdb's tests call `DBTestBase`'s `Put(…)`, `Flush()`, `Key(i)` and `NewIterator(…)` bare, and those calls were guessed by name, often reaching a namesake in another class at 0.4 (`ColumnFamilyTestBase::Put`); `auto iter = NewIterator(…)` gave `iter` no type, so `iter->SeekToFirst()` reached the nested `MemTableRep::Iterator`. - cppImplicitThisMethod is #2479's lookup, taken out of matchCppImplicitThisCall: the caller's class, its bases, the classes around it, with cppTestFixture as the owner of a gtest body. matchCppImplicitThisCall keeps the local-shadowing check for the link. - cppExpressionType types a bare call by the same lookup, as on `this`, instead of the class's first declaration. Of two classes sharing a name in different test files it reads the caller's own, and in a template a dependent base a using-declaration names still counts, as before (protobuf's `using RepeatedFieldWrapper::MutableRepeatedField;`). `this` in a gtest body points to the fixture. - A `->` call on a value of a type from outside the project no longer takes the type's last name for the class it is in (isCppArrowThroughOutsideType); the name strategies decide. matchCppDeclaredTypeCall steps aside for `->` on a value before #2532's outside-type check, so typing JsonTest's `auto m = ToProto(…)` (an `absl::StatusOr`) had sent `m->value()` to protobuf's own `hpb::StatusOr::value` at 0.9. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/cpp-test-fixture-call.test.ts | 407 ++++++++++++++++++++++++ src/resolution/name-matcher.ts | 68 +++- 3 files changed, 465 insertions(+), 11 deletions(-) create mode 100644 __tests__/cpp-test-fixture-call.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 01aa134aea..06c0869652 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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(…)` or `auto* mutex = static_cast(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`. 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 shutting_down_`, no longer links to a project method that only shares the method's name. Re-index C++ projects after upgrading. +- In C++, a call written without a receiver in a gtest `TEST_F` or `TEST_P` body now links to the test fixture's method, its own or one it inherits, as C++ finds it: facebook/rocksdb's tests call `DBTestBase`'s `Put(…)`, `Flush()` and `NewIterator(…)` this way. A variable such a call initializes, like `auto iter = NewIterator(…)`, takes the type the method returns, so `iter->SeekToFirst()` reaches the iterator class. Before, those calls were guessed by name and often reached a namesake in another class, like RocksDB's `ColumnFamilyTestBase::Put` or protocolbuffers/protobuf's `EncodeDecodeTest::ExpectNoErrors`, and `iter->SeekToFirst()` reached the unrelated `MemTableRep::Iterator`. A `->` call on a value of a type from outside the project, like protobuf's `m->value()` on an `absl::StatusOr`, no longer links to a class of the project that shares the type's name, like protobuf's own `hpb::StatusOr`. 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-test-fixture-call.test.ts b/__tests__/cpp-test-fixture-call.test.ts new file mode 100644 index 0000000000..1406adcc57 --- /dev/null +++ b/__tests__/cpp-test-fixture-call.test.ts @@ -0,0 +1,407 @@ +/** + * A bare call in a gtest `TEST_F(Fixture, Name)` or `TEST_P` body reaches + * the fixture's method. + * + * The index holds such a body as a function named `TEST_F`, but gtest makes + * it a method of a class it derives from `Fixture`, declared where the + * `TEST_F` is. C++ looks a name the body calls up in that class, then in the + * fixture and the classes it derives from, before the namespace. Since PR + * #2533 the fixture's data members were found that way (`db_->Put(…)`); its + * methods were not, so in facebook/rocksdb `Put(…)`, `Flush()` and `auto iter + * = NewIterator(…)` — methods of `DBTestBase` — were guessed by name, and + * `iter->SeekToFirst()` reached the nested `MemTableRep::Iterator`. + * + * A guess can land on the right method by a shared word, so these tests + * check the confidence, not only the target. + */ +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-fixture-')); + 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 }); +} + +/** The function or method `qualifiedName`, the one in `file` when given. */ +function callable(cg: CodeGraph, qualifiedName: string, file?: string): Node { + const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find( + (n) => n.qualifiedName === qualifiedName && (file === undefined || n.filePath === file), + ); + if (!node) throw new Error(`no function or method ${qualifiedName}`); + return node; +} + +/** `calls` callees of a function or method, as `qualifiedName @confidence`, one per call site. */ +function calls(cg: CodeGraph, caller: string, file?: string): string[] { + return cg + .getCallees(callable(cg, caller, file).id) + .filter((r) => r.edge.kind === 'calls') + .map((r) => `${r.node.qualifiedName} @${(r.edge.metadata as { confidence?: number } | undefined)?.confidence}`) + .sort(); +} + +/** `calls` callees as `qualifiedName by resolvedBy`. */ +function resolvers(cg: CodeGraph, caller: string): string[] { + return cg + .getCallees(callable(cg, caller).id) + .filter((r) => r.edge.kind === 'calls') + .map((r) => `${r.node.qualifiedName} by ${(r.edge.metadata as { resolvedBy?: string } | undefined)?.resolvedBy}`) + .sort(); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** rocksdb's public iterator, the nested namesake, and the test fixture base, trimmed. */ +const ROCKSDB = { + 'include/rocksdb/iterator.h': lines( + 'namespace rocksdb {', + 'class IteratorBase {', + ' public:', + ' virtual void SeekToFirst() = 0;', + ' virtual bool Valid() const = 0;', + '};', + 'class Iterator : public IteratorBase {};', + '} // namespace rocksdb', + ), + 'db/db_test_util.h': lines( + '#include "include/rocksdb/iterator.h"', + 'namespace rocksdb {', + 'class DBTestBase {', + ' public:', + ' Iterator* NewIterator();', + ' void Put(int key);', + ' void Flush();', + '};', + '} // namespace rocksdb', + ), + // As in rocksdb, the fixture base's methods are defined out of line. + 'db/db_test_util.cc': lines( + '#include "db/db_test_util.h"', + 'namespace rocksdb {', + 'Iterator* DBTestBase::NewIterator() { return nullptr; }', + 'void DBTestBase::Put(int key) {}', + 'void DBTestBase::Flush() {}', + '} // namespace rocksdb', + ), +}; + +/** Namesakes beside the tests, which a guess by name prefers. */ +const NAMESAKES = lines( + 'class MemTableRep {', + ' public:', + ' class Iterator {', + ' public:', + ' void SeekToFirst() {}', + ' bool Valid() const { return false; }', + ' };', + ' Iterator* NewIterator() { return nullptr; }', + '};', + 'class WriteBatch {', + ' public:', + ' void Put(int key) {}', + ' void Flush() {}', + '};', +); + +describe('C++ bare calls in gtest TEST_F / TEST_P bodies', () => { + it("reaches the fixture's own and inherited methods", async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/db_basic_test.cc': lines( + '#include "db/db_test_util.h"', + 'namespace rocksdb {', + NAMESAKES, + 'class DBBasicTest : public DBTestBase {', + ' public:', + ' void Reopen() {}', + '};', + 'TEST_F(DBBasicTest, PutFlush) {', + ' Put(1);', + ' Flush();', + ' Reopen();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::TEST_F')).toEqual([ + 'rocksdb::DBBasicTest::Reopen @0.9', + 'rocksdb::DBTestBase::Flush @0.9', + 'rocksdb::DBTestBase::Put @0.9', + ]); + } finally { + cg.close(); + } + }); + + it('types `auto iter = NewIterator()` from the fixture\'s method', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/db_iterator_test.cc': lines( + '#include "db/db_test_util.h"', + 'namespace rocksdb {', + NAMESAKES, + 'class DBIteratorTest : public DBTestBase {};', + 'TEST_F(DBIteratorTest, IterSeekToFirst) {', + ' auto iter = NewIterator();', + ' iter->SeekToFirst();', + ' if (iter->Valid()) {}', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::TEST_F')).toEqual([ + 'rocksdb::DBTestBase::NewIterator @0.9', + 'rocksdb::IteratorBase::SeekToFirst @0.9', + 'rocksdb::IteratorBase::Valid @0.9', + ]); + } finally { + cg.close(); + } + }); + + it('does the same in a TEST_P body, and on `this->`', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/db_param_test.cc': lines( + '#include "db/db_test_util.h"', + 'namespace rocksdb {', + NAMESAKES, + 'class DBParamTest : public DBTestBase {};', + 'TEST_P(DBParamTest, Write) {', + ' this->Put(2);', + ' Flush();', + ' auto it = this->NewIterator();', + ' it->SeekToFirst();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::TEST_P')).toEqual([ + 'rocksdb::DBTestBase::Flush @0.9', + 'rocksdb::DBTestBase::NewIterator @0.9', + 'rocksdb::DBTestBase::Put @0.9', + 'rocksdb::IteratorBase::SeekToFirst @0.9', + ]); + } finally { + cg.close(); + } + }); + + it('uses the fixture declared in the test\'s own file, of two sharing its name', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/other_test_util.h': lines( + 'namespace rocksdb {', + NAMESAKES, + 'class OtherTestBase {', + ' public:', + ' MemTableRep::Iterator* NewIterator() { return nullptr; }', + '};', + '} // namespace rocksdb', + ), + 'db/a_test.cc': lines( + '#include "db/other_test_util.h"', + 'namespace rocksdb {', + 'class SharedTest : public OtherTestBase {};', + '} // namespace rocksdb', + ), + 'db/b_test.cc': lines( + '#include "db/db_test_util.h"', + 'namespace rocksdb {', + 'class SharedTest : public DBTestBase {};', + 'TEST_F(SharedTest, Seek) {', + ' auto iter = NewIterator();', + ' iter->SeekToFirst();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::TEST_F', 'db/b_test.cc')).toEqual([ + 'rocksdb::DBTestBase::NewIterator @0.9', + 'rocksdb::IteratorBase::SeekToFirst @0.9', + ]); + } finally { + cg.close(); + } + }); + + it("types a method's bare call on what the inherited method returns, past a nested class's parameter of its name", async () => { + // leveldb's SpecialEnv: a class declared in the method takes a `target` + // parameter, which is no local the bare `target()` calls. + const cg = await indexed({ + 'db/db_test.cc': lines( + 'namespace leveldb {', + 'class Env {', + ' public:', + ' virtual int NewRandomAccessFile() { return 0; }', + '};', + 'class EnvWrapper : public Env {', + ' public:', + ' Env* target() const { return target_; }', + ' private:', + ' Env* target_;', + '};', + 'class SpecialEnv : public EnvWrapper {', + ' public:', + ' int NewRandomAccessFile() override {', + ' class CountingFile {', + ' public:', + ' explicit CountingFile(Env* target) : target_(target) {}', + ' private:', + ' Env* target_;', + ' };', + ' return target()->NewRandomAccessFile();', + ' }', + '};', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::SpecialEnv::NewRandomAccessFile')).toContain('leveldb::Env::NewRandomAccessFile @0.85'); + } finally { + cg.close(); + } + }); + + it("types a template's bare call into a base it names with a using-declaration", async () => { + // protobuf's RepeatedFieldPrimitiveAccessor: `MutableRepeatedField` is + // its dependent base's, brought in by `using RepeatedFieldWrapper::MutableRepeatedField;`. + const cg = await indexed({ + 'src/google/protobuf/reflection_internal.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'template ', + 'class RepeatedField {', + ' public:', + ' void Swap(RepeatedField* other) {}', + '};', + 'template ', + 'class RepeatedFieldWrapper {', + ' public:', + ' RepeatedField* MutableRepeatedField(void* data) const { return nullptr; }', + '};', + 'template ', + 'class RepeatedFieldPrimitiveAccessor final : public RepeatedFieldWrapper {', + ' using RepeatedFieldWrapper::MutableRepeatedField;', + ' public:', + ' void Swap(void* data, void* other_data) const {', + ' MutableRepeatedField(data)->Swap(MutableRepeatedField(other_data));', + ' }', + '};', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::RepeatedFieldPrimitiveAccessor::Swap')).toContain('google::protobuf::RepeatedField::Swap @0.85'); + } finally { + cg.close(); + } + }); + + it("gives `->` on a value of a type from outside the project no project class sharing the type's name", async () => { + // protobuf's JsonTest: `ToProto(…)` returns an `absl::StatusOr`, + // whose `->` reaches the `Any` it holds, not protobuf's own `hpb::StatusOr`. + const cg = await indexed({ + 'hpb/status.h': lines( + 'namespace hpb {', + 'template ', + 'class StatusOr {', + ' public:', + ' T& value() { return value_; }', + ' private:', + ' T value_;', + '};', + '} // namespace hpb', + ), + 'src/google/protobuf/json/json_test.cc': lines( + '#include "hpb/status.h"', + 'namespace google {', + 'namespace protobuf {', + 'namespace json {', + 'class JsonTest {', + ' protected:', + ' template ', + ' absl::StatusOr ToProto(absl::string_view json) { return Proto(); }', + '};', + 'TEST_P(JsonTest, Any) {', + ' auto m = ToProto("{}");', + ' m->value();', + '}', + 'void Parsed(absl::StatusOr parsed) {', + ' parsed->value();', + '}', + '} // namespace json', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + // (The name strategies still decide, as for any receiver of an unknown type; + // in this small project the one `value` is that one.) + expect(calls(cg, 'google::protobuf::json::TEST_P')).not.toContain('hpb::StatusOr::value @0.9'); + expect(calls(cg, 'google::protobuf::json::Parsed')).not.toContain('hpb::StatusOr::value @0.9'); + } finally { + cg.close(); + } + }); + + it('leaves a TEST body, which has no fixture, to the name strategies', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/db_plain_test.cc': lines( + '#include "db/db_test_util.h"', + 'namespace rocksdb {', + 'class DBPlainTest : public DBTestBase {};', + 'TEST(DBPlainTest, Plain) {', + ' Put(1);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(resolvers(cg, 'rocksdb::TEST')).not.toContain('rocksdb::DBTestBase::Put by instance-method'); + } finally { + cg.close(); + } + }); + + it('calls a local of the name the body declares, not the fixture\'s method', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/db_local_test.cc': lines( + '#include "db/db_test_util.h"', + 'namespace rocksdb {', + 'class DBLocalTest : public DBTestBase {};', + 'TEST_F(DBLocalTest, Local) {', + ' auto Put = [](int key) {};', + ' Put(1);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(resolvers(cg, 'rocksdb::TEST_F')).not.toContain('rocksdb::DBTestBase::Put by instance-method'); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index a6bd02fb1d..5bbf1de040 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -9402,8 +9402,11 @@ function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionC if (!caller) return null; // What the calling function writes is looked up from inside it. const callers = { scope: caller.qualifiedName, file: at.filePath, line: at.line, parameters: cppTemplateParameters(caller, context) }; - const owner = cppMemberOwner(caller, context); - if (text === 'this') return owner ? { ...callers, text: `${owner}*` } : null; + if (text === 'this') { + // (A gtest `TEST_F` body's object is of a class derived from its fixture.) + const owner = cppMemberOwner(caller, context) ?? cppTestFixture(caller, at, context); + return owner ? { ...callers, text: `${owner}*` } : null; + } const name = /^(?:this\s*->\s*)?([A-Za-z_]\w*)$/.exec(text); if (name) { const decl: CppReceiverDeclaration = {}; @@ -9445,10 +9448,14 @@ function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionC } if (!/^(?:[A-Za-z_]\w*\s*::\s*)*[A-Za-z_]\w*(?:\s*<[\s\S]*>)?$/.test(callee)) return null; const spelled = stripCppTemplateArguments(callee).replace(/\s+/g, ''); - // `NewIterator(…)` in a method: its class's own or inherited method first. - const ownClass = owner && !spelled.includes('::') ? cppClassDecls(owner, context)[0] : undefined; - const own = ownClass ? cppMethodsOf(ownClass, spelled, at, context) : []; - if (own.length > 0) return cppReturnTypeOf(own, context); + // `NewIterator(…)` in a method, or in a gtest body: what the method of its + // class, or of a class it derives from, returns (cppImplicitThisMethod), + // before anything outside. It is looked up as on `this`: in a template, a + // bare call into a base that depends on its parameters compiles only + // through a using-declaration, like protobuf's `using + // RepeatedFieldWrapper::MutableRepeatedField;`. + const own = spelled.includes('::') ? undefined : cppImplicitThisMethod(spelled, 'this', caller, at, context); + if (own) return cppReturnTypeOf([own], context); // `f(…)`, `ns::f(…)`, `Class::f(…)`: looked up from the caller outwards. for (const scope of cppScopesWithin(cppParentScope(caller.qualifiedName))) { const fns = context.getNodesByQualifiedName(scope ? `${scope}::${spelled}` : spelled).filter(isCppFunctionNode); @@ -9917,6 +9924,23 @@ function matchCppDeclaredTypeCall( return cppLacksMethod(found.node, context) ? null : undefined; } +/** + * Is a C++ `->` call on `receiver` made on a value — not a pointer — of a + * type from outside the project that nothing the file sees declares + * (isCppOutsideQualifiedType)? Its `operator->` hands the call to a type the + * declared one only wraps (`absl::StatusOr`'s `Any`), so the declared + * type's last name names no class the method is in. (A standard smart + * pointer or optional is matchCppHeldTypeCall's.) + */ +function isCppArrowThroughOutsideType(receiver: string, decl: CppReceiverDeclaration, ref: UnresolvedRef, context: ResolutionContext): boolean { + if (!decl.callers || decl.shadowed || !decl.raw || decl.pointer || decl.aliased) return false; + if (cppMemberOperator(receiver, ref, context) !== '->' || isCppLibraryType(decl.raw, context)) return false; + const caller = context.getNodeById?.(ref.fromNodeId); + if (!caller) return false; + const site = decl.declared ?? cppCallSite(caller, ref, context); + return cppClassWritten(decl.raw, site, ref, context) === undefined && isCppOutsideQualifiedType(decl.raw, site, context); +} + /** * Is the C++ type `raw`, which nothing the file sees declares, written under * a namespace the project declares no type in — `absl::Status`, @@ -10048,16 +10072,33 @@ function matchCppImplicitThisCall(ref: UnresolvedRef, context: ResolutionContext const receiver = cppImplicitReceiver(ref, context); if (receiver === null) return undefined; const caller = context.getNodeById?.(ref.fromNodeId); - const owner = caller ? cppMemberOwner(caller, context) : null; - if (!caller || owner === null) return undefined; + if (!caller) return undefined; + const found = cppImplicitThisMethod(name, receiver, caller, ref, context); + // (A recursive call names the function itself, which no local shadows.) + if (!found || (receiver === 'bare' && name !== caller.name && isCppLocalName(name, caller, ref, context))) return undefined; + return { original: ref, targetNodeId: found.id, confidence: 0.9, resolvedBy: 'instance-method' }; +} + +/** + * The method a C++ call of `name` written with no receiver, or on `this`, + * reaches from `caller` by C++ lookup (matchCppImplicitThisCall): of the + * caller's class, a class it derives from, or one it is nested in. A gtest + * `TEST_F(Fixture, …)` or `TEST_P` body is a method of a class gtest derives + * from `Fixture`, so the fixture's methods are its own: rocksdb's tests call + * `DBTestBase`'s `Put(…)` and `NewIterator(…)` bare. Undefined when the + * caller is no member function, or no class on the way has a method of that + * name in the index. (Whether a local of the caller's shadows it is the + * caller's to ask.) + */ +function cppImplicitThisMethod(name: string, receiver: 'bare' | 'this', caller: Node, ref: UnresolvedRef, context: ResolutionContext): Node | undefined { + const owner = cppMemberOwner(caller, context) ?? cppTestFixture(caller, ref, context); + if (owner === null) return undefined; let found: Node | null | undefined = null; for (const scope of cppEnclosingClasses(owner, caller, context)) { found = cppScopeMethod(scope, name, receiver, ref, context); if (found !== null || receiver === 'this') break; } - // (A recursive call names the function itself, which no local shadows.) - if (!found || (receiver === 'bare' && name !== caller.name && isCppLocalName(name, caller, ref, context))) return undefined; - return { original: ref, targetNodeId: found.id, confidence: 0.9, resolvedBy: 'instance-method' }; + return found ?? undefined; } /** `this->` / `(*this).` before a member's name, with a `template` keyword after it. */ @@ -12360,6 +12401,11 @@ export function matchMethodCall( const declared = nmTimedT('mc-cppdecl', ref, () => matchCppDeclaredTypeCall(objectOrClass!, methodName!, cppDecl, ref, context)); if (declared !== undefined) return declared; + // `->` on a value of a type from outside the project reaches what its + // `operator->` returns, which the type's last name doesn't name: + // protobuf's `absl::StatusOr m; m->value()` is no member of the + // project's own `hpb::StatusOr`. The name strategies decide. + if (inferredType && isCppArrowThroughOutsideType(objectOrClass!, cppDecl, ref, context)) inferredType = null; } // A pytest test's parameter is what its fixture returns: flaskbb's // `cli_runner.invoke(…)` is click's `CliRunner`, not the project's one `invoke`. From 59238cb131f0124d51499e42bee519494b8bd27f Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Tue, 13 Oct 2026 01:31:31 -0500 Subject: [PATCH 2/2] fix(cpp): `->` on an absl::StatusOr reaches the class it holds; a using-declared member of a dependent base is found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps in C++ member lookup. In protobuf, `absl::StatusOr resolver = …; resolver->MergeFeatures(…)` was guessed by the method's name: the declared type's last name, `StatusOr`, is no class of the project's, and only std's smart pointers and optional were followed to what they hold. In rocksdb's typed_cache.h, `using BasicTypedCacheHelperFns::DownCastValue;` makes a bare `DownCastValue(v)` the base's, but a bare name skipped every base that depends on the class's template parameters. - cppHeldType (was cppStdHeldType) also reads `absl::StatusOr`, `absl::optional` and `absl::NoDestructor` as holders, and matchCppHeldTypeCall takes one whose name no project type has (isCppLibraryHolder) — std's holders as before. - cppUsingDeclares reads the `using Base::name;` declarations a class body writes. cppScopeMethod and cppInheritedDataMember look a bare name up in a dependent base when the class brings it in from that base. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/cpp-absl-holder-receiver.test.ts | 213 +++++++++++++++++++++ src/resolution/name-matcher.ts | 85 ++++++-- 3 files changed, 278 insertions(+), 21 deletions(-) create mode 100644 __tests__/cpp-absl-holder-receiver.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c0869652..8443b5e83b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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(…)` or `auto* mutex = static_cast(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`. 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 shutting_down_`, no longer links to a project method that only shares the method's name. Re-index C++ projects after upgrading. - In C++, a call written without a receiver in a gtest `TEST_F` or `TEST_P` body now links to the test fixture's method, its own or one it inherits, as C++ finds it: facebook/rocksdb's tests call `DBTestBase`'s `Put(…)`, `Flush()` and `NewIterator(…)` this way. A variable such a call initializes, like `auto iter = NewIterator(…)`, takes the type the method returns, so `iter->SeekToFirst()` reaches the iterator class. Before, those calls were guessed by name and often reached a namesake in another class, like RocksDB's `ColumnFamilyTestBase::Put` or protocolbuffers/protobuf's `EncodeDecodeTest::ExpectNoErrors`, and `iter->SeekToFirst()` reached the unrelated `MemTableRep::Iterator`. A `->` call on a value of a type from outside the project, like protobuf's `m->value()` on an `absl::StatusOr`, no longer links to a class of the project that shares the type's name, like protobuf's own `hpb::StatusOr`. Re-index C++ projects after upgrading. +- 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. - 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-absl-holder-receiver.test.ts b/__tests__/cpp-absl-holder-receiver.test.ts new file mode 100644 index 0000000000..d656db6922 --- /dev/null +++ b/__tests__/cpp-absl-holder-receiver.test.ts @@ -0,0 +1,213 @@ +/** + * A C++ `->` call on an `absl::StatusOr`, an `absl::optional` or an + * `absl::NoDestructor` calls a member of the type it holds, as one on a + * `std::unique_ptr` or `std::optional` does; and a bare call in a class + * template finds a member its dependent base declares when the class brings + * the name in with a using-declaration. + * + * In protocolbuffers/protobuf, `absl::StatusOr resolver = + * FeatureResolver::Create(…); resolver->MergeFeatures(…)` was guessed by the + * method's name: the declared type's last name, `StatusOr`, is no class of + * the project's. In facebook/rocksdb's typed_cache.h, `using + * BasicTypedCacheHelperFns::DownCastValue;` makes a bare + * `DownCastValue(v)` the base's, which C++ doesn't look in otherwise: the + * base depends on the class's template parameters. + */ +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-absl-held-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +function callable(cg: CodeGraph, qualifiedName: string): Node { + const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName); + if (!node) throw new Error(`no function or method ${qualifiedName}`); + return node; +} + +/** `calls` callees of a function or method, as `qualifiedName @confidence`. */ +function calls(cg: CodeGraph, caller: string): string[] { + return cg + .getCallees(callable(cg, caller).id) + .filter((r) => r.edge.kind === 'calls') + .map((r) => `${r.node.qualifiedName} @${(r.edge.metadata as { confidence?: number } | undefined)?.confidence}`) + .sort(); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +// Two classes with a method of each name, so a guess by the name can miss. +const RESOLVERS = lines( + 'namespace google {', + 'namespace protobuf {', + 'class FeatureResolver {', + ' public:', + ' int MergeFeatures(int parent, int child) const { return parent; }', + '};', + 'class FeatureSetResolver {', + ' public:', + ' int MergeFeatures(int parent, int child) const { return child; }', + '};', + '} // namespace protobuf', + '} // namespace google', +); + +describe('C++ absl holders and using-declared members of dependent bases', () => { + it("is the held class's method for `->` on an absl::StatusOr, absl::optional or absl::NoDestructor", async () => { + const cg = await indexed({ + 'src/feature_resolver.h': RESOLVERS, + 'src/feature_resolver_test.cc': lines( + '#include "src/feature_resolver.h"', + 'namespace google {', + 'namespace protobuf {', + 'int Merge(absl::StatusOr resolver) {', + ' return resolver->MergeFeatures(1, 2);', + '}', + 'int MergeOptional(absl::optional resolver) {', + ' return resolver->MergeFeatures(1, 2);', + '}', + 'int MergeStatic() {', + ' static absl::NoDestructor resolver;', + ' return resolver->MergeFeatures(1, 2);', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + for (const caller of ['Merge', 'MergeOptional', 'MergeStatic']) { + expect(calls(cg, `google::protobuf::${caller}`)).toEqual(['google::protobuf::FeatureResolver::MergeFeatures @0.9']); + } + } finally { + cg.close(); + } + }); + + it("is not a project's own StatusOr, nor what a StatusOr of a pointer holds", async () => { + const cg = await indexed({ + 'src/feature_resolver.h': RESOLVERS, + // protobuf's hpb declares a `StatusOr` of its own. + 'hpb/status.h': lines( + 'namespace absl {', + 'template ', + 'class StatusOr {', + ' public:', + ' T value() const;', + '};', + '} // namespace absl', + ), + 'src/use.cc': lines( + '#include "src/feature_resolver.h"', + '#include "hpb/status.h"', + 'namespace google {', + 'namespace protobuf {', + 'int Merge(absl::StatusOr resolver) {', + ' return resolver->MergeFeatures(1, 2);', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::Merge').filter((c) => c.startsWith('google::protobuf::FeatureResolver::') && c.endsWith('@0.9'))).toEqual([]); + } finally { + cg.close(); + } + }); + + it('is the member of the base the using-declaration names, of two dependent bases', async () => { + const cg = await indexed({ + 'util/runner.h': lines( + 'namespace rocksdb {', + 'template ', + 'class Second {', + ' public:', + ' void Run() {}', + '};', + 'template ', + 'class First {', + ' public:', + ' void Run() {}', + '};', + 'template ', + 'class Both : public Second, public First {', + ' public:', + ' using First::Run;', + ' void Go() { Run(); }', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Both::Go')).toEqual(['rocksdb::First::Run @0.9']); + } finally { + cg.close(); + } + }); + + it('is the dependent base member a using-declaration brings in, for a bare call and a data member', async () => { + const cg = await indexed({ + // (The base in a header of its own, where the derived class's file doesn't write its member.) + 'cache/typed_cache_base.h': lines( + 'namespace rocksdb {', + 'class Cache {', + ' public:', + ' int Insert(int key) { return key; }', + '};', + 'class Other {', + ' public:', + ' int Insert(int key) { return 0; }', + '};', + 'template ', + 'class BasicTypedCacheHelperFns {', + ' public:', + ' static TValue* DownCastValue(void* v) { return nullptr; }', + ' protected:', + ' Cache* cache_;', + '};', + '} // namespace rocksdb', + ), + 'cache/typed_cache.h': lines( + '#include "cache/typed_cache_base.h"', + 'namespace rocksdb {', + 'inline int* DownCastValue(void* v) { return nullptr; }', + 'template ', + 'class FullTypedCacheHelperFns : public BasicTypedCacheHelperFns {', + ' public:', + ' using BasicTypedCacheHelperFns::DownCastValue;', + ' using BasicTypedCacheHelperFns::cache_;', + ' static void Delete(void* v) {', + ' DownCastValue(v);', + ' }', + ' int Store(int key) {', + ' return cache_->Insert(key);', + ' }', + '};', + '} // namespace rocksdb', + ), + }); + try { + // (Without the using-declarations C++ finds the namespace's `DownCastValue`, as before.) + expect(calls(cg, 'rocksdb::FullTypedCacheHelperFns::Delete')).toEqual(['rocksdb::BasicTypedCacheHelperFns::DownCastValue @0.9']); + expect(calls(cg, 'rocksdb::FullTypedCacheHelperFns::Store')).toEqual(['rocksdb::Cache::Insert @0.9']); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 5bbf1de040..3092725511 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -9285,7 +9285,8 @@ function cppInheritedDataMember(decls: readonly Node[], name: string, context: R if (depth >= 4 || !context.getSupertypeNodes) return null; for (const decl of decls) { for (const base of context.getSupertypeNodes(decl.id)) { - if (!CPP_CLASS_KINDS.has(base.kind) || seen.has(base.qualifiedName) || isCppDependentBase(decl, base, context)) continue; + if (!CPP_CLASS_KINDS.has(base.kind) || seen.has(base.qualifiedName)) continue; + if (isCppDependentBase(decl, base, context) && !cppUsingDeclares(decl, base, name, context)) continue; seen.add(base.qualifiedName); const inherited = cppInheritedDataMember([base], name, context, depth + 1, seen); if (inherited) return inherited; @@ -9313,7 +9314,7 @@ function cppTestFixture(caller: Node, ref: UnresolvedRef, context: ResolutionCon /** * The type `*x` has when `x` has the type `type`: what a pointer points at, - * or what a standard smart pointer or optional holds. Null for any other type. + * or what a smart pointer, an optional or an `absl::StatusOr` holds (cppHeldType). Null for any other type. */ function cppPointee(type: CppWrittenType): CppWrittenType | null { const text = type.text.replace(/&+\s*$/, '').trim(); @@ -9324,7 +9325,7 @@ function cppPointee(type: CppWrittenType): CppWrittenType | null { else if (ch === '<') depth--; else if (ch === '*' && depth === 0) return { ...type, text: text.slice(0, i).trim() }; } - const held = cppStdHeldType(text); + const held = cppHeldType(text); return held ? { ...type, text: held } : null; } @@ -9437,8 +9438,8 @@ function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionC // `std::make_unique(…)`: the smart pointer it makes. const made = /^(?:::\s*)?(?:std|absl)\s*::\s*make_(unique|shared)\s*<([\s\S]+)>$/.exec(callee); if (made) return { ...callers, text: `std::${made[1]}_ptr<${made[2]!.trim()}>` }; - // `std::unique_ptr(…)`: the smart pointer or optional it makes. - if (cppStdHeldType(callee)) return { ...callers, text: callee }; + // `std::unique_ptr(…)`: the holder it makes (cppHeldType). + if (cppHeldType(callee)) return { ...callers, text: callee }; // `recv.method(…)`, `recv->method(…)`: the method of the receiver's class. const member = /^(.+?)\s*(\.|->)\s*([A-Za-z_]\w*)$/.exec(callee); if (member) { @@ -9548,7 +9549,7 @@ function cppAutoLocalType( function isCppTemplateDependentType(type: CppWrittenType, context: ResolutionContext): boolean { if (type.parameters.size === 0) return false; if (isCppTemplateParameterType(type)) return true; - const held = cppStdHeldType(type.text); + const held = cppHeldType(type.text); if (held) return isCppTemplateDependentType({ ...type, text: held }, context); // (A trait of one, `std::conditional_t`, too: it names another type.) return (isCppLibraryType(type.text, context) || cppTypeSegments(type.text)?.[0] === 'std') && @@ -9799,19 +9800,31 @@ function isCppLibraryType(raw: string, context: ResolutionContext): boolean { return !declared.some((n) => n.qualifiedName === spelled || n.qualifiedName.endsWith(`::${spelled}`)); } -/** Standard-library types whose `->` reaches the value they hold. */ -const CPP_STD_HOLDERS: ReadonlySet = new Set(['unique_ptr', 'shared_ptr', 'optional']); +/** Library types whose `->` reaches the value they hold. */ +const CPP_HOLDERS: ReadonlySet = new Set(['std::unique_ptr', 'std::shared_ptr', 'std::optional', 'absl::optional', 'absl::StatusOr', 'absl::NoDestructor']); -/** The type a standard smart pointer or optional written as `raw` holds (`Iterator` for `std::unique_ptr`), or null. */ -function cppStdHeldType(raw: string): string | null { +/** + * The type a standard smart pointer, an optional, an `absl::StatusOr` or an + * `absl::NoDestructor` written as `raw` holds (`Iterator` for + * `std::unique_ptr`), or null. + */ +function cppHeldType(raw: string): string | null { const holder = cppTypeSegments(raw); - if (holder?.length !== 2 || holder[0] !== 'std' || !CPP_STD_HOLDERS.has(holder[1]!)) return null; + if (holder?.length !== 2 || !CPP_HOLDERS.has(holder.join('::'))) return null; return cppTemplateArguments(raw)?.[0] ?? null; } +/** Whether the holder `raw` names (cppHeldType) is the library's: no type of the project's has its name. */ +function isCppLibraryHolder(raw: string, context: ResolutionContext): boolean { + const spelled = cppTypeSegments(raw)!.join('::'); + return !cppTypesNamed(cppLastSegment(spelled), context).some((n) => n.qualifiedName === spelled || n.qualifiedName.endsWith(`::${spelled}`)); +} + /** - * A `->` call on a C++ receiver declared as a standard smart pointer or - * optional (`std::unique_ptr iter; iter->Valid()`) calls a member of + * A `->` call on a C++ receiver declared as a standard smart pointer, an + * optional or an `absl::StatusOr` (`std::unique_ptr iter; + * iter->Valid()`, protobuf's `absl::StatusOr resolver; + * resolver->MergeFeatures(…)`) calls a member of * the type it holds. That type is looked up as C++ looks it up where the call * is written (cpp-supertypes.ts): the scopes around the call, the file's * `using`s, and aliases followed. Looking it up by its last name instead @@ -9829,8 +9842,8 @@ function cppStdHeldType(raw: string): string | null { * doesn't name): the call goes on as before. */ function matchCppHeldTypeCall(raw: string, methodName: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined { - const held = cppStdHeldType(raw); - if (!held || !isCppLibraryType(raw, context)) return undefined; + const held = cppHeldType(raw); + if (!held || !isCppLibraryHolder(raw, context)) return undefined; const caller = context.getNodeById?.(ref.fromNodeId); if (!caller || isCppPointerType(held)) return undefined; if (isCppLibraryType(held, context)) return null; @@ -9929,8 +9942,9 @@ function matchCppDeclaredTypeCall( * type from outside the project that nothing the file sees declares * (isCppOutsideQualifiedType)? Its `operator->` hands the call to a type the * declared one only wraps (`absl::StatusOr`'s `Any`), so the declared - * type's last name names no class the method is in. (A standard smart - * pointer or optional is matchCppHeldTypeCall's.) + * type's last name names no class the method is in. (A smart pointer, an + * optional or an `absl::StatusOr` holding one of the project's classes is + * matchCppHeldTypeCall's.) */ function isCppArrowThroughOutsideType(receiver: string, decl: CppReceiverDeclaration, ref: UnresolvedRef, context: ResolutionContext): boolean { if (!decl.callers || decl.shadowed || !decl.raw || decl.pointer || decl.aliased) return false; @@ -10275,7 +10289,7 @@ function cppScopeMethod(scope: CppClassScope, name: string, receiver: 'bare' | ' for (const decl of scope.decls) { for (const base of context.getSupertypeNodes(decl.id)) { if (!CPP_CLASS_KINDS.has(base.kind)) continue; - if (receiver === 'bare' && isCppDependentBase(decl, base, context)) continue; + if (receiver === 'bare' && isCppDependentBase(decl, base, context) && !cppUsingDeclares(decl, base, name, context)) continue; const inherited = cppMethodOf(base, name, ref, context, 1, new Set([scope.qualifiedName])); if (!inherited) continue; // Its class's other overloads, which the arguments may fit better @@ -10363,10 +10377,37 @@ function cppBaseSpecifiers(decl: Node, context: ResolutionContext): string[] { return specs; } +/** Per context: a C++ class declaration's id → the `using Base::name;` declarations its body writes, as [base, name]. */ +const CPP_USING_MEMBERS = new WeakMap>>(); + +/** + * Whether the C++ class declaration `decl` brings `name` in from its base + * `base` with a using-declaration: rocksdb's `FullTypedCacheHelperFns` + * writes `using BasicTypedCacheHelperFns::DownCastValue;`, and a bare + * `DownCastValue(v)` in it then finds the base's, though the base depends on + * the class's template parameters (isCppDependentBase). + */ +function cppUsingDeclares(decl: Node, base: Node, name: string, context: ResolutionContext): boolean { + let memo = CPP_USING_MEMBERS.get(context); + if (!memo) CPP_USING_MEMBERS.set(context, (memo = new Map())); + let usings = memo.get(decl.id); + if (!usings) { + usings = []; + const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/) ?? []; + const body = lines.slice(decl.startLine - 1, decl.endLine ?? decl.startLine).map((l) => cppCodeOf(l) ?? '').join('\n'); + for (const m of body.matchAll(/\busing\s+(?!namespace\b)((?:::\s*)?[A-Za-z_][\w:]*(?:\s*<[^;{}]*>)?)\s*::\s*([A-Za-z_]\w*)\s*;/g)) { + usings.push([cppLastSegment(stripCppTemplateArguments(m[1]!).trim()), m[2]!]); + } + memo.set(decl.id, usings); + } + return usings.some(([of, member]) => of === base.name && member === name); +} + /** * Whether `base`, a base of the C++ class declaration `decl`, depends on the * template parameters of `decl` or a class around it: `template - * class Foo : public Base`. A bare name is not looked up in such a base. + * class Foo : public Base`. A bare name is not looked up in such a base, + * unless the class brings it in with a using-declaration (cppUsingDeclares). */ function isCppDependentBase(decl: Node, base: Node, context: ResolutionContext): boolean { const parameters = cppTemplateParameters(decl, context); @@ -11211,6 +11252,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { clearCppRawStringMemos(context); CPP_CLASSES_IN_FILE.delete(context); CPP_BASE_SPECS.delete(context); + CPP_USING_MEMBERS.delete(context); CPP_MEMBER_MACRO_ARGS.delete(context); CPP_LOOP_ELEMENTS.delete(context); CPP_AUTO_LOCALS.delete(context); @@ -12517,8 +12559,9 @@ export function matchMethodCall( // followed. if (isUndeclaredTypeName(cppDecl.written!, ref, context)) return null; } - // A `->` call on a standard smart pointer or optional the calling - // function or its class declares is a call on the type it holds: + // A `->` call on a smart pointer, an optional or an `absl::StatusOr` + // (cppHeldType) the calling function or its class declares is a call on + // the type it holds: // rocksdb's `std::unique_ptr iter; iter->Valid()` reached // `ArenaWrappedDBIter::Valid` by the method's name. if (ref.language === 'cpp' && cppDecl.callers && !cppDecl.aliased && !cppDecl.shadowed && !cppDecl.pointer &&