From 6af7cf78f76e20571bb83353ae459790c49bc450 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Sun, 1 Nov 2026 07:52:08 -0600 Subject: [PATCH] fix(cpp): a call through a template parameter links to each class written as its argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Txn::PutEntity(…)` in `template class SecondaryIndexMixin` names a member of whatever class the template is given, so no lookup at the call can answer it. 1.6.2 reached three such calls on rocksdb by name coincidence, and the boundary fix that ended that rule left them failed. What decides the target is written elsewhere, in `new SecondaryIndexMixin(…)`, and with several instantiations each really runs its own, so every class gets an edge. A new pass, cppTemplateEdges, reads the failed references that go through a template parameter: `T::x`, `T::T/2`, what `T x;` constructs, an alias of a member of the parameter, and a call on a value of its type. It finds what the template is written with: base clauses, variables, members, `new`, aliases, explicit instantiations, `f()`, deduced `f(x)` where the argument's type is known exactly, gtest's type lists, and arguments that are another template's parameter, four templates at most. An argument is a project class by the lookup a declared type gets, a class local to the function first, in a file that sees it; anything else gives no edge and takes nothing from the rest. A specialization takes its instantiations from the template. A parameter that reaches more than 40 classes gets no edges: on seven repos that cut one reference, folly's OptionalStorage. The hop names the argument, the parameter and where it's written, in explore's Flow and the node trail. A walk over every file now has ResolutionContext.scanFile, which stays out of the content cache and skips UTF-8 decoding. Validation against main: all three rocksdb entries reached, 106 tests, 87 of them red with the pass off. Added edges: rocksdb 347, abseil 1,013, protobuf 969, folly 1,978, json 479, fmt 254, googletest 114, leveldb 3, gin and flask 0. Nodes, unresolved refs and every other edge are identical on all ten repos, with column and metadata. Pool, sequential and repeat runs give the same edges in the same order, and sync matches a fresh index on rocksdb, abseil and fmt. rocksdb index, 10 interleaved rounds: CPU 107.1 to 108.1 s mean, +1.0% (median +1.2%), peak RSS -0.5%, wall -0.1%. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 4 +- CHANGELOG.md | 1 + __tests__/cpp-template-instantiations.test.ts | 1249 +++++++++++++++++ src/db/queries.ts | 30 + src/graph/synthesized-hop.ts | 3 + src/resolution/callback-synthesizer.ts | 6 +- src/resolution/cpp-supertypes.ts | 4 +- src/resolution/cpp-template-instantiations.ts | 1133 +++++++++++++++ src/resolution/cpp-type-aliases.ts | 84 +- src/resolution/index.ts | 11 + src/resolution/name-matcher.ts | 16 +- src/resolution/types.ts | 6 + 12 files changed, 2531 insertions(+), 16 deletions(-) create mode 100644 __tests__/cpp-template-instantiations.test.ts create mode 100644 src/resolution/cpp-template-instantiations.ts diff --git a/AGENTS.md b/AGENTS.md index 88d0dfb26d..ef1ba06821 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,7 +78,7 @@ The public API surface is `src/index.ts` — the `CodeGraph` class wires all the **It must stay light to load:** a read command, a query worker and the prompt hook all load it, and never index. What only indexing needs (extraction, resolution, the watcher) goes through `engine()` (`src/lazy-engine.ts`, listed in `src/engine.ts`), never an import of its own; `__tests__/lazy-engine.test.ts` fails otherwise. - `src/db/` — `DatabaseConnection`, `QueryBuilder` (prepared statements), `schema.sql`, `sqlite-adapter.ts`. Backed by Node's built-in **`node:sqlite`** (`DatabaseSync`) — real SQLite with WAL + FTS5, exposed through a thin better-sqlite3-shaped adapter. The bundled runtime always ships Node ≥22.5, so `node:sqlite` is always available: **no native build step and no wasm fallback**. (Running from source needs Node ≥22.5.) `codegraph status` reports the live backend (`node-sqlite`, the sole backend). - `src/extraction/` — `ExtractionOrchestrator`, tree-sitter wrappers, per-language extractors under `languages/` (one file per language), plus standalone extractors for non-tree-sitter formats (`svelte-extractor.ts`, `vue-extractor.ts`, `liquid-extractor.ts`, `dfm-extractor.ts` for Delphi). `parse-worker.ts` runs heavy parsing off the main thread. -- `src/resolution/` — `ReferenceResolver` orchestrates `import-resolver.ts` (with `path-aliases.ts` for tsconfig path aliases + cargo workspace member globs), `name-matcher.ts`, and `frameworks/` (Express, Laravel, CodeIgniter 3, Rails, FastAPI, Django, Flask, Spring, Gin, Axum, ASP.NET, Vapor, React Router, Next.js — `nextjs.ts`: pages and `route.ts` handlers from files, `router.push` / `redirect` / `NextResponse.redirect` as `navigates` edges, with `next-router-synthesizer.ts` for `` and for the routes a proxy's `config.matcher` accepts, read by `next-proxy.ts` — Expo Router, SvelteKit, Vue/Nuxt, Cargo workspaces). Frameworks emit `route` nodes and `references` edges. `callback-synthesizer.ts` holds the whole-graph synthesis passes (`SYNTH_PASSES`, merged in registry order — first-seen wins a duplicate pair) with the language gates; `tier-synthesizer.ts` is the cross-tier pass (a client's literal `fetch`/`axios` path onto its own route, a queue job onto its consumer, a bus / socket event onto its handler — `channel`, `tier`, `registeredAt` on every edge; registered before the in-process emitter pass so its more specific edge wins); `synth-utils.ts` has the helpers they share (`enclosingFn`, `enclosingValue`, `makeLineAt`). Express's `postExtract` composes `app.use('/prefix', router)` mounts onto a mounted file's route names, idempotently (the original path stays in `qualifiedName`). +- `src/resolution/` — `ReferenceResolver` orchestrates `import-resolver.ts` (with `path-aliases.ts` for tsconfig path aliases + cargo workspace member globs), `name-matcher.ts`, and `frameworks/` (Express, Laravel, CodeIgniter 3, Rails, FastAPI, Django, Flask, Spring, Gin, Axum, ASP.NET, Vapor, React Router, Next.js — `nextjs.ts`: pages and `route.ts` handlers from files, `router.push` / `redirect` / `NextResponse.redirect` as `navigates` edges, with `next-router-synthesizer.ts` for `` and for the routes a proxy's `config.matcher` accepts, read by `next-proxy.ts` — Expo Router, SvelteKit, Vue/Nuxt, Cargo workspaces). Frameworks emit `route` nodes and `references` edges. `callback-synthesizer.ts` holds the whole-graph synthesis passes (`SYNTH_PASSES`, merged in registry order — first-seen wins a duplicate pair) with the language gates; `cpp-template-instantiations.ts` is the pass for a C++ call through a template parameter, onto each class the project writes as the argument (read from `Name`, `f()`, a deduced call or a gtest type list), capped per parameter; `tier-synthesizer.ts` is the cross-tier pass (a client's literal `fetch`/`axios` path onto its own route, a queue job onto its consumer, a bus / socket event onto its handler — `channel`, `tier`, `registeredAt` on every edge; registered before the in-process emitter pass so its more specific edge wins); `synth-utils.ts` has the helpers they share (`enclosingFn`, `enclosingValue`, `makeLineAt`). Express's `postExtract` composes `app.use('/prefix', router)` mounts onto a mounted file's route names, idempotently (the original path stays in `qualifiedName`). - `src/graph/` — `GraphTraverser` (BFS/DFS, impact radius, path finding) and `GraphQueryManager` (high-level queries), plus the shared query-time derivations more than one surface renders: `named-symbol-flow.ts` (the one path finder, behind `codegraph_explore`'s Flow section and the viewer's Flow strip), `dynamic-boundary-report.ts` (where the graph stops), `type-hierarchy.ts` (ancestors/subtypes and the implementation count explore prints and the viewer draws), `dead-code.ts` (unreferenced symbols, and every reason a candidate is NOT claimed), `synthesized-hop.ts` (how a synthesized hop reads, in explore's Flow, the `codegraph_node` trail and ContextBuilder's call paths — a new synthesizer's wording goes here), `file-cycles.ts` (circular imports, in `codegraph cycles` and the viewer's map). A derivation that two callers render must live here, not in `ToolHandler` — two derivations eventually disagree. - `src/context/` — `ContextBuilder` + formatter for markdown/JSON output. @@ -158,7 +158,7 @@ Two functions in `src/mcp/tools.ts` scale explore with indexed file count. This ### Dynamic-dispatch coverage — the flow must EXIST in the graph end-to-end -Static tree-sitter extraction misses computed/indirect calls, so flows break at dynamic dispatch and the agent reads to reconstruct them. Synthesizers/resolvers bridge these so `codegraph_explore` connects them end-to-end (`src/resolution/callback-synthesizer.ts`, `src/resolution/frameworks/`). Channels today: callback/observer, EventEmitter, **React re-render** (`setState`→`render`), **JSX child** (`render`→child component), **React Native native→JS events** (`sendEvent(withName:)` / JVM `emit` → the `addListener` handler, named or inline, `rn-event-channel`), django ORM descriptor. The JS→native direction is a *resolver* (`frameworks/react-native.ts`: `RCT_EXPORT_METHOD`, `RCT_EXTERN_MODULE` Swift shims, TurboModules), which trusts receiver evidence — an alias bound to `NativeModules.X` — over the import resolver. All synthesized edges are `provenance:'heuristic'` with `metadata.synthesizedBy` + `registeredAt` (the wiring site), surfaced inline in `codegraph_explore`'s Flow section and the `codegraph_node` trail. +Static tree-sitter extraction misses computed/indirect calls, so flows break at dynamic dispatch and the agent reads to reconstruct them. Synthesizers/resolvers bridge these so `codegraph_explore` connects them end-to-end (`src/resolution/callback-synthesizer.ts`, `src/resolution/frameworks/`). Channels today: callback/observer, EventEmitter, **React re-render** (`setState`→`render`), **JSX child** (`render`→child component), **React Native native→JS events** (`sendEvent(withName:)` / JVM `emit` → the `addListener` handler, named or inline, `rn-event-channel`), django ORM descriptor, **C++ template arguments** (`Txn::Put` or `shard.Clear()` in a template → that member of each class the project instantiates it with). The JS→native direction is a *resolver* (`frameworks/react-native.ts`: `RCT_EXPORT_METHOD`, `RCT_EXTERN_MODULE` Swift shims, TurboModules), which trusts receiver evidence — an alias bound to `NativeModules.X` — over the import resolver. All synthesized edges are `provenance:'heuristic'` with `metadata.synthesizedBy` + `registeredAt` (the wiring site), surfaced inline in `codegraph_explore`'s Flow section and the `codegraph_node` trail. **Principle: partial coverage is WORSE than none.** Bridging one boundary but not the next reveals a hop the agent then drills + reads to finish. Measured on excalidraw: react-render alone *raised* reads to 5–7; only completing the flow (adding the jsx-child hop) dropped it to 0–1. **Always close the flow end-to-end and re-measure** — never ship a half-bridged flow. diff --git a/CHANGELOG.md b/CHANGELOG.md index f9854bde30..7ace81b248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -260,6 +260,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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, C++ and Objective-C, a struct, class or union now links to the types its members are declared with, whether held by value, by pointer, in an array, through a function pointer or inside a template like `std::vector`, and a `typedef` or `using` alias links to the type it names, so impact on a type reaches every struct that holds one, like redis's `redisServer` for `pause_event`. A member's type is the one C++ name lookup finds from the class, so a builtin, a macro or a function of the same name links nothing, and a type a macro renames is what the macro names where the file sees it, like hiredis's `#define sds hisds` in redis-cli.c. Re-index C, C++ and Objective-C projects after upgrading. - In C and C++, a function-pointer table's handler, like `.read = impl_read` in an ops struct, no longer links to a Python or Rust function that only shares its name, and such a function no longer takes the place of the C function the table names. Re-index C and C++ projects that also hold Python or Rust after upgrading. Thanks @bompus. +- In C++, a call through a template parameter now links to that member of each class the project writes as the template's argument: `Txn::Put(…)` in a `template class Mixin`, `shard.Clear()` on a `CacheShard& shard`, a function template's `Traits::Make()`, or `TypeParam::Make()` in a googletest typed test. `codegraph_explore`, callers and impact now follow a flow through the template, and the hop says which argument it came from. A template written with a great many classes, such as a container, is left unlinked. - 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. An alias of a type written from the global scope names that global type, also inside a namespace of the same name: protocolbuffers/protobuf's `using Arena = ::upb::Arena;` in `hpb::internal::backend::upb`, so hpb's `arena_.Fuse(…)` on a `backend::Arena` now reaches `upb::Arena::Fuse` instead of the calling method itself. Re-index C++ projects after upgrading. - In C++, indexing no longer fails with "Maximum call stack size exceeded" on a call whose arguments call the same overloaded method, like darts-clone's `extras(extras(id).prev()).set_next(…)` on a class with a `const` and a non-`const` `extras(id)`. Choosing the overload read its arguments, which chose the same overload again without end, and the whole index run failed. Such a call now takes the first overload when the choice comes back to itself, as one whose argument types are unknown does. Thanks @JosefAschauer. - In C++, a call on a variable, parameter or member declared with template arguments nested inside its template arguments, like `autovector>` or `Striped>`, now links to the method of the class it is declared as. Before, the nested arguments hid that class: in RocksDB, `files_marked_for_compaction_.clear()` on such an `autovector` linked to an unrelated `CompactionInputFiles::clear` that only shares words with the variable's name, and `mutex_.Get(key)` on a `Striped` linked to nothing. Re-index C++ projects after upgrading. diff --git a/__tests__/cpp-template-instantiations.test.ts b/__tests__/cpp-template-instantiations.test.ts new file mode 100644 index 0000000000..673365d9f8 --- /dev/null +++ b/__tests__/cpp-template-instantiations.test.ts @@ -0,0 +1,1249 @@ +/** + * A C++ call through a template parameter links to that member of each class + * the project writes as the template's argument: rocksdb's `Txn::PutEntity(…)` + * in `template class SecondaryIndexMixin`, `Traits::ClearOutput` + * in a function template given two local `Traits`, and a typed test's + * `TypeParam::KeyGen`. No lookup at the call can answer these, so they are + * synthesized from the instantiations, one edge per 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 { describeSynthesizedHop } from '../src/graph/synthesized-hop'; +import { hasSynthesisPattern } from '../src/resolution/callback-synthesizer'; +import { CPP_TEMPLATE_ARGUMENT_CAP } from '../src/resolution/cpp-template-instantiations'; +import type { Edge } from '../src/types'; + +const roots: string[] = []; +const open: CodeGraph[] = []; +afterEach(() => { + for (const cg of open.splice(0)) cg.close(); + for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); +}); + +const lines = (...l: string[]): string => [...l, ''].join('\n'); + +function project(files: Record): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-template-')); + roots.push(root); + write(root, files); + return root; +} + +function write(root: string, files: Record): void { + 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); + } +} + +async function opened(root: string): Promise { + const cg = await CodeGraph.init(root, { index: true }); + open.push(cg); + return cg; +} + +const indexed = (files: Record): Promise => opened(project(files)); + +interface Row { + source: string; + target: string; + targetKind: string; + kind: string; + line: number; + via: string; + argument: string; + registeredAt: string; + provenance: string; + fnRef: number | null; +} + +/** Every edge the pass made, as text, in insertion order. */ +function templateEdges(cg: CodeGraph): Row[] { + const db = (cg as unknown as { db: { db: { prepare(sql: string): { all(): Row[] } } } }).db.db; + return db.prepare( + `SELECT s.qualified_name source, t.qualified_name target, t.kind targetKind, e.kind kind, e.line line, e.provenance provenance, + json_extract(e.metadata, '$.via') via, json_extract(e.metadata, '$.argument') argument, json_extract(e.metadata, '$.registeredAt') registeredAt, + json_extract(e.metadata, '$.fnRef') fnRef + FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target + WHERE json_extract(e.metadata, '$.synthesizedBy') = 'cpp-template-argument' ORDER BY e.id` + ).all(); +} + +/** The targets the pass linked from `source`, sorted. */ +function reached(cg: CodeGraph, source: string): string[] { + return templateEdges(cg).filter((e) => e.source === source).map((e) => e.target).sort(); +} + +const TXN = lines( + 'class BaseTxn {', + ' public:', + ' void Commit() {}', + '};', + 'class PlainTxn : public BaseTxn {', + ' public:', + ' void Put(int k) {}', + ' void Put(int k, int v) {}', + ' static void Make() {}', + '};', + 'class FancyTxn : public BaseTxn {', + ' public:', + ' void Put(int k) {}', + ' static void Make() {}', + '};', + 'class NoPutTxn {};', +); + +/** `Mixin` with one call through its parameter, in a header. */ +const MIXIN = lines( + '#include "txn.h"', + 'template ', + 'class Mixin : public Txn {', + ' public:', + ' void Add(int k) { Txn::Put(k); }', + ' void Finish() { Txn::Commit(); }', + ' static void Build() { Txn::Make(); }', + '};', +); + +const PUT_PLAIN = 'PlainTxn::Put'; + +describe('the forms of a written instantiation', () => { + const uses = async (use: string): Promise => indexed({ + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'use.cc': lines('#include "mixin.h"', use), + }); + + it('links to the argument of the one instantiation, the rocksdb SecondaryIndexMixin shape', async () => { + const cg = await uses('void Run() { auto* m = new Mixin(); m->Add(1); }'); + expect(reached(cg, 'Mixin::Add')).toEqual([PUT_PLAIN]); + expect(reached(cg, 'Mixin::Build')).toEqual(['PlainTxn::Make']); + const [edge] = templateEdges(cg).filter((e) => e.source === 'Mixin::Add'); + expect(edge).toMatchObject({ kind: 'calls', via: 'Txn', argument: 'PlainTxn', registeredAt: 'use.cc:2', provenance: 'heuristic', line: 5 }); + }); + + it('chooses the overload the call makes', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'pair.h': lines('#include "txn.h"', 'template ', 'struct Pair {', ' void Both(int a, int b) { T::Put(a, b); }', '};'), + 'use.cc': lines('#include "pair.h"', 'Pair pair;'), + }); + const [edge] = templateEdges(cg); + expect(edge?.target).toBe(PUT_PLAIN); + const db = (cg as unknown as { db: { db: { prepare(sql: string): { all(): Array<{ start_line: number }> } } } }).db.db; + const line = db.prepare(`SELECT n.start_line FROM edges e JOIN nodes n ON n.id = e.target WHERE json_extract(e.metadata, '$.synthesizedBy') = 'cpp-template-argument'`).all(); + expect(line).toEqual([{ start_line: 8 }]); + }); + + it.each([ + ['a base clause', 'class Logged : public Mixin {};'], + ['a variable', 'Mixin global;'], + ['a member', 'class Holder { Mixin held_; };'], + ['a pointer to it', 'Mixin* maybe = nullptr;'], + ['an argument of another template', 'std::vector> many;'], + ['a using alias', 'using Aliased = Mixin;'], + ['a typedef', 'typedef Mixin Aliased;'], + ['an explicit instantiation', 'template class Mixin;'], + ['an extern explicit instantiation', 'extern template class Mixin;'], + ['a static member call', 'void Run() { Mixin::Build(); }'], + ['the name written with a namespace', 'namespace other { ::Mixin global; }'], + ['a list broken over lines', 'Mixin<\n PlainTxn\n> global;'], + ])('reads %s', async (_form, use) => { + const cg = await uses(use); + expect(reached(cg, 'Mixin::Add')).toEqual([PUT_PLAIN]); + }); + + it('reads past the arrow of a trailing return type', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'bound.h': lines('#include "txn.h"', 'template ', 'struct Bound {', ' void Run() { T::Make(); }', '};'), + 'use.cc': lines('#include "bound.h"', 'Boundint, PlainTxn> bound;'), + }); + expect(reached(cg, 'Bound::Run')).toEqual(['PlainTxn::Make']); + }); + + it('links an explicit function template argument', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'run.cc': lines( + '#include "txn.h"', + 'template ', + 'void Build() { T::Make(); }', + 'void Run() {', + ' Build();', + ' Build();', + '}', + ), + }); + expect(reached(cg, 'Build')).toEqual(['FancyTxn::Make', 'PlainTxn::Make']); + expect(templateEdges(cg).map((e) => e.registeredAt).sort()).toEqual(['run.cc:5', 'run.cc:6']); + }); + + it('links a member function template and a class template method each to its own head', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'host.h': lines( + '#include "txn.h"', + 'template ', + 'class Host {', + ' public:', + ' template ', + ' void Both() { A::Make(); B::Make(); }', + '};', + ), + 'use.cc': lines('#include "host.h"', 'void Run() {', ' Host h;', ' h.Both();', '}'), + }); + expect(reached(cg, 'Host::Both')).toEqual(['FancyTxn::Make', 'PlainTxn::Make']); + }); + + it('reads the parameter of a member defined outside its class, by position', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'pair.h': lines( + '#include "txn.h"', + 'template ', + 'class Pair {', + ' public:', + ' void Go();', + ' template void More();', + '};', + 'template ', + 'void Pair::Go() { B::Make(); }', + 'template ', + 'template ', + 'void Pair::More() { A::Make(); C::Make(); }', + ), + 'use.cc': lines('#include "pair.h"', 'void Run() {', ' Pair<1, PlainTxn, FancyTxn> p;', ' p.More();', '}'), + }); + expect(reached(cg, 'Pair::Go')).toEqual(['FancyTxn::Make']); + expect(reached(cg, 'Pair::More')).toEqual(['PlainTxn::Make']); + }); + + it('reads the parameter of a class around the caller, and of a function around a local class', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'all.cc': lines( + '#include "txn.h"', + 'template ', + 'class Outer {', + ' public:', + ' class Inner {', + ' public:', + ' void Go() { T::Make(); }', + ' };', + '};', + 'Outer outer;', + 'template ', + 'void Run() {', + ' struct Local {', + ' void Go() { T::Make(); }', + ' };', + ' auto later = [] { T::Make(); };', + '}', + 'void Start() { Run(); }', + ), + }); + expect(reached(cg, 'Outer::Inner::Go')).toEqual(['PlainTxn::Make']); + expect(reached(cg, 'Run::Local::Go')).toEqual(['FancyTxn::Make']); + expect(reached(cg, 'Run')).toEqual(['FancyTxn::Make']); + }); + + it('reads the parameter of a nested class defined outside its template', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'list.h': lines( + '#include "txn.h"', + 'template ', + 'class List {', + ' public:', + ' struct Node;', + '};', + 'template ', + 'struct List::Node {', + ' void Go() { T::Make(); }', + '};', + ), + 'use.cc': lines('#include "list.h"', 'List list;'), + }); + expect(templateEdges(cg).map((e) => e.target)).toEqual(['PlainTxn::Make']); + }); + + it('reads a parameter pack', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'pack.h': lines('#include "txn.h"', 'template ', 'struct All {', ' static void Go() { (Ts::Make(), ...); }', '};'), + 'use.cc': lines('#include "pack.h"', 'All<1, PlainTxn, FancyTxn> all;'), + }); + expect(reached(cg, 'All::Go')).toEqual(['FancyTxn::Make', 'PlainTxn::Make']); + }); + + it('takes a default the instantiation does not write', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'def.h': lines('#include "txn.h"', 'template ', 'struct Def {', ' void Go() { B::Commit(); B::Put(1); }', '};'), + 'use.cc': lines('#include "def.h"', 'Def plain;', 'Def written;'), + }); + expect(reached(cg, 'Def::Go')).toEqual(['BaseTxn::Commit', 'FancyTxn::Put', PUT_PLAIN]); + }); + + it('looks the template up from a namespace, with no declaration around the site', async () => { + const cg = await indexed({ + 'all.h': lines( + 'namespace ns {', + 'struct Item { static void Go() {} };', + 'template struct Box { void Run() { T::Go(); } };', + '}', + ), + 'other.h': lines('namespace other {', 'struct Item { static void Go() {} };', '}'), + 'use.cc': lines('#include "all.h"', '#include "other.h"', 'namespace ns {', 'template class Box;', '}'), + }); + expect(reached(cg, 'ns::Box::Run')).toEqual(['ns::Item::Go']); + }); + + it('counts a namespace a macro opens', async () => { + const cg = await indexed({ + 'macros.h': lines('#define NS_BEGIN namespace ns {', '#define NS_END }'), + 'all.h': lines( + '#include "macros.h"', + 'NS_BEGIN', + 'struct Item { static void Go() {} };', + 'template struct Box { void Run() { T::Go(); } };', + 'NS_END', + ), + 'use.cc': lines('#include "all.h"', 'NS_BEGIN', 'template class Box;', 'NS_END'), + }); + expect(templateEdges(cg).map((e) => e.registeredAt)).toEqual(['use.cc:3']); + }); + + it('reads a function pointer to a member through a parameter', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'use.cc': lines( + '#include "txn.h"', + 'template ', + 'struct Each {', + ' void Go() { Run(&T::Make); }', + ' void Run(void (*fn)()) { fn(); }', + '};', + 'Each each;', + ), + }); + const [edge] = templateEdges(cg); + expect(edge).toMatchObject({ kind: 'references', source: 'Each::Go', target: 'PlainTxn::Make', fnRef: 1 }); + }); +}); + +describe('a call on a value of the parameter\'s type', () => { + const body = (code: string): Record => ({ + 'shards.h': lines( + 'struct Inner { void Deep() {} };', + 'struct FastShard { using Part = Inner; void Clear() {} };', + 'struct SlowShard { using Part = Inner; void Clear() {} };', + ), + 'cache.h': lines( + '#include "shards.h"', + 'template ', + 'class Cache {', + ' public:', + code, + ' private:', + ' Shard own_;', + ' Shard* many_;', + '};', + ), + 'use.cc': lines('#include "cache.h"', 'Cache fast;', 'Cache slow;'), + }); + const CLEARS = ['FastShard::Clear', 'SlowShard::Clear']; + + it.each([ + ['a parameter by reference', 'void Run(Shard& shard) { shard.Clear(); }'], + ['a parameter by const reference', 'void Run(const Shard& shard) { shard.Clear(); }'], + ['a parameter by pointer', 'void Run(Shard* shard) { shard->Clear(); }'], + ['a local', 'void Run() { Shard shard; shard.Clear(); }'], + ['a member', 'void Run() { own_.Clear(); }'], + ['a pointer member', 'void Run() { many_->Clear(); }'], + ])('reads %s', async (_form, code) => { + const cg = await indexed(body(code)); + expect(reached(cg, 'Cache::Run')).toEqual(CLEARS); + }); + + it('reads a member of the parameter\'s type, unless a local of another type hides it', async () => { + const hidden = await indexed(body('void Run() { Inner own_; own_.Clear(); }')); + expect(templateEdges(hidden)).toEqual([]); + }); + + it('reads a member type of the parameter', async () => { + const cg = await indexed(body('void Run() { typename Shard::Part part; part.Deep(); }')); + expect(reached(cg, 'Cache::Run')).toEqual(['Inner::Deep']); + }); + + it('reads an alias of a member type', async () => { + const cg = await indexed(body('using Part = typename Shard::Part;\n void Run(Part& part) { part.Deep(); }')); + expect(reached(cg, 'Cache::Run')).toEqual(['Inner::Deep']); + }); + + it('keeps out an arrow on a value: that is the argument\'s own operator', async () => { + const cg = await indexed(body('void Run(Shard& shard) { shard->Clear(); }')); + expect(templateEdges(cg)).toEqual([]); + }); + + it('keeps out a variable of another type', async () => { + const cg = await indexed(body('void Run(Other& shard) { shard.Clear(); }')); + expect(templateEdges(cg)).toEqual([]); + }); + + it('leaves a call a resolver links alone', async () => { + const cg = await indexed(body('void Run(Inner& inner) { inner.Deep(); }')); + expect(templateEdges(cg)).toEqual([]); + expect(cg.getCallees(cg.getNodesByKind('method').find((n) => n.qualifiedName === 'Cache::Run')!.id).map((r) => r.node.qualifiedName)).toEqual(['Inner::Deep']); + }); +}); + +describe('a deduced argument', () => { + const deduced = (use: string): Promise => indexed({ + 'txn.h': TXN, + 'run.cc': lines( + '#include "txn.h"', + 'template void ByValue(T x) { T::Make(); }', + 'template void ByRef(const T& x) { T::Make(); }', + 'template void ByPointer(T* x) { T::Make(); }', + 'template void ByHolder(std::vector* x) { T::Make(); }', + use, + ), + }); + + it('reads the type of a declared argument, whole', async () => { + const cg = await deduced('void Run() { PlainTxn p; ByValue(p); ByRef(p); }'); + expect(reached(cg, 'ByValue')).toEqual(['PlainTxn::Make']); + expect(reached(cg, 'ByRef')).toEqual(['PlainTxn::Make']); + }); + + it('reads what a pointer points at', async () => { + const cg = await deduced('void Run() { FancyTxn f; ByPointer(&f); FancyTxn* q = nullptr; ByPointer(q); }'); + expect(reached(cg, 'ByPointer')).toEqual(['FancyTxn::Make']); + }); + + it('follows an argument whose type is the caller\'s own parameter', async () => { + const cg = await deduced('template void Outer(U u) { ByRef(u); }\nvoid Run() { Outer(PlainTxn()); }'); + expect(reached(cg, 'ByRef')).toEqual(['PlainTxn::Make']); + }); + + it('gives no edge for a literal, an unknown name or a type the parameter only holds', async () => { + const cg = await deduced('void Run(int i) { ByValue(1); ByRef(nowhere); ByRef(i); std::vector v; ByHolder(&v); PlainTxn p; ByPointer(p); }'); + expect(templateEdges(cg)).toEqual([]); + }); + + it('keeps a call that gives a type beside one that gives a literal', async () => { + const cg = await deduced('void Run() { ByValue(1); PlainTxn p; ByValue(p); }'); + expect(reached(cg, 'ByValue')).toEqual(['PlainTxn::Make']); + }); + + it('takes the written argument over the deduced one', async () => { + const cg = await deduced('void Run() { PlainTxn p; ByValue(p); }'); + expect(reached(cg, 'ByValue')).toEqual(['FancyTxn::Make']); + }); +}); + +describe('an instantiation inside another template', () => { + it('follows the argument to the outer template, through a chain', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'chain.h': lines( + '#include "mixin.h"', + 'template ', + 'class Middle : public Mixin {};', + 'template ', + 'using Short = Middle;', + 'template ', + 'void Outer() { Short s; }', + ), + 'use.cc': lines('#include "chain.h"', 'void Run() { Outer(); }', 'Short direct;'), + }); + expect(reached(cg, 'Mixin::Add')).toEqual(['FancyTxn::Put', PUT_PLAIN]); + expect(templateEdges(cg).find((e) => e.target === PUT_PLAIN)?.registeredAt).toBe('use.cc:2'); + }); + + it('links no call to itself', async () => { + const cg = await indexed({ + 'all.cc': lines( + 'template ', + 'struct Base { static void Make() { D::Make(); } };', + 'struct Plain : public Base {};', + 'struct Own : public Base { static void Make() {} };', + ), + }); + expect(reached(cg, 'Base::Make')).toEqual(['Own::Make']); + }); + + it('follows an argument written const', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'inner.h': lines( + '#include "txn.h"', + 'template struct Inner { void Go() { T::Make(); } };', + 'template struct Outer { Inner held; };', + ), + 'use.cc': lines('#include "inner.h"', 'Outer outer;'), + }); + expect(reached(cg, 'Inner::Go')).toEqual(['PlainTxn::Make']); + }); + + it('stops at a cycle, keeping what the rest of the walk finds', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'loop.h': lines( + '#include "txn.h"', + 'template class B;', + 'template ', + 'class A {', + ' public:', + ' void Go() { T::Make(); }', + ' B* next_;', + '};', + 'template ', + 'class B {', + ' A* next_;', + ' A>* self_;', + '};', + ), + 'use.cc': lines('#include "loop.h"', 'B start;'), + }); + expect(reached(cg, 'A::Go')).toEqual(['PlainTxn::Make']); + }); + + it.each([ + [4, ['PlainTxn::Make']], + [5, []], + ])('follows an argument through %i templates at most, and stops short of a wrong answer', async (depth, targets) => { + const chain = Array.from({ length: depth }, (_, i) => lines(`template class C${i} : public C${i + 1} {};`)).reverse(); + const cg = await indexed({ + 'txn.h': TXN, + 'chain.h': lines( + '#include "txn.h"', + `template class C${depth} { public: void Go() { T::Make(); } };`, + ...chain, + ), + 'use.cc': lines('#include "chain.h"', 'C0 deep;'), + }); + expect(reached(cg, `C${depth}::Go`)).toEqual(targets); + }); +}); + +describe('gtest typed tests', () => { + const RIBBON = lines( + '#include ', + 'struct SmallGen { SmallGen(int a, int b) {} };', + 'struct BigGen { BigGen(int a, int b) {} };', + 'struct Base { using KeyGen = BigGen; static int Chance() { return 1; } };', + 'struct Small : public Base { using KeyGen = SmallGen; static int Chance() { return 2; } };', + 'struct Other : public Base {};', + 'template ', + 'class RibbonTest : public ::testing::Test {};', + 'using Listed = ::testing::Types<', + ' Small,', + ' Other>;', + 'TYPED_TEST_SUITE(RibbonTest, Listed);', + 'TYPED_TEST(RibbonTest, Works) {', + ' using KeyGen = typename TypeParam::KeyGen;', + ' KeyGen one(1, 2);', + ' int c = TypeParam::Chance();', + '}', + ); + + it('links TypeParam to each listed type, inherited or its own', async () => { + const cg = await indexed({ 'ribbon_test.cc': RIBBON }); + const edges = templateEdges(cg); + expect(edges.filter((e) => e.kind === 'calls' && e.target.endsWith('Chance')).map((e) => e.target).sort()).toEqual(['Base::Chance', 'Small::Chance']); + expect(edges.filter((e) => e.kind === 'instantiates').map((e) => e.target).sort()).toEqual(['Base::KeyGen', 'Small::KeyGen']); + expect(edges.filter((e) => e.kind === 'instantiates').every((e) => e.targetKind === 'type_alias')).toBe(true); + expect(edges.find((e) => e.target === 'Small::KeyGen')).toMatchObject({ via: 'TypeParam', argument: 'Small', registeredAt: 'ribbon_test.cc:10' }); + }); + + it('links the constructor of what each member alias names', async () => { + const cg = await indexed({ 'ribbon_test.cc': RIBBON }); + expect(templateEdges(cg).filter((e) => /Gen$/.test(e.target) && e.targetKind === 'method').map((e) => e.target).sort()) + .toEqual(['BigGen::BigGen', 'SmallGen::SmallGen']); + }); + + it.each([ + ['TYPED_TEST_CASE', 'TYPED_TEST_CASE(T1, Listed);', 'TYPED_TEST(T1, Works)'], + ['a list written in the macro', 'TYPED_TEST_SUITE(T1, ::testing::Types);', 'TYPED_TEST(T1, Works)'], + ['a typedef of the list', 'typedef ::testing::Types Spare; TYPED_TEST_SUITE(T1, Spare);', 'TYPED_TEST(T1, Works)'], + ['a parameterized suite', 'INSTANTIATE_TYPED_TEST_SUITE_P(Pre, T1, Listed);', 'TYPED_TEST_P(T1, Works)'], + ['the older parameterized macro', 'INSTANTIATE_TYPED_TEST_CASE_P(Pre, T1, Listed);', 'TYPED_TEST_P(T1, Works)'], + ])('reads %s', async (_form, suite, test) => { + const cg = await indexed({ + 'a_test.cc': lines( + '#include ', + 'struct A { static int Go() { return 1; } };', + 'template class T1 : public ::testing::Test { public: int Own() { return T::Go(); } };', + 'using Listed = ::testing::Types;', + suite, + `${test} { TypeParam::Go(); }`, + ), + }); + const targets = templateEdges(cg).map((e) => e.target); + expect(targets).toContain('A::Go'); + expect(reached(cg, 'T1::Own')).toEqual(['A::Go']); + }); + + it('reads a typed suite whose tests live in a header', async () => { + const cg = await indexed({ + 'suite.h': lines( + 'template class Suite : public ::testing::Test {};', + 'TYPED_TEST_SUITE_P(Suite);', + 'TYPED_TEST_P(Suite, Works) { TypeParam::Go(); }', + ), + 'a_test.cc': lines( + '#include "suite.h"', + 'struct A { static int Go() { return 1; } };', + 'using Listed = ::testing::Types;', + 'INSTANTIATE_TYPED_TEST_SUITE_P(My, Suite, Listed);', + ), + }); + expect(templateEdges(cg).map((e) => e.target)).toEqual(['A::Go']); + }); + + it('follows TypeParam into a helper the test instantiates', async () => { + const cg = await indexed({ + 'a_test.cc': lines( + '#include ', + 'struct A { static int Go() { return 1; } };', + 'template void Helper() { T::Go(); }', + 'template class T1 : public ::testing::Test {};', + 'TYPED_TEST_SUITE(T1, ::testing::Types);', + 'TYPED_TEST(T1, Works) { Helper(); }', + ), + }); + expect(reached(cg, 'Helper')).toEqual(['A::Go']); + }); +}); + +describe('what an argument is', () => { + it('finds a class local to the function that writes it, one per function', async () => { + const cg = await indexed({ + 'batch.cc': lines( + 'namespace ns {', + 'class Batch {', + ' public:', + ' template void Impl();', + ' void Get();', + ' void GetEntity();', + '};', + 'template ', + 'void Batch::Impl() { Traits::Clear(); }', + 'void Batch::Get() {', + ' struct Traits { static void Clear() {} };', + ' Impl();', + '}', + 'void Batch::GetEntity() {', + ' struct Traits { static void Clear() {} };', + ' Impl();', + '}', + '} // namespace ns', + ), + }); + expect(templateEdges(cg).map((e) => [e.source, e.registeredAt])).toEqual([ + ['ns::Batch::Impl', 'batch.cc:12'], + ['ns::Batch::Impl', 'batch.cc:16'], + ]); + expect(reached(cg, 'ns::Batch::Impl')).toHaveLength(2); + }); + + it('follows an alias of the class, at namespace scope and in the function', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'use.cc': lines( + '#include "mixin.h"', + 'namespace ns { using Named = PlainTxn; }', + 'ns::Named* unused;', + 'Mixin global;', + 'void Run() {', + ' using Local = FancyTxn;', + ' Mixin local;', + '}', + ), + }); + expect(reached(cg, 'Mixin::Add')).toEqual(['FancyTxn::Put', PUT_PLAIN]); + }); + + it('looks an argument up from where it is written', async () => { + const cg = await indexed({ + 'a.h': lines('namespace a { struct Item { static void Go() {} }; }'), + 'b.h': lines('namespace b { struct Item { static void Go() {} }; }'), + 'box.h': lines('template ', 'struct Box { void Run() { T::Go(); } };'), + 'use.cc': lines('#include "a.h"', '#include "b.h"', '#include "box.h"', 'namespace b { Box here; }'), + }); + expect(reached(cg, 'Box::Run')).toEqual(['b::Item::Go']); + }); + + it('takes no class of a scope the site does not name', async () => { + const cg = await indexed({ + 'txn.h': lines(TXN, 'namespace deep { struct Hidden { static void Make() {} }; }'), + 'box.h': lines('#include "txn.h"', 'template ', 'struct Box { void Run() { T::Make(); } };'), + 'use.cc': lines('#include "box.h"', 'Box b;'), + }); + expect(templateEdges(cg)).toEqual([]); + }); + + it('gives an outside argument no edge, and keeps the others', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'use.cc': lines( + '#include "mixin.h"', + '#include ', + 'Mixin s;', + 'Mixin i;', + 'Mixin p;', + ), + }); + expect(reached(cg, 'Mixin::Add')).toEqual([PUT_PLAIN]); + }); + + it('gives a pointer or a reference no edge: no such instantiation has the member', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'use.cc': lines('#include "mixin.h"', 'Mixin p;', 'Mixin r;', 'Mixin c;'), + }); + expect(reached(cg, 'Mixin::Add')).toEqual(['FancyTxn::Put']); + }); + + it('links a member a class inherits, and none it lacks', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'use.cc': lines('#include "mixin.h"', 'Mixin p;', 'Mixin n;'), + }); + expect(reached(cg, 'Mixin::Finish')).toEqual(['BaseTxn::Commit']); + expect(reached(cg, 'Mixin::Add')).toEqual([PUT_PLAIN]); + }); + + it('reads a member type of a parameter', async () => { + const cg = await indexed({ + 'all.cc': lines( + 'struct Key { static void Make() {} };', + 'struct Lock { static void Make() {} };', + 'struct Traits { using K = Key; struct Nested { static void Go() {} }; };', + 'template ', + 'struct User {', + ' using Mine = typename T::K;', + ' void Run() { Mine::Make(); T::Nested::Go(); }', + '};', + 'User user;', + ), + }); + expect(reached(cg, 'User::Run')).toEqual(['Key::Make', 'Traits::Nested::Go']); + }); + + it('reads a member enum of a parameter', async () => { + const cg = await indexed({ + 'item.h': lines('struct Item { enum Kind { One }; };', 'struct Odd { enum Kind { Two }; };'), + 'box.h': lines('#include "item.h"', 'template ', 'struct Box { void Run() { using K = typename T::Kind; K kind(0); } };'), + 'use.cc': lines('#include "box.h"', 'Box box;'), + }); + expect(reached(cg, 'Box::Run')).toEqual(['Item::Kind']); + }); + + it('constructs through a parameter', async () => { + const cg = await indexed({ + 'all.cc': lines( + 'struct Two { Two(int a, int b) {} };', + 'template ', + 'void Make() {', + ' T value(1, 2);', + ' T* made = new T(3, 4);', + '}', + 'void Run() { Make(); }', + ), + }); + const edges = templateEdges(cg); + expect(edges.filter((e) => e.kind === 'instantiates').map((e) => e.target)).toEqual(['Two']); + expect(edges.filter((e) => e.kind === 'calls').map((e) => e.target)).toEqual(['Two::Two']); + }); +}); + +describe('what is no instantiation', () => { + it('reads nothing from a comment, a string, a raw string or a macro body', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'use.cc': lines( + '#include "mixin.h"', + '// Mixin in a line comment', + '/* Mixin in a block', + ' Mixin comment */', + 'const char* s = "Mixin";', + 'const char* r = R"cc(', + ' Mixin raw;', + ')cc";', + '#define USES Mixin', + '#define CONTINUED \\', + ' Mixin', + ), + }); + expect(templateEdges(cg)).toEqual([]); + }); + + it('reads no typed suite from a macro body', async () => { + const cg = await indexed({ + 'a_test.cc': lines( + '#include ', + 'struct A { static int Go() { return 1; } };', + 'struct B { static int Go() { return 2; } };', + 'template class T1 : public ::testing::Test {};', + '#define NEVER TYPED_TEST_SUITE(T1, ::testing::Types)', + 'TYPED_TEST_SUITE(T1, ::testing::Types);', + 'TYPED_TEST(T1, Works) { TypeParam::Go(); }', + ), + }); + expect(templateEdges(cg).map((e) => e.target)).toEqual(['A::Go']); + }); + + it('reads no longer name that ends in the template\'s', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'use.cc': lines('#include "mixin.h"', 'template struct SuperMixin {};', 'SuperMixin other;', 'Mixin mine;'), + }); + expect(reached(cg, 'Mixin::Add')).toEqual([PUT_PLAIN]); + }); + + it('reads no friend, and no comparison', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'use.cc': lines( + '#include "mixin.h"', + 'class Friendly { friend class Mixin; };', + 'int Mixin = 3;', + 'bool Less(int PlainTxn, int x) { return Mixin < PlainTxn && x > 1; }', + ), + }); + expect(templateEdges(cg)).toEqual([]); + }); + + it('keeps a namesake template out', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'mixin.h': lines( + '#include "txn.h"', + 'namespace mine {', + 'template ', + 'class Mixin { public: void Add(int k) { Txn::Put(k); } };', + '}', + ), + 'other.h': lines('namespace yours {', 'template class Mixin {};', '}'), + 'use.cc': lines( + '#include "mixin.h"', + '#include "other.h"', + 'yours::Mixin elsewhere;', + 'mine::Mixin here;', + ), + }); + expect(reached(cg, 'mine::Mixin::Add')).toEqual(['FancyTxn::Put']); + }); + + it('keeps a template of a source file to that file', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'a.cc': lines('#include "txn.h"', 'template struct Local { void Go() { T::Make(); } };', 'Local a;'), + 'b.cc': lines('#include "txn.h"', 'template struct Local { void Go() { T::Make(); } };', 'Local b;'), + }); + const edges = templateEdges(cg).map((e) => `${e.target} ${e.registeredAt}`).sort(); + expect(edges).toEqual(['FancyTxn::Make b.cc:3', 'PlainTxn::Make a.cc:3']); + }); + + it('keeps a call from a file that cannot see the function template out', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'run.h': lines('#include "txn.h"', 'template ', 'void Run() { T::Make(); }'), + 'use.cc': lines('#include "run.h"', 'void Use() { Run(); }'), + 'other.cc': lines('#include "txn.h"', 'void Elsewhere() { Run(); }'), + }); + expect(reached(cg, 'Run')).toEqual(['PlainTxn::Make']); + }); + + it('keeps a member template call out, whatever it shares a name with', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'get.h': lines('#include "txn.h"', 'template struct get { void Run() { T::Make(); } };'), + 'use.cc': lines('#include "get.h"', 'void Run(Holder& h, Holder* p) { h.get(); p->get(); }'), + }); + expect(templateEdges(cg)).toEqual([]); + }); + + it('reads a parameter in a head no node holds as a parameter, not as a class of its name', async () => { + const cg = await indexed({ + 'all.h': lines( + 'struct T { static void Go() {} };', + 'template struct Box { void Run() { U::Go(); } };', + 'template Box Make();', + 'template > struct Other {};', + ), + }); + expect(templateEdges(cg)).toEqual([]); + }); + + it('keeps two headers that declare one fixture name apart', async () => { + const test = (kind: string): string => lines( + 'template class Shared : public ::testing::Test {};', + 'TYPED_TEST_SUITE_P(Shared);', + `TYPED_TEST_P(Shared, ${kind}) { TypeParam::Go(); }`, + ); + const unit = (kind: string): string => lines( + `#include "${kind.toLowerCase()}_test.h"`, + `struct ${kind}Type { static int Go() { return 1; } };`, + `INSTANTIATE_TYPED_TEST_SUITE_P(${kind}, Shared, ::testing::Types<${kind}Type>);`, + ); + const cg = await indexed({ + 'map_test.h': test('Map'), + 'set_test.h': test('Set'), + 'map_test.cc': unit('Map'), + 'set_test.cc': unit('Set'), + }); + const db = (cg as unknown as { db: { db: { prepare(sql: string): { all(): Array<{ file: string; target: string }> } } } }).db.db; + expect(db.prepare( + `SELECT s.file_path file, t.qualified_name target FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target + WHERE json_extract(e.metadata, '$.synthesizedBy') = 'cpp-template-argument' ORDER BY file` + ).all()).toEqual([{ file: 'map_test.h', target: 'MapType::Go' }, { file: 'set_test.h', target: 'SetType::Go' }]); + }); + + it('keeps a vendored copy of a class out of a file that does not include it', async () => { + const cg = await indexed({ + 'include/item.h': lines('namespace lib { struct Item { static void Go() {} }; }'), + 'tests/abi/item.h': lines('namespace lib { struct Item { static void Go() {} }; }'), + 'include/box.h': lines('#include "item.h"', 'namespace lib {', 'template ', 'struct Box { void Run() { T::Go(); } };', '}'), + 'tests/use.cc': lines('#include "../include/box.h"', 'lib::Box b;'), + }); + expect(templateEdges(cg).map((e) => e.registeredAt)).toEqual(['tests/use.cc:2']); + const db = (cg as unknown as { db: { db: { prepare(sql: string): { all(): Array<{ file_path: string }> } } } }).db.db; + expect(db.prepare(`SELECT n.file_path FROM edges e JOIN nodes n ON n.id = e.target WHERE json_extract(e.metadata, '$.synthesizedBy') = 'cpp-template-argument'`).all()) + .toEqual([{ file_path: 'include/item.h' }]); + }); +}); + +describe('a vendored copy under a plain namespace', () => { + it.each([ + ['at file scope', lines('using Mine = lib::Item;', 'Box b;')], + ['in a function', lines('void Use() {', ' using Mine = lib::Item;', ' Box b;', '}')], + ])('is not what an alias %s names when a macro hides the real one', async (_where, use) => { + const cg = await indexed({ + 'include/macros.h': lines('#define NS_BEGIN namespace lib {', '#define NS_END }'), + 'include/item.h': lines('#include "macros.h"', 'NS_BEGIN', 'struct Item { static void Go() {} };', 'NS_END'), + 'tests/abi/item.h': lines('namespace lib { struct Item { static void Go() {} }; }'), + 'include/box.h': lines('#include "item.h"', 'template ', 'struct Box { void Run() { T::Go(); } };'), + 'tests/use.cc': lines('#include "../include/box.h"', use), + }); + const db = (cg as unknown as { db: { db: { prepare(sql: string): { all(): Array<{ file_path: string }> } } } }).db.db; + expect(db.prepare(`SELECT n.file_path FROM edges e JOIN nodes n ON n.id = e.target WHERE json_extract(e.metadata, '$.synthesizedBy') = 'cpp-template-argument' AND n.file_path LIKE 'tests/%'`).all()) + .toEqual([]); + }); +}); + +describe('a member alias', () => { + it('names a class the file declaring it can see, not a vendored copy\'s', async () => { + const cg = await indexed({ + 'macros.h': lines('#define NS_BEGIN namespace lib {', '#define NS_END }'), + 'real/part.h': lines('#include "../macros.h"', 'NS_BEGIN', 'struct Part { static void Make() {} };', 'NS_END'), + 'vendored/part.h': lines('namespace lib { struct Part { static void Make() {} }; }'), + 'real/item.h': lines('#include "part.h"', 'struct Item { using P = ::lib::Part; };'), + 'other.h': lines('struct Lone { static void Make() {} };'), + 'box.h': lines('#include "real/item.h"', '#include "other.h"', 'template ', 'struct Box { void Run() { using P = typename T::P; P::Make(); } };'), + 'use.cc': lines('#include "box.h"', 'Box b;'), + }); + expect(templateEdges(cg).map((e) => e.target)).not.toContain('lib::Part::Make'); + }); +}); + +describe('a member alias of a vendored copy', () => { + it('is no member of the class the argument names', async () => { + const cg = await indexed({ + 'real/item.h': lines('struct Item {};'), + 'vendored/item.h': lines('struct Item { using Spare = int; };'), + 'box.h': lines('#include "real/item.h"', 'template ', 'struct Box { void Run() { using S = typename T::Spare; S spare(0); } };'), + 'use.cc': lines('#include "box.h"', 'Box b;'), + }); + expect(templateEdges(cg)).toEqual([]); + }); +}); + +describe('specializations', () => { + const HEADER = lines( + '#include "txn.h"', + 'template ', + 'struct Traits {', + ' void Primary() { T::Make(); }', + '};', + 'template <>', + 'struct Traits {', + ' void Full() {}', + '};', + 'template ', + 'struct Traits {', + ' void Partial() { U::Make(); }', + '};', + 'template ', + 'struct Traits {', + ' void Pointer() { T::Make(); }', + '};', + ); + + it('an instantiation a full specialization takes does not run the template', async () => { + const cg = await indexed({ + 'txn.h': lines(TXN, 'class ThirdTxn { public: static void Make() {} };'), + 'traits.h': HEADER, + 'use.cc': lines('#include "traits.h"', 'Traits full;', 'Traits plain;'), + }); + expect(reached(cg, 'Traits::Primary')).toEqual(['ThirdTxn::Make']); + }); + + it('a partial specialization reads its own parameter from what it matches', async () => { + const cg = await indexed({ + 'txn.h': lines(TXN, 'class ThirdTxn { public: static void Make() {} };'), + 'traits.h': HEADER, + 'use.cc': lines('#include "traits.h"', 'Traits a;', 'Traits b;'), + }); + expect(reached(cg, '::Partial')).toEqual(['PlainTxn::Make']); + expect(reached(cg, '::Pointer')).toEqual(['ThirdTxn::Make']); + expect(reached(cg, 'Traits::Primary')).toEqual([]); + }); + + it('an instantiation no specialization matches stays with the template', async () => { + const cg = await indexed({ + 'txn.h': lines(TXN, 'class ThirdTxn { public: static void Make() {} };'), + 'traits.h': HEADER, + 'use.cc': lines('#include "traits.h"', 'Traits a;'), + }); + expect(reached(cg, 'Traits::Primary')).toEqual(['ThirdTxn::Make']); + expect(reached(cg, '::Pointer')).toEqual([]); + }); + + it('each specialization keeps to the instantiations it takes', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'holder.h': lines( + '#include "txn.h"', + 'template struct Holder {};', + 'template struct Holder { void Pointer() { T::Make(); } };', + 'template struct Holder { void Ref() { T::Make(); } };', + ), + 'use.cc': lines('#include "holder.h"', 'Holder a;', 'Holder b;'), + }); + expect(reached(cg, '::Pointer')).toEqual(['PlainTxn::Make']); + expect(reached(cg, '::Ref')).toEqual(['FancyTxn::Make']); + }); + + it('reads a member of a partial specialization defined outside it', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'pair.h': lines( + '#include "txn.h"', + 'template ', + 'struct Pair { void Go(); };', + 'template ', + 'struct Pair { void Go(); };', + 'template ', + 'void Pair::Go() { T::Make(); }', + ), + 'use.cc': lines('#include "pair.h"', 'Pair a;', 'Pair b;'), + }); + expect(templateEdges(cg).map((e) => e.target)).toEqual(['PlainTxn::Make']); + }); + + it('a condition no one can read keeps an instantiation out of the template', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'traits.h': lines( + '#include "txn.h"', + 'template ', + 'struct Traits { void Primary() { T::Make(); } };', + 'template ', + 'struct Traits> { void Small() {} };', + ), + 'use.cc': lines('#include "traits.h"', 'Traits maybe;'), + }); + expect(reached(cg, 'Traits::Primary')).toEqual([]); + }); + + it('a member specialized out of line leaves the instantiation with the rest', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'use.cc': lines('#include "mixin.h"', 'template <> void Mixin::Finish() {}', 'Mixin p;'), + }); + expect(reached(cg, 'Mixin::Add')).toEqual([PUT_PLAIN]); + }); +}); + +describe('the cap', () => { + const classes = (n: number): string => Array.from({ length: n }, (_, i) => `struct C${i} { static void Go() {} };`).join('\n'); + const uses = (n: number): string => Array.from({ length: n }, (_, i) => `Box b${i};`).join('\n'); + const box = lines('template ', 'struct Box { void Run() { T::Go(); } };'); + + it('links every class up to the cap', async () => { + const cg = await indexed({ 'all.cc': `${box}${classes(CPP_TEMPLATE_ARGUMENT_CAP)}\n${uses(CPP_TEMPLATE_ARGUMENT_CAP)}\n` }); + expect(reached(cg, 'Box::Run')).toHaveLength(CPP_TEMPLATE_ARGUMENT_CAP); + }); + + it('links none past it, not some', async () => { + const n = CPP_TEMPLATE_ARGUMENT_CAP + 1; + const cg = await indexed({ 'all.cc': `${box}${classes(n)}\n${uses(n)}\n` }); + expect(reached(cg, 'Box::Run')).toEqual([]); + }); + + it('counts classes, not sites', async () => { + const sites = Array.from({ length: CPP_TEMPLATE_ARGUMENT_CAP * 3 }, (_, i) => `Box s${i};`).join('\n'); + const cg = await indexed({ 'all.cc': `${box}${classes(1)}\n${sites}\n` }); + expect(reached(cg, 'Box::Run')).toEqual(['C0::Go']); + }); + + it('counts a chain with the classes it reaches', async () => { + const n = CPP_TEMPLATE_ARGUMENT_CAP + 1; + const wrapped = Array.from({ length: n }, (_, i) => `Wrap w${i};`).join('\n'); + const cg = await indexed({ 'all.cc': `${box}template struct Wrap : Box {};\n${classes(n)}\n${wrapped}\n` }); + expect(reached(cg, 'Box::Run')).toEqual([]); + }); +}); + +/** The template edges as text, for comparing two graphs. */ +const asText = (cg: CodeGraph): string[] => templateEdges(cg).map((e) => JSON.stringify(e)).sort(); + +describe('sync matches a fresh index', () => { + const BASE = { + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'keep.cc': lines('#include "mixin.h"', 'Mixin kept;'), + }; + + /** Index `before`, write `after`, sync, and compare with a fresh index of `after`. */ + const syncs = async (before: Record, after: Record, removed: string[] = []): Promise => { + const root = project(before); + const cg = await opened(root); + write(root, after); + for (const file of removed) fs.rmSync(path.join(root, file)); + await cg.sync(); + const fresh = await indexed(Object.fromEntries( + Object.entries({ ...before, ...after }).filter(([file]) => !removed.includes(file)))); + expect(asText(cg)).toEqual(asText(fresh)); + return reached(cg, 'Mixin::Add'); + }; + + it('an instantiation added in a third file', async () => { + expect(await syncs(BASE, { 'added.cc': lines('#include "mixin.h"', 'Mixin added;') })) + .toEqual(['FancyTxn::Put', PUT_PLAIN]); + }); + + it('an instantiation changed in a third file', async () => { + const before = { ...BASE, 'third.cc': lines('#include "mixin.h"', 'Mixin third;') }; + expect(await syncs(before, { 'third.cc': lines('#include "mixin.h"', 'Mixin third;') })).toEqual([PUT_PLAIN]); + }); + + it('an instantiation removed from a third file', async () => { + const before = { ...BASE, 'third.cc': lines('#include "mixin.h"', 'Mixin third;') }; + expect(await syncs(before, { 'third.cc': lines('#include "mixin.h"', 'int nothing;') })).toEqual([PUT_PLAIN]); + }); + + it('the file holding an instantiation deleted', async () => { + const before = { ...BASE, 'third.cc': lines('#include "mixin.h"', 'Mixin third;') }; + expect(await syncs(before, {}, ['third.cc'])).toEqual([PUT_PLAIN]); + }); + + it('a file holding nothing but an instantiation', async () => { + const before = { + ...BASE, + 'bare.hpp': 'int nothing;\n', + 'unit.cc': lines('#include "mixin.h"', '#include "bare.hpp"'), + }; + expect(await syncs(before, { 'bare.hpp': 'Mixin bare;\n' })).toEqual(['FancyTxn::Put', PUT_PLAIN]); + expect(hasSynthesisPattern('bare.hpp', 'Mixin bare;\n')).toBe(true); + }); + + it('a member added to, and removed from, an argument', async () => { + const before = { ...BASE, 'third.cc': lines('#include "mixin.h"', 'Mixin third;') }; + const withPut = TXN.replace('class NoPutTxn {};', 'class NoPutTxn { public: void Put(int k) {} };'); + expect(await syncs(before, { 'txn.h': withPut })).toEqual(['NoPutTxn::Put', PUT_PLAIN]); + }); + + it('a template edited to call another member', async () => { + const after = { 'mixin.h': MIXIN.replace('Txn::Commit();', 'Txn::Commit(); Txn::Make();') }; + await syncs(BASE, after); + }); + + it('a typed list edited', async () => { + const test = (list: string): string => lines( + '#include ', + 'struct A { static int Go() { return 1; } };', + 'struct B { static int Go() { return 2; } };', + 'template class T1 : public ::testing::Test {};', + 'TYPED_TEST_SUITE(T1, ::testing::Types);', + 'TYPED_TEST(T1, Works) { TypeParam::Go(); }', + ).replace('Types', `Types<${list}>`); + const root = project({ 'a_test.cc': test('A') }); + const cg = await opened(root); + write(root, { 'a_test.cc': test('A, B') }); + await cg.sync(); + const fresh = await indexed({ 'a_test.cc': test('A, B') }); + expect(asText(cg)).toEqual(asText(fresh)); + expect(templateEdges(cg).map((e) => e.target).sort()).toEqual(['A::Go', 'B::Go']); + }); +}); + +describe('determinism', () => { + const FILES = { + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'b.cc': lines('#include "mixin.h"', 'Mixin b;', 'Mixin again;'), + 'a.cc': lines('#include "mixin.h"', 'Mixin a;'), + }; + + it('gives the same edges, in the same order, with the resolver pool and without', async () => { + const before = process.env.CODEGRAPH_PARALLEL_RESOLVE_MIN; + try { + process.env.CODEGRAPH_PARALLEL_RESOLVE_MIN = '999999999'; + const sequential = (await indexed(FILES)); + process.env.CODEGRAPH_PARALLEL_RESOLVE_MIN = '0'; + const pooled = (await indexed(FILES)); + expect(templateEdges(pooled)).toEqual(templateEdges(sequential)); + expect(templateEdges(sequential).length).toBeGreaterThan(0); + } finally { + if (before === undefined) delete process.env.CODEGRAPH_PARALLEL_RESOLVE_MIN; + else process.env.CODEGRAPH_PARALLEL_RESOLVE_MIN = before; + } + }); + + it('names the first site by file and line, whichever a run reads first', async () => { + const cg = await indexed(FILES); + expect(templateEdges(cg).find((e) => e.target === PUT_PLAIN)?.registeredAt).toBe('a.cc:2'); + }); +}); + +describe('how a hop reads', () => { + it('says which argument, for which parameter, and where', () => { + const edge = { + source: 'a', target: 'b', kind: 'calls', provenance: 'heuristic', + metadata: { synthesizedBy: 'cpp-template-argument', via: 'Txn', argument: 'PlainTxn', registeredAt: 'use.cc:2' }, + } as unknown as Edge; + expect(describeSynthesizedHop(edge)).toEqual({ summary: 'template argument `PlainTxn` for `Txn`', detail: '', registeredAt: 'use.cc:2' }); + }); + + it('connects a flow through the template in explore', async () => { + const cg = await indexed({ + 'txn.h': TXN, + 'mixin.h': MIXIN, + 'use.cc': lines('#include "mixin.h"', 'void Start() { Mixin m; m.Add(1); }'), + }); + const { ToolHandler } = await import('../src/mcp/tools'); + const handler = new ToolHandler(cg); + const result = await handler.execute('codegraph_explore', { query: 'Start Mixin::Add PlainTxn::Put' }); + const text = result.content.map((c) => ('text' in c ? c.text : '')).join('\n'); + expect(text).toContain('template argument `PlainTxn` for `Txn`'); + expect(text).toContain('use.cc:2'); + }); +}); diff --git a/src/db/queries.ts b/src/db/queries.ts index 87199338f5..f62ea6cd81 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -4454,6 +4454,36 @@ export class QueryBuilder { })); } + /** + * The failed C++ calls and function values with a qualifier outside `std` + * (`Txn::Put`, `T::T/2`), calls on a variable (`shard.Clear`), and what + * values are constructed as, which may go through a template parameter + * (cpp-template-instantiations.ts), one at a time: there can be a great + * many, few of which do. + */ + *iterateFailedCppTypeReferences(): IterableIterator { + const rows = this.db.prepare( + `SELECT from_node_id, reference_name, reference_kind, line, col, file_path FROM unresolved_refs + WHERE status = 'failed' AND language = 'cpp' AND (reference_kind = 'instantiates' + OR (reference_kind IN ('calls', 'function_ref') AND reference_name GLOB '*::*' AND reference_name NOT GLOB 'std::*') + OR (reference_kind = 'calls' AND reference_name GLOB '*.*' AND reference_name NOT GLOB '*(*'))` + ).iterate() as IterableIterator>; + for (const row of rows) { + yield { + fromNodeId: row.from_node_id, + referenceName: row.reference_name, + referenceKind: row.reference_kind as EdgeKind, + line: row.line, + column: row.col, + filePath: row.file_path, + language: 'cpp', + }; + } + // The walk reads the whole table, which would stay in this connection's page + // cache through the end of synthesis, where an index peaks in memory. + this.db.exec('PRAGMA shrink_memory'); + } + /** * Get the count of PENDING (never-attempted) references without loading * them into memory. Rows marked status='failed' — attempted by a completed diff --git a/src/graph/synthesized-hop.ts b/src/graph/synthesized-hop.ts index 39b7fece44..7f5673ae15 100644 --- a/src/graph/synthesized-hop.ts +++ b/src/graph/synthesized-hop.ts @@ -85,6 +85,9 @@ export function describeSynthesizedHop(edge: Edge | null | undefined): Synthesiz return hop(m.matcher ? `Next proxy, matcher ${quoted(m.matcher, '')}` : 'Next proxy, on every request'); case 'const-alias': return hop(`const alias of ${quoted(m.via, 'a function')}`); + case 'cpp-template-argument': + // C++: a call through a template parameter; the wiring site writes the argument. + return hop(`template argument ${quoted(m.argument, 'a class')} for ${quoted(m.via, 'a parameter')}`); case 'go-func-field': // Go: a call through a struct field holding a function, `d.refreshf(ctx)`; // `via` is the field it was copied from, which the wiring site writes. diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index b69341030e..9202172225 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -28,6 +28,7 @@ import { isGeneratedFile } from '../extraction/generated-detection'; import { stripCommentsForRegex } from './strip-comments'; import { escapeRegExp } from '../utils'; import { cFnPointerDispatchEdges } from './c-fnptr-synthesizer'; +import { cppTemplateArgumentEdges } from './cpp-template-instantiations'; import { goframeRouteEdges } from './goframe-synthesizer'; import { goFuncFieldEdges } from './go-func-field-synthesizer'; import { expoRouterReturnEdges } from './expo-router-synthesizer'; @@ -5313,7 +5314,7 @@ export function hasSynthesisPattern(filePath: string, content: string): boolean // A Next proxy and the routes its matcher covers are joined by path and config, not by any line of code. if (NEXT_PROXY_FILE.test(filePath) || nextRouteForFile(filePath) !== null) return true; if (/\.(?:c|h|cc|cpp|cxx|hpp|hh|hxx|cppm|ipp|inl|tcc|def|inc|tbl)$/i.test(filePath) && - /\b(?:struct|union|typedef|virtual|override)\b|#\s*(?:include|define|if)|=|->|\[/.test(content)) return true; + /\b(?:struct|union|typedef|virtual|override)\b|#\s*(?:include|define|if)|=|->|\[| has('c', 'cpp'), run: (q, c, y, sub) => cFnPointerDispatchEdges(q, c, y, sub), }, + // `Txn::Put(…)` in `template class Mixin` onto each class the project writes as `Txn`. + // After the other C++ passes: a pair one of them bridges keeps its edge. + { name: 'cppTemplateEdges', gate: (has) => has('cpp'), run: (q, c, y) => cppTemplateArgumentEdges(q, c, y) }, { name: 'goframeEdges', gate: (has) => has('go'), run: (_q, c, y) => goframeRouteEdges(c, y) }, // `d.refreshf(ctx)` onto the functions put in that func field (`RefreshF: d.refresh`). // After the Go framework passes: a pair one of them bridges keeps its more specific edge. diff --git a/src/resolution/cpp-supertypes.ts b/src/resolution/cpp-supertypes.ts index ca30a7d7e3..a79a9f17cd 100644 --- a/src/resolution/cpp-supertypes.ts +++ b/src/resolution/cpp-supertypes.ts @@ -107,7 +107,7 @@ export function isCppDeclaredTypeRef(ref: UnresolvedRef, context: ResolutionCont function seenFrom(site: CppNameSite, context: ResolutionContext, unit?: string): (n: Node) => boolean { const visible = visibleFrom(site.file, unit); return (n) => { - if (!visible(n)) return false; + if (!visible(n) || site.sees?.(n) === false) return false; const fn = cppFunctionAround(n, context); return !fn || (fn.filePath === site.file && site.line >= fn.startLine && site.line <= fn.endLine); }; @@ -230,6 +230,8 @@ export interface CppNameSite { * before the enclosing namespaces'. */ classScope?: string; + /** Which declarations the file writing the name can see, when that is narrower than a header's being anyone's. */ + sees?: (n: Node) => boolean; } /** The class a C or C++ type name names. */ diff --git a/src/resolution/cpp-template-instantiations.ts b/src/resolution/cpp-template-instantiations.ts new file mode 100644 index 0000000000..959788092b --- /dev/null +++ b/src/resolution/cpp-template-instantiations.ts @@ -0,0 +1,1133 @@ +/** + * A C++ template parameter reaches the classes the template is instantiated + * with. + * + * `Txn::PutEntity(…)` in rocksdb's `template class + * SecondaryIndexMixin` is a member of whatever class the template is given, so + * no lookup at the call can answer it. A guess by name is no answer: the rule + * that took `WriteCommittedTxn::PutEntity` because the class's name ends in + * `Txn` sent other calls to the wrong class, and every such call now fails + * (gateTypeParameter). What decides the target is written elsewhere, in + * `new SecondaryIndexMixin(…)`, and with several + * instantiations there is no one target: each really runs its own. So this is + * a whole-graph pass over the references that failed, not a resolver: one + * reference gets one edge per class the project writes as the argument. + * + * What it reads, from each failed reference that goes through a parameter: + * - The template that declares the parameter, and which parameter it is: the + * function's own head, the class templates around it, or, defined outside + * its class, the head that matches `C::` (cppTemplateHeads). A gtest + * `TYPED_TEST(Fixture, Name)` body's `TypeParam` is the fixture's first. + * - An alias of a member of a parameter counts as that member: + * `using KeyGen = typename TypeParam::KeyGen;`. + * - A variable declared with such a type counts too: `shard.Clear()` on a + * `CacheShard& shard`. + * - A class template's instantiations are wherever `Name<…>` is written and + * `Name` is that class by C++ lookup: a base clause, a variable, member or + * `new`, an alias, `template class Name;`, gtest's `::testing::Types` + * through `TYPED_TEST_SUITE`. A function template's are the calls the graph + * already holds, read for the `f(…)` they write, or for the type of + * the argument that decides what `f(x)` deduces. + * - An argument that is itself a parameter of a template around the site is + * followed to that template's instantiations, a few hops at most. + * - An argument is a project class by the lookup a declared type gets + * (cppClassWritten), a class local to the function that writes it first, + * in a file that sees it. Anything else (`std::string`, `int`, a pointer) + * gives no edge, and takes nothing from the other instantiations. + * - The member must be that class's own or inherited (cppMethodOf); a + * constructor is chosen as a declared object's is (matchCppConstructor). + * - An instantiation a specialization takes runs the specialization's + * members, not the members of the template it specializes. + * + * The argument's site is the edge's `registeredAt`: a file holding only an + * instantiation is what the edge depends on, so a sync that touches it runs + * this pass again. Nothing here looks at an edge a resolver made, and a + * reference with no answer stays failed. + */ +import type { Edge, EdgeKind, Node } from '../types'; +import type { QueryBuilder } from '../db/queries'; +import type { MaybeYield } from './cooperative-yield'; +import type { ResolutionContext, UnresolvedRef } from './types'; +import { matchCppConstructor } from './cpp-constructor'; +import { cppIncluders } from './cpp-includers'; +import { cppMacroNamespaceFrames } from './cpp-namespaces'; +import { cppLiteralsBlanked, cppNonCodeLines } from './cpp-raw-strings'; +import { cppClassWritten, type CppNameSite } from './cpp-supertypes'; +import { + cppAliasWrittenAt, + cppAliasedClass, + cppClassNamed, + cppDeclaresLocalAlias, + cppParentScope, + cppTemplateHeads, + cppTemplateParametersIn, + cppTypeNamed, + cppTypeNamesDeclared, + cppTypeSegments, + isCppPointerType, + resolveCppAliasedType, + stripCppTemplateArguments, + type CppTemplateParameter, +} from './cpp-type-aliases'; +import { + cppArgumentType, + cppCallArguments, + cppCallTemplateArguments, + cppCallersMember, + cppMethodOf, + cppOverloadParameters, + isCppLocalName, + splitCppTopLevel, +} from './name-matcher'; +import { LRUCache } from './lru-cache'; +import { stripCommentsForRegex } from './strip-comments'; +import { makeLineAt } from './synth-utils'; + +/** + * How many classes one parameter may reach before it reaches none. A template + * written with more than this is a container or a test helper, whose every + * instantiation is no hop a flow takes (see AGENTS.md on a god-function's + * fan-out). A template over the cap gets no edges, not some. + */ +export const CPP_TEMPLATE_ARGUMENT_CAP = 40; + +/** How many templates an argument is followed through, from a parameter to the parameter it is written as. */ +const MAX_TEMPLATE_HOPS = 4; +/** How far an argument list may run: a gtest list of types takes a line per type. */ +const MAX_ARGUMENT_LIST = 64 * 1024; +/** How much text before a `Name<` tells what is written there: a template head, at most. */ +const HEAD_WINDOW = 400; +/** How many files' text and declarations are kept: every file of most projects, and a bound on the rest. */ +const MAX_FILES_HELD = 4096; +/** How many templates' instantiations are kept. */ +const MAX_TEMPLATES_HELD = 1024; + +const CLASS_KINDS: ReadonlySet = new Set(['class', 'struct', 'union']); +const FUNCTION_KINDS: ReadonlySet = new Set(['function', 'method']); +/** What a template argument is written in, and so what a site can sit in. */ +const DECLARATION_KINDS: ReadonlySet = new Set([...CLASS_KINDS, ...FUNCTION_KINDS, 'type_alias', 'variable', 'constant']); + +const CPP_FILE = /\.(?:c|h|cc|cpp|cxx|hpp|hh|hxx|cppm|ipp|inl|tcc)$/i; +const IDENTIFIER = /^[A-Za-z_]\w*$/; +/** A call on a variable, as a reference spells it: `shard.Clear`, for `shard->Clear()` too. */ +const VALUE_CALL = /^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/; +/** What a declaration may write around its type and between it and the name. */ +const CV_QUALIFIER = /\b(?:const|volatile|typename|mutable|static)\b/g; +/** A declaration of a value: the type's first name, then the variable's: `Shard& shard`, `Shard::Part* part`, `T* NONNULL p`. */ +const DECLARED_VALUE = /\b([A-Za-z_]\w*)(?:\s*::\s*[A-Za-z_]\w*)*(?:\s*<[^<>;{}()]*>)?[\s*&]+(?:[A-Z][A-Z0-9_]*\s+)*(?=([A-Za-z_]\w*)\b)/g; + +/** gtest's macros that give a typed fixture its list of types, with the fixture. */ +const TYPED_SUITE = /\b(?:TYPED_TEST_(?:SUITE|CASE)\s*\(|INSTANTIATE_TYPED_TEST_(?:SUITE|CASE)_P\s*\(\s*[A-Za-z_]\w*\s*,)\s*((?:::\s*)?[A-Za-z_]\w*(?:\s*::\s*[A-Za-z_]\w*)*)\s*,/g; +/** How the index names a class it reads no name for, such as a specialization. */ +const ANONYMOUS = ''; +/** A class declaration that writes template arguments after its name, up to the `<`. */ +const SPECIALIZATION_HEAD = /^(?:class|struct|union)\s+(?:[A-Z][A-Z0-9_]*(?:\([^()]*\))?\s+)*((?:[A-Za-z_]\w*\s*::\s*)*[A-Za-z_]\w*)\s*`. */ +const CONDITION = /\b(?:enable_if|void_t|disjunction|conjunction|negation|conditional|requires)\w*|\bis_\w+/; + +/** A template's head, as the declaration it belongs to writes it. */ +interface Template { + /** One per template, whichever declaration of it a reference is in. */ + key: string; + kind: 'class' | 'alias' | 'function'; + name: string; + /** The declaration of a class or alias template; the very function for a function template. */ + node: Node; + parameters: readonly CppTemplateParameter[]; + /** What a specialization writes after the name (`T*`, `int`); null for the template itself. */ + pattern: readonly string[] | null; +} + +/** A parameter of a template. */ +interface Slot { + template: Template; + index: number; +} + +/** A reference that goes through a parameter. */ +interface Through { + slot: Slot; + /** The parameter as the code around the reference names it. */ + parameter: string; + /** Member types between the parameter and what is named: `Key` of `T::Key::Make`, or of an alias of `T::Key`. */ + types: readonly string[]; + /** The member called: `Make`; null for the type itself. */ + member: string | null; + /** The argument count of a constructor call: `T x(a, b);`. */ + arity: number | null; +} + +/** Where a type name is written: what it is looked up from. */ +interface Place { + file: string; + line: number; + scope: string; + /** The declaration around it. */ + around: Node | null; + /** The templates around it, innermost first. */ + templates: readonly Template[]; + /** The names that are parameters there, those of a head no node holds as well. */ + parameters: ReadonlySet; + ref: UnresolvedRef; +} + +/** A template argument as written. */ +interface Argument { + text: string; + place: Place; +} + +/** A written `Name<…>` that names a template. */ +interface Instantiation { + args: readonly Argument[]; +} + +/** A call of a function template. */ +interface Call { + ref: UnresolvedRef; + place: Place; + /** The template arguments it writes; null for none, leaving them to be deduced. */ + args: readonly Argument[] | null; +} + +/** One argument a specialization writes: `T`, `T*`, `int`, or a condition on `T`. */ +interface PatternItem { + text: string; + /** The parameter it is, whole. */ + parameter?: string; + pack?: boolean; + /** What only the arguments decide. */ + condition?: boolean; + /** How it reads a parameter out of an argument, with the parameters it does. */ + shape?: RegExp; + words: readonly string[]; +} + +/** A specialization of a class template: `template class Name`. */ +interface Specialization { + pattern: readonly PatternItem[]; + /** The arguments as they compare, to find a declaration's own. */ + written: string; +} + +/** What a class template's name is written as across the project. */ +interface WrittenTemplate { + instantiations: Instantiation[]; + specializations: Specialization[]; +} + +/** A file's text with comments and strings blanked, offsets kept. */ +interface Source { + text: string; + lineAt: (offset: number) => number; +} + +/** The arguments between the `<` at `open` and the `>` at `close`, split at the top level, each with where it starts. */ +function argumentsBetween(text: string, open: number, close: number): Array<{ text: string; offset: number }> { + const out: Array<{ text: string; offset: number }> = []; + let depth = 0; + let parens = 0; + let start = open + 1; + const push = (end: number): void => { + const raw = text.slice(start, end); + const trimmed = raw.trim(); + if (trimmed) out.push({ text: trimmed, offset: start + raw.indexOf(trimmed) }); + }; + for (let i = open + 1; i < close; i++) { + const c = text[i]!; + if (c === '(' || c === '[') parens++; + else if (c === ')' || c === ']') parens--; + else if (parens > 0) continue; + else if (c === '<') depth++; + else if (c === '>' && text[i - 1] !== '-') depth--; + else if (c === ',' && depth === 0) { + push(i); + start = i + 1; + } + } + push(close); + return out; +} + +/** The `>` closing the argument list opened at `open`, or -1 when `<` opens none there. */ +function closingAngle(text: string, open: number): number { + let depth = 0; + let parens = 0; + const end = Math.min(text.length, open + MAX_ARGUMENT_LIST); + for (let i = open; i < end; i++) { + const c = text[i]!; + if (c === '(' || c === '[') parens++; + else if (c === ')' || c === ']') { + if (--parens < 0) return -1; + } else if (parens > 0) continue; + else if (c === '<') depth++; + else if (c === '>') { + if (text[i - 1] === '-') continue; + if (--depth === 0) return i; + } else if (c === ';' || c === '{' || c === '}') return -1; + } + return -1; +} + +const isWord = (code: number): boolean => + (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code === 95; +const isSpace = (code: number): boolean => code === 32 || code === 9 || code === 10 || code === 13; + +/** A copy of a name read from a file's text. V8 keeps a whole string alive for a long enough part of it. */ +const detached = (name: string): string => `_${name}`.slice(1); + +/** Add the names `text` writes right before a `<`. */ +function addNamesBeforeAngle(text: string, names: Set): void { + for (let open = text.indexOf('<'); open >= 0; open = text.indexOf('<', open + 1)) { + let end = open; + while (end > 0 && isSpace(text.charCodeAt(end - 1))) end--; + let start = end; + while (start > 0 && isWord(text.charCodeAt(start - 1))) start--; + if (start < end) names.add(text.slice(start, end)); + } +} + +/** The qualifiers `A::B::` written right before `end`, outermost first, each with the arguments it writes. */ +function qualifiersBefore(text: string, end: number): Array<{ name: string; args: string[] | null }> { + const out: Array<{ name: string; args: string[] | null }> = []; + let at = end; + for (;;) { + while (at > 0 && isSpace(text.charCodeAt(at - 1))) at--; + if (text.slice(at - 2, at) !== '::') return out; + at -= 2; + while (at > 0 && isSpace(text.charCodeAt(at - 1))) at--; + let args: string[] | null = null; + if (text[at - 1] === '>') { + let depth = 0; + let i = at - 1; + for (; i >= 0; i--) { + if (text[i] === '>') depth++; + else if (text[i] === '<' && --depth === 0) break; + } + if (i < 0) return out; + args = argumentsBetween(text, i, at - 1).map((a) => a.text); + at = i; + while (at > 0 && isSpace(text.charCodeAt(at - 1))) at--; + } + let start = at; + while (start > 0 && isWord(text.charCodeAt(start - 1))) start--; + if (start === at) return out; + out.unshift({ name: text.slice(start, at), args }); + at = start; + } +} + +const escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +/** A written template argument as it compares: spaced only between words. */ +function argumentKey(arg: string): string { + return arg.replace(/\s*([^\w\s])\s*/g, '$1').replace(/\s+/g, ' '); +} + +const slotKey = (slot: Slot): string => `${slot.template.key}#${slot.index}#${slot.template.pattern?.join() ?? ''}`; + +/** Do the arguments a declaration writes for its template repeat its head: `C` under `template `? */ +function isOwnHead(args: readonly string[], head: readonly CppTemplateParameter[]): boolean { + return args.length === head.length && args.every((a, k) => a.replace(/\s*\.\.\.$/, '') === head[k]!.name); +} + +/** A specialization, from the arguments it writes (as they compare) and its head. */ +function specializationOf(args: readonly string[], parameters: readonly CppTemplateParameter[]): Specialization { + const names = new Set(parameters.flatMap((p) => (p.name ? [p.name] : []))); + const pattern = args.map((text): PatternItem => { + const bare = text.replace(/\.\.\.$/, ''); + if (names.has(bare)) return { text, parameter: bare, pack: bare !== text, words: [] }; + const words = (text.match(/[A-Za-z_]\w*/g) ?? []).filter((w) => names.has(w)); + if (words.length === 0) return { text, words }; + if (CONDITION.test(text)) return { text, condition: true, words }; + return { text, words, shape: new RegExp(`^${text.split(/([A-Za-z_]\w*)/).map((part) => (names.has(part) ? '(.+)' : escapeRegExp(part))).join('')}$`) }; + }); + return { pattern, written: args.join() }; +} + +/** The `namespace a { … }` blocks of a blanked file, by offset, outermost first. */ +function namespaceBlocks(text: string): Array<{ start: number; end: number; path: readonly string[] }> { + const blocks: Array<{ start: number; end: number; path: readonly string[] }> = []; + const open: Array<{ start: number; end: number; path: readonly string[] } | null> = []; + let path: readonly string[] = []; + for (const m of text.matchAll(/\bnamespace\b(?:\s+([A-Za-z_][\w:\s]*?))?\s*\{|[{}]/g)) { + if (m[0] === '{') open.push(null); + else if (m[0] === '}') { + const block = open.pop(); + if (block) { + block.end = m.index!; + path = open.reduce((p, b) => b?.path ?? p, []); + } + } else { + const names = m[1]?.replace(/\s+/g, '') ?? ''; + const block = { start: m.index!, end: text.length, path: names ? [...path, ...names.split('::')] : path }; + blocks.push(block); + open.push(block); + path = block.path; + } + } + return blocks; +} + +class TemplateInstantiations { + private readonly sources = new LRUCache(MAX_FILES_HELD); + private readonly declarations = new LRUCache(MAX_FILES_HELD); + private readonly namespaces = new LRUCache>(MAX_FILES_HELD); + private readonly templates = new Map(); + private readonly written = new LRUCache(MAX_TEMPLATES_HELD); + private readonly calls = new Map(); + private readonly seen = new Map(); + private readonly values = new Map>(); + /** The C and C++ files, each with the names it writes before a `<`, read once. */ + private readonly filesNaming = new Map(); + /** The files that use gtest's typed tests. */ + private readonly typedFiles: string[] = []; + private suites: Map> | null = null; + private scanned = 0; + + constructor( + private readonly queries: QueryBuilder, + private readonly ctx: ResolutionContext, + private readonly onYield: MaybeYield, + ) {} + + /** + * Read every C and C++ file once, for the names each writes before a `<`, + * and return the names each that uses templates declares as a parameter or + * an alias. + */ + async read(): Promise>> { + const declared = new Map>(); + for (const file of [...this.ctx.getAllFiles()].sort()) { + if (!CPP_FILE.test(file)) continue; + if ((++this.scanned & 63) === 0) await this.onYield(); + const text = this.ctx.scanFile?.(file) ?? this.ctx.readFile(file); + if (!text) continue; + const names = new Set(); + addNamesBeforeAngle(text, names); + for (const name of names) { + const list = this.filesNaming.get(name); + if (list) list.push(file); + else this.filesNaming.set(detached(name), [file]); + } + const typed = text.includes('TYPED_TEST'); + if (typed) this.typedFiles.push(file); + if (!typed && !text.includes('template')) continue; + const types = new Set([...cppTypeNamesDeclared(text)].map(detached)); + if (typed) types.add('TypeParam'); + declared.set(file, types); + } + return declared; + } + + private lines(file: string): readonly string[] { + return this.ctx.getFileLines?.(file) ?? this.ctx.readFile(file)?.split(/\r?\n/) ?? []; + } + + /** The classes of a qualified name, in file and line order: the walks above take the first that fits. */ + private classesNamed(qualified: string): Node[] { + return this.ctx.getNodesByQualifiedName(qualified).filter((n) => CLASS_KINDS.has(n.kind)) + .sort((a, b) => (a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0) || a.startLine - b.startLine); + } + + /** The declaration of a class or function around `node`, whose lines hold it, in the scope its qualified name gives. */ + private declarationAround(node: Node): Node | null { + for (let scope = cppParentScope(node.qualifiedName); scope; scope = cppParentScope(scope)) { + const around = this.ctx.getNodesByQualifiedName(scope).find((n) => + n.id !== node.id && (CLASS_KINDS.has(n.kind) || FUNCTION_KINDS.has(n.kind)) && n.filePath === node.filePath && + n.startLine <= node.startLine && (n.endLine ?? n.startLine) >= (node.endLine ?? node.startLine)); + if (around) return around; + } + return null; + } + + /** + * The class template a class declaration's head belongs to. A specialization + * has no name of its own in the index (``), and a nested class + * defined outside its template names another class, so that is read from + * what the declaration writes, `struct Traits`, and found as any + * name is. Null when no class of that name is found. + */ + private templateOfClass(decl: Node, head: readonly CppTemplateParameter[]): Template | null { + const text = this.lines(decl.filePath).slice(decl.startLine - 1, decl.startLine + 7).join('\n').slice(decl.startColumn); + const written = SPECIALIZATION_HEAD.exec(text); + const close = written ? closingAngle(text, written[0].length - 1) : -1; + const args = close < 0 ? null : argumentsBetween(text, written![0].length - 1, close).map((a) => a.text); + const pattern = args && !isOwnHead(args, head) ? args : null; + // `template struct List::Node {`: its head is `List`'s. + const nested = close >= 0 && /^\s*::/.test(text.slice(close + 1)); + if (decl.name !== ANONYMOUS && !nested) return this.classTemplate(decl, head, pattern); + if (!args) return null; + const place = this.placeIn(decl, decl.startLine); + const found = cppClassWritten(written![1]!.replace(/\s+/g, ''), { ...this.site(place), scope: cppParentScope(decl.qualifiedName) }, place.ref, this.ctx); + return found?.exact && CLASS_KINDS.has(found.node.kind) ? this.classTemplate(found.node, head, pattern) : null; + } + + private classTemplate(decl: Node, parameters: readonly CppTemplateParameter[], pattern: readonly string[] | null): Template { + return { key: `class:${decl.qualifiedName}@${decl.filePath}`, kind: 'class', name: decl.name, node: decl, parameters, pattern }; + } + + private functionTemplate(node: Node, parameters: readonly CppTemplateParameter[]): Template { + return { key: `function:${node.id}`, kind: 'function', name: node.name, node, parameters, pattern: null }; + } + + /** + * The class templates a definition outside its class writes in its + * declarator (`template void C::f()`), innermost first, after + * a member template's own head. + */ + private outOfLineTemplates(node: Node, heads: ReadonlyArray): Template[] { + const text = this.lines(node.filePath).slice(node.startLine - 1, node.startLine + 11).join('\n').slice(node.startColumn); + const name = new RegExp(String.raw`::\s*(?:template\s+)?~?${escapeRegExp(node.name)}\s*[(<]`).exec(text); + const written = name ? qualifiersBefore(text, name.index + 2).filter((q) => q.args) : []; + const out: Template[] = []; + if (heads.length !== written.length && heads.length !== written.length + 1) return out; + if (heads.length === written.length + 1) out.push(this.functionTemplate(node, heads[heads.length - 1]!)); + const segments = node.qualifiedName.split('::'); + for (let i = written.length - 1; i >= 0; i--) { + const q = written[i]!; + const head = heads[i]!; + const at = segments.lastIndexOf(q.name, segments.length - 2); + const decls = at < 0 ? [] : this.classesNamed(segments.slice(0, at + 1).join('::')); + const decl = decls.find((n) => n.filePath === node.filePath) ?? decls.find((n) => this.sees(node.filePath, n)); + if (!decl || head.length === 0) continue; + out.push(this.classTemplate(decl, head, isOwnHead(q.args!, head) ? null : q.args)); + } + return out; + } + + /** The typed fixture a gtest `TYPED_TEST` body is a member of, with `TypeParam` as its first parameter. */ + private typedTestTemplate(caller: Node): Template | null { + if (caller.kind !== 'function' || !/^TYPED_TEST(?:_P)?$/.test(caller.name)) return null; + const head = this.lines(caller.filePath).slice(caller.startLine - 1, caller.startLine + 1).join(' '); + const fixture = /\bTYPED_TEST(?:_P)?\s*\(\s*((?:::\s*)?[A-Za-z_]\w*(?:\s*::\s*[A-Za-z_]\w*)*)\s*,/.exec(head)?.[1]; + if (!fixture) return null; + const place = this.placeIn(caller, caller.startLine); + const found = cppClassWritten(fixture.replace(/\s+/g, ''), this.site(place), place.ref, this.ctx); + if (!found?.exact || !CLASS_KINDS.has(found.node.kind)) return null; + return this.classTemplate(found.node, [{ name: 'TypeParam', pack: false, fallback: null }], null); + } + + /** The templates around a declaration, innermost first, each with the parameters its body sees. */ + templatesAround(node: Node): readonly Template[] { + const hit = this.templates.get(node.id); + if (hit) return hit; + const out: Template[] = []; + this.templates.set(node.id, out); + const heads = cppTemplateHeads(node, this.ctx); + const around = this.declarationAround(node); + if (heads.length > 0) { + const own = heads[heads.length - 1]!; + if (CLASS_KINDS.has(node.kind)) { + const template = this.templateOfClass(node, own); + if (template) out.push(template); + } else if (node.kind === 'type_alias') { + out.push({ key: `alias:${node.qualifiedName}`, kind: 'alias', name: node.name, node, parameters: own, pattern: null }); + } else if (FUNCTION_KINDS.has(node.kind)) { + if (around || !cppParentScope(node.qualifiedName)) out.push(this.functionTemplate(node, own)); + else out.push(...this.outOfLineTemplates(node, heads)); + } + } + const typed = this.typedTestTemplate(node); + if (typed) out.push(typed); + if (around) out.push(...this.templatesAround(around)); + return out; + } + + private slotNamed(name: string, templates: readonly Template[]): Slot | null { + for (const template of templates) { + const index = template.parameters.findIndex((p) => p.name === name); + if (index >= 0) return { template, index }; + } + return null; + } + + /** + * The parameter a type written as `segments` starts with where `ref` is + * made, with the member types after it: itself, or through an alias + * (`using KeyGen = typename TypeParam::KeyGen;`). + */ + private parameterOf(segments: readonly string[], ref: UnresolvedRef, caller: Node): { slot: Slot; parameter: string; types: string[] } | null { + const head = segments[0]!; + const direct = this.slotNamed(head, this.templatesAround(caller)); + if (direct) return { slot: direct, parameter: head, types: segments.slice(1) }; + const alias = cppAliasWrittenAt(head, ref, this.ctx); + const written = alias && cppTypeSegments(alias.text); + if (!written) return null; + const slot = this.slotNamed(written[0]!, this.templatesAround(alias.alias ?? caller)); + return slot ? { slot, parameter: written[0]!, types: [...written.slice(1), ...segments.slice(1)] } : null; + } + + /** + * The names a function's lines declare with a type that starts with a name + * of `types`, a parameter or an alias: all a call on a variable of the + * function's own can go through. Typing every variable a template calls + * something on would be most of what this pass costs. + */ + private localValues(caller: Node, types: ReadonlySet): ReadonlySet { + const hit = this.values.get(caller.id); + if (hit) return hit; + const values = new Set(); + const text = this.lines(caller.filePath).slice(caller.startLine - 1, caller.endLine ?? caller.startLine).join('\n').replace(CV_QUALIFIER, ' '); + for (const m of text.matchAll(DECLARED_VALUE)) if (types.has(m[1]!)) values.add(m[2]!); + this.values.set(caller.id, values); + return values; + } + + /** A call on a variable whose declared type is a parameter: `shard.Clear()` on a `CacheShard& shard`, `p->Clear()` on a `T* p`. */ + private throughValue(ref: UnresolvedRef, caller: Node, receiver: string, member: string, types: ReadonlySet): Through | null { + if (this.templatesAround(caller).length === 0) return null; + let text: string | undefined; + if (this.localValues(caller, types).has(receiver)) { + const declared = cppArgumentType(receiver, ref, this.ctx); + text = declared?.kind === 'type' ? declared.text : undefined; + } else { + // (Typing an argument leaves out a member of a template parameter's type.) + const field = cppCallersMember(receiver, caller, ref, this.ctx)?.text; + const head = field ? cppTypeSegments(field)?.[0] : undefined; + if (head && types.has(head) && !isCppLocalName(receiver, caller, ref, this.ctx)) text = field; + } + const written = text ? cppTypeSegments(text) : null; + const type = written && this.parameterOf(written, ref, caller); + if (!type) return null; + // A `->` on a value of the parameter's type is the argument's own `operator->`. + const line = this.lines(ref.filePath)[ref.line - 1] ?? ''; + const arrow = new RegExp(String.raw`\b${receiver}\s*->\s*(?:template\s+)?${member}\b`).test(line); + return arrow !== isCppPointerType(text!) ? null : { ...type, member, arity: null }; + } + + /** How a reference goes through a parameter, or null when it doesn't. */ + through(ref: UnresolvedRef, caller: Node, types: ReadonlySet): Through | null { + const value = VALUE_CALL.exec(ref.referenceName); + if (value) return this.throughValue(ref, caller, value[1]!, value[2]!, types); + const constructor = /^(.*)::([^:]+)\/(\d+)$/.exec(ref.referenceName); + const segments = (constructor ? constructor[1]! : ref.referenceName).replace(/::\s*template\s+/g, '::').split('::'); + if (!segments.every((s) => IDENTIFIER.test(s))) return null; + const type = this.parameterOf(segments, ref, caller); + if (!type) return null; + if (constructor) return { ...type, member: null, arity: Number(constructor[3]) }; + if (ref.referenceKind === 'instantiates') return { ...type, member: null, arity: null }; + return { ...type, types: type.types.slice(0, -1), member: type.types[type.types.length - 1]!, arity: null }; + } + + private source(file: string): Source | null { + let source = this.sources.get(file); + if (source === undefined) { + const raw = this.ctx.readFile(file); + const text = raw === null ? null : stripCommentsForRegex(raw, 'cpp'); + source = text === null ? null : { text, lineAt: makeLineAt(text, 1) }; + this.sources.set(file, source); + } + return source; + } + + private declarationsIn(file: string): Node[] { + let decls = this.declarations.get(file); + if (!decls) { + decls = this.ctx.getNodesInFile(file).filter((n) => DECLARATION_KINDS.has(n.kind) && (n.language === 'cpp' || n.language === 'c')); + this.declarations.set(file, decls); + } + return decls; + } + + /** The tightest declaration whose lines hold `line`. */ + private declarationAt(file: string, line: number): Node | null { + let best: Node | null = null; + for (const n of this.declarationsIn(file)) { + if (n.startLine > line || (n.endLine ?? n.startLine) < line) continue; + if (!best || n.startLine > best.startLine || (n.startLine === best.startLine && (n.endLine ?? n.startLine) < (best.endLine ?? best.startLine))) best = n; + } + return best; + } + + /** The namespace at `offset`, outside every declaration: the index holds no node for one. */ + private namespaceAt(file: string, offset: number, line: number): string { + const source = this.source(file); + if (!source) return ''; + let blocks = this.namespaces.get(file); + if (!blocks) this.namespaces.set(file, (blocks = namespaceBlocks(source.text))); + let path: readonly string[] = []; + for (const block of blocks) if (block.start <= offset && offset < block.end) path = block.path; + const macros = cppMacroNamespaceFrames(file, this.ctx).filter((f) => f.start <= line && f.end >= line).flatMap((f) => f.path); + return [...macros, ...path].join('::'); + } + + private placeIn(around: Node, line: number): Place { + const file = around.filePath; + const scope = CLASS_KINDS.has(around.kind) ? around.qualifiedName : cppParentScope(around.qualifiedName); + const ref: UnresolvedRef = { fromNodeId: around.id, referenceName: '', referenceKind: 'references', line, column: 0, filePath: file, language: 'cpp' }; + return { file, line, scope, around, templates: [], parameters: new Set(), ref }; + } + + private parametersOf(templates: readonly Template[]): Set { + const parameters = new Set(); + for (const t of templates) for (const p of t.parameters) if (p.name) parameters.add(p.name); + return parameters; + } + + /** Where a caller's call, or the body of a template, writes a name. */ + private placeOfCaller(caller: Node, line: number): Place { + const templates = this.templatesAround(caller); + return { ...this.placeIn(caller, line), templates, parameters: this.parametersOf(templates) }; + } + + /** Where the text at `offset` of `file` is written. */ + private placeAt(file: string, offset: number): Place { + const source = this.source(file)!; + const line = source.lineAt(offset); + const around = this.declarationAt(file, line); + const place: Place = around ? this.placeOfCaller(around, line) : { + file, line, around: null, templates: [], parameters: new Set(), + scope: this.namespaceAt(file, offset, line), + ref: { fromNodeId: '', referenceName: '', referenceKind: 'references', line, column: 0, filePath: file, language: 'cpp' }, + }; + // A head no node holds: a prototype's, or a default's in a head. + const start = Math.max(source.text.lastIndexOf(';', offset), source.text.lastIndexOf('{', offset), source.text.lastIndexOf('}', offset)) + 1; + const statement = source.text.slice(start, offset); + if (statement.includes('template')) { + const parameters = new Set(place.parameters); + for (const name of cppTypeNamesDeclared(`${statement}>`)) parameters.add(name); + place.parameters = parameters; + } + return place; + } + + private site(place: Place): CppNameSite { + return { scope: place.scope, file: place.file, line: place.line, parameters: place.parameters, sees: (n) => this.sees(place.file, n) }; + } + + /** + * Can code in `file` see `decl`: its own, what it includes, or a header + * beside it in one translation unit? A project's vendored copy of itself + * declares the same names, and nothing it declares is any other file's. + */ + private sees(file: string, decl: Node): boolean { + if (decl.filePath === file) return true; + const key = `${file}\0${decl.filePath}`; + let seen = this.seen.get(key); + if (seen === undefined) { + const includers = cppIncluders(decl.filePath, this.ctx); + seen = includers.has(file) || cppIncluders(file, this.ctx).has(decl.filePath); + if (!seen) for (const unit of cppIncluders(file, this.ctx)) if (includers.has(unit)) { seen = true; break; } + this.seen.set(key, seen); + } + return seen; + } + + /** Is the line a preprocessor directive, or a continuation of one? */ + private isDirective(file: string, line: number): boolean { + const lines = this.lines(file); + let at = line - 1; + while (at > 0 && /\\\s*$/.test(lines[at - 1] ?? '')) at--; + return /^\s*#/.test(lines[at] ?? ''); + } + + /** Is what the line holds at the column code, not a string or a comment? */ + private isCode(file: string, line: number, column: number, name: string): boolean { + return cppLiteralsBlanked(this.lines(file)[line - 1] ?? '', line, cppNonCodeLines(file, this.ctx)).startsWith(name, column); + } + + /** + * Is the declaration a lookup found in `file` `template`? Two headers can + * declare one name for separate translation units, and so can two source + * files: abseil's map and set tests each have a `ConstructorTest`. + */ + private isTemplate(found: Node, template: Template, file: string): boolean { + return found.qualifiedName === template.node.qualifiedName && this.sees(file, template.node); + } + + /** Every `name<…>` a class or alias template is written as, and its specializations, in file and offset order. */ + private async writtenAs(template: Template): Promise { + const hit = this.written.get(template.key); + if (hit) return hit; + const out: WrittenTemplate = { instantiations: [], specializations: [] }; + this.written.set(template.key, out); + const { name } = template; + for (const file of this.filesNaming.get(name) ?? []) { + if ((++this.scanned & 15) === 0) await this.onYield(); + const source = this.source(file); + if (!source) continue; + const { text } = source; + for (let at = text.indexOf(name); at >= 0; at = text.indexOf(name, at + name.length)) { + if (at > 0 && isWord(text.charCodeAt(at - 1))) continue; + let open = at + name.length; + while (open < text.length && isSpace(text.charCodeAt(open))) open++; + if (text[open] !== '<') continue; + const close = closingAngle(text, open); + if (close < 0) continue; + const before = text.slice(Math.max(0, at - HEAD_WINDOW), at); + // (`x.get()` and `p->get()` call a member template.) + if (/(?:\.|->)\s*(?:template\s+)?$/.test(before) || /\bfriend\s+(?:class|struct)\s+[\w:\s]*$/.test(before)) continue; + const line = source.lineAt(at); + if (this.isDirective(file, line) || !this.isCode(file, line, at - text.lastIndexOf('\n', at) - 1, name)) continue; + const place = this.placeAt(file, at); + const written = [...qualifiersBefore(text, at).map((q) => q.name), name].join('::'); + const site = template.kind === 'alias' ? { ...this.site(place), declared: true } : this.site(place); + const found = cppClassWritten(written, site, place.ref, this.ctx); + if (!found?.exact || !this.isTemplate(found.node, template, file)) continue; + const args = argumentsBetween(text, open, close).map((a) => ({ text: a.text, place: { ...place, line: source.lineAt(a.offset) } })); + const after = text.slice(close + 1, close + 8); + // `template class Name;` instantiates; `template <…> class Name {` specializes. + const keyword = CLASS_HEAD.exec(before); + const head = keyword && /^\s*(?:[{;]|:(?!:)|final\b)/.test(after) ? /\btemplate\s*<([^;{}]*)>\s*$/.exec(before.slice(0, keyword.index)) : null; + if (head) { + out.specializations.push(specializationOf(args.map((a) => argumentKey(a.text)), cppTemplateParametersIn(head[1]!))); + continue; + } + out.instantiations.push({ args }); + } + } + if (template.kind === 'class') for (const list of this.typedLists(template)) out.instantiations.push(list); + return out; + } + + /** gtest's typed fixtures, by their last name, with where each is given its list of types. */ + private typedSuites(): Map> { + if (this.suites) return this.suites; + const suites = new Map>(); + for (const file of this.typedFiles) { + const source = this.source(file); + if (!source) continue; + for (const m of source.text.matchAll(TYPED_SUITE)) { + if (this.isDirective(file, source.lineAt(m.index!))) continue; + const fixture = m[1]!.replace(/\s+/g, ''); + const last = fixture.slice(fixture.lastIndexOf(':') + 1); + const list = suites.get(last); + const suite = { file, offset: m.index! + m[0].length, fixture }; + if (list) list.push(suite); + else suites.set(last, [suite]); + } + } + this.suites = suites; + return suites; + } + + /** The instantiations `TYPED_TEST_SUITE(Fixture, Types)` writes for a fixture: one per listed type. */ + private typedLists(template: Template): Instantiation[] { + const out: Instantiation[] = []; + for (const suite of this.typedSuites().get(template.name) ?? []) { + const place = this.placeAt(suite.file, suite.offset); + const found = cppClassWritten(suite.fixture, this.site(place), place.ref, this.ctx); + if (!found?.exact || !this.isTemplate(found.node, template, suite.file)) continue; + for (const arg of this.typeList(suite.file, suite.offset, 0)) out.push({ args: [arg] }); + } + return out; + } + + /** The types a `::testing::Types<…>` written at `offset` of `file` lists, directly or through an alias of it. */ + private typeList(file: string, offset: number, hops: number): Argument[] { + const source = this.source(file); + if (!source || hops > 3) return []; + const { text } = source; + const from = text.slice(offset, offset + HEAD_WINDOW); + const list = /\bTypes\s* ({ text: a.text, place: { ...place, line: source.lineAt(a.offset) } })); + } + const name = from.slice(0, end).replace(/\s+/g, ''); + if (!cppTypeSegments(name)) return []; + const place = this.placeAt(file, offset); + const found = cppClassWritten(name, { ...this.site(place), declared: true }, place.ref, this.ctx); + if (!found || found.node.kind !== 'type_alias') return []; + const alias = found.node; + const aliased = this.source(alias.filePath); + if (!aliased) return []; + let start = 0; + for (let i = 1; i < alias.startLine; i++) start = aliased.text.indexOf('\n', start) + 1; + return this.typeList(alias.filePath, start + alias.startColumn, hops + 1); + } + + /** A function template's calls, in file and line order, with the arguments each writes. */ + private callsOf(template: Template): readonly Call[] { + const hit = this.calls.get(template.key); + if (hit) return hit; + const out: Call[] = []; + for (const edge of this.queries.getIncomingEdges(template.node.id, ['calls', 'references'])) { + const caller = this.ctx.getNodeById?.(edge.source); + if (!caller || edge.line === undefined || edge.line === null || !this.sees(caller.filePath, template.node)) continue; + const ref: UnresolvedRef = { + fromNodeId: caller.id, referenceName: template.name, referenceKind: 'calls', + line: edge.line, column: edge.column ?? 0, filePath: caller.filePath, language: 'cpp', + }; + const place = this.placeOfCaller(caller, edge.line); + const args = cppCallTemplateArguments(ref, this.ctx)?.map((text) => ({ text, place })) ?? null; + out.push({ ref, place, args }); + } + out.sort((a, b) => (a.ref.filePath < b.ref.filePath ? -1 : a.ref.filePath > b.ref.filePath ? 1 : 0) || a.ref.line - b.ref.line || a.ref.column - b.ref.column); + this.calls.set(template.key, out); + return out; + } + + /** + * The argument a call deduces for a parameter a function's own parameter + * has as its whole type (`T`, `const T&`, `T&&`, `T*`), when the call's + * argument there has a written type: the deduction has nothing else to go + * on, and what it deduces is exact. + */ + private deduced(template: Template, index: number, call: Call): Argument | null { + const name = template.parameters[index]!.name; + if (!name) return null; + const list = cppOverloadParameters(template.node, template.name, this.ctx); + if (list === null) return null; + const type = new RegExp(String.raw`^(?:const\s+)?${escapeRegExp(name)}\s*(?:const\s*)?(\*|&&?)?\s*(?:const\s+)?(?:[A-Za-z_]\w*)?\s*(?:=.*)?$`, 's'); + const parameters = splitCppTopLevel(list); + const position = parameters.findIndex((p) => type.test(p.trim())); + if (position < 0) return null; + const given = cppCallArguments(call.ref, template.name, this.ctx)?.[position]; + const argument = given === undefined ? null : cppArgumentType(given, call.ref, this.ctx); + if (argument?.kind !== 'type') return null; + const value = argument.text.replace(/&+\s*$/, '').trim(); + if (type.exec(parameters[position]!.trim())?.[1] !== '*') return { text: value, place: call.place }; + return /\*\s*(?:const\s*)?$/.test(value) ? { text: value.replace(/\*\s*(?:const\s*)?$/, '').trim(), place: call.place } : null; + } + + /** The arguments a site gives a parameter of the template itself; its default stands in for one it doesn't write. */ + private argumentsFor(slot: Slot, args: readonly Argument[]): readonly Argument[] { + const parameter = slot.template.parameters[slot.index]!; + if (parameter.pack) return args.slice(slot.index); + const given = args[slot.index]; + if (given) return [given]; + if (parameter.fallback === null || slot.template.kind === 'function') return []; + const decl = slot.template.node; + return [{ text: parameter.fallback, place: this.placeOfCaller(decl, decl.startLine) }]; + } + + /** + * What the specialization binds its parameters to, when `args` can be taken + * by it: `Foo` for `T` of `Name` given `Name`. Null when what each + * side writes tells it isn't taken. A condition (`enable_if_t<…>`) can't be + * told from what is written, and counts as taken. + */ + private bindings(spec: Specialization, primary: readonly CppTemplateParameter[], args: readonly string[]): Map | null { + const bound = new Map(); + for (let i = 0; i < spec.pattern.length; i++) { + const item = spec.pattern[i]!; + if (item.parameter) { + const given = item.pack ? args.slice(i) : args[i] !== undefined ? [args[i]!] : []; + bound.set(item.parameter, [...(bound.get(item.parameter) ?? []), ...given]); + continue; + } + if (item.condition) continue; + const given = args[i] ?? (primary[i]?.fallback ? argumentKey(primary[i]!.fallback!) : null); + if (given === null) return null; + if (!item.shape) { + if (given !== item.text && !given.endsWith(`::${item.text}`) && !item.text.endsWith(`::${given}`)) return null; + continue; + } + const m = item.shape.exec(given); + if (!m) return null; + item.words.forEach((w, k) => bound.set(w, [m[k + 1]!])); + } + return bound; + } + + /** The arguments each instantiation gives a parameter, once specializations have taken theirs. */ + private async argumentsOf(slot: Slot): Promise { + const { template, index } = slot; + if (template.kind === 'function') { + return this.callsOf(template).flatMap((call) => { + const written = this.argumentsFor(slot, call.args ?? []); + const deduced = written.length === 0 ? this.deduced(template, index, call) : null; + return deduced ? [deduced] : written; + }); + } + const { instantiations, specializations } = await this.writtenAs(template); + const primary = specializations.length === 0 ? template.parameters : this.primaryParameters(template); + const own = template.pattern?.map(argumentKey).join() ?? null; + const out: Argument[] = []; + for (const site of instantiations) { + const given = specializations.length > 0 ? site.args.map((a) => argumentKey(a.text)) : []; + const taken = specializations.flatMap((spec) => { + const bound = this.bindings(spec, primary, given); + return bound ? [{ spec, bound }] : []; + }); + if (own === null) { + if (taken.length === 0) out.push(...this.argumentsFor(slot, site.args)); + continue; + } + // A specialization's own members, from the instantiations only it takes. + if (taken.length !== 1 || taken[0]!.spec.written !== own) continue; + const name = template.parameters[index]?.name; + const place = site.args[0]?.place; + if (name && place) for (const text of taken[0]!.bound.get(name) ?? []) out.push({ text, place }); + } + return out; + } + + /** The head of the template itself, whichever declaration a reference is in. */ + private primaryParameters(template: Template): readonly CppTemplateParameter[] { + if (!template.pattern) return template.parameters; + for (const decl of this.classesNamed(template.node.qualifiedName)) { + const heads = cppTemplateHeads(decl, this.ctx); + const head = heads[heads.length - 1]; + if (head && !this.templateOfClass(decl, head)?.pattern) return head; + } + return []; + } + + /** + * The classes a parameter reaches, each once, with the argument that writes + * it: those its own instantiations write, then those of each template whose + * parameter they pass on, a hop at a time and each parameter once, so a + * cycle ends. It stops past the cap: all that is known then is the cap. + */ + async classesOf(start: Slot): Promise> { + const classes = new Map(); + const seen = new Set([slotKey(start)]); + let frontier = [start]; + for (let hop = 0; hop <= MAX_TEMPLATE_HOPS && frontier.length > 0; hop++) { + const next: Slot[] = []; + for (const slot of frontier) { + for (const arg of await this.argumentsOf(slot)) { + const text = arg.text.replace(/\s*\.\.\.$/, '').trim(); + const name = text.replace(/\b(?:const|volatile|typename)\b/g, ' ').trim(); + const outer = IDENTIFIER.test(name) ? this.slotNamed(name, arg.place.templates) : null; + if (outer) { + if (!seen.has(slotKey(outer))) next.push(outer); + seen.add(slotKey(outer)); + continue; + } + const node = this.classWritten(text, arg.place); + if (node && !classes.has(node.id)) classes.set(node.id, { node, at: arg }); + if (classes.size > CPP_TEMPLATE_ARGUMENT_CAP) return classes; + } + } + frontier = next; + } + return classes; + } + + /** The project class an argument names, or null. */ + private classWritten(text: string, place: Place): Node | null { + const plain = stripCppTemplateArguments(text); + if (/[&*()[\]]/.test(plain) || isCppPointerType(text) || /\bdecltype\b/.test(plain)) return null; + const segments = cppTypeSegments(text); + if (!segments) return null; + if (place.around && FUNCTION_KINDS.has(place.around.kind)) { + const fn = place.around; + if (segments.length === 1) { + // A class local to the function that writes it, which the index names after the function alone. + const local = this.declarationsIn(place.file).filter((n) => CLASS_KINDS.has(n.kind) && n.name === segments[0] && + n.startLine >= fn.startLine && (n.endLine ?? n.startLine) <= (fn.endLine ?? fn.startLine) && n.startLine <= place.line); + if (local.length > 0) return local[local.length - 1]!; + } + if (cppDeclaresLocalAlias(text, place.ref, this.ctx)) { + const aliased = resolveCppAliasedType(text, place.ref, this.ctx); + const named = aliased && !aliased.pointer ? cppAliasedClass(aliased, this.ctx, place.ref) : undefined; + return named ? this.classesNamed(named).find((n) => this.sees(place.file, n)) ?? null : null; + } + } + const found = cppClassWritten(text, this.site(place), place.ref, this.ctx); + // (A lookup falls back on what it can't see when that is all a name has: a copy + // of the project declares it plainly where the project's own macro hides it.) + return found?.exact && CLASS_KINDS.has(found.node.kind) && this.sees(place.file, found.node) ? found.node : null; + } + + /** What a reference through a parameter names in the class its argument is, or null. */ + target(cls: Node, through: Through, ref: UnresolvedRef): Node | null { + const { types, member, arity } = through; + const spelled = types.join('::'); + const visible = (n: Node): boolean => this.sees(cls.filePath, n); + if (member === null && arity === null) { + if (types.length === 0) return cls; + const named = cppTypeNamed(spelled, [cls.qualifiedName], ref, this.ctx, visible); + if (!named) return null; + if ('alias' in named) return named.alias; + if ('enums' in named) return named.enums[0] ?? null; + return this.classNode(named.cls, cls); + } + let owner: Node | null = cls; + if (types.length > 0) { + const named = cppClassNamed(spelled, [cls.qualifiedName], ref, this.ctx, visible); + owner = named && 'cls' in named ? this.classNode(named.cls, cls) : null; + } + if (!owner) return null; + if (arity !== null) { + const made = matchCppConstructor({ ...ref, referenceName: `::${owner.qualifiedName}::${owner.name}/${arity}` }, this.ctx); + return made ? this.ctx.getNodeById?.(made.targetNodeId) ?? null : null; + } + return cppMethodOf(owner, member!, ref, this.ctx); + } + + private classNode(qualified: string, near: Node): Node | null { + const classes = this.classesNamed(qualified); + return classes.find((n) => n.filePath === near.filePath) ?? classes.find((n) => this.sees(near.filePath, n)) ?? null; + } +} + +const position = (a: UnresolvedRef, b: UnresolvedRef): number => + (a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0) || a.line - b.line || a.column - b.column || + (a.referenceName < b.referenceName ? -1 : a.referenceName > b.referenceName ? 1 : 0) || + (a.referenceKind < b.referenceKind ? -1 : a.referenceKind > b.referenceKind ? 1 : 0); + +const EDGE_KIND: Readonly> = { calls: 'calls', instantiates: 'instantiates', function_ref: 'references' }; + +/** + * One edge per class a failed reference through a template parameter reaches + * (see the top of this file). Reads the graph and source only. + */ +export async function cppTemplateArgumentEdges( + queries: QueryBuilder, + ctx: ResolutionContext, + onYield: MaybeYield, +): Promise { + const reader = new TemplateInstantiations(queries, ctx, onYield); + // A name in a template's head or an alias, by file: most references go no further. + const declared = await reader.read(); + if (declared.size === 0) return []; + const refs: UnresolvedRef[] = []; + for (const ref of queries.iterateFailedCppTypeReferences()) { + const names = declared.get(ref.filePath as string); + const head = /^[A-Za-z_]\w*/.exec(ref.referenceName)?.[0]; + if (names && head && (names.has(head) || ref.referenceName.includes('.'))) refs.push(ref as UnresolvedRef); + } + refs.sort(position); + const groups = new Map }>(); + let scanned = 0; + for (const ref of refs) { + if ((++scanned & 255) === 0) await onYield(); + const caller = ctx.getNodeById?.(ref.fromNodeId); + if (!caller) continue; + const through = reader.through(ref, caller, declared.get(ref.filePath)!); + if (!through) continue; + const key = slotKey(through.slot); + const group = groups.get(key); + if (group) group.refs.push({ ref, through }); + else groups.set(key, { slot: through.slot, refs: [{ ref, through }] }); + } + const edges: Edge[] = []; + for (const group of groups.values()) { + await onYield(); + const classes = await reader.classesOf(group.slot); + if (classes.size > CPP_TEMPLATE_ARGUMENT_CAP) continue; + for (const { ref, through } of group.refs) { + for (const { node, at } of classes.values()) { + const target = reader.target(node, through, ref); + if (!target || target.id === ref.fromNodeId) continue; + edges.push({ + source: ref.fromNodeId, + target: target.id, + kind: EDGE_KIND[ref.referenceKind]!, + line: ref.line, + column: ref.column, + provenance: 'heuristic', + metadata: { + synthesizedBy: 'cpp-template-argument', + via: through.parameter, + argument: node.name, + registeredAt: `${at.place.file}:${at.place.line}`, + ...(ref.referenceKind === 'function_ref' ? { fnRef: true } : {}), + }, + }); + } + } + } + return edges; +} diff --git a/src/resolution/cpp-type-aliases.ts b/src/resolution/cpp-type-aliases.ts index e28709d6df..217bfdabb9 100644 --- a/src/resolution/cpp-type-aliases.ts +++ b/src/resolution/cpp-type-aliases.ts @@ -400,15 +400,21 @@ export function clearCppTypeAliasMemos(context: ResolutionContext): void { LOCAL_ALIAS_LINES.delete(context); } +/** + * What may come between a `template <…>` head and where its declaration + * starts: protobuf's `template ` / + * `PROTOBUF_NDEBUG_INLINE static T* Create(…)`. + */ +const HEAD_SPECIFIERS = String.raw`(?:\s*(?:\[\[[^\]]*\]\]|(?:static|inline|constexpr|consteval|virtual|explicit|friend|extern)\b|[A-Z][A-Z0-9]*_[A-Z0-9_]*\b(?:\s*\([^()]*\))?))*\s*`; +const HEAD_ENDS_DECLARATION = new RegExp(String.raw`^template\s*<([\s\S]*)>${HEAD_SPECIFIERS}$`); +const ONLY_SPECIFIERS = new RegExp(`^${HEAD_SPECIFIERS}$`); + /** 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 { // (A header can take several lines: fmt's `join` spends four on its parameters' defaults.) const before = [...lines.slice(Math.max(0, startLine - 9), startLine - 1), (lines[startLine - 1] ?? '').slice(0, startColumn)].join('\n'); const at = before.lastIndexOf('template'); - // Specifiers, attributes and macros may come between the header and where - // the declaration starts: protobuf's `template ` / `PROTOBUF_NDEBUG_INLINE static T* Create(…)`. - const header = at < 0 ? null : /^template\s*<([\s\S]*)>(?:\s*(?:\[\[[^\]]*\]\]|(?:static|inline|constexpr|consteval|virtual|explicit|friend|extern)\b|[A-Z][A-Z0-9]*_[A-Z0-9_]*\b(?:\s*\([^()]*\))?))*\s*$/.exec(before.slice(at)); + const header = at < 0 ? null : HEAD_ENDS_DECLARATION.exec(before.slice(at)); if (!header) return; for (const item of splitTopLevel(header[1]!)) addTemplateParameter(item, names); } @@ -792,6 +798,76 @@ export function cppTemplateParameters(node: Node, context: ResolutionContext): R return templateParametersAround(node, context); } +/** One parameter of a C++ `template <…>` head, in order: an unnamed one holds its place. */ +export interface CppTemplateParameter { + name: string | null; + /** `typename... Ts`: every argument from here on. */ + pack: boolean; + /** Its default, as written: `TcParser` of `typename Parser = TcParser`. */ + fallback: string | null; +} + +/** One item of a `template <…>` head. */ +function templateParameter(item: string): CppTemplateParameter { + const declared = stripCppTemplateArguments(item); + const at = declared.indexOf('='); + const name = /([A-Za-z_]\w*)\s*$/.exec(at < 0 ? declared : declared.slice(0, at))?.[1]; + // (A default can hold a `=` of its own, so it is read from the item as written.) + const eq = item.indexOf('='); + return { + name: name && name !== 'typename' && name !== 'class' ? name : null, + pack: declared.includes('...'), + fallback: at < 0 || eq < 0 ? null : item.slice(eq + 1).trim(), + }; +} + +/** The parameters of a `template <…>` head, from what its brackets hold. */ +export function cppTemplateParametersIn(inner: string): CppTemplateParameter[] { + return inner.trim() ? splitTopLevel(inner).map(templateParameter) : []; +} + +/** + * The `template <…>` heads right before a declaration, outermost first, each + * with its parameters in order: one for a template, none for anything else, + * and two for a member template defined outside its class template + * (`template template void C::f()`). + */ +export function cppTemplateHeads(node: Node, context: ResolutionContext): CppTemplateParameter[][] { + const lines = linesOf(node.filePath, context); + if (!lines) return []; + const before = [...lines.slice(Math.max(0, node.startLine - 13), node.startLine - 1), (lines[node.startLine - 1] ?? '').slice(0, node.startColumn)].join('\n'); + const heads: CppTemplateParameter[][] = []; + const starts = [...before.matchAll(/\btemplate\s*= 0; i--) { + const open = starts[i]!.index! + starts[i]![0].length - 1; + const close = templateHeaderEnd(before, open); + if (close < 0 || !ONLY_SPECIFIERS.test(before.slice(close + 1, end))) break; + const inner = before.slice(open + 1, close); + heads.unshift(cppTemplateParametersIn(inner)); + end = starts[i]!.index!; + } + return heads; +} + +/** + * The type an alias in scope where `ref` is made declares, as written, with + * the alias when the index holds one: `typename TypeParam::KeyGen` for + * `using KeyGen = typename TypeParam::KeyGen;` in the calling function, or an + * alias of the calling function's class. Undefined when no such alias has + * that name. + */ +export function cppAliasWrittenAt(name: string, ref: UnresolvedRef, context: ResolutionContext): { text: string; alias?: Node } | undefined { + const caller = context.getNodeById?.(ref.fromNodeId); + const local = localAliasedType(name, caller, ref, context); + if (local !== undefined) return local === null ? undefined : { text: local }; + const found = lookupUnqualified(name, false, scopesWithin(callerScope(caller)), ref, context); + if (!found || typeof found !== 'object' || !('alias' in found)) return undefined; + const text = declarationText(found.alias, context); + const written = text ? aliasedTypeIn(text, found.alias.name) : null; + return written ? { text: written, alias: found.alias } : undefined; +} + /** * What a written name declares, looked up from `scopes` (innermost first), and * how many of its segments that took: a class's member is looked up in the diff --git a/src/resolution/index.ts b/src/resolution/index.ts index a0196577dc..9a711db535 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -805,6 +805,16 @@ export class ReferenceResolver { } } + /** The text a walk over the indexed files reads: see ResolutionContext.scanFile. */ + private scanFile(filePath: string): string | null { + if (this.fileCache.has(filePath)) return this.fileCache.get(filePath) ?? null; + try { + return fs.readFileSync(path.join(this.projectRoot, filePath), 'latin1'); + } catch { + return null; + } + } + /** * Create the resolution context */ @@ -992,6 +1002,7 @@ export class ReferenceResolver { readFile: (filePath: string) => this.readFileCached(filePath), fileContains: (filePath: string, needle: string) => this.fileContains(filePath, needle), + scanFile: (filePath: string) => this.scanFile(filePath), getFileLines: (filePath: string) => { const cached = this.fileLinesCache.get(filePath); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 5d4e0cc8ee..a84db0fe0a 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -17369,7 +17369,7 @@ const CPP_CALLERS_MEMBERS = new WeakMap()): Node | null { +export function cppMethodOf(cls: Node, name: string, ref: UnresolvedRef, context: ResolutionContext, depth = 0, seen = new Set()): Node | null { const want = `${cls.qualifiedName}::${name}`; const named = context.getMethodMatches?.(cls.name, name, ref.language) ?? context.getNodesByName(name).filter((n) => n.kind === 'method' && sameLanguageFamily(n.language, ref.language)); @@ -19251,7 +19251,7 @@ export function cppSpecializationArguments(n: Node, context: ResolutionContext): * calls — `unsigned short` for fmt's `num_bits()` — or null * when it writes none, leaving them to be deduced. */ -function cppCallTemplateArguments(ref: UnresolvedRef, context: ResolutionContext): string[] | null { +export function cppCallTemplateArguments(ref: UnresolvedRef, context: ResolutionContext): string[] | null { const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/) ?? []; const text = lines.slice(ref.line - 1, ref.line + 3).join('\n'); return cppTemplateArgumentsAfter(text, ref.referenceName.split(/::|\.|->/).pop()!, Math.max(0, ref.column)); @@ -19327,7 +19327,7 @@ type CppArgumentType = * `TryLock(column_family[i], keys[i], …)` on a `std::vector`. Null * otherwise. (String literals are cppOverloadFit's.) */ -function cppArgumentType(arg: string, ref: UnresolvedRef, context: ResolutionContext): CppArgumentType | null { +export function cppArgumentType(arg: string, ref: UnresolvedRef, context: ResolutionContext): CppArgumentType | null { const text = arg.trim(); if (/^(?:u8|u|U|L)?'(?:[^'\\]|\\.)+'$/.test(text)) return { kind: 'char' }; if (text === 'true' || text === 'false') return { kind: 'bool' }; @@ -19644,7 +19644,7 @@ function isCppDependentBase(decl: Node, base: Node, context: ResolutionContext): * declaration in an earlier block the call is outside of counts too: the * name strategies decide, as before. */ -function isCppLocalName(name: string, caller: Node, ref: UnresolvedRef, context: ResolutionContext): boolean { +export function isCppLocalName(name: string, caller: Node, ref: UnresolvedRef, context: ResolutionContext): boolean { if (caller.filePath !== ref.filePath || CPP_CLASS_KINDS.has(caller.kind)) return false; const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/); if (!lines) return false; @@ -32833,7 +32833,7 @@ function matchCppMacroNamespaced(ref: UnresolvedRef, context: ResolutionContext) } /** Split `a, f(b, c), d` at its top-level commas. */ -function splitCppTopLevel(text: string): string[] { +export function splitCppTopLevel(text: string): string[] { const out: string[] = []; let depth = 0; // Where each template argument list open around the current character closes. @@ -32950,7 +32950,7 @@ function cppWithoutComments(text: string): string { return out; } -function cppCallArguments(ref: UnresolvedRef, name: string, context: ResolutionContext): string[] | null { +export function cppCallArguments(ref: UnresolvedRef, name: string, context: ResolutionContext): string[] | null { const list = cppParenListAfter(ref.filePath, ref.line, Math.max(0, ref.column), name, context); return list === null ? null : splitCppTopLevel(cppWithoutComments(list)); } @@ -32968,7 +32968,7 @@ const CPP_OVERLOAD_PARAMETERS = new WeakMap