diff --git a/CHANGELOG.md b/CHANGELOG.md index 9258a5e1a..b1e75921c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,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 through a `typedef` or `using` alias now links to the method of the type the alias names, with the alias looked up the way C++ does it: in the calling function, its class and the classes that class inherits from, then the namespaces around it. In google/leveldb, `table_.Insert(buf)` on a `Table table_;` declared next to `typedef SkipList Table;` now reaches `SkipList::Insert` instead of the unrelated `HandleTable::Insert`, and `Table::Iterator iter(&table_);` reaches `SkipList::Iterator`'s constructor. A class that only shares the alias's name is no longer taken for it, and when the aliased type has no such method, as with an alias of `std::vector` or of a template parameter, no method is guessed from the receiver's name. An alias of a type written from the global scope names that global type, also inside a namespace of the same name: protocolbuffers/protobuf's `using Arena = ::upb::Arena;` in `hpb::internal::backend::upb`, so hpb's `arena_.Fuse(…)` on a `backend::Arena` now reaches `upb::Arena::Fuse` instead of the calling method itself. Re-index C++ projects after upgrading. - In C++, a call on a variable, parameter or member declared with template arguments nested inside its template arguments, like `autovector>` or `Striped>`, now links to the method of the class it is declared as. Before, the nested arguments hid that class: in RocksDB, `files_marked_for_compaction_.clear()` on such an `autovector` linked to an unrelated `CompactionInputFiles::clear` that only shares words with the variable's name, and `mutex_.Get(key)` on a `Striped` linked to nothing. Re-index C++ projects after upgrading. - In C++, a base class like `Message` in `class DynamicMessage final : public Message` now links to the class C++ itself would pick: the one declared in the namespaces around the class, or brought in by the file's `using` declarations and namespace aliases, and the class a `typedef` or `using` alias names. Before, a base class was matched by its name alone, so it could land on a class of the same name in another namespace or another language: in protocolbuffers/protobuf, `DynamicMessage` derived from the JSON parser's internal `Message` class, and in google/leveldb and RocksDB, iterators derived from an unrelated nested `Iterator` class such as the skip list's. The wrong base showed up in `codegraph_explore`'s type hierarchy and in impact, and tied the base's methods to the wrong overrides. A template parameter used as a base no longer links to a class that shares its name, and a base written from the global scope, like `public ::testing::Test`, now links to its class. Re-index C++ projects after upgrading. +- In C++ and Rust, a call written with a qualifier, like `Status::OK()`, now links to a method of the class it names, never to one of a class whose name only ends the same way. Before, facebook/rocksdb's calls to `Status::OK()`, `Status::InvalidArgument(…)` and `Status::Corruption(…)` linked to `IOStatus`'s methods of those names, a subclass calling its base, like `return FileSystemWrapper::NewWritableFile(…)` or google/leveldb's `return DB::Put(…)`, linked to another subclass or to the calling method itself, and BurntSushi/ripgrep's `Path::new(…)` on the standard library's `Path` linked to its own `PrinterPath::new`. When classes in several languages share the name, the one in the call's own language is taken, so protocolbuffers/protobuf's C++ `DescriptorPool::generated_pool()` calls, which met its Ruby namesake first and linked to nothing, now link to the C++ method. A C++ call through a template parameter, like `CacheShard::ComputeHash(key)` in rocksdb's `template class ShardedCache`, no longer links to a class that merely shares words with the parameter's name, unless that class is the parameter's default, like `TcParser` in `template `. A template's value parameter, like `const PrefetchOpts& kOpts`, is no longer mistaken for a type parameter, so references to `PrefetchOpts` inside it now link. Re-index C++ and Rust projects after upgrading. - In C and C++, a header or source file named like a test, such as protocolbuffers/protobuf's `conformance_test.h` and `test_runner.h`, now counts as part of the code that `#include`s it, directly or through other headers. Before, calls from that code into it were dropped, so protobuf's conformance suites' `suite_.ReportFailure(…)` and `RunValidInputTest(…)` calls, and its unit tests' calls to the shared `TestUtil` helpers, linked to nothing. A test file that only tests include is still kept out of reach of the rest of your code. Re-index C and C++ projects after upgrading. - In C++, a class, struct or method now keeps the namespaces and the class it is declared in when the parser misreads something earlier in the file, such as an unknown macro in front of a member or in a class header. Before, everything after the misread code could lose them, or land inside a class it isn't in: in protocolbuffers/protobuf, `FieldDescriptor` was indexed without `google::protobuf::`, and in RocksDB, the `Opts` struct declared inside a cache table class was indexed outside it, so finding a class's base classes and the methods called on its objects fell back to guessing by name. Methods that had come loose from their class are its members again, and classes the misread code had hidden, like leveldb's POSIX file and environment classes, are now indexed. Re-index C++ projects after upgrading. - In C++, a call on a variable, parameter or member declared as a standard-library or other outside type with a lowercase name, like `std::string`, `std::vector`, `std::set` or `absl::flat_hash_set`, no longer links to a project method that only shares the method's name, even one of a project class in another namespace with the type's name, like your own `util::flat_hash_set`. A variable declared outside the calling function, like another function's or a file's `std::string text`, no longer links its calls to such a class either. 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. A call on a member reached through other members, like facebook/rocksdb's `options.cf_paths.emplace_back(…)` on a `std::vector`, `props->rep.db_id.size()` on a `std::string` or `bbto.filter_policy.reset(…)` on a `std::shared_ptr`, no longer links to a project method of that name either: those had reached `autovector::emplace_back`, `BlobContents::size` and `FSWritableFilePtr::reset`. The same goes for a member of an outside type like `absl::Status`. Re-index C++ projects after upgrading. diff --git a/__tests__/qualified-name-boundary.test.ts b/__tests__/qualified-name-boundary.test.ts new file mode 100644 index 000000000..415615629 --- /dev/null +++ b/__tests__/qualified-name-boundary.test.ts @@ -0,0 +1,357 @@ +/** + * A qualified name matches a declaration's qualified name only at a scope + * boundary: `Status::OK` is `rocksdb::Status::OK`, never `rocksdb::IOStatus::OK`, + * and `CacheShard::ComputeHash` is never `…::ClockCacheShard::ComputeHash`. + * + * The partial qualified-name match took any declaration whose qualified name + * ended with the written text. facebook/rocksdb sent about 4,000 calls to its + * most common functions (`Status::OK()`, `Status::InvalidArgument(…)`) to + * `IOStatus`'s namesakes, a base-class call from an override (leveldb's + * `return DB::Put(…)` in `ModelDB::Put`) to the override itself, and + * BurntSushi/ripgrep's `Path::new(…)` (std's `Path`) to its own `PrinterPath::new`. + * + * A C++ name qualified by a template parameter — `CacheShard::ComputeHash(…)` + * in `template class ShardedCache` — is a member of whatever + * type the template is instantiated with, so it gets no edge, from this match + * or from a guess by the receiver's name. + */ +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-qualified-boundary-')); + 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'); + +describe('a partial qualified-name match starts at a scope boundary', () => { + it('reproduction: `CacheShard::ComputeHash` on a template parameter is not ClockCacheShard::ComputeHash', async () => { + const cg = await indexed({ + 'cache/sharded_cache.h': lines( + 'namespace rocksdb {', + 'template ', + 'class ShardedCache {', + ' public:', + ' unsigned long Lookup(const char* key) {', + ' return CacheShard::ComputeHash(key, hash_seed_);', + ' }', + ' private:', + ' unsigned hash_seed_ = 0;', + '};', + '} // namespace rocksdb', + ), + 'cache/clock_cache.h': lines( + 'namespace rocksdb {', + 'namespace clock_cache {', + 'class ClockCacheShard {', + ' public:', + ' static unsigned long ComputeHash(const char* key, unsigned seed) { return seed; }', + '};', + '} // namespace clock_cache', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::ShardedCache::Lookup')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('`Status::OK()` is Status::OK, not IOStatus::OK declared before it', async () => { + const cg = await indexed({ + 'include/rocksdb/status.h': lines( + 'namespace rocksdb {', + 'class IOStatus {', + ' public:', + ' static IOStatus OK() { return IOStatus(); }', + '};', + 'class Status {', + ' public:', + ' static Status OK() { return Status(); }', + '};', + '} // namespace rocksdb', + ), + 'db/db_impl.cc': lines( + '#include "include/rocksdb/status.h"', + 'namespace rocksdb {', + 'Status Flush() {', + ' return Status::OK();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Flush')).toEqual(['rocksdb::Status::OK']); + } finally { + cg.close(); + } + }); + + it('an override calling `DB::Put(…)` reaches the base method, not itself', async () => { + const cg = await indexed({ + 'include/leveldb/db.h': lines( + 'namespace leveldb {', + 'class DB {', + ' public:', + ' virtual int Put(int k, int v);', + '};', + '} // namespace leveldb', + ), + 'db/db_impl.cc': lines( + '#include "include/leveldb/db.h"', + 'namespace leveldb {', + 'int DB::Put(int k, int v) { return k + v; }', + '} // namespace leveldb', + ), + 'db/db_test.cc': lines( + '#include "include/leveldb/db.h"', + 'namespace leveldb {', + 'class ModelDB : public DB {', + ' public:', + ' int Put(int k, int v) override {', + ' return DB::Put(k, v);', + ' }', + '};', + '} // namespace leveldb', + ), + }); + try { + expect(calls(cg, 'leveldb::ModelDB::Put')).toEqual(['leveldb::DB::Put']); + } finally { + cg.close(); + } + }); + + it('a C++ `DescriptorPool::generated_pool()` reaches the C++ class, not a Ruby namesake indexed first', async () => { + const cg = await indexed({ + 'lib/google/protobuf/descriptor_pool.rb': lines( + 'module Google', + ' module Protobuf', + ' class DescriptorPool', + ' def self.generated_pool', + ' @generated_pool ||= new', + ' end', + ' end', + ' end', + 'end', + ), + 'src/google/protobuf/descriptor.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'class DescriptorPool {', + ' public:', + ' static const DescriptorPool* generated_pool() { return nullptr; }', + '};', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/compiler/main.cc': lines( + '#include "src/google/protobuf/descriptor.h"', + 'namespace google {', + 'namespace protobuf {', + 'const DescriptorPool* Pool() {', + ' return DescriptorPool::generated_pool();', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::Pool')).toEqual(['google::protobuf::DescriptorPool::generated_pool']); + } finally { + cg.close(); + } + }); + + it('Rust: `Path::new` on std\'s Path is not the project\'s PrinterPath::new', async () => { + const cg = await indexed({ + 'Cargo.toml': lines('[package]', 'name = "rg"', 'version = "0.1.0"', 'edition = "2021"'), + 'src/printer.rs': lines( + 'pub struct PrinterPath(Vec);', + '', + 'impl PrinterPath {', + ' pub fn new(path: &std::path::Path) -> PrinterPath {', + ' PrinterPath(path.to_string_lossy().as_bytes().to_vec())', + ' }', + '}', + ), + 'src/main.rs': lines( + 'mod printer;', + 'use std::path::Path;', + '', + 'fn resolve(program: &str) -> bool {', + ' let bin = Path::new(program);', + ' bin.exists()', + '}', + ), + }); + try { + expect(calls(cg, 'resolve')).toEqual([]); + } finally { + cg.close(); + } + }); +}); + +describe('a C++ name qualified by a template parameter gets no edge', () => { + it('a function template\'s `Hasher::Hash(k)` is neither MurmurHasher::Hash by suffix nor by the receiver\'s name', async () => { + const cg = await indexed({ + 'util/hash.h': lines( + 'namespace rocksdb {', + 'struct MurmurHasher {', + ' static unsigned Hash(int k) { return k * 31u; }', + '};', + 'template ', + 'unsigned HashOf(int k) {', + ' return Hasher::Hash(k);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::HashOf')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a mixin\'s `Txn::Put(k)` on the base it is given is not WriteCommittedTxn::Put', async () => { + const cg = await indexed({ + 'utilities/transactions/txn.h': lines( + 'namespace rocksdb {', + 'class WriteCommittedTxn {', + ' public:', + ' int Put(int k) { return k; }', + '};', + '} // namespace rocksdb', + ), + 'utilities/secondary_index/secondary_index_mixin.h': lines( + 'namespace rocksdb {', + 'template ', + 'class SecondaryIndexMixin : public Txn {', + ' public:', + ' int AddPrimaryEntry(int k) {', + ' return Txn::Put(k);', + ' }', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::SecondaryIndexMixin::AddPrimaryEntry')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a parameter with a default, `typename Parser = TcParser`, still reaches the default\'s `Parser::ParseLoop`', async () => { + const cg = await indexed({ + 'src/parse_context.h': lines( + 'namespace pb {', + 'class TcParser {', + ' public:', + ' static const char* ParseLoop(const char* ptr) { return ptr; }', + '};', + 'class ParseContext {', + ' public:', + ' template ', + ' const char* ParseMessage(const char* ptr) {', + ' return Parser::ParseLoop(ptr);', + ' }', + '};', + '} // namespace pb', + ), + }); + try { + expect(calls(cg, 'pb::ParseContext::ParseMessage')).toEqual(['pb::TcParser::ParseLoop']); + } finally { + cg.close(); + } + }); + + it('a non-type parameter is named by its last word: `WireFormatLite::FieldType kKeyFieldType` leaves WireFormatLite a class', async () => { + const cg = await indexed({ + 'src/wire_format_lite.h': lines( + 'namespace pb {', + 'class WireFormatLite {', + ' public:', + ' enum FieldType { TYPE_INT32 = 1 };', + ' static int WriteTagToArray(int tag) { return tag; }', + '};', + '} // namespace pb', + ), + 'src/map_type_handler.h': lines( + '#include "src/wire_format_lite.h"', + 'namespace pb {', + 'template ', + 'struct MapEntryFuncs {', + ' static int InternalSerialize(int tag) {', + ' return WireFormatLite::WriteTagToArray(tag);', + ' }', + '};', + '} // namespace pb', + ), + }); + try { + expect(calls(cg, 'pb::MapEntryFuncs::InternalSerialize')).toEqual(['pb::WireFormatLite::WriteTagToArray']); + } finally { + cg.close(); + } + }); + + it('an explicit specialization\'s arguments are not parameters: `ReadPrimitive` still calls WireFormatLite', async () => { + const cg = await indexed({ + 'src/wire_format_lite.h': lines( + 'namespace pb {', + 'class WireFormatLite {', + ' public:', + ' enum FieldType { TYPE_INT32 = 5 };', + ' template ', + ' static bool ReadPrimitive(int* value);', + ' static int ReadVarint(int* value) { return *value; }', + '};', + 'template <>', + 'inline bool WireFormatLite::ReadPrimitive(int* value) {', + ' return WireFormatLite::ReadVarint(value) > 0;', + '}', + '} // namespace pb', + ), + }); + try { + expect(calls(cg, 'pb::WireFormatLite::ReadPrimitive')).toEqual(['pb::WireFormatLite::ReadVarint']); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index b1140f64b..50486f203 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -11626,16 +11626,23 @@ export function matchByQualifiedName( // Try partial qualified name match — again preferring the call site's own // file when more than one symbol's qualifiedName ends with the reference. + // The written name must start a scope of the candidate's: rocksdb's + // `CacheShard::ComputeHash` (a template parameter's) is no + // `clock_cache::ClockCacheShard::ComputeHash`. A namesake in the call's own + // language comes first: protobuf's C++ `DescriptorPool::generated_pool()` + // is never its Ruby `Google::Protobuf::DescriptorPool`'s. const parts = ref.referenceName.split(/[:.]/); const lastName = parts[parts.length - 1]; if (lastName) { const partialCandidates = cppSpecializationPool(keepForRef(context.getNodesByName(lastName)) - .filter((candidate) => candidate.qualifiedName.endsWith(ref.referenceName)), ref, context); - const chosen = preferCallSiteFile(partialCandidates, ref.filePath)[0]; + .filter((candidate) => endsAtScopeBoundary(candidate.qualifiedName, ref.referenceName)), ref, context); + const ownLanguage = partialCandidates.filter((candidate) => sameLanguageFamily(candidate.language, ref.language)); + const pool = ownLanguage.length > 0 ? ownLanguage : partialCandidates; + const chosen = preferCallSiteFile(pool, ref.filePath)[0]; if (chosen) { return { original: ref, - targetNodeId: cppFunctionOverload(chosen, partialCandidates, ref, context).id, + targetNodeId: cppFunctionOverload(chosen, pool, ref, context).id, confidence: 0.85, resolvedBy: 'qualified-name', }; @@ -11645,6 +11652,19 @@ export function matchByQualifiedName( return null; } +/** + * Whether qualified name `qn` is `name`, or ends with it right after a scope + * separator: `::`, or the `.` of a dotted package, namespace, object path or + * config key. A name written from the global scope (`::Foo::bar`) is never + * the tail of a nested one. + */ +function endsAtScopeBoundary(qn: string, name: string): boolean { + if (qn === name) return true; + if (!qn.endsWith(name)) return false; + const before = qn.slice(0, qn.length - name.length); + return before.endsWith('::') || before.endsWith('.'); +} + /** A node a `Receiver.method()` call can name as the method's owning type. */ function isMethodOwnerKind(n: Node): boolean { return n.kind === 'class' || n.kind === 'struct' || n.kind === 'union' || n.kind === 'interface' || diff --git a/src/resolution/type-parameters.ts b/src/resolution/type-parameters.ts index d5c4a9ba1..c0588cba4 100644 --- a/src/resolution/type-parameters.ts +++ b/src/resolution/type-parameters.ts @@ -32,7 +32,7 @@ const DECLARATION_KINDS: ReadonlySet = new Set([ /** How far past a declaration's first line its head can run: a parameter list may take a line per parameter. */ const HEAD_LINES = 40; -const memos = new WeakMap>>(); +const memos = new WeakMap>>(); /** Drop the memos (see ReferenceResolver.clearCaches). */ export function clearTypeParameterMemos(context: ResolutionContext): void { @@ -69,6 +69,23 @@ export function gateTypeParameter(resolved: ResolvedRef | null, ref: UnresolvedR if (!chosen) return null; result = { ...result, targetNodeId: chosen.id }; } + // A C++ name qualified by a template parameter is a member of whatever type + // the template is instantiated with: rocksdb's `CacheShard::ComputeHash(…)` + // in `template class ShardedCache` is not the + // `ClockCacheShard::ComputeHash` a guess by name reaches. Only the + // parameter's default is known: protobuf's `Parser::ParseLoop(…)` under + // `template ` is TcParser's for every caller + // that passes no parser. + if (ref.language === 'cpp') { + const qualifier = /^([A-Za-z_]\w*)::/.exec(ref.referenceName)?.[1]; + if (qualifier) { + for (const declaration of enclosingDeclarations(ref, context)) { + const parameters = declaredTypeParameters(declaration, context); + if (!parameters.has(qualifier)) continue; + return isMemberOfDefault(result, parameters.get(qualifier), context) ? result : null; + } + } + } if (!GENERIC_LANGUAGES.has(ref.language) || !TYPE_REF_KINDS.has(ref.referenceKind)) return result; const name = ref.referenceName; if (!/^[A-Z][A-Za-z0-9_]*$/.test(name)) return result; @@ -78,6 +95,14 @@ export function gateTypeParameter(resolved: ResolvedRef | null, ref: UnresolvedR return result; } +/** Whether the resolved target is a member of the class a C++ template parameter defaults to (`= TcParser`, `= std::allocator`). */ +function isMemberOfDefault(result: ResolvedRef, fallback: string | undefined, context: ResolutionContext): boolean { + const type = fallback === undefined ? undefined : /([A-Za-z_]\w*)\s*(?:<[\s\S]*>)?\s*$/.exec(fallback)?.[1]; + if (!type) return false; + const segments = context.getNodeById?.(result.targetNodeId)?.qualifiedName.split('::') ?? []; + return segments.length >= 2 && segments[segments.length - 2] === type; +} + const SCALA_OWNER_KINDS: ReadonlySet = new Set(['class', 'trait', 'module', 'interface', 'struct', 'enum']); const SCALA_TYPE_KINDS: ReadonlySet = new Set(['class', 'trait', 'interface', 'struct', 'enum', 'type_alias']); @@ -129,7 +154,8 @@ function enclosingDeclarations(ref: UnresolvedRef, context: ResolutionContext): .filter((n) => DECLARATION_KINDS.has(n.kind) && n.startLine <= ref.line && n.endLine >= ref.line); } -function declaredTypeParameters(node: Node, context: ResolutionContext): ReadonlySet { +/** The type parameters a declaration declares, each with the default a C++ head gives it (`typename Parser = TcParser`). */ +function declaredTypeParameters(node: Node, context: ResolutionContext): ReadonlyMap { let memo = memos.get(context); if (!memo) { memo = new Map(); @@ -137,12 +163,14 @@ function declaredTypeParameters(node: Node, context: ResolutionContext): Readonl } const hit = memo.get(node.id); if (hit) return hit; - const names = new Set(); + const names = new Map(); const lines = context.getFileLines?.(node.filePath) ?? context.readFile(node.filePath)?.split(/\r?\n/) ?? null; if (lines) { const head = lines.slice(node.startLine - 1, Math.min(node.endLine, node.startLine - 1 + HEAD_LINES)).join('\n'); const at = nameIndex(head, node.name); - if (at >= 0) { + // (C++ declares them only in a `template <…>` head, below: the `<…>` after + // a C++ class's name is a specialization's arguments.) + if (at >= 0 && node.language !== 'cpp') { // After the declared name: `Foo`, `f[A]`. let open = at + node.name.length; while (open < head.length && /[ \t]/.test(head[open]!)) open++; @@ -159,11 +187,13 @@ function declaredTypeParameters(node: Node, context: ResolutionContext): Readonl const receiver = /^\s*func\s*\(\s*(?:[A-Za-z_]\w*\s+)?\*?\s*[A-Za-z_]\w*\s*\[/.exec(head); if (receiver) collect(head, receiver[0].length - 1, names); } - // C++: `template ` on the lines just above. + // C++: `template ` on the lines just above. Each + // parameter is named last: `int N = 0`, `detail::fixed_string S` and + // `std::integral T` declare N, S and T. if (node.language === 'cpp') { const above = lines.slice(Math.max(0, node.startLine - 3), node.startLine).join('\n'); const template = /\btemplate\s*): void { +/** + * Add the parameter names of the bracket group opening at `open`. With + * `lastWord` (C++), each is the last word before its default, which is kept. + */ +function collect(text: string, open: number, names: Map, lastWord = false): void { const close = matchingBracket(text, open); if (close < 0) return; for (const item of splitTopLevel(text.slice(open + 1, close), text[open] === '<')) { + if (lastWord) { + const at = item.indexOf('='); + const param = /([A-Za-z_]\w*)\s*$/.exec(at < 0 ? item : item.slice(0, at))?.[1]; + if (param && param !== 'typename' && param !== 'class' && param !== 'struct') { + names.set(param, at < 0 ? undefined : item.slice(at + 1).trim()); + } + continue; + } // Past annotations (`@sp(Int, Long) A`, `@NonNull T`), variance and keywords. const bare = item.replace(/^\s*(?:@[\w.]+(?:\([^()]*\))?\s*)+/, ''); const param = /^\s*(?:(?:in|out|reified|const|typename|class|struct)\s+|[+\-']\s*)*([A-Za-z_]\w*)/.exec(bare)?.[1]; - if (param && param !== 'typename' && param !== 'class') names.add(param); + if (param && param !== 'typename' && param !== 'class' && !names.has(param)) names.set(param, undefined); } }