diff --git a/CHANGELOG.md b/CHANGELOG.md index 01aa134aea..4aca5fb6e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, text inside a string literal is no longer taken for a variable's declaration. That covers the C code protocolbuffers/protobuf's upb generator writes in a raw string (`R"cc( … upb_MiniTableField field = $field_init$; … )cc"`) and log messages like facebook/rocksdb's `"… in WriteBatch handler; …"`. Before, a later call like `field.ctype()` on the function's own `upb::FieldDefPtr field` parameter, or `handler->PutCF(…)` on a `WriteBatch::Handler* handler`, was matched against the type that text seemed to name, so it linked to nothing or to another class's method. Re-index C++ projects after upgrading. - In C++, a call on an `auto` variable now links to the method of the class C++ finds for the type its initializer gives it, as a call on a variable declared with that type does. The type is read from a cast or smart pointer written out, like facebook/rocksdb's `auto iter = std::unique_ptr(…)` or `auto* mutex = static_cast(arg)`, from `new` or `std::make_unique` of a class, and from what a function or method is declared to return, like the `Iterator*` of `txn->GetIterator(…)`, read to the end of an initializer that goes on to the next line. Before, such a variable took the type of an earlier variable of the same name, or none: in RocksDB, `iter->SeekToFirst()` reached `MemTableRep::Iterator` and `mutex->Lock()` reached `port::Mutex`. A chained call like `cfd->current()->storage_info()` now looks up the class the inner call is declared to return the same way, through a smart pointer too, so protocolbuffers/protobuf's Java generator no longer reaches the C# generator's `EnumGenerator`. A type from outside the project no longer links to a class of the project that shares its name: protobuf's `status.ok()` on an `absl::Status` reached `upb::Status::ok`, and fmtlib/fmt's `os.flush()` on a `std::ostream&` reached fmt's own `ostream::flush`. Re-index C++ projects after upgrading. - In C++, a call on a data member of the calling method's class, like `versions_->GetColumnFamilySet()` in facebook/rocksdb's `db_impl_write.cc`, now links to the method of the member's type wherever the class is declared. Before, only the call's own file and the header of the same name were read for the member, and RocksDB declares `DBImpl` in `db_impl.h` while its methods are spread over `db_impl_write.cc`, `db_impl_compaction_flush.cc`, `db_impl_open.cc` and more, so such calls were guessed by the member's name. A member declared below an inline method that uses it, one a base class declares, and, in a gtest `TEST_F` or `TEST_P` body, one of the test's fixture, like RocksDB's `db_`, are found the same way, and a member of a class nested in the caller's class is no longer taken for the caller's own. A call on a member of a standard-library type, like `std::atomic shutting_down_`, no longer links to a project method that only shares the method's name. Re-index C++ projects after upgrading. +- In C++, a 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. - 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-member-receiver.test.ts b/__tests__/cpp-member-receiver.test.ts index 5fe0408bc0..762ccb9457 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();', '}', 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/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index a6bd02fb1d..4429cdc416 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -8913,24 +8913,88 @@ function normalizeCppTypeName(typeName: string): string | null { return last; } +/** cv-qualifiers and `__restrict`, before or after a declarator's `*` or `&`: `Iterator* const iter`, `Slice const& key`. */ +const CPP_DECLARATOR_CV = String.raw`\b(?:const|volatile|__restrict__|__restrict)\b`; +/** + * What a pointer or reference declarator may carry after its `*` or `&`: + * clang's and abseil's nullability annotations (CPP_NULLABILITY), and the + * ALL-CAPS macros c-cpp.ts blanks for the parser — protobuf's `Arena* + * PROTOBUF_NULLABLE arena`, xxhash's `XXH_RESTRICT`. + */ +const CPP_DECLARATOR_ANNOTATION = String.raw`\b(?:absl_nonnull|absl_nullable|absl_nullability_unknown|_Nonnull|_Nullable|_Null_unspecified|[A-Z][A-Z0-9]*_[A-Z0-9_]*)\b`; +/** A declared type as the declarator regex reads it, what it carries included: `const FieldDescriptor* PROTOBUF_NULLABLE*`. */ +const CPP_DECLARATOR_TYPE = String.raw`[A-Za-z_][\w:]*(?:\s*<[^;=(){}]+>)?(?:\s*(?:${CPP_DECLARATOR_CV}|[*&]+(?:\s*${CPP_DECLARATOR_ANNOTATION})*))*`; +/** An annotation macro's call after a declared name: leveldb's `VersionSet* const versions_ GUARDED_BY(mutex_);`. */ +const CPP_TRAILING_ANNOTATION = String.raw`\b[A-Z][A-Z0-9]*_[A-Z0-9_]*\s*\([^()]*\)`; + // Declarator regex: matches `Type receiver`, `Type* receiver`, `Type *receiver`, // `Type*receiver`, `Type receiver`, etc., REQUIRING a declarator terminator // (`;`, `=`, `,`, `)`, `[`, `{`, `(`, or end-of-line) after the receiver. The // terminator rules out uses like `return receiver->m()` where the preceding // token is a keyword, not a type. A range-based for's `Type receiver :` is read -// by cppRangeForDeclaration instead. +// by cppRangeForDeclaration instead. Qualifiers and annotations around the +// name are read past, and matchCppDeclarator drops the annotations from the +// type. function buildDeclaratorRegex(escapedReceiver: string): RegExp { return new RegExp( - `([A-Za-z_][\\w:]*(?:\\s*<[^;=(){}]+>)?(?:\\s*[*&]+)?)\\s*\\b${escapedReceiver}\\b\\s*(?=[;=,)\\[{(]|$)`, + `(${CPP_DECLARATOR_TYPE})\\s*\\b${escapedReceiver}\\b(?:\\s*${CPP_TRAILING_ANNOTATION})*\\s*(?=[;=,)\\[{(]|$)`, ); } +/** buildDeclaratorRegex's `regex` run on the code `line`, with its type as cppDeclaratorType gives it. */ +function matchCppDeclarator(line: string, regex: RegExp): RegExpExecArray | null { + const m = regex.exec(line); + if (m?.[1] !== undefined) m[1] = cppDeclaratorType(m[1]); + return m; +} + +const CPP_DECLARATOR_ANNOTATION_WORD = new RegExp(`^${CPP_DECLARATOR_ANNOTATION}$`); +const CPP_WORD_AT = /[A-Za-z_]\w*/y; + +/** + * A type the declarator regex read, without the annotations it reads past + * after a `*` or `&`, outside template arguments: `Arena* + * PROTOBUF_NULLABLE` → `Arena*`. Taken as part of the type, one read as its + * name, of a class no project has. An ALL-CAPS word stands for an annotation + * only there, so a type's own name stays (`OS_HANDLE* const h`), and so do + * its cv-qualifiers, which what reads a type reads past. A type with nothing + * to drop comes back as it is. + */ +function cppDeclaratorType(text: string): string { + let out = ''; + let depth = 0; + let afterPointer = false; + let dropped = false; + for (let i = 0; i < text.length; ) { + CPP_WORD_AT.lastIndex = i; + const word = depth === 0 && !/[\w:]/.test(text[i - 1] ?? ' ') ? CPP_WORD_AT.exec(text)?.[0] : undefined; + if (word) { + if (afterPointer && CPP_DECLARATOR_ANNOTATION_WORD.test(word)) { + dropped = true; + } else { + out += word; + afterPointer = false; + } + i += word.length; + continue; + } + const c = text[i]!; + if (c === '<') depth++; + else if (c === '>') depth--; + else if (depth === 0 && (c === '*' || c === '&')) afterPointer = true; + else if (depth === 0 && !/\s/.test(c)) afterPointer = false; + out += c; + i++; + } + return dropped ? out.replace(/\s+/g, ' ').trim() : text; +} + // A range-based for's declaration, `for (const Foo& name :`, up to the single // colon (not `::`) that ends it. What may precede the type — `const`, a // global-scope `::` — is never an initializer, a call or a member access. const CPP_RANGE_FOR_DECLARATION = /\bfor\s*\(([^;=(){}?."']*?)\b([A-Za-z_]\w*)\s*:(?!:)/g; // The type a declaration ends with, read as the declarator regex reads it. -const CPP_DECLARED_TYPE_TAIL = /([A-Za-z_][\w:]*(?:\s*<[^;=(){}]+>)?(?:\s*[*&]+)?)\s*$/; +const CPP_DECLARED_TYPE_TAIL = new RegExp(String.raw`(${CPP_DECLARATOR_TYPE})\s*$`); /** * The type the range-based for on `line` (comments blanked) declares @@ -8944,7 +9008,7 @@ function cppRangeForDeclaration(line: string, receiverName: string): { type: str for (const m of line.matchAll(CPP_RANGE_FOR_DECLARATION)) { if (m[2] !== receiverName) continue; const tail = m[1]!.match(CPP_DECLARED_TYPE_TAIL); - return tail ? { type: tail[1]!, paren: m.index! + m[0].indexOf('('), colon: m.index! + m[0].length } : null; + return tail ? { type: cppDeclaratorType(tail[1]!), paren: m.index! + m[0].indexOf('('), colon: m.index! + m[0].length } : null; } return null; } @@ -9168,7 +9232,7 @@ function cppDataMemberIn(decl: Node, name: string, context: ResolutionContext): for (let l = decl.startLine; l <= end; l++) { if (!word.test(lines[l - 1]!) || nested.some(([start, stop]) => l >= start && l <= stop)) continue; const code = cppCodeOf(cppLiteralsBlanked(lines[l - 1]!, l, rawLines)); - const m = code ? declarator.exec(code) : null; + const m = code ? matchCppDeclarator(code, declarator) : null; if (!m || code!.slice(m.index + m[0].length).startsWith('(')) continue; let depth = depths[l - decl.startLine]!; if (depth < 0) continue; @@ -10446,7 +10510,7 @@ function inferCppReceiverType( const line = cppCodeOf(cppLiteralsBlanked(text, i + 1, rawLines)); if (!line || !receiverPattern.test(line)) continue; - const declaratorMatch = line.match(declaratorRegex); + const declaratorMatch = matchCppDeclarator(line, declaratorRegex); if (declaratorMatch) { const normalized = normalizeCppTypeName(declaratorMatch[1] ?? ''); if (normalized === 'auto') { @@ -10530,7 +10594,7 @@ function inferCppReceiverType( if (!receiverPattern.test(text)) continue; const line = cppCodeOf(cppLiteralsBlanked(text, i + 1, headerRawLines)); if (!line) continue; - const declaratorMatch = line.match(declaratorRegex); + const declaratorMatch = matchCppDeclarator(line, declaratorRegex); if (!declaratorMatch) continue; const normalized = normalizeCppTypeName(declaratorMatch[1] ?? ''); if (normalized && normalized !== 'auto') {