diff --git a/CHANGELOG.md b/CHANGELOG.md index 1af3e88544..58f4f3bf7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a call on a variable, parameter or member declared as a standard-library or other outside type with a lowercase name, like `std::string`, `std::vector`, `std::set` or `absl::flat_hash_set`, no longer links to a project method that only shares the method's name. In google/leveldb, `saved_key_.clear()` on a `std::string saved_key_;` reached `Slice::clear`, and in protocolbuffers/protobuf, `proto.append(…)` on a `std::string` reached an unrelated `LeftoverBuffer::append`. The same goes for `->` through a raw pointer like `std::string* out`, while `->` through a smart pointer, iterator or optional still links to the type it holds. Comments also no longer pass for a variable's declaration, so more calls now link to the method of the type the variable is really declared with. Re-index C++ projects after upgrading. - In C++, a class written with attribute macros the parser can't read now keeps its members. That covers several macros between `class` and the class name, like protocolbuffers/protobuf's generated `class PROTOBUF_EXPORT PROTOBUF_FUTURE_ADD_EARLY_WARN_UNUSED Any final`, a macro with arguments there, like RocksDB's `struct ALIGN_AS(64U) HandleImpl`, or one before a template specialization, and macros on a member: in front of it (`PROTOBUF_FUTURE_ADD_EARLY_NODISCARD`), between a pointer and its name (`PROTOBUF_NONNULL`), after its parameters or its name (`ABSL_ATTRIBUTE_LIFETIME_BOUND`, google/leveldb's `LOCKS_EXCLUDED(mu_)` and `GUARDED_BY(mu_)`), or alone on a line above a comment ({fmt}'s `FMT_BEGIN_EXPORT`). Before, protobuf's generated message classes were missing and their members, like `GetDescriptor`, `GetReflection` and `unknown_fields`, were indexed as functions outside any class, and leveldb's annotated methods were indexed under the annotation's name, so calls to them linked to nothing. Re-index C++ projects after upgrading. - In C++, a call on a range-based `for` loop's variable, like `suite->SetVerbose(verbose)` inside `for (ConformanceTestSuite *suite : suites)`, now links to the method of the type the loop declares. Before, the loop's declaration was never read, so the call was matched by the variable's name alone: in RocksDB, `log->file()` on a `log::Writer` went to `BlobLogWriter::file`, and a loop variable named like a member of its class was taken for that member. The variable counts only inside its loop, and loops over `auto` elements resolve as before. Re-index C++ projects after upgrading. +- In C++, a call made with `->` on a variable, parameter or member declared as a `std::unique_ptr`, `std::shared_ptr` or `std::optional`, like `iter->Valid()` on a `std::unique_ptr iter`, now links to that method of the type it holds, or of a class that type inherits from, with the type looked up the way C++ does it, so a class of the same name nested elsewhere is not mistaken for it. Before, the call went to whichever project method shared its name: in facebook/rocksdb, `iter->Valid()` reached `ArenaWrappedDBIter::Valid` and `fs->GetFileSize(…)` on a `std::shared_ptr` reached a test's stand-in file system, and calls like `it->Next()` were often not linked at all. When what it holds is a template parameter or a standard-library type, like `std::unique_ptr>`, the call no longer links to a project method. Re-index C++ projects after upgrading. - Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading. - In a repository that mixes languages, a web framework's naming conventions now link only code written in that framework's own languages. Before, Express's also linked Go and Python code: in etcd a Go method's `*zap.Logger` result type linked to the method itself, and in Harbor a Python `self.jaeger.validate()` call linked to another class's `validate`. Links that cross languages on purpose, like React Native and Expo native modules, Swift and Objective-C, Drupal's `routing.yml`, SvelteKit's `$lib/…` imports in `.ts` files and a Razor page's `@model`, are unchanged. - In C and C++, a call written from the global scope, like `::leveldb::RepairDB(…)` or `::hpb::CreateMessage(…)`, now links to the function of exactly that name, and one through a namespace alias, like protobuf's `::_pbi::…` calls, to the function in the aliased namespace. Before, these calls were dropped unless the repository also held Swift and Objective-C files, and even then a name spelled out in full, like `::google::protobuf::internal::TSanWrite(…)`, linked to nothing, while `::operator delete(…)` linked to one class's own `operator delete`. A global call never links to a function or method of the same name inside a namespace or class, such as a wrapper `open()` that calls the C library's `::open(…)`, and `::std::…` and `::testing::…` calls stay unlinked unless that library's source is part of your project. Re-index C and C++ projects after upgrading. diff --git a/__tests__/cpp-smart-pointer-receiver.test.ts b/__tests__/cpp-smart-pointer-receiver.test.ts new file mode 100644 index 0000000000..842e9755e5 --- /dev/null +++ b/__tests__/cpp-smart-pointer-receiver.test.ts @@ -0,0 +1,450 @@ +/** + * A C++ `->` call on a receiver declared as a standard smart pointer or + * optional — `std::unique_ptr iter; iter->Valid()` — calls a member + * of the type it holds. + * + * facebook/rocksdb has about 14,000 of these calls on a declaration in the + * calling function or its class. Receiver inference read the declared type as + * `unique_ptr`, which has no project methods, so the call fell through to a + * guess by the method's name and the receiver's words: `iter->Valid()` went + * to `ArenaWrappedDBIter::Valid`, `db_copy->Get()` to a test's `Get`. + * + * The held type is looked up as C++ looks a name up where the call is + * written — the scopes around the call, the file's `using`s, aliases + * followed — and the method is taken from that class or the classes it + * derives from. Its last name alone is not enough: rocksdb's `Iterator` is + * also the name of `MemTableRep::Iterator` and other nested classes, while the + * `Iterator` the call means inherits `Valid` from `IteratorBase`. A template + * parameter, a library type and an alias of a type outside the project hold + * no project class, so those calls get no edge; neither does a call the held + * class has no method for (unless a class template's specialization may + * declare it). A held type the project doesn't declare — a generated message + * — leaves the call as it was. + */ +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-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`. */ +function calls(cg: CodeGraph, caller: string): string[] { + return cg + .getCallees(callable(cg, caller).id) + .filter((r) => r.edge.kind === 'calls') + .map((r) => r.node.qualifiedName) + .sort(); +} + +const lines = (...l: string[]): string => [...l, ''].join('\n'); + +/** rocksdb's iterator family, trimmed: the public Iterator, a nested namesake, and an implementation a guess reaches. */ +const ROCKSDB = { + 'include/rocksdb/iterator_base.h': lines( + 'namespace rocksdb {', + 'class IteratorBase {', + ' public:', + ' virtual ~IteratorBase() {}', + ' virtual bool Valid() const = 0;', + ' virtual void Next() = 0;', + '};', + '} // namespace rocksdb', + ), + 'include/rocksdb/iterator.h': lines( + '#include "include/rocksdb/iterator_base.h"', + 'namespace rocksdb {', + 'class Iterator : public IteratorBase {', + ' public:', + ' virtual int value() const = 0;', + '};', + '} // namespace rocksdb', + ), + 'include/rocksdb/memtablerep.h': lines( + 'namespace rocksdb {', + 'class MemTableRep {', + ' public:', + ' class Iterator {', + ' public:', + ' virtual bool Valid() const = 0;', + ' virtual void Next() = 0;', + ' };', + '};', + '} // namespace rocksdb', + ), + 'db/arena_wrapped_db_iter.h': lines( + '#include "include/rocksdb/iterator.h"', + 'namespace rocksdb {', + 'class ArenaWrappedDBIter : public Iterator {', + ' public:', + ' bool Valid() const override { return true; }', + ' void Next() override {}', + ' int value() const override { return 0; }', + '};', + '} // namespace rocksdb', + ), + 'include/rocksdb/db.h': lines( + '#include "include/rocksdb/iterator.h"', + 'namespace rocksdb {', + 'class DB {', + ' public:', + ' virtual int Get(int key) = 0;', + ' virtual Iterator* NewIterator() = 0;', + '};', + 'class DBGetBatch {', + ' public:', + ' int Get(int key) { return key; }', + '};', + '} // namespace rocksdb', + ), +}; + +describe('a `->` call on a standard smart pointer or optional reaches the type it holds', () => { + it('reproduction: `std::unique_ptr iter; iter->Valid()` is the Iterator the scope sees, not a namesake', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/db_impl.cc': lines( + '#include ', + '#include "include/rocksdb/db.h"', + '#include "db/arena_wrapped_db_iter.h"', + 'namespace rocksdb {', + 'int CountKeys(DB* db) {', + ' std::unique_ptr iter(db->NewIterator());', + ' int n = 0;', + ' for (; iter->Valid(); iter->Next()) n += iter->value();', + ' return n;', + '}', + '} // namespace rocksdb', + ), + }); + try { + // `Valid` and `Next` are IteratorBase's, which Iterator derives from. + expect(calls(cg, 'rocksdb::CountKeys')).toEqual([ + 'rocksdb::DB::NewIterator', + 'rocksdb::Iterator::value', + 'rocksdb::IteratorBase::Next', + 'rocksdb::IteratorBase::Valid', + ]); + } finally { + cg.close(); + } + }); + + it('`std::shared_ptr`, `std::optional`, a parameter, and a member the class declares in its header', async () => { + const cg = await indexed({ + ...ROCKSDB, + // Classes the receivers' names point a guess at. + 'table/table_db.h': lines( + 'namespace rocksdb {', + 'class TableDB {', + ' public:', + ' int Get(int key) { return key; }', + '};', + 'class CuckooTableDB {', + ' public:', + ' int Get(int key) { return key; }', + '};', + '} // namespace rocksdb', + ), + 'db/db_impl.h': lines( + '#include ', + '#include "include/rocksdb/db.h"', + 'namespace rocksdb {', + 'class DBImpl {', + ' public:', + ' int Lookup(int key);', + ' private:', + ' std::shared_ptr table_db_;', + '};', + '} // namespace rocksdb', + ), + 'db/db_impl.cc': lines( + '#include ', + '#include "db/db_impl.h"', + '#include "table/table_db.h"', + 'namespace rocksdb {', + 'int DBImpl::Lookup(int key) {', + ' return table_db_->Get(key);', + '}', + 'int Probe(const std::shared_ptr& cuckoo_db) {', + ' std::optional batch;', + ' return cuckoo_db->Get(1) + batch->Get(2);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::DBImpl::Lookup')).toEqual(['rocksdb::DB::Get']); + expect(calls(cg, 'rocksdb::Probe')).toEqual(['rocksdb::DB::Get', 'rocksdb::DBGetBatch::Get']); + } finally { + cg.close(); + } + }); + + it('a nested class is found first from inside the class that declares it', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'memtable/skiplistrep.cc': lines( + '#include ', + '#include "include/rocksdb/iterator.h"', + '#include "include/rocksdb/memtablerep.h"', + 'namespace rocksdb {', + 'class SkipListRep : public MemTableRep {', + ' public:', + ' bool Check();', + ' private:', + ' std::unique_ptr iter_;', + '};', + 'bool SkipListRep::Check() { return iter_->Valid(); }', + '} // namespace rocksdb', + ), + }); + try { + // MemTableRep's own `Iterator`, which SkipListRep inherits, hides rocksdb::Iterator. + expect(calls(cg, 'rocksdb::SkipListRep::Check')).toEqual(['rocksdb::MemTableRep::Iterator::Valid']); + } finally { + cg.close(); + } + }); + + it('a held type named through an alias, a `using namespace`, and a class local to the function', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'table/internal_iterator.h': lines( + 'namespace rocksdb {', + 'template ', + 'class InternalIteratorBase {', + ' public:', + ' virtual void SeekToFirst() = 0;', + '};', + 'using InternalIterator = InternalIteratorBase;', + 'class MergingIterator : public InternalIterator {', + ' public:', + ' void SeekToFirst() override {}', + '};', + '} // namespace rocksdb', + ), + 'table/scan.cc': lines( + '#include ', + '#include "table/internal_iterator.h"', + 'namespace rocksdb {', + 'void ScanTable(InternalIterator* raw) {', + ' std::unique_ptr it(raw);', + ' it->SeekToFirst();', + '}', + 'class EventListener {', + ' public:', + ' void OnFlushCompleted() {}', + '};', + 'void Notify() {', + ' class FlushListener {', + ' public:', + ' void OnFlushCompleted() {}', + ' };', + ' std::shared_ptr listener;', + ' listener->OnFlushCompleted();', + '}', + '} // namespace rocksdb', + ), + 'tools/dump.cc': lines( + '#include ', + '#include "include/rocksdb/db.h"', + '#include "include/rocksdb/memtablerep.h"', + 'using namespace rocksdb;', + 'int Dump(DB* db) {', + ' std::unique_ptr it(db->NewIterator());', + ' return it->Valid() ? 1 : 0;', + '}', + ), + }); + try { + expect(calls(cg, 'rocksdb::ScanTable')).toEqual(['rocksdb::InternalIteratorBase::SeekToFirst']); + expect(calls(cg, 'rocksdb::Notify')).toEqual(['rocksdb::Notify::FlushListener::OnFlushCompleted']); + expect(calls(cg, 'Dump')).toEqual(['rocksdb::DB::NewIterator', 'rocksdb::IteratorBase::Valid']); + } finally { + cg.close(); + } + }); +}); + +describe('a held type that is no class of the project\'s gets no guess', () => { + it('a template parameter, a library type, and an alias of one', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'table/block.h': lines( + 'namespace rocksdb {', + 'class Block {', + ' public:', + ' unsigned size() const { return 0; }', + ' int front() const { return 0; }', + '};', + '} // namespace rocksdb', + ), + 'db_stress_tool/stress.cc': lines( + '#include ', + '#include ', + '#include ', + '#include "db/arena_wrapped_db_iter.h"', + '#include "table/block.h"', + 'namespace rocksdb {', + 'template ', + 'void TestIterate(IterType* raw) {', + ' std::unique_ptr iter(raw);', + ' iter->Next();', + '}', + 'using BlockList = std::vector;', + 'class BlockBasedTableIterator {', + ' public:', + ' int First();', + ' private:', + ' std::unique_ptr> block_handles_;', + ' std::shared_ptr block_list_;', + '};', + 'int BlockBasedTableIterator::First() {', + ' return block_handles_->front() + static_cast(block_list_->size());', + '}', + '} // namespace rocksdb', + ), + }); + try { + // Not ArenaWrappedDBIter::Next, Block::front or Block::size. + expect(calls(cg, 'rocksdb::TestIterate')).toEqual([]); + expect(calls(cg, 'rocksdb::BlockBasedTableIterator::First')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a class that has no such method, nor does any class it derives from', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/compaction.cc': lines( + '#include ', + '#include "include/rocksdb/db.h"', + '#include "db/arena_wrapped_db_iter.h"', + 'namespace rocksdb {', + 'class CompactionIterator {', + ' public:', + ' void Release() {}', + '};', + 'void Compact(DB* raw) {', + ' std::unique_ptr db(raw);', + ' db->Release();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Compact')).toEqual([]); + } finally { + cg.close(); + } + }); +}); + +describe('what keeps the call as it was', () => { + it('a type the project does not declare, such as a generated message, and a class template\'s specialization', async () => { + // protocolbuffers/protobuf's tests hold generated messages, which derive + // from Message; facebook/rocksdb's order-maintenance tree has members only + // a specialization declares. + const cg = await indexed({ + 'src/google/protobuf/message.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'class Message {', + ' public:', + ' unsigned SpaceUsedLong() const { return 0; }', + '};', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/extension_set_unittest.cc': lines( + '#include ', + '#include "src/google/protobuf/message.h"', + 'namespace google {', + 'namespace protobuf {', + 'unsigned SpaceUsed() {', + ' std::unique_ptr message;', + ' return message->SpaceUsedLong();', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + 'util/omt.h': lines( + '#include ', + 'namespace toku {', + 'template ', + 'class omt_node {', + ' public:', + ' void clear_stolen_bits() {}', + '};', + 'template ', + 'class omt_node {', + ' public:', + ' bool get_marked() const { return false; }', + '};', + 'bool HasMarks() {', + ' std::unique_ptr> node;', + ' return node->get_marked();', + '}', + '} // namespace toku', + ), + }); + try { + expect(calls(cg, 'google::protobuf::SpaceUsed')).toEqual(['google::protobuf::Message::SpaceUsedLong']); + expect(cg.getCallees(callable(cg, 'toku::HasMarks').id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.name)).toEqual(['get_marked']); + } finally { + cg.close(); + } + }); + + it('`->` through a raw pointer to a smart pointer is the smart pointer\'s own member', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'cache/cache.h': lines( + 'namespace rocksdb {', + 'class Cache {', + ' public:', + ' void reset() {}', + '};', + '} // namespace rocksdb', + ), + 'db/version_set.cc': lines( + '#include ', + '#include "include/rocksdb/iterator.h"', + '#include "cache/cache.h"', + 'namespace rocksdb {', + 'void Drop(std::unique_ptr* iter_cache) {', + ' iter_cache->reset();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Drop')).toEqual([]); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/cpp-namespaces.ts b/src/resolution/cpp-namespaces.ts new file mode 100644 index 0000000000..79c4d9ca68 --- /dev/null +++ b/src/resolution/cpp-namespaces.ts @@ -0,0 +1,113 @@ +/** + * C and C++ namespaces the source spells in ways the index cannot see: a + * namespace alias (`namespace py = pybind11;`) and a macro that opens a + * namespace (`FMT_BEGIN_NAMESPACE`). Shared by name matching and the C++ + * class lookup (cpp-supertypes.ts). + */ +import type { ResolutionContext } from './types'; +import { stripCommentsForRegex } from './strip-comments'; + +const CPP_NS_MACROS = new WeakMap; openerFns: Set; closers: Map; aliases: Map }>(); +const CPP_NS_FRAMES = new WeakMap>>(); +/** A closing macro's body: `}` / `} }`, maybe beside a pragma macro (`PYBIND11_WARNING_POP }`). */ +const CPP_CLOSER_BODY = /^(?:[A-Za-z_]\w*\s+)*\}(?:\s*\})*\s*;?$/; +const CPP_NS_ALIASES = new WeakMap>(); + +/** The project's namespace aliases: `namespace py = pybind11;`. */ +export function cppNamespaceAliases(context: ResolutionContext): Map { + const hit = CPP_NS_ALIASES.get(context); + if (hit) return hit; + const aliases = new Map(); + for (const file of context.getAllFiles()) { + if (!/\.(?:h|hh|hpp|hxx|inl|c|cc|cpp|cxx)$/i.test(file)) continue; + const source = context.readFile(file); + if (!source || !source.includes('namespace')) continue; + for (const m of source.matchAll(/^[ \t]*namespace[ \t]+([A-Za-z_]\w*)[ \t]*=[ \t]*(?:::)?([A-Za-z_][\w:]*)[ \t]*;/gm)) { + if (!aliases.has(m[1]!)) aliases.set(m[1]!, m[2]!); + } + } + CPP_NS_ALIASES.set(context, aliases); + return aliases; +} + +/** + * The project's namespace-opening macros — `#define FMT_BEGIN_NAMESPACE + * namespace fmt { inline namespace v12 {`, `#define RAPIDJSON_NAMESPACE_BEGIN + * namespace RAPIDJSON_NAMESPACE {` (through `#define RAPIDJSON_NAMESPACE + * rapidjson`) — as the namespace path each opens (inline namespaces are + * transparent), and the closing macros as how many scopes each closes. + */ +function cppNamespaceMacros(context: ResolutionContext): { openers: Map; openerFns: Set; closers: Map; aliases: Map } { + const hit = CPP_NS_MACROS.get(context); + if (hit) return hit; + const openers = new Map(); + // `#define PYBIND11_NAMESPACE_BEGIN(name) namespace name {`, used as `PYBIND11_NAMESPACE_BEGIN(detail)`. + const openerFns = new Set(); + const closers = new Map(); + const aliases = new Map(); + const bodies: Array<[string, string]> = []; + for (const file of context.getAllFiles()) { + if (!/\.(?:h|hh|hpp|hxx|h\+\+|inl|ipp|tcc)$/i.test(file)) continue; + const raw = context.readFile(file); + if (!raw || !raw.includes('#') || !raw.includes('define')) continue; + const source = stripCommentsForRegex(raw.replace(/\\\r?\n/g, ' '), 'cpp'); + for (const m of source.matchAll(/^[ \t]*#[ \t]*define[ \t]+([A-Za-z_]\w*)(\(\s*([A-Za-z_]\w*)?\s*\))?[ \t]+([^\n]*)$/gm)) { + const body = m[4]!.trim(); + if (m[2] !== undefined) { + // (a trailing pragma macro — `PYBIND11_WARNING_PUSH` — rides along) + if (m[3] && new RegExp(`^namespace\\s+${m[3]}\\s*\\{[\\w\\s]*$`).test(body)) openerFns.add(m[1]!); + else if (CPP_CLOSER_BODY.test(body)) closers.set(m[1]!, (body.match(/\}/g) ?? []).length); + continue; + } + if (/^[A-Za-z_]\w*$/.test(body)) aliases.set(m[1]!, body); + else if (CPP_CLOSER_BODY.test(body)) closers.set(m[1]!, (body.match(/\}/g) ?? []).length); + // An inline namespace (transparent, and often named by a macro call) is skipped. + else if (/^(?:inline\s+namespace\s+[^{}]*\{\s*|namespace\s+[A-Za-z_]\w*\s*\{\s*)+[\w\s]*$/.test(body)) bodies.push([m[1]!, body]); + } + } + for (const [name, body] of bodies) { + if (openers.has(name)) continue; + const path = [...body.replace(/inline\s+namespace\s+[^{}]*\{/g, '').matchAll(/namespace\s+([A-Za-z_]\w*)/g)] + .map((m) => aliases.get(m[1]!) ?? m[1]!); + if (path.length > 0) openers.set(name, path); + } + const macros = { openers, openerFns, closers, aliases }; + CPP_NS_MACROS.set(context, macros); + return macros; +} + +/** The line ranges of a C / C++ file each namespace macro opens, with the namespace path it opens. */ +export function cppMacroNamespaceFrames(file: string, context: ResolutionContext): Array<{ start: number; end: number; path: string[] }> { + let memo = CPP_NS_FRAMES.get(context); + if (!memo) { + memo = new Map(); + CPP_NS_FRAMES.set(context, memo); + } + const hit = memo.get(file); + if (hit) return hit; + const frames: Array<{ start: number; end: number; path: string[] }> = []; + const { openers, openerFns, closers, aliases } = cppNamespaceMacros(context); + if (openers.size > 0 || openerFns.size > 0) { + const lines = context.getFileLines?.(file) ?? context.readFile(file)?.split(/\r?\n/) ?? []; + const open: Array<{ start: number; path: string[] }> = []; + lines.forEach((text, i) => { + const m = /^[ \t]*([A-Z_][A-Z0-9_]*)(?:\(\s*([A-Za-z_]\w*)?\s*\))?[ \t]*;?[ \t]*(?:\/\/.*|\/\*.*\*\/[ \t]*)?\r?$/.exec(text); + const token = m?.[1]; + if (!token) return; + const arg = m[2]; + const path = arg !== undefined && openerFns.has(token) ? [aliases.get(arg) ?? arg] : arg === undefined ? openers.get(token) : undefined; + if (path) open.push({ start: i + 1, path }); + else if (closers.has(token) && open.length > 0) frames.push({ ...open.pop()!, end: i + 1 }); + }); + for (const frame of open) frames.push({ ...frame, end: lines.length }); + } + memo.set(file, frames); + return frames; +} + +/** Drop the per-context namespace memos (see ReferenceResolver.clearCaches). */ +export function clearCppNamespaceMemos(context: ResolutionContext): void { + CPP_NS_MACROS.delete(context); + CPP_NS_FRAMES.delete(context); + CPP_NS_ALIASES.delete(context); +} diff --git a/src/resolution/cpp-supertypes.ts b/src/resolution/cpp-supertypes.ts index 7ac3c74e44..f7d895c4a9 100644 --- a/src/resolution/cpp-supertypes.ts +++ b/src/resolution/cpp-supertypes.ts @@ -36,6 +36,10 @@ * * Only C and C++ declarations are candidates, and no other strategy (a * framework's, the import resolver, name matching) sees these references. + * + * The same lookup (cppClassWritten) finds the class a `std::unique_ptr`, + * `std::shared_ptr` or `std::optional` holds, for a `->` call on it, looked + * up from the calling function (name-matcher's matchCppHeldTypeCall). */ import type { Node } from '../types'; import type { ResolvedRef, ResolutionContext, UnresolvedRef } from './types'; @@ -48,7 +52,7 @@ import { stripCppTemplateArguments, type CppNamedClass, } from './cpp-type-aliases'; -import { cppMacroNamespaceFrames, cppNamespaceAliases } from './name-matcher'; +import { cppMacroNamespaceFrames, cppNamespaceAliases } from './cpp-namespaces'; import { stripCommentsForRegex } from './strip-comments'; const CLASS_KINDS: ReadonlySet = new Set(['class', 'struct', 'union']); @@ -74,30 +78,73 @@ function visibleFrom(file: string): (n: Node) => boolean { export function matchCppSupertype(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { const derived = context.getNodeById?.(ref.fromNodeId); if (!derived || !isCFamily(derived) || !CLASS_KINDS.has(derived.kind)) return null; - const written = writtenBase(ref, context); + const found = cppClassWritten(writtenBase(ref, context), { + scope: cppParentScope(derived.qualifiedName), + file: derived.filePath, + line: ref.line, + parameters: cppTemplateParameters(derived, context), + self: derived, + }, ref, context); + if (!found) return null; + return found.exact + ? { original: ref, targetNodeId: found.node.id, confidence: 0.9, resolvedBy: 'qualified-name' } + : { original: ref, targetNodeId: found.node.id, confidence: 0.7, resolvedBy: 'exact-match' }; +} + +/** Where a C or C++ type name is written, which is where its lookup starts. */ +export interface CppNameSite { + /** + * The scope it is looked up from: a class's enclosing scope for its base + * clause, the class of a method for code in the method. + */ + scope: string; + /** The file that writes it: its translation unit decides which classes it sees, and its `using`s before `line` apply. */ + file: string; + line: number; + /** The template parameters in scope there; a name that starts with one names no particular class. */ + parameters: ReadonlySet; + /** A class the name cannot be: the deriving class, in its own base clause. */ + self?: Node; +} + +/** The class a C or C++ type name names. */ +export interface CppWrittenClass { + /** The class, or an alias of a type that is none of the project's classes. */ + node: Node; + /** Found by C++ name lookup, rather than as the only class of that name the file can see. */ + exact: boolean; +} + +/** + * The class a C or C++ type name written at `site` names, looked up as the + * comment at the top of this file says. Null when the name names no + * particular class (a template parameter, an enum, a member of a type outside + * the project); undefined when nothing the file can see is named so, or + * several classes could be. + */ +export function cppClassWritten(written: string, site: CppNameSite, ref: UnresolvedRef, context: ResolutionContext): CppWrittenClass | null | undefined { const segments = cppTypeSegments(written); if (!segments) return null; const global = written.startsWith('::'); - if (!global && cppTemplateParameters(derived, context).has(segments[0]!)) return null; + if (!global && site.parameters.has(segments[0]!)) return null; - const visible = visibleFrom(derived.filePath); - const scopes = cppScopesWithin(cppParentScope(derived.qualifiedName)); - const resolved = (target: Node | null | undefined): ResolvedRef | null => - target ? { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'qualified-name' } : null; + const visible = visibleFrom(site.file); + const scopes = cppScopesWithin(site.scope); + const resolved = (node: Node): CppWrittenClass => ({ node, exact: true }); - // The scopes around the class, or only the global one for `::Base`. - const lexical = classNode(cppClassNamed(written, global ? [''] : scopes, ref, context, visible), derived, context); - if (lexical !== undefined) return resolved(lexical); + // The scopes around the name, or only the global one for `::Base`. + const lexical = classNode(cppClassNamed(written, global ? [''] : scopes, ref, context, visible), site, context); + if (lexical !== undefined) return lexical && resolved(lexical); - // What the file writes before the class, the nearest first: each stands for + // What the file writes before the name, the nearest first: each stands for // the name's first segment, and the rest stays as written. const path = segments.join('::'); const first = /^\s*(?:::\s*)?[A-Za-z_]\w*/.exec(written)?.[0] ?? ''; const after = written.slice(first.length); - const usings = usingsIn(derived.filePath, context).filter((u) => u.line < ref.line).reverse(); + const usings = usingsIn(site.file, context).filter((u) => u.line < site.line).reverse(); const lookUp = (spelled: string): Node | undefined => { const absolute = spelled.startsWith('::'); - return classNode(cppClassNamed(spelled, absolute ? [''] : scopes, ref, context, visible), derived, context) ?? undefined; + return classNode(cppClassNamed(spelled, absolute ? [''] : scopes, ref, context, visible), site, context) ?? undefined; }; // `::_pbi::MessageGlobalsBase` under `namespace _pbi = ::google::protobuf::internal;`. if (segments.length > 1) { @@ -122,16 +169,16 @@ export function matchCppSupertype(ref: UnresolvedRef, context: ResolutionContext } // `fmt::detail::buffer`, where a macro opens `namespace fmt` around `detail::buffer`. if (segments.length > 1) { - const hit = pick(namedClasses(segments, derived, context, visible) - .filter((n) => macroQualifiedName(n, context) === path), derived); + const hit = pick(namedClasses(segments, site, context, visible) + .filter((n) => macroQualifiedName(n, context) === path), site.file); if (hit) return resolved(hit); } // The only class of that name the file can see, by its qualified tail. - const tail = namedClasses(segments, derived, context, visible) + const tail = namedClasses(segments, site, context, visible) .filter((n) => n.qualifiedName === path || n.qualifiedName.endsWith(`::${path}`)); - const own = tail.filter((n) => n.filePath === derived.filePath); + const own = tail.filter((n) => n.filePath === site.file); const sole = own.length > 0 ? own : tail; - return sole.length === 1 ? { original: ref, targetNodeId: sole[0]!.id, confidence: 0.7, resolvedBy: 'exact-match' } : null; + return sole.length === 1 ? { node: sole[0]!, exact: false } : undefined; } /** @@ -168,36 +215,36 @@ function writtenBase(ref: UnresolvedRef, context: ResolutionContext): string { } /** - * The node of the class a lookup found, as the deriving class's file sees it - * (null: the name names no particular class; undefined: keep looking). + * The node of the class a lookup found, as the file that writes the name sees + * it (null: the name names no particular class; undefined: keep looking). */ -function classNode(named: CppNamedClass, derived: Node, context: ResolutionContext): Node | null | undefined { +function classNode(named: CppNamedClass, site: CppNameSite, context: ResolutionContext): Node | null | undefined { if (named === null || named === undefined) return named; if ('alias' in named) return named.alias; const classes = context .getNodesByQualifiedName(named.cls) - .filter((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && n.id !== derived.id); + .filter((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && n.id !== site.self?.id); // A lookup that ended on the class itself (its namespace lost) found nothing. if (classes.length === 0) return undefined; - const visible = classes.filter(visibleFrom(derived.filePath)); - return pick(visible.length > 0 ? visible : classes, derived); + const visible = classes.filter(visibleFrom(site.file)); + return pick(visible.length > 0 ? visible : classes, site.file); } /** * One of several declarations of a class (separate translation units, - * vendored copies): the deriving class's own file's, then its header's, then - * the nearest by path. + * vendored copies): the one in `file` itself, then in its header, then the + * nearest by path. */ -function pick(classes: Node[], derived: Node): Node | undefined { +function pick(classes: Node[], file: string): Node | undefined { if (classes.length <= 1) return classes[0]; - const own = classes.find((n) => n.filePath === derived.filePath); + const own = classes.find((n) => n.filePath === file); if (own) return own; const stem = (p: string): string => p.replace(/\.[^./\\]+$/, ''); - const header = classes.find((n) => stem(n.filePath) === stem(derived.filePath)); + const header = classes.find((n) => stem(n.filePath) === stem(file)); if (header) return header; const shared = (p: string): number => { const a = p.split('/'); - const b = derived.filePath.split('/'); + const b = file.split('/'); let i = 0; while (i < a.length - 1 && i < b.length - 1 && a[i] === b[i]) i++; return i; @@ -205,11 +252,11 @@ function pick(classes: Node[], derived: Node): Node | undefined { return classes.reduce((best, n) => (shared(n.filePath) > shared(best.filePath) ? n : best)); } -/** C and C++ classes named like the last segment of `segments` that `derived`'s file can see. */ -function namedClasses(segments: readonly string[], derived: Node, context: ResolutionContext, visible: (n: Node) => boolean): Node[] { +/** C and C++ classes named like the last segment of `segments` that the file at `site` can see. */ +function namedClasses(segments: readonly string[], site: CppNameSite, context: ResolutionContext, visible: (n: Node) => boolean): Node[] { return context .getNodesByName(segments[segments.length - 1]!) - .filter((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && n.id !== derived.id && visible(n)); + .filter((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && n.id !== site.self?.id && visible(n)); } /** A declaration's qualified name with the namespaces macros open around it: `fmt::detail::buffer` for `detail::buffer`. */ diff --git a/src/resolution/cpp-type-aliases.ts b/src/resolution/cpp-type-aliases.ts index 2a4cbf33eb..040d6e9794 100644 --- a/src/resolution/cpp-type-aliases.ts +++ b/src/resolution/cpp-type-aliases.ts @@ -124,6 +124,23 @@ export function cppTypeSegments(raw: string): string[] | null { return cppTypeName(raw)?.names ?? null; } +/** + * The top-level arguments of the first template argument list in a written + * type: `['Iterator']` for `const std::unique_ptr&`, `['Foo', + * 'Deleter']` for `std::unique_ptr>`. Null when it has + * none, or the list is not closed. + */ +export function cppTemplateArguments(raw: string): string[] | null { + const open = raw.indexOf('<'); + if (open < 0) return null; + let depth = 0; + for (let i = open; i < raw.length; i++) { + if (raw[i] === '<') depth++; + else if (raw[i] === '>' && --depth === 0) return splitTopLevel(raw.slice(open + 1, i)).map((a) => a.trim()); + } + return null; +} + /** C and C++ declare the names a C++ file can see (a header may be either). */ function isCFamily(n: Node): boolean { return n.language === 'cpp' || n.language === 'c'; diff --git a/src/resolution/index.ts b/src/resolution/index.ts index ccec8c630d..3003fc660a 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -291,6 +291,8 @@ export class ReferenceResolver { // 565s of combined worker time (~581µs each, recursion-multiplied). private supertypeGen = 0; private supertypeMemo = new Map(); + // getSupertypeNodes' memo, by node id, under the same generation tag. + private supertypeNodeMemo = new Map(); /** Invalidate the getSupertypes memo — call when resolved edges may have advanced. */ private advanceSupertypeGeneration(): void { @@ -298,6 +300,7 @@ export class ReferenceResolver { // Lazy invalidation via the gen tag; bound the map so a long run over many // batches doesn't accrete dead entries. if (this.supertypeMemo.size > 50_000) this.supertypeMemo.clear(); + if (this.supertypeNodeMemo.size > 50_000) this.supertypeNodeMemo.clear(); } // Node kinds are a small fixed set (~24), so this is a plain Map, not an LRU. // getNodesByKind returns the FULL node list for a kind; it was previously @@ -561,6 +564,7 @@ export class ReferenceResolver { this.methodMatchCache.clear(); this.methodOwnerIndexCache.clear(); this.supertypeMemo.clear(); + this.supertypeNodeMemo.clear(); this.supertypeGen++; this.nodesByKindCache.clear(); this.fileExistsMemo.clear(); @@ -934,6 +938,18 @@ export class ReferenceResolver { return supers; }, + getSupertypeNodes: (id: string) => { + const hit = this.supertypeNodeMemo.get(id); + if (hit && hit.gen === this.supertypeGen) return hit.nodes; + const nodes: Node[] = []; + for (const edge of this.queries.getOutgoingEdges(id, RESOLUTION_READ_EDGE_KINDS)) { + const target = this.nodeById(edge.target); + if (target) nodes.push(target); + } + this.supertypeNodeMemo.set(id, { gen: this.supertypeGen, nodes }); + return nodes; + }, + getImportMappings: (filePath: string, language) => { const cacheKey = filePath; const cached = this.importMappingCache.get(cacheKey); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index c25714a7f4..8ede67751b 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -14,7 +14,9 @@ import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-vis import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, inSameDartLibrary } from './dart-libraries'; import { isDartLocallyBound } from './dart-local-scope'; import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers'; -import { cppAliasedTypeName, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments } from './cpp-type-aliases'; +import { cppAliasedTypeName, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments } from './cpp-type-aliases'; +import { clearCppNamespaceMemos, cppMacroNamespaceFrames, cppNamespaceAliases } from './cpp-namespaces'; +import { cppClassWritten } from './cpp-supertypes'; import { cppIncludedFile, cppIncluders } from './cpp-includers'; import { isTestPath } from '../search/query-utils'; import { isMinifiedContent } from '../extraction/generated-detection'; @@ -9135,6 +9137,84 @@ function isCppLibraryType(raw: string, context: ResolutionContext): boolean { return !declared.some((n) => n.qualifiedName === spelled || n.qualifiedName.endsWith(`::${spelled}`)); } +/** Standard-library types whose `->` reaches the value they hold. */ +const CPP_STD_HOLDERS: ReadonlySet = new Set(['unique_ptr', 'shared_ptr', 'optional']); + +/** + * A `->` call on a C++ receiver declared as a standard smart pointer or + * optional (`std::unique_ptr iter; iter->Valid()`) calls a member of + * the type it holds. That type is looked up as C++ looks it up where the call + * is written (cpp-supertypes.ts): the scopes around the call, the file's + * `using`s, and aliases followed. Looking it up by its last name instead + * finds namesakes: 21 other classes in rocksdb are named `Iterator`, nested + * ones like `MemTableRep::Iterator`, while the `Iterator` the call means + * inherits `Valid` from `IteratorBase`. + * - The member of the class it names, or of a class that class derives from. + * - null when no project method is called: the type is a template + * parameter's, a library type (`std::unique_ptr>`) or an + * alias of one, or a class that lacks the method (but a class template's + * specialization may declare it, so not there). + * - undefined when `raw` declares no such holder, or the type it holds is + * none the project declares (a generated message, a class the index + * doesn't name): the call goes on as before. + */ +function matchCppHeldTypeCall(raw: string, methodName: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined { + const holder = cppTypeSegments(raw); + if (holder?.length !== 2 || holder[0] !== 'std' || !CPP_STD_HOLDERS.has(holder[1]!) || !isCppLibraryType(raw, context)) return undefined; + const held = cppTemplateArguments(raw)?.[0]; + const caller = context.getNodeById?.(ref.fromNodeId); + if (!held || !caller || isCppPointerType(held)) return undefined; + if (isCppLibraryType(held, context)) return null; + // A template parameter's type (`std::unique_ptr`, `typename T::Iter`) names no particular class: null. + const found = cppClassWritten(held, { + // A class local to the calling function is found first. + scope: caller.kind === 'file' ? '' : caller.qualifiedName, + file: ref.filePath, + line: ref.line, + parameters: cppTemplateParameters(caller, context), + }, ref, context); + if (found === undefined) return undefined; + if (found === null) return null; + if (!CPP_CLASS_KINDS.has(found.node.kind)) { + // An alias that leads to none of the project's classes. When it names a + // library type (`using VectorWalPtr = std::vector<…>;`) or a template + // parameter's, nothing of the project's is called. Otherwise it may be one + // each `#if` arm declares differently (rocksdb's `port::Thread`, a + // `std::thread` or a `WindowsThread`) or name a class the index keeps + // under another namespace (protobuf's `MapField`): no telling. + const aliased = resolveCppAliasedType(held, ref, context); + return aliased === null || (aliased && isCppLibraryType(aliased.target.join('::'), context)) ? null : undefined; + } + const method = cppMethodOf(found.node, methodName, ref, context); + if (method) return { original: ref, targetNodeId: method.id, confidence: found.exact ? 0.9 : 0.85, resolvedBy: 'instance-method' }; + return cppTemplateParameters(found.node, context).size > 0 ? undefined : null; +} + +const CPP_CLASS_KINDS: ReadonlySet = new Set(['class', 'struct', 'union']); + +/** + * C++ class `cls`'s method `name`: its own, else the nearest one of a class + * it derives from, through the base edges every declaration of `cls` has. + */ +function cppMethodOf(cls: Node, name: string, ref: UnresolvedRef, context: ResolutionContext, depth = 0, seen = new Set()): Node | null { + const want = `${cls.qualifiedName}::${name}`; + const named = context.getMethodMatches?.(cls.name, name, ref.language) ?? + context.getNodesByName(name).filter((n) => n.kind === 'method' && sameLanguageFamily(n.language, ref.language)); + const own = named.filter((n) => n.qualifiedName === want); + if (own.length > 0) return preferCallSiteFile(own, ref.filePath)[0]!; + seen.add(cls.qualifiedName); + if (depth >= 4 || !context.getSupertypeNodes) return null; + for (const decl of context.getNodesByQualifiedName(cls.qualifiedName)) { + if ((decl.language !== 'cpp' && decl.language !== 'c') || !CPP_CLASS_KINDS.has(decl.kind)) continue; + for (const base of context.getSupertypeNodes(decl.id)) { + if (!CPP_CLASS_KINDS.has(base.kind) || seen.has(base.qualifiedName)) continue; + const inherited = cppMethodOf(base, name, ref, context, depth + 1, seen); + if (inherited) return inherited; + } + } + return null; +} + /** * A C++ source line as code, for reading declarations: null for a line of a * comment (`// …`, ` * …`), else the line with its comments blanked, columns @@ -9776,9 +9856,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { KOTLIN_RECEIVER_TYPES.delete(context); KOTLIN_HIERARCHIES.delete(context); KOTLIN_FRAMES.delete(context); - CPP_NS_MACROS.delete(context); - CPP_NS_FRAMES.delete(context); - CPP_NS_ALIASES.delete(context); + clearCppNamespaceMemos(context); CPP_CLASSES_IN_FILE.delete(context); SOLIDITY_SUPERS.delete(context); DECLARED_SUPERS.delete(context); @@ -11043,6 +11121,15 @@ export function matchMethodCall( isCppLibraryType(cppDecl.raw!, context)) { return null; } + // A `->` call on a standard smart pointer or optional the calling + // function or its class declares is a call on the type it holds: + // rocksdb's `std::unique_ptr iter; iter->Valid()` reached + // `ArenaWrappedDBIter::Valid` by the method's name. + if (ref.language === 'cpp' && cppDecl.callers && !cppDecl.aliased && !cppDecl.shadowed && !cppDecl.pointer && + cppMemberOperator(objectOrClass!, ref, context) === '->') { + const held = nmTimedT('mc-cppheld', ref, () => matchCppHeldTypeCall(cppDecl.raw!, methodName!, ref, context)); + if (held !== undefined) return held; + } } // Go 2-hop field chain `base.field.Method` (#1276): the base's type comes @@ -13753,104 +13840,6 @@ function erlangImportedModule(name: string, arity: string, ref: UnresolvedRef, c return undefined; } -const CPP_NS_MACROS = new WeakMap; openerFns: Set; closers: Map; aliases: Map }>(); -const CPP_NS_FRAMES = new WeakMap>>(); -/** A closing macro's body: `}` / `} }`, maybe beside a pragma macro (`PYBIND11_WARNING_POP }`). */ -const CPP_CLOSER_BODY = /^(?:[A-Za-z_]\w*\s+)*\}(?:\s*\})*\s*;?$/; -const CPP_NS_ALIASES = new WeakMap>(); - -/** The project's namespace aliases: `namespace py = pybind11;`. */ -export function cppNamespaceAliases(context: ResolutionContext): Map { - const hit = CPP_NS_ALIASES.get(context); - if (hit) return hit; - const aliases = new Map(); - for (const file of context.getAllFiles()) { - if (!/\.(?:h|hh|hpp|hxx|inl|c|cc|cpp|cxx)$/i.test(file)) continue; - const source = context.readFile(file); - if (!source || !source.includes('namespace')) continue; - for (const m of source.matchAll(/^[ \t]*namespace[ \t]+([A-Za-z_]\w*)[ \t]*=[ \t]*(?:::)?([A-Za-z_][\w:]*)[ \t]*;/gm)) { - if (!aliases.has(m[1]!)) aliases.set(m[1]!, m[2]!); - } - } - CPP_NS_ALIASES.set(context, aliases); - return aliases; -} - -/** - * The project's namespace-opening macros — `#define FMT_BEGIN_NAMESPACE - * namespace fmt { inline namespace v12 {`, `#define RAPIDJSON_NAMESPACE_BEGIN - * namespace RAPIDJSON_NAMESPACE {` (through `#define RAPIDJSON_NAMESPACE - * rapidjson`) — as the namespace path each opens (inline namespaces are - * transparent), and the closing macros as how many scopes each closes. - */ -function cppNamespaceMacros(context: ResolutionContext): { openers: Map; openerFns: Set; closers: Map; aliases: Map } { - const hit = CPP_NS_MACROS.get(context); - if (hit) return hit; - const openers = new Map(); - // `#define PYBIND11_NAMESPACE_BEGIN(name) namespace name {`, used as `PYBIND11_NAMESPACE_BEGIN(detail)`. - const openerFns = new Set(); - const closers = new Map(); - const aliases = new Map(); - const bodies: Array<[string, string]> = []; - for (const file of context.getAllFiles()) { - if (!/\.(?:h|hh|hpp|hxx|h\+\+|inl|ipp|tcc)$/i.test(file)) continue; - const raw = context.readFile(file); - if (!raw || !raw.includes('#') || !raw.includes('define')) continue; - const source = stripCommentsForRegex(raw.replace(/\\\r?\n/g, ' '), 'cpp'); - for (const m of source.matchAll(/^[ \t]*#[ \t]*define[ \t]+([A-Za-z_]\w*)(\(\s*([A-Za-z_]\w*)?\s*\))?[ \t]+([^\n]*)$/gm)) { - const body = m[4]!.trim(); - if (m[2] !== undefined) { - // (a trailing pragma macro — `PYBIND11_WARNING_PUSH` — rides along) - if (m[3] && new RegExp(`^namespace\\s+${m[3]}\\s*\\{[\\w\\s]*$`).test(body)) openerFns.add(m[1]!); - else if (CPP_CLOSER_BODY.test(body)) closers.set(m[1]!, (body.match(/\}/g) ?? []).length); - continue; - } - if (/^[A-Za-z_]\w*$/.test(body)) aliases.set(m[1]!, body); - else if (CPP_CLOSER_BODY.test(body)) closers.set(m[1]!, (body.match(/\}/g) ?? []).length); - // An inline namespace (transparent, and often named by a macro call) is skipped. - else if (/^(?:inline\s+namespace\s+[^{}]*\{\s*|namespace\s+[A-Za-z_]\w*\s*\{\s*)+[\w\s]*$/.test(body)) bodies.push([m[1]!, body]); - } - } - for (const [name, body] of bodies) { - if (openers.has(name)) continue; - const path = [...body.replace(/inline\s+namespace\s+[^{}]*\{/g, '').matchAll(/namespace\s+([A-Za-z_]\w*)/g)] - .map((m) => aliases.get(m[1]!) ?? m[1]!); - if (path.length > 0) openers.set(name, path); - } - const macros = { openers, openerFns, closers, aliases }; - CPP_NS_MACROS.set(context, macros); - return macros; -} - -/** The line ranges of a C / C++ file each namespace macro opens, with the namespace path it opens. */ -export function cppMacroNamespaceFrames(file: string, context: ResolutionContext): Array<{ start: number; end: number; path: string[] }> { - let memo = CPP_NS_FRAMES.get(context); - if (!memo) { - memo = new Map(); - CPP_NS_FRAMES.set(context, memo); - } - const hit = memo.get(file); - if (hit) return hit; - const frames: Array<{ start: number; end: number; path: string[] }> = []; - const { openers, openerFns, closers, aliases } = cppNamespaceMacros(context); - if (openers.size > 0 || openerFns.size > 0) { - const lines = context.getFileLines?.(file) ?? context.readFile(file)?.split(/\r?\n/) ?? []; - const open: Array<{ start: number; path: string[] }> = []; - lines.forEach((text, i) => { - const m = /^[ \t]*([A-Z_][A-Z0-9_]*)(?:\(\s*([A-Za-z_]\w*)?\s*\))?[ \t]*;?[ \t]*(?:\/\/.*|\/\*.*\*\/[ \t]*)?\r?$/.exec(text); - const token = m?.[1]; - if (!token) return; - const arg = m[2]; - const path = arg !== undefined && openerFns.has(token) ? [aliases.get(arg) ?? arg] : arg === undefined ? openers.get(token) : undefined; - if (path) open.push({ start: i + 1, path }); - else if (closers.has(token) && open.length > 0) frames.push({ ...open.pop()!, end: i + 1 }); - }); - for (const frame of open) frames.push({ ...frame, end: lines.length }); - } - memo.set(file, frames); - return frames; -} - /** * Resolve `fmt::format` / `fmt::detail::to_unsigned` to the declaration a * namespace macro puts there: a node named `format` (qualified `format` or diff --git a/src/resolution/types.ts b/src/resolution/types.ts index eb17e40e82..d61c9a40b3 100644 --- a/src/resolution/types.ts +++ b/src/resolution/types.ts @@ -156,6 +156,14 @@ export interface ResolutionContext { * method). Optional so external/test contexts compile without it. */ getSupertypes?(typeName: string, language: Language): string[]; + /** + * The nodes one type declaration extends or implements, through its own + * resolved `implements`/`extends` edges: where getSupertypes unions those of + * every type sharing a simple name, this is the supertypes of the node + * `id`. Empty during the first resolution pass, as getSupertypes is. + * Optional so external/test contexts compile without it. + */ + getSupertypeNodes?(id: string): Node[]; /** * Look up a node by its id. Lets matchers derive the FROM-symbol's * enclosing-class scope (Swift implicit-self method scoping, `this.X`