diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aca5fb6e2..119127f14f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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. - 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-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/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 4429cdc416..669d2f158f 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -8922,8 +8922,15 @@ const CPP_DECLARATOR_CV = String.raw`\b(?:const|volatile|__restrict__|__restrict * 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 function wrapper's type, whose template argument is a function type: + * rocksdb's `std::function create_iterator`. + */ +const CPP_FUNCTION_WRAPPER_TYPE = String.raw`(?:::\s*)?(?:std\s*::\s*(?:function|move_only_function|copyable_function)|absl\s*::\s*(?:FunctionRef|AnyInvocable))\s*<[^;={}]*>`; /** 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})*))*`; +const CPP_DECLARATOR_TYPE = String.raw`(?:${CPP_FUNCTION_WRAPPER_TYPE}|[A-Za-z_][\w:]*(?:\s*<[^;=(){}]+>)?)(?:\s*(?:${CPP_DECLARATOR_CV}|[*&]+(?:\s*${CPP_DECLARATOR_ANNOTATION})*))*`; +/** A line that is a declared type and nothing else, what may come before a type included: `const std::map&`. */ +const CPP_TYPE_LINE = new RegExp(String.raw`^\s*(?:(?:const|volatile|static|mutable|constexpr|inline)\s+)*${CPP_DECLARATOR_TYPE}\s*$`); /** 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*\([^()]*\)`; @@ -9062,6 +9069,10 @@ const CPP_LIBRARY_SEQUENCES: ReadonlySet = new Set([ ]); /** Widely used project containers of the same kind: protobuf's repeated fields, rocksdb's `autovector`. */ const CPP_PROJECT_SEQUENCES: ReadonlySet = new Set(['RepeatedField', 'RepeatedPtrField', 'autovector']); +/** The standard library's and abseil's maps, whose elements are `std::pair`s of a key and a value. */ +const CPP_LIBRARY_MAPS: ReadonlySet = new Set([ + 'map', 'multimap', 'unordered_map', 'unordered_multimap', 'flat_hash_map', 'node_hash_map', 'btree_map', 'btree_multimap', +]); /** Clang's and abseil's nullability annotations: `const FieldDescriptor* absl_nonnull`. */ const CPP_NULLABILITY = /\b(?:absl_nonnull|absl_nullable|absl_nullability_unknown|_Nonnull|_Nullable|_Null_unspecified)\b/g; @@ -9231,18 +9242,32 @@ function cppDataMemberIn(decl: Node, name: string, context: ResolutionContext): const depths = cppClassParenDepths(decl, lines, end, rawLines, context); 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 ? matchCppDeclarator(code, declarator) : null; + let code = cppCodeOf(cppLiteralsBlanked(lines[l - 1]!, l, rawLines)); + // The line the declaration's code starts on: the one above, for a type + // written there and nothing else (rocksdb's + // `std::vector>>` + // above `cfh_iter_pairs_;`), so a default member initializer continued + // on the next line (`bool ready_ = flag_ &&` above `it_;`) isn't one. + let from = l; + let m = code ? matchCppDeclarator(code, declarator) : null; + if (!m && code && l > decl.startLine) { + const above = cppCodeOf(cppLiteralsBlanked(lines[l - 2]!, l - 1, rawLines)); + if (above && CPP_TYPE_LINE.test(above)) { + code = `${above} ${code}`; + from = l - 1; + m = matchCppDeclarator(code, declarator); + } + } if (!m || code!.slice(m.index + m[0].length).startsWith('(')) continue; - let depth = depths[l - decl.startLine]!; + let depth = depths[from - decl.startLine]!; if (depth < 0) continue; - for (const c of code!.slice(l === decl.startLine ? decl.startColumn : 0, m.index)) { + for (const c of code!.slice(from === decl.startLine ? decl.startColumn : 0, m.index)) { if (c === '(') depth++; else if (c === ')') depth--; } const type = depth <= 0 ? normalizeCppTypeName(m[1]!) : null; if (type && type !== 'auto') { - member = { text: m[1]!, scope: decl.qualifiedName, file: decl.filePath, line: l, parameters: cppTemplateParameters(decl, context) }; + member = { text: m[1]!, scope: decl.qualifiedName, file: decl.filePath, line: from, parameters: cppTemplateParameters(decl, context) }; break; } } @@ -9412,6 +9437,11 @@ function cppRangeElementOf(type: CppWrittenType, at: UnresolvedRef, context: Res const element = cppTemplateArguments(range.text)?.[0]; return element ? { ...range, text: cppWithoutNullability(element) } : null; } + // A map's elements pair its key with its value. + if (CPP_LIBRARY_MAPS.has(last) && (segments[0] === 'std' || segments[0] === 'absl')) { + const [key, value] = cppTemplateArguments(range.text) ?? []; + return key && value ? { ...range, text: `std::pair` } : null; + } const cls = cppWrittenClass(range, at, context); const begin = cls ? cppReturnTypeOf(cppMethodsOf(cls, 'begin', at, context), context) : null; return begin ? cppIteratorElement(begin, at, context, hops) : null; @@ -9500,6 +9530,12 @@ function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionC if (made) return { ...callers, text: `std::${made[1]}_ptr<${made[2]!.trim()}>` }; // `std::unique_ptr(…)`: the smart pointer or optional it makes. if (cppStdHeldType(callee)) return { ...callers, text: callee }; + // `create_iterator()` on a `std::function create_iterator`: what its function type returns. + if (/^[A-Za-z_]\w*$/.test(callee)) { + const wrapper = cppExpressionType(callee, at, context, depth + 1); + const returned = wrapper && cppWrapperReturnType(wrapper); + if (returned) return returned; + } // `recv.method(…)`, `recv->method(…)`: the method of the receiver's class. const member = /^(.+?)\s*(\.|->)\s*([A-Za-z_]\w*)$/.exec(callee); if (member) { @@ -10566,6 +10602,14 @@ function inferCppReceiverType( return cppDeclaredType(element.text, elementType, true, ref, context, found); } } + // `for (auto& [cfh, iter] : cfh_iter_pairs_)`, `auto [status, iter] = Open();`. + const bound = !found.shadowed ? cppStructuredBindingType(line, lines, i, receiverName, callLineIndex, ref, context, rawLines) : null; + const boundType = bound && normalizeCppTypeName(bound.text); + if (bound && boundType && boundType !== 'auto') { + noteCppDeclaration(found, bound.text, true); + found.declared = bound; + return cppDeclaredType(bound.text, boundType, true, ref, context, found); + } if (cppRebindsReceiver(line, escapedReceiver)) found.shadowed = true; } } @@ -10761,8 +10805,19 @@ function cppAutoInitializerStatement( ): string | null { const escaped = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const start = new RegExp(`\\b${escaped}\\b\\s*=(?!=)`).exec(line); - if (!start) return null; - let text = line.slice(start.index + start[0].length); + return start ? cppStatementFrom(line.slice(start.index + start[0].length), lines, index, rawLines) : null; +} + +/** + * The C++ statement that `text`, the rest of `lines[index]`, begins, read on + * to the `;` that ends it. Null when no `;` ends it within a few lines. + */ +function cppStatementFrom( + text: string, + lines: readonly string[], + index: number, + rawLines: ReturnType, +): string | null { for (let next = index + 1; ; next++) { const end = cppStatementEnd(text); if (end >= 0) return text.slice(0, end).trim() || null; @@ -10771,6 +10826,82 @@ function cppAutoInitializerStatement( } } +/** A structured binding's names: `[cfh, iter]` in `for (auto& [cfh, iter] : …)` or `const auto [status, iter] = …;`. */ +const CPP_STRUCTURED_BINDING = /\bauto\s*&{0,2}\s*\[([^\]]*)\]/g; + +/** + * The type the structured binding on line `index` (`line`, its code) gives + * `receiverName`, as written: the element at the name's place of the pair + * or tuple bound. That is an element of the range a range-for over the + * binding iterates, when the call is in the loop's body — rocksdb's `for + * (auto& [cfh, iter] : cfh_iter_pairs_)` over a + * `std::vector>>`, + * or a map's key and value — or the initializer of a declaration the + * calling function makes (`auto [status, iter] = Open();`). Null when that + * can't be read. + */ +function cppStructuredBindingType( + line: string, + lines: readonly string[], + index: number, + receiverName: string, + callLineIndex: number, + ref: UnresolvedRef, + context: ResolutionContext, + rawLines: ReturnType, +): CppWrittenType | null { + if (!line.includes('[')) return null; + for (const m of line.matchAll(CPP_STRUCTURED_BINDING)) { + const place = m[1]!.split(',').map((name) => name.trim()).indexOf(receiverName); + if (place < 0) continue; + const end = m.index! + m[0].length; + const loop = /\bfor\s*\(\s*(?:(?:const|volatile)\s+)*$/.exec(line.slice(0, m.index!)); + const colon = /^\s*:(?!:)/.exec(line.slice(end)); + let value: CppWrittenType | null = null; + if (loop && colon) { + if (!cppForBodyEncloses(lines, index, line.indexOf('(', loop.index), callLineIndex, ref.column)) return null; + value = cppAutoLoopElement(lines, index, { colon: end + colon[0].length }, ref, context); + } else { + const assign = /^\s*=(?!=)/.exec(line.slice(end)); + if (!assign || !isCppCallersDeclaration(ref.filePath, index + 1, ref, context)) return null; + const init = cppStatementFrom(line.slice(end + assign[0].length), lines, index, rawLines); + value = init ? cppAutoLocalType(init, index + 1, m[0], ref, context) : null; + } + return (value && cppBindingElements(value, ref, context)?.[place]) ?? null; + } + return null; +} + +/** + * What calling a function wrapper of the written type `type` returns, as + * written: what the function type that is its template argument returns + * (`Iterator*` for `std::function`). Only a wrapper's + * declaration is read with one (CPP_FUNCTION_WRAPPER_TYPE). Null when its + * template argument is no function type. + */ +function cppWrapperReturnType(type: CppWrittenType): CppWrittenType | null { + const signature = cppTemplateArguments(type.text)?.[0] ?? ''; + let depth = 0; + for (let i = 0; i < signature.length; i++) { + const c = signature[i]!; + if (c === '<') depth++; + else if (c === '>') depth--; + else if (c === '(' && depth === 0) { + const returned = signature.slice(0, i).trim(); + return returned ? { ...type, text: returned } : null; + } + } + return null; +} + +/** What a structured binding of a value of the type `type` names, in order: a `std::pair`'s or `std::tuple`'s elements. */ +function cppBindingElements(type: CppWrittenType, at: UnresolvedRef, context: ResolutionContext): CppWrittenType[] | null { + const followed = cppFollowWrittenAliases(type, at, context); + const segments = followed && cppTypeSegments(followed.text); + if (!followed || segments?.length !== 2 || segments[0] !== 'std' || (segments[1] !== 'pair' && segments[1] !== 'tuple')) return null; + return cppTemplateArguments(followed.text)?.map((text) => ({ ...followed, text: cppWithoutNullability(text) })) ?? null; +} + /** Where the statement that `text` begins ends: its first `;` outside brackets, or -1. */ function cppStatementEnd(text: string): number { let depth = 0;