Bladeren bron

feat(extraction): C++ inheritance (base_class_clause) + virtual-override synthesis

C/C++ direct dispatch already resolves well (redis 29k / leveldb 1.4k
cross-file calls). Two changes close the C++ virtual-dispatch gap:

- extractInheritance handled base_clause (PHP) but not C++'s
  base_class_clause, so C++ `extends` edges were missing/partial. Add the
  C++ branch (emit an extends ref per base type, skipping access
  specifiers) — leveldb extends 219→298.
- cpp-override synthesizer channel (the C++ analog of react-render): for
  each extends edge, link each base method → the subclass override of the
  same name, so trace/callees from a virtual/interface method reach the
  implementation. Gated to C++, capped per class. leveldb 12 precise edges
  (Iterator::Next/Seek/Prev → MergingIterator), 0 on C (redis) and TS
  (excalidraw). Test: base virtual → subclass override bridge.

Frontier: C callback structs (cmd->proc() → 422-way fan-out, too noisy)
and C++ pure-virtual base methods (declarations aren't nodes, so those
overrides can't bridge). Suite green (804).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Colby McHenry 1 maand geleden
bovenliggende
commit
49257e91b0
3 gewijzigde bestanden met toevoegingen van 115 en 1 verwijderingen
  1. 44 0
      __tests__/frameworks-integration.test.ts
  2. 21 0
      src/extraction/tree-sitter.ts
  3. 50 1
      src/resolution/callback-synthesizer.ts

+ 44 - 0
__tests__/frameworks-integration.test.ts

@@ -153,3 +153,47 @@ describe('Flutter end-to-end — setState→build synthesis', () => {
     cg.close();
   });
 });
+
+describe('C++ end-to-end — virtual override synthesis', () => {
+  let tmpDir: string | undefined;
+  afterEach(() => {
+    if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
+    tmpDir = undefined;
+  });
+
+  it('bridges a base virtual method to the subclass override', async () => {
+    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-'));
+    fs.writeFileSync(
+      path.join(tmpDir, 'iter.cpp'),
+      'class Iterator {\n' +
+        ' public:\n' +
+        '  virtual void Next() { }\n' +
+        '};\n' +
+        'class DBIter : public Iterator {\n' +
+        ' public:\n' +
+        '  void Next() override { advance(); }\n' +
+        '  void advance() { }\n' +
+        '};\n'
+    );
+
+    const cg = CodeGraph.initSync(tmpDir);
+    await cg.indexAll();
+
+    // Two methods named Next: the base virtual (lower line) and the override.
+    const nexts = cg
+      .getNodesByKind('method')
+      .filter((n) => n.name === 'Next')
+      .sort((a, b) => a.startLine - b.startLine);
+    expect(nexts.length).toBe(2);
+    const [baseNext, overrideNext] = nexts;
+
+    // A vtable call to Iterator::Next dispatches to DBIter::Next — bridge it so
+    // trace/callees from the interface method reaches the implementation.
+    const edge = cg
+      .getOutgoingEdges(baseNext!.id)
+      .find((e) => e.target === overrideNext!.id && e.kind === 'calls');
+    expect(edge, 'Iterator::Next should reach DBIter::Next via override synthesis').toBeDefined();
+
+    cg.close();
+  });
+});

+ 21 - 0
src/extraction/tree-sitter.ts

@@ -1799,6 +1799,27 @@ export class TreeSitterExtractor {
         }
       }
 
+      // C++ base classes: `class Derived : public Base, private Other` →
+      // base_class_clause holds access specifiers + base type(s). Emit an extends
+      // ref per base type (skip the public/private/protected keywords).
+      if (child.type === 'base_class_clause') {
+        for (const t of child.namedChildren) {
+          if (
+            t.type === 'type_identifier' ||
+            t.type === 'qualified_identifier' ||
+            t.type === 'template_type'
+          ) {
+            this.unresolvedReferences.push({
+              fromNodeId: classId,
+              referenceName: getNodeText(t, this.source),
+              referenceKind: 'extends',
+              line: t.startPosition.row + 1,
+              column: t.startPosition.column,
+            });
+          }
+        }
+      }
+
       if (
         child.type === 'implements_clause' ||
         child.type === 'class_interface_clause' ||

+ 50 - 1
src/resolution/callback-synthesizer.ts

@@ -278,6 +278,54 @@ function flutterBuildEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[
   return edges;
 }
 
+/**
+ * Phase 4c: C++ virtual override. A call through a base/interface pointer
+ * (`db->Get(...)`, `iter->Next()`) dispatches at runtime to a subclass override,
+ * but that hop is a vtable indirection — no static call edge — so a flow stops at
+ * the abstract base method. Bridge it like react-render: for each C++ class that
+ * `extends` a base, link each base method → the subclass method of the same name
+ * (the override), so trace/callees from the interface method reach the
+ * implementation(s). Over-approximation accepted (reachability-correct); capped
+ * per class and gated to C++ to avoid touching other languages' dispatch.
+ */
+function cppOverrideEdges(queries: QueryBuilder): Edge[] {
+  const edges: Edge[] = [];
+  const seen = new Set<string>();
+  const methodsOf = (classId: string): Node[] =>
+    queries
+      .getOutgoingEdges(classId, ['contains'])
+      .map((e) => queries.getNodeById(e.target))
+      .filter((n): n is Node => !!n && n.kind === 'method');
+  for (const cls of queries.getNodesByKind('class')) {
+    const subMethods = methodsOf(cls.id).filter((n) => n.language === 'cpp');
+    if (subMethods.length === 0) continue;
+    for (const ext of queries.getOutgoingEdges(cls.id, ['extends'])) {
+      const base = queries.getNodeById(ext.target);
+      if (!base || base.language !== 'cpp' || base.id === cls.id) continue;
+      const baseMethods = new Map(methodsOf(base.id).map((m) => [m.name, m]));
+      let added = 0;
+      for (const m of subMethods) {
+        if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
+        const bm = baseMethods.get(m.name);
+        if (!bm || bm.id === m.id) continue;
+        const key = `${bm.id}>${m.id}`;
+        if (seen.has(key)) continue;
+        seen.add(key);
+        edges.push({
+          source: bm.id,
+          target: m.id,
+          kind: 'calls',
+          line: bm.startLine,
+          provenance: 'heuristic',
+          metadata: { synthesizedBy: 'cpp-override', via: m.name, registeredAt: `${m.filePath}:${m.startLine}` },
+        });
+        added++;
+      }
+    }
+  }
+  return edges;
+}
+
 /**
  * Phase 5: React JSX child rendering. A component that returns `<Child .../>`
  * mounts Child — React calls it — but JSX instantiation isn't a static call edge,
@@ -424,10 +472,11 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
   const jsxEdges = reactJsxChildEdges(ctx);
   const vueEdges = vueTemplateEdges(ctx);
   const flutterEdges = flutterBuildEdges(queries, ctx);
+  const cppEdges = cppOverrideEdges(queries);
 
   const merged: Edge[] = [];
   const seen = new Set<string>();
-  for (const e of [...fieldEdges, ...emitterEdges, ...renderEdges, ...jsxEdges, ...vueEdges, ...flutterEdges]) {
+  for (const e of [...fieldEdges, ...emitterEdges, ...renderEdges, ...jsxEdges, ...vueEdges, ...flutterEdges, ...cppEdges]) {
     const key = `${e.source}>${e.target}`;
     if (seen.has(key)) continue;
     seen.add(key);