diff --git a/AGENTS.md b/AGENTS.md
index 88d0dfb26..ef1ba0682 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 f9854bde3..7ace81b24 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 000000000..673365d9f
--- /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 87199338f..f62ea6cd8 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 39b7fece4..7f5673ae1 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 b69341030..920217222 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 ca30a7d7e..a79a9f17c 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 000000000..959788092
--- /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