fix(cpp,rust): a qualified call like Status::OK() links the class it names, not one whose name ends the same way (#2723)

The partial qualified-name match took any declaration whose qualified
name ended with the written text, with no scope boundary. In
facebook/rocksdb about 4,100 calls to `Status::OK()`,
`Status::InvalidArgument(…)` and their siblings went to `IOStatus`'s
methods of those names; base-class calls from an override, like
`return FileSystemWrapper::NewWritableFile(…)` or leveldb's
`return DB::Put(…)`, went to a namesake subclass or to the calling
method itself; and ripgrep's `Path::new(…)` on std's `Path` went to its
own `PrinterPath::new`.

- The written name must start a scope of the candidate's: the
  candidate's qualified name is the name, or ends with it right after
  `::` or `.`.
- Among those candidates, the ones in the call's own language come
  first. protobuf's C++ `DescriptorPool::generated_pool()` met a Ruby
  namesake first, which the language gate then dropped.
- A C++ name qualified by a template parameter, like
  `CacheShard::ComputeHash` in `template <class CacheShard> class
  ShardedCache`, gets no edge from any strategy, except to a member of
  the parameter's default (`typename Parser = TcParser`). The boundary
  alone moved rocksdb's call to the receiver-word guess, with the same
  wrong target.
- The C++ template-head reader names each parameter by its last word,
  `int N`, `detail::fixed_string S`, `const PrefetchOpts& kOpts`, and
  keeps its default. A `<…>` after a C++ declaration's name is a
  specialization's arguments, not parameters. Before, `WireFormatLite::
  FieldType kKeyFieldType` made `WireFormatLite` a type parameter.

Measured on main bd62d9c0 and re-checked on 17f1453a: rocksdb 4,219
retargeted, 83 added, 30 removed; protobuf 24, 47, 40; fmt 7, 0, 21;
leveldb 2, 0, 0; ripgrep 0, 0, 38. Nine Ruby, Java, C#, C, JS, Python
and Go controls are byte-identical. Only C++, Rust and Ruby reach this
match; the file-path suffix match is already anchored and is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 01:53:51 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 17f1453a6c
commit ac9f96f8a3
4 changed files with 431 additions and 12 deletions
+1
View File
@@ -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<const char*, KeyComparator> 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<std::pair<int, FileMetaData*>>` or `Striped<CacheAlignedWrapper<port::Mutex>>`, 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 CacheShard> 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 <typename Parser = TcParser>`. 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.
+357
View File
@@ -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 CacheShard> 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<string, string>): Promise<CodeGraph> {
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 CacheShard>',
'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<u8>);',
'',
'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 <typename Hasher>',
'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 <typename Txn>',
'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 <typename Parser = TcParser>',
' 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 <typename Key, WireFormatLite::FieldType kKeyFieldType>',
'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<int, WireFormatLite::TYPE_INT32>` 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 <typename CType, FieldType DeclaredType>',
' static bool ReadPrimitive(int* value);',
' static int ReadVarint(int* value) { return *value; }',
'};',
'template <>',
'inline bool WireFormatLite::ReadPrimitive<int, WireFormatLite::TYPE_INT32>(int* value) {',
' return WireFormatLite::ReadVarint(value) > 0;',
'}',
'} // namespace pb',
),
});
try {
expect(calls(cg, 'pb::WireFormatLite::ReadPrimitive')).toEqual(['pb::WireFormatLite::ReadVarint']);
} finally {
cg.close();
}
});
});
+23 -3
View File
@@ -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' ||
+50 -9
View File
@@ -32,7 +32,7 @@ const DECLARATION_KINDS: ReadonlySet<string> = 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<ResolutionContext, Map<string, ReadonlySet<string>>>();
const memos = new WeakMap<ResolutionContext, Map<string, ReadonlyMap<string, string | undefined>>>();
/** 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 CacheShard> 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 <typename Parser = TcParser>` 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<T>`). */
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<string> = new Set(['class', 'trait', 'module', 'interface', 'struct', 'enum']);
const SCALA_TYPE_KINDS: ReadonlySet<string> = 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<string> {
/** 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<string, string | undefined> {
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<string>();
const names = new Map<string, string | undefined>();
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<T>`, `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 <typename T, class U>` on the lines just above.
// C++: `template <typename T, class U>` 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*</g;
for (let m = template.exec(above); m; m = template.exec(above)) collect(above, m.index + m[0].length - 1, names);
for (let m = template.exec(above); m; m = template.exec(above)) collect(above, m.index + m[0].length - 1, names, true);
}
}
memo.set(node.id, names);
@@ -231,15 +261,26 @@ function nameIndex(head: string, name: string): number {
return -1;
}
/** Add the parameter names of the bracket group opening at `open`. */
function collect(text: string, open: number, names: Set<string>): 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<string, string | undefined>, 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);
}
}