mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 11:24:49 +08:00
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:
co-authored by
Claude Opus 5.5
parent
accf600e47
commit
009e4377ff
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user