diff --git a/CHANGELOG.md b/CHANGELOG.md index 071dd2931..26d23d097 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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 and C++, a header or source file named like a test, such as protocolbuffers/protobuf's `conformance_test.h` and `test_runner.h`, now counts as part of the code that `#include`s it, directly or through other headers. Before, calls from that code into it were dropped, so protobuf's conformance suites' `suite_.ReportFailure(…)` and `RunValidInputTest(…)` calls, and its unit tests' calls to the shared `TestUtil` helpers, linked to nothing. A test file that only tests include is still kept out of reach of the rest of your code. Re-index C and C++ projects after upgrading. - In C++, a class, struct or method now keeps the namespaces and the class it is declared in when the parser misreads something earlier in the file, such as an unknown macro in front of a member or in a class header. Before, everything after the misread code could lose them, or land inside a class it isn't in: in protocolbuffers/protobuf, `FieldDescriptor` was indexed without `google::protobuf::`, and in RocksDB, the `Opts` struct declared inside a cache table class was indexed outside it, so finding a class's base classes and the methods called on its objects fell back to guessing by name. Methods that had come loose from their class are its members again, and classes the misread code had hidden, like leveldb's POSIX file and environment classes, are now indexed. 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 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 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. 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 6528db81b..1c46a5d0b 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 { cppIncludedFile, cppIncluders } from './cpp-includers'; import { isTestPath } from '../search/query-utils'; import { isMinifiedContent } from '../extraction/generated-detection'; @@ -8388,6 +8388,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; } /** @@ -8421,37 +8431,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); } /** @@ -8484,7 +8593,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); @@ -8495,11 +8606,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; } } @@ -8517,13 +8633,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); } } @@ -9038,6 +9157,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); @@ -10274,6 +10394,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