diff --git a/CHANGELOG.md b/CHANGELOG.md index f6661ba6..bb37f95b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -256,6 +256,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In TypeScript and JavaScript, an import of a package your project declares itself with `declare module '…'` in a `.d.ts` file, like a `typings/mammoth/index.d.ts` written for an untyped package or VS Code's own `vscode.d.ts`, now links to that declaration, so `codegraph callers`, impact and `codegraph affected` show the code that uses your typings instead of nothing or a project symbol that only shares a name. Named, default and namespace imports, `export =` and renaming export lists are read the way TypeScript reads them, a function such a file declares without a body (`export function convertToHtml(…): Promise;`) is now indexed so calls to it have something to link to, and only an import of its package reaches it. A `declare module` that augments a package's own types, a wildcard like `declare module '*.svg'`, and copies of typings kept in test fixtures are left alone. Re-index TypeScript projects after upgrading. - In TypeScript, an interface that extends other interfaces, like `interface UserRepo extends Repo, Named`, is now linked to each of them: the type hierarchy shows them above the interface and above every class that implements it, a change to an interface reaches the interfaces built on it and their implementations in impact results, and a call on a value typed by such an interface, like `hass.formatEntityName(…)` on a `HomeAssistant` that extends seven interfaces, links to the method the interface inherits instead of a same-named method elsewhere. Before, only a class's `extends` and `implements` were read, so every interface stood alone. A type from a package, like `ButtonProps` from `@mui/material`, or one of TypeScript's own, like `Omit<…>`, still links to nothing rather than to a type of the same name elsewhere in your project. Re-index TypeScript projects after upgrading. - In TypeScript and JavaScript, a type, class, constant or other declaration that a `.d.ts` file declares inside `declare module '…'` now links only from files that use that module: files that import it, the module's other declarations, and files that import it through a file of yours that re-exports it. Before, any file using the same name linked to it: Node's `import { readFileSync } from 'fs'` linked to the `fs` that VS Code's `vscode.d.ts` declares, and a `ProcessEnv`, or an LSP `TextDocument` imported through a barrel, to the `vscode` module's types of those names. Re-index TypeScript projects after upgrading. +- In C++, a call written through a class the index doesn't hold, like protocolbuffers/protobuf's `FeatureSet::descriptor()` or `OneofOptions::default_instance()` (both classes are declared in `descriptor.pb.h`, a file too large to index), no longer links to another class's method of that name whose class only shares a word with it, like `JavaFeatures_NestInFileClassFeature::descriptor` or `CppFileOptions::default_instance`. A class named like the call's would have been found first, so one that only shares a word is another class. A call through a template parameter, through an alias, including one the calling function or a macro it expands declares, or through a namespace the file opens, aliases or uses links as before, and so does a call of a method only one class in the project declares. Re-index C++ projects after upgrading. ## [1.6.2] - 2026-10-03 diff --git a/__tests__/cpp-undeclared-qualifier.test.ts b/__tests__/cpp-undeclared-qualifier.test.ts new file mode 100644 index 00000000..c3facc27 --- /dev/null +++ b/__tests__/cpp-undeclared-qualifier.test.ts @@ -0,0 +1,496 @@ +/** + * A C++ call through a class the index doesn't hold — `FeatureSet::descriptor()` + * — gets no edge, rather than a guess by the words the qualifier shares with + * some other class's name. + * + * protocolbuffers/protobuf declares `FeatureSet` in `descriptor.pb.h`, which is + * over the indexer's file-size cap, so no class of that name is indexed. The + * call fell through to the last strategy, which scores every method named + * `descriptor` by the words its class shares with the qualifier, and picked + * `pb::JavaFeatures_NestInFileClassFeature::descriptor` ("Feature"). The + * Python extension's `OneofOptions::default_instance()` went to + * `pb::file::CppFileOptions::default_instance` ("Options"). A class whose name + * only shares a word with the qualifier is, by construction, another class: + * one named like the qualifier would have been found first. + * + * Untouched: a qualifier the index holds (a class, a nested class, an alias), + * a namespace, and a name the calling file makes something itself — a + * template parameter, an alias it declares in a function body or through a + * macro it expands, googletest's `TestFixture` in a typed test. So is the one + * method of a name in the project, which is still taken: protobuf's + * `MapTestPeer`, declared in a `.inc` file the index doesn't read as C++, + * reaches the one `CalculateHiCutoff` there is, the `KeyMapBase` method it + * hands on to. + */ +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-undeclared-')); + 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(); +} + +/** Where the `calls` edges written in `file` lead, as `name @ file:line` of each target. */ +function callsFrom(cg: CodeGraph, file: string): string[] { + const out: string[] = []; + for (const caller of [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')]) { + if (caller.filePath !== file) continue; + for (const r of cg.getCallees(caller.id)) { + if (r.edge.kind === 'calls') out.push(`${r.node.name} @ ${r.node.filePath}:${r.node.startLine}`); + } + } + return out.sort(); +} + +const lines = (...l: string[]): string => [...l, ''].join('\n'); + +/** protobuf's classes whose methods share their names with the generated messages' ones. */ +const PROTOBUF = { + 'src/google/protobuf/compiler/java/java_features.pb.h': lines( + 'namespace pb {', + 'class JavaFeatures_NestInFileClassFeature {', + ' public:', + ' static const void* descriptor() { return nullptr; }', + ' static const void* GetDescriptor() { return nullptr; }', + ' static const JavaFeatures_NestInFileClassFeature& default_instance() { return *instance_; }', + ' private:', + ' static JavaFeatures_NestInFileClassFeature* instance_;', + '};', + '} // namespace pb', + ), + 'src/google/protobuf/cpp_file_options.pb.h': lines( + 'namespace pb {', + 'namespace file {', + 'class CppFileOptions {', + ' public:', + ' static const CppFileOptions& default_instance() { return *instance_; }', + ' private:', + ' static CppFileOptions* instance_;', + '};', + '} // namespace file', + '} // namespace pb', + ), + 'src/google/protobuf/compiler/plugin.pb.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'namespace compiler {', + 'class CodeGeneratorResponse_File {', + ' public:', + ' static const CodeGeneratorResponse_File& default_instance() { return *instance_; }', + ' private:', + ' static CodeGeneratorResponse_File* instance_;', + '};', + '} // namespace compiler', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/message.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'class Message {', + ' public:', + ' const void* GetDescriptor() const { return nullptr; }', + '};', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/compiler/cpp/message.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'namespace compiler {', + 'namespace cpp {', + 'class MessageGenerator {', + ' public:', + ' const void* descriptor() const { return descriptor_; }', + ' private:', + ' const void* descriptor_;', + '};', + '} // namespace cpp', + '} // namespace compiler', + '} // namespace protobuf', + '} // namespace google', + ), + // `descriptor.pb.h`, which declares FeatureSet and the options messages, is + // over the size cap; its `.cc` defines some of their methods out of line. + 'src/google/protobuf/descriptor.pb.cc': lines( + '#include "google/protobuf/descriptor.pb.h"', + 'namespace google {', + 'namespace protobuf {', + 'void FeatureSet::Clear() {}', + '} // namespace protobuf', + '} // namespace google', + ), +}; + +describe('a C++ call through a class the index does not hold', () => { + it('reproduction: `FeatureSet::descriptor()` is no JavaFeatures_NestInFileClassFeature::descriptor', async () => { + const cg = await indexed({ + ...PROTOBUF, + 'src/google/protobuf/compiler/code_generator.cc': lines( + '#include "google/protobuf/compiler/code_generator.h"', + '#include "google/protobuf/descriptor.pb.h"', + 'namespace google {', + 'namespace protobuf {', + 'namespace compiler {', + 'const void* FeatureSetDescriptor() {', + ' return FeatureSet::descriptor();', + '}', + '} // namespace compiler', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::compiler::FeatureSetDescriptor')).toEqual([]); + const decoy = callable(cg, 'pb::JavaFeatures_NestInFileClassFeature::descriptor'); + expect(cg.getCallers(decoy.id).filter((r) => r.edge.kind === 'calls')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('the Python extension\'s options and source-info defaults, and a message a using-declaration names', async () => { + const cg = await indexed({ + ...PROTOBUF, + 'python/google/protobuf/pyext/descriptor.cc': lines( + '#include "google/protobuf/descriptor.pb.h"', + 'namespace google {', + 'namespace protobuf {', + 'namespace python {', + 'const void* Defaults() {', + ' const void* a = &OneofOptions::default_instance();', + ' const void* b = &ServiceOptions::default_instance();', + ' const void* c = &SourceCodeInfo::default_instance();', + ' return FeatureSetDefaults::GetDescriptor() ? a : b ? b : c;', + '}', + '} // namespace python', + '} // namespace protobuf', + '} // namespace google', + ), + 'editions/generated_files_test.cc': lines( + '#include "editions/golden/editions_default.pb.h"', + 'using ::protobuf_editions_test::EditionsDefaultMessage;', + 'namespace google {', + 'namespace protobuf {', + 'const void* DefaultDescriptor() {', + ' return EditionsDefaultMessage::descriptor();', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::python::Defaults')).toEqual([]); + expect(calls(cg, 'google::protobuf::DefaultDescriptor')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a method the class defines out of line is still found by its qualified name', async () => { + const cg = await indexed({ + ...PROTOBUF, + 'src/google/protobuf/descriptor.cc': lines( + '#include "google/protobuf/descriptor.pb.h"', + 'namespace google {', + 'namespace protobuf {', + 'void ResetFeatures() {', + ' FeatureSet::Clear();', + ' FeatureSet::descriptor();', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::ResetFeatures')).toEqual(['google::protobuf::FeatureSet::Clear']); + } finally { + cg.close(); + } + }); +}); + +describe('qualified calls the index can place are untouched', () => { + it('a project class, and a nested class from inside its owner', async () => { + const cg = await indexed({ + 'arena.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'class Arena {', + ' public:', + ' static void* Create(int n) { return nullptr; }', + '};', + 'class ArenaStringPtr {', + ' public:', + ' static void* Create(int n) { return nullptr; }', + '};', + 'class Outer {', + ' public:', + ' class Inner {', + ' public:', + ' static int Count() { return 0; }', + ' };', + ' int Total() { return Inner::Count(); }', + '};', + 'class InnerCounter {', + ' public:', + ' static int Count() { return 1; }', + '};', + '} // namespace protobuf', + '} // namespace google', + ), + 'arena.cc': lines( + '#include "arena.h"', + 'namespace google {', + 'namespace protobuf {', + 'void* Make() { return Arena::Create(1); }', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::Make')).toEqual(['google::protobuf::Arena::Create']); + expect(calls(cg, 'google::protobuf::Outer::Total')).toEqual(['google::protobuf::Outer::Inner::Count']); + } finally { + cg.close(); + } + }); + + it('a project namespace, a namespace alias, and abseil', async () => { + const cg = await indexed({ + 'util/strings.h': lines( + 'namespace Util {', + 'inline int Trim(int x) { return x; }', + '} // namespace Util', + 'namespace proto {', + 'namespace wire {', + 'inline int Format(int x) { return x; }', + '} // namespace wire', + '} // namespace proto', + ), + 'util/use.cc': lines( + '#include "util/strings.h"', + '#include "absl/strings/str_cat.h"', + 'namespace Wire = proto::wire;', + 'int Use(int x) {', + ' return Util::Trim(x) + Wire::Format(x) + absl::StrCat(x);', + '}', + ), + }); + try { + expect(calls(cg, 'Use')).toEqual(['Util::Trim', 'proto::wire::Format']); + } finally { + cg.close(); + } + }); +}); + +/** rocksdb's ribbon solutions: an alias named after one shares a word with it. */ +const RIBBON = { + 'util/ribbon_impl.h': lines( + 'namespace rocksdb {', + 'namespace ribbon {', + 'template ', + 'class InMemSimpleSolution {', + ' public:', + ' static int RoundUpNumSlots(int n) { return n; }', + '};', + 'template ', + 'class SerializableInterleavedSolution {', + ' public:', + ' static int RoundUpNumSlots(int n) { return n; }', + '};', + '} // namespace ribbon', + '} // namespace rocksdb', + '#define IMPORT_RIBBON_IMPL_TYPES(TypesAndSettings) \\', + ' using SimpleSoln = \\', + ' rocksdb::ribbon::InMemSimpleSolution; \\', + ' using InterleavedSoln = \\', + ' rocksdb::ribbon::SerializableInterleavedSolution; \\', + ' static_assert(sizeof(SimpleSoln) > 0, "semicolon expected after macro call")', + ), +}; + +describe('a name the calling file makes something itself', () => { + it('an alias a macro the file expands declares (rocksdb\'s IMPORT_RIBBON_IMPL_TYPES)', async () => { + const cg = await indexed({ + ...RIBBON, + 'util/ribbon_test.cc': lines( + '#include "util/ribbon_impl.h"', + 'template ', + 'int RoundUp(int n) {', + ' IMPORT_RIBBON_IMPL_TYPES(TypeParam);', + ' return SimpleSoln::RoundUpNumSlots(n) + InterleavedSoln::RoundUpNumSlots(n);', + '}', + ), + }); + try { + expect(calls(cg, 'RoundUp')).toEqual([ + 'rocksdb::ribbon::InMemSimpleSolution::RoundUpNumSlots', + 'rocksdb::ribbon::SerializableInterleavedSolution::RoundUpNumSlots', + ]); + } finally { + cg.close(); + } + }); + + it('an alias the calling function declares with `using` or `typedef`, and one the index holds', async () => { + const cg = await indexed({ + ...RIBBON, + 'util/ribbon_alias.h': lines( + '#include "util/ribbon_impl.h"', + 'namespace rocksdb {', + 'using InterleavedSoln = ribbon::SerializableInterleavedSolution;', + '} // namespace rocksdb', + ), + 'util/ribbon_bench.cc': lines( + '#include "util/ribbon_alias.h"', + 'namespace rocksdb {', + 'int Slots(int n) {', + ' using SimpleSoln = ribbon::InMemSimpleSolution;', + ' return SimpleSoln::RoundUpNumSlots(n) + InterleavedSoln::RoundUpNumSlots(n);', + '}', + 'int OldStyleSlots(int n) {', + ' typedef ribbon::InMemSimpleSolution SimpleSolnT;', + ' return SimpleSolnT::RoundUpNumSlots(n);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Slots')).toEqual([ + 'rocksdb::ribbon::InMemSimpleSolution::RoundUpNumSlots', + 'rocksdb::ribbon::SerializableInterleavedSolution::RoundUpNumSlots', + ]); + expect(calls(cg, 'rocksdb::OldStyleSlots')).toEqual(['rocksdb::ribbon::InMemSimpleSolution::RoundUpNumSlots']); + } finally { + cg.close(); + } + }); + + it('a template parameter with a default argument (fmt\'s `Traits::count()`)', async () => { + const cg = await indexed({ + 'include/fmt/base.h': lines( + 'namespace fmt {', + 'namespace detail {', + 'class buffer_traits {', + ' public:', + ' static int count() { return 0; }', + '};', + 'class fixed_buffer {', + ' public:', + ' static int count() { return 0; }', + '};', + 'template ', + 'class iterator_buffer : public Traits {', + ' public:', + ' int count() const { return Traits::count() + 1; }', + '};', + '} // namespace detail', + '} // namespace fmt', + ), + }); + try { + expect(calls(cg, 'fmt::detail::iterator_buffer::count')).toEqual(['fmt::detail::buffer_traits::count']); + } finally { + cg.close(); + } + }); + + it('googletest\'s `TestFixture` in a typed test', async () => { + const cg = await indexed({ + 'upb/util/required_fields_test.cc': lines( + '#include ', + 'template ', + 'class RequiredFieldsTest : public testing::Test {', + ' public:', + ' void CheckRequired(const char* json) {}', + '};', + 'class FieldsChecker {', + ' public:', + ' void CheckRequired(const char* json) {}', + '};', + 'TYPED_TEST_SUITE(RequiredFieldsTest, MyTypes);', + 'TYPED_TEST(RequiredFieldsTest, Empty) {', + ' TestFixture::CheckRequired("{}");', + '}', + ), + }); + try { + const target = callable(cg, 'RequiredFieldsTest::CheckRequired'); + expect(callsFrom(cg, 'upb/util/required_fields_test.cc')).toEqual([`CheckRequired @ ${target.filePath}:${target.startLine}`]); + } finally { + cg.close(); + } + }); +}); + +describe('the one method of a name in the project', () => { + it('is still taken, as before: protobuf\'s MapTestPeer, declared in a `.inc` file that is not read as C++', async () => { + const cg = await indexed({ + 'src/google/protobuf/map.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'namespace internal {', + 'class KeyMapBase {', + ' public:', + ' static unsigned long CalculateHiCutoff(unsigned long num_buckets) { return num_buckets * 3 / 4; }', + '};', + '} // namespace internal', + '} // namespace protobuf', + '} // namespace google', + ), + // `map_test.inc` declares MapTestPeer, whose CalculateHiCutoff hands on to KeyMapBase's. + 'src/google/protobuf/map_test.cc': lines( + '#include "google/protobuf/map.h"', + '#include "google/protobuf/map_test.inc"', + 'namespace google {', + 'namespace protobuf {', + 'namespace internal {', + 'unsigned long HiCutoff(unsigned long capacity) {', + ' return MapTestPeer::CalculateHiCutoff(capacity);', + '}', + '} // namespace internal', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + const edges = cg.getCallees(callable(cg, 'google::protobuf::internal::HiCutoff').id).filter((r) => r.edge.kind === 'calls'); + expect(edges.map((r) => r.node.qualifiedName)).toEqual(['google::protobuf::internal::KeyMapBase::CalculateHiCutoff']); + expect(edges[0]!.edge.metadata?.resolvedBy).toBe('instance-method'); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/cpp-type-aliases.ts b/src/resolution/cpp-type-aliases.ts index 740da793..da410e2c 100644 --- a/src/resolution/cpp-type-aliases.ts +++ b/src/resolution/cpp-type-aliases.ts @@ -383,12 +383,81 @@ function collectTemplateHeader(lines: readonly string[], startLine: number, star // Args>` / `PROTOBUF_NDEBUG_INLINE static T* Create(…)`. const header = at < 0 ? null : /^template\s*<([\s\S]*)>(?:\s*(?:\[\[[^\]]*\]\]|(?:static|inline|constexpr|consteval|virtual|explicit|friend|extern)\b|[A-Z][A-Z0-9]*_[A-Z0-9_]*\b(?:\s*\([^()]*\))?))*\s*$/.exec(before.slice(at)); if (!header) return; - for (const item of splitTopLevel(header[1]!)) { - // `typename T = int`, `template class Policy`, `typename... Ts`, `int N`. - const declared = stripCppTemplateArguments(item).split('=')[0]!; - const name = /([A-Za-z_]\w*)\s*$/.exec(declared)?.[1]; - if (name && name !== 'typename' && name !== 'class') names.add(name); + for (const item of splitTopLevel(header[1]!)) addTemplateParameter(item, names); +} + +/** Add the name one parameter of a `template <…>` header declares. */ +function addTemplateParameter(item: string, names: Set): void { + // `typename T = int`, `template class Policy`, `typename... Ts`, `int N`. + const declared = stripCppTemplateArguments(item).split('=')[0]!; + const name = /([A-Za-z_]\w*)\s*$/.exec(declared)?.[1]; + if (name && name !== 'typename' && name !== 'class') names.add(name); +} + +/** How far a `template <…>` header may run; one that runs longer is not read. */ +const MAX_TEMPLATE_HEADER = 1000; + +/** The `>` closing the template parameter list opened at `open`, or -1 when a `;` or a brace comes first. */ +function templateHeaderEnd(text: string, open: number): number { + let depth = 0; + let parens = 0; + for (let i = open; i < text.length && i < open + MAX_TEMPLATE_HEADER; i++) { + const c = text[i]!; + if (c === '(' || c === '[') parens++; + else if (c === ')' || c === ']') parens = Math.max(0, parens - 1); + // `N > 0` in a default argument is written in parentheses. + else if (parens > 0) continue; + else if (c === '<') depth++; + else if (c === '>') { + if (--depth === 0) return i; + } else if (c === ';' || c === '{' || c === '}') return -1; } + return -1; +} + +/** + * The declarators of the `typedef` whose keyword ends at `from`, up to its + * `;`: those after the body of a type it defines (`typedef struct {…} X;`). + */ +function typedefDeclarators(text: string, from: number): string { + let depth = 0; + let start = from; + for (let i = from; i < text.length && i < from + 4 * MAX_TEMPLATE_HEADER; i++) { + const c = text[i]!; + if (c === '{') depth++; + else if (c === '}') { + if (--depth < 0) return ''; + if (depth === 0) start = i + 1; + } else if (c === ';' && depth === 0) return text.slice(start, i); + } + return ''; +} + +/** + * The names C++ source text declares as template parameters — every + * `template <…>` header's, `Traits` of `typename Traits = buffer_traits` — + * or as aliases: `using X = …;`, `typedef … X;`, `typedef struct {…} X;`, + * `typedef void (*X)(int);`. Function and macro bodies count, though the + * index has no node for what they declare. + */ +export function cppTypeNamesDeclared(text: string): Set { + const names = new Set(); + for (const m of text.matchAll(/\btemplate\s* open) for (const item of splitTopLevel(text.slice(open + 1, close))) addTemplateParameter(item, names); + } + for (const m of text.matchAll(/\busing\s+([A-Za-z_]\w*)\s*(?:\[\[[^\]]*\]\]\s*)?=(?!=)/g)) names.add(m[1]!); + for (const m of text.matchAll(/\btypedef\b/g)) { + for (const declarator of splitTopLevel(typedefDeclarators(text, m.index! + m[0].length))) { + // `(*Handler)` of a pointer to a function or member; else the last name, + // past a function type's parameters and an array's bounds. + const name = /\(\s*(?:[A-Za-z_]\w*\s*::\s*)*[*&^]+\s*(?:const\s+)?([A-Za-z_]\w*)\s*\)/.exec(declarator)?.[1] ?? + /([A-Za-z_]\w*)\s*(?:\[[^\]]*\]\s*)*$/.exec(declarator.replace(/\([^()]*\)\s*(?:const\s*)?$/, ''))?.[1]; + if (name) names.add(name); + } + } + return names; } /** diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 8fa0b3ab..be2ccc8a 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -14,7 +14,7 @@ import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-vis import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, inSameDartLibrary } from './dart-libraries'; import { isDartLocallyBound } from './dart-local-scope'; import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers'; -import { cppAliasedClass, cppAliasedTypeName, cppAliasedTypeWritten, cppDeclaresLocalAlias, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments, type CppAliasedType } from './cpp-type-aliases'; +import { cppAliasedClass, cppAliasedTypeName, cppAliasedTypeWritten, cppDeclaresLocalAlias, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeNamesDeclared, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments, type CppAliasedType } from './cpp-type-aliases'; import { clearCppNamespaceMemos, cppMacroNamespaceFrames, cppMacroNamespaceHeads, cppNameMacros, cppNamespaceAliases } from './cpp-namespaces'; import { clearCppRawStringMemos, cppLiteralsBlanked, cppNonCodeLines } from './cpp-raw-strings'; import { cppClassWritten, type CppNameSite } from './cpp-supertypes'; @@ -16155,6 +16155,81 @@ function isUndeclaredTypeName(typeName: string, ref: UnresolvedRef, context: Res !context.getNodesByName(typeName).some((n) => isMethodOwnerKind(n) && sameLanguageFamily(n.language, ref.language)); } +/** + * Is the qualifier of a C++ `X::m()` call a class the index doesn't hold, as + * protobuf's `FeatureSet` is (declared in `descriptor.pb.h`, which is over the + * indexer's size cap)? A capitalized name that no C or C++ type, alias or + * namespace node has, and that the calling file makes nothing of itself + * (cppNamesBroughtIn). + */ +function isCppUndeclaredQualifier(qualifier: string, ref: UnresolvedRef, context: ResolutionContext): boolean { + return isUndeclaredTypeName(qualifier, ref, context) && + cppTypesNamed(qualifier, context).length === 0 && + !context.getNodesByName(qualifier).some((n) => (n.language === 'cpp' || n.language === 'c') && (n.kind === 'namespace' || n.kind === 'module')) && + !cppNamesBroughtIn(ref.filePath, context).has(qualifier); +} + +const CPP_FILE_TYPE_NAMES = new WeakMap>>(); +const CPP_MACRO_TYPE_NAMES = new WeakMap>>(); + +/** + * The names a C++ file makes types or namespaces of itself, which the index + * holds no node for: its template parameters (fmt's `Traits` of `typename + * Traits = buffer_traits`), the aliases it declares — in a function body + * too, or by expanding a macro that declares them — and the namespaces it + * names: opened, aliased or used (`using namespace …;`). googletest's typed + * tests declare `TypeParam` and `TestFixture`. + */ +function cppNamesBroughtIn(file: string, context: ResolutionContext): ReadonlySet { + let memo = CPP_FILE_TYPE_NAMES.get(context); + if (!memo) CPP_FILE_TYPE_NAMES.set(context, (memo = new Map())); + const hit = memo.get(file); + if (hit) return hit; + const text = stripCommentsForRegex((context.readFile(file) ?? '').replace(/\\\r?\n/g, ' '), 'cpp'); + const names = cppTypeNamesDeclared(text); + for (const m of text.matchAll(/\bnamespace\s+([^;{}()]{1,200}?)\s*[;{]/g)) { + for (const name of m[1]!.match(/[A-Za-z_]\w*/g) ?? []) names.add(name); + } + for (const [macro, declared] of cppMacroTypeNames(context)) { + if (hasCppWord(text, macro)) for (const name of declared) names.add(name); + } + if (/\bTYPED_TEST(?:_P)?\s*\(/.test(text)) names.add('TypeParam').add('TestFixture'); + memo.set(file, names); + return names; +} + +/** + * The type names each of the project's macros declares where it is + * expanded: rocksdb's `IMPORT_RIBBON_IMPL_TYPES(T)` declares `using + * SimpleSoln = …;`, a vendored googletest's `TYPED_TEST` its `TestFixture`. + */ +function cppMacroTypeNames(context: ResolutionContext): Map> { + const hit = CPP_MACRO_TYPE_NAMES.get(context); + if (hit) return hit; + const macros = new Map>(); + for (const file of context.getAllFiles()) { + if (!/\.(?:h|hh|hpp|hxx|h\+\+|inl|inc|ipp|tcc)$/i.test(file)) continue; + const raw = context.readFile(file); + if (!raw || !raw.includes('define')) continue; + // Each `#define` with the lines it continues; only its body is stripped of comments. + for (const m of raw.matchAll(/^[ \t]*#[ \t]*define[ \t]+([A-Za-z_]\w*)(?:\([^)\n]*\))?((?:[^\n\\]|\\\r?\n|\\(?!\r?\n))*)/gm)) { + if (macros.has(m[1]!) || !/\b(?:using|typedef|template)\b/.test(m[2]!)) continue; + const names = cppTypeNamesDeclared(stripCommentsForRegex(m[2]!.replace(/\\\r?\n/g, ' '), 'cpp')); + if (names.size > 0) macros.set(m[1]!, names); + } + } + CPP_MACRO_TYPE_NAMES.set(context, macros); + return macros; +} + +/** Does `text` hold `word` as a whole identifier? */ +function hasCppWord(text: string, word: string): boolean { + for (let at = text.indexOf(word); at >= 0; at = text.indexOf(word, at + 1)) { + if (!/\w/.test(text[at - 1] ?? '') && !/\w/.test(text[at + word.length] ?? '')) return true; + } + return false; +} + function inferCppReceiverType( receiverName: string, ref: UnresolvedRef, @@ -17250,6 +17325,8 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { KOTLIN_FRAMES.delete(context); clearCppNamespaceMemos(context); clearCppRawStringMemos(context); + CPP_FILE_TYPE_NAMES.delete(context); + CPP_MACRO_TYPE_NAMES.delete(context); CPP_CLASSES_IN_FILE.delete(context); CPP_STEM_HEADERS.delete(context); TEST_SUITE_FILES.delete(context); @@ -19298,6 +19375,13 @@ export function matchMethodCall( // `$comment->delete()` — names the caller's own class only by a shared // word. The guess is the caller itself, so there is no guess. if (bestMatch && bestScore >= 2 && bestMatch.id !== ref.fromNodeId) { + // Nor is there one for a C++ `X::m()` through a class the index + // doesn't hold: a class whose name only shares a word with `X` is + // another class, since one named `X` was looked for first. + // protobuf's `FeatureSet::descriptor()` went to + // `JavaFeatures_NestInFileClassFeature::descriptor`, its Python + // extension's `OneofOptions::default_instance()` to `CppFileOptions`'. + if (ref.language === 'cpp' && match === colonMatch && isCppUndeclaredQualifier(objectOrClass!, ref, context)) return null; return { original: ref, targetNodeId: bestMatch.id,