From 15570369eb651f21abd6e6b3c1c9aba9386f78ce Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Tue, 6 Oct 2026 22:04:50 -0500 Subject: [PATCH] fix(cpp): a receiver declared as a std or other outside type with a lowercase name calls that type's own member A C++ call on a variable, parameter or member the calling function or its class declares as a namespace-qualified type the project does not declare, with a lowercase name (std::string, std::vector, absl::flat_hash_set, ...), no longer falls through to Strategy 3's guess by the method's name. leveldb's `std::string saved_key_; saved_key_.clear()` reached Slice::clear, protobuf's `std::string proto; proto.append(...)` LeftoverBuffer::append. The gate applies to `.` on a value and `->` through a raw pointer; `->` on a smart pointer, iterator or optional still reaches the element type. It skips declarations it cannot trust: read from outside the caller's function and class (including a class nested in the caller's), contradicted by the call (`.` on a pointer), shadowed by an unreadable re-declaration (`auto`, range-for, structured binding), unqualified, with unbalanced `<>` (the tail of a multi-line declaration), or a `_t` name (scalars, tags, traits like std::conditional_t). A `std::` name counts as the project's only when the project declares it in `std` (vendored googletest's testing::internal::string). Receiver inference now blanks comments before matching declarations, so `// ... non-null imm_` no longer types leveldb's imm_ as `null`. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/cpp-library-type-receiver.test.ts | 522 ++++++++++++++++++++ src/resolution/name-matcher.ts | 181 ++++++- 3 files changed, 680 insertions(+), 24 deletions(-) create mode 100644 __tests__/cpp-library-type-receiver.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fe1144fe4..87524b06e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C, C++ and Objective-C, a struct or class member declared as a pointer, reference, array or function, like `SharedState* shared;`, `jv elements[];` or `virtual Status Put(…) = 0;`, no longer makes the struct look like it inherits from the member's type. Before, `codegraph_explore` and impact listed those types as base classes, every struct holding a pointer to a type as one of its subclasses, and a method of that struct as an override of the type's method with the same name. Real base classes like `class Derived : public Base`, Objective-C superclasses and Go's embedded fields are unchanged. Re-index C, C++ and Objective-C projects after upgrading. - In C++, a call on a variable, parameter or member declared through a `typedef` or `using` alias now links to the method of the type the alias names, with the alias looked up the way C++ does it: in the calling function, its class and the classes that class inherits from, then the namespaces around it. In google/leveldb, `table_.Insert(buf)` on a `Table table_;` declared next to `typedef SkipList Table;` now reaches `SkipList::Insert` instead of the unrelated `HandleTable::Insert`, and `Table::Iterator iter(&table_);` reaches `SkipList::Iterator`'s constructor. A class that only shares the alias's name is no longer taken for it, and when the aliased type has no such method, as with an alias of `std::vector` or of a template parameter, no method is guessed from the receiver's name. Re-index C++ projects after upgrading. - In C++, a base class like `Message` in `class DynamicMessage final : public Message` now links to the class C++ itself would pick: the one declared in the namespaces around the class, or brought in by the file's `using` declarations and namespace aliases, and the class a `typedef` or `using` alias names. Before, a base class was matched by its name alone, so it could land on a class of the same name in another namespace or another language: in protocolbuffers/protobuf, `DynamicMessage` derived from the JSON parser's internal `Message` class, and in google/leveldb and RocksDB, iterators derived from an unrelated nested `Iterator` class such as the skip list's. The wrong base showed up in `codegraph_explore`'s type hierarchy and in impact, and tied the base's methods to the wrong overrides. A template parameter used as a base no longer links to a class that shares its name, and a base written from the global scope, like `public ::testing::Test`, now links to its class. Re-index C++ projects after upgrading. +- In C++, a call on a variable, parameter or member declared as a standard-library or other outside type with a lowercase name, like `std::string`, `std::vector`, `std::set` or `absl::flat_hash_set`, no longer links to a project method that only shares the method's name. In google/leveldb, `saved_key_.clear()` on a `std::string saved_key_;` reached `Slice::clear`, and in protocolbuffers/protobuf, `proto.append(…)` on a `std::string` reached an unrelated `LeftoverBuffer::append`. The same goes for `->` through a raw pointer like `std::string* out`, while `->` through a smart pointer, iterator or optional still links to the type it holds. Comments also no longer pass for a variable's declaration, so more calls now link to the method of the type the variable is really declared with. 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 Go, an interface that embeds another, like gin's `IRouter` embedding `IRoutes`, now links to it, so `codegraph_explore` and impact show the embedded interface as its base. Before, every embedded interface was dropped. An embedded field written through another package of your project, like `*refresh.Discovery` or `storage.Appender`, or with type arguments, like `List[int]`, now links too. One from the standard library or another module, like `io.Closer` or `sync.Mutex`, links to nothing rather than to a project type that shares its name, and a type-set constraint like `~int | ~float64` is not an embedding. An embedded type written without a package now always links to its own package's type: before, etcd's `Client`, which embeds its own `Lease` interface, was linked to a `Lease` struct in another package. Re-index Go projects after upgrading. - In Go, working out which structs implement an interface now counts the methods of the interfaces it embeds, and the methods a struct gets from the types it embeds. Before, only each type's own methods counted: etcd's `AuthReadTx`, which embeds `UnsafeAuthReader` and adds `RLock` and `RUnlock`, was listed as implemented by every read-write lock in the project, an interface that only embeds others had no implementations at all, and a struct that gets its methods from an embedded base, like prometheus's service discoveries embedding `refresh.Discovery` or gin's `Engine` embedding `RouterGroup`, was missing. `codegraph_explore`, impact and the type hierarchy now list the right implementations, and a call through the interface reaches a method the embedding struct overrides, like gin's `Engine.Use`. Re-index Go projects after upgrading. diff --git a/__tests__/cpp-library-type-receiver.test.ts b/__tests__/cpp-library-type-receiver.test.ts new file mode 100644 index 000000000..70d368dcd --- /dev/null +++ b/__tests__/cpp-library-type-receiver.test.ts @@ -0,0 +1,522 @@ +/** + * A C++ receiver declared as a type from outside the project with a lowercase + * name — `std::string`, `std::vector<…>`, `absl::flat_hash_set<…>` — calls + * that type's own member, not a project method. + * + * google/leveldb's `DBIter` declares `std::string saved_key_;` and calls + * `saved_key_.clear()`. Receiver inference read the type as `string`, which + * names no project class, and the check for a type from outside the project + * only caught capitalized names (`List`, `String`), so the call fell through + * to a guess by the method's name: `Slice::clear`, the one project `clear`. + * protocolbuffers/protobuf had about 900 such edges (`std::string proto; + * proto.append(…)` reached `LeftoverBuffer::append`). + * + * When the calling function or its class declares the receiver that way, a + * `.` call — or a `->` through a raw pointer, `std::string* out` — gets no + * project edge. A `->` on a smart pointer, iterator or optional still goes to + * the element type, and a call that contradicts its declaration (`.` on a + * pointer) keeps its guess, since that declaration is another variable's. + * Comments no longer pass for declarations in the scan: `// … non-null imm_` + * read as `imm_`'s declaration, with the type `null`. + */ +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-library-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +function callable(cg: CodeGraph, qualifiedName: string): Node { + const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName); + if (!node) throw new Error(`no function or method ${qualifiedName}`); + return node; +} + +/** `calls` callees of a function or method, as `qualifiedName`. */ +function calls(cg: CodeGraph, caller: string): string[] { + return cg + .getCallees(callable(cg, caller).id) + .filter((r) => r.edge.kind === 'calls') + .map((r) => r.node.qualifiedName) + .sort(); +} + +const lines = (...l: string[]): string => [...l, ''].join('\n'); + +/** leveldb's Slice and its namesakes elsewhere: every method a library type's call could be mistaken for. */ +const PROJECT = { + 'include/leveldb/slice.h': lines( + 'namespace leveldb {', + 'class Slice {', + ' public:', + ' const char* data() const { return data_; }', + ' unsigned size() const { return size_; }', + ' void clear() { size_ = 0; }', + ' const char* begin() const { return data_; }', + ' void remove_prefix(unsigned n) { size_ -= n; }', + ' private:', + ' const char* data_;', + ' unsigned size_;', + '};', + '} // namespace leveldb', + ), + 'util/buffers.h': lines( + 'namespace leveldb {', + 'class LeftoverBuffer {', + ' public:', + ' void append(const char* s, unsigned n) {}', + '};', + 'class Arena {', + ' public:', + ' void insert(int block) {}', + '};', + 'class Cache {', + ' public:', + ' void reset() {}', + '};', + 'class Iterator {', + ' public:', + ' void SeekToFirst() {}', + '};', + '} // namespace leveldb', + ), +}; + +describe('a C++ receiver declared as a library type with a lowercase name', () => { + it('reproduction: `std::string saved_key_; saved_key_.clear()` is not Slice::clear', async () => { + const cg = await indexed({ + ...PROJECT, + 'db/db_iter.cc': lines( + '#include ', + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'class DBIter {', + ' public:', + ' void Next();', + ' private:', + ' std::string saved_key_; // == current key when direction_==kReverse', + '};', + 'void DBIter::Next() {', + ' saved_key_.clear();', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::DBIter::Next')).toEqual([]); + expect(cg.getCallers(callable(cg, 'leveldb::Slice::clear').id).filter((r) => r.edge.kind === 'calls')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a local, a parameter, a raw pointer, nested template arguments and an abseil container', async () => { + const cg = await indexed({ + ...PROJECT, + 'db/encode.cc': lines( + '#include ', + '#include ', + '#include "absl/container/flat_hash_set.h"', + '#include "include/leveldb/slice.h"', + '#include "util/buffers.h"', + 'namespace leveldb {', + 'void Encode(std::string* dst, const Slice& value) {', + ' dst->clear();', + ' dst->append(value.data(), value.size());', + '}', + 'std::string Render(const std::vector>& files) {', + ' std::string proto;', + ' proto.append("x", 1);', + ' absl::flat_hash_set seen;', + ' seen.insert(1);', + ' const char* first = proto.data();', + ' return files.begin() == files.end() ? proto : std::string(first);', + '}', + '} // namespace leveldb', + ), + }); + try { + // `value` is a Slice; `dst`, `proto`, `seen` and `files` are not. + expect(calls(cg, 'leveldb::Encode')).toEqual(['leveldb::Slice::data', 'leveldb::Slice::size']); + expect(calls(cg, 'leveldb::Render')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a `std::` name beside a project alias of the same name elsewhere', async () => { + // facebook/rocksdb vendors googletest, whose `typedef ::std::string + // string;` in `testing::internal` is a project type named `string`. + const cg = await indexed({ + ...PROJECT, + 'third_party/gtest.h': lines( + '#include ', + 'namespace testing {', + 'namespace internal {', + 'typedef ::std::string string;', + '} // namespace internal', + '} // namespace testing', + ), + 'tools/trim.cc': lines( + '#include ', + '#include "include/leveldb/slice.h"', + 'void Trim(std::string& text) {', + ' text.clear();', + '}', + ), + }); + try { + expect(calls(cg, 'Trim')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a smart pointer: `.` is its own member, `->` reaches the element type', async () => { + const cg = await indexed({ + ...PROJECT, + 'db/scan.cc': lines( + '#include ', + '#include "util/buffers.h"', + 'namespace leveldb {', + 'void Scan(Iterator* raw) {', + ' std::unique_ptr iter(raw);', + ' iter->SeekToFirst();', + ' iter.reset();', + '}', + '} // namespace leveldb', + ), + }); + try { + // `iter.reset()` is unique_ptr's, not Cache::reset. + expect(calls(cg, 'leveldb::Scan')).toEqual(['leveldb::Iterator::SeekToFirst']); + } finally { + cg.close(); + } + }); + + it('a std trait or member type names another type, maybe the project\'s', async () => { + // facebook/rocksdb's compression.cc: `std::conditional_t dict` is a Slice when the flag is off. + const cg = await indexed({ + ...PROJECT, + 'util/compression.cc': lines( + '#include ', + '#include ', + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'template ', + 'unsigned DictSize(std::conditional_t dict) {', + ' return dict.size();', + '}', + 'void Drop(const std::vector& slices) {', + ' std::vector::value_type first = slices[0];', + ' first.remove_prefix(1);', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::DictSize')).toEqual(['leveldb::Slice::size']); + expect(calls(cg, 'leveldb::Drop')).toEqual(['leveldb::Slice::remove_prefix']); + } finally { + cg.close(); + } + }); + + it('a project type with a lowercase name is still the project\'s', async () => { + const cg = await indexed({ + ...PROJECT, + 'util/arena.h': lines( + 'namespace util {', + 'class arena {', + ' public:', + ' void* allocate(unsigned n) { return 0; }', + '};', + '} // namespace util', + ), + 'util/use.cc': lines( + '#include "util/arena.h"', + 'void Use(util::arena& a) {', + ' a.allocate(8);', + '}', + ), + }); + try { + expect(calls(cg, 'Use')).toEqual(['util::arena::allocate']); + } finally { + cg.close(); + } + }); +}); + +describe('a declaration that is not the receiver\'s does not rule a guess out', () => { + it('a range-for variable, or an `auto` one, hides an earlier library-typed one of the same name', async () => { + const cg = await indexed({ + ...PROJECT, + 'db/shadow.cc': lines( + '#include ', + '#include ', + '#include "include/leveldb/slice.h"', + '#include "util/buffers.h"', + 'namespace leveldb {', + 'void Trim(const std::vector& slices) {', + ' std::string key;', + ' for (const Slice& key : slices) {', + ' key.remove_prefix(1);', + ' }', + '}', + 'void Flush(Cache* caches) {', + ' { std::string buf; buf.clear(); }', + ' auto buf = caches[0];', + ' buf.reset();', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::Trim')).toEqual(['leveldb::Slice::remove_prefix']); + // The block's `buf.clear()` is the string's; the `auto` one's `reset` is a guess left alone. + expect(calls(cg, 'leveldb::Flush')).toEqual(['leveldb::Cache::reset']); + } finally { + cg.close(); + } + }); + + it('a call that contradicts the declaration it found: `.` on what reads as a pointer', async () => { + // As in fmtlib/fmt's bigint, where `carry = (upper * bigits_[i] …` reads + // as a declaration `upper* bigits_[…]`: the product here reads as + // `absl::kWeightScale* key[…]`, but `key.size()` is a `.` call. + const cg = await indexed({ + ...PROJECT, + 'util/weight.cc': lines( + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'unsigned Weight(const Slice& key) {', + ' unsigned w = absl::kWeightScale * key[0];', + ' return w + key.size();', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::Weight')).toEqual(['leveldb::Slice::size']); + } finally { + cg.close(); + } + }); + + it('a constant of a project class read as a type, and a scalar', async () => { + const cg = await indexed({ + ...PROJECT, + 'util/mask.cc': lines( + '#include ', + '#include "include/leveldb/slice.h"', + '#define SLICE_AT(var, list, i) const Slice& var = list[i]', + 'namespace leveldb {', + 'unsigned Mask(const Slice& key) {', + // reads like `Slice::kMask& key[…]` + ' unsigned m = Slice::kMask & key[0];', + ' return m + key.size();', + '}', + 'void Visit(const Slice* slices) {', + ' std::size_t key = 0;', + ' {', + // the macro declares the `key` the call is made on + ' SLICE_AT(key, slices, 0);', + ' key.remove_prefix(1);', + ' }', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::Mask')).toEqual(['leveldb::Slice::size']); + expect(calls(cg, 'leveldb::Visit')).toEqual(['leveldb::Slice::remove_prefix']); + } finally { + cg.close(); + } + }); + + it('a member of another class in the file', async () => { + // google/leveldb's db/log_test.cc: StringDest's `std::string contents_` + // sits above StringSource, whose own `Slice contents_` comes after its methods. + const cg = await indexed({ + ...PROJECT, + 'db/log_test.cc': lines( + '#include ', + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'class StringDest {', + ' public:', + ' std::string contents_;', + '};', + 'class StringSource {', + ' public:', + ' void Skip(unsigned n) { contents_.remove_prefix(n); }', + ' private:', + ' Slice contents_;', + '};', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::StringSource::Skip')).toEqual(['leveldb::Slice::remove_prefix']); + } finally { + cg.close(); + } + }); + + it('the last line of a declaration that starts on an earlier one', async () => { + // facebook/rocksdb's BlobFileReader: `autovector>>& blob_reqs` starts the next. + const cg = await indexed({ + 'util/autovector.h': lines( + 'namespace rocksdb {', + 'template ', + 'class autovector {', + ' public:', + ' unsigned size() const { return 0; }', + '};', + '} // namespace rocksdb', + ), + 'db/blob/blob_file_reader.cc': lines( + '#include ', + '#include "util/autovector.h"', + 'namespace rocksdb {', + 'class BlobFileReader {', + ' public:', + ' unsigned MultiGetBlob(autovector>>& blob_reqs);', + '};', + 'unsigned BlobFileReader::MultiGetBlob(autovector>>& blob_reqs) {', + ' return blob_reqs.size();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::BlobFileReader::MultiGetBlob')).toEqual(['rocksdb::autovector::size']); + } finally { + cg.close(); + } + }); + + it('a member of a class nested in the caller\'s', async () => { + // facebook/rocksdb's MultiScan: its nested MultiScanIterator declares a + // `scan_opts_` of its own, above MultiScan's. + const cg = await indexed({ + 'include/rocksdb/multi_scan.h': lines( + '#include ', + 'namespace rocksdb {', + 'struct ScanOptions {};', + 'class MultiScanArgs {', + ' public:', + ' int GetScanRanges() const { return 0; }', + '};', + 'class MultiScan {', + ' public:', + ' class MultiScanIterator {', + ' private:', + ' const std::vector& scan_opts_;', + ' };', + ' int begin() { return scan_opts_.GetScanRanges(); }', + ' private:', + ' const MultiScanArgs scan_opts_;', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::MultiScan::begin')).toEqual(['rocksdb::MultiScanArgs::GetScanRanges']); + } finally { + cg.close(); + } + }); +}); + +describe('comments in the declaration scan', () => { + it('a comment that mentions the receiver is not its declaration', async () => { + // google/leveldb's db_impl.cc: `// … since there is a non-null imm_` + // above `imm_->…` read as `imm_`'s declaration, with the type `null`. + const cg = await indexed({ + 'db/memtable.h': lines( + 'namespace leveldb {', + 'class MemTable {', + ' public:', + ' unsigned ApproximateMemoryUsage() { return 0; }', + '};', + 'class Arena {', + ' public:', + ' unsigned ApproximateMemoryUsage() { return 0; }', + '};', + '} // namespace leveldb', + ), + 'db/db_impl.h': lines( + '#include "db/memtable.h"', + 'namespace leveldb {', + 'class DBImpl {', + ' public:', + ' unsigned MemoryUsage();', + ' private:', + ' MemTable* imm_;', + '};', + '} // namespace leveldb', + ), + 'db/db_impl.cc': lines( + '#include "db/db_impl.h"', + 'namespace leveldb {', + 'unsigned DBImpl::MemoryUsage() {', + ' // Count the memtable being compacted, since there is a non-null imm_', + ' return imm_->ApproximateMemoryUsage();', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::DBImpl::MemoryUsage')).toEqual(['leveldb::MemTable::ApproximateMemoryUsage']); + } finally { + cg.close(); + } + }); + + it('a parameter after a leading `/* static */` is the caller\'s own', async () => { + const cg = await indexed({ + ...PROJECT, + 'util/status.cc': lines( + '#include ', + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'class Status {', + ' public:', + ' static bool Check(const std::string& msg);', + '};', + '/* static */ bool Status::Check(const std::string& msg) {', + ' return msg.begin() != nullptr;', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::Status::Check')).toEqual([]); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 08dc5575f..b00d118eb 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 { cppAliasedTypeName, isCppPointerType, resolveCppAliasedType } from './cpp-type-aliases'; +import { cppAliasedTypeName, cppTypeSegments, isCppPointerType, resolveCppAliasedType } from './cpp-type-aliases'; import { isTestPath } from '../search/query-utils'; import { isMinifiedContent } from '../extraction/generated-detection'; import { getCargoWorkspaceCrateMap } from './frameworks/cargo-workspace'; @@ -8302,6 +8302,16 @@ interface CppReceiverDeclaration { pointer?: boolean; /** The type is one of the project's class templates, whose specializations may add members. */ classTemplate?: boolean; + /** The declared type as written (`std::vector*`). */ + raw?: string; + /** The declaration is the calling function's own, or a member of its class (isCppCallersDeclaration). */ + callers?: boolean; + /** + * On its way back to the declaration the scan passed one of the receiver + * it could not read a type from — `auto x = Make();`, `for (Foo& x : xs)`, + * `auto [x, y] = …` — so the declaration it found is an earlier variable's. + */ + shadowed?: boolean; } /** @@ -8335,37 +8345,136 @@ function cppDeclaredType( return aliased ? cppAliasedTypeName(aliased) : null; } +const CPP_CLASSES_IN_FILE = new WeakMap>(); + +/** The classes, structs and unions a C or C++ file declares. */ +function cppClassesIn(file: string, context: ResolutionContext): Node[] { + let memo = CPP_CLASSES_IN_FILE.get(context); + if (!memo) CPP_CLASSES_IN_FILE.set(context, (memo = new Map())); + let classes = memo.get(file); + if (!classes) { + classes = context.getNodesInFile(file).filter((n) => n.kind === 'class' || n.kind === 'struct' || n.kind === 'union'); + memo.set(file, classes); + } + return classes; +} + /** - * Is line `line` of `file` — where `text` declares a C++ receiver at column - * `at` — the calling function's own code, or a member declaration of the - * caller's class? Receiver inference reads back to the top of the file and - * through the header, so a declaration it finds elsewhere (an earlier - * function, another class in the header, a comment) may be another variable - * of the same name. + * Is line `line` of `file`, which declares a C++ receiver, the calling + * function's own code, or a member declaration of the caller's class? + * Receiver inference reads back to the top of the file and through the + * header, so a declaration it finds elsewhere — an earlier function, another + * class in the header, a class nested in the caller's (rocksdb's + * `MultiScan::MultiScanIterator` has a `scan_opts_` of its own) — may be + * another variable of the same name. (The scan has blanked comments.) */ -function isCppCallersDeclaration(file: string, line: number, text: string, at: number, ref: UnresolvedRef, context: ResolutionContext): boolean { - if (/^\s*(?:\/\/|\/\*|\*)/.test(text)) return false; - const comment = text.indexOf('//'); - if (comment >= 0 && comment < at) return false; +function isCppCallersDeclaration(file: string, line: number, ref: UnresolvedRef, context: ResolutionContext): boolean { const caller = context.getNodeById?.(ref.fromNodeId); if (!caller || (caller.kind !== 'method' && caller.kind !== 'function')) return false; if (file === ref.filePath && line >= caller.startLine && line <= ref.line) return true; const cut = caller.qualifiedName.lastIndexOf('::'); if (cut < 0) return false; - return context.getNodesByQualifiedName(caller.qualifiedName.slice(0, cut)).some((owner) => - (owner.kind === 'class' || owner.kind === 'struct' || owner.kind === 'union') && owner.filePath === file && - line >= owner.startLine && line <= (owner.endLine ?? owner.startLine)); + let innermost: Node | undefined; + for (const cls of cppClassesIn(file, context)) { + if (line < cls.startLine || line > (cls.endLine ?? cls.startLine)) continue; + if (!innermost || cls.startLine >= innermost.startLine) innermost = cls; + } + return innermost?.qualifiedName === caller.qualifiedName.slice(0, cut); } -/** Is the C++ member call at `ref` written `receiver.method(…)`, not through `->`? */ -function isCppDotCall(receiver: string, ref: UnresolvedRef, context: ResolutionContext): boolean { +/** + * The operator the C++ member call at `ref` is written with: `.` for + * `receiver.method(…)`, `->` through a pointer, iterator or smart pointer, or + * null when the source doesn't show it. + */ +function cppMemberOperator(receiver: string, ref: UnresolvedRef, context: ResolutionContext): '.' | '->' | null { const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/); const at = lines?.[ref.line - 1]?.slice(ref.column); - if (!lines || !at?.startsWith(receiver)) return false; + if (!lines || !at?.startsWith(receiver)) return null; let rest = at.slice(receiver.length); // `symbols_by_parent_` ending its line, `.insert(…)` starting the next. for (let next = ref.line; !rest.trim() && next < Math.min(lines.length, ref.line + 3); next++) rest = lines[next]!; - return /^\s*\./.test(rest); + const op = /^\s*(\.|->)/.exec(rest)?.[1]; + return op === '.' || op === '->' ? op : null; +} + +/** Is the C++ member call at `ref` written `receiver.method(…)`, not through `->`? */ +function isCppDotCall(receiver: string, ref: UnresolvedRef, context: ResolutionContext): boolean { + return cppMemberOperator(receiver, ref, context) === '.'; +} + +/** Kinds that declare a C or C++ type name. */ +const CPP_TYPE_KINDS: ReadonlySet = new Set(['class', 'struct', 'union', 'enum', 'interface', 'type_alias']); + +/** The C and C++ types the project declares under `name`. */ +function cppTypesNamed(name: string, context: ResolutionContext): Node[] { + return context.getNodesByName(name).filter((n) => (n.language === 'cpp' || n.language === 'c') && CPP_TYPE_KINDS.has(n.kind)); +} + +/** + * Is a C++ declared type, as written (`std::vector*`, `const + * absl::flat_hash_set&`), one from outside the project with a lowercase + * name, as the standard library's and abseil's are? (isUndeclaredTypeName + * takes the capitalized ones.) The name is qualified, by namespaces that are + * no project type's (`Foo::kMask & key` reads like a declaration of `key`), + * and the project declares no type of that name: in `std` itself for a + * `std::` name (googletest's `testing::internal::string` is no + * `std::string`), anywhere for any other, since a namespace a macro opens + * (fmt's `FMT_BEGIN_NAMESPACE`) is in no qualified name. An unqualified name + * is not known to be std's: fmt's `using namespace std;` is inside `namespace + * adl { … }`, and its `basic_string_view` is fmt's own. Nor is a type whose + * `>`s outnumber its `<`s: the end of a declaration begun on an earlier line + * (`autovector>>& reqs`). A + * `_t` name has no members of its own: it is a scalar (`std::size_t`, so + * the declaration is another variable's), a tag, or a trait that names + * another type (rocksdb's `std::conditional_t + * dict`). + */ +function isCppLibraryType(raw: string, context: ResolutionContext): boolean { + if ((raw.match(//g)?.length ?? 0)) return false; + const segments = cppTypeSegments(raw); + if (!segments || segments.length < 2) return false; + const name = segments[segments.length - 1]!; + if (!/^[a-z]/.test(name) || /_t$/.test(name)) return false; + if (segments.slice(0, -1).some((s) => cppTypesNamed(s, context).length > 0)) return false; + const declared = cppTypesNamed(name, context); + if (segments[0] !== 'std') return declared.length === 0; + const spelled = segments.join('::'); + return !declared.some((n) => n.qualifiedName === spelled || n.qualifiedName.endsWith(`::${spelled}`)); +} + +/** + * A C++ source line as code, for reading declarations: null for a line of a + * comment (`// …`, ` * …`), else the line with its comments blanked, columns + * kept. Receiver inference read leveldb's `// … non-null imm_` as a + * declaration of `imm_` with the type `null`. + */ +function cppCodeOf(line: string): string | null { + if (/^\s*(?:\/\/|\*)/.test(line)) return null; + let code = line.replace(/\/\*.*?\*\//g, (c) => ' '.repeat(c.length)); + // A block comment opening here runs past the line. + const open = code.indexOf('/*'); + if (open >= 0) code = code.slice(0, open); + const comment = code.indexOf('//'); + return comment >= 0 ? code.slice(0, comment) : code; +} + +/** + * Does `line` declare the receiver in a form the declarator pattern doesn't + * read: a range-`for` variable (`for (const Foo& x : xs)`) or a structured + * binding (`auto [x, y] = …`)? + */ +function cppRebindsReceiver(line: string, escapedReceiver: string): boolean { + return new RegExp(String.raw`\bfor\s*\(.*\b${escapedReceiver}\s*:(?!:)`).test(line) || + new RegExp(String.raw`\bauto\s*&{0,2}\s*\[[^\]]*\b${escapedReceiver}\b[^\]]*\]`).test(line); +} + +/** Record the C++ declaration receiver inference settles on. */ +function noteCppDeclaration(found: CppReceiverDeclaration | undefined, raw: string, callers: boolean): void { + if (!found) return; + found.raw = raw; + found.callers = callers; + found.pointer = isCppPointerType(raw); } /** @@ -8398,7 +8507,9 @@ function inferCppReceiverType( const declaratorRegex = buildDeclaratorRegex(escapedReceiver); for (let i = callLineIndex; i >= 0; i--) { - const line = lines[i]; + const text = lines[i]; + if (!text || !receiverPattern.test(text)) continue; + const line = cppCodeOf(text); if (!line || !receiverPattern.test(line)) continue; const declaratorMatch = line.match(declaratorRegex); @@ -8409,11 +8520,16 @@ function inferCppReceiverType( // from the initializer (call return type / construction) (#645). const initType = inferCppAutoInitializerType(line, receiverName, ref, context, depth); if (initType) return initType; - // No usable initializer on this line — keep scanning earlier ones. + // No usable initializer on this line — keep scanning earlier ones, + // where what is declared is an earlier variable. + if (found) found.shadowed = true; } else if (normalized) { - const inCallerScope = isCppCallersDeclaration(ref.filePath, i + 1, line, declaratorMatch.index ?? 0, ref, context); + const inCallerScope = isCppCallersDeclaration(ref.filePath, i + 1, ref, context); + noteCppDeclaration(found, declaratorMatch[1]!, inCallerScope); return cppDeclaredType(declaratorMatch[1]!, normalized, inCallerScope, ref, context, found); } + } else if (found && cppRebindsReceiver(line, escapedReceiver)) { + found.shadowed = true; } } @@ -8431,13 +8547,16 @@ function inferCppReceiverType( if (!headerLines) continue; for (let i = 0; i < headerLines.length; i++) { - const line = headerLines[i]!; - if (!receiverPattern.test(line)) continue; + const text = headerLines[i]!; + if (!receiverPattern.test(text)) continue; + const line = cppCodeOf(text); + if (!line) continue; const declaratorMatch = line.match(declaratorRegex); if (!declaratorMatch) continue; const normalized = normalizeCppTypeName(declaratorMatch[1] ?? ''); if (normalized && normalized !== 'auto') { - const inCallerScope = isCppCallersDeclaration(headerPath, i + 1, line, declaratorMatch.index ?? 0, ref, context); + const inCallerScope = isCppCallersDeclaration(headerPath, i + 1, ref, context); + noteCppDeclaration(found, declaratorMatch[1]!, inCallerScope); return cppDeclaredType(declaratorMatch[1]!, normalized, inCallerScope, ref, context, found); } } @@ -8952,6 +9071,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { CPP_NS_MACROS.delete(context); CPP_NS_FRAMES.delete(context); CPP_NS_ALIASES.delete(context); + CPP_CLASSES_IN_FILE.delete(context); SOLIDITY_SUPERS.delete(context); DECLARED_SUPERS.delete(context); INHERITED_METHODS.delete(context); @@ -10188,6 +10308,19 @@ export function matchMethodCall( // followed. if (isUndeclaredTypeName(cppDecl.written!, ref, context)) return null; } + // A C++ receiver the calling function or its class declares as a type + // from outside the project with a lowercase name calls that type's own + // member, through `.` or a raw pointer's `->`: leveldb's `std::string + // data_; data_.data()` went to `Slice::data`, protobuf's `std::string + // proto; proto.append(…)` to `LeftoverBuffer::append`. Not so for `->` + // on such a value — an iterator, smart pointer or optional hands the call + // to its element type — nor for a call that doesn't fit the declaration + // (`.` on a pointer), which then is another variable's. + if (ref.language === 'cpp' && cppDecl.callers && !cppDecl.aliased && !cppDecl.shadowed && + cppMemberOperator(objectOrClass!, ref, context) === (cppDecl.pointer ? '->' : '.') && + isCppLibraryType(cppDecl.raw!, context)) { + return null; + } } // Go 2-hop field chain `base.field.Method` (#1276): the base's type comes