fix(cpp): a call through a class the index doesn't hold gets no guess by a shared word (#2729)

A C++ call written through a class, `X::m()`, whose class has no node in the
index fell through to the last strategy of matchMethodCall, which scores
every method named `m` by the words its class shares with `X` and picks one.
protocolbuffers/protobuf declares its descriptor messages in descriptor.pb.h,
which is over the indexer's size cap, so `FeatureSet::default_instance()`
linked to `UnknownFieldSet::default_instance`, `FeatureSetDefaults::GetDescriptor()`
to `JavaFeatures_NestInFileClassFeature::GetDescriptor`, and the options
messages' `default_instance()` to `pb::enumvalue::JsonEnumValueOptions`'. A
class whose name only shares a word with `X` is another class: one named `X`
was looked for first, by qualified name and by class name.

That guess is now dropped when `X` is a capitalized name (isUndeclaredTypeName)
that no C or C++ class, struct, union, enum, alias or namespace node has, and
that the calling file makes nothing of itself (cppNamesBroughtIn): not a
template parameter of any `template <…>` head in it, not an alias it declares
(a function-local `using FD = FieldDescriptor;`, a typedef, or one a macro it
expands declares, like rocksdb's IMPORT_RIBBON_IMPL_TYPES), not a namespace it
opens, aliases or uses, and not googletest's TypeParam or TestFixture in a
typed test. The one method of a name in the project is still taken as before.

The scans read the file once (cppTypeNamesDeclared in cpp-type-aliases.ts,
shared with the template-head reader) and the project's headers once for
macro bodies, only when a guess would otherwise be returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 05:11:50 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent accf600e47
commit 009e4377ff
4 changed files with 656 additions and 6 deletions
+1
View File
@@ -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<Result>;`) 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<User>, 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
+496
View File
@@ -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<string, string>): Promise<CodeGraph> {
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 TypesAndSettings>',
'class InMemSimpleSolution {',
' public:',
' static int RoundUpNumSlots(int n) { return n; }',
'};',
'template <class TypesAndSettings>',
'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<TypesAndSettings>; \\',
' using InterleavedSoln = \\',
' rocksdb::ribbon::SerializableInterleavedSolution<TypesAndSettings>; \\',
' 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 <typename TypeParam>',
'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<int>;',
'} // namespace rocksdb',
),
'util/ribbon_bench.cc': lines(
'#include "util/ribbon_alias.h"',
'namespace rocksdb {',
'int Slots(int n) {',
' using SimpleSoln = ribbon::InMemSimpleSolution<int>;',
' return SimpleSoln::RoundUpNumSlots(n) + InterleavedSoln::RoundUpNumSlots(n);',
'}',
'int OldStyleSlots(int n) {',
' typedef ribbon::InMemSimpleSolution<long> 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 <typename OutputIt, typename T,',
' typename Traits = buffer_traits>',
'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 <gtest/gtest.h>',
'template <typename T>',
'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();
}
});
});
+74 -5
View File
@@ -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 <typename> 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<string>): void {
// `typename T = int`, `template <typename> 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<string> {
const names = new Set<string>();
for (const m of text.matchAll(/\btemplate\s*</g)) {
const open = m.index! + m[0].length - 1;
const close = templateHeaderEnd(text, open);
if (close > 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;
}
/**
+85 -1
View File
@@ -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<ResolutionContext, Map<string, ReadonlySet<string>>>();
const CPP_MACRO_TYPE_NAMES = new WeakMap<ResolutionContext, Map<string, ReadonlySet<string>>>();
/**
* 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<string> {
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<string, ReadonlySet<string>> {
const hit = CPP_MACRO_TYPE_NAMES.get(context);
if (hit) return hit;
const macros = new Map<string, ReadonlySet<string>>();
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,