diff --git a/CHANGELOG.md b/CHANGELOG.md index 700436a78..fb52ae289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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. diff --git a/__tests__/cpp-type-alias-receiver.test.ts b/__tests__/cpp-type-alias-receiver.test.ts new file mode 100644 index 000000000..835b20361 --- /dev/null +++ b/__tests__/cpp-type-alias-receiver.test.ts @@ -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 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): Promise { + 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 ', + '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 ', + 'SkipList::SkipList(Comparator cmp, int* arena) {}', + 'template ', + 'void SkipList::Insert(const Key& key) {}', + 'template ', + 'SkipList::Iterator::Iterator(const SkipList* list) {}', + 'template ', + 'void SkipList::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 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 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 ', + 'namespace leveldb {', + 'class VersionSet {', + ' public:', + ' void Clear();', + ' private:', + ' using Table = std::vector;', + ' 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 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 ', + '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` 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 ', + '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` iterator, and `it->` is an Expectation. + const cg = await indexed({ + 'gmock/expectation.h': [ + '#include ', + 'namespace testing {', + 'class Expectation {', + ' public:', + ' int expectation_base() const { return 0; }', + '};', + 'class ExpectationSet {', + ' public:', + ' typedef ::std::set 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` only. + const cg = await indexed({ + 'util/omt.h': [ + 'namespace toku {', + 'namespace omt_internal {', + 'template ', + 'class omt_node_templated {', + ' public:', + ' void clear_stolen_bits() {}', + '};', + 'template ', + 'class omt_node_templated {', + ' public:', + ' bool get_marked() const { return false; }', + '};', + '} // namespace omt_internal', + 'template ', + 'class omt {', + ' public:', + ' bool has_marks() const;', + ' private:', + ' typedef omt_internal::omt_node_templated omt_node;', + ' omt_node root_;', + '};', + 'template ', + 'bool omt::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()` 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 class scan_data {', + ' public:', + ' int make_args() { return 0; }', + '};', + 'template int scan() {', + ' auto data = scan_data();', + ' 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')).toEqual(['SkipList']); + expect(cppTypeSegments('const ::leveldb::SkipList>::Iterator&')).toEqual(['leveldb', 'SkipList', 'Iterator']); + expect(cppTypeSegments('typename Base::Iter')).toEqual(['Base', 'Iter']); + expect(cppTypeSegments('struct Node*')).toEqual(['Node']); + expect(cppTypeSegments('unsigned int')).toBeNull(); + expect(cppTypeSegments('void (*)(int)')).toBeNull(); + }); +}); diff --git a/src/resolution/cpp-constructor.ts b/src/resolution/cpp-constructor.ts index 92d2f4d7a..30c756de0 100644 --- a/src/resolution/cpp-constructor.ts +++ b/src/resolution/cpp-constructor.ts @@ -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. diff --git a/src/resolution/cpp-type-aliases.ts b/src/resolution/cpp-type-aliases.ts new file mode 100644 index 000000000..82070e803 --- /dev/null +++ b/src/resolution/cpp-type-aliases.ts @@ -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 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 = new Set([ + 'class', 'struct', 'union', 'enum', 'interface', 'namespace', 'module', +]); +const CLASS_KINDS: ReadonlySet = 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>::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&` → `['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` for `typedef SkipList + * Table;` and for `using Table = SkipList;`. + * 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 using Vec = std::vector;`)? */ +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>(); + +/** 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>(); +const TEMPLATE_PARAMETERS = new WeakMap>>(); + +/** 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): 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 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 { + 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(); + 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(/(? 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 = 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, +): CppAliasedType | null { + // `typename Traits::Field`, `PolicyTemplate` 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 }; +} diff --git a/src/resolution/index.ts b/src/resolution/index.ts index bf0917ad7..7e6d2034a 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -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); } } diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 794d2af01..023826641 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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;`), 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` + // adds `get_marked`): that call goes on as it did before aliases were + // followed. + if (isUndeclaredTypeName(cppDecl.written!, ref, context)) return null; } }