diff --git a/CHANGELOG.md b/CHANGELOG.md index ab3c4e089f..c60ff68f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -479,7 +479,24 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In this fork, the two C++ changes above run in the native resolver. A call on a variable whose type is a template parameter, like `T* iter`, stays unlinked rather than reaching a class that happens to share the parameter's name. A call stays linked as before when the class has no method of that name but one of its base classes comes from an outside library other than the standard library, such as googletest's `testing::Test`. - 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 variable, parameter or member whose declaration puts a qualifier or an annotation around its name is now read for its type, so calls on it link to the declared class's methods: a `const` after the `*` or `&`, like facebook/rocksdb's `Iterator* const iter = iter_guard.get();` or google/leveldb's `Env* const env_;`, a type written before its `const`, like `Slice const& key`, a nullability annotation after the `*`, like protocolbuffers/protobuf's `io::EpsCopyOutputStream* PROTOBUF_NONNULL stream`, and a thread-safety annotation after the name, like LevelDB's `MemTable* imm_ GUARDED_BY(mutex_);`. Before, such a declaration was skipped, or the annotation was taken for its type, so the call took the type of an earlier variable of the same name or was guessed by the method's name: RocksDB's `iter->SeekToFirst()` reached the unrelated `ArenaWrappedDBIter`, and LevelDB's `env_->NewWritableFile(…)` reached `InMemoryEnv`. Re-index C++ projects after upgrading. +- In C++, a call on a name a structured binding declares now links to the method of the type C++ gives it. That covers a range-based `for` over pairs or a map, like facebook/rocksdb's `for (auto& [cfh, iter] : cfh_iter_pairs_) { iter->Valid(); }`, and a declaration like protocolbuffers/protobuf's `auto [dst_ext, is_new] = Insert(…);`. A call on what a `std::function` or another function wrapper returns, like `create_iterator()->Valid()` on a `std::function`, links the same way, and so does a call on a class member whose type is written on the line above its name. Before, these calls were guessed by the method's name, and calls on a library container declared that way, like a `std::priority_queue` member's `pop()`, reached a project class's method of that 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. +- In C and C++, a call or a type name no longer links to something only another source file can see: a class, struct or alias a `.c` or `.cc` file declares for itself, its members, or anything inside that file's unnamed `namespace { … }`. In facebook/rocksdb, other test files' bare `Put(…)`, `Flush()` and `dbfull()` calls had linked to the methods of a test fixture declared in `column_family_test.cc`, and calls on an `Env*` had linked to a test-only environment class in `memenv.cc`. When a header and a source file each declare a class of the same name, like RocksDB's `SharedState`, a call now links to the header's. A source file's own declarations are still found from the file itself, from a file that `#include`s it, and from a header it includes below them. Re-index C and C++ projects after upgrading. +- In C++, a call to an overloaded method now links to the overload its arguments fit: by how many there are, and then by their types. That covers a call on a variable or member of a known class, like facebook/rocksdb's `db->Get(ro, key, &value)` and `db->Put(write_opts, key, value)`, as well as a call written without a receiver, like `Insert(ch)` on a `char ch`. Before, a call on a variable always linked to the first overload declared, so RocksDB's convenience overloads of `DB::Get`, `DB::Put` and `DB::NewIterator` and protocolbuffers/protobuf's `io::Printer::Print`, `Emit` and `Annotate` had almost no callers, and a call without a receiver picked by argument count alone. A literal, a variable the calling function declares, `&variable`, `container[i]` and the result of a call are read for their types; a parameter's default value is read from the method's declaration in its class, and comments in argument lists are skipped. Re-index C++ projects after upgrading. +- In C++, a nested class defined outside the class it belongs to, like google/leveldb's `struct SkipList::Node { … }` or facebook/rocksdb's `struct BlockBasedTable::Rep { … }`, is now indexed under its own name, `SkipList::Node`, with its methods and members. Before, it was indexed as an unnamed class, so it was missing from search and `codegraph_explore`, its methods belonged to no class you could name, and a call like `x->Next(level)` on a `Node*` was matched by the method's name alone. Creating an object, as in `new log::Writer(…)` or `Writer w(…)`, now links to the class C++ itself finds for the type as written there: leveldb's `new log::Writer(…)` in `DBImpl` methods no longer links to `DBImpl`'s own `Writer`, RocksDB's tests' `Random rnd(301)` links to RocksDB's `Random` instead of googletest's, an object created through an alias links to the class the alias names, and one links to its class rather than to the class's constructor. An object of a template parameter's type, or of a type the function names with its own `using`, like protocolbuffers/protobuf's `using Map = std::map<…>`, no longer links to a project class that only shares the name. Re-index C++ projects after upgrading. +- In C++, a call whose receiver's type can't be read no longer links to a method of a class nested in the class the receiver's name suggests, or of a class whose name only contains that name. Before, such a guess took any method of the call's name whose qualified name contained the class's name: protocolbuffers/protobuf's `descriptor->file()` in a template reached `Descriptor::ExtensionRange::file`, and facebook/rocksdb's `CompositeEnv::IsInstanceOf(name)` reached `CompositeEnvWrapper::IsInstanceOf`. A call written through a class, like `PessimisticTransaction::PopSavePoint()` in an override, now links to the method that class inherits, here `TransactionBaseImpl::PopSavePoint`. Re-index C++ projects after upgrading. +- In C++, an alias that names a class through a class derived from the one that declares it, like protocolbuffers/protobuf's `using ConformanceRequestSetting = BinaryAndJsonConformanceSuite::ConformanceRequestSetting;`, now leads to that class, `ConformanceTestSuite::ConformanceRequestSetting`. Before, an alias was followed only to a class declared under exactly the name it spells, so objects created through this one linked to the alias rather than the class. Re-index C++ projects after upgrading. +- In C++, a name written in a googletest `TEST_F` or `TEST_P` body now finds the test fixture's nested types, those of its base classes included, before anything in the surrounding namespace, as C++ does for the member function such a body is: facebook/rocksdb's `new TestUserDefinedIndexFactory()` links to the class nested in the fixture's base. A class declared inside a test body, or inside any function, is now only found from that function, so the local classes many RocksDB tests each declare under one name, like `FakeWF` or `MyManager`, no longer stand in for one another, and a call like `fwf->SetIOError(true)` reaches the `FakeWF` its own test declares. Re-index C++ projects after upgrading. +- In C++, a declaration continued on a line that starts with `*`, like facebook/rocksdb's `const auto& el =` over `*reinterpret_cast*>(jhandle);`, is now read, so calls such as `el->OnFlushCompleted(…)` link to `EventListener`'s methods; such a line used to be skipped as part of a comment. Code commented out in a `/* … */` block, and the text of a string literal, like protocolbuffers/protobuf's `[[deprecated("Please use SpaceUsedLong() instead")]]`, are no longer read as declarations, which had sent a call to the wrong method or to none. Re-index C++ projects after upgrading. +- In C++, a call with a comparison or a shift among its arguments, like protocolbuffers/protobuf's `p->Emit({{"declare", has_bit_word_index < 0 ? … : ""}, …}, "…")` or `WriteVarint(num << 3, s)`, is now read with the right number of arguments, so it links to the overload that fits rather than one taking fewer. A parameter or variable whose type has template arguments is now read even after another such parameter on the same line, like facebook/rocksdb's `const autovector& cf_ids`, or with empty ones, like fmt's `parse_context<>& ctx`: calls on it link to that class's methods, and calls on a `std::vector` declared that way no longer link to a project class's method of the same name. Re-index C++ projects after upgrading. +- In C++, a call passing `nullptr` now links to the overload that takes a `std::nullptr_t`, as C++ picks it, rather than one taking a pointer, and a call no longer links to an overload declared `= delete`, which no call that compiles can reach: protocolbuffers/protobuf's `ptr.reset(nullptr)` on an `Arena::UniquePtr` reaches `reset(std::nullptr_t)`, and `field.Add("foo")` on a `RepeatedPtrField` reaches `Add(Element&&)` instead of the deleted `Add(const Element&)`. Re-index C++ projects after upgrading. +- In C++, a variable or parameter declared over more than one line, with its type begun on a line above or alone on the line above its name, now has its type read, so calls on it link to that class's methods: protocolbuffers/protobuf's `extension.number()` on a `const google::protobuf::internal::ExtensionIdentifier<` … `>& extension` parameter reaches protobuf's `ExtensionIdentifier`, not hpb's, and facebook/rocksdb's `cf_sv_pairs.size()` on an `autovector<…>` written above `cf_sv_pairs;` reaches `autovector`. Calls on a standard-library container declared that way no longer link to a project class's method of the same name. Re-index C++ projects after upgrading. +- In C++, a variable initialized from a call of a member function template with explicit template arguments now has the type that template returns for them, so calls on it link to that class's methods: protocolbuffers/protobuf's `const auto& rep = reflection->GetRaw>(…)` is a `RepeatedPtrField`, and facebook/rocksdb's `auto bbto = factory->GetOptions()` a `BlockBasedTableOptions`. Re-index C++ projects after upgrading. - In this fork, the two C++ changes above also run in the native resolver. A call on a variable, member or chain whose type it cannot read, such as one a template parameter decides (`T*`, `std::shared_ptr`), an alias of a trait of a parameter, or the result of a function template whose return type is its parameter, stays unlinked rather than being guessed by the receiver's name, and a chain of up to four calls (`cfd->current()->storage_info()->LevelFiles()`) is read from its declarations, not only its first inner call; a longer one is looked up by the last name, as before. A chained call spelled with `.` on a returned `std::unique_ptr` or `std::optional` is that type's own member, not a method of what it holds (`->` still reaches that); and a variable declared in the initializer of a `for`, `if` or `switch` statement is seen before a same-named member the class inherits. +- In C++, a pointer declared `__restrict`, a pointer to a `const` class template and a member followed by an annotation macro that takes no arguments now have their type read, so calls on them link to that class's methods. A quoted `(` or `)` in a template argument no longer cuts the type short, a deleted overload whose declaration runs over many lines is no longer taken, a bare call no longer takes an inherited or fixture method's return type when a local lambda or callable of that name shadows it, and a type named through a base class is found only in a file the code includes. +- In this fork, the C++ changes above differ from upstream's in two more ways. A call whose receiver's type can't be read, like a call on an `auto` local whose initializer names a function declared nowhere, or on a name that is neither a member nor a local the method declares, stays unlinked; upstream guesses the class from the receiver's name. And object creation sees a header under a test directory, like nlohmann/json's `tests/abi/include/nlohmann/json_v3_10_5.hpp`, an old release of the library, only from inside that directory's tree or through an `#include`. That copy spells out `namespace nlohmann {`, which the library's own headers open with a macro, so the docs examples' and benchmarks' `json j(…)` and `json::json_pointer p(…)` had linked to its classes, 233 times on nlohmann/json. 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. diff --git a/README.md b/README.md index d8c5106719..0f5a36266d 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates. ## About this fork -This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`df3ce972`](https://github.com/colbymchenry/codegraph/commit/df3ce972) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests. +This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`1753472a`](https://github.com/colbymchenry/codegraph/commit/1753472a) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests. The fork publishes no releases. The install scripts, npm package, badges and `codegraph upgrade` further down this page install **upstream's** releases. To run the fork, build it from source (below). @@ -154,6 +154,7 @@ Call resolution the fork adds: - C++ `->` calls on a `std::unique_ptr`, `std::shared_ptr` or `std::optional` reach the type it holds, looked up through the caller's enclosing classes and their bases, its namespaces, `using` directives and aliases. A held type the project does not declare leaves the call unresolved, as a plain pointer to one does; upstream guesses a callee from the receiver's name there. A partial class template specialization is indexed as an anonymous class, so a call to a member only it declares stays unresolved. - C++ calls on an `auto` local, on a chained call and on a data member of the caller's class (one a base class declares, or a gtest `TEST_F` / `TEST_P` fixture's) take the type its declaration writes, as upstream's do. Where that type cannot be read (a template parameter's, a `std::shared_ptr` of one, a trait of one, what a function template returns), the call stays unlinked, as does a local of a type the file does not declare; upstream guesses a callee from the receiver's name at 0.7. A chain of up to four calls is read from its declarations, where upstream reads its first inner call; a longer one is looked up by the last name, as before. - C++ namespace aliases and declarations in macro-opened namespaces are looked up only in the caller's include closure; upstream pools them across all files. +- C++ object creation sees a header under a test directory, such as nlohmann/json's old-release copy in `tests/abi/`, only from inside that directory's tree or through an `#include`; upstream sees every header. - C# namespace `using` directives and `using` aliases apply only inside their enclosing namespace, not to sibling namespaces in the same file (`using static` is still file-wide, as upstream). - A Java field declared with a qualified type (`outside.Repository`) keeps its qualifier, so calls through it never resolve to an unrelated project class with the same simple name. - Python `super().method()` resolves along the class's C3 method resolution order, starting after the class itself. Upstream resolves it like `self.method()` and drops the link only when it lands on the calling method. @@ -188,7 +189,7 @@ Call resolution the fork adds: ### Measured results -Upstream `main` at `34dd3d2f` against the fork at `f045e5f7`, each run on Node.js 24.21.0 and on Bun 1.4.3. The fork contains upstream through `df3ce972`; upstream's seven later commits are C++ fixes, which touch these corpora only through CPython's C files. Measured 2026-10-13 on a 16-vCPU WSL2 host over seven corpora, with the arms in mirrored order; index figures are the median of two runs and sync figures the median of four. Upstream refuses to start on Bun, which reports itself as Node 26, because upstream blocks Node 25 and newer. With `CODEGRAPH_ALLOW_UNSAFE_NODE=1` it runs; those numbers, the method and the graph sizes are in [`docs/benchmarks/fork-vs-upstream-node-bun-2026-10-13.md`](docs/benchmarks/fork-vs-upstream-node-bun-2026-10-13.md), which also compares this run with the 2026-10-12 one. The scripts that produced them are in [`scripts/benchmarks/runtime/`](scripts/benchmarks/runtime/). +Upstream `main` at `34dd3d2f` against the fork at `f045e5f7`, each run on Node.js 24.21.0 and on Bun 1.4.3. The fork at `f045e5f7` contained upstream through `df3ce972`; upstream's seven later commits are C++ fixes, which touch these corpora only through CPython's C files. Measured 2026-10-13 on a 16-vCPU WSL2 host over seven corpora, with the arms in mirrored order; index figures are the median of two runs and sync figures the median of four. Upstream refuses to start on Bun, which reports itself as Node 26, because upstream blocks Node 25 and newer. With `CODEGRAPH_ALLOW_UNSAFE_NODE=1` it runs; those numbers, the method and the graph sizes are in [`docs/benchmarks/fork-vs-upstream-node-bun-2026-10-13.md`](docs/benchmarks/fork-vs-upstream-node-bun-2026-10-13.md), which also compares this run with the 2026-10-12 one. The scripts that produced them are in [`scripts/benchmarks/runtime/`](scripts/benchmarks/runtime/). ![Fork against upstream CodeGraph on n8n and the command line: the fork indexes n8n 1.2 times faster, syncs an edited file 3.0 times faster, answers a warm question 3.3 times faster and uses 32% less memory while indexing. The fork is better in 60 of 76 comparisons, about the same in 4 and worse in 12.](assets/fork-vs-upstream-performance.svg?v=2) diff --git a/__tests__/cpp-absl-holder-receiver.test.ts b/__tests__/cpp-absl-holder-receiver.test.ts new file mode 100644 index 0000000000..95a2b6071f --- /dev/null +++ b/__tests__/cpp-absl-holder-receiver.test.ts @@ -0,0 +1,217 @@ +/** + * 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);', + '}', + 'int MergeThroughPointer(absl::StatusOr resolver) {', + ' return resolver->MergeFeatures(1, 2);', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::Merge')).toEqual([]); + expect(calls(cg, 'google::protobuf::MergeThroughPointer')).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/__tests__/cpp-alias-inherited-member.test.ts b/__tests__/cpp-alias-inherited-member.test.ts new file mode 100644 index 0000000000..12774fa205 --- /dev/null +++ b/__tests__/cpp-alias-inherited-member.test.ts @@ -0,0 +1,151 @@ +/** + * A C++ alias can name a class one of a class's bases declares: + * protocolbuffers/protobuf's conformance suites write `using + * ConformanceRequestSetting = BinaryAndJsonConformanceSuite::ConformanceRequestSetting;`, + * a class their base `ConformanceTestSuite` declares. Following the alias + * looked the class up only by the qualified name as spelled, which no class + * has, so constructions through the alias stopped at the alias and calls on + * such a variable lost their class. The aliased type is now looked up the way + * a declared type is, a class's members through its bases too. + */ +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'; + +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-alias-base-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** What a function's or method's `kind` edges reach, as `qualifiedName @confidence`. */ +function targets(cg: CodeGraph, qualifiedName: string, kind: 'calls' | 'instantiates'): string[] { + const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!; + return cg.getOutgoingEdgesFrom([from.id], [kind]) + .map((e) => `${cg.getNode(e.target)?.qualifiedName} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`) + .sort(); +} + +const NS = 'google::protobuf::conformance'; +const SUITE = lines( + 'namespace google {', + 'namespace protobuf {', + 'namespace conformance {', + 'class ConformanceTestSuite {', + ' public:', + ' class ConformanceRequestSetting {', + ' public:', + ' ConformanceRequestSetting(int level, const char* name) {}', + ' const char* GetTestName() const { return ""; }', + ' };', + '};', + '} // namespace conformance', + '} // namespace protobuf', + '} // namespace google', +); + +describe('C++ aliases of a class a base declares', () => { + it('reach the class through an alias spelled through the derived class', async () => { + const cg = await indexed({ + 'conformance/conformance_test.h': SUITE, + 'conformance/binary_json_conformance_suite.h': lines( + '#include "conformance/conformance_test.h"', + 'namespace google {', + 'namespace protobuf {', + 'namespace conformance {', + 'class BinaryAndJsonConformanceSuite : public ConformanceTestSuite {};', + 'template ', + 'class BinaryAndJsonConformanceSuiteImpl {', + ' public:', + ' void RunTest();', + ' private:', + ' using ConformanceRequestSetting =', + ' BinaryAndJsonConformanceSuite::ConformanceRequestSetting;', + '};', + '} // namespace conformance', + '} // namespace protobuf', + '} // namespace google', + ), + 'conformance/binary_json_conformance_suite.cc': lines( + '#include "conformance/binary_json_conformance_suite.h"', + 'namespace google {', + 'namespace protobuf {', + 'namespace conformance {', + 'template ', + 'void BinaryAndJsonConformanceSuiteImpl::RunTest() {', + ' ConformanceRequestSetting setting(1, "x");', + ' setting.GetTestName();', + '}', + '// An alias the function declares itself is not indexed: read from the function.', + 'void RunLocal() {', + ' using ConformanceRequestSetting = BinaryAndJsonConformanceSuite::ConformanceRequestSetting;', + ' ConformanceRequestSetting local(3, "w");', + ' local.GetTestName();', + '}', + '} // namespace conformance', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + const run = `${NS}::BinaryAndJsonConformanceSuiteImpl::RunTest`; + expect(targets(cg, run, 'instantiates')).toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting @0.9`]); + expect(targets(cg, run, 'calls').filter((t) => t.includes('GetTestName'))) + .toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting::GetTestName @0.9`]); + expect(targets(cg, `${NS}::RunLocal`, 'instantiates')).toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting @0.9`]); + expect(targets(cg, `${NS}::RunLocal`, 'calls').filter((t) => t.includes('GetTestName'))) + .toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting::GetTestName @0.9`]); + } finally { + cg.close(); + } + }); + + it("reach the class through an alias a derived class writes with the base's name alone", async () => { + const cg = await indexed({ + 'conformance/conformance_test.h': SUITE, + 'conformance/text_format_suite.h': lines( + '#include ', + '#include "conformance/conformance_test.h"', + 'namespace google {', + 'namespace protobuf {', + 'namespace conformance {', + 'class TextFormatSuite : public ConformanceTestSuite {', + ' public:', + ' using Setting = ConformanceRequestSetting;', + ' using Name = std::string;', + ' void Run() {', + ' Setting setting(2, "y");', + ' setting.GetTestName();', + ' Name name("z");', + ' }', + '};', + '} // namespace conformance', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + const run = `${NS}::TextFormatSuite::Run`; + // (An alias of a type from outside the project still makes the alias.) + expect(targets(cg, run, 'instantiates')) + .toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting @0.9`, `${NS}::TextFormatSuite::Name @0.9`]); + expect(targets(cg, run, 'calls').filter((t) => t.includes('GetTestName'))) + .toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting::GetTestName @0.9`]); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-auto-local-receiver.test.ts b/__tests__/cpp-auto-local-receiver.test.ts index 19ab514fe9..55a66e7431 100644 --- a/__tests__/cpp-auto-local-receiver.test.ts +++ b/__tests__/cpp-auto-local-receiver.test.ts @@ -518,10 +518,9 @@ describe('what keeps the call as it was', () => { ), }); try { - // Not the shared_ptr member's type, which has no SetName. What `CheckedCast` - // returns is a template parameter, so the local's type is unknown and the fork, which guesses - // no method by the name of a receiver it cannot type, links nothing (upstream: SetName @0.7). - expect(calls(cg, 'rocksdb::Rename').filter((c) => c.includes('SetName'))).toEqual([]); + // Not the shared_ptr member's type, which has no SetName: + // the type `CheckedCast()` returns. + expect(calls(cg, 'rocksdb::Rename').filter((c) => c.includes('SetName'))).toEqual(['rocksdb::test::ChanglingMergeOperator::SetName @0.9']); } finally { cg.close(); } diff --git a/__tests__/cpp-binding-and-wrapper-receivers.test.ts b/__tests__/cpp-binding-and-wrapper-receivers.test.ts new file mode 100644 index 0000000000..eae9019559 --- /dev/null +++ b/__tests__/cpp-binding-and-wrapper-receivers.test.ts @@ -0,0 +1,237 @@ +/** + * A C++ call on a name a structured binding declares, or on what a + * `std::function` returns, links to the method of the type C++ gives it. + * + * facebook/rocksdb's MultiCfIteratorImpl writes `for (auto& [cfh, iter] : + * cfh_iter_pairs_) { iter->Valid(); … }` over a member declared, across two + * lines, as a `std::vector>>`; a binding's names had no type, so the calls + * were guessed by the method's name. `iter` is the pair's second element, a + * `std::unique_ptr`, and `->` reaches `Iterator`'s method. A map's + * elements pair its key and value; a declaration's (`auto [a, b] = f();`) + * are what its initializer returns, a `std::pair` or `std::tuple`. And a + * call through a `std::function`, or the other wrappers, has + * the type the function type returns. + */ +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-binding-')); + 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` edges of a function or method, as `qualifiedName @confidence`, one per call. */ +function calls(cg: CodeGraph, caller: string): string[] { + return cg + .getOutgoingEdgesFrom([callable(cg, caller).id], ['calls']) + .map((e) => `${cg.getNode(e.target)?.qualifiedName} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`) + .sort(); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +// `Valid` is also a namesake class's, so a guess by the name can miss. +const ITERATORS = lines( + '#include ', + 'namespace rocksdb {', + 'class ColumnFamilyHandle {};', + 'class IteratorBase {', + ' public:', + ' virtual bool Valid() const { return false; }', + '};', + 'class Iterator : public IteratorBase {};', + 'class MemTableIterator {', + ' public:', + ' bool Valid() const { return true; }', + '};', + '} // namespace rocksdb', +); + +describe('C++ structured bindings and function wrappers as receivers', () => { + it("is the pair element a range-for's binding names, over a member declared across two lines", async () => { + const cg = await indexed({ + 'include/rocksdb/iterator.h': ITERATORS, + 'db/multi_cf_iterator_impl.h': lines( + '#include "include/rocksdb/iterator.h"', + 'namespace rocksdb {', + 'class MultiCfIteratorImpl {', + ' public:', + ' void SeekToFirst() {', + ' for (auto& [cfh, iter] : cfh_iter_pairs_) {', + ' iter->Valid();', + ' }', + ' }', + ' private:', + ' std::vector>>', + ' cfh_iter_pairs_;', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::MultiCfIteratorImpl::SeekToFirst')).toEqual(['rocksdb::IteratorBase::Valid @0.9']); + } finally { + cg.close(); + } + }); + + it("is a map's value, and a declaration's pair or tuple element", async () => { + const cg = await indexed({ + 'include/rocksdb/iterator.h': ITERATORS, + 'db/use.cc': lines( + '#include "include/rocksdb/iterator.h"', + 'namespace rocksdb {', + 'std::pair MakePair() { return {nullptr, nullptr}; }', + 'std::tuple MakeTuple() { return {0, nullptr, false}; }', + 'void ByName(const absl::flat_hash_map& iters) {', + ' for (const auto& [name, iter] : iters) {', + ' iter->Valid();', + ' }', + '}', + 'void ByPair() {', + ' auto [cfh, iter] = MakePair();', + ' iter->Valid();', + '}', + 'void ByTuple() {', + ' const auto& [count, iter, done] = MakeTuple();', + ' iter->Valid();', + '}', + '} // namespace rocksdb', + ), + }); + try { + for (const caller of ['ByName', 'ByPair', 'ByTuple']) { + expect(calls(cg, `rocksdb::${caller}`).filter((c) => c.includes('Valid'))).toEqual(['rocksdb::IteratorBase::Valid @0.9']); + } + } finally { + cg.close(); + } + }); + + it("is what a std::function's function type returns, called or as an auto local's initializer", async () => { + const cg = await indexed({ + 'include/rocksdb/iterator.h': ITERATORS, + 'db/scan.cc': lines( + '#include "include/rocksdb/iterator.h"', + 'namespace rocksdb {', + 'void Scan(std::function create_iterator) {', + ' create_iterator()->Valid();', + ' auto* it = create_iterator();', + ' it->Valid();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Scan')).toEqual(['rocksdb::IteratorBase::Valid @0.85', 'rocksdb::IteratorBase::Valid @0.9']); + } finally { + cg.close(); + } + }); + + it("gives no type from another function's binding, or a template's arguments that aren't a pair's", async () => { + const cg = await indexed({ + 'include/rocksdb/iterator.h': ITERATORS, + 'db/use.cc': lines( + '#include "include/rocksdb/iterator.h"', + 'namespace rocksdb {', + 'MemTableIterator* iter = nullptr;', + 'std::pair MakePair() { return {nullptr, nullptr}; }', + '// Members in another order than the template parameters.', + 'template struct Duo { B second; A first; };', + 'Duo MakeDuo() { return {}; }', + 'void Earlier() {', + ' auto [cfh, iter] = MakePair();', + '}', + 'void Later() {', + ' iter->Valid();', + '}', + 'void Unpack() {', + ' auto [p, q] = MakeDuo();', + ' p->Valid();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Later').filter((c) => c === 'rocksdb::IteratorBase::Valid @0.9')).toEqual([]); + expect(calls(cg, 'rocksdb::Unpack').filter((c) => c === 'rocksdb::IteratorBase::Valid @0.9')).toEqual([]); + } finally { + cg.close(); + } + }); + + it("reads a member's type from the line above only when that line is a type", async () => { + const cg = await indexed({ + 'include/rocksdb/iterator.h': ITERATORS, + 'db/reader.h': lines( + '#include "include/rocksdb/iterator.h"', + 'namespace rocksdb {', + 'class Reader {', + ' public:', + ' bool Check() const { return it_->Valid(); }', + ' private:', + ' bool flag_ = true;', + ' // A default member initializer continued on the next line.', + ' bool ready_ = flag_ &&', + ' it_;', + ' MemTableIterator* it_ = nullptr;', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Reader::Check')).toEqual(['rocksdb::MemTableIterator::Valid @0.9']); + } finally { + cg.close(); + } + }); + + it("gives no type for a call outside the loop, or a binding of something that isn't a pair or tuple", async () => { + const cg = await indexed({ + 'include/rocksdb/iterator.h': ITERATORS, + 'db/use.cc': lines( + '#include "include/rocksdb/iterator.h"', + 'namespace rocksdb {', + 'struct Point { Iterator* first; Iterator* second; };', + 'void After(const std::vector>& pairs, MemTableIterator* iter) {', + ' for (const auto& [cfh, iter] : pairs) {', + ' }', + ' iter->Valid();', + '}', + 'void Aggregate(Point point) {', + ' auto [x, y] = point;', + ' y->Valid();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::After').filter((c) => c === 'rocksdb::IteratorBase::Valid @0.9')).toEqual([]); + expect(calls(cg, 'rocksdb::Aggregate').filter((c) => c === 'rocksdb::IteratorBase::Valid @0.9')).toEqual([]); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-block-comment-lines.test.ts b/__tests__/cpp-block-comment-lines.test.ts new file mode 100644 index 0000000000..fde985067e --- /dev/null +++ b/__tests__/cpp-block-comment-lines.test.ts @@ -0,0 +1,209 @@ +/** + * The C++ declaration scans read source a line at a time and took any line + * starting with `*` for the middle of a block comment, and any other line + * for code. So a declaration continued onto a line starting with `*` was + * missed — `Widget` above `*widget_;` — and code commented out inside + * `/* … *\/` without leading stars was read as a declaration. Block comments + * are now found by reading the file once, as raw strings are, and blanked, + * with the contents of string literals, in every scan that reads lines. + */ +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'; + +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-block-comments-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** What a function's or method's calls reach, as `qualifiedName @confidence`. */ +function callees(cg: CodeGraph, qualifiedName: string): string[] { + const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!; + return cg.getOutgoingEdgesFrom([from.id], ['calls']) + .map((e) => `${cg.getNode(e.target)?.qualifiedName} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`) + .sort(); +} + +/** The same, with how each call was resolved. */ +function resolutions(cg: CodeGraph, qualifiedName: string): string[] { + const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!; + return cg.getOutgoingEdgesFrom([from.id], ['calls']) + .map((e) => { + const meta = e.metadata as { confidence?: number; resolvedBy?: string } | undefined; + return `${cg.getNode(e.target)?.qualifiedName} @${meta?.confidence} ${meta?.resolvedBy}`; + }) + .sort(); +} + +const SHAPES = lines( + 'class Widget {', + ' public:', + ' void Draw() {}', + '};', + 'class Canvas {', + ' public:', + ' void Draw() {}', + '};', +); + +describe('C++ declarations around block comments', () => { + it('reads a declaration continued on a line that starts with `*`', async () => { + const cg = await indexed({ + 'shapes.h': SHAPES, + 'painter.h': lines( + '#include "shapes.h"', + 'class Painter {', + ' public:', + ' void Paint() { widget_->Draw(); }', + ' private:', + ' Widget', + ' *widget_;', + '};', + ), + }); + try { + expect(callees(cg, 'Painter::Paint')).toEqual(['Widget::Draw @0.9']); + } finally { + cg.close(); + } + }); + + it('reads no declaration from code commented out in a block comment', async () => { + const cg = await indexed({ + 'shapes.h': SHAPES, + 'run.cc': lines( + '#include "shapes.h"', + 'void Run(Canvas* c) {', + ' /*', + ' Widget* c = nullptr;', + ' */', + ' c->Draw();', + '}', + 'void Doc(Canvas* w) {', + ' /*', + ' * Widget* w = nullptr;', + ' */', + ' w->Draw();', + '}', + ), + }); + try { + expect(callees(cg, 'Run')).toEqual(['Canvas::Draw @0.9']); + expect(callees(cg, 'Doc')).toEqual(['Canvas::Draw @0.9']); + } finally { + cg.close(); + } + }); + + it("reads no member, base, using-declaration or local from a class's or function's comments", async () => { + const cg = await indexed({ + 'others.h': lines( + 'class HolderBase {', + ' public:', + ' void GetMessage() {}', + '};', + 'class Old {', + ' public:', + ' void OnlyOld() {}', + '};', + 'class Base {};', + 'class Other {};', + 'template class TBase {', + ' public:', + ' void Insert(int x) {}', + '};', + ), + 'holders.h': lines( + '#include "others.h"', + 'class Holder : public HolderBase {', + ' public:', + ' /*', + ' LOCAL_VAR_ACCESSOR(Message*, Message);', + ' */', + ' void Run() { GetMessage(); }', + '};', + 'class Derived : public Base,', + ' /*', + ' public Old,', + ' */', + ' public Other {', + '};', + 'template class TDerived : public TBase {', + ' public:', + ' /*', + ' using TBase::Insert;', + ' */', + ' void Add() { Insert(1); }', + '};', + 'class Db {', + ' public:', + ' void Flush() {}', + ' void Run() {', + ' /*', + ' auto Flush = [] {};', + ' */', + ' Flush();', + ' }', + '};', + 'void UseDerived(Derived* d) { d->OnlyOld(); }', + ), + }); + try { + // No macro of `Holder`'s might declare `GetMessage`: C++ lookup goes + // on to the base's. + expect(resolutions(cg, 'Holder::Run')).toEqual(['HolderBase::GetMessage @0.9 instance-method']); + // `Derived`'s bases are `Base` and `Other`, the two the index links, + // neither with an `OnlyOld`: the call is not guessed onto `Old`'s. + expect(resolutions(cg, 'UseDerived')).toEqual([]); + // No using-declaration brings the dependent base's `Insert` in: only + // its name, which nothing else declares, links it. + expect(resolutions(cg, 'TDerived::Add')).toEqual(['TBase::Insert @0.9 exact-match']); + // `Flush` is no local: C++ lookup finds the member. + expect(resolutions(cg, 'Db::Run')).toEqual(['Db::Flush @0.9 instance-method']); + } finally { + cg.close(); + } + }); + + it('reads no local from a string literal', async () => { + // protobuf's `[[deprecated("Please use SpaceUsedLong() instead")]]` and + // rocksdb's `"Unknown code(%d): "` read as declarations of a local. + const cg = await indexed({ + 'status.h': lines( + 'class Status {', + ' public:', + ' int code() const { return 0; }', + ' [[deprecated("Please use code() instead")]] int Code() const { return code(); }', + ' void ToString() const;', + '};', + ), + 'status.cc': lines( + '#include "status.h"', + 'void Status::ToString() const {', + ' char tmp[30];', + ' snprintf(tmp, sizeof(tmp), "Unknown code(%d): ", code());', + '}', + ), + }); + try { + expect(resolutions(cg, 'Status::Code')).toEqual(['Status::code @0.9 instance-method']); + expect(resolutions(cg, 'Status::ToString')).toEqual(['Status::code @0.9 instance-method']); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-class-name-receiver-members.test.ts b/__tests__/cpp-class-name-receiver-members.test.ts new file mode 100644 index 0000000000..c3986f653f --- /dev/null +++ b/__tests__/cpp-class-name-receiver-members.test.ts @@ -0,0 +1,120 @@ +/** + * When a C++ call's receiver can't be typed, matchMethodCall guesses its + * class from the receiver's name: the class it names exactly (`Outer::Count()`) + * or its capitalized form (`arena.get()` → `Arena`). It then took any method + * of the call's name in that class's file whose qualified name merely + * contained the class's name. Once out-of-line nested classes were named, + * protocolbuffers/protobuf's `arena.get()` on a `std::unique_ptr` + * reached `Arena::UniquePtr::get`, a class nested in `Arena`, at 0.8; and a + * namesake like `ThreadSafeArena::get` matched the same way. In C and C++ the + * method must now be a member of exactly that class. + */ +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'; + +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-class-receiver-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** What the calls at `line` of the function or method `qualifiedName` reach, as `qualifiedName @confidence`. */ +function callsAt(cg: CodeGraph, qualifiedName: string, line: number): string[] { + const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!; + return cg.getOutgoingEdgesFrom([from.id], ['calls']) + .filter((e) => e.line === line) + .map((e) => `${cg.getNode(e.target)?.qualifiedName} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`) + .sort(); +} + +const ARENA = lines( + 'namespace google {', + 'namespace protobuf {', + 'class Arena {', + ' public:', + ' template class UniquePtr;', + ' void* AllocateAligned(int n) { return nullptr; }', + '};', + 'template ', + 'class Arena::UniquePtr {', + ' public:', + ' T* get() const { return nullptr; }', + '};', + 'class ThreadSafeArena {', + ' public:', + ' int get() const { return 0; }', + '};', + '} // namespace protobuf', + '} // namespace google', +); + +describe('C++ calls on a receiver named like a class', () => { + it("take only that class's own method, not a nested class's or a namesake's", async () => { + const cg = await indexed({ + 'arena.h': ARENA, + 'arena_test.cc': lines( + '#include "arena.h"', + 'namespace google {', + 'namespace protobuf {', + 'void* Run() {', + ' auto arena = MakeArena();', + ' Use(arena.get());', + ' return arena.AllocateAligned(8);', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(callsAt(cg, 'google::protobuf::Run', 6)).toEqual([]); + // `arena` is an `auto` local whose initializer's type can't be read (`MakeArena` is + // declared nowhere). The fork's resolver leaves such a call unlinked rather than + // guessing the class from the receiver's name, as upstream does at 0.8 (README, + // "About this fork": C++ calls on an `auto` local). + expect(callsAt(cg, 'google::protobuf::Run', 7)).toEqual([]); + } finally { + cg.close(); + } + }); + + it('reach a method the named class inherits before a nested class of the same name', async () => { + const cg = await indexed({ + 'outer.h': lines( + 'class Base {', + ' public:', + ' static int Count() { return 0; }', + '};', + 'class Outer : public Base {', + ' public:', + ' class Inner {', + ' public:', + ' static int Count() { return 1; }', + ' };', + '};', + ), + 'main.cc': lines( + '#include "outer.h"', + 'int Total() { return Outer::Count(); }', + ), + }); + try { + expect(callsAt(cg, 'Total', 2)).toEqual(['Base::Count @0.8']); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-comparisons-and-template-declarators.test.ts b/__tests__/cpp-comparisons-and-template-declarators.test.ts new file mode 100644 index 0000000000..ebe6fc77c9 --- /dev/null +++ b/__tests__/cpp-comparisons-and-template-declarators.test.ts @@ -0,0 +1,171 @@ +/** + * Two ways C++ text was misread around `<`: + * - A call's arguments were split at commas outside brackets, counting every + * `<` as a template's. protocolbuffers/protobuf's `p->Emit({{"declare", + * has_bit_word_index < 0 ? … : ""}, …}, R"cc(…)cc")` read as one argument, + * so the overload without substitutions was taken. A `<` now opens a + * template's arguments only right after a name and when a `>` closes it. + * - A declaration's template arguments were matched as any run up to a `>`, + * so on a line declaring `const std::vector& a, std::unique_ptr b` + * the type read for `b` began at `a`'s, and calls on `b` got no type; and + * as one character at least, so fmt's `parse_context<>& ctx` was no type. + */ +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'; + +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-angles-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** What the calls at `line` of the function or method `qualifiedName` reach, as `target (its first line) @confidence`. */ +function callsAt(cg: CodeGraph, qualifiedName: string, line: number): string[] { + const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!; + return cg.getOutgoingEdgesFrom([from.id], ['calls']) + .filter((e) => e.line === line) + .map((e) => { + const t = cg.getNode(e.target)!; + return `${t.qualifiedName}:${t.startLine} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`; + }) + .sort(); +} + +describe('C++ text around `<`', () => { + it('splits a call whose argument compares or shifts at its real commas', async () => { + const cg = await indexed({ + 'printer.h': lines( + 'class Printer {', + ' public:', + ' void Emit(const char* text) {}', + ' void Emit(int vars, const char* text) {}', + ' void Emit(int a, int b, const char* text) {}', + ' void Run(int x, int y, Printer* p) {', + ' Emit(x < 0 ? 1 : 2, "text");', + ' Emit(Size(), "text");', + ' Emit("text");', + ' Emit(Pick(x > 0, 1), "text");', + ' Emit(x < 0, y > 0 ? 1 : 2, "text");', + ' Emit(1<<20, y>>1, "text");', + ' Emit(x<=0, y > 0, "text");', + " Emit(x<0 ? 1 : 2, y ? 3 : '>', \"text\");", + ' Emit(x 0), "text");', + ' Emit(Pick(x<1), y > 0, "text");', + ' Emit(xn, "text");', + ' }', + ' template static int Size() { return 0; }', + ' static int Pick(bool b, int v = 0) { return v; }', + ' int n = 0;', + '};', + ), + }); + try { + const emits = (line: number): string[] => callsAt(cg, 'Printer::Run', line).filter((t) => t.startsWith('Printer::Emit')); + // A comparison, then template arguments with a comma in them. + expect(emits(7)).toEqual(['Printer::Emit:4 @0.9']); + expect(emits(8)).toEqual(['Printer::Emit:4 @0.9']); + expect(emits(9)).toEqual(['Printer::Emit:3 @0.9']); + // A `>` closes no parenthesis. + expect(emits(10)).toEqual(['Printer::Emit:4 @0.9']); + // No template's `<`: after no name, a shift, a `<=`. + expect(emits(11)).toEqual(['Printer::Emit:5 @0.9']); + expect(emits(12)).toEqual(['Printer::Emit:5 @0.9']); + expect(emits(13)).toEqual(['Printer::Emit:5 @0.9']); + // Nor one closed only by a `>` in a literal, in brackets, after the + // bracket around it closes, or in a `->`. + expect(emits(14)).toEqual(['Printer::Emit:5 @0.9']); + expect(emits(15)).toEqual(['Printer::Emit:5 @0.9']); + expect(emits(16)).toEqual(['Printer::Emit:5 @0.9']); + expect(emits(17)).toEqual(['Printer::Emit:5 @0.9']); + } finally { + cg.close(); + } + }); + + it("reads a parameter's type from its own template arguments, none or nested ones included", async () => { + const cg = await indexed({ + 'run.cc': lines( + '#include ', + '#include ', + 'class Foo {', + ' public:', + ' void Go() {}', + '};', + 'class Bar {', + ' public:', + ' void Go() {}', + '};', + 'void Run(const std::vector& a, std::unique_ptr b) {', + ' b->Go();', + '}', + 'template class Context {', + ' public:', + ' void Begin() {}', + '};', + 'template class Outer {', + ' public:', + ' void Go() {}', + '};', + 'template class Inner {};', + 'void Parse(Context<>& ctx) { ctx.Begin(); }', + 'void Deep(std::unique_ptr>> p) { p->Go(); }', + ), + }); + try { + expect(callsAt(cg, 'Run', 12)).toEqual(['Bar::Go:9 @0.9']); + expect(callsAt(cg, 'Parse', 23)).toEqual(['Context::Begin:16 @0.9']); + expect(callsAt(cg, 'Deep', 24)).toEqual(['Outer::Go:20 @0.9']); + } finally { + cg.close(); + } + }); + + it('reads a `->` or a `>>` before the receiver as no declaration begun on a line above', async () => { + // (A templated type whose `>`s outnumber its `<`s is one: see + // cpp-nested-template-receiver.test.ts.) + const cg = await indexed({ + 'painter.h': lines( + 'class Widget {', + ' public:', + ' void Draw() {}', + '};', + 'class Gadget {', + ' public:', + ' void Draw() {}', + '};', + 'class Stream {};', + 'Stream& operator>>(Stream& in, Widget& w);', + 'void Use(Widget* w);', + 'class Painter {', + ' public:', + ' void Run(Stream& in) {', + ' Use(&this->w_);', + ' in >> w_;', + ' w_.Draw();', + ' }', + ' private:', + ' Widget w_;', + '};', + ), + }); + try { + expect(callsAt(cg, 'Painter::Run', 17)).toEqual(['Widget::Draw:3 @0.9']); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-construction-test-tree-headers.test.ts b/__tests__/cpp-construction-test-tree-headers.test.ts new file mode 100644 index 0000000000..9b3751260f --- /dev/null +++ b/__tests__/cpp-construction-test-tree-headers.test.ts @@ -0,0 +1,112 @@ +/** + * A C++ construction is placed by lookup over the headers its file can see. + * nlohmann/json keeps an old release of the whole library in + * `tests/abi/include/nlohmann/json_v3_10_5.hpp`, which spells `namespace + * nlohmann {` out, while the library's own headers open it with a macro, so + * only the old copy's types are indexed under `nlohmann::`. The docs + * examples' `json::json_pointer p("/")`, through `using json = + * nlohmann::json;`, reached that copy. A header of a test tree is now seen + * only from inside that tree or by including it, and an alias lookup can't + * follow leaves a member the project declares a class for to the name + * strategies. + */ +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'; + +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-test-tree-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** The files of what a function's `instantiates` edges reach, as `qualifiedName in file`. */ +function made(cg: CodeGraph, qualifiedName: string): string[] { + const from = cg.getNodesByKind('function').find((n) => n.qualifiedName === qualifiedName && !n.filePath.includes('include'))!; + return cg.getOutgoingEdgesFrom([from.id], ['instantiates']) + .map((e) => cg.getNode(e.target)!) + .map((n) => `${n.qualifiedName} in ${n.filePath}`) + .sort(); +} + +const LIBRARY = lines( + '#define LIB_BEGIN namespace lib {', + '#define LIB_END }', + 'LIB_BEGIN', + 'class json_pointer { public: json_pointer(const char* s) {} };', + 'class basic_json { public: using json_pointer = ::lib::json_pointer; basic_json(int v) {} };', + 'using json = basic_json;', + 'LIB_END', +); + +const OLD_RELEASE = lines( + 'namespace lib {', + 'class json_pointer { public: json_pointer(const char* s) {} };', + 'class basic_json { public: using json_pointer = ::lib::json_pointer; basic_json(int v) {} };', + 'using json = basic_json;', + '} // namespace lib', +); + +const files = { + // Two copies of the library, so `` resolves to neither. + 'include/lib/json.hpp': LIBRARY, + 'single_include/lib/json.hpp': LIBRARY, + 'tests/abi/include/lib/json_v1.hpp': OLD_RELEASE, +}; + +describe('C++ constructions and the headers of a test tree', () => { + it('are not placed in a test tree code outside it does not include', async () => { + const cg = await indexed({ + ...files, + 'docs/examples/pointer.cpp': lines( + '#include ', + 'using json = lib::json;', + 'int main() {', + ' json::json_pointer p("/");', + ' json j(1);', + ' return 0;', + '}', + ), + 'tests/benchmarks/bench.cpp': lines( + '#include ', + 'using json = lib::json;', + 'int bench() {', + ' json j(1);', + ' return 0;', + '}', + ), + }); + const examples = made(cg, 'main'); + expect(examples.some((t) => t.includes('tests/abi/'))).toBe(false); + expect(examples.some((t) => t.startsWith('json_pointer in '))).toBe(true); + expect(made(cg, 'bench')).toEqual(['json in tests/benchmarks/bench.cpp']); + cg.close(); + }); + + it('are placed there from inside the tree', async () => { + const cg = await indexed({ + ...files, + 'tests/abi/abi.cpp': lines( + 'int abi() {', + ' lib::json_pointer p("/");', + ' return 0;', + '}', + ), + }); + expect(made(cg, 'abi')).toEqual(['lib::json_pointer in tests/abi/include/lib/json_v1.hpp']); + cg.close(); + }); +}); diff --git a/__tests__/cpp-declarator-edge-cases.test.ts b/__tests__/cpp-declarator-edge-cases.test.ts new file mode 100644 index 0000000000..8655c39ab3 --- /dev/null +++ b/__tests__/cpp-declarator-edge-cases.test.ts @@ -0,0 +1,129 @@ +/** + * Edge cases of how a C++ declaration's type is read for a call's receiver, and + * which overloads a call reaches: + * + * - `Foo* __restrict p` is a `Foo` pointer, as `Foo* const p` is. + * - An annotation macro without arguments after a declared name + * (`Box* b ABSL_ATTRIBUTE_LIFETIME_BOUND;`) does not hide the declaration. + * - A `const` pointer to a class template (`const Box* b`) is read as a + * `Box`. + * - A deleted overload is none a call reaches, however many lines its + * declaration runs over before the `= delete;`. + */ +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'; + +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-declarator-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** What the calls at `line` of the function or method `qualifiedName` reach, as `target:its first line @confidence`. */ +function callsAt(cg: CodeGraph, qualifiedName: string, line: number): string[] { + const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!; + return cg.getOutgoingEdgesFrom([from.id], ['calls']) + .filter((e) => e.line === line) + .map((e) => { + const t = cg.getNode(e.target)!; + return `${t.qualifiedName}:${t.startLine} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`; + }) + .sort(); +} + +// `Open` is also a namesake class's, so a guess by the name can miss. +const BOXES = lines( + 'template ', + 'class Box {', + ' public:', + ' void Open() {}', + '};', + 'class Cask {', + ' public:', + ' void Open() {}', + '};', +); + +describe('C++ declarations read for a call receiver', () => { + it('reads `Box* __restrict` as a Box pointer', async () => { + const cg = await indexed({ + 'box.h': BOXES, + 'use.cc': lines('#include "box.h"', 'void Use(Cask* __restrict c) {', ' c->Open();', '}'), + }); + try { + expect(callsAt(cg, 'Use', 3)).toEqual(['Cask::Open:8 @0.9']); + } finally { + cg.close(); + } + }); + + it('reads a member followed by an annotation macro that takes no arguments', async () => { + const cg = await indexed({ + 'box.h': BOXES, + 'owner.h': lines( + '#include "box.h"', + '#define LIFETIME_BOUND_ANNOTATION', + 'class Owner {', + ' public:', + ' void Run() { cask_->Open(); }', + ' private:', + ' Cask* const cask_ LIFETIME_BOUND_ANNOTATION;', + '};', + ), + }); + try { + expect(callsAt(cg, 'Owner::Run', 5)).toEqual(['Cask::Open:8 @0.9']); + } finally { + cg.close(); + } + }); + + it('reads a const pointer to a class template as the template', async () => { + const cg = await indexed({ + 'box.h': BOXES, + 'use.cc': lines('#include "box.h"', 'void Use(const Box* b) {', ' b->Open();', '}'), + }); + try { + expect(callsAt(cg, 'Use', 3)).toEqual(['Box::Open:4 @0.9']); + } finally { + cg.close(); + } + }); +}); + +describe('C++ deleted overloads', () => { + it('takes no deleted overload whose declaration runs over many lines', async () => { + const comments = Array.from({ length: 14 }, (_, i) => ` // reason ${i + 1}`); + const cg = await indexed({ + 'meter.h': lines( + 'class Meter {', + ' public:', + ' void Scale(', + ...comments, + ' double factor) = delete;', + ' void Scale(int factor) {}', + ' void Run() { Scale(2); }', + '};', + ), + }); + try { + expect(callsAt(cg, 'Meter::Run', 20)).toEqual(['Meter::Scale:19 @0.9']); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-gtest-fixture-scope.test.ts b/__tests__/cpp-gtest-fixture-scope.test.ts new file mode 100644 index 0000000000..147af281fb --- /dev/null +++ b/__tests__/cpp-gtest-fixture-scope.test.ts @@ -0,0 +1,182 @@ +/** + * A gtest `TEST_F(Fixture, Name) { … }` or `TEST_P` body is a member function + * of a class derived from its fixture, so an unqualified name in it finds the + * body's own declarations, then the fixture's members, its bases' included, + * then the enclosing namespaces'. C++ lookup from the body started at the + * function and went straight to the namespaces, so facebook/rocksdb's + * `new TestUserDefinedIndexFactory()`, a class nested in the fixture's base, + * was found only as the one class of that name the file could see (0.7), and + * a namespace-level class of the same name won over the fixture's. + */ +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-scope-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** The `instantiates` edges of every function named `name`, by its first line, as `startLine -> qualifiedName @confidence`. */ +function made(cg: CodeGraph, name: string): string[] { + return cg.getNodesByKind('function').filter((n: Node) => n.name === name).sort((a, b) => a.startLine - b.startLine) + .flatMap((fn) => cg.getOutgoingEdgesFrom([fn.id], ['instantiates']) + .map((e) => `${fn.startLine} -> ${cg.getNode(e.target)?.qualifiedName} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`)); +} + +describe('C++ names in a gtest TEST_F or TEST_P body', () => { + it("find the fixture's nested types, its bases' included", async () => { + const cg = await indexed({ + 'table/table_test.cc': lines( + 'namespace rocksdb {', + 'class UserDefinedIndexTestBase : public testing::Test {', + ' public:', + ' class TestUserDefinedIndexFactory {', + ' public:', + ' TestUserDefinedIndexFactory() {}', + ' };', + '};', + 'class UserDefinedIndexTest : public UserDefinedIndexTestBase {};', + 'TEST_F(UserDefinedIndexTest, Basic) {', + ' auto factory = new TestUserDefinedIndexFactory();', + '}', + 'TEST_P(UserDefinedIndexTest, Params) {', + ' auto factory = new TestUserDefinedIndexFactory();', + '}', + '} // namespace rocksdb', + ), + }); + try { + const nested = 'rocksdb::UserDefinedIndexTestBase::TestUserDefinedIndexFactory @0.9'; + expect(made(cg, 'TEST_F')).toEqual([`10 -> ${nested}`]); + expect(made(cg, 'TEST_P')).toEqual([`13 -> ${nested}`]); + } finally { + cg.close(); + } + }); + + it("find the fixture's type before a namespace's, and the body's own before both", async () => { + const cg = await indexed({ + 'db/db_test.cc': lines( + 'namespace rocksdb {', + 'class Listener {', + ' public:', + ' explicit Listener(int n) {}', + '};', + 'class DBTest : public testing::Test {', + ' public:', + ' class Listener {', + ' public:', + ' explicit Listener(int n) {}', + ' };', + '};', + 'TEST_F(DBTest, Fixture) {', + ' Listener listener(1);', + '}', + 'TEST_F(DBTest, Local) {', + ' class Listener {', + ' public:', + ' explicit Listener(int n) {}', + ' };', + ' Listener listener(2);', + '}', + 'TEST(DBPlainTest, NoFixture) {', + ' Listener listener(3);', + '}', + 'void Helper() { Listener listener(4); }', + '} // namespace rocksdb', + ), + }); + try { + expect(made(cg, 'TEST_F')).toEqual(['13 -> rocksdb::DBTest::Listener @0.9', '16 -> rocksdb::TEST_F::Listener @0.9']); + expect(made(cg, 'TEST')).toEqual(['23 -> rocksdb::Listener @0.9']); + expect(made(cg, 'Helper')).toEqual(['26 -> rocksdb::Listener @0.9']); + } finally { + cg.close(); + } + }); + + it("call the methods of the body's own local class, not another body's", async () => { + const cg = await indexed({ + 'db/listener_test.cc': lines( + 'namespace rocksdb {', + 'class ListenerTest : public testing::Test {};', + 'TEST_F(ListenerTest, First) {', + ' class Listener {', + ' public:', + ' void Fire() {}', + ' };', + ' Listener listener;', + ' listener.Fire();', + '}', + 'TEST_F(ListenerTest, Second) {', + ' class Listener {', + ' public:', + ' void Fire() {}', + ' };', + ' Listener listener;', + ' listener.Fire();', + '}', + '} // namespace rocksdb', + ), + }); + try { + const fired = cg.getNodesByKind('function').filter((n: Node) => n.name === 'TEST_F').sort((a, b) => a.startLine - b.startLine) + .map((fn) => cg.getOutgoingEdgesFrom([fn.id], ['calls']) + .map((e) => `${fn.startLine} -> ${cg.getNode(e.target)?.qualifiedName}:${cg.getNode(e.target)?.startLine} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`)); + expect(fired).toEqual([['3 -> rocksdb::TEST_F::Listener::Fire:6 @0.9'], ['11 -> rocksdb::TEST_F::Listener::Fire:14 @0.9']]); + } finally { + cg.close(); + } + }); + + it("find the body's own local class from a method of that class", async () => { + // rocksdb's env_test.cc: two bodies each declare a `CB`, and a static + // method of the second's calls `cb->Run()` on a `CB*`. + const cg = await indexed({ + 'env/env_test.cc': lines( + 'namespace rocksdb {', + 'class EnvTest : public testing::Test {};', + 'TEST_F(EnvTest, RunMany) {', + ' struct CB {', + ' void Run() {}', + ' };', + '}', + 'TEST_F(EnvTest, TwoPools) {', + ' class CB {', + ' public:', + ' static void Trampoline(void* v) {', + ' CB* cb = static_cast(v);', + ' cb->Run();', + ' }', + ' void Run() {}', + ' };', + '}', + '} // namespace rocksdb', + ), + }); + try { + const from = cg.getNodesByKind('method').find((n: Node) => n.name === 'Trampoline')!; + const calls = cg.getOutgoingEdgesFrom([from.id], ['calls']) + .map((e) => `${cg.getNode(e.target)?.qualifiedName}:${cg.getNode(e.target)?.startLine} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`); + expect(calls).toEqual(['rocksdb::TEST_F::CB::Run:15 @0.9']); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-member-receiver.test.ts b/__tests__/cpp-member-receiver.test.ts index 47361e7da6..b4650115d9 100644 --- a/__tests__/cpp-member-receiver.test.ts +++ b/__tests__/cpp-member-receiver.test.ts @@ -401,8 +401,8 @@ describe('what keeps the call as it was', () => { it('a parameter of a method the class declares, which is no member', async () => { // rocksdb's DB declares `CreateColumnFamilies(const std::vector<…>& - // column_families, …)`; DBImpl's `const ColumnFamilySet* const - // column_families` is a local the scan cannot read. + // column_families, …)`. (The function declares no local of the names: + // one the scan reads would decide first.) const cg = await indexed({ ...ROCKSDB, 'db/db_impl/db_impl.h': ROCKSDB['db/db_impl/db_impl.h'].replace( @@ -413,8 +413,6 @@ describe('what keeps the call as it was', () => { '#include "db/db_impl/db_impl.h"', 'namespace rocksdb {', 'int DBImpl::CountColumnFamilies() {', - ' const ColumnFamilySet* const column_families = versions_->GetColumnFamilySet();', - ' const Statistics* const stats = nullptr;', ' if (stats->load()) return 0;', ' return column_families->NumberOfColumnFamilies();', '}', @@ -422,13 +420,13 @@ describe('what keeps the call as it was', () => { ), }); try { - // The fork reads the local's declared type from the syntax tree, so these are typed calls - // (upstream's line scan cannot read `const ColumnFamilySet* const` and guesses, at 0.7). - expect(calls(cg, 'rocksdb::DBImpl::CountColumnFamilies').filter((c) => c.includes('NumberOfColumnFamilies'))).toEqual([ - 'rocksdb::ColumnFamilySet::NumberOfColumnFamilies @0.9', - ]); + // Neither name is a member, so neither call is typed through one. Upstream then guesses + // the class from the receiver's name (ColumnFamilySet::NumberOfColumnFamilies and + // Statistics::load, at 0.7); the fork's resolver leaves a receiver whose type it can't + // read unlinked instead (README, "About this fork": C++ calls on an `auto` local, ...). + expect(calls(cg, 'rocksdb::DBImpl::CountColumnFamilies').filter((c) => c.includes('NumberOfColumnFamilies'))).toEqual([]); // Nor is a method the class declares (`stats()`) a member of its name. - expect(calls(cg, 'rocksdb::DBImpl::CountColumnFamilies').filter((c) => c.includes('load'))).toEqual(['rocksdb::Statistics::load @0.9']); + expect(calls(cg, 'rocksdb::DBImpl::CountColumnFamilies').filter((c) => c.includes('load'))).toEqual([]); } finally { cg.close(); } diff --git a/__tests__/cpp-member-template-returns.test.ts b/__tests__/cpp-member-template-returns.test.ts new file mode 100644 index 0000000000..73b08a72e3 --- /dev/null +++ b/__tests__/cpp-member-template-returns.test.ts @@ -0,0 +1,189 @@ +/** + * An `auto` local initialized from a call of a C++ member template with + * explicit template arguments has the type the template returns for them. + * A member call was read only when its callee ended in the method's name, so + * `reflection->GetRaw>(…)` was no call it could + * read; and a return type that is the template's own parameter (`template + * const Type& GetRaw(…)`) is what the arguments make it. + * protocolbuffers/protobuf's `rep.size()` on such a local, in a macro whose + * lines end in `\`, guessed `SooRep::size`; facebook/rocksdb's `auto bbto = + * factory->GetOptions();` gave `bbto` no type. + */ +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'; + +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-member-template-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** What a function's calls of `method` reach, as `qualifiedName resolvedBy@confidence`. */ +function calls(cg: CodeGraph, qualifiedName: string, method: string): string[] { + const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!; + return cg.getOutgoingEdgesFrom([from.id], ['calls']) + .map((e) => { + const meta = e.metadata as { confidence?: number; resolvedBy?: string } | undefined; + return `${cg.getNode(e.target)?.qualifiedName} ${meta?.resolvedBy}@${meta?.confidence}`; + }) + .filter((t) => t.split(' ')[0]!.endsWith(`::${method}`)) + .sort(); +} + +const SHAPES = lines( + 'class Widget {', + ' public:', + ' void Draw() {}', + '};', + 'class Gadget {', + ' public:', + ' void Draw() {}', + '};', +); + +describe('C++ member templates called with explicit template arguments', () => { + it("types protobuf's `const auto& rep =` from GetRaw>, its line spliced with `\\`", async () => { + const cg = await indexed({ + 'src/google/protobuf/reflection.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'class Message {};', + 'class FieldDescriptor {};', + 'template ', + 'class RepeatedPtrField {', + ' public:', + ' int size() const { return 0; }', + '};', + 'class SooRep {', + ' public:', + ' int size() const { return 0; }', + '};', + 'class Reflection {', + ' public:', + ' template ', + ' const Type& GetRaw(const Message& message, const FieldDescriptor* field) const;', + '};', + 'template ', + 'const Type& Reflection::GetRaw(const Message& message, const FieldDescriptor* field) const {', + ' static Type value;', + ' return value;', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/reflection_visit_fields.h': lines( + '#include "src/google/protobuf/reflection.h"', + 'namespace google {', + 'namespace protobuf {', + 'int VisitFields(const Reflection* reflection, const Message& message,', + ' const FieldDescriptor* field) {', + // protobuf writes this in a macro, whose lines end in `\`. + ' const auto& rep = \\', + ' reflection->GetRaw>(message, field); \\', + ' ;', + ' return rep.size();', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::VisitFields', 'size')).toEqual(['google::protobuf::RepeatedPtrField::size instance-method@0.9']); + } finally { + cg.close(); + } + }); + + it("types rocksdb's `auto bbto =` from GetOptions(), const and not", async () => { + const cg = await indexed({ + 'include/rocksdb/configurable.h': lines( + 'namespace rocksdb {', + 'class Configurable {', + ' public:', + ' template ', + ' const T* GetOptions() const { return nullptr; }', + ' template ', + ' T* GetOptions() { return nullptr; }', + '};', + 'struct BlockBasedTableOptions {', + ' bool IsValid() const { return true; }', + '};', + 'struct PlainTableOptions {', + ' bool IsValid() const { return true; }', + '};', + '} // namespace rocksdb', + ), + 'db/check.cc': lines( + '#include "include/rocksdb/configurable.h"', + 'namespace rocksdb {', + 'bool Check(Configurable* factory) {', + ' auto bbto = factory->GetOptions();', + ' return bbto->IsValid();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Check', 'IsValid')).toEqual(['rocksdb::BlockBasedTableOptions::IsValid instance-method@0.9']); + } finally { + cg.close(); + } + }); + + it('reads a template overload only, a defaulted parameter by its name, and no guess between overloads', async () => { + const cg = await indexed({ + 'shapes.h': SHAPES, + 'store.cc': lines( + '#include "shapes.h"', + 'class Store {', + ' public:', + ' Widget* Find() { return nullptr; }', + ' template ', + ' T* Find() { return nullptr; }', + ' template ', + ' T* Make() { return nullptr; }', + ' template ', + ' A* Get() { return nullptr; }', + ' template ', + ' B* Get(int key) { return nullptr; }', + '};', + 'void Found(Store* store) {', + ' auto found = store->Find();', + ' found->Draw();', + '}', + 'void Made(Store* store) {', + ' auto made = store->Make();', + ' made->Draw();', + '}', + 'void Got(Store* store) {', + ' auto got = store->Get(1);', + ' got->Draw();', + '}', + ), + }); + try { + // Explicit template arguments call `T* Find()`, never `Widget* Find()`. + expect(calls(cg, 'Found', 'Draw')).toEqual(['Gadget::Draw instance-method@0.9']); + expect(calls(cg, 'Made', 'Draw')).toEqual(['Gadget::Draw instance-method@0.9']); + // The overloads return different parameters; which one the call takes + // isn't read, so neither is guessed. + expect(calls(cg, 'Got', 'Draw').filter((t) => t.endsWith('@0.9'))).toEqual([]); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-nested-template-receiver.test.ts b/__tests__/cpp-nested-template-receiver.test.ts index 1bf6e3a498..e1821b137e 100644 --- a/__tests__/cpp-nested-template-receiver.test.ts +++ b/__tests__/cpp-nested-template-receiver.test.ts @@ -194,11 +194,12 @@ describe('a C++ receiver declared with nested template arguments', () => { } }); - it('a multiline parameter retains its complete type instead of its template argument', async () => { + it('the end of a declaration begun on the line above is read with that line, not as its template argument\'s type', async () => { // rocksdb's blob_file_reader.h breaks `autovector>>& blob_reqs` over two lines. The second // line alone, read as a type, is a `CachableEntry` here: the receiver's - // type is the `autovector` on the line above. + // type is the `autovector` on the line above, which the two lines read + // together declare. const cg = await indexed({ ...ROCKSDB, 'table/block_based/cachable_entry.h': [ @@ -227,7 +228,7 @@ describe('a C++ receiver declared with nested template arguments', () => { ].join('\n'), }); try { - expect(calls(cg, 'rocksdb::MultiGetBlob')).toEqual(['rocksdb::autovector::size (util/autovector.h)']); + expect(resolvedCalls(cg, 'rocksdb::MultiGetBlob')).toEqual(['rocksdb::autovector::size instance-method@0.9']); } finally { cg.close(); } diff --git a/__tests__/cpp-nullptr-deleted-overloads.test.ts b/__tests__/cpp-nullptr-deleted-overloads.test.ts new file mode 100644 index 0000000000..7c30d5ad69 --- /dev/null +++ b/__tests__/cpp-nullptr-deleted-overloads.test.ts @@ -0,0 +1,131 @@ +/** + * Two ways a C++ call reached an overload no call that compiles reaches: + * - A `nullptr` argument fitted any pointer parameter as well as it could fit + * anything, and a `std::nullptr_t` parameter no better than a class, so + * protocolbuffers/protobuf's `ptr.reset(nullptr)` on an + * `Arena::UniquePtr` took `reset(T*)` over `reset(std::nullptr_t)`. + * `nullptr` is a `std::nullptr_t` exactly, and a pointer by conversion. + * - That `reset(T*)` is declared `= delete`. A deleted overload is now taken + * only when every overload is. + */ +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'; + +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-nullptr-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** What the calls at `line` of the function or method `qualifiedName` reach, as `target:its first line @confidence`. */ +function callsAt(cg: CodeGraph, qualifiedName: string, line: number): string[] { + const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!; + return cg.getOutgoingEdgesFrom([from.id], ['calls']) + .filter((e) => e.line === line) + .map((e) => { + const t = cg.getNode(e.target)!; + return `${t.qualifiedName}:${t.startLine} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`; + }) + .sort(); +} + +describe('C++ overloads for nullptr, and deleted ones', () => { + it("takes protobuf's reset(std::nullptr_t) for reset(nullptr), not the deleted reset(T*)", async () => { + const cg = await indexed({ + 'arena.h': lines( + '#include ', + 'namespace google {', + 'namespace protobuf {', + 'class Arena {', + ' public:', + ' template ', + ' class UniquePtr {', + ' public:', + ' void reset() {}', + ' void reset(std::nullptr_t) {}', + ' void reset(T* PROTOBUF_NULLABLE) = delete;', + ' };', + '};', + '} // namespace protobuf', + '} // namespace google', + ), + 'arena_unittest.cc': lines( + '#include "arena.h"', + 'namespace google {', + 'namespace protobuf {', + 'void Reset() {', + ' Arena::UniquePtr ptr;', + ' ptr.reset(nullptr);', + ' ptr.reset();', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(callsAt(cg, 'google::protobuf::Reset', 6)).toEqual(['google::protobuf::Arena::UniquePtr::reset:10 @0.9']); + expect(callsAt(cg, 'google::protobuf::Reset', 7)).toEqual(['google::protobuf::Arena::UniquePtr::reset:9 @0.9']); + } finally { + cg.close(); + } + }); + + it('fits nullptr to std::nullptr_t before a pointer, and 0 to an int before both', async () => { + const cg = await indexed({ + 'canvas.h': lines( + '#include ', + 'class Widget {};', + 'class Canvas {', + ' public:', + ' void Set(Widget* w) {}', + ' void Set(std::nullptr_t) {}', + ' void Set(int v) {}', + ' void Run() {', + ' Set(nullptr);', + ' Set(0);', + ' }', + '};', + ), + }); + try { + expect(callsAt(cg, 'Canvas::Run', 9)).toEqual(['Canvas::Set:6 @0.9']); + expect(callsAt(cg, 'Canvas::Run', 10)).toEqual(['Canvas::Set:7 @0.9']); + } finally { + cg.close(); + } + }); + + it('takes no deleted overload while another fits, reading each declaration to its own end', async () => { + const cg = await indexed({ + 'meter.h': lines( + 'class Meter {', + ' public:', + ' void Scale(double factor) = delete;', + ' void Scale(int factor) {}', + ' void Scale(', + ' long factor) = delete;', + ' void Run() { Scale(2); }', + '};', + ), + }); + try { + expect(callsAt(cg, 'Meter::Run', 7)).toEqual(['Meter::Scale:4 @0.9']); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-overload-argument-types.test.ts b/__tests__/cpp-overload-argument-types.test.ts new file mode 100644 index 0000000000..52ad3b7a1f --- /dev/null +++ b/__tests__/cpp-overload-argument-types.test.ts @@ -0,0 +1,515 @@ +/** + * A C++ call among a class's overloads that take the same number of + * arguments picks the one the arguments' types fit. + * + * Overloads were told apart by argument count and string literals only, so + * a tie went to the first one written. In facebook/rocksdb's + * lru_cache_test.cc, `Insert(ch)` on a `char ch` reached `Insert(const + * std::string& key, …)` instead of `Insert(char key, …)`. What a call's + * arguments are is read where it can be: a character, number, `true`/`false` + * or `nullptr` literal, a name the calling function declares (or its + * address), or an expression whose type is known. + */ +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-overload-type-')); + 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 method `qualifiedName` whose declaration starts on `line`, or the only one. */ +function method(cg: CodeGraph, qualifiedName: string, line?: number): Node { + const node = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName && (line === undefined || n.startLine === line)); + if (!node) throw new Error(`no method ${qualifiedName}${line === undefined ? '' : ` at line ${line}`}`); + return node; +} + +/** A method's `calls` edges, as ` -> : @`. */ +function callsAt(cg: CodeGraph, caller: Node): string[] { + return cg + .getOutgoingEdgesFrom([caller.id], ['calls']) + .map((e) => { + const target = cg.getNode(e.target); + return `${e.line} -> ${target?.qualifiedName}:${target?.startLine} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`; + }) + .sort(); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +describe('C++ overloads chosen by argument types', () => { + it("is the char overload for a char, and the string one for a string", async () => { + const cg = await indexed({ + 'cache/lru_cache_test.cc': lines( + '#include ', + 'namespace rocksdb {', + 'class LRUCacheTest {', + ' public:', + ' void Insert(const std::string& key, int priority = 0, size_t charge = 1) {}', + ' void Insert(char key, int priority = 0) {', + ' Insert(std::string(1, key), priority);', + ' }', + ' void Fill() {', + " for (char ch = 'a'; ch <= 'e'; ch++) {", + ' Insert(ch);', + ' }', + ' std::string name = "x";', + ' Insert(name);', + ' Insert(name, 1, 2);', + ' }', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(callsAt(cg, method(cg, 'rocksdb::LRUCacheTest::Fill'))).toEqual([ + '11 -> rocksdb::LRUCacheTest::Insert:6 @0.9', + '14 -> rocksdb::LRUCacheTest::Insert:5 @0.9', + '15 -> rocksdb::LRUCacheTest::Insert:5 @0.9', + ]); + expect(callsAt(cg, method(cg, 'rocksdb::LRUCacheTest::Insert', 6))).toEqual(['7 -> rocksdb::LRUCacheTest::Insert:5 @0.9']); + } finally { + cg.close(); + } + }); + + it('is the overload a number, bool, char or null literal fits', async () => { + const cg = await indexed({ + 'db/writer.cc': lines( + 'namespace rocksdb {', + 'class Slice;', + 'class Writer {', + ' public:', + ' void Write(bool flag) {}', + ' void Write(int n) {}', + ' void Write(const Slice* s) {}', + ' void Run() {', + ' Write(7);', + ' Write(false);', + ' Write(nullptr);', + " Write('c');", + ' }', + '};', + 'class Holder {', + ' public:', + ' void Set(const std::string& name) {}', + ' void Set(const Slice* slice) {}', + ' void Clear() { Set(0); }', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(callsAt(cg, method(cg, 'rocksdb::Writer::Run'))).toEqual([ + '10 -> rocksdb::Writer::Write:5 @0.9', + '11 -> rocksdb::Writer::Write:7 @0.9', + '12 -> rocksdb::Writer::Write:6 @0.9', + '9 -> rocksdb::Writer::Write:6 @0.9', + ]); + // A `0` is a null pointer as well as a number. + expect(callsAt(cg, method(cg, 'rocksdb::Holder::Clear'))).toEqual(['19 -> rocksdb::Holder::Set:18 @0.9']); + } finally { + cg.close(); + } + }); + + it("is the pointer overload for an object's address, and the string_view one for a string", async () => { + const cg = await indexed({ + 'compiler/builder.cc': lines( + '#include ', + 'namespace protobuf {', + 'struct Proto {};', + 'class Builder {', + ' public:', + ' int Build(absl::string_view schema) { return 0; }', + ' int Build(const Proto* proto) { return 1; }', + ' void Run(const Proto& proto, const std::string& text) {', + ' Build(&proto);', + ' Build(text);', + ' }', + '};', + '} // namespace protobuf', + ), + }); + try { + expect(callsAt(cg, method(cg, 'protobuf::Builder::Run'))).toEqual([ + '10 -> protobuf::Builder::Build:6 @0.9', + '9 -> protobuf::Builder::Build:7 @0.9', + ]); + } finally { + cg.close(); + } + }); + + it('is the overload a typed receiver takes the arguments to', async () => { + const cg = await indexed({ + 'cache/lru_cache.cc': lines( + '#include ', + 'namespace rocksdb {', + 'class LRUCacheShard {', + ' public:', + ' void Insert(const std::string& key, int priority = 0) {}', + ' void Insert(char key, int priority = 0) {}', + '};', + 'void Fill(LRUCacheShard* shard) {', + " char ch = 'a';", + ' shard->Insert(ch);', + '}', + '} // namespace rocksdb', + ), + }); + try { + const fill = cg.getNodesByKind('function').find((n) => n.qualifiedName === 'rocksdb::Fill')!; + expect(callsAt(cg, fill)).toEqual(['10 -> rocksdb::LRUCacheShard::Insert:6 @0.9']); + } finally { + cg.close(); + } + }); + + it("is an overload of the receiver's own class, not of a namesake another source file declares", async () => { + // leveldb's db_bench.cc and db_bench_sqlite3.cc each declare a RandomGenerator. + const cg = await indexed({ + 'benchmarks/db_bench.cc': lines( + 'class RandomGenerator {', + ' public:', + ' const char* Generate(size_t len) { return nullptr; }', + '};', + 'class Benchmark {', + ' int value_size_;', + ' void Write() {', + ' RandomGenerator gen;', + ' gen.Generate(value_size_);', + ' }', + '};', + ), + 'benchmarks/db_bench_sqlite3.cc': lines( + 'class RandomGenerator {', + ' public:', + ' const char* Generate(int len) { return nullptr; }', + '};', + ), + }); + try { + expect(callsAt(cg, method(cg, 'Benchmark::Write')).map((c) => c.replace(/ @.*$/, ''))).toEqual(['9 -> RandomGenerator::Generate:3']); + const generate = cg.getOutgoingEdgesFrom([method(cg, 'Benchmark::Write').id], ['calls']).map((e) => cg.getNode(e.target)?.filePath); + expect(generate).toEqual(['benchmarks/db_bench.cc']); + } finally { + cg.close(); + } + }); + + it("keeps a chained call's first overload: the arguments where it starts are the inner call's", async () => { + const cg = await indexed({ + 'gmock/gmock.h': lines( + 'namespace testing {', + 'class Sequence {};', + 'namespace internal {', + 'template ', + 'class TypedExpectation {', + ' public:', + ' TypedExpectation& InSequence(const Sequence& s) { return *this; }', + ' TypedExpectation& InSequence(const Sequence& s1, const Sequence& s2) { return *this; }', + ' TypedExpectation& InSequence(const Sequence& s1, const Sequence& s2,', + ' const Sequence& s3) {', + ' return InSequence(s1, s2).InSequence(s3);', + ' }', + '};', + '} // namespace internal', + '} // namespace testing', + ), + }); + try { + expect(callsAt(cg, method(cg, 'testing::internal::TypedExpectation::InSequence', 9)).map((c) => c.replace(/ @.*$/, ''))).toEqual([ + '11 -> testing::internal::TypedExpectation::InSequence:7', + '11 -> testing::internal::TypedExpectation::InSequence:8', + ]); + } finally { + cg.close(); + } + }); + + it("is not an overload whose parameter a string literal can't be passed to", async () => { + // rocksdb's `db_->Get(ro, db_->DefaultColumnFamily(), "empty_key", &result)`. + const cg = await indexed({ + 'include/rocksdb/db.h': lines( + '#include ', + 'namespace rocksdb {', + 'struct ReadOptions {};', + 'class Slice { public: Slice(const char* s) {} };', + 'struct PinnableSlice {};', + 'struct ColumnFamilyHandle {};', + 'class DB {', + ' public:', + ' virtual int Get(const ReadOptions& options, const Slice& key, std::string* value, std::string* timestamp) = 0;', + ' virtual int Get(const ReadOptions& options, ColumnFamilyHandle* column_family, const Slice& key, PinnableSlice* value) = 0;', + '};', + 'int Read(DB* db, ColumnFamilyHandle* cf, PinnableSlice* result) {', + ' ReadOptions ro;', + ' return db->Get(ro, cf, "empty_key", result);', + '}', + '} // namespace rocksdb', + ), + }); + try { + const read = cg.getNodesByKind('function').find((n) => n.qualifiedName === 'rocksdb::Read')!; + expect(callsAt(cg, read)).toEqual(['14 -> rocksdb::DB::Get:10 @0.9']); + } finally { + cg.close(); + } + }); + + it("reads a method's defaults from its declaration in the class, and a raw string as a string literal", async () => { + // protobuf's io::Printer, whose overloads are defined below the class or in printer.cc. + const cg = await indexed({ + 'io/printer.h': lines( + 'namespace io {', + 'struct Sub {};', + 'class EnumDescriptor {};', + 'class Printer {', + ' public:', + ' void Annotate(absl::string_view varname, absl::string_view file_name, int semantic = 0) {}', + ' template ', + ' void Annotate(absl::string_view begin_varname, absl::string_view end_varname,', + ' const Desc* descriptor, int semantic = 0);', + ' void Emit(absl::string_view format, SourceLocation loc = SourceLocation::current());', + ' void Emit(absl::Span vars, absl::string_view format,', + ' SourceLocation loc = SourceLocation::current());', + '};', + 'template ', + 'void Printer::Annotate(absl::string_view begin_varname, absl::string_view end_varname,', + ' const Desc* descriptor, int semantic) {}', + 'inline void Printer::Emit(absl::string_view format, SourceLocation loc) {}', + '} // namespace io', + ), + 'io/printer.cc': lines( + '#include "io/printer.h"', + 'namespace io {', + 'void Printer::Emit(absl::Span vars, absl::string_view format, SourceLocation loc) {}', + '} // namespace io', + ), + 'compiler/enum.cc': lines( + '#include "io/printer.h"', + 'namespace io {', + 'void Generate(Printer* printer, const EnumDescriptor* descriptor) {', + ' printer->Annotate("{", "}", descriptor);', + ' printer->Emit({Sub()}, R"cc(', + ' $name$_ { visibility }', + ' )cc");', + '}', + '} // namespace io', + ), + }); + try { + const generate = cg.getNodesByKind('function').find((n) => n.qualifiedName === 'io::Generate')!; + expect(callsAt(cg, generate).map((c) => c.replace(/ @.*$/, ''))).toEqual([ + '4 -> io::Printer::Annotate:15', + '5 -> io::Printer::Emit:3', + ]); + } finally { + cg.close(); + } + }); + + it("reads an element of a container a subscript names", async () => { + // rocksdb's TransactionBaseImpl: `TryLock(column_family[i], keys[i], …)` over a + // `std::vector`, in the .cc that defines the SliceParts overload. + const cg = await indexed({ + 'utilities/transactions/transaction_base.h': lines( + 'namespace rocksdb {', + 'class Slice {};', + 'class SliceParts {};', + 'class ColumnFamilyHandle {};', + 'class TransactionBaseImpl {', + ' public:', + ' int TryLock(ColumnFamilyHandle* column_family, const Slice& key, bool read_only, bool exclusive) { return 0; }', + ' int TryLock(ColumnFamilyHandle* column_family, const SliceParts& key, bool read_only, bool exclusive);', + ' void MultiGetForUpdate(const std::vector& column_family, const std::vector& keys);', + '};', + '} // namespace rocksdb', + ), + 'utilities/transactions/transaction_base.cc': lines( + '#include "utilities/transactions/transaction_base.h"', + 'namespace rocksdb {', + 'int TransactionBaseImpl::TryLock(ColumnFamilyHandle* column_family, const SliceParts& key, bool read_only, bool exclusive) {', + ' return 1;', + '}', + 'void TransactionBaseImpl::MultiGetForUpdate(const std::vector& column_family,', + ' const std::vector& keys) {', + ' for (size_t i = 0; i < keys.size(); i++) {', + ' TryLock(column_family[i], keys[i], true, true);', + ' }', + '}', + '} // namespace rocksdb', + ), + }); + try { + const multiGet = cg.getNodesByKind('method').find((n) => n.qualifiedName === 'rocksdb::TransactionBaseImpl::MultiGetForUpdate')!; + const tryLock = cg.getOutgoingEdgesFrom([multiGet.id], ['calls']).map((e) => cg.getNode(e.target)).filter((n) => n?.name === 'TryLock'); + expect(tryLock.map((n) => `${n!.filePath}:${n!.startLine}`)).toEqual(['utilities/transactions/transaction_base.h:7']); + } finally { + cg.close(); + } + }); + + it('reads arguments and parameters past their comments', async () => { + const cg = await indexed({ + 'cache/cache.cc': lines( + '#include ', + 'namespace rocksdb {', + 'class Cache {', + ' public:', + ' int Insert(const std::string& key, const void* value, int hash) { return 1; }', + ' int Insert(const std::string& key, int hash, const void* value) { return 0; }', + ' void Fill(const std::string& key) {', + ' Insert(key, 0 /*hash*/, nullptr /*value*/);', + ' }', + '};', + 'class TextGenerator {', + ' public:', + ' void PrintEnum(int32_t /*val*/, const std::string& name) {}', + ' void PrintEnum(int32_t val, absl::string_view name) {', + ' PrintEnum(val, std::string(name));', + ' }', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(callsAt(cg, method(cg, 'rocksdb::Cache::Fill'))).toEqual(['8 -> rocksdb::Cache::Insert:6 @0.9']); + // (protobuf's text_format.cc: a parameter named in a comment is still an `int32_t`.) + expect(callsAt(cg, method(cg, 'rocksdb::TextGenerator::PrintEnum', 14))).toEqual(['15 -> rocksdb::TextGenerator::PrintEnum:13 @0.9']); + } finally { + cg.close(); + } + }); + + it('scores each kind of argument against each kind of parameter, whatever the order they are declared in', async () => { + const cg = await indexed({ + 'util/scorer.h': lines( + '#include ', + 'namespace rocksdb {', + 'class Slice { public: Slice(const char* s) {} };', + 'class Foo {};', + "char MakeChar() { return 'a'; }", + 'class Scorer {', + ' public:', + ' void Num(int32_t n) {}', + ' void Num(int64_t n) {}', + ' void Text(const Slice& s) {}', + ' void Text(absl::string_view s) {}', + ' void Value(const std::string& s) {}', + ' void Value(Foo f) {}', + ' void Count(int n) {}', + ' void Count(Foo f) {}', + ' void Ptr(const Slice* p) {}', + ' void Ptr(Foo f) {}', + ' void Lit(int n) {}', + ' void Lit(const char* s) {}', + ' void Key(const std::string& k) {}', + ' void Key(char k) {}', + ' void Run(int64_t wide, const std::string& name, int n) {', + ' Num(wide);', + ' Text(name);', + ' Value(n);', + ' Count(name);', + ' Ptr(n);', + ' Lit(R"(raw)");', + ' Key(MakeChar());', + ' }', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(callsAt(cg, method(cg, 'rocksdb::Scorer::Run')).map((c) => c.replace(/ @.*$/, ''))).toEqual([ + // The type itself over another of its kind. + '23 -> rocksdb::Scorer::Num:9', + // A string for a string type, over a class of another name. + '24 -> rocksdb::Scorer::Text:11', + // A number for no string, a string for no number, a number for no pointer. + '25 -> rocksdb::Scorer::Value:13', + '26 -> rocksdb::Scorer::Count:15', + '27 -> rocksdb::Scorer::Ptr:17', + // A raw string is a string literal. + '28 -> rocksdb::Scorer::Lit:19', + // What a call returns. + '29 -> rocksdb::MakeChar:5', + '29 -> rocksdb::Scorer::Key:21', + ]); + } finally { + cg.close(); + } + }); + + it("reads no type from another function's local of the name", async () => { + const cg = await indexed({ + 'cache/lru_cache_test.cc': lines( + '#include ', + 'namespace rocksdb {', + 'class LRUCacheTest {', + ' public:', + ' void Insert(const std::string& key) {}', + ' void Insert(char key) {}', + '};', + "void Earlier() { char ch = 'x'; }", + 'void Later(LRUCacheTest* t) {', + ' t->Insert(ch);', + '}', + '} // namespace rocksdb', + ), + }); + try { + const later = cg.getNodesByKind('function').find((n) => n.qualifiedName === 'rocksdb::Later')!; + expect(callsAt(cg, later).map((c) => c.replace(/ @.*$/, ''))).toEqual(['10 -> rocksdb::LRUCacheTest::Insert:5']); + } finally { + cg.close(); + } + }); + + it('keeps the first overload when the arguments say nothing', async () => { + const cg = await indexed({ + 'cache/lru_cache_test.cc': lines( + '#include ', + 'namespace rocksdb {', + 'class LRUCacheTest {', + ' public:', + ' void Insert(const std::string& key) {}', + ' void Insert(char key) {}', + ' void Fill() {', + " char ch = 'a';", + ' {', + ' auto ch = MakeKey();', + ' Insert(ch);', + ' }', + ' Insert(MakeKey());', + ' }', + '};', + '} // namespace rocksdb', + ), + }); + try { + // The inner `ch` is MakeKey()'s, which isn't known: not the outer `char ch`. + expect(callsAt(cg, method(cg, 'rocksdb::LRUCacheTest::Fill')).filter((c) => c.includes('Insert'))).toEqual([ + '11 -> rocksdb::LRUCacheTest::Insert:5 @0.9', + '13 -> rocksdb::LRUCacheTest::Insert:5 @0.9', + ]); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-qualified-declarator-receiver.test.ts b/__tests__/cpp-qualified-declarator-receiver.test.ts new file mode 100644 index 0000000000..817848f81e --- /dev/null +++ b/__tests__/cpp-qualified-declarator-receiver.test.ts @@ -0,0 +1,438 @@ +/** + * A C++ call on a variable, parameter or member whose declaration puts + * qualifiers or annotations around its name reaches the method of the + * declared type. + * + * Receiver inference reads a declaration as a type followed by the name. + * What may stand between them was read as the type: in facebook/rocksdb's + * `Iterator* const iter = iter_guard.get();` the type was `const`, which + * names nothing, so the scan went on to an earlier function's `iter` and + * guessed by its type's last name. The same held for a type written before + * its `const` (`Slice const& key`), for protobuf's nullability annotations + * (`Arena* PROTOBUF_NULLABLE arena`, read as a type named + * `PROTOBUF_NULLABLE`), and for an annotation after the name (leveldb's + * `VersionSet* const versions_ GUARDED_BY(mutex_);`, not read at all). + * + * 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-qualified-')); + 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`, one per call site. */ +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'); + +/** rocksdb's two `Iterator`s: the public one and the nested `MemTableRep::Iterator`. */ +const ITERATORS = lines( + 'namespace rocksdb {', + 'class Iterator {', + ' public:', + ' virtual ~Iterator() {}', + ' virtual void SeekToFirst() = 0;', + ' virtual bool Valid() const = 0;', + '};', + 'class MemTableRep {', + ' public:', + ' class Iterator {', + ' public:', + ' void SeekToFirst() {}', + ' bool Valid() const { return false; }', + ' };', + '};', + '} // namespace rocksdb', +); + +/** A class the calls are in the namespace of, declared in a header. */ +const UI_WIDGET = lines( + 'namespace ui {', + 'class Widget {', + ' public:', + ' void Draw() {}', + '};', + '} // namespace ui', +); + +/** A namesake beside the calls, which a guess by the type's last name prefers. */ +const LEGACY_WIDGET = lines( + 'namespace legacy {', + 'class Widget {', + ' public:', + ' void Draw() {}', + '};', + '} // namespace legacy', +); + +describe('C++ declarations with qualifiers or annotations around the name', () => { + it('reads `Iterator* const iter`, not an earlier function\'s `iter`', async () => { + const cg = await indexed({ + 'include/iterator.h': lines( + 'namespace rocksdb {', + 'class Iterator {', + ' public:', + ' virtual ~Iterator() {}', + ' virtual void SeekToFirst() = 0;', + ' virtual bool Valid() const = 0;', + '};', + '} // namespace rocksdb', + ), + // As in rocksdb's memtable.cc, the nested namesake sits beside the calls. + 'db/memtable.cc': lines( + '#include "include/iterator.h"', + 'namespace rocksdb {', + 'class MemTableRep {', + ' public:', + ' class Iterator {', + ' public:', + ' void SeekToFirst() {}', + ' bool Valid() const { return false; }', + ' };', + '};', + 'void Earlier() {', + ' MemTableRep::Iterator* iter = nullptr;', + ' iter->SeekToFirst();', + '}', + 'void Scan(Iterator* source) {', + ' Iterator* const iter = source;', + ' iter->SeekToFirst();', + ' if (iter->Valid()) {}', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Scan')).toEqual(['rocksdb::Iterator::SeekToFirst @0.9', 'rocksdb::Iterator::Valid @0.9']); + } finally { + cg.close(); + } + }); + + it('reads a type written before its `const` (`Slice const& key`)', async () => { + const cg = await indexed({ + 'util/slice.h': lines( + 'namespace leveldb {', + 'class Slice {', + ' public:', + ' int size() const { return 0; }', + '};', + 'namespace log {', + 'class Slice {', + ' public:', + ' int size() const { return 0; }', + '};', + '} // namespace log', + '} // namespace leveldb', + ), + 'util/length.cc': lines( + '#include "util/slice.h"', + 'namespace leveldb {', + 'int Length(Slice const& key) {', + ' return key.size();', + '}', + 'int Count(Slice const* key) {', + ' return key->size();', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::Length')).toEqual(['leveldb::Slice::size @0.9']); + expect(calls(cg, 'leveldb::Count')).toEqual(['leveldb::Slice::size @0.9']); + } finally { + cg.close(); + } + }); + + it('reads past a nullability annotation after the `*` (`Arena* PROTOBUF_NULLABLE arena`)', async () => { + const cg = await indexed({ + 'src/google/protobuf/arena.h': lines( + '#define PROTOBUF_NULLABLE', + '#define PROTOBUF_NONNULL', + 'namespace google {', + 'namespace protobuf {', + 'class Arena {', + ' public:', + ' void Reset() {}', + '};', + 'class ArenaStringPtr {', + ' public:', + ' void Reset() {}', + '};', + 'void Clear(Arena* PROTOBUF_NULLABLE arena) {', + ' arena->Reset();', + '}', + 'void ClearOwned(Arena* owner) {', + ' Arena* absl_nonnull arena = owner;', + ' arena->Reset();', + '}', + 'class Printer {', + ' public:', + ' void Print() {}', + '};', + 'class Generator {', + ' public:', + ' void Emit();', + ' private:', + ' Printer* PROTOBUF_NONNULL printer_;', + '};', + '} // namespace protobuf', + '} // namespace google', + ), + // A member read from its class, in a file of another name. + 'src/google/protobuf/generator_emit.cc': lines( + '#include "src/google/protobuf/arena.h"', + 'namespace google {', + 'namespace protobuf {', + 'class Formatter {', + ' public:', + ' void Print() {}', + '};', + 'void Generator::Emit() {', + ' printer_->Print();', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::Clear')).toEqual(['google::protobuf::Arena::Reset @0.9']); + expect(calls(cg, 'google::protobuf::ClearOwned')).toEqual(['google::protobuf::Arena::Reset @0.9']); + expect(calls(cg, 'google::protobuf::Generator::Emit')).toEqual(['google::protobuf::Printer::Print @0.9']); + } finally { + cg.close(); + } + }); + + it('reads a member with an annotation after its name (`VersionSet* const versions_ GUARDED_BY(mutex_);`)', async () => { + const cg = await indexed({ + 'db/version_set.h': lines( + 'namespace leveldb {', + 'class VersionSet {', + ' public:', + ' int LogNumber() const { return 0; }', + '};', + 'class VersionEdit {', + ' public:', + ' int LogNumber() const { return 0; }', + '};', + '} // namespace leveldb', + ), + 'db/db_impl.h': lines( + '#include "db/version_set.h"', + '#define GUARDED_BY(x)', + 'namespace leveldb {', + 'class DBImpl {', + ' public:', + ' void Recover();', + ' private:', + ' VersionSet* const versions_ GUARDED_BY(mutex_);', + '};', + '} // namespace leveldb', + ), + 'db/db_impl_open.cc': lines( + '#include "db/db_impl.h"', + 'namespace leveldb {', + 'void DBImpl::Recover() {', + ' versions_->LogNumber();', + '}', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::DBImpl::Recover')).toEqual(['leveldb::VersionSet::LogNumber @0.9']); + } finally { + cg.close(); + } + }); + + it('reads `auto* const` and smart-pointer locals, and range-for variables', async () => { + const cg = await indexed({ + 'ui/widget.h': UI_WIDGET, + 'ui/paint.cc': lines( + '#include ', + '#include ', + '#include "ui/widget.h"', + '#define PROTOBUF_NONNULL', + LEGACY_WIDGET, + 'namespace ui {', + 'void Paint() {', + ' auto* const w = new Widget();', + ' w->Draw();', + '}', + 'void PaintOwned(std::unique_ptr const w) {', + ' w->Draw();', + '}', + 'void PaintAll(std::vector& ws) {', + ' for (Widget* const w : ws) w->Draw();', + '}', + 'void PaintEach(std::vector>& ws) {', + ' for (std::unique_ptr const& w : ws) w->Draw();', + '}', + 'void PaintMarked(std::vector& ws) {', + ' for (Widget* PROTOBUF_NONNULL w : ws) w->Draw();', + '}', + '} // namespace ui', + ), + }); + try { + expect(calls(cg, 'ui::Paint')).toEqual(['ui::Widget::Draw @0.9']); + expect(calls(cg, 'ui::PaintOwned')).toEqual(['ui::Widget::Draw @0.9']); + expect(calls(cg, 'ui::PaintAll')).toEqual(['ui::Widget::Draw @0.9']); + expect(calls(cg, 'ui::PaintEach')).toEqual(['ui::Widget::Draw @0.9']); + expect(calls(cg, 'ui::PaintMarked')).toEqual(['ui::Widget::Draw @0.9']); + } finally { + cg.close(); + } + }); + + it('keeps an ALL-CAPS type name, which is no annotation before a `*`', async () => { + const cg = await indexed({ + 'handle.cc': lines( + 'struct OS_HANDLE {', + ' void Close() {}', + '};', + 'struct FILE_HANDLE {', + ' void Close() {}', + '};', + 'void Shut(OS_HANDLE* const h) {', + ' h->Close();', + '}', + ), + }); + try { + expect(calls(cg, 'Shut')).toEqual(['OS_HANDLE::Close @0.9']); + } finally { + cg.close(); + } + }); + + it('keeps the operator a qualified declaration takes: `.` on a reference, `->` on a pointer', async () => { + const cg = await indexed({ + 'ui/widget.h': UI_WIDGET, + 'ui/draw.cc': lines( + '#include "ui/widget.h"', + LEGACY_WIDGET, + 'namespace ui {', + 'void ByRef(Widget const& w) { w.Draw(); }', + 'void ByPtr(Widget const* const w) { w->Draw(); }', + '} // namespace ui', + ), + }); + try { + expect(calls(cg, 'ui::ByRef')).toEqual(['ui::Widget::Draw @0.9']); + expect(calls(cg, 'ui::ByPtr')).toEqual(['ui::Widget::Draw @0.9']); + } finally { + cg.close(); + } + }); + + it('reads globals the same-named header declares with qualifiers', async () => { + const cg = await indexed({ + 'registry.h': lines( + '#define PROTOBUF_NULLABLE', + 'class Registry {', + ' public:', + ' void Add() {}', + '};', + 'extern Registry* const g_registry;', + 'extern Registry* PROTOBUF_NULLABLE g_fallback;', + ), + 'registry.cc': lines( + '#include "registry.h"', + 'class Catalog {', + ' public:', + ' void Add() {}', + '};', + 'void Use() {', + ' g_registry->Add();', + '}', + 'void UseFallback() {', + ' g_fallback->Add();', + '}', + ), + }); + try { + expect(calls(cg, 'Use')).toEqual(['Registry::Add @0.9']); + expect(calls(cg, 'UseFallback')).toEqual(['Registry::Add @0.9']); + } finally { + cg.close(); + } + }); + + it('takes a parameter declared `std::function const Finish` for what a bare `Finish()` calls, not the method', async () => { + // C++ lookup no longer claims the call for the class's method; the name + // strategies decide, as for any parameter or local that shadows a member. + const cg = await indexed({ + 'job.cc': lines( + '#include ', + 'class Job {', + ' public:', + ' void Finish() {}', + ' void Run(std::function const Finish) {', + ' Finish();', + ' }', + '};', + ), + }); + try { + const resolvedBy = cg + .getCallees(callable(cg, 'Job::Run').id) + .filter((r) => r.edge.kind === 'calls') + .map((r) => (r.edge.metadata as { resolvedBy?: string } | undefined)?.resolvedBy); + expect(resolvedBy).not.toContain('instance-method'); + } finally { + cg.close(); + } + }); + + it('reads a declaration without qualifiers as before', async () => { + const cg = await indexed({ + 'include/iterator.h': ITERATORS, + 'db/plain.cc': lines( + '#include "include/iterator.h"', + 'namespace rocksdb {', + 'void Plain(Iterator* iter) {', + ' iter->SeekToFirst();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Plain')).toEqual(['rocksdb::Iterator::SeekToFirst @0.9']); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-test-fixture-call.test.ts b/__tests__/cpp-test-fixture-call.test.ts new file mode 100644 index 0000000000..da4b6dbb49 --- /dev/null +++ b/__tests__/cpp-test-fixture-call.test.ts @@ -0,0 +1,432 @@ +/** + * 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 not type `auto it = NewIterator()` from the fixture's method when a local lambda has that name", async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/db_lambda_test.cc': lines( + '#include "db/db_test_util.h"', + 'namespace rocksdb {', + NAMESAKES, + 'class DBLambdaTest : public DBTestBase {};', + 'TEST_F(DBLambdaTest, LocalLambda) {', + ' auto NewIterator = [&] { return 1; };', + ' auto it = NewIterator();', + ' it->SeekToFirst();', + '}', + '} // namespace rocksdb', + ), + }); + try { + // The call is to the local lambda, so `it` is no `Iterator*` of the fixture's method. + expect(calls(cg, 'rocksdb::TEST_F')).not.toContain('rocksdb::IteratorBase::SeekToFirst @0.9'); + expect(calls(cg, 'rocksdb::TEST_F')).not.toContain('rocksdb::DBTestBase::NewIterator @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/__tests__/cpp-translation-unit-visibility.test.ts b/__tests__/cpp-translation-unit-visibility.test.ts new file mode 100644 index 0000000000..714d71891b --- /dev/null +++ b/__tests__/cpp-translation-unit-visibility.test.ts @@ -0,0 +1,473 @@ +/** + * What a C or C++ source file declares for itself is no target of a name + * written in another file. + * + * A class or alias a `.cc` file declares, and whatever its unnamed namespace + * (`namespace { … }`) declares, belongs to that translation unit: another + * file can't name it. Name matching took them anyway. In facebook/rocksdb, + * other test files' bare `Put(…)` reached column_family_test.cc's own + * `ColumnFamilyTestBase::Put` 7,989 times, and inlineskiplist.h's `Node*` + * receivers reached the `Node` hash_linklist_rep.cc declares in an unnamed + * namespace. A source file's declarations are still seen by the file itself, + * by a file that includes it, and by a header it includes after them + * (rocksdb's version_set.cc includes version_set_sync_and_async.h below its + * `FilePicker` classes). + */ +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-tu-')); + 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 [file] @confidence`. */ +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.node.filePath}] @${(r.edge.metadata as { confidence?: number } | undefined)?.confidence}`) + .sort(); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +describe('C/C++ declarations local to another translation unit', () => { + it("is not another source file's class in an unnamed namespace", async () => { + const cg = await indexed({ + 'memtable/hash_linklist_rep.cc': lines( + 'namespace rocksdb {', + 'namespace {', + 'struct Node {', + ' Node* Next() { return nullptr; }', + '};', + '} // namespace', + '} // namespace rocksdb', + ), + 'memtable/inlineskiplist.h': lines( + 'namespace rocksdb {', + 'template ', + 'class InlineSkipList {', + ' private:', + ' struct Node;', + ' public:', + ' void Insert() {', + ' Node* x = head_;', + ' x->Next();', + ' }', + ' private:', + ' Node* head_;', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::InlineSkipList::Insert').filter((c) => c.includes('hash_linklist_rep.cc'))).toEqual([]); + } finally { + cg.close(); + } + }); + + it("is not another test file's fixture method a bare call names", async () => { + const cg = await indexed({ + 'db/column_family_test.cc': lines( + 'namespace rocksdb {', + 'class ColumnFamilyTestBase {', + ' public:', + ' void Put(int key) {}', + '};', + '} // namespace rocksdb', + ), + 'db/db_basic_test.cc': lines( + 'namespace rocksdb {', + 'void Put(int key);', + 'void Check() {', + ' Put(1);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Check').filter((c) => c.includes('column_family_test.cc'))).toEqual([]); + } finally { + cg.close(); + } + }); + + it("is not a function another source file's unnamed namespace declares", async () => { + const cg = await indexed({ + 'conformance/matchers.cc': lines( + 'namespace conformance {', + 'namespace {', + 'int RawPayload(int response) { return response; }', + '} // namespace', + '} // namespace conformance', + ), + 'conformance/matchers_test.cc': lines( + 'namespace conformance {', + 'int Describe() {', + ' return RawPayload(1);', + '}', + '} // namespace conformance', + ), + }); + try { + expect(calls(cg, 'conformance::Describe').filter((c) => c.includes('matchers.cc'))).toEqual([]); + } finally { + cg.close(); + } + }); + + it("is a member of the header's class, not of a source file's namesake", async () => { + // rocksdb declares a `SharedState` in db_stress_shared_state.h and two + // more of its own in cache_bench_tool.cc and db_bench_tool.cc. + const cg = await indexed({ + 'cache/cache_bench_tool.cc': lines( + 'namespace rocksdb {', + 'class SharedState {', + ' public:', + ' int* GetMutex() { return nullptr; }', + '};', + '} // namespace rocksdb', + ), + 'db_stress_tool/db_stress_shared_state.h': lines( + 'namespace rocksdb {', + 'class SharedState {', + ' public:', + ' int* GetMutex() { return nullptr; }', + '};', + '} // namespace rocksdb', + ), + 'db_stress_tool/db_stress_common.cc': lines( + '#include "db_stress_tool/db_stress_shared_state.h"', + 'namespace rocksdb {', + 'void Lock(SharedState* shared) {', + ' shared->GetMutex();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Lock')).toEqual(['rocksdb::SharedState::GetMutex [db_stress_tool/db_stress_shared_state.h] @0.9']); + } finally { + cg.close(); + } + }); + + it("is the header's class for a receiver a namespace-scope variable holds", async () => { + const cg = await indexed({ + 'cache/cache_bench_tool.cc': lines( + 'namespace rocksdb {', + 'class SharedState {', + ' public:', + ' int* GetMutex() { return nullptr; }', + '};', + '} // namespace rocksdb', + ), + 'db_stress_tool/db_stress_shared_state.h': lines( + 'namespace rocksdb {', + 'class SharedState {', + ' public:', + ' int* GetMutex() { return nullptr; }', + '};', + '} // namespace rocksdb', + ), + 'db_stress_tool/db_stress_common.cc': lines( + '#include "db_stress_tool/db_stress_shared_state.h"', + 'namespace rocksdb {', + 'SharedState* shared_state = nullptr;', + 'void LockShared() {', + ' shared_state->GetMutex();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::LockShared')).toEqual(['rocksdb::SharedState::GetMutex [db_stress_tool/db_stress_shared_state.h] @0.9']); + } finally { + cg.close(); + } + }); + + it("is a header's class, with no include the index follows", async () => { + const cg = await indexed({ + 'include/rocksdb/cache.h': lines( + 'namespace rocksdb {', + 'class Cache {', + ' public:', + ' virtual int Insert(int key) { return key; }', + '};', + '} // namespace rocksdb', + ), + 'cache/user.cc': lines( + 'namespace rocksdb {', + 'int Use(Cache* cache) {', + ' return cache->Insert(1);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Use')).toEqual(['rocksdb::Cache::Insert [include/rocksdb/cache.h] @0.9']); + } finally { + cg.close(); + } + }); + + it("is not an out-of-line method of a class another source file declares", async () => { + // protobuf's descriptor.cc declares `FileDescriptorTables` outside its + // unnamed namespaces and defines the class's methods below it. + const cg = await indexed({ + 'src/descriptor.cc': lines( + 'namespace google {', + 'namespace protobuf {', + 'class FileDescriptorTables {', + ' public:', + ' const int* FindFieldByLowercaseName(const char* name) const;', + '};', + 'const int* FileDescriptorTables::FindFieldByLowercaseName(const char* name) const {', + ' return nullptr;', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + 'python/message.cc': lines( + 'namespace google {', + 'namespace protobuf {', + 'namespace python {', + 'const int* Find(CMessage* self, const char* name) {', + ' return self->message_descriptor->FindFieldByLowercaseName(name);', + '}', + '} // namespace python', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::python::Find')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('is not a class another source file declares, named as a type', async () => { + // rocksdb's db/log_test.cc declares a `StringSource` of its own; other + // tests' `new test::StringSource(…)` are test_util's. + const cg = await indexed({ + 'db/log_test.cc': lines( + 'namespace rocksdb {', + 'namespace log {', + 'class StringSource {', + ' public:', + ' explicit StringSource(int contents) {}', + '};', + '} // namespace log', + '} // namespace rocksdb', + ), + 'db/plain_table_db_test.cc': lines( + 'namespace rocksdb {', + 'void* Make() {', + ' return new test::StringSource(1);', + '}', + '} // namespace rocksdb', + ), + }); + try { + const make = callable(cg, 'rocksdb::Make'); + expect(cg.getOutgoingEdgesFrom([make.id]).map((e) => cg.getNode(e.target)?.filePath).filter((f) => f === 'db/log_test.cc')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('reads an unnamed namespace past comments, preprocessor lines and literals that spell one', async () => { + const cg = await indexed({ + 'db/c.cc': lines( + 'namespace rocksdb {', + '// namespace { in a line comment', + '/* namespace { in a block comment */', + '#define ROCKSDB_ANONYMOUS_BEGIN namespace {', + 'const char* kDoc = "namespace {";', + 'const char* kRaw = R"(a "b namespace {" c)";', + 'int Helper(int x) { return x; }', + '} // namespace rocksdb', + ), + // A macro continued onto the next line, in a file checked out with CRLF. + 'db/crlf.cc': ['namespace rocksdb {', '#define ROCKSDB_ANONYMOUS_BEGIN \\', ' namespace {', 'int Other(int x) { return x; }', '} // namespace rocksdb', ''].join('\r\n'), + 'db/c_test.cc': lines( + 'namespace rocksdb {', + 'int Check() {', + ' return Helper(1) + Other(2);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Check')).toEqual(['rocksdb::Helper [db/c.cc] @0.9', 'rocksdb::Other [db/crlf.cc] @0.9']); + } finally { + cg.close(); + } + }); + + it("is still the file's own, and a header's it includes after the declaration", async () => { + const cg = await indexed({ + 'db/version_set.cc': lines( + 'namespace rocksdb {', + 'namespace {', + 'class FilePicker {', + ' public:', + ' int GetNextFile() { return 0; }', + '};', + '} // namespace', + 'int Pick(FilePicker& fp) {', + ' return fp.GetNextFile();', + '}', + '} // namespace rocksdb', + '#include "db/version_set_sync_and_async.h"', + ), + 'db/version_set_sync_and_async.h': lines( + 'namespace rocksdb {', + 'int PickAsync(FilePicker& fp) {', + ' return fp.GetNextFile();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Pick')).toEqual(['rocksdb::FilePicker::GetNextFile [db/version_set.cc] @0.9']); + expect(calls(cg, 'rocksdb::PickAsync')).toEqual(['rocksdb::FilePicker::GetNextFile [db/version_set.cc] @0.9']); + } finally { + cg.close(); + } + }); + + it('is seen by a file that includes the source file', async () => { + const cg = await indexed({ + 'util/impl.cc': lines( + 'namespace {', + 'class Impl {', + ' public:', + ' int Run() { return 0; }', + '};', + '} // namespace', + ), + 'util/impl_test.cc': lines( + '#include "util/impl.cc"', + 'int Test(Impl& impl) {', + ' return impl.Run();', + '}', + ), + }); + try { + expect(calls(cg, 'Test')).toEqual(['Impl::Run [util/impl.cc] @0.9']); + } finally { + cg.close(); + } + }); + + it('keeps an out-of-line method of a class a header declares, through an alias too', async () => { + const cg = await indexed({ + 'include/rocksdb/multi_scan.h': lines( + 'namespace rocksdb {', + 'class MultiScan {', + ' public:', + ' class MultiScanIterator {', + ' public:', + ' void SeekCurrentRange();', + ' void Next() { SeekCurrentRange(); }', + ' };', + '};', + 'class Table {', + ' public:', + ' int Open();', + '};', + '} // namespace rocksdb', + ), + 'db/multi_scan.cc': lines( + '#include "include/rocksdb/multi_scan.h"', + 'namespace rocksdb {', + 'using MultiScanIterator = MultiScan::MultiScanIterator;', + 'void MultiScanIterator::SeekCurrentRange() {}', + 'int Table::Open() { return 0; }', + '} // namespace rocksdb', + ), + 'db/open.cc': lines( + '#include "include/rocksdb/multi_scan.h"', + 'namespace rocksdb {', + 'int OpenTable(Table* t) {', + ' return t->Open();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::MultiScan::MultiScanIterator::Next').filter((c) => c.includes('SeekCurrentRange'))).toHaveLength(1); + expect(calls(cg, 'rocksdb::OpenTable')).toEqual(['rocksdb::Table::Open [db/multi_scan.cc] @0.9']); + } finally { + cg.close(); + } + }); + + it("keeps a namespace's function a source file defines out of line, beside another file's namesake struct", async () => { + // fmt's posix-mock-test.cc defines posix-mock.h's `test::close`, and + // ostream-test.cc declares a `struct test` of its own. + const cg = await indexed({ + 'test/posix-mock.h': lines('namespace test {', 'int close(int fildes);', '} // namespace test'), + 'test/posix-mock-test.cc': lines('#include "posix-mock.h"', 'int test::close(int fildes) { return fildes; }'), + 'test/ostream-test.cc': lines('struct test {};'), + 'test/os-test.cc': lines('#include "posix-mock.h"', 'int Shut(int fd) {', ' return test::close(fd);', '}'), + }); + try { + expect(calls(cg, 'Shut').filter((c) => c.startsWith('test::close'))).toHaveLength(1); + } finally { + cg.close(); + } + }); + + it("gives a class no base another source file declares", async () => { + const cg = await indexed({ + 'util/base.cc': lines( + 'namespace rocksdb {', + 'class Base {', + ' public:', + ' virtual void Run() {}', + '};', + '} // namespace rocksdb', + ), + 'util/derived.h': lines( + 'namespace rocksdb {', + 'class Derived : public Base {};', + '} // namespace rocksdb', + ), + }); + try { + const derived = cg.getNodesByKind('class').find((n) => n.qualifiedName === 'rocksdb::Derived')!; + expect(cg.getOutgoingEdgesFrom([derived.id], ['extends']).map((e) => cg.getNode(e.target)?.filePath)).toEqual([]); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-two-line-receivers.test.ts b/__tests__/cpp-two-line-receivers.test.ts new file mode 100644 index 0000000000..2e302a441c --- /dev/null +++ b/__tests__/cpp-two-line-receivers.test.ts @@ -0,0 +1,164 @@ +/** + * A C++ receiver whose declaration is written over more than one line: its + * type begun on a line above, or alone on the line above its name. Receiver + * inference read a declaration a line at a time, so the line naming the + * receiver declared nothing it could read, and the call fell to name matching: + * protocolbuffers/protobuf's `extension.number()` on a `const + * google::protobuf::internal::ExtensionIdentifier<` / `FeatureSet, + * TypeTraitsT, field_type, is_packed>& extension` reached hpb's + * `ExtensionIdentifier::number`. The lines are now read together. + */ +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'; + +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-two-line-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** What a function's or method's calls reach, as `qualifiedName resolvedBy@confidence`. */ +function calls(cg: CodeGraph, qualifiedName: string): string[] { + const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!; + return cg.getOutgoingEdgesFrom([from.id], ['calls']) + .map((e) => { + const meta = e.metadata as { confidence?: number; resolvedBy?: string } | undefined; + return `${cg.getNode(e.target)?.qualifiedName} ${meta?.resolvedBy}@${meta?.confidence}`; + }) + .sort(); +} + +const TABLES = lines( + '#include ', + '#include ', + 'template ', + 'class Table {', + ' public:', + ' int size() const { return 0; }', + '};', + 'class Index {', + ' public:', + ' int size() const { return 0; }', + '};', +); + +describe('C++ receivers declared over more than one line', () => { + it("reads a type begun on a line above: protobuf's ExtensionIdentifier parameter", async () => { + const cg = await indexed({ + 'src/google/protobuf/extension_set.h': lines( + '#include ', + 'namespace google {', + 'namespace protobuf {', + 'namespace internal {', + 'template ', + 'class ExtensionIdentifier {', + ' public:', + ' int number() const { return 0; }', + '};', + '} // namespace internal', + '} // namespace protobuf', + '} // namespace google', + ), + 'hpb/extension.h': lines( + 'namespace hpb {', + 'namespace internal {', + 'template ', + 'class ExtensionIdentifier {', + ' public:', + ' int number() const { return 0; }', + '};', + '} // namespace internal', + '} // namespace hpb', + ), + 'src/google/protobuf/descriptor.h': lines( + '#include "src/google/protobuf/extension_set.h"', + 'namespace google {', + 'namespace protobuf {', + 'class FeatureSet {};', + 'class DescriptorPool {', + ' public:', + ' template ', + ' bool ResolvesFeaturesFor(', + ' const google::protobuf::internal::ExtensionIdentifier<', + ' FeatureSet, TypeTraitsT, field_type, is_packed>& extension) const {', + ' return extension.number() > 0;', + ' }', + '};', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::DescriptorPool::ResolvesFeaturesFor')) + .toEqual(['google::protobuf::internal::ExtensionIdentifier::number instance-method@0.9']); + } finally { + cg.close(); + } + }); + + it('reads a type whose template arguments run over several lines above', async () => { + const cg = await indexed({ + 'tables.h': TABLES, + 'visit.cc': lines( + '#include "tables.h"', + 'int Visit(', + ' const Table<', + ' std::string,', + ' std::shared_ptr>& table) {', + ' return table.size();', + '}', + ), + }); + try { + expect(calls(cg, 'Visit')).toEqual(['Table::size instance-method@0.9']); + } finally { + cg.close(); + } + }); + + it('reads a type written alone on the line above the name, and no expression continued there', async () => { + const cg = await indexed({ + 'tables.h': TABLES, + 'build.cc': lines( + '#include "tables.h"', + 'struct Meters {', + ' int value() const { return 0; }', + '};', + 'Meters operator*(int k, const Meters& m);', + 'int Build() {', + ' Table>', + ' table;', + ' Table', + ' *table_ptr = nullptr;', + ' return table.size() + table_ptr->size();', + '}', + 'int Scaled(int factor, const Meters& length) {', + ' Meters total = factor', + ' * length;', + ' return length.value() + total.value();', + '}', + ), + }); + try { + expect(calls(cg, 'Build')).toEqual(['Table::size instance-method@0.9', 'Table::size instance-method@0.9']); + // `* length;` continues `Meters total = factor`: `length` is the parameter. + expect(calls(cg, 'Scaled')).toEqual(['Meters::value instance-method@0.9', 'Meters::value instance-method@0.9']); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/cpp-written-scope-class-names.test.ts b/__tests__/cpp-written-scope-class-names.test.ts new file mode 100644 index 0000000000..b0125308e9 --- /dev/null +++ b/__tests__/cpp-written-scope-class-names.test.ts @@ -0,0 +1,498 @@ +/** + * A C++ class, struct or union whose declaration writes its name with a + * scope — a nested class defined outside the class it is nested in — is + * indexed by its own name, qualified under that scope. + * + * google/leveldb defines `template struct + * SkipList::Node { … }` below the class, and facebook/rocksdb + * `struct BlockBasedTable::Rep { … }` in a source file. Each was indexed as a + * class named ``, so its members belonged to no class anyone + * names, and a call on a `Node*` or `Rep*` couldn't reach them: leveldb's + * `x->Next(level)` was guessed by the method's name. Such a class is now + * `Node`, qualified `leveldb::SkipList::Node` the way an out-of-line method + * `SkipList::Node::Next` already was, and its members are + * qualified under it. A specialization (`struct fmt::formatter`) + * keeps out of its template's name. + * + * A construction is recorded by its type's last name, so with + * `DBImpl::Writer` named, leveldb's `new log::Writer(…)` in a `DBImpl` method + * went to it. A construction now makes the class C++ lookup finds for the + * type as written there. + */ +import { describe, it, expect, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import type { Node } from '../src/types'; + +const roots: string[] = []; +afterEach(() => { + for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); +}); + +async function indexed(files: Record): Promise { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-scoped-class-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** Every class, struct and union node, as `kind qualifiedName`, with the members it contains. */ +function classes(cg: CodeGraph): Record { + const out: Record = {}; + for (const n of [...cg.getNodesByKind('class'), ...cg.getNodesByKind('struct'), ...cg.getNodesByKind('union')]) { + const members = cg.getOutgoingEdgesFrom([n.id], ['contains']) + .map((e) => cg.getNode(e.target)) + .filter((m): m is Node => m !== null && (m.kind === 'method' || m.kind === 'field' || m.kind === 'property')) + .map((m) => m.qualifiedName) + .sort(); + out[`${n.kind} ${n.qualifiedName}`] = members; + } + return out; +} + +const SKIPLIST = lines( + 'namespace leveldb {', + 'template ', + 'class SkipList {', + ' public:', + ' void Insert(const Key& key) {', + ' Node* x = head_;', + ' x->SetNext(0, x->Next(0));', + ' }', + ' private:', + ' struct Node;', + ' Node* head_;', + '};', + 'template ', + 'struct SkipList::Node {', + ' Node* Next(int n) { return nullptr; }', + ' void SetNext(int n, Node* x) {}', + '};', + 'class Table {', + ' public:', + ' int GlobalSeqno() const { return rep_->get_global_seqno(); }', + ' private:', + ' struct Rep;', + ' Rep* rep_;', + '};', + 'struct Table::Rep {', + ' int get_global_seqno() const { return 0; }', + '};', + '// Another class with methods of these names, so a guess by the name can miss.', + 'class MemTableRep {', + ' public:', + ' void* Next(int n) { return nullptr; }', + ' void SetNext(int n, void* x) {}', + ' int get_global_seqno() const { return 1; }', + '};', + '} // namespace leveldb', +); + +describe('C++ classes named with a scope or template arguments', () => { + it('names an out-of-line nested class, qualified with its members under its scope', async () => { + const cg = await indexed({ + 'db/skiplist.h': SKIPLIST, + // rocksdb's thread pool hides its state in a `class` defined in its source file. + 'util/threadpool.h': lines( + 'namespace rocksdb {', + 'class ThreadPoolImpl {', + ' public:', + ' int Size() const;', + ' private:', + ' class Impl;', + ' Impl* impl_;', + '};', + '} // namespace rocksdb', + ), + 'util/threadpool.cc': lines( + '#include "util/threadpool.h"', + 'namespace rocksdb {', + 'class ThreadPoolImpl::Impl {', + ' public:', + ' int GetQueueLen() const { return 0; }', + '};', + 'int ThreadPoolImpl::Size() const { return impl_->GetQueueLen(); }', + '} // namespace rocksdb', + ), + }); + try { + const all = classes(cg); + expect(all['struct leveldb::SkipList::Node']).toEqual(['leveldb::SkipList::Node::Next', 'leveldb::SkipList::Node::SetNext']); + expect(all['struct leveldb::Table::Rep']).toEqual(['leveldb::Table::Rep::get_global_seqno']); + expect(all['class rocksdb::ThreadPoolImpl::Impl']).toEqual(['rocksdb::ThreadPoolImpl::Impl::GetQueueLen']); + expect(Object.keys(all).filter((k) => k.includes(''))).toEqual([]); + } finally { + cg.close(); + } + }); + + it("leaves a specialization out of its template's name", async () => { + // Named `formatter`, fmt's specializations stood in for the template and + // for each other: `formatter : formatter` + // extended `formatter`. + const cg = await indexed({ + 'include/fmt/base.h': lines( + 'namespace fmt {', + 'template struct formatter {', + ' formatter() = delete;', + '};', + 'template <> struct formatter {', + ' auto format(int x) const { return x; }', + '};', + '} // namespace fmt', + ), + 'test/format-test.cc': lines( + '#include "include/fmt/base.h"', + 'struct Answer {};', + 'template <> struct fmt::formatter : fmt::formatter {};', + ), + }); + try { + const named = [...cg.getNodesByKind('struct'), ...cg.getNodesByKind('class')] + .filter((n) => n.name === 'formatter' || n.qualifiedName.includes('formatter')); + expect(named.map((n) => `${n.qualifiedName} ${n.filePath}:${n.startLine}`)).toEqual(['fmt::formatter include/fmt/base.h:2']); + } finally { + cg.close(); + } + }); + + it('leaves a class unnamed when its scope is no plain name', async () => { + const cg = await indexed({ + 'outer.h': lines( + 'namespace ns {', + 'template struct Outer { struct Inner; struct Other; };', + 'template struct Outer<(N > 1)>::Inner { int x; };', + 'struct decltype(Outer<1>())::Other { int y; };', + '} // namespace ns', + ), + }); + try { + expect(Object.keys(classes(cg)).sort()).toEqual(['struct ns::', 'struct ns::Outer']); + } finally { + cg.close(); + } + }); + + it("links a call on such a class's pointer to its method", async () => { + const cg = await indexed({ 'db/skiplist.h': SKIPLIST }); + try { + expect(targets(cg, 'leveldb::SkipList::Insert', 'calls')).toEqual(['leveldb::SkipList::Node::Next @0.9', 'leveldb::SkipList::Node::SetNext @0.9']); + expect(targets(cg, 'leveldb::Table::GlobalSeqno', 'calls')).toEqual(['leveldb::Table::Rep::get_global_seqno @0.9']); + } finally { + cg.close(); + } + }); +}); + +/** What a function's or method's `kind` edges reach, as `qualifiedName @confidence`. */ +function targets(cg: CodeGraph, qualifiedName: string, kind: 'calls' | 'instantiates'): string[] { + const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!; + return cg.getOutgoingEdgesFrom([from.id], [kind]) + .map((e) => `${cg.getNode(e.target)?.qualifiedName} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`) + .sort(); +} + +describe('C++ constructions of a type a class shares its name with', () => { + // leveldb's db_impl.cc constructs `log::Writer`s and, as `Writer w(…)`, the + // `DBImpl::Writer` it defines out of line: by the type's last name alone, + // the two were one. + const LEVELDB = { + 'db/log_writer.h': lines( + 'namespace leveldb {', + 'namespace log {', + 'class Writer {', + ' public:', + ' explicit Writer(int* dest) {}', + '};', + '} // namespace log', + '} // namespace leveldb', + ), + 'db/db_impl.h': lines( + '#include "db/log_writer.h"', + 'namespace leveldb {', + 'class DBImpl {', + ' public:', + ' void NewDB();', + ' void Write();', + ' private:', + ' struct Writer;', + ' log::Writer* log_;', + '};', + '} // namespace leveldb', + ), + 'db/db_impl.cc': lines( + '#include "db/db_impl.h"', + 'namespace leveldb {', + 'struct DBImpl::Writer {', + ' explicit Writer(int* mu) {}', + '};', + 'void DBImpl::NewDB() {', + ' int file = 0;', + ' log_ = new log::Writer(&file);', + ' const log::Writer scratch(&file);', + '}', + 'void DBImpl::Write() {', + ' int mu = 0;', + ' Writer w(&mu);', + '}', + '} // namespace leveldb', + ), + 'db/log_test.cc': lines( + '#include "db/log_writer.h"', + 'namespace leveldb {', + 'namespace log {', + 'class LogTest {', + ' public:', + ' void Reset() { writer_ = new Writer(nullptr); }', + ' private:', + ' Writer* writer_;', + '};', + '} // namespace log', + '} // namespace leveldb', + ), + }; + + it('makes the class C++ lookup finds for the type as written', async () => { + const cg = await indexed(LEVELDB); + try { + expect(targets(cg, 'leveldb::DBImpl::NewDB', 'instantiates')).toEqual(['leveldb::log::Writer @0.9', 'leveldb::log::Writer @0.9']); + expect(targets(cg, 'leveldb::DBImpl::Write', 'instantiates')).toEqual(['leveldb::DBImpl::Writer @0.9']); + expect(targets(cg, 'leveldb::log::LogTest::Reset', 'instantiates')).toEqual(['leveldb::log::Writer @0.9']); + } finally { + cg.close(); + } + }); + + it("tells apart two classes' nested classes of one name", async () => { + // rocksdb's `new BlockBasedTable::Rep(…)` reached `BlockBasedTableBuilder::Rep`. + const cg = await indexed({ + 'table/reader.h': lines( + 'namespace rocksdb {', + 'class BlockBasedTable {', + ' public:', + ' static void Open();', + ' struct Rep;', + '};', + 'struct BlockBasedTable::Rep {', + ' explicit Rep(int level) {}', + '};', + '} // namespace rocksdb', + ), + 'table/reader.cc': lines( + '#include "table/reader.h"', + 'namespace rocksdb {', + 'void BlockBasedTable::Open() {', + ' Rep* rep = new BlockBasedTable::Rep(0);', + '}', + '} // namespace rocksdb', + ), + 'table/builder.cc': lines( + '#include "table/reader.h"', + 'namespace rocksdb {', + 'class BlockBasedTableBuilder {', + ' public:', + ' void Finish();', + ' private:', + ' struct Rep;', + ' Rep* rep_;', + '};', + 'struct BlockBasedTableBuilder::Rep {', + ' explicit Rep(int level) {}', + '};', + 'void BlockBasedTableBuilder::Finish() {', + ' rep_ = new Rep(1);', + ' BlockBasedTable::Rep* other = new BlockBasedTable::Rep(2);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(targets(cg, 'rocksdb::BlockBasedTable::Open', 'instantiates')).toEqual(['rocksdb::BlockBasedTable::Rep @0.9']); + expect(targets(cg, 'rocksdb::BlockBasedTableBuilder::Finish', 'instantiates')) + .toEqual(['rocksdb::BlockBasedTable::Rep @0.9', 'rocksdb::BlockBasedTableBuilder::Rep @0.9']); + } finally { + cg.close(); + } + }); + + it('makes an enum, the class a local alias names, or nothing, where lookup finds no class', async () => { + // rocksdb declares a `Comparator` class, and its skip lists take one as a + // template parameter; protobuf's `CounterMap()` makes a `std::map` it + // calls `Map`, beside protobuf's own `Map`. + const cg = await indexed({ + 'util/counters.cc': lines( + '#include ', + '#include "util/comparator.h"', + 'namespace rocksdb {', + 'void* CounterMap() {', + ' using Bytes = std::map;', + ' return new Bytes{};', + '}', + 'void* Buffer() { return new Bytes(1); }', + 'void* Owned() {', + ' using Comparator = Bytes;', + ' return new Comparator(2);', + '}', + '} // namespace rocksdb', + ), + 'util/comparator.h': lines( + 'namespace rocksdb {', + 'class Comparator {', + ' public:', + ' explicit Comparator(int n) {}', + '};', + 'class Bytes {', + ' public:', + ' explicit Bytes(int n) {}', + '};', + 'class value_type {', + ' public:', + ' explicit value_type(char c) {}', + '};', + 'enum ValueType { kTypeValue, kTypeDeletion };', + '} // namespace rocksdb', + ), + 'memtable/skiplist.h': lines( + '#include ', + '#include "util/comparator.h"', + 'namespace rocksdb {', + 'namespace memtable {', + 'using Bytes = std::string;', + 'template ', + 'class SkipList {', + ' public:', + ' void Insert() {', + ' Comparator compare(1);', + ' Bytes key(8);', + " Bytes::value_type first('a');", + ' const ValueType types[] = {kTypeValue};', + ' }', + '};', + 'template ', + 'void Fill() { ValueType value(1); }', + '} // namespace memtable', + '} // namespace rocksdb', + ), + // An enum of the same name another source file declares for itself. + 'util/a.cc': lines( + 'namespace rocksdb {', + 'enum ValueType { kOther };', + '} // namespace rocksdb', + ), + }); + try { + expect(targets(cg, 'rocksdb::memtable::SkipList::Insert', 'instantiates')).toEqual(['rocksdb::ValueType @0.9', 'rocksdb::memtable::Bytes @0.9']); + const insert = cg.getNodesByKind('method').find((n) => n.qualifiedName === 'rocksdb::memtable::SkipList::Insert')!; + expect(cg.getOutgoingEdgesFrom([insert.id], ['instantiates']).map((e) => cg.getNode(e.target)!) + .filter((n) => n.kind === 'enum').map((n) => n.filePath)).toEqual(['util/comparator.h']); + expect(targets(cg, 'rocksdb::memtable::Fill', 'instantiates')).toEqual([]); + expect(targets(cg, 'rocksdb::CounterMap', 'instantiates')).toEqual([]); + expect(targets(cg, 'rocksdb::Buffer', 'instantiates')).toEqual(['rocksdb::Bytes @0.9']); + expect(targets(cg, 'rocksdb::Owned', 'instantiates')).toEqual(['rocksdb::Bytes @0.9']); + } finally { + cg.close(); + } + }); + + it('takes the class only the file sees at less than C++ lookup', async () => { + const cg = await indexed({ + 'widget.h': lines( + 'namespace ui {', + 'class Widget {', + ' public:', + ' explicit Widget(int n) {}', + '};', + '} // namespace ui', + ), + 'main.cc': lines( + '#include "widget.h"', + 'class App {', + ' public:', + ' void Run() { Widget w(1); }', + '};', + ), + }); + try { + expect(targets(cg, 'App::Run', 'instantiates')).toEqual(['ui::Widget @0.7']); + } finally { + cg.close(); + } + }); + + it("reads the type's template arguments, and tells same-named local classes apart by where they are", async () => { + const cg = await indexed({ + 'util/writer_test.cc': lines( + 'namespace rocksdb {', + 'template class Outer {', + ' public:', + ' class Inner {};', + '};', + 'class WriterTest {', + ' public:', + ' void Make() {', + ' auto* a = new Outer::Inner();', + ' auto* b = new Outer<(sizeof(int) > 2)>::Inner();', + ' }', + '};', + 'TEST_F(WriterTest, RangeSync) {', + ' class FakeWF {', + ' public:', + ' explicit FakeWF(int n) {}', + ' };', + ' FakeWF* wf = new FakeWF(1);', + '}', + 'TEST_F(WriterTest, IncrementalBuffer) {', + ' class FakeWF {', + ' public:', + ' explicit FakeWF(int n) {}', + ' };', + ' FakeWF* wf = new FakeWF(2);', + '}', + '} // namespace rocksdb', + ), + // googlemock's specializations, unnamed, each nest an `Impl` and an alias. + 'gmock/gmock.h': lines( + 'namespace testing {', + 'class Matchers {', + ' template class RefMatcher;', + ' template class RefMatcher {', + ' public:', + ' using Tuple = std::tuple;', + ' class Impl {};', + " // The next specialization's `Tuple` and `Impl` are nearer to", + ' // this `Make` than its own.', + ' //', + ' //', + ' //', + ' void* Make() { Tuple t(1); return new Impl(); }', + ' };', + ' template class RefMatcher {', + ' public:', + ' using Tuple = std::tuple;', + ' class Impl {};', + ' void* Make() { Tuple t(nullptr); return new Impl(); }', + ' };', + '};', + '} // namespace testing', + ), + }); + try { + // (Recorded by the name before its first `<`, `Outer`.) + expect(targets(cg, 'rocksdb::WriterTest::Make', 'instantiates')).toEqual(['rocksdb::Outer::Inner @0.9', 'rocksdb::Outer::Inner @0.9']); + const made = (callers: Node[]): string[][] => callers.sort((a, b) => a.startLine - b.startLine) + .map((fn) => cg.getOutgoingEdgesFrom([fn.id], ['instantiates']).map((e) => `${fn.startLine} -> ${cg.getNode(e.target)?.startLine}`).sort()); + expect(made(cg.getNodesByKind('function').filter((n) => n.name === 'TEST_F'))).toEqual([['13 -> 14'], ['20 -> 21']]); + expect(made(cg.getNodesByKind('method').filter((n) => n.filePath === 'gmock/gmock.h'))) + .toEqual([['13 -> 6', '13 -> 7'], ['19 -> 17', '19 -> 18']]); + } finally { + cg.close(); + } + }); +}); diff --git a/__tests__/fixtures/golden/torture-multilang.dump b/__tests__/fixtures/golden/torture-multilang.dump index 97524d883c..9de005b74e 100644 --- a/__tests__/fixtures/golden/torture-multilang.dump +++ b/__tests__/fixtures/golden/torture-multilang.dump @@ -3695,7 +3695,7 @@ {"source":"function:92e7a37a37fcdd1233f2060dcaa08d46","target":"function:510392b0722e8898090560466c5a5ec5","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"handler\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":100,"col":9,"provenance":null} {"source":"function:92e7a37a37fcdd1233f2060dcaa08d46","target":"function:5856f8137240d29ffd5ec6ced63378bc","kind":"references","metadata":"{\"confidence\":0.95,\"resolvedBy\":\"function-ref\",\"refName\":\"cbB\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":99,"col":18,"provenance":null} {"source":"function:92e7a37a37fcdd1233f2060dcaa08d46","target":"method:84b1066798bf4890531cf6d4cd0b61b2","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"make\"}","line":93,"col":2,"provenance":null} -{"source":"function:9374676602bba21f257767ff6086419a","target":"class:0083dc18baa2b629e8572db20ba2b464","kind":"instantiates","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":133,"col":31,"provenance":null} +{"source":"function:9374676602bba21f257767ff6086419a","target":"class:0083dc18baa2b629e8572db20ba2b464","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget\"}","line":133,"col":31,"provenance":null} {"source":"function:973ef3de44975d37a59ae5993585dcb8","target":"enum:fa2b2411f27fe00e081bceff1312896a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Flag\"}","line":31,"col":13,"provenance":null} {"source":"function:973ef3de44975d37a59ae5993585dcb8","target":"enum:fa2b2411f27fe00e081bceff1312896a","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Flag\"}","line":32,"col":12,"provenance":null} {"source":"function:975ff1961c427064617a254938d60cd1","target":"enum:fcb9d383c69413c946db24cec4719f95","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"ColorT\"}","line":217,"col":12,"provenance":null} @@ -3715,8 +3715,8 @@ {"source":"function:a17e01edc91eda2d9ff83f0051193fa3","target":"function:9374676602bba21f257767ff6086419a","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"make_widget\"}","line":142,"col":2,"provenance":null} {"source":"function:a17e01edc91eda2d9ff83f0051193fa3","target":"method:35a476809e2727e80d7f2c8c404180b2","kind":"calls","metadata":"{\"confidence\":0.85,\"resolvedBy\":\"instance-method\",\"refName\":\"make_widget().Tick\"}","line":142,"col":2,"provenance":null} {"source":"function:a17e01edc91eda2d9ff83f0051193fa3","target":"method:8ce5cf7b5413599d7a690c53b25e19f4","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"function-ref\",\"refName\":\"app::Config::apply\",\"refKind\":\"function_ref\",\"fnRef\":true}","line":153,"col":13,"provenance":null} -{"source":"function:a17e01edc91eda2d9ff83f0051193fa3","target":"struct:08acded06d18f155491d1e66e44ba80b","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Vec2\"}","line":138,"col":2,"provenance":null} -{"source":"function:a17e01edc91eda2d9ff83f0051193fa3","target":"struct:08acded06d18f155491d1e66e44ba80b","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Vec2\"}","line":140,"col":2,"provenance":null} +{"source":"function:a17e01edc91eda2d9ff83f0051193fa3","target":"struct:08acded06d18f155491d1e66e44ba80b","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Vec2\"}","line":138,"col":2,"provenance":null} +{"source":"function:a17e01edc91eda2d9ff83f0051193fa3","target":"struct:08acded06d18f155491d1e66e44ba80b","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Vec2\"}","line":140,"col":2,"provenance":null} {"source":"function:a17e01edc91eda2d9ff83f0051193fa3","target":"variable:6bc194c6a17ac46928d714fb50070e52","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"GlobalConfig\"}","line":148,"col":14,"provenance":null} {"source":"function:a1998a5fcada0a95b1aebb30d3f1222c","target":"class:03853e00aca8d3407b8504dc2c5cdfe8","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"FactoryT\"}","line":77,"col":2,"provenance":null} {"source":"function:a1998a5fcada0a95b1aebb30d3f1222c","target":"class:e1f5ba7cde2f24dbd5d8d72eeba2b2d6","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"ConfigT\"}","line":70,"col":2,"provenance":null} @@ -3747,11 +3747,11 @@ {"source":"function:ae77b9f848fb3b2cb309ca8e5667e81b","target":"function:0499cc5aef87518260d547d716f829ce","kind":"calls","metadata":"{\"synthesizedBy\":\"rtk-query\",\"via\":\"getWidget\",\"registeredAt\":\"torture.tsx:184\"}","line":184,"col":null,"provenance":"heuristic"} {"source":"function:b06c2d27fa49d5e97af860ffe032fbdd","target":"class:fa355652331eee519e97b9ef15a61b2d","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetT\"}","line":49,"col":34,"provenance":null} {"source":"function:b06c2d27fa49d5e97af860ffe032fbdd","target":"class:fa355652331eee519e97b9ef15a61b2d","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetT\"}","line":49,"col":5,"provenance":null} -{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"class:185f6260a8d5b738c53be0accb2a4e19","kind":"instantiates","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":40,"col":2,"provenance":null} -{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"class:185f6260a8d5b738c53be0accb2a4e19","kind":"instantiates","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":41,"col":2,"provenance":null} -{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"class:185f6260a8d5b738c53be0accb2a4e19","kind":"instantiates","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":42,"col":2,"provenance":null} -{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"class:185f6260a8d5b738c53be0accb2a4e19","kind":"instantiates","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":43,"col":2,"provenance":null} -{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"class:185f6260a8d5b738c53be0accb2a4e19","kind":"instantiates","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":44,"col":2,"provenance":null} +{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"class:185f6260a8d5b738c53be0accb2a4e19","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget\"}","line":40,"col":2,"provenance":null} +{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"class:185f6260a8d5b738c53be0accb2a4e19","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget\"}","line":41,"col":2,"provenance":null} +{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"class:185f6260a8d5b738c53be0accb2a4e19","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget\"}","line":42,"col":2,"provenance":null} +{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"class:185f6260a8d5b738c53be0accb2a4e19","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget\"}","line":43,"col":2,"provenance":null} +{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"class:185f6260a8d5b738c53be0accb2a4e19","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget\"}","line":44,"col":2,"provenance":null} {"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"constant:f91b4dd2faf508ed32bf113beb9f745e","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"method:36b2c35b96abd233c6449d02d9c4ef2f","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Box::Box/1\"}","line":45,"col":2,"provenance":null} {"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"method:7e714128b5b3c81a4e4f571a142197ba","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget::Widget/0\"}","line":39,"col":2,"provenance":null} @@ -3760,13 +3760,13 @@ {"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"method:7e714128b5b3c81a4e4f571a142197ba","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget\"}","line":46,"col":2,"provenance":null} {"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"method:f73f31bd6cc45134433935ad6eaae9f9","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget::Widget/2\"}","line":42,"col":2,"provenance":null} {"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"method:f73f31bd6cc45134433935ad6eaae9f9","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget::Widget/2\"}","line":43,"col":2,"provenance":null} -{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"struct:dc1fb066a6c08cc705216a045fa93a24","kind":"instantiates","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"Box\"}","line":45,"col":2,"provenance":null} -{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"struct:dcb8897c9696246c318260fb9188d7fb","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Aggregate\"}","line":38,"col":2,"provenance":null} +{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"struct:dc1fb066a6c08cc705216a045fa93a24","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Box\"}","line":45,"col":2,"provenance":null} +{"source":"function:b3507638f9afee3fde16cc943fd670cf","target":"struct:dcb8897c9696246c318260fb9188d7fb","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Aggregate\"}","line":38,"col":2,"provenance":null} {"source":"function:b8275c2cd511278505b4f6a60897df9c","target":"struct:ab1ddae19b44311cc03a2493e898f4db","kind":"contains","metadata":null,"line":null,"col":null,"provenance":null} {"source":"function:bb0cdf86bfb5dc9391a3d5fca445ebc2","target":"function:bb0cdf86bfb5dc9391a3d5fca445ebc2","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"tailer\"}","line":40,"col":53,"provenance":null} {"source":"function:bcafa05abfafe5a4b4bc5c8b3f7e3e0d","target":"class:fa355652331eee519e97b9ef15a61b2d","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetT\"}","line":50,"col":0,"provenance":null} {"source":"function:c062ca02de706ffe85e7dfb2f9586331","target":"class:c2dfee99ca5361d8de7ae7de6858d0bf","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"WidgetK\",\"refKind\":\"calls\"}","line":13,"col":11,"provenance":null} -{"source":"function:c2971e4b1912de92c9c1e02416630c6b","target":"class:185f6260a8d5b738c53be0accb2a4e19","kind":"instantiates","metadata":"{\"confidence\":0.4,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":59,"col":2,"provenance":null} +{"source":"function:c2971e4b1912de92c9c1e02416630c6b","target":"class:185f6260a8d5b738c53be0accb2a4e19","kind":"instantiates","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget\"}","line":59,"col":2,"provenance":null} {"source":"function:c2971e4b1912de92c9c1e02416630c6b","target":"method:7e714128b5b3c81a4e4f571a142197ba","kind":"calls","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"qualified-name\",\"refName\":\"Widget::Widget/0\"}","line":59,"col":2,"provenance":null} {"source":"function:c5b82c471b23eb80ba82537084522458","target":"struct:0c309a8ed6f33456724d94ba73599e04","kind":"references","metadata":"{\"confidence\":0.9,\"resolvedBy\":\"exact-match\",\"refName\":\"Widget\"}","line":200,"col":34,"provenance":null} {"source":"function:c97d0d5969167c7958098f13defe9e08","target":"function:89b5279cd19a084e8974e1c0985c27d9","kind":"calls","metadata":"{\"confidence\":0.5,\"resolvedBy\":\"exact-match\",\"refName\":\"fetchUser\"}","line":61,"col":23,"provenance":null} diff --git a/codegraph-kernel/src/ccpp/mod.rs b/codegraph-kernel/src/ccpp/mod.rs index 8b7702512f..6db8c26309 100644 --- a/codegraph-kernel/src/ccpp/mod.rs +++ b/codegraph-kernel/src/ccpp/mod.rs @@ -351,6 +351,11 @@ pub struct Walker<'t> { brace_scopes: Option, class_scopes: NestedIntervals, class_scope_rows: HashSet, + /// The qualified name a class written in a scope (`struct SkipList::Node`) is known by, by its row: its members are qualified + /// under it in place of the scopes around it (objectPathOwners in + /// tree-sitter.ts, written_scope_qualified_name). + written_scope_rows: HashMap, walk_declared_types: bool, } @@ -411,6 +416,7 @@ impl<'t> Walker<'t> { brace_scopes: None, class_scopes: NestedIntervals::new(), class_scope_rows: HashSet::new(), + written_scope_rows: HashMap::new(), walk_declared_types: false, } } @@ -436,7 +442,10 @@ impl<'t> Walker<'t> { let qualified = extra.qualified_name.unwrap_or_else(|| { let mut parts: Vec<&str> = self.namespace_prefix.iter().map(|s| s.as_str()).collect(); for s in &self.stack { - if s.kind != "file" { + if let Some(q) = self.written_scope_rows.get(&s.row) { + parts.clear(); + parts.push(q); + } else if s.kind != "file" { parts.push(&s.name); } } diff --git a/codegraph-kernel/src/ccpp/names.rs b/codegraph-kernel/src/ccpp/names.rs index 2e28921d2a..11395108d1 100644 --- a/codegraph-kernel/src/ccpp/names.rs +++ b/codegraph-kernel/src/ccpp/names.rs @@ -16,6 +16,55 @@ impl<'t> Walker<'t> { Some(self.text(identifier).to_string()) } + /// cppWrittenClassParts (languages/c-cpp.ts): the `::` segments of a class, + /// struct or union name written with a scope, the scope's template + /// arguments dropped (`["SkipList", "Node"]` for `struct SkipList::Node`). None for a plain name and for a specialization, + /// whose innermost name has template arguments. + fn cpp_written_class_parts(&self, node: Node) -> Option> { + if !matches!(node.kind(), "class_specifier" | "struct_specifier" | "union_specifier") { + return None; + } + let written = node.child_by_field_name("name")?; + if written.kind() != "qualified_identifier" { + return None; + } + let mut innermost = Some(written); + while let Some(n) = innermost.filter(|n| n.kind() == "qualified_identifier") { + innermost = n.child_by_field_name("name"); + } + if innermost.map(|n| n.kind()) != Some("type_identifier") { + return None; + } + let parts: Vec = strip_cpp_template_args(self.text(written)) + .split("::") + .map(|p| p.trim().to_string()) + .filter(|p| !p.is_empty()) + .collect(); + let identifier = |p: &String| { + let mut chars = p.chars(); + chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_') + && chars.all(|c| c.is_ascii_alphanumeric() || c == '_') + }; + if parts.is_empty() || !parts.iter().all(identifier) { + return None; + } + Some(parts) + } + + /// writtenScopeQualifiedName (tree-sitter.ts): a class whose name is + /// written in a scope is qualified like an out-of-line method under it. + pub(super) fn written_scope_qualified_name(&self, node: Node, name: &str) -> Option { + if self.variant != Variant::Cpp { + return None; + } + let parts = self.cpp_written_class_parts(node)?; + if parts.len() < 2 { + return None; + } + Some(self.compose_receiver_qualified_name(&parts[..parts.len() - 1].join("::"), name)) + } + /// extractNameRaw for the c/cpp extractor configs (nameField 'declarator'; /// cpp resolveName = extractCppQualifiedMethodName). pub(super) fn extract_name_raw(&self, node: Node) -> String { @@ -35,6 +84,11 @@ impl<'t> Walker<'t> { } } if let Some(name) = self.unnamed_type_variable_name(node) { return name; } + if self.variant == Variant::Cpp { + if let Some(mut parts) = self.cpp_written_class_parts(node) { + return parts.pop().unwrap_or_default(); + } + } if let Some(name_node) = node.child_by_field_name("declarator") { let mut resolved = name_node; // Unwrap pointer/reference declarators (`int* f()`, `T& f()`). diff --git a/codegraph-kernel/src/ccpp/types.rs b/codegraph-kernel/src/ccpp/types.rs index 227919b74e..4917c2a752 100644 --- a/codegraph-kernel/src/ccpp/types.rs +++ b/codegraph-kernel/src/ccpp/types.rs @@ -13,13 +13,18 @@ impl<'t> Walker<'t> { stack_guard!(); let Some(body) = node.child_by_field_name("body") else { return }; let name = self.extract_name(node); + let scoped = self.written_scope_qualified_name(node, &name); let extra = Extra { docstring: self.class_like_docstring(node), visibility: self.visibility_of(node), end: self.brace_body_end(body), + qualified_name: scoped.clone(), ..Extra::default() }; let Some(row) = self.create_node("class", &name, node, extra) else { return }; + if let Some(q) = scoped { + self.written_scope_rows.insert(row, q); + } self.extract_inheritance(node, row); let scope = Scope { row, kind: "class", name }; self.open_class_scope(&scope, body); @@ -35,13 +40,18 @@ impl<'t> Walker<'t> { stack_guard!(); let Some(body) = node.child_by_field_name("body") else { return }; let name = self.extract_name(node); + let scoped = self.written_scope_qualified_name(node, &name); let extra = Extra { docstring: self.class_like_docstring(node), visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None }, end: self.brace_body_end(body), + qualified_name: scoped.clone(), ..Extra::default() }; let Some(row) = self.create_node(kind, &name, node, extra) else { return }; + if let Some(q) = scoped { + self.written_scope_rows.insert(row, q); + } self.extract_inheritance(node, row); let scope = Scope { row, kind, name }; self.open_class_scope(&scope, body); diff --git a/codegraph-kernel/src/resolve/bound.rs b/codegraph-kernel/src/resolve/bound.rs index 86f4dfb0cf..5f374acf5b 100644 --- a/codegraph-kernel/src/resolve/bound.rs +++ b/codegraph-kernel/src/resolve/bound.rs @@ -726,6 +726,26 @@ if site.language == "dart" && site.reference_kind == "calls" { }) .cloned() .collect(); + if members.len() > 1 && site.language == "cpp" && site.reference_kind == "calls" && !site.reference_name.contains("().") { + // A C++ call on a typed receiver takes the overload its arguments fit, not the first + // one declared (a chained call `inner(…).name(…)` keeps the first: the arguments at + // its position are the inner call's). + // When other classes share the owner's qualified name, its own overloads are the ones + // defined in its body, else in its file or a file that file is included by (leveldb's + // db_bench.cc and db_bench_sqlite3.cc each declare a `RandomGenerator`). + let ordered = prefer_call_site_file(self.cpp_visible_first(members, site)?, &site.file_path); + let defined: Vec<_> = if self.cpp_class_decls(&owner.qualified_name)?.len() > 1 { + let in_body: Vec<_> = ordered.iter().filter(|n| n.file_path == owner.file_path && n.start_line >= owner.start_line && n.end_line <= owner.end_line).cloned().collect(); + if in_body.is_empty() { + let mut near = Vec::new(); + for n in &ordered { + if n.file_path == owner.file_path || self.namespace_visible_files(&n.file_path, "cpp")?.contains(&owner.file_path) { near.push(n.clone()); } + } + near + } else { in_body } + } else { ordered.clone() }; + return self.cpp_overload_for(if defined.is_empty() { ordered } else { defined }, method, site).map(Some); + } Ok(if members.len() == 1 { members.into_iter().next() } else { @@ -1273,6 +1293,15 @@ Ok(self.dart_member_of(type_name, method, r, false)? } return Ok(None); } + let matches = self.cpp_visible_first(matches, r)?; + // A C++ call on a typed receiver takes the overload its arguments fit, not the first one + // declared (a chained call `inner(…).name(…)` keeps the first: the arguments at its + // position are the inner call's). + if r.language == "cpp" && r.reference_kind == "calls" && matches.len() > 1 && !r.reference_name.contains("().") { + let ordered = prefer_call_site_file(matches, &r.file_path); + let node = self.cpp_overload_for(ordered, method, r)?; + return Ok(Some(KCand { node, confidence, resolved_by })); + } Ok(Self::pick_method_match(matches, r, confidence, resolved_by, preferred_fqn)) } diff --git a/codegraph-kernel/src/resolve/cpp_aliases.rs b/codegraph-kernel/src/resolve/cpp_aliases.rs index 03bd9762e6..240ca649f8 100644 --- a/codegraph-kernel/src/resolve/cpp_aliases.rs +++ b/codegraph-kernel/src/resolve/cpp_aliases.rs @@ -11,7 +11,22 @@ pub(super) struct CppAlias { pub(super) fn type_segments(raw: &str) -> Option> { let mut text = String::new(); let mut depth = 0usize; + let mut parens = 0usize; + let mut quote = None; + let mut escaped = false; + let mut prev = ' '; for c in raw.chars() { + let before = std::mem::replace(&mut prev, c); + // (A quoted character or string in a template argument holds no grouping: `Bits<('(' == c)>`.) + if let Some(open) = quote { + if escaped { escaped = false; } else if c == '\\' { escaped = true; } else if c == open { quote = None; } + continue; + } + // (A `'` after a digit or letter separates digits: `1'000`.) + if depth > 0 && (c == '"' || (c == '\'' && !before.is_alphanumeric())) { quote = Some(c); continue; } + // (In template arguments, a `<` or `>` in parentheses compares: `Bits<(N > 8)>`.) + if depth > 0 && matches!(c, '(' | ')') { parens = if c == '(' { parens + 1 } else { parens.saturating_sub(1) }; continue; } + if parens > 0 { continue; } match c { '<' => depth += 1, '>' => depth = depth.saturating_sub(1), @@ -202,7 +217,7 @@ mod tests { use super::type_segments; #[test] fn nested_type_segments() { - for (raw, expected) in [("SkipList", "SkipList"), ("const ::leveldb::SkipList>::Iterator&", "leveldb::SkipList::Iterator"), ("typename Base::Iter", "Base::Iter"), ("struct Node*", "Node")] { + for (raw, expected) in [("SkipList", "SkipList"), ("const ::leveldb::SkipList>::Iterator&", "leveldb::SkipList::Iterator"), ("typename Base::Iter", "Base::Iter"), ("struct Node*", "Node"), ("Bits<('(' == '(')>::Nested", "Bits::Nested"), ("Bits<(\")\" == s)>::Nested", "Bits::Nested"), ("Bits<(1'000 > 8)>::Nested", "Bits::Nested")] { assert_eq!(type_segments(raw).unwrap().join("::"), expected); } assert!(type_segments("unsigned int").is_none()); diff --git a/codegraph-kernel/src/resolve/cpp_annotations.rs b/codegraph-kernel/src/resolve/cpp_annotations.rs new file mode 100644 index 0000000000..c5f41db3f7 --- /dev/null +++ b/codegraph-kernel/src/resolve/cpp_annotations.rs @@ -0,0 +1,214 @@ +//! The C++ the resolver parses has the annotation macros extraction blanks before parsing blanked +//! the same way, so a declaration reads as extraction read it: protobuf's `Printer* +//! PROTOBUF_NONNULL printer_;` declares `printer_`, and leveldb's `VersionSet* const versions_ +//! GUARDED_BY(mutex_);` declares `versions_`. tree-sitter takes such a macro for the declared name, +//! or for a second declarator, and errors. +//! +//! Port of two of c-cpp.ts's preParse passes, `blankCppPointerAnnotationMacros` and +//! `blankCppDeclaratorAttributeMacros`, matched on the code with comments and literals masked. +//! Each macro is replaced by spaces, so every byte offset stays where it was. +use std::borrow::Cow; +use std::rc::Rc; +use std::sync::LazyLock; + +use regex::Regex; + +/// `text` with comments and the contents of string and character literals (raw strings included) +/// replaced by spaces, byte for byte (line breaks kept). +pub(super) fn masked_code(text: &str) -> Vec { + let bytes = text.as_bytes(); + let mut out = bytes.to_vec(); + let blank = |out: &mut Vec, from: usize, to: usize| { + for b in &mut out[from..to.min(bytes.len())] { if *b != b'\n' && *b != b'\r' { *b = b' '; } } + }; + let mut i = 0; + while i < bytes.len() { + match bytes[i] { + b'/' if bytes.get(i + 1) == Some(&b'/') => { + let end = text[i..].find('\n').map_or(bytes.len(), |n| i + n); + blank(&mut out, i, end); + i = end; + } + b'/' if bytes.get(i + 1) == Some(&b'*') => { + let end = text[i + 2..].find("*/").map_or(bytes.len(), |n| i + 2 + n + 2); + blank(&mut out, i, end); + i = end; + } + b'R' if bytes.get(i + 1) == Some(&b'"') && (i == 0 || !(bytes[i - 1].is_ascii_alphanumeric() || bytes[i - 1] == b'_') || matches!(bytes[i - 1], b'8' | b'u' | b'U' | b'L')) => { + let open = i + 1; + let Some(paren) = text[open..].find('(').map(|p| open + p) else { i += 1; continue }; + let closing = format!("){}\"", &text[open + 1..paren]); + let end = text[paren..].find(&closing).map_or(bytes.len(), |n| paren + n + closing.len()); + blank(&mut out, open, end); + i = end; + } + // (A `'` after a digit or letter separates digits: `1'000'000`.) + b'\'' if i > 0 && bytes[i - 1].is_ascii_alphanumeric() => i += 1, + quote @ (b'"' | b'\'') => { + let mut j = i + 1; + while j < bytes.len() && bytes[j] != quote && bytes[j] != b'\n' { + j += if bytes[j] == b'\\' { 2 } else { 1 }; + } + blank(&mut out, i + 1, j.min(bytes.len())); + i = j + 1; + } + _ => i += 1, + } + } + out +} + +fn is_word(b: u8) -> bool { b.is_ascii_alphanumeric() || b == b'_' } + +/// The end of the ALL-CAPS annotation macro name (`[A-Z][A-Z0-9]*_[A-Z0-9_]*`) starting at `at`. +fn macro_name_end(code: &[u8], at: usize) -> Option { + if !code.get(at)?.is_ascii_uppercase() || (at > 0 && is_word(code[at - 1])) { return None; } + let mut end = at; + while end < code.len() && is_word(code[end]) { end += 1; } + let name = &code[at..end]; + (name.contains(&b'_') && name.iter().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || *b == b'_')).then_some(end) +} + +fn skip_space(code: &[u8], mut i: usize) -> usize { + while i < code.len() && code[i].is_ascii_whitespace() { i += 1; } + i +} + +/// The end of the balanced argument list opening at `open`, three levels deep at most. +fn arguments_end(code: &[u8], open: usize) -> Option { + let mut depth = 0; + for (i, &b) in code.iter().enumerate().skip(open) { + match b { + b'(' => { depth += 1; if depth > 3 { return None; } } + b')' => { depth -= 1; if depth == 0 { return Some(i + 1); } } + b';' | b'{' | b'}' => return None, + _ => {} + } + } + None +} + +const ALTERNATIVE_OPERATORS: &[&str] = &["and", "and_eq", "bitand", "bitor", "compl", "not", "not_eq", "or", "or_eq", "xor", "xor_eq"]; +const NOT_A_TYPE: &[&str] = &["return", "throw", "co_return", "case", "goto", "else", "do", "new", "delete", "sizeof", "typedef", "using"]; + +/// The spans of the macros right after a `*` or `&` and before the name it declares (or another +/// `*`): `Arena* PROTOBUF_NULLABLE arena`, `const FieldDescriptor* PROTOBUF_NULLABLE* PROTOBUF_NONNULL +/// field`. Not after a `)`, `]` or number (a product), in a `return` or an assignment's value, or +/// when nothing follows (`Foo* PROTOBUF_NONNULL)` names the parameter). +fn pointer_annotation_spans(code: &[u8], text: &str, spans: &mut Vec<(usize, usize)>) { + for (star, &b) in code.iter().enumerate() { + if b != b'*' && b != b'&' { continue; } + let mut at = skip_space(code, star + 1); + let mut run = Vec::new(); + while let Some(end) = macro_name_end(code, at) { + run.push((at, end)); + let next = skip_space(code, end); + at = next; + if next == end && code.get(end) != Some(&b'*') { break; } + } + if run.is_empty() { continue; } + let after = at; + let follows = match code.get(after) { + Some(b'*') => b == b'*', + Some(b':') => code.get(after + 1) == Some(&b':'), + Some(c) if c.is_ascii_alphabetic() || *c == b'_' || *c == b'~' => { + let mut end = after; + while end < code.len() && is_word(code[end]) { end += 1; } + !ALTERNATIVE_OPERATORS.contains(&&text[after..end]) + } + _ => false, + }; + if !follows { continue; } + let before = String::from_utf8_lossy(&code[star.saturating_sub(40)..star]); + if re!(r"(?:[)\]]|\b\d[\w.]*)\s*$").is_match(&before) { continue; } + let boundary = code[..star].iter().rposition(|c| matches!(c, b';' | b'{' | b'}')).map_or(0, |p| p + 1); + let prefix = String::from_utf8_lossy(&code[boundary..star]); + if re!(r"\b(?:return|throw|co_return)\b").is_match(&prefix) || re!(r"(?:^|[^=!<>])=(?:[^=]|$)").is_match(&prefix) { continue; } + spans.extend(run); + } +} + +/// The spans of the macros after the name a declaration declares, or after its array brackets: +/// `versions_ GUARDED_BY(mutex_);`, `cord ABSL_ATTRIBUTE_LIFETIME_BOUND)`. The macro is followed by +/// `;`, `=`, `,` or `)`, and stands after `]` or after a name with a lowercase letter that itself +/// follows a type (a name, `*`, `&` or `>`), so a variable named in capitals stays. +fn declarator_attribute_spans(code: &[u8], text: &str, spans: &mut Vec<(usize, usize)>) { + let mut i = 0; + while i < code.len() { + let Some(mut end) = macro_name_end(code, i) else { i += 1; continue }; + let start = i; + let mut run = Vec::new(); + let mut at = start; + while let Some(name_end) = macro_name_end(code, at) { + end = name_end; + let open = skip_space(code, end); + if code.get(open) == Some(&b'(') { + match arguments_end(code, open) { Some(close) => end = close, None => break } + } + run.push((at, end)); + at = skip_space(code, end); + } + i = end.max(start + 1); + if run.is_empty() || !matches!(code.get(at), Some(b';' | b'=' | b',' | b')')) || code.get(at + 1) == Some(&b'=') && code[at] == b'=' { continue; } + // What stands before the macro. + let mut k = start; + let space_end = k; + while k > 0 && code[k - 1].is_ascii_whitespace() { k -= 1; } + if k == space_end || k == 0 { continue; } + let declares = if code[k - 1] == b']' { true } else { + let name_end = k; + while k > 0 && is_word(code[k - 1]) { k -= 1; } + let name = &text[k..name_end]; + if name.is_empty() || !name.bytes().any(|b| b.is_ascii_lowercase()) || name.as_bytes()[0].is_ascii_digit() { false } else { + let mut t = k; + while t > 0 && code[t - 1].is_ascii_whitespace() { t -= 1; } + match t.checked_sub(1).map(|p| code[p]) { + Some(b'*' | b'&' | b'>') => true, + Some(c) if is_word(c) => { + let word_end = t; + while t > 0 && is_word(code[t - 1]) { t -= 1; } + !NOT_A_TYPE.contains(&&text[t..word_end]) + } + _ => false, + } + } + }; + if declares { spans.extend(run); } + } +} + +/// C++ `text` with the annotation macros extraction blanks before parsing replaced by spaces. +pub(super) fn cpp_without_annotations(text: &str) -> Cow<'_, str> { + if !re!(r"[*&]\s*[A-Z][A-Z0-9]*_|[\w\]]\s+[A-Z][A-Z0-9]*_[A-Z0-9_]*\s*[(;=,)]").is_match(text) { return Cow::Borrowed(text); } + let code = masked_code(text); + let mut spans = Vec::new(); + pointer_annotation_spans(&code, text, &mut spans); + declarator_attribute_spans(&code, text, &mut spans); + if spans.is_empty() { return Cow::Borrowed(text); } + let mut out = text.as_bytes().to_vec(); + for (from, to) in spans { + for b in &mut out[from..to] { if *b != b'\n' && *b != b'\r' { *b = b' '; } } + } + // Only ASCII macro names and their (ASCII-delimited) arguments' bytes are blanked whole. + Cow::Owned(String::from_utf8(out).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())) +} + +#[cfg(test)] +mod tests { + use super::cpp_without_annotations; + + #[test] + fn blanks_declaration_annotations_only() { + let input = "int n = 1'000; // a \"b\"\nf(\"x(\", 'y'); /* z\n w */ g();"; + let code = String::from_utf8(super::masked_code(input)).unwrap(); + assert_eq!(code.len(), input.len()); + assert_eq!(code.split('\n').map(str::trim_end).collect::>(), ["int n = 1'000;", "f(\" \", ' ');", " g();"]); + let text = "Printer* PROTOBUF_NONNULL printer_;\nVersionSet* const versions_ GUARDED_BY(mutex_);\nint n = a * MAX_LEN + 1;\nFoo DEFAULT_OPTIONS;\nreturn x * FOO_BAR;\n// Foo* PROTOBUF_NONNULL c;\n"; + let out = cpp_without_annotations(text); + assert_eq!(out.len(), text.len()); + let blank = |name: &str| " ".repeat(name.len()); + let expected = format!("Printer* {} printer_;\nVersionSet* const versions_ {};\n", blank("PROTOBUF_NONNULL"), blank("GUARDED_BY(mutex_)")); + assert!(out.starts_with(&expected), "{out}"); + assert!(out.contains("a * MAX_LEN + 1") && out.contains("Foo DEFAULT_OPTIONS;") && out.contains("x * FOO_BAR") && out.contains("Foo* PROTOBUF_NONNULL c;")); + } +} diff --git a/codegraph-kernel/src/resolve/cpp_construction.rs b/codegraph-kernel/src/resolve/cpp_construction.rs new file mode 100644 index 0000000000..9c104daf8a --- /dev/null +++ b/codegraph-kernel/src/resolve/cpp_construction.rs @@ -0,0 +1,324 @@ +//! A C++ construction (`new log::Writer(…)`, `Writer w(…)`, which extraction records by the type's +//! last name) makes the class C++ lookup finds for the type as written there: from the scopes +//! around the code outwards (a gtest `TEST_F` body's fixture right after the body itself, a class +//! scope with its bases), its qualifier followed a segment at a time and an alias to the class it +//! names. leveldb's db_impl.cc writes `new log::Writer(…)` in `DBImpl` methods, which also +//! construct the `DBImpl::Writer` it defines out of line as `Writer w(…)`; by the last name alone +//! the two were one. +//! - At 0.7 when lookup doesn't place the type and the class is the only one of that name the +//! file can see. +//! - An enum lookup finds is the enum: rocksdb's `const ValueType types[] = {…}`. +//! - Through an alias the calling function declares itself, the class it names, if one of the +//! project's; else nothing. +//! - Nothing (no edge) for a template parameter, a member of a type from outside the project +//! (`stl_wrappers::KVMap::value_type`) or a function's own alias of one. A member of an alias +//! lookup can't follow that names no standard type is left undecided when the project +//! declares a class of the member's name. +//! - A header of a test tree is seen only from inside that tree or by including it. +//! - Undecided, for the name strategies, when nothing the file sees is named so. +//! +//! Of several classes or aliases the file declares under one qualified name (the local classes of +//! separate gtest `TEST_F` bodies, the members of unnamed specializations), the one declared +//! around the construction. +//! +//! Port of upstream's `matchCppConstruction`, `cppEnumWritten`, `cppNamesakeAround` and +//! `cppTypeWrittenAt` (name-matcher.ts at 34dd3d2f, #2542), with the fixture scope and local-class +//! visibility of #2551 (`seenFrom`, cpp-supertypes.ts at d94a92ea). +use super::cpp_aliases::type_segments; +use super::*; + +/// What a lookup of a written type found. +enum Lookup { + /// The class, enum or alias, and whether C++ lookup placed it (rather than its being the only + /// one of that name the file can see). + Found(Arc, bool), + /// The name names no particular class of the project's. + NoClass, + /// Nothing the file sees is named so. + Unknown, +} + +fn type_kind(n: &KNode) -> bool { + matches!(n.language.as_str(), "c" | "cpp") && matches!(n.kind.as_str(), "class" | "struct" | "union" | "type_alias" | "enum") +} + +fn class_like(n: &KNode) -> bool { + matches!(n.language.as_str(), "c" | "cpp") && matches!(n.kind.as_str(), "class" | "struct" | "union") +} + +/// Whether a C or C++ file is a source file (a translation unit), whose classes only it sees. +fn source_file(file: &str) -> bool { + re!(r"(?i)\.(?:c|cc|cpp|cxx|c\+\+|cu|metal)$").is_match(file) +} + +/// The type name `text` starts with, template arguments and all: `log::Writer` of `log::Writer(…)`, +/// `Outer::Inner` of `Outer::Inner x(…)`. None when it starts with none, or a template +/// argument list goes on past the line. (A `>` in parentheses compares: `Bits<(N > 8)>`.) +fn type_written_at(text: &str) -> Option { + let bytes = text.as_bytes(); + let mut end = 0; + loop { + let segment = if end == 0 { re!(r"^(?:::\s*)?[A-Za-z_]\w*") } else { re!(r"^\s*::\s*[A-Za-z_]\w*") }; + let Some(name) = segment.find(&text[end..]) else { return (end > 0).then(|| text[..end].to_string()) }; + end += name.end(); + let Some(open) = re!(r"^\s*<").find(&text[end..]) else { continue }; + end += open.end(); + let (mut angles, mut parens) = (1, 0); + while angles > 0 { + match bytes.get(end)? { + b'(' => parens += 1, + b')' => parens -= 1, + _ if parens > 0 => {} + b'<' => angles += 1, + b'>' => angles -= 1, + _ => {} + } + end += 1; + } + } +} + +/// One of several declarations of a class: the one in `file` itself, then in its header, then the +/// nearest by path. +fn pick(classes: Vec>, file: &str) -> Option> { + if classes.len() <= 1 { return classes.into_iter().next(); } + if let Some(own) = classes.iter().find(|n| n.file_path == file) { return Some(own.clone()); } + let stem = |p: &str| p.rsplit_once('.').filter(|(head, _)| !head.ends_with('/')).map_or(p.to_string(), |(head, _)| head.to_string()); + if let Some(header) = classes.iter().find(|n| stem(&n.file_path) == stem(file)) { return Some(header.clone()); } + let shared = |p: &str| { + let (a, b): (Vec<_>, Vec<_>) = (p.split('/').collect(), file.split('/').collect()); + a.iter().zip(&b).take(a.len().min(b.len()).saturating_sub(1)).take_while(|(x, y)| x == y).count() + }; + classes.into_iter().reduce(|best, n| if shared(&n.file_path) > shared(&best.file_path) { n } else { best }) +} + +impl KernelResolver { + /// The class (or enum, or alias) a C++ `instantiates` ref makes: Some(None) for no edge, None + /// to leave it to the name strategies. + pub(super) fn cpp_construction(&mut self, r: &ResolveRefIn) -> Res>> { + if r.language != "cpp" || r.reference_kind != "instantiates" { return Ok(None); } + let Some(caller) = self.node_by_id(&r.from_node_id)? else { return Ok(None) }; + let Some(source) = self.read_file(&r.file_path) else { return Ok(None) }; + let Some(line) = source.get((r.line - 1).max(0) as usize) else { return Ok(None) }; + let text = &line[super::names::js_unit_to_byte(line, r.column.max(0) as usize).min(line.len())..]; + // (A declaration's column is where its specifiers start: `const Slice key(…)`.) + let specifiers = re!(r"^(?:(?:new|const|volatile|static|thread_local|constexpr|constinit|mutable|struct|class|union|typename)\s+)*").find(text).map_or(0, |m| m.end()); + let Some(written) = type_written_at(&text[specifiers..]) else { return Ok(None) }; + let made = |node: Arc, exact: bool| KCand { node, confidence: if exact { 0.9 } else { 0.7 }, resolved_by: if exact { "qualified-name" } else { "exact-match" } }; + let Some(parts) = type_segments(&written) else { return Ok(Some(None)) }; + let global = written.trim_start().starts_with("::"); + let visible = self.cpp_construction_visible_files(&r.file_path)?; + if !global && self.cpp_alias_declaration(&written, &parts, r, 0, &visible)?.is_some_and(|(_, declared_in, _)| declared_in.id == caller.id) { + let class = match self.cpp_alias_expansion(&written, r, 0, &visible)? { + Some(super::cpp_aliases::CppAlias { raw: Some(raw), site, pointer: false, .. }) => self.cpp_type_owner_visible(&raw, &site, 0, true, &visible)?.filter(|n| class_like(n)), + _ => None, + }; + return Ok(Some(class.map(|n| made(n, true)))); + } + if !global && self.cpp_template_parameter(&caller, &parts[0], r)? { return Ok(Some(None)); } + let (node, exact) = match self.cpp_construction_lookup(&written, &parts, global, &caller, r, &visible)? { + Lookup::Found(node, exact) => (node, exact), + Lookup::NoClass => return Ok(Some(None)), + Lookup::Unknown => return Ok(None), + }; + let namesakes: Vec<_> = self.nodes_by_qualified_name(&node.qualified_name)?.iter() + .filter(|n| n.file_path == r.file_path && type_kind(n) && n.kind != "enum").cloned().collect(); + if namesakes.len() < 2 { return Ok(Some(Some(made(node, exact)))); } + Ok(self.cpp_namesake_around(&namesakes, r)?.map(|n| Some(made(n, exact)))) + } + + /// The files whose declarations a C or C++ file can see without knowing its includes: its own + /// and every header, but for a header of a test tree, which code outside one sees only by + /// including it. + fn cpp_construction_visible_files(&mut self, file: &str) -> Res>> { + if let Some((memo_file, hit)) = &self.cpp_visible_memo { if memo_file == file { return Ok(hit.clone()); } } + let files = match self.sorted_files() { Some(files) => files, None => Arc::new(self.table()?.files.iter().cloned().collect()) }; + let mut visible = HashSet::new(); + for f in files.iter().filter(|f| *f == file || !source_file(f)) { + if !self.cpp_unincluded_test_header(f, file)? { visible.insert(f.clone()); } + } + let visible = Rc::new(visible); + self.cpp_visible_memo = Some((file.to_string(), visible.clone())); + Ok(visible) + } + + /// Whether `header` is a header of a test tree (a test directory's child: `tests/abi/`) that + /// `file`, outside that tree, doesn't include: nlohmann/json's examples and benchmarks + /// construct `json::json_pointer` and `json` with the library's headers, not with the old + /// release `tests/abi/include/nlohmann/json_v3_10_5.hpp` keeps. + fn cpp_unincluded_test_header(&mut self, header: &str, file: &str) -> Res { + if header == file || !is_test_path(header) { return Ok(false); } + let dirs: Vec<_> = header.split('/').collect(); + let dirs = &dirs[..dirs.len() - 1]; + let Some(at) = (1..=dirs.len()).find(|&i| is_test_path(&format!("{}/x", dirs[..i].join("/")))) else { return Ok(false) }; + let root = format!("{}/", dirs[..(at + 1).min(dirs.len())].join("/")); + if file.starts_with(&root) { return Ok(false); } + Ok(!self.namespace_visible_files(file, "cpp")?.contains(header)) + } + + /// Whether code at `r` sees the declaration `n`: one in its own file or a header (of a test + /// tree only from a test or by including it), and a class local to a function only inside + /// that function (the index names it after the function, and every gtest `TEST_F` body in a + /// namespace is `ns::TEST_F`). + fn cpp_construction_sees(&mut self, n: &KNode, r: &ResolveRefIn) -> Res { + if n.file_path != r.file_path && source_file(&n.file_path) { return Ok(false); } + if self.cpp_unincluded_test_header(&n.file_path, &r.file_path)? { return Ok(false); } + let Some((scope, _)) = n.qualified_name.rsplit_once("::") else { return Ok(true) }; + let around = self.nodes_by_qualified_name(scope)?.iter().find(|f| matches!(f.kind.as_str(), "function" | "method") + && f.file_path == n.file_path && f.start_line <= n.start_line && f.end_line >= n.start_line).cloned(); + Ok(around.is_none_or(|f| f.file_path == r.file_path && f.start_line <= r.line && r.line <= f.end_line)) + } + + /// The types named `name` in the scope `scope` that code at `r` sees, else in the classes a + /// class of that name derives from, nearest first. + pub(super) fn cpp_scope_types(&mut self, scope: &str, name: &str, r: &ResolveRefIn, depth: u32) -> Res>> { + let qualified = if scope.is_empty() { name.to_string() } else { format!("{scope}::{name}") }; + let mut found = Vec::new(); + for n in self.nodes_by_qualified_name(&qualified)?.iter().filter(|n| type_kind(n)) { + if self.cpp_construction_sees(n, r)? { found.push(n.clone()); } + } + if !found.is_empty() || scope.is_empty() || depth >= 4 { return Ok(found); } + let classes: Vec<_> = self.nodes_by_qualified_name(scope)?.iter().filter(|n| class_like(n)).cloned().collect(); + for class in classes { + for base in self.supertype_nodes(&class.id)?.into_iter().filter(|n| class_like(n)) { + let inherited = self.cpp_scope_types(&base.qualified_name, name, r, depth + 1)?; + if !inherited.is_empty() { return Ok(inherited); } + } + } + Ok(Vec::new()) + } + + /// The class (or enum, or alias of a type outside the project) `written` names, looked up as + /// C++ looks it up from `caller`'s body. + fn cpp_construction_lookup(&mut self, written: &str, parts: &[String], global: bool, caller: &KNode, r: &ResolveRefIn, visible: &HashSet) -> Res { + let mut scopes: Vec = Vec::new(); + if !global && caller.kind != "file" { + let names: Vec<_> = caller.qualified_name.split("::").collect(); + scopes = (1..=names.len()).rev().map(|i| names[..i].join("::")).collect(); + if let Some(fixture) = self.cpp_gtest_fixture(caller, r)? { scopes.insert(1.min(scopes.len()), fixture); } + } + scopes.push(String::new()); + for scope in &scopes { + // Namespaces have no nodes: the first type is the shortest prefix of the path that + // names one. + let mut first = None; + for count in 1..=parts.len() { + let found = self.cpp_scope_types(scope, &parts[..count].join("::"), r, 0)?; + if let Some(found) = pick(found, &r.file_path) { first = Some((found, count)); break; } + } + let Some((mut current, consumed)) = first else { continue }; + for part in &parts[consumed..] { + match current.kind.as_str() { + "enum" => return Ok(Lookup::NoClass), + "type_alias" => match self.cpp_expand_owner(¤t, r, 0, false, visible)? { + Some(class) => current = class, + // An alias of a standard type (`using Bytes = std::string;`) has no + // member of the project's. Another the lookup can't follow may lead to a + // class of the member's name through what it doesn't read: nlohmann/json's + // `using json = nlohmann::json;` names a namespace a macro opens. + None if !self.cpp_alias_of_std(¤t) && self.cpp_project_type_named(part, r)? => return Ok(Lookup::Unknown), + None => return Ok(Lookup::NoClass), + }, + _ => {} + } + let found = self.cpp_scope_types(¤t.qualified_name, part, r, 0)?; + let Some(next) = pick(found, &r.file_path) else { return Ok(Lookup::Unknown) }; + current = next; + } + if current.kind == "type_alias" { + if let Some(class) = self.cpp_expand_owner(¤t, r, 0, false, visible)? { current = class; } + } + return Ok(Lookup::Found(current, true)); + } + if !global { + if let Some(found) = self.cpp_type_via_using(written, parts, r, visible)?.filter(|n| class_like(n)) { return Ok(Lookup::Found(found, true)); } + } + // The only class of that name the file can see, by its qualified tail. + let path = parts.join("::"); + let mut tail = Vec::new(); + for n in self.nodes_by_name(parts.last().unwrap())?.iter().filter(|n| class_like(n) && (n.qualified_name == path || n.qualified_name.ends_with(&format!("::{path}")))) { + if self.cpp_construction_sees(n, r)? { tail.push(n.clone()); } + } + let own: Vec<_> = tail.iter().filter(|n| n.file_path == r.file_path).cloned().collect(); + let sole = if own.is_empty() { tail } else { own }; + Ok(match sole.as_slice() { [only] => Lookup::Found(only.clone(), false), _ => Lookup::Unknown }) + } + + /// Whether the alias `alias` names a type of the standard library: `using Bytes = std::string;`, + /// `typedef std::map Counts;`. + fn cpp_alias_of_std(&mut self, alias: &KNode) -> bool { + let Some(source) = self.read_file(&alias.file_path) else { return false }; + let text = source.get((alias.start_line - 1).max(0) as usize..alias.end_line.max(0) as usize).unwrap_or(&[]).join(" "); + re!(r"(?:=|\btypedef)\s*(?:typename\s+)?(?:::)?std\s*::").is_match(&text) + } + + /// Whether a class the file at `r` can see is named `name`. + fn cpp_project_type_named(&mut self, name: &str, r: &ResolveRefIn) -> Res { + for n in self.nodes_by_name(name)?.iter().filter(|n| class_like(n)) { + if self.cpp_construction_sees(n, r)? { return Ok(true); } + } + Ok(false) + } + + /// The type `raw` names, looked up from the scopes around `r`'s caller a segment at a time, + /// each class's bases included: protobuf's `using ConformanceRequestSetting = + /// BinaryAndJsonConformanceSuite::ConformanceRequestSetting;` names a class the suite inherits + /// from `ConformanceTestSuite`. (Port of upstream's #2549.) + pub(super) fn cpp_type_through_bases(&mut self, raw: &str, r: &ResolveRefIn, included: &HashSet) -> Res>> { + let Some(parts) = type_segments(raw) else { return Ok(None) }; + let mut scopes: Vec = Vec::new(); + if !raw.trim_start().starts_with("::") { + if let Some(caller) = self.node_by_id(&r.from_node_id)? { + let mut names: Vec<_> = caller.qualified_name.split("::").map(str::to_string).collect(); + if is_inheritance_ref(&r.reference_kind) { names.pop(); } + scopes = (1..=names.len()).rev().map(|i| names[..i].join("::")).collect(); + } + } + scopes.push(String::new()); + for scope in &scopes { + let mut first = None; + for count in 1..=parts.len() { + let found = self.cpp_scope_types(scope, &parts[..count].join("::"), r, 0)?; + let found = found.into_iter().filter(|n| included.contains(&n.file_path)).collect(); + if let Some(found) = pick(found, &r.file_path) { first = Some((found, count)); break; } + } + let Some((mut current, consumed)) = first else { continue }; + for part in &parts[consumed..] { + if !class_like(¤t) { return Ok(None); } + let next: Vec<_> = self.cpp_scope_types(¤t.qualified_name, part, r, 0)?.into_iter().filter(|n| included.contains(&n.file_path)).collect(); + let Some(next) = pick(next, &r.file_path) else { return Ok(None) }; + current = next; + } + return Ok(Some(current)); + } + Ok(None) + } + + /// Of several classes or aliases `r`'s file declares under one qualified name, the one whose + /// holder (the function or class it is declared in, else the file) holds `r`. None unless + /// exactly one is. + fn cpp_namesake_around(&mut self, namesakes: &[Arc], r: &ResolveRefIn) -> Res>> { + let nodes = self.nodes_in_file(&r.file_path)?; + let holders: Vec<_> = nodes.iter().filter(|n| matches!(n.kind.as_str(), "function" | "method" | "class" | "struct" | "union")).collect(); + let around: Vec<_> = namesakes.iter().filter(|cls| { + let holder = holders.iter().filter(|n| n.id != cls.id && n.start_line <= cls.start_line && n.end_line >= cls.end_line) + .min_by_key(|n| n.end_line - n.start_line); + holder.is_none_or(|h| h.start_line <= r.line && r.line <= h.end_line) + }).cloned().collect(); + Ok(match around.as_slice() { [only] => Some(only.clone()), _ => None }) + } +} + +#[cfg(test)] +mod tests { + use super::type_written_at; + + #[test] + fn reads_the_type_a_construction_writes() { + assert_eq!(type_written_at("log::Writer(&file);").as_deref(), Some("log::Writer")); + assert_eq!(type_written_at("Outer<(sizeof(int) > 2)>::Inner();").as_deref(), Some("Outer<(sizeof(int) > 2)>::Inner")); + assert_eq!(type_written_at("Map> m;").as_deref(), Some("Map>")); + assert_eq!(type_written_at("Map, r: &ResolveRefIn) -> Res>> { + // A local of that name (a lambda, a callable) is what a bare call calls. + if name != caller.name && self.cpp_local_name(name, caller, r, true)? { return Ok(None); } + self.cpp_implicit_this_lookup(name, Receiver::This, caller, r) + } + + /// The method a C++ call of `name` with no receiver, or on `this`, reaches from `caller`: 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. None when + /// the caller is no member function, or no class on the way has a method of that name. (Whether + /// a local of the caller's shadows it is the caller's to ask.) + fn cpp_implicit_this_lookup(&mut self, name: &str, receiver: Receiver, caller: &Arc, r: &ResolveRefIn) -> Res>> { + let owner = match self.cpp_member_owner(caller)? { + Some(owner) => owner, + None => match self.cpp_gtest_fixture(caller, r)? { Some(fixture) => fixture, None => return Ok(None) }, + }; let mut found = Lookup::Missing; - for scope in self.cpp_enclosing_classes(&owner, &caller)? { + for scope in self.cpp_enclosing_classes(&owner, caller)? { found = self.cpp_scope_method(&scope, name, receiver, r)?; if !matches!(found, Lookup::Missing) || receiver == Receiver::This { break; } } - let Lookup::Found(node) = found else { return Ok(None) }; - // (A recursive call names the function itself, which no local shadows.) - if receiver == Receiver::Bare && name != caller.name && self.cpp_local_name(name, &caller, r)? { return Ok(None); } - Ok(Some(KCand { node, confidence: 0.9, resolved_by: "instance-method" })) + Ok(match found { Lookup::Found(node) => Some(node), _ => None }) } /// How a C++ call the extractor recorded by its bare name is written, read at its column: @@ -272,7 +294,7 @@ impl KernelResolver { if !inline.is_empty() || !out_of_line.is_empty() { let mut overloads = inline; overloads.extend(prefer_call_site_file(out_of_line, &r.file_path)); - return Ok(Lookup::Found(self.cpp_overload_for(overloads, name, r))); + return Ok(Lookup::Found(self.cpp_overload_for(overloads, name, r)?)); } for decl in &scope.decls { let words = self.cpp_member_macro_arguments(decl); @@ -282,7 +304,7 @@ impl KernelResolver { for decl in &scope.decls { for base in self.supertype_nodes(&decl.id)? { if !class_kind(&base.kind) { continue; } - if receiver == Receiver::Bare && self.cpp_dependent_base(decl, &base)? { continue; } + if receiver == Receiver::Bare && self.cpp_dependent_base(decl, &base)? && !self.cpp_using_declares(decl, &base, name) { continue; } let mut seen = HashSet::from([scope.qualified_name.clone()]); let Some(inherited) = self.cpp_method_of(&base, name, r, 1, &mut seen)? else { continue }; // Its class's other overloads, which the arguments may fit better (rocksdb's @@ -291,7 +313,7 @@ impl KernelResolver { .iter().filter(|n| n.id != inherited.id && n.kind == "method" && c_family(&n.language)).cloned().collect(); let mut overloads = vec![inherited]; overloads.extend(prefer_call_site_file(siblings, &r.file_path)); - return Ok(Lookup::Found(self.cpp_overload_for(overloads, name, r))); + return Ok(Lookup::Found(self.cpp_overload_for(overloads, name, r)?)); } } Ok(Lookup::Missing) @@ -302,7 +324,13 @@ impl KernelResolver { pub(super) fn cpp_method_of(&mut self, cls: &KNode, name: &str, r: &ResolveRefIn, depth: usize, seen: &mut HashSet) -> Res>> { let own: Vec<_> = self.nodes_by_qualified_name(&format!("{}::{name}", cls.qualified_name))? .iter().filter(|n| n.kind == "method" && c_family(&n.language)).cloned().collect(); - if !own.is_empty() { return Ok(prefer_call_site_file(own, &r.file_path).into_iter().next()); } + if !own.is_empty() { + // Of its overloads, the one the call's arguments fit; a chained call `inner(…).name(…)` + // keeps the first, since the arguments at its position are the inner call's. + let own = prefer_call_site_file(self.cpp_visible_first(own, r)?, &r.file_path); + if r.reference_name.contains("().") || own.len() < 2 { return Ok(own.into_iter().next()); } + return Ok(Some(self.cpp_overload_for(own, name, r)?)); + } seen.insert(cls.qualified_name.clone()); if depth >= 4 || !self.supertypes_complete { return Ok(None); } for decl in self.cpp_class_decls(&cls.qualified_name)? { @@ -314,22 +342,6 @@ impl KernelResolver { Ok(None) } - /// Of one class's overloads of `name` (in preference order), the one the call's arguments fit best. - fn cpp_overload_for(&mut self, overloads: Vec>, name: &str, r: &ResolveRefIn) -> Arc { - let mut best = overloads[0].clone(); - if overloads.len() == 1 { return best; } - let Some(args) = self.call_arguments(r, name) else { return best }; - let mut best_fit = self.cpp_overload_fit(&best, &args, r); - for n in overloads.into_iter().skip(1) { - let fit = self.cpp_overload_fit(&n, &args, r); - if fit > best_fit { - best = n; - best_fit = fit; - } - } - best - } - /// The words in the arguments of the macros a C++ class declaration calls among its members, /// like `Message` in protobuf's `LOCAL_VAR_ACCESSOR(Message*, Message);`: such a macro can /// declare a member the index has no node for (`Get##name` makes `GetMessage`). @@ -382,6 +394,22 @@ impl KernelResolver { /// 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. + /// Whether the C++ class declaration `decl` brings `name` in from its base `base` with a + /// using-declaration (cppUsingDeclares): 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. + pub(super) fn cpp_using_declares(&mut self, decl: &KNode, base: &KNode, name: &str) -> bool { + let Some(source) = self.read_file(&decl.file_path) else { return false }; + let lines = source.cpp_code_lines(); + let from = (decl.start_line - 1).max(0) as usize; + let to = (decl.end_line.max(decl.start_line) as usize).min(lines.len()); + if from >= to { return false; } + let body = lines[from..to].join("\n"); + re!(r"\busing\s+((?:::\s*)?[A-Za-z_][\w:]*(?:\s*<[^;{}]*>)?)\s*::\s*([A-Za-z_]\w*)\s*;").captures_iter(&body).any(|m| { + &m[2] == name && !m[1].trim_start().starts_with("namespace") && cpp_last_segment(strip_template_arguments(&m[1]).trim()) == base.name + }) + } + pub(super) fn cpp_dependent_base(&mut self, decl: &KNode, base: &KNode) -> Res { let parameters = self.cpp_template_parameter_names(decl)?; if parameters.is_empty() { return Ok(false); } @@ -409,8 +437,9 @@ impl KernelResolver { /// Whether the C++ function `caller` declares `name` before the call at `r` — a parameter, a /// local, a lambda's parameter, a range-`for` variable, a structured binding — which a bare /// call then calls, not a member. A declaration in an earlier block the call is outside of - /// counts too: the name strategies decide, as before. - fn cpp_local_name(&mut self, name: &str, caller: &KNode, r: &ResolveRefIn) -> Res { + /// counts too: the name strategies decide, as before. With `callable`, only a declaration of + /// something a bare call can call (not a pointer). + fn cpp_local_name(&mut self, name: &str, caller: &KNode, r: &ResolveRefIn, callable: bool) -> Res { if caller.file_path != r.file_path || class_kind(&caller.kind) { return Ok(false); } let Some(source) = self.read_file(&r.file_path) else { return Ok(false) }; let lines = source.cpp_code_lines(); @@ -424,6 +453,9 @@ impl KernelResolver { // `std::function done;` reads as a declaration once its template arguments are gone. for variant in [code.clone(), strip_template_arguments(code)] { let Some(ty) = self.cpp_declarator_match(&variant, &escaped)? else { continue }; + // (A pointer is no callable, and a class declared in the function takes its + // constructor's `Env* target`: the name stays a member's.) + if callable && ty.contains('*') { continue; } // `Owner::name(` is a definition's own declarator; `return name(` no declaration. if !ty.trim_end().ends_with("::") && self.normalize_cpp_type_name(&ty, false)?.is_some() { return Ok(true); } } diff --git a/codegraph-kernel/src/resolve/cpp_overloads.rs b/codegraph-kernel/src/resolve/cpp_overloads.rs new file mode 100644 index 0000000000..f7c131491c --- /dev/null +++ b/codegraph-kernel/src/resolve/cpp_overloads.rs @@ -0,0 +1,251 @@ +//! Which of a C++ class's overloads a call's arguments fit: by their count and string literals +//! (`cpp_overload_fit`), then, among overloads those fit alike, by the types the arguments have +//! where the call reads them. rocksdb's `Insert(ch)` on a `char ch` is `Insert(char key, …)`, not +//! `Insert(const std::string& key, …)`, and `db->Get(ro, key, &value)` is not the five-parameter +//! `Get(…, std::string* timestamp)`. +//! +//! Port of upstream's `cppOverloadFor`, `cppArgumentType` and `cppArgumentTypeFit` (name-matcher.ts +//! at a6ceb047, #2541); read the original with `git show a6ceb047:src/resolution/name-matcher.ts`. +//! The arguments are read from the syntax tree: a literal's kind, or the type a name, `&name`, +//! `name[i]` or another expression has as written where it is declared. +use super::cpp_implicit_this::strip_template_arguments; +use super::cpp_range_for::CppWritten; +use super::iteration::{descendant_for_position, named_children}; +use super::receivers::cpp_last_segment; +use super::*; + +/// What a C++ call's argument is: a literal's kind, or the type a name or expression has as written. +pub(super) enum CppArgument { + Char, + Number, + Zero, + Bool, + Null, + Type(String), +} + +/// What a C++ type is, for an argument to fit a parameter of it. +#[derive(Clone, Copy, PartialEq)] +pub(super) enum CppTypeCategory { + String, + CString, + Char, + Number, + Bool, + Pointer, + /// `std::nullptr_t`. + Null, + Class, +} + +const CHAR_TYPES: &[&str] = &["char", "wchar_t", "char8_t", "char16_t", "char32_t"]; +const NUMBER_TYPES: &[&str] = &[ + "int", "unsigned", "signed", "short", "long", "float", "double", "size_t", "ssize_t", "ptrdiff_t", + "int8_t", "int16_t", "int32_t", "int64_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t", "intptr_t", "uintptr_t", +]; +const STRING_TYPES: &[&str] = &["string", "wstring", "string_view", "wstring_view", "basic_string", "basic_string_view"]; + +/// The category of the written C++ type `ty`: a pointer's, a string's, a character's, a number's, a +/// class's. +pub(super) fn cpp_type_category(ty: &str) -> CppTypeCategory { + use CppTypeCategory::*; + let stripped = strip_template_arguments(ty); + let flat = re!(r"\b(?:const|volatile|struct|class|typename|enum)\b").replace_all(&stripped, " ").replace('&', " "); + let flat = flat.trim(); + let words: Vec<&str> = flat.split(|c: char| c == '*' || c.is_whitespace()).filter(|w| !w.is_empty()).collect(); + let last = cpp_last_segment(words.last().copied().unwrap_or("")); + if flat.contains('*') { + return if CHAR_TYPES.contains(&last.as_str()) && flat.matches('*').count() == 1 { CString } else { Pointer }; + } + if last == "nullptr_t" { return Null; } + if last == "bool" { return Bool; } + if CHAR_TYPES.contains(&last.as_str()) && words.iter().all(|w| CHAR_TYPES.contains(w) || *w == "signed" || *w == "unsigned") { return Char; } + if NUMBER_TYPES.contains(&last.as_str()) || (!words.is_empty() && words.iter().all(|w| NUMBER_TYPES.contains(w))) { return Number; } + if STRING_TYPES.contains(&last.as_str()) { String } else { Class } +} + +/// A C++ parameter declaration's type: without its default argument and its name. +pub(super) fn cpp_parameter_type(param: &str) -> String { + let mut text = param; + let mut depth = 0i32; + for (i, c) in param.char_indices() { + match c { + '<' | '(' | '[' => depth += 1, + '>' | ')' | ']' => depth -= 1, + '=' if depth == 0 => { text = ¶m[..i]; break; } + _ => {} + } + } + let text = text.trim(); + // The name, when a type comes before it. + if let Some(named) = re!(r"^(.*[\w>*&\s])\b[A-Za-z_]\w*\s*(?:\[[^\]]*\])?$").captures(text) { + let ty = named[1].trim(); + if re!(r"[A-Za-z_]").is_match(&named[1]) && !re!(r"^(?:const|volatile|unsigned|signed|struct|class|enum)\s*$").is_match(ty) { + return ty.to_string(); + } + } + text.to_string() +} + +/// How a literal fits a parameter of each category. `nullptr` is a `std::nullptr_t` exactly, and a +/// pointer only by conversion: protobuf's `ptr.reset(nullptr)` is `reset(std::nullptr_t)`, not +/// `reset(T*)` (#2554). +fn literal_fit(argument: &CppArgument, category: CppTypeCategory) -> i64 { + use CppTypeCategory::*; + match (argument, category) { + (CppArgument::Char, Char) => 3, + (CppArgument::Char, Number) => 1, + (CppArgument::Char, String | CString | Pointer | Null) => -3, + (CppArgument::Number, Number) => 2, + (CppArgument::Number, Char) => 1, + (CppArgument::Number, String | CString | Pointer | Null) => -3, + (CppArgument::Zero, Number) => 2, + (CppArgument::Zero, Char | Pointer | CString | Null) => 1, + (CppArgument::Zero, String) => -3, + (CppArgument::Bool, Bool) => 3, + (CppArgument::Bool, Number) => 1, + (CppArgument::Bool, String | CString | Pointer | Null) => -3, + (CppArgument::Null, Null) => 4, + (CppArgument::Null, Pointer | CString) => 3, + (CppArgument::Null, Class) => 1, + (CppArgument::Null, String | Char | Number) => -3, + (CppArgument::Null, Bool) => -1, + _ => 0, + } +} + +impl KernelResolver { + /// Of one class's overloads of `name` (in preference order), the one the call's arguments fit + /// best: by their count and string literals, then, among overloads those fit alike, by the + /// types the arguments have where the call reads them. A deleted overload (`void reset(T*) = + /// delete;`) is none a call that compiles reaches, so it is only taken when every one is. + pub(super) fn cpp_overload_for(&mut self, candidates: Vec>, name: &str, r: &ResolveRefIn) -> Res> { + if candidates.len() == 1 { return Ok(candidates[0].clone()); } + let callable: Vec<_> = candidates.iter().filter(|n| !self.cpp_deleted_function(n, name)).cloned().collect(); + let overloads = if callable.is_empty() { candidates } else { callable }; + if overloads.len() == 1 { return Ok(overloads[0].clone()); } + let Some(args) = self.call_arguments(r, name) else { return Ok(overloads[0].clone()) }; + let fits: Vec = overloads.iter().map(|n| self.cpp_overload_fit(n, &args, r)).collect(); + let top = fits.iter().copied().max().unwrap_or(0); + let tied: Vec<_> = overloads.iter().zip(&fits).filter(|(_, f)| **f == top).map(|(n, _)| n.clone()).collect(); + if tied.len() == 1 || args.is_empty() { return Ok(tied[0].clone()); } + let Some(types) = self.cpp_call_argument_types(name, r)? else { return Ok(tied[0].clone()) }; + if types.iter().all(Option::is_none) { return Ok(tied[0].clone()); } + let mut best = tied[0].clone(); + let mut best_fit = self.cpp_argument_type_fit(&best, &types, r)?; + for n in tied.into_iter().skip(1) { + let fit = self.cpp_argument_type_fit(&n, &types, r)?; + if fit > best_fit { + best = n; + best_fit = fit; + } + } + Ok(best) + } + + /// Whether the C++ function or method `n`, named `name`, is declared deleted: from its name to + /// the declaration's `;` or body, it says `= delete` (protobuf's `void reset(T* + /// PROTOBUF_NULLABLE) = delete;`). + fn cpp_deleted_function(&mut self, n: &KNode, name: &str) -> bool { + let Some(source) = self.read_file(&n.file_path) else { return false }; + let from = (n.start_line - 1).max(0) as usize; + let Ok(word) = shared_regex(&format!(r"\b{}\s*\(", regex::escape(name))) else { return false }; + // (A declaration may run over any number of lines before its `;` or body.) + let mut text = String::new(); + for line in source.iter().skip(from).take(200) { + text.push_str(line); + text.push('\n'); + if let Some(at) = word.find(&text) { + if let Some(end) = text[at.start()..].find([';', '{']) { return re!(r"=\s*delete\b").is_match(&text[at.start()..at.start() + end]); } + } + } + false + } + + /// How well the read argument types `types` fit the parameters of the overload `n`: the same + /// type, or a character to a character, scores; a character, number or pointer to a string, or + /// a string or pointer to a number, counts against. Classes of other names score nothing: a + /// conversion may join them. + fn cpp_argument_type_fit(&mut self, n: &KNode, types: &[Option], r: &ResolveRefIn) -> Res { + let Some(params) = self.declaration_parameters(n, r) else { return Ok(0) }; + let params: Vec<_> = params.into_iter().filter(|p| p != "void").collect(); + let mut score = 0; + for (argument, param) in types.iter().zip(¶ms) { + let Some(argument) = argument else { continue }; + let param = cpp_parameter_type(param); + if param.contains("...") { continue; } + let category = cpp_type_category(¶m); + score += match argument { + CppArgument::Type(text) => self.cpp_written_type_fit(text, ¶m, category)?, + literal => literal_fit(literal, category), + }; + } + Ok(score) + } + + /// How an argument written as the type `argument` fits a parameter of the type `param`. + fn cpp_written_type_fit(&mut self, argument: &str, param: &str, category: CppTypeCategory) -> Res { + use CppTypeCategory::*; + let argument_category = cpp_type_category(argument); + let pointer = |c: CppTypeCategory| c == Pointer || c == CString; + if self.normalize_cpp_type_name(argument, false)? == self.normalize_cpp_type_name(param, false)? && pointer(argument_category) == pointer(category) { return Ok(3); } + if argument_category == category && argument_category != Class { return Ok(2); } + let textual = |c: CppTypeCategory| c == String || c == CString; + Ok(if matches!(argument_category, Char | Number | Pointer) && category == String + || (textual(argument_category) || argument_category == Pointer) && matches!(category, Char | Number) + || matches!(argument_category, Char | Number) && matches!(category, Pointer | CString) { -3 } else { 0 }) + } + + /// What each argument of the call of `name` at `r` is, where that can be read (cppArgumentType): + /// a character, number, `true`/`false` or `nullptr` literal, or the written type of a name the + /// calling function declares, of `&name`, of `name[i]` (rocksdb's `TryLock(column_family[i], + /// keys[i], …)` on a `std::vector`), or of an expression. None for the call when it + /// can't be found. (String literals are `cpp_overload_fit`'s.) + pub(super) fn cpp_call_argument_types(&mut self, name: &str, r: &ResolveRefIn) -> Res>>> { + let Some(source) = self.read_file(&r.file_path) else { return Ok(None) }; + let Some(tree) = self.parsed_tree(&source, r) else { return Ok(None) }; + let callee = shared_regex(&format!(r"(?:^|[.:>])\s*{}(?:\s*<[\s\S]*>)?\s*$", regex::escape(name)))?; + let mut call = descendant_for_position(tree.root_node(), source.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize)); + loop { + if call.kind() == "call_expression" && call.child_by_field_name("function").is_some_and(|f| callee.is_match(&source.text()[f.byte_range()])) { break; } + let Some(parent) = call.parent() else { return Ok(None) }; + call = parent; + } + let Some(arguments) = call.child_by_field_name("arguments") else { return Ok(None) }; + let mut types = Vec::new(); + for argument in named_children(arguments).into_iter().filter(|n| !n.kind().contains("comment")) { + types.push(self.cpp_argument_type(argument, &source, r)?); + } + Ok(Some(types)) + } + + fn cpp_argument_type(&mut self, node: tree_sitter::Node, source: &Rc, r: &ResolveRefIn) -> Res> { + let text = source.text()[node.byte_range()].trim(); + Ok(match node.kind() { + "char_literal" => Some(CppArgument::Char), + "true" | "false" => Some(CppArgument::Bool), + "null" | "nullptr" => Some(CppArgument::Null), + "identifier" if text == "NULL" => Some(CppArgument::Null), + "number_literal" if re!(r"^0+[uUlL]*$").is_match(text) => Some(CppArgument::Zero), + "number_literal" => Some(CppArgument::Number), + "unary_expression" if re!(r"^[-+]\s*[0-9.]").is_match(text) => Some(CppArgument::Number), + "string_literal" | "raw_string_literal" | "concatenated_string" => None, + // `&value`: a pointer to what the name is. + "pointer_expression" => { + let operator = node.child_by_field_name("operator").map(|o| o.kind()); + match node.child_by_field_name("argument").filter(|a| a.kind() == "identifier" && operator == Some("&")) { + Some(name) => self.cpp_written_name(name, source, r)?.map(|w| CppArgument::Type(format!("{}*", w.text))), + None => self.cpp_written_expression(node, source, r)?.map(|w| CppArgument::Type(w.text)), + } + } + "identifier" => self.cpp_written_name(node, source, r)?.map(|w| CppArgument::Type(w.text)), + // `keys[i]`: an element of the container, or what the pointer points at. + "subscript_expression" => { + let Some(container) = node.child_by_field_name("argument").filter(|a| a.kind() == "identifier") else { return Ok(None) }; + let Some(written) = self.cpp_written_name(container, source, r)? else { return Ok(None) }; + self.cpp_subscript_element(written, r)?.map(|w| CppArgument::Type(w.text)) + } + _ => self.cpp_written_expression(node, source, r)?.map(|w: CppWritten| CppArgument::Type(w.text)), + }) + } +} diff --git a/codegraph-kernel/src/resolve/cpp_range_for.rs b/codegraph-kernel/src/resolve/cpp_range_for.rs index 513c6e53c3..7354d3ed16 100644 --- a/codegraph-kernel/src/resolve/cpp_range_for.rs +++ b/codegraph-kernel/src/resolve/cpp_range_for.rs @@ -17,7 +17,7 @@ //! gives no element, and the call resolves as it did before. use super::cpp_aliases::type_segments; use super::cpp_implicit_this::strip_template_arguments; -use super::cpp_receivers::{cpp_ast_declared_type, cpp_binding_type, cpp_declarator_name, cpp_held_type}; +use super::cpp_receivers::{cpp_ast_declared_type, cpp_binding_type, cpp_declarator_name, cpp_held_type, cpp_holder_arguments}; use super::iteration::{descendant_for_position, named_children}; use super::*; @@ -31,13 +31,15 @@ pub(super) struct CppWritten { } /// The standard library's and abseil's containers whose elements are their first template -/// argument. A map's elements are key and value pairs, which no call this resolves is made on. +/// argument. const LIBRARY_SEQUENCES: &[&str] = &[ "vector", "list", "forward_list", "deque", "set", "multiset", "unordered_set", "unordered_multiset", "array", "span", "initializer_list", "Span", "InlinedVector", "FixedArray", "flat_hash_set", "node_hash_set", "btree_set", "btree_multiset", ]; /// Widely used project containers of the same kind: protobuf's repeated fields, rocksdb's `autovector`. const PROJECT_SEQUENCES: &[&str] = &["RepeatedField", "RepeatedPtrField", "autovector"]; +/// The standard library's and abseil's maps, whose elements are `std::pair`s of a key and a value. +const LIBRARY_MAPS: &[&str] = &["map", "multimap", "unordered_map", "unordered_multimap", "flat_hash_map", "node_hash_map", "btree_map", "btree_multimap"]; const MAX_DEPTH: u32 = 4; /// A written type without nullability annotations (`const FieldDescriptor* absl_nonnull`) or @@ -47,6 +49,10 @@ pub(super) fn cpp_plain_type(text: &str) -> String { text.split_whitespace().collect::>().join(" ") } +/// The class a member expression reads from, the member's name, and its explicit template +/// arguments (`<…>` as written). +type CppMemberRead = (Arc, String, Option); + /// A type written `auto`, `auto&`, `const auto*`. pub(super) fn is_cpp_auto(raw: &str) -> bool { type_segments(raw).is_some_and(|parts| parts.len() == 1 && parts[0] == "auto") @@ -106,8 +112,7 @@ pub(super) fn cpp_pointee(text: &str) -> Option { } } let plain = cpp_plain_type(text); - let held = re!(r"^(?:::)?std::(?:unique_ptr|shared_ptr|optional)\s*<(.*)>$").captures(&plain)?; - cpp_held_type(&held[1]) + cpp_held_type(cpp_holder_arguments(&plain)?) } /// The function declarator of a declarator that declares a function (`f()`, `*f()`, `&f()`), not @@ -163,7 +168,7 @@ fn type_depends_on(text: &str, parameters: &HashSet, depth: u32) -> bool let plain = cpp_plain_type(text); let primary = plain.split('<').next().unwrap_or("").trim().trim_start_matches("::").trim_end_matches(['*', '&', ' ']); if parameters.contains(primary.split("::").next().unwrap_or("").trim()) { return true; } - if let Some(held) = re!(r"^(?:::)?std::(?:unique_ptr|shared_ptr|optional)\s*<(.*)>[\s*&]*$").captures(&plain).and_then(|c| cpp_held_type(&c[1])) { + if let Some(held) = cpp_holder_arguments(plain.trim_end_matches(['*', '&', ' '])).and_then(cpp_held_type) { return type_depends_on(&held, parameters, depth + 1); } re!(r"^(?:::)?(?:std|absl)::").is_match(&plain) && re!(r"[A-Za-z_]\w*").find_iter(&plain).any(|word| parameters.contains(word.as_str())) @@ -366,7 +371,7 @@ impl KernelResolver { if depth >= 4 || !self.supertypes_complete { return Ok(None); } for decl in &decls { for base in self.supertype_nodes(&decl.id)? { - if !cpp_class_node(&base) || self.cpp_dependent_base(decl, &base)? { continue; } + if !cpp_class_node(&base) || (self.cpp_dependent_base(decl, &base)? && !self.cpp_using_declares(decl, &base, field)) { continue; } if let Some(member) = self.cpp_member_written_in(&base.qualified_name, field, r, depth + 1, seen)? { return Ok(Some(member)); } } } @@ -390,7 +395,7 @@ impl KernelResolver { /// The fixture class of a gtest `TEST_F(Fixture, Name)` / `TEST_P(Fixture, Name)` body, which is /// a method of a class derived from it. - fn cpp_gtest_fixture(&mut self, caller: &KNode, r: &ResolveRefIn) -> Res> { + pub(super) fn cpp_gtest_fixture(&mut self, caller: &KNode, r: &ResolveRefIn) -> Res> { if !matches!(caller.name.as_str(), "TEST_F" | "TEST_P") || !matches!(caller.kind.as_str(), "function" | "method") { return Ok(None); } let Some(source) = self.read_file(&caller.file_path) else { return Ok(None) }; let from = (caller.start_line - 1).max(0) as usize; @@ -503,7 +508,9 @@ impl KernelResolver { }, "this" => { let Some(caller) = self.node_by_id(&r.from_node_id)? else { return Ok(None) }; - Ok(self.cpp_member_owner(&caller)?.map(|owner| CppWritten { text: format!("::{owner}*"), site: r.clone() })) + // (A gtest `TEST_F` body's object is of a class derived from its fixture.) + let owner = match self.cpp_member_owner(&caller)? { Some(owner) => Some(owner), None => self.cpp_gtest_fixture(&caller, r)? }; + Ok(owner.map(|owner| CppWritten { text: format!("::{owner}*"), site: r.clone() })) } "identifier" => self.cpp_name_written(node, source, r, depth), "pointer_expression" => { @@ -515,7 +522,7 @@ impl KernelResolver { } // `compact_->sub_compact_states`: a data member of the object's class. "field_expression" => { - let Some((cls, field)) = self.cpp_member_of_expression(node, source, r, depth)? else { return Ok(None) }; + let Some((cls, field, None)) = self.cpp_member_of_expression(node, source, r, depth)? else { return Ok(None) }; self.cpp_data_member_written(&cls.qualified_name, &field, r) } // `new Foo(…)`: a pointer to what it constructs. @@ -542,30 +549,44 @@ impl KernelResolver { return Ok(Some(CppWritten { text: format!("std::{}_ptr<{}>", &made[1], made[2].trim()), site: r.clone() })); } // `std::unique_ptr(…)`: the smart pointer or optional it makes. - if re!(r"^(?:::\s*)?std\s*::\s*(?:unique_ptr|shared_ptr|optional)\s*<[\s\S]+>$").is_match(callee) { + if cpp_holder_arguments(&callee.split_whitespace().collect::()).is_some() { return Ok(Some(CppWritten { text: callee.to_string(), site: r.clone() })); } match function.kind() { - // `recv.method(…)`, `recv->method(…)`: the method of the receiver's class. + // `recv.method(…)`, `recv->method(…)`, `recv->method(…)`: the method of the + // receiver's class. "field_expression" => { - let Some((cls, method)) = self.cpp_member_of_expression(function, source, r, depth)? else { return Ok(None) }; + let Some((cls, method, template_arguments)) = self.cpp_member_of_expression(function, source, r, depth)? else { return Ok(None) }; let methods = self.cpp_methods_named(&cls, &method, r)?; - self.cpp_return_type_of(&methods, r) + let Some(arguments) = template_arguments else { return self.cpp_return_type_of(&methods, r) }; + // Explicit template arguments call a template, never an overload that is none. + let mut templates = Vec::new(); + for function in methods { + if !self.cpp_own_template_parameters(&function, r)?.is_empty() { templates.push(function); } + } + match self.cpp_member_template_return(&templates, &arguments, r)? { + Some(returned) => Ok(Some(returned)), + None => self.cpp_return_type_of(&templates, r), + } } // `f(…)`, `ns::f(…)`, `Class::f(…)`, `Foo(…)`: the caller's class's own or // inherited method first, then the function looked up from the caller's scope // outwards, then the class it constructs. "identifier" | "qualified_identifier" | "template_function" => { + // `create_iterator()` on a `std::function create_iterator`: what + // its function type returns. + if function.kind() == "identifier" { + if let Some(wrapper) = self.cpp_name_written(function, source, r, depth + 1)? { + if let Some(returned) = cpp_wrapper_return_type(&wrapper) { return Ok(Some(returned)); } + } + } let spelled: String = strip_template_arguments(callee).split_whitespace().collect(); if !re!(r"^(?:[A-Za-z_]\w*::)*[A-Za-z_]\w*$").is_match(&spelled) { return Ok(None); } let Some(caller) = self.node_by_id(&r.from_node_id)? else { return Ok(None) }; + // `NewIterator(…)` in a method, or in a gtest body: what the method of its + // class, or of a class it derives from, returns, looked up as on `this`. if !spelled.contains("::") { - if let Some(owner) = self.cpp_member_owner(&caller)? { - if let Some(cls) = self.cpp_class_decls(&owner)?.into_iter().next() { - let own = self.cpp_methods_named(&cls, &spelled, r)?; - if !own.is_empty() { return self.cpp_return_type_of(&own, r); } - } - } + if let Some(own) = self.cpp_implicit_this_method(&spelled, &caller, r)? { return self.cpp_return_type_of(&[own], r); } } let scopes: Vec = caller.qualified_name.split("::").map(str::to_string).collect(); for i in (0..scopes.len()).rev() { @@ -585,9 +606,35 @@ impl KernelResolver { } } + /// The declared type of the name `node` in `r`'s caller, as `cpp_name_written` reads it. + pub(super) fn cpp_written_name(&mut self, node: TsNode, source: &Rc, r: &ResolveRefIn) -> Res> { + self.cpp_name_written(node, source, r, 0) + } + + /// The type of a C++ expression in `r`'s caller, as `cpp_expression_written` reads it. + pub(super) fn cpp_written_expression(&mut self, node: TsNode, source: &Rc, r: &ResolveRefIn) -> Res> { + self.cpp_expression_written(node, source, r, 0) + } + + /// What `x[i]` is when `x` has the type `container`: what a pointer points at, or a + /// container's element. + pub(super) fn cpp_subscript_element(&mut self, container: CppWritten, r: &ResolveRefIn) -> Res> { + let Some(followed) = self.cpp_follow_written_aliases(container, r)? else { return Ok(None) }; + if top_level_pointer(&followed.text) { return Ok(cpp_pointee(&followed.text).map(|text| CppWritten { text, site: followed.site })); } + self.cpp_range_element_of(followed, r, 0) + } + /// The class of the object a member expression `x.m` or `x->m` names a member of, and `m`. - fn cpp_member_of_expression(&mut self, member: TsNode, source: &Rc, r: &ResolveRefIn, depth: u32) -> Res, String)>> { - let (Some(argument), Some(field)) = (member.child_by_field_name("argument"), member.child_by_field_name("field")) else { return Ok(None) }; + /// The class of the object a member expression `recv.name`, `recv->name` or `recv->name<…>` + /// reads from, with the member's name and its explicit template arguments (`<…>` as written). + fn cpp_member_of_expression(&mut self, member: TsNode, source: &Rc, r: &ResolveRefIn, depth: u32) -> Res> { + let (Some(argument), Some(mut field)) = (member.child_by_field_name("argument"), member.child_by_field_name("field")) else { return Ok(None) }; + let mut template_arguments = None; + if field.kind() == "template_method" { + let (Some(name), Some(arguments)) = (field.child_by_field_name("name"), field.child_by_field_name("arguments")) else { return Ok(None) }; + template_arguments = Some(source.text()[arguments.byte_range()].to_string()); + field = name; + } let name = source.text()[field.byte_range()].to_string(); if !re!(r"^[A-Za-z_]\w*$").is_match(&name) { return Ok(None); } let operator = super::awaited::strip_ts_comments(&source.text()[argument.end_byte()..field.start_byte()]); @@ -601,7 +648,54 @@ impl KernelResolver { "." if !top_level_pointer(&object.text) => object, _ => return Ok(None), }; - Ok(self.cpp_written_class(&object, r)?.map(|cls| (cls, name))) + Ok(self.cpp_written_class(&object, r)?.map(|cls| (cls, name, template_arguments))) + } + + /// The names of the template parameters a C++ function template's own `template <…>` header + /// declares, in order, without their defaults: `["Type"]` for protobuf's `template const Type& GetRaw(…)`. Empty for a function that is no template. + fn cpp_own_template_parameters(&mut self, function: &Arc, r: &ResolveRefIn) -> Res> { + let Some(source) = self.read_file(&function.file_path) else { return Ok(Vec::new()) }; + let site = site_at(r, function); + let Some(tree) = self.parsed_tree(&source, &site) else { return Ok(Vec::new()) }; + let mut node = descendant_for_position(tree.root_node(), source.text(), ((function.start_line - 1).max(0) as usize, function.start_column.max(0) as usize)); + loop { + if node.kind() == "template_declaration" { break; } + if matches!(node.kind(), "field_declaration_list" | "compound_statement" | "translation_unit" | "namespace_definition") { return Ok(Vec::new()); } + let Some(parent) = node.parent() else { return Ok(Vec::new()) }; + node = parent; + } + let Some(list) = node.child_by_field_name("parameters") else { return Ok(Vec::new()) }; + Ok(named_children(list).into_iter().filter(|p| !p.kind().contains("comment")).map(|p| { + let name = p.child_by_field_name("name").or_else(|| p.child_by_field_name("declarator")) + .or_else(|| named_children(p).into_iter().rev().find(|c| matches!(c.kind(), "type_identifier" | "identifier"))); + name.map(|n| source.text()[n.byte_range()].trim_start_matches("...").trim().to_string()).unwrap_or_default() + }).collect()) + } + + /// What a call of the C++ member template whose overloads are `templates`, with the explicit + /// template arguments `arguments` (`<…>` as written), returns when every overload is declared + /// to return the same one of its own template parameters, perhaps const, a pointer or a + /// reference: that argument, so qualified, as written at the call. protobuf's `template + /// const Type& GetRaw(…)` called as `GetRaw>(…)` returns + /// a `RepeatedPtrField&`. None otherwise: then the return type is read as written. + /// + /// Port of upstream's `cppMemberTemplateReturn` (name-matcher.ts at 1753472a, #2556). + fn cpp_member_template_return(&mut self, templates: &[Arc], arguments: &str, r: &ResolveRefIn) -> Res> { + let mut slot: Option<(usize, String, String)> = None; + for function in templates { + let Some((written, _)) = self.cpp_written_return_type(function, r)? else { continue }; + let param = re!(r"(?:const|volatile)|[*&]").replace_all(&written, " ").trim().to_string(); + let index = if param.is_empty() { None } else { self.cpp_own_template_parameters(function, r)?.iter().position(|p| *p == param) }; + let Some(index) = index else { return Ok(None) }; + if slot.as_ref().is_some_and(|(seen, _, _)| *seen != index) { return Ok(None); } + slot.get_or_insert((index, written, param)); + } + let Some((index, written, param)) = slot else { return Ok(None) }; + let inner = arguments.trim().strip_prefix('<').and_then(|a| a.strip_suffix('>')).unwrap_or(""); + let Some(argument) = super::overloads_upstream::split_top_level(inner).into_iter().nth(index).map(|a| a.trim().to_string()).filter(|a| !a.is_empty()) else { return Ok(None) }; + let Ok(word) = shared_regex(&format!(r"\b{}\b", regex::escape(¶m))) else { return Ok(None) }; + Ok(Some(CppWritten { text: word.replace(&written, regex::NoExpand(&argument)).into_owned(), site: r.clone() })) } /// The declared type of the name `node` in `r`'s caller: a parameter or local (an `auto` local @@ -610,6 +704,7 @@ impl KernelResolver { let name = &source.text()[node.byte_range()]; let mut at = node; while let Some(parent) = at.parent() { + if let Some(bound) = self.cpp_structured_binding_written(parent, at, name, source, r)? { return Ok(bound); } if let Some(raw) = cpp_binding_type(parent, at, name, source) { if raw.is_empty() || raw.contains('[') { return Ok(None); } if !is_cpp_auto(&raw) { return Ok(Some(CppWritten { text: raw, site: r.clone() })); } @@ -633,6 +728,54 @@ impl KernelResolver { self.cpp_caller_member(name, r) } + /// The type a structured binding in `parent` gives `name` where `at` is: the element at the + /// name's place of the pair or tuple bound (cppStructuredBindingType). That is an element of + /// the range a range-for over the binding iterates, when `at` is the loop's body (rocksdb's + /// `for (auto& [cfh, iter] : cfh_iter_pairs_)`, a map's key and value), or the initializer of + /// a declaration before `at` (`auto [status, iter] = Open();`). + /// - `None`: `parent` binds no such name there. + /// - `Some(None)`: it does, but the type can't be read. + pub(super) fn cpp_structured_binding_written(&mut self, parent: TsNode, at: TsNode, name: &str, source: &Rc, r: &ResolveRefIn) -> Res>> { + let bound = if parent.kind() == "for_range_loop" { + if !parent.child_by_field_name("body").is_some_and(|body| body.id() == at.id()) { return Ok(None); } + let Some(place) = parent.child_by_field_name("declarator").and_then(|d| binding_place(d, name, source)) else { return Ok(None) }; + self.cpp_range_for_element(parent, source, r, 0)?.map(|value| (value, place)) + } else { + let children = match parent.kind() { + "compound_statement" | "declaration_list" | "translation_unit" => named_children(parent), + _ => statement_initializer(parent).into_iter().collect(), + }; + let mut found = None; + for declaration in children.into_iter().rev().filter(|n| n.kind() == "declaration" && n.end_byte() <= at.start_byte()) { + let mut cursor = declaration.walk(); + let declarators: Vec<_> = declaration.children_by_field_name("declarator", &mut cursor).collect(); + if let Some((declarator, place)) = declarators.iter().find_map(|d| binding_place(*d, name, source).map(|p| (*d, p))) { + found = Some((declarator, place)); + break; + } + // A nearer declaration of the name of any other kind is the name's. + if declarators.iter().any(|d| cpp_declarator_name(*d, source) == name) { return Ok(None); } + } + let Some((declarator, place)) = found else { return Ok(None) }; + match declarator.child_by_field_name("value").filter(|_| declarator.kind() == "init_declarator").filter(|v| !matches!(v.kind(), "initializer_list" | "argument_list")) { + Some(value) => self.cpp_initializer_written(value, source, r)?.map(|value| (value, place)), + None => None, + } + }; + let Some((value, place)) = bound else { return Ok(Some(None)) }; + Ok(Some(self.cpp_binding_elements(value, r)?.and_then(|elements| elements.into_iter().nth(place)))) + } + + /// What a structured binding of a value of the type `value` names, in order: a `std::pair`'s or + /// `std::tuple`'s elements. None for any other type. + fn cpp_binding_elements(&mut self, value: CppWritten, r: &ResolveRefIn) -> Res>> { + let Some(followed) = self.cpp_follow_written_aliases(value, r)? else { return Ok(None) }; + let plain = cpp_plain_type(&followed.text); + if !type_segments(&plain).is_some_and(|parts| parts.len() == 2 && parts[0] == "std" && (parts[1] == "pair" || parts[1] == "tuple")) { return Ok(None); } + if top_level_pointer(&plain) { return Ok(None); } + Ok(template_arguments(&plain).map(|arguments| arguments.into_iter().map(|text| CppWritten { text: cpp_plain_type(&text), site: followed.site.clone() }).collect())) + } + /// The element type of a range of the type `range`, as written: a container's first template /// argument, or, for one of the project's classes, what its `begin()` returns, dereferenced. fn cpp_range_element_of(&mut self, range: CppWritten, r: &ResolveRefIn, hops: u32) -> Res> { @@ -641,6 +784,11 @@ impl KernelResolver { if top_level_pointer(&range.text) { return Ok(None); } let Some(parts) = type_segments(&range.text) else { return Ok(None) }; let last = parts.last().map(String::as_str).unwrap_or(""); + // A map's elements pair its key with its value. + if LIBRARY_MAPS.contains(&last) && (parts[0] == "std" || parts[0] == "absl") { + let Some([key, value]) = template_arguments(&range.text).and_then(|a| <[String; 2]>::try_from(a.into_iter().take(2).collect::>()).ok()) else { return Ok(None) }; + return Ok(Some(CppWritten { text: format!("std::pair", cpp_plain_type(&key), cpp_plain_type(&value)), site: range.site })); + } let library = LIBRARY_SEQUENCES.contains(&last); if library || PROJECT_SEQUENCES.contains(&last) { // A project type of a standard container's name, written without `std::` or `absl::`, @@ -690,6 +838,39 @@ impl KernelResolver { } } +/// Where `name` is in the structured binding `declarator` writes (`[cfh, iter]` in `auto& [cfh, +/// iter]`, `auto [a, b] = …`), when it writes one. +fn binding_place(mut declarator: TsNode, name: &str, source: &SourceFile) -> Option { + while matches!(declarator.kind(), "init_declarator" | "reference_declarator" | "pointer_declarator" | "parenthesized_declarator") { + declarator = declarator.child_by_field_name("declarator").or_else(|| declarator.named_child(0))?; + } + if declarator.kind() != "structured_binding_declarator" { return None; } + named_children(declarator).iter().position(|n| &source.text()[n.byte_range()] == name) +} + +/// What calling a function wrapper of the written type `wrapper` returns, as written: what the +/// function type that is its template argument returns (`Iterator*` for +/// `std::function`). None for any other type. +fn cpp_wrapper_return_type(wrapper: &CppWritten) -> Option { + let plain = cpp_plain_type(&wrapper.text); + let plain = plain.trim_end_matches(['&', ' ']); + if !re!(r"^(?:::\s*)?(?:std\s*::\s*(?:function|move_only_function|copyable_function)|absl\s*::\s*(?:FunctionRef|AnyInvocable))\s*<").is_match(plain) { return None; } + let signature = template_arguments(plain)?.into_iter().next()?; + let mut depth = 0i32; + for (i, c) in signature.char_indices() { + match c { + '<' => depth += 1, + '>' => depth -= 1, + '(' if depth == 0 => { + let returned = signature[..i].trim(); + return (!returned.is_empty()).then(|| CppWritten { text: returned.to_string(), site: wrapper.site.clone() }); + } + _ => {} + } + } + None +} + #[cfg(test)] mod tests { use super::{cpp_pointee, template_arguments, cpp_plain_type}; diff --git a/codegraph-kernel/src/resolve/cpp_receivers.rs b/codegraph-kernel/src/resolve/cpp_receivers.rs index fc6fe5f583..81f3f00feb 100644 --- a/codegraph-kernel/src/resolve/cpp_receivers.rs +++ b/codegraph-kernel/src/resolve/cpp_receivers.rs @@ -37,6 +37,14 @@ impl KernelResolver { // The initializer of an `auto` local, which gives it its type. let mut initializer = None; while let Some(parent) = scope.parent() { + // `for (auto& [cfh, iter] : pairs)`, `auto [status, iter] = Open();`: the pair or tuple element. + if let Some(bound) = self.cpp_structured_binding_written(parent, scope, name, &source, r)? { + let Some(written) = bound else { return Ok(Some(None)) }; + raw = Some(super::cpp_range_for::cpp_plain_type(&written.text)); + site = written.site; + element = true; + break; + } if let Some(ty) = cpp_binding_type(parent, scope, name, &source) { if super::cpp_range_for::is_cpp_auto(&ty) { initializer = super::cpp_range_for::binding_initializer(parent, scope, name, &source); } raw = Some(ty); @@ -97,12 +105,19 @@ impl KernelResolver { let ty = match operator.trim() { "." if pointers == 0 => raw.trim_end_matches('&').trim().to_string(), "->" if pointers == 1 && !raw.contains('<') => raw.replace(['*', '&'], "").trim().to_string(), - // An element that points at a class template (`const autovector*`). - "->" if pointers == 1 && element => super::cpp_range_for::cpp_pointee(&raw).unwrap_or_default(), + // A pointer to a class template (`const autovector*`, a `Table` + // pointer declared over two lines); a library template's members are no project method. + "->" if pointers == 1 => { + let pointee = super::cpp_range_for::cpp_pointee(&raw).unwrap_or_default(); + if re!(r"^(?:const\s+)?(?:::)?(?:std|absl)::").is_match(pointee.trim()) { return Ok(Some(None)); } + super::cpp_range_for::cpp_plain_type(&pointee) + } // A standard smart pointer or optional: `->` reaches a member of the type it holds. + // So does an `absl::StatusOr`, `absl::optional` or `absl::NoDestructor` of the library's. "->" if pointers == 0 => { - let Some(hit) = re!(r"^(?:const\s+)?(?:::)?std::(?:unique_ptr|shared_ptr|optional)\s*<(.*)>\s*&?$").captures(&raw) else { return Ok(Some(None)) }; - let Some(held) = cpp_held_type(&hit[1]) else { return Ok(None) }; + let Some(hit) = re!(r"^(?:const\s+)?(?:::)?((?:std|absl)::[A-Za-z_]\w*)\s*<(.*)>\s*&?$").captures(&raw) else { return Ok(Some(None)) }; + if !CPP_HOLDERS.contains(&&hit[1]) || !self.cpp_library_holder(&hit[1])? { return Ok(Some(None)); } + let Some(held) = cpp_held_type(&hit[2]) else { return Ok(None) }; if held.trim_start_matches("::").starts_with("std::") { return Ok(Some(None)); } held } @@ -280,7 +295,8 @@ impl KernelResolver { fn cpp_field_tree(&mut self, source: &Rc, r: &ResolveRefIn) -> Option> { let normalized = source.cpp_field_tree.get_or_init(|| { // Match extraction's padded access-macro rewrite; byte offsets stay fixed. - let text = re!(r"(?m)^([ \t]*)([A-Z][A-Z0-9]*_[A-Z0-9_]*)([ \t]*):([ \t]*(?://[^\n]*)?\r?$)").replace_all(source.text(), |m: ®ex::Captures| { + let blanked = super::cpp_annotations::cpp_without_annotations(source.text()); + let text = re!(r"(?m)^([ \t]*)([A-Z][A-Z0-9]*_[A-Z0-9_]*)([ \t]*):([ \t]*(?://[^\n]*)?\r?$)").replace_all(&blanked, |m: ®ex::Captures| { let name = &m[2]; let keyword = if name.contains("PRIVATE") { "private" } else if name.contains("PROTECTED") { "protected" } else if name.contains("PUBLIC") || matches!(name, "Q_SIGNALS" | "Q_SLOTS") { "public" } else { return m[0].to_string(); }; @@ -288,7 +304,7 @@ impl KernelResolver { if keyword.len() > width { return m[0].to_string(); } format!("{}{}{}:{}", &m[1], keyword, " ".repeat(width - keyword.len()), &m[4]) }); - if text.as_ref() == source.text() { return None; } + if text.as_ref() == blanked.as_ref() { return None; } crate::tree::parse_with_cached_parser(text.as_ref(), "cpp").ok().map(Rc::new) }).clone(); normalized.or_else(|| self.parsed_tree(source, r)) @@ -367,6 +383,27 @@ impl KernelResolver { } } +/// Library types whose `->` reaches the value they hold. +pub(super) const CPP_HOLDERS: &[&str] = &["std::unique_ptr", "std::shared_ptr", "std::optional", "absl::optional", "absl::StatusOr", "absl::NoDestructor"]; + +/// The template arguments of a holder `raw` writes (CPP_HOLDERS): `Iterator` of +/// `std::unique_ptr`, `FeatureResolver` of `absl::StatusOr`. +pub(super) fn cpp_holder_arguments(raw: &str) -> Option<&str> { + let hit = re!(r"^(?:::)?((?:std|absl)::[A-Za-z_]\w*)\s*<(.*)>$").captures(raw)?; + CPP_HOLDERS.contains(&hit.get(1)?.as_str()).then(|| hit.get(2).map(|m| m.as_str())).flatten() +} + +impl KernelResolver { + /// Whether the holder `spelled` names is the library's: no type of the project's has its name + /// (isCppLibraryHolder). + pub(super) fn cpp_library_holder(&mut self, spelled: &str) -> Res { + let last = spelled.rsplit("::").next().unwrap_or(spelled); + let suffix = format!("::{spelled}"); + Ok(!self.nodes_by_name(last)?.iter().any(|n| matches!(n.language.as_str(), "c" | "cpp") + && matches!(n.kind.as_str(), "class" | "struct" | "union" | "type_alias") && (n.qualified_name == spelled || n.qualified_name.ends_with(&suffix)))) + } +} + /// The class a subscript of a local reaches: the element of a standard sequence container, or of a /// one-dimensional array of a class (`Item items[2]`, which the declared type reads as `Item[]`). fn cpp_element_type(raw: &str) -> Option { diff --git a/codegraph-kernel/src/resolve/cpp_translation_unit.rs b/codegraph-kernel/src/resolve/cpp_translation_unit.rs new file mode 100644 index 0000000000..addde4883f --- /dev/null +++ b/codegraph-kernel/src/resolve/cpp_translation_unit.rs @@ -0,0 +1,203 @@ +//! What only another C or C++ translation unit can name: a type or alias a source file declares, +//! a member of a class only source files declare, and what a source file's unnamed namespace +//! (`namespace { … }`) declares. rocksdb's test fixture `ColumnFamilyTestBase` is declared in +//! column_family_test.cc, and other test files' bare `Put(…)` reached its `Put` by name; +//! inlineskiplist.h's `Node*` reached hash_linklist_rep.cc's own `Node`. +//! +//! Port of upstream's `isCppTranslationUnitLocal` and `cppVisibleFirst` (name-matcher.ts at +//! 61cb4188, #2538); read the original with `git show 61cb4188:src/resolution/name-matcher.ts`. +//! What a source file declares is still seen by the file itself, by a file that includes it, +//! directly or through others (a test that includes the source it tests), and by a header it +//! includes below the declaration (rocksdb's version_set.cc includes version_set_sync_and_async.h +//! after its `FilePicker` classes). +use super::tables::c_source_ext_re; +use super::*; + +/// What a C or C++ type declaration can be: the kinds a translation-unit-local type has. +fn cpp_type_kind(kind: &str) -> bool { + matches!(kind, "class" | "struct" | "union" | "enum" | "interface" | "type_alias") +} + +fn cpp_class_kind(kind: &str) -> bool { + matches!(kind, "class" | "struct" | "union") +} + +/// The line spans (1-based, inclusive) of the unnamed namespaces `text` writes, the outermost of +/// nested ones: what one declares has internal linkage, so no other translation unit names it. +/// Comments, preprocessor lines and string, character and raw string literals are skipped. +pub(super) fn unnamed_namespace_spans(text: &str) -> Vec<(i64, i64)> { + let mut spans = Vec::new(); + if !re!(r"\bnamespace\s*\{").is_match(text) { return spans; } + let bytes = text.as_bytes(); + // Each open brace: the line it opens on, and whether it opens an unnamed namespace. + let mut open: Vec<(i64, bool)> = Vec::new(); + let mut line = 1i64; + let mut line_start = true; + // The code since the last brace or `;`, without comments and literals. + let mut code = String::new(); + let mut i = 0; + let count_lines = |from: usize, to: usize| bytes[from..to.min(bytes.len())].iter().filter(|&&b| b == b'\n').count() as i64; + while i < bytes.len() { + let c = bytes[i]; + if c == b'\n' { + line += 1; + line_start = true; + code.push(' '); + i += 1; + continue; + } + if line_start && c == b'#' { + // A preprocessor line, continued by a trailing backslash. + let mut end = i; + while end < bytes.len() && (bytes[end] != b'\n' || (end > 0 && bytes[end - 1] == b'\\') || (end > 1 && bytes[end - 1] == b'\r' && bytes[end - 2] == b'\\')) { + if bytes[end] == b'\n' { line += 1; } + end += 1; + } + i = end; + continue; + } + if !c.is_ascii_whitespace() { line_start = false; } + if c == b'/' && bytes.get(i + 1) == Some(&b'/') { + i = text[i..].find('\n').map_or(bytes.len(), |n| i + n); + continue; + } + if c == b'/' && bytes.get(i + 1) == Some(&b'*') { + let stop = text[i + 2..].find("*/").map_or(bytes.len(), |n| i + 2 + n + 2); + line += count_lines(i, stop); + i = stop; + continue; + } + if c == b'"' && { + let from = (i.saturating_sub(4)..i).find(|&k| text.is_char_boundary(k)).unwrap_or(i); + re!(r"(?:^|[^\w$])(?:u8|[uUL])?R$").is_match(&text[from..i]) + } { + // `R"delim( … )delim"`. + let stop = match text[i..].find('(') { + Some(p) => { + let paren = i + p; + let closing = format!("){}\"", &text[i + 1..paren]); + text[paren..].find(&closing).map_or(bytes.len(), |n| paren + n + closing.len()) + } + None => bytes.len(), + }; + line += count_lines(i, stop); + i = stop; + continue; + } + if c == b'"' || c == b'\'' { + let mut j = i + 1; + while j < bytes.len() && bytes[j] != c && bytes[j] != b'\n' { + j += if bytes[j] == b'\\' { 2 } else { 1 }; + } + i = j + 1; + continue; + } + match c { + b'{' => { + open.push((line, re!(r"(?:^|[^\w$])namespace\s*$").is_match(&code))); + code.clear(); + } + b'}' => { + if let Some((start, unnamed)) = open.pop() { + if unnamed && !open.iter().any(|(_, u)| *u) { spans.push((start, line)); } + } + code.clear(); + } + b';' => code.clear(), + _ => { + // (Only the code's end is read; keep it short.) + if code.len() > 200 { code.drain(..code.len() - 100); } + if c.is_ascii() { code.push(c as char); } else { code.push('_'); } + } + } + i += 1; + } + spans +} + +impl KernelResolver { + /// Whether only another C or C++ translation unit than `r`'s can name `candidate`. + pub(super) fn cpp_translation_unit_local(&mut self, candidate: &KNode, r: &ResolveRefIn) -> Res { + if !matches!(r.language.as_str(), "c" | "cpp") { return Ok(false); } + if cpp_type_kind(&candidate.kind) { return Ok(!self.cpp_seen_from(candidate, &r.file_path)?); } + // A member written in its class's declaration is that declaration's; one defined out of + // line is the class's, wherever a declaration of it its own file sees is (through an + // alias, the class may be declared anywhere). A scope its file sees no type of may be a + // namespace: fmt's posix-mock-test.cc defines `test::close` of posix-mock.h's `namespace + // test`, and ostream-test.cc declares a `struct test` of its own. + if let Some(enclosing) = self.cpp_enclosing_class_decl(candidate)? { + return Ok(!self.cpp_seen_from(&enclosing, &r.file_path)?); + } + let scope = match candidate.qualified_name.rfind("::") { Some(cut) => &candidate.qualified_name[..cut], None => "" }; + if !scope.is_empty() { + let mut decls = Vec::new(); + for d in self.nodes_by_qualified_name(scope)?.iter() { + if cpp_type_kind(&d.kind) && matches!(d.language.as_str(), "c" | "cpp") && self.cpp_seen_from(d, &candidate.file_path)? { decls.push(d.clone()); } + } + if decls.iter().any(|d| d.kind == "type_alias") { return Ok(false); } + if !decls.is_empty() { + for d in &decls { + if self.cpp_seen_from(d, &r.file_path)? { return Ok(false); } + } + return Ok(true); + } + } + if self.cpp_seen_from(candidate, &r.file_path)? { return Ok(false); } + let Some(source) = self.read_file(&candidate.file_path) else { return Ok(false) }; + Ok(source.cpp_unnamed_namespaces().iter().any(|&(start, end)| candidate.start_line >= start && candidate.start_line <= end)) + } + + /// Whether the declaration `decl` is seen from `file`: it is no source file's, or `file` is its + /// own, includes its file, or is a header its file includes below it. + fn cpp_seen_from(&mut self, decl: &KNode, file: &str) -> Res { + if !c_source_ext_re().is_match(&decl.file_path) || decl.file_path == file { return Ok(true); } + if self.namespace_visible_files(file, &decl.language)?.contains(&decl.file_path) { return Ok(true); } + self.cpp_includes_after(&decl.file_path, decl.start_line, file, &decl.language) + } + + /// Whether the C or C++ file `source` `#include`s `header` below line `line`. + fn cpp_includes_after(&mut self, source: &str, line: i64, header: &str, language: &str) -> Res { + let includes: Vec<_> = self.nodes_in_file(source)?.iter() + .filter(|n| n.kind == "import" && n.start_line > line && matches!(n.language.as_str(), "c" | "cpp")).cloned().collect(); + for node in includes { + let delimiter = if node.signature.as_deref().is_some_and(|s| s.contains('<')) { '<' } else { '"' }; + if self.resolve_cpp_include(source, delimiter, &node.name, language)?.as_deref() == Some(header) { return Ok(true); } + } + Ok(false) + } + + /// The innermost C or C++ class, struct or union declaration of its file whose lines hold `member`. + fn cpp_enclosing_class_decl(&mut self, member: &KNode) -> Res>> { + let mut innermost: Option> = None; + for cls in self.nodes_in_file(&member.file_path)?.iter() { + if !cpp_class_kind(&cls.kind) || cls.id == member.id || member.start_line < cls.start_line || member.start_line > cls.end_line { continue; } + if innermost.as_ref().is_none_or(|i| cls.start_line >= i.start_line) { innermost = Some(cls.clone()); } + } + Ok(innermost) + } + + /// Of the methods a C or C++ type's name gives, those `r`'s translation unit can see, when + /// there are any (cppVisibleFirst). Classes of one qualified name can be declared apart: + /// rocksdb's db_stress_shared_state.h and two benchmark `.cc` files each declare a + /// `SharedState`, and a call in db_stress_common.cc is the header's. + pub(super) fn cpp_visible_first(&mut self, methods: Vec>, r: &ResolveRefIn) -> Res>> { + if methods.len() < 2 || !matches!(r.language.as_str(), "c" | "cpp") { return Ok(methods); } + let mut visible = Vec::new(); + for m in &methods { + if !self.cpp_translation_unit_local(m, r)? { visible.push(m.clone()); } + } + Ok(if visible.is_empty() { methods } else { visible }) + } +} + +#[cfg(test)] +mod tests { + use super::unnamed_namespace_spans; + + #[test] + fn unnamed_namespaces_past_comments_and_literals() { + let text = "namespace a {\nnamespace {\nint f();\nnamespace {\n}\n}\n// namespace {\n/* namespace { */\n#define X namespace {\nconst char* s = \"namespace {\";\nconst char* r = R\"(a \"namespace {\" b)\";\n}\n"; + assert_eq!(unnamed_namespace_spans(text), vec![(2, 6)]); + assert_eq!(unnamed_namespace_spans("#define B \\\n namespace {\nint g();\n"), Vec::<(i64, i64)>::new()); + } +} diff --git a/codegraph-kernel/src/resolve/cpp_types.rs b/codegraph-kernel/src/resolve/cpp_types.rs index 4d7b89c422..a10dccbf5f 100644 --- a/codegraph-kernel/src/resolve/cpp_types.rs +++ b/codegraph-kernel/src/resolve/cpp_types.rs @@ -130,6 +130,10 @@ impl KernelResolver { let [owner] = visible.as_slice() else { return Ok(None) }; return self.cpp_expand_owner(owner, r, depth, constructor, included); } + // A name C++ finds in a class's base: `Derived::Nested` for a `Nested` the base declares. + if let Some(found) = self.cpp_type_through_bases(raw, r, included)? { + return self.cpp_expand_owner(&found, r, depth, constructor, included); + } // Namespace-opening macros can leave a header's indexed owner unqualified. if ty.contains("::") { let named = self.nodes_by_name(simple)?; @@ -187,7 +191,7 @@ impl KernelResolver { Ok(false) } - fn cpp_expand_owner(&mut self, owner: &Arc, r: &ResolveRefIn, depth: u32, constructor: bool, included: &HashSet) -> Res>> { + pub(super) fn cpp_expand_owner(&mut self, owner: &Arc, r: &ResolveRefIn, depth: u32, constructor: bool, included: &HashSet) -> Res>> { if owner.kind != "type_alias" { return Ok((owner.kind != "enum").then(|| owner.clone())); } let Some(source) = self.read_file(&owner.file_path) else { return Ok(None) }; let mut lines = source.get((owner.start_line - 1).max(0) as usize..owner.end_line.max(0) as usize).unwrap_or(&[]).to_vec(); diff --git a/codegraph-kernel/src/resolve/iteration.rs b/codegraph-kernel/src/resolve/iteration.rs index c2ccd91b07..56c1ef0c57 100644 --- a/codegraph-kernel/src/resolve/iteration.rs +++ b/codegraph-kernel/src/resolve/iteration.rs @@ -247,7 +247,9 @@ impl KernelResolver { return tree.clone(); } } - let tree = crate::tree::parse_with_cached_parser(file.text(), &r.language).ok().map(Rc::new); + // C++ is parsed as extraction parses it, with its declarations' annotation macros blanked. + let text = if r.language == "cpp" { super::cpp_annotations::cpp_without_annotations(file.text()) } else { std::borrow::Cow::Borrowed(file.text()) }; + let tree = crate::tree::parse_with_cached_parser(&text, &r.language).ok().map(Rc::new); self.tree_cache.put(key, (file.clone(), tree.clone())); tree } diff --git a/codegraph-kernel/src/resolve/method_call.rs b/codegraph-kernel/src/resolve/method_call.rs index bb6cbc48c4..e07b0f291c 100644 --- a/codegraph-kernel/src/resolve/method_call.rs +++ b/codegraph-kernel/src/resolve/method_call.rs @@ -1354,13 +1354,26 @@ if inferred.is_none() && r.language == "dart" { if !class_like || c.language != r.language { continue; } + // C++'s `Outer::Count()` is the member C++ looks up in `Outer`: its own, else one a + // base declares, never one of a class nested in it, like `Outer::Inner::Count` beside + // an inherited `Base::Count`. + if c.language == "cpp" && resolved_by == "qualified-name" { + let Some(found) = self.cpp_method_of(c, method, r, 0, &mut HashSet::new())? else { continue }; + let own = found.qualified_name == format!("{}::{method}", c.qualified_name); + return Ok(Some(KCand { node: found, confidence: if own { 0.85 } else { 0.8 }, resolved_by })); + } let in_file = self.nodes_in_file(&c.file_path)?; if let Some(mn) = in_file .iter() // An owner segment equal to the class name: `Loud` is not - // `Loudspeaker`. + // `Loudspeaker`. In C and C++, only a method of exactly that + // class: protobuf's `arena.get()` is not `Arena::UniquePtr::get`. .find(|n| { - n.kind == "method" && n.name == method && n.qualified_name.split([':', '.']).any(|s| s == c.name) + n.kind == "method" && n.name == method && if matches!(c.language.as_str(), "c" | "cpp") { + n.qualified_name == format!("{}::{method}", c.qualified_name) + } else { + n.qualified_name.split([':', '.']).any(|s| s == c.name) + } }) { return Ok(Some(KCand { node: mn.clone(), confidence, resolved_by })); diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index a4ff15216f..a3467a75cc 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -343,6 +343,7 @@ pub(super) struct SourceFile { php_file_scope: OnceCell, rust_code_lines: OnceCell>, cpp_code_lines: OnceCell>, + cpp_unnamed_namespaces: OnceCell>, python_file: OnceCell, python_field_file: OnceCell, cpp_field_tree: OnceCell>>, @@ -362,6 +363,7 @@ impl SourceFile { php_file_scope: OnceCell::new(), rust_code_lines: OnceCell::new(), cpp_code_lines: OnceCell::new(), + cpp_unnamed_namespaces: OnceCell::new(), python_file: OnceCell::new(), python_field_file: OnceCell::new(), cpp_field_tree: OnceCell::new(), @@ -416,9 +418,16 @@ impl SourceFile { .get_or_init(|| fields::mask_rust_code(self.text()).split('\n').map(str::to_string).collect()) } + /// The line spans of the file's unnamed namespaces (cpp_translation_unit). + pub(super) fn cpp_unnamed_namespaces(&self) -> &[(i64, i64)] { + self.cpp_unnamed_namespaces.get_or_init(|| cpp_translation_unit::unnamed_namespace_spans(self.text())) + } + /// C++ lines with raw literals and comments masked, shared by receiver lookups. pub(super) fn cpp_code_lines(&self) -> &[String] { - self.cpp_code_lines.get_or_init(|| awaited::strip_ts_comments(&cpp::mask_cpp_raw_strings(self.text())).split('\n').map(str::to_string).collect()) + // (Block comments and string contents are blanked too: protobuf's `[[deprecated("Please use + // code() instead")]]` and rocksdb's `"Unknown code(%d): "` declare no local.) + self.cpp_code_lines.get_or_init(|| String::from_utf8_lossy(&cpp_annotations::masked_code(&cpp::mask_cpp_raw_strings(self.text()))).split('\n').map(str::to_string).collect()) } /// The file's Python statements and the scopes they run in @@ -574,6 +583,10 @@ mod cpp_receivers; mod cpp_range_for; mod cpp_implicit_this; mod cpp_namespace; +mod cpp_annotations; +mod cpp_construction; +mod cpp_overloads; +mod cpp_translation_unit; mod inherited_method; mod store; mod esm_scope; @@ -675,6 +688,9 @@ pub struct KernelResolver { /// A Python class's resolved bases, by class id: they depend on the class's /// own header and file, so every scan over the same classes shares them. py_bases_memo: HashMap>>>, + /// The files a C or C++ file sees without knowing its includes, for the file last asked: + /// every construction in a file shares them. (One entry: a set holds nearly every header.) + cpp_visible_memo: Option<(String, Rc>)>, /// Assignments and annotations typing a Python class's `self.`, /// by (class id, field). py_field_facts_memo: HashMap<(String, String), Rc>>, @@ -879,6 +895,7 @@ impl KernelResolver { kotlin_supers_memo: HashMap::new(), py_descendants_memo: HashMap::new(), py_bases_memo: HashMap::new(), + cpp_visible_memo: None, py_field_facts_memo: HashMap::new(), py_globals: Default::default(), ruby_ancestry_memo: HashMap::new(), diff --git a/codegraph-kernel/src/resolve/names.rs b/codegraph-kernel/src/resolve/names.rs index 71d94ea3ac..c62d80002c 100644 --- a/codegraph-kernel/src/resolve/names.rs +++ b/codegraph-kernel/src/resolve/names.rs @@ -311,12 +311,15 @@ impl KernelResolver { || !self.language_type_visible(candidate, r)? { return Ok(false); } let lang = candidate.language.as_str(); if lang == "c" || lang == "cpp" { - return Ok(candidate.kind != "function" + let static_seen = candidate.kind != "function" || !c_source_ext_re().is_match(&candidate.file_path) || !self.is_static_c_function(candidate)? || (matches!(r.language.as_str(), "c" | "cpp") && (self.c_includes(&r.file_path, &candidate.file_path)? - || self.c_sole_includer(candidate, &r.file_path)?))); + || self.c_sole_includer(candidate, &r.file_path)?)); + // So is what a source file's unnamed namespace declares, and a type only source files + // declare, with its members (cpp_translation_unit). + return Ok(static_seen && !self.cpp_translation_unit_local(candidate, r)?); } if lang == "go" { let first = candidate.name.chars().next().unwrap_or('_'); diff --git a/codegraph-kernel/src/resolve/overloads_upstream.rs b/codegraph-kernel/src/resolve/overloads_upstream.rs index aa109ab558..faee16c880 100644 --- a/codegraph-kernel/src/resolve/overloads_upstream.rs +++ b/codegraph-kernel/src/resolve/overloads_upstream.rs @@ -1,4 +1,5 @@ //! Source argument shape disambiguates a method's overload family. +use super::cpp_overloads::CppTypeCategory; use super::*; pub(super) fn split_top_level(text: &str) -> Vec { @@ -200,7 +201,7 @@ impl KernelResolver { }) .cloned() .collect(); - if siblings.is_empty() { + if siblings.is_empty() || (r.language == "cpp" && r.reference_name.contains("().")) { return Ok(hit); } if r.language == "swift" { @@ -711,14 +712,20 @@ impl KernelResolver { if flat.contains("...") { break; } - if !re!(r#"^(?:u8|u|U|L)?"|^FMT_STRING\s*\("#).is_match(arg) { + if !re!(r#"^(?:u8|u|U|L)?R?"|^FMT_STRING\s*\("#).is_match(arg) { continue; } - let wide_arg = arg.starts_with("L\""); + let wide_arg = re!(r#"^LR?""#).is_match(arg); let wide_param = re!(r"\bw(?:string|char_t|format|string_view)|wchar_t").is_match(param); + // A literal, raw ones too (protobuf's `p->Emit({…}, R"cc(…)cc")`), is a `const char*`: + // no pointer to anything else (rocksdb's `std::string* value`), number or character + // takes it, which counts for more than a class of another name. + let category = super::cpp_overloads::cpp_type_category(&super::cpp_overloads::cpp_parameter_type(param)); if wide_arg != wide_param && re!(r"string|char|Char|format").is_match(param) { score -= 2; + } else if n.language == "cpp" && matches!(category, CppTypeCategory::Pointer | CppTypeCategory::Number | CppTypeCategory::Bool | CppTypeCategory::Char) { + score -= 3; } else { score += if re!(r"string|char|Char|\bstr\b").is_match(param) { 3 @@ -740,9 +747,11 @@ impl KernelResolver { || !self.dart_top_level_visible(&hit.node, r)? || !self.dart_bare_member_visible(&hit.node, r)? { return Ok(false); } + // (A chained call `inner(…).name(…)` has the inner call's arguments at its position.) if r.language == "cpp" && r.reference_kind == "calls" && !super::cpp::is_cpp_constructor_ref(r) + && !r.reference_name.contains("().") { let candidate = self.retarget_overload( KCand { diff --git a/codegraph-kernel/src/resolve/pipeline.rs b/codegraph-kernel/src/resolve/pipeline.rs index 24192d7124..3c958eeb25 100644 --- a/codegraph-kernel/src/resolve/pipeline.rs +++ b/codegraph-kernel/src/resolve/pipeline.rs @@ -776,6 +776,9 @@ if super::lang_scope::is_dart_member_read(r) { /// The per-ref pipeline: the Rust `::`-path arm, then one route. pub(super) fn resolve_ref(&mut self, r: &ResolveRefIn) -> Res { + if let Some(result) = self.cpp_construction(r)? { + return match result { Some(candidate) => self.finish_pre_framework(r, candidate), None => Ok(self.refused()) }; + } if let Some(result) = self.cpp_plain_call(r)? { return match result { Some(candidate) => self.finish_pre_framework(r, candidate), None => Ok(self.refused()) }; } diff --git a/codegraph-kernel/src/resolve/receivers.rs b/codegraph-kernel/src/resolve/receivers.rs index 60726cecf5..c83aead920 100644 --- a/codegraph-kernel/src/resolve/receivers.rs +++ b/codegraph-kernel/src/resolve/receivers.rs @@ -613,7 +613,7 @@ impl KernelResolver { /// normalizeCppTypeName — strip cv-qualifiers/keywords, refs, generics; /// take the last `::` segment (or the qualified name when preserving). pub(super) fn normalize_cpp_type_name(&mut self, raw: &str, preserve: bool) -> Res> { - let kw = re!(r"(?-u:\b)(?:const|volatile|mutable|typename|class|struct)(?-u:\b)") + let kw = re!(r"(?-u:\b)(?:const|volatile|mutable|typename|class|struct|__restrict__|__restrict)(?-u:\b)") .replace_all(raw, " "); let no_ref = re!(r"[&*]+").replace_all(&kw, " "); let mut no_gen = String::new(); @@ -645,11 +645,16 @@ impl KernelResolver { /// buildDeclaratorRegex — `Type receiver` requiring a declarator /// terminator. The JS lookahead `(?=[;=,)\[{(]|$)` is post-checked on the /// remainder: the greedy `\s*` tail can't shrink into a passing position. + /// Qualifiers and annotations around the name are read past + /// (`Iterator* const iter`, `Arena* PROTOBUF_NULLABLE arena`, `VersionSet* + /// versions_ GUARDED_BY(mutex_);`), and the type comes back without the + /// annotations (cpp_declarator_type). pub(super) fn cpp_declarator_match(&mut self, line: &str, escaped_receiver: &str) -> Res> { let code = super::awaited::strip_ts_comments(line); let line = code.as_str(); let re = shared_regex(&format!( - r"([A-Za-z_][A-Za-z0-9_:]*(?:\s*<[^;=(){{}}]+>)?(?:\s*[*&]+)?)\s*(?-u:\b){}(?-u:\b)\s*", + r"({})\s*(?-u:\b){}(?-u:\b)(?:\s*{CPP_TRAILING_ANNOTATION})*\s*", + cpp_declarator_type_pattern(), escaped_receiver ))?; for caps in re.captures_iter(line) { @@ -661,7 +666,7 @@ impl KernelResolver { }; if ok { if let Some(m1) = caps.get(1) { - return Ok(Some(m1.as_str().to_string())); + return Ok(Some(cpp_declarator_type(m1.as_str()))); } } } @@ -1207,3 +1212,68 @@ fn go_result_from_node(node: &KNode) -> Option { literal.captures(node.signature.as_deref()?) .map(|hit| hit[1].to_string()) } + +/// cv-qualifiers and `__restrict`, before or after a declarator's `*` or `&`: `Iterator* const +/// iter`, `Slice const& key`. +const CPP_DECLARATOR_CV: &str = r"(?-u:\b)(?:const|volatile|__restrict__|__restrict)(?-u:\b)"; +/// What a pointer or reference declarator may carry after its `*` or `&`: clang's and abseil's +/// nullability annotations, and ALL-CAPS macros like protobuf's `Arena* PROTOBUF_NULLABLE arena`. +const CPP_DECLARATOR_ANNOTATION: &str = + r"(?-u:\b)(?:absl_nonnull|absl_nullable|absl_nullability_unknown|_Nonnull|_Nullable|_Null_unspecified|[A-Z][A-Z0-9]*_[A-Z0-9_]*)(?-u:\b)"; +/// A function wrapper's type, whose template argument is a function type: rocksdb's +/// `std::function create_iterator`. +pub(super) const CPP_FUNCTION_WRAPPER_TYPE: &str = + r"(?:::\s*)?(?:std\s*::\s*(?:function|move_only_function|copyable_function)|absl\s*::\s*(?:FunctionRef|AnyInvocable))\s*<[^;={}]*>"; +/// An annotation macro's call after a declared name: leveldb's `VersionSet* const versions_ +/// GUARDED_BY(mutex_);`. +const CPP_TRAILING_ANNOTATION: &str = r"(?-u:\b)[A-Z][A-Z0-9]*_[A-Z0-9_]*(?:\s*\([^()]*\))?"; + +/// A declared type as the declarator regex reads it, what it carries included: `const +/// FieldDescriptor* PROTOBUF_NULLABLE*`. +pub(super) fn cpp_declarator_type_pattern() -> &'static str { + static PATTERN: LazyLock = LazyLock::new(|| format!( + r"(?:{CPP_FUNCTION_WRAPPER_TYPE}|[A-Za-z_][A-Za-z0-9_:]*(?:\s*<[^;=(){{}}]+>)?)(?:\s*(?:{CPP_DECLARATOR_CV}|[*&]+(?:\s*{CPP_DECLARATOR_ANNOTATION})*))*" + )); + &PATTERN +} + +/// A type the declarator regex read, without the annotations it reads past after a `*` or `&`, +/// outside template arguments: `Arena* PROTOBUF_NULLABLE` is `Arena*` (cppDeclaratorType). An +/// ALL-CAPS word is an annotation only there, so a type's own name stays (`OS_HANDLE* const h`), +/// and so do its cv-qualifiers. A type with nothing to drop comes back as it is. +pub(super) fn cpp_declarator_type(text: &str) -> String { + let annotation = re!(r"^(?:absl_nonnull|absl_nullable|absl_nullability_unknown|_Nonnull|_Nullable|_Null_unspecified|[A-Z][A-Z0-9]*_[A-Z0-9_]*)$"); + let mut out = String::with_capacity(text.len()); + let mut depth = 0i32; + let mut after_pointer = false; + let mut dropped = false; + let mut prev: Option = None; + let mut rest = text; + while let Some(c) = rest.chars().next() { + let starts_word = (c.is_ascii_alphabetic() || c == '_') && !prev.is_some_and(|p| p.is_alphanumeric() || p == '_' || p == ':'); + if depth == 0 && starts_word { + let len = rest.find(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_')).unwrap_or(rest.len()); + let word = &rest[..len]; + if after_pointer && annotation.is_match(word) { + dropped = true; + } else { + out.push_str(word); + after_pointer = false; + } + prev = word.chars().last(); + rest = &rest[len..]; + continue; + } + match c { + '<' => depth += 1, + '>' => depth -= 1, + '*' | '&' if depth == 0 => after_pointer = true, + _ if depth == 0 && !c.is_whitespace() => after_pointer = false, + _ => (), + } + out.push(c); + prev = Some(c); + rest = &rest[c.len_utf8()..]; + } + if dropped { out.split_whitespace().collect::>().join(" ") } else { text.to_string() } +} diff --git a/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts index ef42e93d13..8b33fc464c 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -193,6 +193,31 @@ function recoverCppMacroDefinedName(node: SyntaxNode, source: string): string | return name; } +const CPP_CLASS_SPECIFIERS: ReadonlySet = new Set(['class_specifier', 'struct_specifier', 'union_specifier']); + +/** + * The `::` segments of a C++ class, struct or union name written with a + * scope, the scope's template arguments dropped: `['SkipList', 'Node']` for + * an out-of-line nested class `struct SkipList::Node`. Such + * a class was indexed ``, so its members were no class's. + * Undefined for a plain name, and for a specialization (`class + * FunctionMocker`, `struct fmt::formatter`): named after its + * template, it would stand in for the template and every other + * specialization of it (fmt's `formatter : formatter` extended `formatter`). Mirrored in the kernel + * (ccpp/mod.rs). + */ +function cppWrittenClassParts(node: SyntaxNode, source: string): string[] | undefined { + if (!CPP_CLASS_SPECIFIERS.has(node.type)) return undefined; + const written = getChildByField(node, 'name'); + if (written?.type !== 'qualified_identifier') return undefined; + let innermost: SyntaxNode | null = written; + while (innermost?.type === 'qualified_identifier') innermost = getChildByField(innermost, 'name'); + if (innermost?.type !== 'type_identifier') return undefined; + const parts = stripCppTemplateArgs(getNodeText(written, source)).split('::').map((p) => p.trim()).filter(Boolean); + return parts.length > 0 && parts.every((p) => /^[A-Za-z_]\w*$/.test(p)) ? parts : undefined; +} + function extractCppQualifiedMethodName(node: SyntaxNode, source: string): string | undefined { const macroDefined = recoverSingleArgMacroDefinedName(node, source) ?? recoverCppMacroDefinedName(node, source); if (macroDefined) return macroDefined; @@ -2398,9 +2423,10 @@ export const cppExtractor: LanguageExtractor = { bodyField: 'body', paramsField: 'parameters', resolveName: (node, source) => - extractCppQualifiedMethodName(node, source) ?? unnamedTypeVariableName(node, source), + extractCppQualifiedMethodName(node, source) ?? unnamedTypeVariableName(node, source) ?? cppWrittenClassParts(node, source)?.at(-1), getDeclarationWrapper: (node) => definingDeclaration(node) ?? undefined, getReceiverType: extractCppReceiverType, + getClassScope: (node, source) => cppWrittenClassParts(node, source)?.slice(0, -1).join('::') || undefined, getReturnType: extractCppReturnType, // Constructors (definitions and class-body declarations) carry their // parameter list as the signature; a trailing semicolon marks a prototype, so a local `T obj(args)` can be matched diff --git a/src/extraction/tree-sitter-types.ts b/src/extraction/tree-sitter-types.ts index 988c8a9451..72d5cc7310 100644 --- a/src/extraction/tree-sitter-types.ts +++ b/src/extraction/tree-sitter-types.ts @@ -314,6 +314,14 @@ export interface LanguageExtractor { */ getReceiverType?: (node: SyntaxNode, source: string) => string | undefined; + /** + * The scope a class, struct or union declaration writes its name in, when + * it does: C++'s `SkipList` for an out-of-line nested class `struct + * SkipList::Node`. The class is qualified under it the way + * an out-of-line method is under its receiver, and so are its members. + */ + getClassScope?: (node: SyntaxNode, source: string) => string | undefined; + /** * Extract a function/method's normalized return type name (bare class name, * smart-pointer pointee unwrapped), stored on the node as `returnType`. Used diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 818f26a078..cd032463f6 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -1844,6 +1844,18 @@ export class TreeSitterExtractor { return prefix.length > 0 ? `${prefix.join('::')}::${base}` : base; } + /** + * The qualified name of a class, struct or union whose declaration writes + * its name in a scope (getClassScope): C++'s out-of-line nested `struct + * SkipList::Node` is `leveldb::SkipList::Node`, composed + * like an out-of-line method's. Its members are qualified under it + * (objectPathOwners). Undefined for any other. + */ + private writtenScopeQualifiedName(node: SyntaxNode, name: string): string | undefined { + const scope = this.extractor?.getClassScope?.(node, this.source); + return scope ? this.composeReceiverQualifiedName(scope, name) : undefined; + } + /** * Build qualified name from node stack */ @@ -2262,14 +2274,17 @@ export class TreeSitterExtractor { const docstring = this.docstringFor(node); const visibility = this.extractor.getVisibility?.(node); const isExported = this.extractor.isExported?.(node, this.source); + const scoped = this.writtenScopeQualifiedName(node, name); const classNode = this.createNode(kind, name, node, { docstring, visibility, isExported, ...this.cppBodyEnd(resolvedBody), + ...(scoped !== undefined ? { qualifiedName: scoped } : {}), }); if (!classNode) return; + if (scoped !== undefined) this.objectPathOwners.set(classNode.id, scoped); this.openCppClassScope(classNode, resolvedBody); // Extract extends/implements @@ -2478,14 +2493,17 @@ export class TreeSitterExtractor { const docstring = this.docstringFor(node); const visibility = this.extractor.getVisibility?.(node); const isExported = this.extractor.isExported?.(node, this.source); + const scoped = this.writtenScopeQualifiedName(node, name); const aggregateNode = this.createNode(kind, name, node, { docstring, visibility, isExported, ...this.cppBodyEnd(body), + ...(scoped !== undefined ? { qualifiedName: scoped } : {}), }); if (!aggregateNode) return; + if (scoped !== undefined) this.objectPathOwners.set(aggregateNode.id, scoped); this.openCppClassScope(aggregateNode, body); // Extract inheritance (e.g. Swift: struct HTTPMethod: RawRepresentable)