fix(cpp): a receiver declared through a typedef or using alias calls the type it names (#2399)

google/leveldb's MemTable::Add calls `table_.Insert(buf)` on `Table table_;`,
declared next to `typedef SkipList<const char*, KeyComparator> Table;` in the
class. Receiver inference returned `Table`, no class `Table` has an `Insert`
(the one class `Table` is the unrelated leveldb::Table, and C++ type_alias
nodes record no aliased type), so the call fell through to the receiver-word
guess HandleTable::Insert. It now reaches SkipList::Insert.

cpp-type-aliases.ts reads the alias from source and looks it up the way C++
does: an alias declared earlier in the calling function, the caller's class
and its bases (read from the class's own base clause), then the namespaces
around it. A qualified name resolves a segment at a time (Table::Iterator is
SkipList::Iterator). Aliases of a template parameter, a dependent typename or
a decltype name no particular class. The constructor path uses the same
lookup, so `Table::Iterator iter(&table_);` constructs SkipList::Iterator and
an alias hides an outer class of its name.

When the aliased type lacks the method, a `.` call or a call through a
pointer on a receiver the caller itself declares gets no guessed edge. A `->`
through an iterator or smart pointer, a project class template (whose
specializations can add members) and declarations read from outside the
caller keep the previous behavior.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-07 01:02:35 +00:00
committed by GitHub
parent 9a274a4b47
commit 02d22aebed
6 changed files with 1239 additions and 14 deletions
+1
View File
@@ -65,6 +65,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- JSON files in a `templates` or `sections` folder are now read as Shopify Liquid only inside a Shopify theme, where the folder that contains `templates` or `sections` also has `layout/theme.liquid` or `config/settings_schema.json`. Before, all such JSON was, so a .NET project template's `template.json`, a schematic or a CMS's content showed up in the index as an empty Liquid file, and one that lists `sections` could be linked to an unrelated Liquid file of the same name. Shopify themes, including one kept in a subfolder of a repository, are indexed as before, and the next sync removes JSON that an earlier version indexed this way.
- In a repository that holds more than one Shopify theme, a theme's `{% render %}`, `{% include %}` and `{% section %}` tags and the sections its JSON templates list now link only to that theme's own snippets and sections, which is where Shopify looks for them. Before, a section or snippet the theme doesn't have was linked to another theme's file of the same name, and a theme kept in a subfolder could link to the root theme's copy instead of its own. A reference the theme can't satisfy now links to nothing. Projects with a single theme are indexed as before. Re-index projects with several Shopify themes after upgrading.
- In C, C++ and Objective-C, a struct or class member declared as a pointer, reference, array or function, like `SharedState* shared;`, `jv elements[];` or `virtual Status Put(…) = 0;`, no longer makes the struct look like it inherits from the member's type. Before, `codegraph_explore` and impact listed those types as base classes, every struct holding a pointer to a type as one of its subclasses, and a method of that struct as an override of the type's method with the same name. Real base classes like `class Derived : public Base`, Objective-C superclasses and Go's embedded fields are unchanged. Re-index C, C++ and Objective-C projects after upgrading.
- In C++, a call on a variable, parameter or member declared through a `typedef` or `using` alias now links to the method of the type the alias names, with the alias looked up the way C++ does it: in the calling function, its class and the classes that class inherits from, then the namespaces around it. In google/leveldb, `table_.Insert(buf)` on a `Table table_;` declared next to `typedef SkipList<const char*, KeyComparator> Table;` now reaches `SkipList::Insert` instead of the unrelated `HandleTable::Insert`, and `Table::Iterator iter(&table_);` reaches `SkipList::Iterator`'s constructor. A class that only shares the alias's name is no longer taken for it, and when the aliased type has no such method, as with an alias of `std::vector` or of a template parameter, no method is guessed from the receiver's name. Re-index C++ projects after upgrading.
- Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading.
- In Go, an interface that embeds another, like gin's `IRouter` embedding `IRoutes`, now links to it, so `codegraph_explore` and impact show the embedded interface as its base. Before, every embedded interface was dropped. An embedded field written through another package of your project, like `*refresh.Discovery` or `storage.Appender`, or with type arguments, like `List[int]`, now links too. One from the standard library or another module, like `io.Closer` or `sync.Mutex`, links to nothing rather than to a project type that shares its name, and a type-set constraint like `~int | ~float64` is not an embedding. An embedded type written without a package now always links to its own package's type: before, etcd's `Client`, which embeds its own `Lease` interface, was linked to a `Lease` struct in another package. Re-index Go projects after upgrading.
- In Angular templates, a property binding, an interpolation, a structural directive or a control-flow block that calls one of the component's own members, like `[name]="icon()"`, `{{ label() }}`, `*ngIf="isOpen()"` or `@if (loading()) {`, now links the component to that member, and so does reading a getter, like `[disabled]="!canSave"`, or handing a method to a child component, like `[displayWith]="displayFn"` or `trackBy: trackById`. Before, only event bindings such as `(click)="save()"` were read, so a signal, a `computed` value, a getter or a method used only from a template had no callers and looked unused. A pipe, a template variable, a call on another object such as `form.reset()`, and a plain field read like `[value]="title"` still link nothing. Re-index Angular projects after upgrading.
+555
View File
@@ -0,0 +1,555 @@
/**
* A C++ receiver declared through a `typedef` / `using` alias calls the type
* the alias names.
*
* google/leveldb's `MemTable::Add` calls `table_.Insert(buf)` on a member
* declared `Table table_;`, beside `typedef SkipList<const char*,
* KeyComparator> Table;` in the same class. Receiver inference read the
* declared type as `Table`; no class `Table` has an `Insert` (the one class
* `Table` is the unrelated `leveldb::Table`), and the C/C++ extractors record
* no aliased type on a `type_alias` node, so the call fell through to a guess
* by the receiver's name: `HandleTable::Insert` in util/cache.cc.
*
* The alias is now looked up the way C++ looks a name up: an alias declared
* earlier in the calling function, then the caller's class and the classes it
* inherits from, then the namespaces around it, innermost first, where a
* class of that name stops the search. The alias's declaration is read and
* its type followed, so the call reaches `SkipList::Insert`, a
* `Table::Iterator` declaration constructs `SkipList::Iterator`, and its
* methods resolve there. When the aliased type has no such method (an alias
* of `std::vector`, say) or the alias names a template parameter, no method
* is guessed by the receiver's name — unless the call goes through `->` on an
* iterator or smart pointer, which reaches a type the alias doesn't name, or
* the declaration was read from outside the calling function and its class.
*/
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 { cppTypeSegments } from '../src/resolution/cpp-type-aliases';
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-alias-'));
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 (file)`. */
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} (${r.node.filePath})`)
.sort();
}
/** google/leveldb's shapes, trimmed: the skip list, the unrelated `Table`, and the decoy `Insert`. */
const LEVELDB = {
'include/leveldb/table.h': [
'namespace leveldb {',
'class Table {',
' public:',
' explicit Table(int* rep);',
' int ApproximateOffsetOf(int key) const;',
'};',
'} // namespace leveldb',
'',
].join('\n'),
'util/cache.cc': [
'namespace leveldb {',
'class HandleTable {',
' public:',
' void Insert(int h) { last_ = h; }',
' private:',
' int last_;',
'};',
'} // namespace leveldb',
'',
].join('\n'),
'db/skiplist.h': [
'namespace leveldb {',
'template <typename Key, class Comparator>',
'class SkipList {',
' public:',
' explicit SkipList(Comparator cmp, int* arena);',
' void Insert(const Key& key);',
' class Iterator {',
' public:',
' explicit Iterator(const SkipList* list);',
' void Seek(const Key& target);',
' };',
'};',
'template <typename Key, class Comparator>',
'SkipList<Key, Comparator>::SkipList(Comparator cmp, int* arena) {}',
'template <typename Key, class Comparator>',
'void SkipList<Key, Comparator>::Insert(const Key& key) {}',
'template <typename Key, class Comparator>',
'SkipList<Key, Comparator>::Iterator::Iterator(const SkipList* list) {}',
'template <typename Key, class Comparator>',
'void SkipList<Key, Comparator>::Iterator::Seek(const Key& target) {}',
'} // namespace leveldb',
'',
].join('\n'),
'db/memtable.h': [
'#include "db/skiplist.h"',
'namespace leveldb {',
'struct KeyComparator {',
' int operator()(const char* a, const char* b) const;',
'};',
'class MemTable {',
' public:',
' void Add(const char* buf);',
' bool Get(const char* key);',
' void Rebuild(int* arena);',
' void Index(const char* key);',
' private:',
' typedef SkipList<const char*, KeyComparator> Table;',
' using Lookup = Table;',
' Table table_;',
' Lookup lookup_;',
'};',
'} // namespace leveldb',
'',
].join('\n'),
'db/memtable.cc': [
'#include "db/memtable.h"',
'namespace leveldb {',
'void MemTable::Add(const char* buf) {',
' table_.Insert(buf);',
'}',
'bool MemTable::Get(const char* key) {',
' Table::Iterator iter(&table_);',
' iter.Seek(key);',
' return true;',
'}',
'void MemTable::Rebuild(int* arena) {',
' KeyComparator cmp;',
' Table fresh(cmp, &arena[0]);',
' Table stale(&arena[1]);',
'}',
'void MemTable::Index(const char* key) {',
' lookup_.Insert(key);',
'}',
'} // namespace leveldb',
'',
].join('\n'),
};
describe('a C++ receiver declared through an alias', () => {
it('reproduction: `table_.Insert` reaches SkipList::Insert, not a class named after the receiver', async () => {
const cg = await indexed(LEVELDB);
try {
expect(calls(cg, 'leveldb::MemTable::Add')).toEqual(['leveldb::SkipList::Insert (db/skiplist.h)']);
expect(cg.getCallers(callable(cg, 'leveldb::HandleTable::Insert').id).filter((r) => r.edge.kind === 'calls')).toEqual([]);
} finally {
cg.close();
}
});
it('a nested type reached through the alias is constructed and called on the aliased type', async () => {
const cg = await indexed(LEVELDB);
try {
expect(calls(cg, 'leveldb::MemTable::Get')).toEqual([
'leveldb::SkipList::Iterator::Iterator (db/skiplist.h)',
'leveldb::SkipList::Iterator::Seek (db/skiplist.h)',
]);
} finally {
cg.close();
}
});
it('a local object of the alias type constructs the aliased type, never an outer class the alias hides', async () => {
const cg = await indexed(LEVELDB);
try {
// `Table fresh(cmp, &arena[0])` is SkipList's two-argument constructor;
// `Table stale(&arena[1])` fits no SkipList constructor, and
// `leveldb::Table(int*)` is not the `Table` this scope names.
expect(calls(cg, 'leveldb::MemTable::Rebuild')).toEqual(['leveldb::SkipList::SkipList (db/skiplist.h)']);
expect(cg.getCallers(callable(cg, 'leveldb::Table::Table').id).filter((r) => r.edge.kind === 'calls')).toEqual([]);
} finally {
cg.close();
}
});
it('follows an alias of an alias', async () => {
const cg = await indexed(LEVELDB);
try {
expect(calls(cg, 'leveldb::MemTable::Index')).toEqual(['leveldb::SkipList::Insert (db/skiplist.h)']);
} finally {
cg.close();
}
});
it('an alias spelled from outside its class, and one declared in the calling function', async () => {
const cg = await indexed({
...LEVELDB,
'db/builder.cc': [
'#include "db/memtable.h"',
'namespace leveldb {',
'void Fill(MemTable::Table* t) {',
' t->Insert("k");',
'}',
'void Build(int* arena) {',
' typedef SkipList<const char*, KeyComparator> List;',
' KeyComparator cmp;',
' List list(cmp, &arena[0]);',
' list.Insert("k");',
'}',
'} // namespace leveldb',
'',
].join('\n'),
});
try {
expect(calls(cg, 'leveldb::Fill')).toEqual(['leveldb::SkipList::Insert (db/skiplist.h)']);
expect(calls(cg, 'leveldb::Build')).toEqual([
'leveldb::SkipList::Insert (db/skiplist.h)',
'leveldb::SkipList::SkipList (db/skiplist.h)',
]);
} finally {
cg.close();
}
});
it('no guess by the receiver\'s name when the aliased type lacks the method', async () => {
const cg = await indexed({
...LEVELDB,
'include/leveldb/slice.h': [
'namespace leveldb {',
'class Slice {',
' public:',
' void clear() { size_ = 0; }',
' private:',
' int size_;',
'};',
'} // namespace leveldb',
'',
].join('\n'),
'db/version_set.h': [
'#include <vector>',
'namespace leveldb {',
'class VersionSet {',
' public:',
' void Clear();',
' private:',
' using Table = std::vector<int>;',
' Table files_;',
'};',
'} // namespace leveldb',
'',
].join('\n'),
'db/version_set.cc': [
'#include "db/version_set.h"',
'namespace leveldb {',
'void VersionSet::Clear() {',
' files_.clear();',
'}',
'} // namespace leveldb',
'',
].join('\n'),
});
try {
// `files_` is a std::vector: not Slice::clear, the one project `clear`.
expect(calls(cg, 'leveldb::VersionSet::Clear')).toEqual([]);
} finally {
cg.close();
}
});
it('nor when the call continues on the next line', async () => {
// protocolbuffers/protobuf's descriptor.cc writes `symbols_by_parent_`
// and `.insert(…)` on separate lines.
const cg = await indexed({
'src/descriptor.cc': [
'#include "absl/container/flat_hash_set.h"',
'namespace google { namespace protobuf {',
'class Arena {',
' public:',
' void insert(int block) {}',
'};',
'class FileDescriptorTables {',
' public:',
' bool AddAliasUnderParent(int symbol);',
' private:',
' typedef absl::flat_hash_set<int> SymbolsByParentSet;',
' SymbolsByParentSet symbols_by_parent_;',
'};',
'bool FileDescriptorTables::AddAliasUnderParent(int symbol) {',
' return symbols_by_parent_',
' .insert(symbol)',
' .second;',
'}',
'} }',
'',
].join('\n'),
});
try {
expect(calls(cg, 'google::protobuf::FileDescriptorTables::AddAliasUnderParent')).toEqual([]);
} finally {
cg.close();
}
});
it('a class declared in an inner scope hides an outer alias of the same name; outside it the alias applies', async () => {
const cg = await indexed({
'app/holder.h': [
'namespace app {',
'class Gadget {',
' public:',
' void Spin();',
'};',
'using Widget = Gadget;',
'class Holder {',
' public:',
' class Widget {',
' public:',
' void Spin();',
' };',
' void Use();',
' private:',
' Widget w_;',
'};',
'} // namespace app',
'',
].join('\n'),
'app/holder.cc': [
'#include "app/holder.h"',
'namespace app {',
'void Gadget::Spin() {}',
'void Holder::Widget::Spin() {}',
'void Holder::Use() {',
' w_.Spin();',
'}',
'void Twirl(Widget& w) {',
' w.Spin();',
'}',
'} // namespace app',
'',
].join('\n'),
});
try {
expect(calls(cg, 'app::Holder::Use')).toEqual(['app::Holder::Widget::Spin (app/holder.cc)']);
// Outside Holder, `Widget` is the namespace's alias of Gadget.
expect(calls(cg, 'app::Twirl')).toEqual(['app::Gadget::Spin (app/holder.cc)']);
} finally {
cg.close();
}
});
});
describe('an alias a class inherits', () => {
it('is found in the base class before an enclosing namespace, and a dependent alias is not followed', async () => {
// protocolbuffers/protobuf's json traits: `Field` in the derived traits is
// the base's `const FieldDescriptor*`, not the namespace's alias template.
const cg = await indexed({
'protobuf/descriptor.h': [
'namespace google { namespace protobuf {',
'class FieldDescriptor {',
' public:',
' bool is_repeated() const;',
'};',
'inline bool FieldDescriptor::is_repeated() const { return false; }',
'class OneofDescriptor {',
' public:',
' bool is_repeated() const { return false; }',
'};',
'} }',
'',
].join('\n'),
'protobuf/json/traits.h': [
'#include "protobuf/descriptor.h"',
'namespace google { namespace protobuf { namespace json_internal {',
'template <typename Traits>',
'using Field = typename Traits::Field;',
'struct Proto2Descriptor {',
' using Field = const FieldDescriptor*;',
'};',
'struct ParseProto2Descriptor : Proto2Descriptor {',
' static bool NewMsg(Field f) { return f->is_repeated(); }',
'};',
'} } }',
'',
].join('\n'),
});
try {
expect(calls(cg, 'google::protobuf::json_internal::ParseProto2Descriptor::NewMsg')).toEqual([
'google::protobuf::FieldDescriptor::is_repeated (protobuf/descriptor.h)',
]);
} finally {
cg.close();
}
});
});
describe('an alias of a template parameter', () => {
it('names no type, so a class that shares the alias\'s name is not it', async () => {
// protocolbuffers/protobuf: `GenericTypeHandler<GenericType>` declares
// `using Type = GenericType;`, and the generated `google.protobuf.Type`
// message is a class named `Type` with a `Clear`.
const cg = await indexed({
'protobuf/type.pb.h': [
'namespace google { namespace protobuf {',
'class Type {',
' public:',
' void Clear();',
'};',
'inline void Type::Clear() {}',
'} }',
'',
].join('\n'),
'protobuf/repeated_ptr_field.h': [
'namespace google { namespace protobuf { namespace internal {',
'template <typename GenericType>',
'class GenericTypeHandler {',
' public:',
' using Type = GenericType;',
' static void Reset(Type* value) { value->Clear(); }',
'};',
'} } }',
'',
].join('\n'),
});
try {
expect(calls(cg, 'google::protobuf::internal::GenericTypeHandler::Reset')).toEqual([]);
} finally {
cg.close();
}
});
});
describe('what does not rule a guess out', () => {
it('`->` through an alias of an iterator reaches the element type, which the alias does not name', async () => {
// google/googletest's gmock: `ExpectationSet::const_iterator` is a
// `std::set<Expectation>` iterator, and `it->` is an Expectation.
const cg = await indexed({
'gmock/expectation.h': [
'#include <set>',
'namespace testing {',
'class Expectation {',
' public:',
' int expectation_base() const { return 0; }',
'};',
'class ExpectationSet {',
' public:',
' typedef ::std::set<Expectation> Set;',
' typedef Set::const_iterator const_iterator;',
'};',
'int Describe(const ExpectationSet& set) {',
' ExpectationSet::const_iterator it;',
' return it->expectation_base();',
'}',
'} // namespace testing',
'',
].join('\n'),
});
try {
expect(calls(cg, 'testing::Describe')).toEqual(['testing::Expectation::expectation_base (gmock/expectation.h)']);
} finally {
cg.close();
}
});
it('a class template\'s specialization can declare a member the template itself does not', async () => {
// facebook/rocksdb's toku order-maintenance tree: `get_marked` is a member
// of `omt_node_templated<omtdata_t, true>` only.
const cg = await indexed({
'util/omt.h': [
'namespace toku {',
'namespace omt_internal {',
'template <typename omtdata_t, bool subtree_supports_marks>',
'class omt_node_templated {',
' public:',
' void clear_stolen_bits() {}',
'};',
'template <typename omtdata_t>',
'class omt_node_templated<omtdata_t, true> {',
' public:',
' bool get_marked() const { return false; }',
'};',
'} // namespace omt_internal',
'template <typename omtdata_t, bool supports_marks = false>',
'class omt {',
' public:',
' bool has_marks() const;',
' private:',
' typedef omt_internal::omt_node_templated<omtdata_t, supports_marks> omt_node;',
' omt_node root_;',
'};',
'template <typename omtdata_t, bool supports_marks>',
'bool omt<omtdata_t, supports_marks>::has_marks() const {',
' const omt_node &node = root_;',
' return node.get_marked();',
'}',
'} // namespace toku',
'',
].join('\n'),
});
try {
const callees = cg.getCallees(callable(cg, 'toku::omt::has_marks').id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.name);
expect(callees).toEqual(['get_marked']);
} finally {
cg.close();
}
});
it('a declaration read from another class is not known to be the receiver\'s', async () => {
// fmtlib/fmt's scan.h: `auto data = scan_data<T...>()` deduces nothing
// the reader can see, and the `data` it then finds is another class's
// `const scan_arg* data;`.
const cg = await indexed({
'test/scan.h': [
'namespace fmt {',
'class basic_scan_arg {',
' public:',
' int type() const { return 0; }',
'};',
'using scan_arg = basic_scan_arg;',
'class scan_args {',
' const scan_arg* data;',
'};',
'template <typename... T> class scan_data {',
' public:',
' int make_args() { return 0; }',
'};',
'template <typename... T> int scan() {',
' auto data = scan_data<T...>();',
' return data.make_args();',
'}',
'} // namespace fmt',
'',
].join('\n'),
});
try {
expect(calls(cg, 'fmt::scan')).toEqual(['fmt::scan_data::make_args (test/scan.h)']);
} finally {
cg.close();
}
});
});
describe('cppTypeSegments', () => {
it('reads a written type as its `::` segments, without qualifiers or template arguments', () => {
expect(cppTypeSegments('SkipList<const char*, KeyComparator>')).toEqual(['SkipList']);
expect(cppTypeSegments('const ::leveldb::SkipList<Key, Cmp<int>>::Iterator&')).toEqual(['leveldb', 'SkipList', 'Iterator']);
expect(cppTypeSegments('typename Base<T>::Iter')).toEqual(['Base', 'Iter']);
expect(cppTypeSegments('struct Node*')).toEqual(['Node']);
expect(cppTypeSegments('unsigned int')).toBeNull();
expect(cppTypeSegments('void (*)(int)')).toBeNull();
});
});
+20 -5
View File
@@ -7,6 +7,8 @@
* - the type is looked up in the LEXICAL namespaces of the call site — for
* `second::use()` writing `Widget w;`, `second::Widget` before a global
* `Widget`; a `::T` spelling is global only;
* - a type named through a `typedef` / `using` alias is the type the alias
* names, looked up from the alias's own scope (cpp-type-aliases.ts);
* - among that type's constructors, the single overload whose parameter
* count admits the argument count wins (defaults and `...` widen a range);
* two admitting overloads (`T(int)` / `T(double)` for `T w(x)`) resolve to
@@ -17,6 +19,7 @@
*/
import type { Node } from '../types';
import type { ResolvedRef, ResolutionContext, UnresolvedRef } from './types';
import { resolveCppAliasedType } from './cpp-type-aliases';
const CONSTRUCTOR_REF = /^(.*)::([^:]+)\/(\d+)$/;
@@ -80,14 +83,26 @@ function constructorShape(signature: string | undefined): { min: number; max: nu
export function matchCppConstructor(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
const match = ref.referenceName.match(CONSTRUCTOR_REF);
if (!match) return null;
const [, rawType, name, count] = match;
const type = rawType!.replace(/^::/, '');
if (type.split('::').pop() !== name) return null;
const [, rawType, ctorName, count] = match;
let type = rawType!.replace(/^::/, '');
if (type.split('::').pop() !== ctorName) return null;
const argc = Number(count);
// Innermost lexical namespace first, then outward, then global.
const caller = context.getNodeById?.(ref.fromNodeId);
const scopes = rawType!.startsWith('::') ? [] : (caller?.qualifiedName.split('::') ?? []);
let scopes = rawType!.startsWith('::') ? [] : (caller?.qualifiedName.split('::') ?? []);
// A type named through an alias (`Table::Iterator iter(&table_);` under
// `typedef SkipList<…> Table;`) constructs the type the alias names, looked
// up from where the alias wrote it — never an outer class the alias hides.
if (!rawType!.startsWith('::')) {
const aliased = resolveCppAliasedType(type, ref, context);
if (aliased === null) return null;
if (aliased) {
type = [...aliased.target, ...aliased.rest].join('::');
scopes = aliased.scope ? aliased.scope.split('::') : [];
}
}
const name = type.split('::').pop()!;
const candidates: string[] = [];
for (let i = scopes.length; i > 0; i--) candidates.push(`${scopes.slice(0, i).join('::')}::${type}`);
candidates.push(type);
@@ -98,7 +113,7 @@ export function matchCppConstructor(ref: UnresolvedRef, context: ResolutionConte
.filter((n) => n.language === 'cpp' && (n.kind === 'class' || n.kind === 'struct' || n.kind === 'union'));
if (owners.length === 0) continue;
const constructors = context
.getNodesByName(name!)
.getNodesByName(name)
.filter((n) => n.language === 'cpp' && n.kind === 'method' && n.qualifiedName === `${qualified}::${name}`);
// Brace-init prefers an initializer_list overload over arity — that
// choice needs the argument types, so decline.
+542
View File
@@ -0,0 +1,542 @@
/**
* C++ type aliases in a declared type: `typedef X Y;` and `using Y = X;`.
*
* Both extractors index an alias as a `type_alias` node and record nothing
* about the type it names, so a receiver declared through one used to be
* looked up by the alias's own name. google/leveldb's `MemTable::Add` calls
* `table_.Insert(buf)` on a member `Table table_;`, with
* `typedef SkipList<const char*, KeyComparator> Table;` declared beside it in
* the class. No class `Table` has an `Insert` (the one class `Table` is the
* unrelated `leveldb::Table`), so the call fell through to a guess by the
* receiver's name and reached `HandleTable::Insert`.
*
* A name is looked up the way C++ looks it up: from the innermost scope of
* the code that uses it outwards. That is an alias declared earlier in the
* calling function, then the caller's class and that class's bases, then the
* classes and namespaces around it, and the global scope. The first scope
* that declares the name decides; a class there is the type itself. An alias
* there is read from its own declaration and followed from the alias's own
* scope, for a few hops at most. A qualified name is resolved a segment at a
* time, so `Table::Iterator` is `SkipList::Iterator` and `MemTable::Table`
* is `SkipList`. Template arguments are dropped. An alias of what only a
* template argument decides (`using Type = GenericType;` in the template
* that declares `GenericType`, `typename Traits::Field`) or of a
* `decltype(…)` names no particular class, and is reported as such.
*/
import type { Node } from '../types';
import type { ResolutionContext, UnresolvedRef } from './types';
/** Kinds that declare a type name in a scope, besides an alias. */
const DECLARING_KINDS: ReadonlySet<string> = new Set([
'class', 'struct', 'union', 'enum', 'interface', 'namespace', 'module',
]);
const CLASS_KINDS: ReadonlySet<string> = new Set(['class', 'struct', 'union']);
/** How many aliases one name is followed through (`A` → `B` → `C`). */
const MAX_HOPS = 4;
/** How many base classes deep a member is looked for. */
const MAX_BASE_DEPTH = 4;
/** How far past a class's first line its base clause may run. */
const HEAD_LINES = 12;
/**
* What a type named through an alias resolved to: the aliased type as written
* in `scope` (`['SkipList']`), then the rest of the name that followed the
* alias (`['Iterator']` for `Table::Iterator`).
*/
export interface CppAliasedType {
target: string[];
rest: string[];
/** The qualified scope `target` is written in (`''` for the global scope). */
scope: string;
/** An alias on the way named a pointer (`using Field = const FieldDescriptor*;`). */
pointer: boolean;
/**
* The type is a class template of the project (or a class nested in one),
* whose specializations can declare members the primary template doesn't.
*/
classTemplate: boolean;
}
/** A written type's `::` segments, each with whether template arguments followed it. */
interface TypeName {
names: string[];
templated: boolean[];
}
/** `SkipList<const char*, Cmp<int>>::Iterator` → `SkipList::Iterator`. */
function stripTemplateArguments(text: string): string {
let out = '';
let depth = 0;
for (const c of text) {
if (c === '<') depth++;
else if (c === '>') depth = Math.max(0, depth - 1);
else if (depth === 0) out += c;
}
return out;
}
/** The `::` segments of a written type, or null when it is not a plain (possibly qualified) type name. */
function cppTypeName(raw: string): TypeName | null {
const parts: string[] = [];
const templated: boolean[] = [];
let depth = 0;
let part = '';
let args = false;
const text = raw.replace(/\b(?:const|volatile|mutable|typename|template|class|struct|union|enum)\b/g, ' ').replace(/[&*]+/g, ' ');
for (let i = 0; i < text.length; i++) {
const c = text[i]!;
if (c === '<') {
depth++;
args = true;
} else if (c === '>') depth = Math.max(0, depth - 1);
else if (depth > 0) continue;
else if (c === ':' && text[i + 1] === ':') {
parts.push(part.trim());
templated.push(args);
part = '';
args = false;
i++;
} else part += c;
}
parts.push(part.trim());
templated.push(args);
if (parts[0] === '') {
// A leading `::` names the global scope.
parts.shift();
templated.shift();
}
if (parts.length === 0 || !parts.every((p) => /^[A-Za-z_]\w*$/.test(p))) return null;
return { names: parts, templated };
}
/** Is a written type a pointer (`Table*`, `const Foo* const`), not counting its template arguments? */
export function isCppPointerType(raw: string): boolean {
return stripTemplateArguments(raw).includes('*');
}
/**
* The `::` segments of a written type: `const ns::Foo<T>&` → `['ns', 'Foo']`.
* Null when what is written is not a plain (possibly qualified) type name —
* `unsigned int`, `decltype(…)`, a function type.
*/
export function cppTypeSegments(raw: string): string[] | null {
return cppTypeName(raw)?.names ?? null;
}
/** C and C++ declare the names a C++ file can see (a header may be either). */
function isCFamily(n: Node): boolean {
return n.language === 'cpp' || n.language === 'c';
}
/** Scopes visible from inside the scope `qualified`, innermost first, ending with the global scope. */
function scopesWithin(qualified: string): string[] {
const parts = qualified ? qualified.split('::') : [];
const scopes: string[] = [];
for (let i = parts.length; i >= 0; i--) scopes.push(parts.slice(0, i).join('::'));
return scopes;
}
/** The scope a declaration sits in: `leveldb::MemTable` for `leveldb::MemTable::Table`. */
function parentScope(qualified: string): string {
const cut = qualified.lastIndexOf('::');
return cut < 0 ? '' : qualified.slice(0, cut);
}
/** The scope the code that makes `ref` is written in: its class for a method, itself for a class. */
function callerScope(caller: Node | null | undefined): string {
if (!caller || caller.kind === 'file') return '';
return DECLARING_KINDS.has(caller.kind) ? caller.qualifiedName : parentScope(caller.qualifiedName);
}
function linesOf(filePath: string, context: ResolutionContext): string[] | null {
return context.getFileLines?.(filePath) ?? context.readFile(filePath)?.split(/\r?\n/) ?? null;
}
const escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
/** The text of `node`'s declaration, cut to its columns when they fit the lines. */
function declarationText(node: Node, context: ResolutionContext): string | null {
const lines = linesOf(node.filePath, context);
if (!lines) return null;
const span = lines.slice(node.startLine - 1, node.endLine ?? node.startLine);
if (span.length === 0) return null;
const whole = span.join('\n');
if (span.length === 1) {
span[0] = span[0]!.slice(node.startColumn, node.endColumn);
} else {
span[0] = span[0]!.slice(node.startColumn);
span[span.length - 1] = span[span.length - 1]!.slice(0, node.endColumn);
}
const cut = span.join('\n');
return /\b(?:typedef|using)\b/.test(cut) ? cut : whole;
}
/** Split `text` on commas outside `<>`, `()`, `[]` and `{}`. */
function splitTopLevel(text: string): string[] {
const parts: string[] = [];
let depth = 0;
let start = 0;
for (let i = 0; i < text.length; i++) {
const c = text[i]!;
if ('<([{'.includes(c)) depth++;
else if ('>)]}'.includes(c)) depth = Math.max(0, depth - 1);
else if (c === ',' && depth === 0) {
parts.push(text.slice(start, i));
start = i + 1;
}
}
parts.push(text.slice(start));
return parts;
}
/**
* What an alias of `name` declares in `text`, as written: `SkipList<const
* char*, KeyComparator>` for `typedef SkipList<const char*, KeyComparator>
* Table;` and for `using Table = SkipList<const char*, KeyComparator>;`.
* Null when `text` declares no alias of that name.
*/
function aliasedTypeIn(text: string, name: string): string | null {
const escaped = escapeRegExp(name);
const using = new RegExp(String.raw`\busing\s+${escaped}\s*(?:\[\[[^\]]*\]\]\s*)?=\s*([^;]+)`).exec(text);
if (using) return using[1]!.trim();
const typedef = /\btypedef\b([^;]+)/.exec(text);
if (!typedef) return null;
// `typedef Foo* FooPtr, FooObj;` indexes its first declarator; the type is
// what precedes that declarator's name.
const declared = new RegExp(String.raw`^([\s\S]*?)\b${escaped}\s*$`).exec(splitTopLevel(typedef[1]!)[0]!);
return declared?.[1]?.trim() || null;
}
/** Is `alias` an alias template (`template <typename T> using Vec = std::vector<T>;`)? */
function isAliasTemplate(alias: Node, context: ResolutionContext): boolean {
const lines = linesOf(alias.filePath, context);
if (!lines) return false;
const before = [
...lines.slice(Math.max(0, alias.startLine - 4), alias.startLine - 1),
(lines[alias.startLine - 1] ?? '').slice(0, alias.startColumn),
].join(' ');
return /\btemplate\s*<[^;{}]*>\s*$/.test(before);
}
const LOCAL_ALIAS_LINES = new WeakMap<ResolutionContext, Map<string, number[]>>();
/** The lines of a function's body that declare an alias (`typedef …;`, `using X = …;`), last first. */
function localAliasLines(caller: Node, lines: readonly string[], context: ResolutionContext): number[] {
let memo = LOCAL_ALIAS_LINES.get(context);
if (!memo) LOCAL_ALIAS_LINES.set(context, (memo = new Map()));
let found = memo.get(caller.id);
if (!found) {
found = [];
const last = Math.min(caller.endLine ?? caller.startLine, lines.length);
for (let i = last - 1; i >= caller.startLine - 1 && i >= 0; i--) {
if (/\b(?:typedef\b|using\s+\w+\s*=)/.test(lines[i]!)) found.push(i);
}
memo.set(caller.id, found);
}
return found;
}
/**
* The type an alias declared earlier in the calling function names, as
* written. Those aliases are not indexed, so the function's own lines are
* read, from the call back to the function's first line.
*/
function localAliasedType(name: string, caller: Node | null | undefined, ref: UnresolvedRef, context: ResolutionContext): string | null | undefined {
if (!caller || (caller.kind !== 'function' && caller.kind !== 'method') || caller.filePath !== ref.filePath) return undefined;
const lines = linesOf(ref.filePath, context);
if (!lines) return undefined;
const declares = new RegExp(String.raw`\b(?:using\s+${escapeRegExp(name)}\s*=|typedef\b[^;]*\b${escapeRegExp(name)}\s*[;,])`);
for (const i of localAliasLines(caller, lines, context)) {
if (i > ref.line - 1) continue;
const line = lines[i]!;
if (line.includes(name) && declares.test(line)) return aliasedTypeIn(line, name);
}
return undefined;
}
/** What a scope declares under a name. */
type Declared =
| { alias: Node }
/** A class, struct or union, by its qualified name. */
| { cls: string }
/** An enum, namespace or other non-class type. */
| 'other'
/** Aliases naming different types, none of them the calling file's. */
| 'ambiguous'
| undefined;
/** C and C++ declarations named `name` that can stand for a type or scope. */
function typeDeclarations(name: string, context: ResolutionContext): Node[] {
return context.getNodesByName(name).filter((n) => isCFamily(n) && (n.kind === 'type_alias' || DECLARING_KINDS.has(n.kind)));
}
function declaredIn(
scope: string,
name: string,
named: readonly Node[],
templated: boolean,
ref: UnresolvedRef,
context: ResolutionContext,
): Declared {
const qualified = scope ? `${scope}::${name}` : name;
let here = named.filter((n) => n.qualifiedName === qualified);
// An alias template is only ever named with template arguments.
if (!templated) here = here.filter((n) => n.kind !== 'type_alias' || !isAliasTemplate(n, context));
if (here.length === 0) return undefined;
// `typedef struct Node Node;` declares the struct as well.
if (here.some((n) => CLASS_KINDS.has(n.kind))) return { cls: qualified };
if (here.some((n) => n.kind !== 'type_alias')) return 'other';
const alias = pickAlias(here, ref, context);
return alias ? { alias } : 'ambiguous';
}
/**
* One of several aliases a scope declares under one name (a translation unit
* sees one of them): the one in the calling file, then in its header, then
* any of them when they all name the same type.
*/
function pickAlias(aliases: Node[], ref: UnresolvedRef, context: ResolutionContext): Node | null {
if (aliases.length === 1) return aliases[0]!;
const sameFile = aliases.filter((a) => a.filePath === ref.filePath);
if (sameFile.length === 1) return sameFile[0]!;
const stem = (p: string): string => p.replace(/\.[^./\\]+$/, '');
const header = aliases.filter((a) => stem(a.filePath) === stem(ref.filePath));
if (header.length === 1) return header[0]!;
const named = new Set(aliases.map((a) => {
const text = declarationText(a, context);
const type = text ? aliasedTypeIn(text, a.name) : null;
return type ? (cppTypeSegments(type)?.join('::') ?? type) : null;
}));
return named.size === 1 && !named.has(null) ? aliases[0]! : null;
}
const BASES = new WeakMap<ResolutionContext, Map<string, string[]>>();
const TEMPLATE_PARAMETERS = new WeakMap<ResolutionContext, Map<string, ReadonlySet<string>>>();
/** Drop the memos of class bases, template parameters and local aliases (see ReferenceResolver.clearCaches). */
export function clearCppTypeAliasMemos(context: ResolutionContext): void {
BASES.delete(context);
TEMPLATE_PARAMETERS.delete(context);
LOCAL_ALIAS_LINES.delete(context);
}
/** Add the parameter names of the `template <…>` header that ends right where a declaration starts. */
function collectTemplateHeader(lines: readonly string[], startLine: number, startColumn: number, names: Set<string>): void {
const before = [...lines.slice(Math.max(0, startLine - 4), startLine - 1), (lines[startLine - 1] ?? '').slice(0, startColumn)].join('\n');
const at = before.lastIndexOf('template');
const header = at < 0 ? null : /^template\s*<([\s\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 = stripTemplateArguments(item).split('=')[0]!;
const name = /([A-Za-z_]\w*)\s*$/.exec(declared)?.[1];
if (name && name !== 'typename' && name !== 'class') names.add(name);
}
}
/**
* The template parameters in scope where `node` is declared: its own
* `template <…>` header and those of the class templates around it.
*/
function templateParametersAround(node: Node, context: ResolutionContext): ReadonlySet<string> {
let memo = TEMPLATE_PARAMETERS.get(context);
if (!memo) TEMPLATE_PARAMETERS.set(context, (memo = new Map()));
const hit = memo.get(node.id);
if (hit) return hit;
const names = new Set<string>();
const lines = linesOf(node.filePath, context);
if (lines) {
collectTemplateHeader(lines, node.startLine, node.startColumn, names);
for (let scope = parentScope(node.qualifiedName); scope; scope = parentScope(scope)) {
for (const cls of context.getNodesByQualifiedName(scope)) {
if (!isCFamily(cls) || !CLASS_KINDS.has(cls.kind) || cls.filePath !== node.filePath) continue;
if (node.startLine < cls.startLine || node.startLine > (cls.endLine ?? cls.startLine)) continue;
collectTemplateHeader(lines, cls.startLine, cls.startColumn, names);
}
}
}
memo.set(node.id, names);
return names;
}
/**
* The qualified names of the classes `cls` derives from, read from the base
* clause of its own declaration (`struct ParseProto2Descriptor :
* Proto2Descriptor {`) and looked up from the scope the class is declared in.
*/
function baseClassesOf(cls: string, context: ResolutionContext): string[] {
let memo = BASES.get(context);
if (!memo) BASES.set(context, (memo = new Map()));
const hit = memo.get(cls);
if (hit) return hit;
memo.set(cls, []);
const bases: string[] = [];
for (const decl of context.getNodesByQualifiedName(cls)) {
if (!isCFamily(decl) || !CLASS_KINDS.has(decl.kind)) continue;
const lines = linesOf(decl.filePath, context);
if (!lines) continue;
const head = lines.slice(decl.startLine - 1, decl.startLine - 1 + HEAD_LINES);
if (head.length === 0) continue;
head[0] = head[0]!.slice(decl.startColumn);
const text = head.join('\n');
const brace = text.indexOf('{');
// A forward declaration (`class Foo;`) has no base clause of its own.
if (brace < 0 || text.slice(0, brace).includes(';')) continue;
const colon = text.slice(0, brace).search(/(?<!:):(?!:)/);
if (colon < 0) continue;
for (const part of splitTopLevel(text.slice(colon + 1, brace))) {
const name = cppTypeName(part.replace(/\b(?:public|protected|private|virtual)\b/g, ' '));
if (!name) continue;
const base = classNamed(name.names, scopesWithin(parentScope(cls)), context);
if (base && base !== cls && !bases.includes(base)) bases.push(base);
}
}
memo.set(cls, bases);
return bases;
}
/** The qualified name of the class `names` spells, looked up from `scopes`. */
function classNamed(names: readonly string[], scopes: readonly string[], context: ResolutionContext): string | undefined {
const spelled = names.join('::');
for (const scope of scopes) {
const qualified = scope ? `${scope}::${spelled}` : spelled;
if (context.getNodesByQualifiedName(qualified).some((n) => isCFamily(n) && CLASS_KINDS.has(n.kind))) return qualified;
}
return undefined;
}
/** `name` declared in class `cls` or one of its bases. */
function memberOf(
cls: string,
name: string,
named: readonly Node[],
templated: boolean,
ref: UnresolvedRef,
context: ResolutionContext,
depth = 0,
seen: Set<string> = new Set(),
): Declared {
const own = declaredIn(cls, name, named, templated, ref, context);
if (own !== undefined || depth >= MAX_BASE_DEPTH) return own;
seen.add(cls);
for (const base of baseClassesOf(cls, context)) {
if (seen.has(base)) continue;
const inherited = memberOf(base, name, named, templated, ref, context, depth + 1, seen);
if (inherited !== undefined) return inherited;
}
return undefined;
}
/** `name` looked up unqualified from `scopes`, innermost first; a class scope includes its bases. */
function lookupUnqualified(name: string, templated: boolean, scopes: readonly string[], ref: UnresolvedRef, context: ResolutionContext): Declared {
const named = typeDeclarations(name, context);
if (named.length === 0) return undefined;
for (const scope of scopes) {
const found = scope ? memberOf(scope, name, named, templated, ref, context) : declaredIn('', name, named, templated, ref, context);
if (found !== undefined) return found;
}
return undefined;
}
/**
* The type a C++ declared type names once the aliases in it are followed,
* looked up from the code that makes `ref`.
* - `undefined`: the type is not an alias; the caller keeps its own reading.
* - `null`: an alias that names no particular class: a template
* parameter's type (`using Type = GenericType;`, `typename
* Traits::Field`), a `decltype(…)` or a function type, or a chain of
* aliases too long to follow.
*/
export function resolveCppAliasedType(rawType: string, ref: UnresolvedRef, context: ResolutionContext): CppAliasedType | null | undefined {
const name = cppTypeName(rawType);
if (!name) return undefined;
const caller = context.getNodeById?.(ref.fromNodeId);
const scope = callerScope(caller);
const local = localAliasedType(name.names[0]!, caller, ref, context);
const resolved = local !== undefined
? expand(local, name.names.slice(1), name.templated.slice(1), scope, ref, context, 1,
caller ? templateParametersAround(caller, context) : undefined)
: follow(name, scope, ref, context, 0);
// `Table* table` is a pointer whatever `Table` names.
return resolved && isCppPointerType(rawType) ? { ...resolved, pointer: true } : resolved;
}
/**
* The name receiver inference gives a type resolved through aliases: the
* aliased type's last segment (`SkipList`), as for any declared type, with
* the nested part of a name reached through the alias kept on it
* (`SkipList::Iterator`), since that owner is what names the nested type.
*/
export function cppAliasedTypeName(resolved: CppAliasedType): string {
return [resolved.target[resolved.target.length - 1]!, ...resolved.rest].join('::');
}
function follow(
name: TypeName,
scope: string,
ref: UnresolvedRef,
context: ResolutionContext,
hops: number,
): CppAliasedType | null | undefined {
const scopes = scopesWithin(scope);
let found = lookupUnqualified(name.names[0]!, name.templated[0]!, scopes, ref, context);
let next = 1;
if (found === undefined && name.names.length > 1) {
// Namespaces are not indexed: `detail::buffer_t` from inside `fmt` is
// found by its spelling in an enclosing scope, longest prefix first.
for (let i = name.names.length; i >= 2 && found === undefined; i--) {
const spelled = name.names.slice(0, i);
const named = typeDeclarations(spelled[spelled.length - 1]!, context);
for (const s of scopes) {
found = declaredIn(s ? `${s}::${spelled.slice(0, -1).join('::')}` : spelled.slice(0, -1).join('::'),
spelled[spelled.length - 1]!, named, name.templated[i - 1]!, ref, context);
if (found !== undefined) break;
}
next = i;
}
}
// `MemTable::Table`, `Table::Iterator`: one segment at a time.
while (found && typeof found === 'object' && 'cls' in found && next < name.names.length) {
found = memberOf(found.cls, name.names[next]!, typeDeclarations(name.names[next]!, context), name.templated[next]!, ref, context);
next++;
}
if (!found || typeof found !== 'object' || !('alias' in found)) return undefined;
const alias = found.alias;
if (hops >= MAX_HOPS) return null;
const text = declarationText(alias, context);
// The aliased type is written in the alias's own scope.
return expand(text ? aliasedTypeIn(text, alias.name) : null, name.names.slice(next), name.templated.slice(next),
parentScope(alias.qualifiedName), ref, context, hops + 1, templateParametersAround(alias, context));
}
/**
* Continue from the type an alias names, as written (`rest` followed the
* alias in the name; `parameters` are the template parameters where it is
* written).
*/
function expand(
written: string | null,
rest: readonly string[],
restTemplated: readonly boolean[],
scope: string,
ref: UnresolvedRef,
context: ResolutionContext,
hops: number,
parameters?: ReadonlySet<string>,
): CppAliasedType | null {
// `typename Traits::Field`, `PolicyTemplate<const T>` inside the template
// that declares `PolicyTemplate`: whatever a template argument makes it.
if (!written || /\btypename\b/.test(written)) return null;
const target = cppTypeName(written);
if (!target || parameters?.has(target.names[0]!)) return null;
const pointer = isCppPointerType(written);
const names = [...target.names, ...rest];
const further = follow({ names, templated: [...target.templated, ...restTemplated] }, scope, ref, context, hops);
if (further === null) return null;
if (further) return { ...further, pointer: further.pointer || pointer };
const cls = classNamed(names, scopesWithin(scope), context);
const classTemplate = !!cls && context.getNodesByQualifiedName(cls)
.some((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && templateParametersAround(n, context).size > 0);
return { target: target.names, rest: [...rest], scope, pointer, classTemplate };
}
+2
View File
@@ -29,6 +29,7 @@ import { clearDartLibraryMemos } from './dart-libraries';
import { clearVbnetReceiverMemos, isVbMemberRead, isVbPathCall, matchVbMemberRead, matchVbPathCall } from './vbnet-receivers';
import { gateTypeParameter, clearTypeParameterMemos } from './type-parameters';
import { gateDartLocal, clearDartLocalScopeMemos } from './dart-local-scope';
import { clearCppTypeAliasMemos } from './cpp-type-aliases';
import { matchShopifyThemeFile } from './shopify-themes';
import { resolveViaImport, resolvePhpImportedStaticCall, resolvePhpQualifiedClassRef, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, isDartImportRef, isLuaRequireRef, isJsPathImportRef, isBoundToOutOfRepoImport, clearImportResolverMemos, resolveImportPath, isExternalImport } from './import-resolver';
import { ResolverPool, minRefsForPool, shouldEngageAdaptively } from './resolver-pool';
@@ -476,6 +477,7 @@ export class ReferenceResolver {
clearVbnetReceiverMemos(this.context);
clearTypeParameterMemos(this.context);
clearDartLocalScopeMemos(this.context);
clearCppTypeAliasMemos(this.context);
}
}
+119 -9
View File
@@ -14,6 +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 { cppAliasedTypeName, isCppPointerType, resolveCppAliasedType } from './cpp-type-aliases';
import { isTestPath } from '../search/query-utils';
import { isMinifiedContent } from '../extraction/generated-detection';
import { getCargoWorkspaceCrateMap } from './frameworks/cargo-workspace';
@@ -8250,11 +8251,102 @@ function buildDeclaratorRegex(escapedReceiver: string): RegExp {
);
}
/** What C++ receiver inference made of a receiver's declared type. */
interface CppReceiverDeclaration {
/**
* The declared type is an alias: `followed` to the type it names, or
* `unreadable` when that type is a template parameter's (`using Type =
* GenericType;`, `typename Traits::Field`) or a `decltype(…)` — no
* particular class.
*/
aliased?: 'followed' | 'unreadable';
/** The type as written in the declaration, without following an alias (`Table`). */
written?: string;
/** The type is reached through a pointer (`Table* t`, `using Field = const FieldDescriptor*;`). */
pointer?: boolean;
/** The type is one of the project's class templates, whose specializations may add members. */
classTemplate?: boolean;
}
/**
* A declared C++ type as receiver inference reads it: its own last segment,
* or, when it is an alias the caller's own declaration uses, the type the
* alias names (cpp-type-aliases.ts) — null when that type can't be known. An
* alias is never a class, so its own name is never looked up as one: that is
* how protobuf's `Field f; f->number()` (`using Field = const
* FieldDescriptor*;`) reached the generated `Field` message's `number`.
*/
function cppDeclaredType(
raw: string,
normalized: string,
inCallerScope: boolean,
ref: UnresolvedRef,
context: ResolutionContext,
found?: CppReceiverDeclaration,
): string | null {
// A declaration read from elsewhere (an earlier function, another class)
// may not be this receiver's, so it is taken as it was before aliases
// were followed.
if (!inCallerScope) return normalized;
const aliased = resolveCppAliasedType(raw, ref, context);
if (aliased === undefined) return normalized;
if (found) {
found.aliased = aliased ? 'followed' : 'unreadable';
found.written = normalized;
found.pointer = aliased ? aliased.pointer : isCppPointerType(raw);
found.classTemplate = aliased?.classTemplate ?? false;
}
return aliased ? cppAliasedTypeName(aliased) : null;
}
/**
* Is line `line` of `file` — where `text` declares a C++ receiver at column
* `at` — the calling function's own code, or a member declaration of the
* caller's class? Receiver inference reads back to the top of the file and
* through the header, so a declaration it finds elsewhere (an earlier
* function, another class in the header, a comment) may be another variable
* of the same name.
*/
function isCppCallersDeclaration(file: string, line: number, text: string, at: number, ref: UnresolvedRef, context: ResolutionContext): boolean {
if (/^\s*(?:\/\/|\/\*|\*)/.test(text)) return false;
const comment = text.indexOf('//');
if (comment >= 0 && comment < at) return false;
const caller = context.getNodeById?.(ref.fromNodeId);
if (!caller || (caller.kind !== 'method' && caller.kind !== 'function')) return false;
if (file === ref.filePath && line >= caller.startLine && line <= ref.line) return true;
const cut = caller.qualifiedName.lastIndexOf('::');
if (cut < 0) return false;
return context.getNodesByQualifiedName(caller.qualifiedName.slice(0, cut)).some((owner) =>
(owner.kind === 'class' || owner.kind === 'struct' || owner.kind === 'union') && owner.filePath === file &&
line >= owner.startLine && line <= (owner.endLine ?? owner.startLine));
}
/** Is the C++ member call at `ref` written `receiver.method(…)`, not through `->`? */
function isCppDotCall(receiver: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/);
const at = lines?.[ref.line - 1]?.slice(ref.column);
if (!lines || !at?.startsWith(receiver)) return false;
let rest = at.slice(receiver.length);
// `symbols_by_parent_` ending its line, `.insert(…)` starting the next.
for (let next = ref.line; !rest.trim() && next < Math.min(lines.length, ref.line + 3); next++) rest = lines[next]!;
return /^\s*\./.test(rest);
}
/**
* A type name that reads as a type (capitalized) yet names no class, struct
* or interface the project declares: one from outside the project.
*/
function isUndeclaredTypeName(typeName: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
return /^[A-Z]/.test(typeName) &&
!context.getNodesByName(typeName).some((n) => isMethodOwnerKind(n) && sameLanguageFamily(n.language, ref.language));
}
function inferCppReceiverType(
receiverName: string,
ref: UnresolvedRef,
context: ResolutionContext,
depth = 0,
found?: CppReceiverDeclaration,
): string | null {
// Per-file lines cache when available — this runs per `receiver->method()`
// ref and re-splitting the file each time is the same quadratic as the
@@ -8283,7 +8375,8 @@ function inferCppReceiverType(
if (initType) return initType;
// No usable initializer on this line — keep scanning earlier ones.
} else if (normalized) {
return normalized;
const inCallerScope = isCppCallersDeclaration(ref.filePath, i + 1, line, declaratorMatch.index ?? 0, ref, context);
return cppDeclaredType(declaratorMatch[1]!, normalized, inCallerScope, ref, context, found);
}
}
}
@@ -8301,12 +8394,16 @@ function inferCppReceiverType(
: (context.readFile(headerPath)?.split(/\r?\n/) ?? null);
if (!headerLines) continue;
for (const line of headerLines) {
for (let i = 0; i < headerLines.length; i++) {
const line = headerLines[i]!;
if (!receiverPattern.test(line)) continue;
const declaratorMatch = line.match(declaratorRegex);
if (!declaratorMatch) continue;
const normalized = normalizeCppTypeName(declaratorMatch[1] ?? '');
if (normalized && normalized !== 'auto') return normalized;
if (normalized && normalized !== 'auto') {
const inCallerScope = isCppCallersDeclaration(headerPath, i + 1, line, declaratorMatch.index ?? 0, ref, context);
return cppDeclaredType(declaratorMatch[1]!, normalized, inCallerScope, ref, context, found);
}
}
}
@@ -9957,9 +10054,10 @@ export function matchMethodCall(
if (typed !== undefined) return typed;
}
const decl: { raw?: string } = {};
const cppDecl: CppReceiverDeclaration = {};
let inferredType = nmTimedT('mc-infer', ref, () =>
ref.language === 'cpp'
? inferCppReceiverType(objectOrClass!, ref, context)
? inferCppReceiverType(objectOrClass!, ref, context, 0, cppDecl)
: inferLocalReceiverType(objectOrClass!, ref, context, decl));
// A pytest test's parameter is what its fixture returns: flaskbb's
// `cli_runner.invoke(…)` is click's `CliRunner`, not the project's one `invoke`.
@@ -10036,11 +10134,23 @@ export function matchMethodCall(
// list wrapper's `add`, commons-lang's `s.length()` to a writer's.
// (Only a type name — `java.util.List`, not a call chain like Python's
// `Device.objects.create(…)` the initializer pattern also captures.)
const typeName = inferredType.split('.').pop()!;
if (/^[A-Z]/.test(typeName) &&
!context.getNodesByName(typeName).some((n) => isMethodOwnerKind(n) && sameLanguageFamily(n.language, ref.language))) {
return null;
}
if (isUndeclaredTypeName(inferredType.split('.').pop()!, ref, context)) return null;
}
if (cppDecl.aliased) {
// A C++ receiver the calling function or its class declares through an
// alias has the type the alias names. When that type and its
// supertypes lack the method (`files_.clear()` through `using Files =
// std::vector<int>;`), or the alias names a template parameter's type,
// a method picked by the receiver's name below would be some other
// type's.
if (!cppDecl.classTemplate && (cppDecl.pointer || isCppDotCall(objectOrClass!, ref, context))) return null;
// Not so for `it->m()` on an iterator or smart pointer, whose `->`
// reaches an element type the alias doesn't name, nor for one of the
// project's class templates, whose specializations may declare members
// the template itself doesn't (rocksdb's `omt_node_templated<T, true>`
// adds `get_marked`): that call goes on as it did before aliases were
// followed.
if (isUndeclaredTypeName(cppDecl.written!, ref, context)) return null;
}
}