c-fnptr-kernel-sweep.test.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. /**
  2. * cFnPtr native extraction sweep — differential gate (task #5 step 2).
  3. *
  4. * The synthesizer's extraction sweep has two implementations: the JS regex
  5. * sweep and the kernel's `cfnptrScanFiles` (codegraph-kernel/src/cfnptr.rs).
  6. * They must be record-identical, which this suite pins end-to-end: the same
  7. * adversarial project is indexed twice — CODEGRAPH_KERNEL_CFNPTR toggled —
  8. * and the synthesized fn-pointer-dispatch edges must match EXACTLY, including
  9. * order (edge order is observable through FANOUT_CAP truncation).
  10. *
  11. * The fixture deliberately stacks the sweep's edge cases: macro-built tables
  12. * behind a non-indexed include, `#ifdef`-guarded inline structs, object-macro
  13. * type aliases, bare fn-pointer arrays with casts and designators, chained
  14. * receivers, field←field propagation, CRLF line endings, NBSP whitespace,
  15. * `\`-continuations, strings containing decoy syntax, an unterminated block
  16. * comment, and modifier/type backtracking shapes (`static x = {…}`).
  17. */
  18. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  19. import * as fs from 'node:fs';
  20. import * as path from 'node:path';
  21. import * as os from 'node:os';
  22. import { CodeGraph } from '../src';
  23. import { getKernel } from '../src/extraction/kernel/loader';
  24. const kernel = getKernel();
  25. const nativeAvailable = !!kernel && typeof kernel.cfnptrScanFiles === 'function';
  26. interface EdgeRow {
  27. src: string;
  28. tgt: string;
  29. via: string;
  30. line: number;
  31. }
  32. describe.runIf(nativeAvailable)('cFnPtr sweep: native vs JS differential', () => {
  33. let dir: string;
  34. beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cfp-k-')); });
  35. afterEach(() => {
  36. delete process.env.CODEGRAPH_KERNEL_CFNPTR;
  37. fs.rmSync(dir, { recursive: true, force: true });
  38. });
  39. const write = (rel: string, body: string) => {
  40. const p = path.join(dir, rel);
  41. fs.mkdirSync(path.dirname(p), { recursive: true });
  42. fs.writeFileSync(p, body);
  43. };
  44. const indexAndCollect = async (): Promise<{ edges: EdgeRow[]; nodes: number }> => {
  45. fs.rmSync(path.join(dir, '.codegraph'), { recursive: true, force: true });
  46. const cg = await CodeGraph.init(dir, { silent: true });
  47. await cg.indexAll();
  48. const db = (cg as any).db.db;
  49. const edges: EdgeRow[] = db
  50. .prepare(
  51. `SELECT s.name src, t.name tgt, json_extract(e.metadata,'$.via') via, e.line line
  52. FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target
  53. WHERE json_extract(e.metadata,'$.synthesizedBy') = 'fn-pointer-dispatch'
  54. ORDER BY e.id`
  55. )
  56. .all();
  57. const nodes = db.prepare('SELECT count(*) c FROM nodes').get().c as number;
  58. cg.close?.();
  59. return { edges, nodes };
  60. };
  61. const writeFixture = () => {
  62. // The git shape + designated init + assignment registration.
  63. write('cmd.c', `
  64. struct cmd { const char *name; int (*fn)(int argc); };
  65. static int cmd_add(int argc) { return argc + 1; }
  66. static int cmd_rm(int argc) { return argc - 1; }
  67. static struct cmd commands[] = {
  68. { "add", cmd_add },
  69. { "rm", cmd_rm },
  70. };
  71. int run(int i, int argc) { return commands[i].fn(argc); }
  72. `);
  73. // Macro-built table with an object-macro struct alias and a non-indexed
  74. // include, redis-style; plus a typedef'd fn-TYPE field.
  75. write('table.c', `
  76. #include "table.h"
  77. #include "cmds.def"
  78. int dispatch(struct client *c, int a) { return c->cur->proc(a); }
  79. `);
  80. write('table.h', `
  81. typedef int cmdProc(int a);
  82. #define TBL_STRUCT redisCmd
  83. struct redisCmd { const char *name; cmdProc *proc; };
  84. struct client { struct redisCmd *cur; };
  85. #define MK(nm, fn) { nm, fn }
  86. static int getCmd(int a);
  87. static int setCmd(int a);
  88. `);
  89. write('cmds.def', `
  90. struct TBL_STRUCT tbl[] = {
  91. MK("get", getCmd),
  92. MK("set", setCmd),
  93. };
  94. `);
  95. write('impl.c', `
  96. #include "table.h"
  97. static int getCmd(int a) { return a; }
  98. static int setCmd(int a) { return a + 1; }
  99. `);
  100. // #ifdef-guarded inline struct table + parenthesized subscript dispatch
  101. // (the vim shape), switched on by the includer.
  102. write('ex.c', `
  103. #define WANT_TABLE
  104. #include "ex_cmds.h"
  105. int exec(int i, int a) { return (cmdtab[i].cmd_fn)(a); }
  106. `);
  107. write('ex_cmds.h', `
  108. #ifdef WANT_TABLE
  109. static int ex_quit(int a);
  110. struct excmd { char *nm; int (*cmd_fn)(int); } cmdtab[] = { { "q", ex_quit } };
  111. #endif
  112. `);
  113. write('ex_impl.c', `static int ex_quit(int a) { return -a; }\n`);
  114. // Bare arrays: fn-TYPE typedef with star, casts, index designators, and a
  115. // same-named file-local collision (the SameBoy/Zend shapes).
  116. write('ops.c', `
  117. typedef int op_t(int);
  118. static int nop(int x) { return x; }
  119. static int halt(int x) { return -x; }
  120. static op_t *ops[4] = { nop, [2] = (op_t *)halt };
  121. int step(int pc, int x) { return ops[pc](x); }
  122. `);
  123. write('ops2.c', `
  124. typedef int op_t(int);
  125. static int trace(int x) { return x * 2; }
  126. static op_t *ops[4] = { trace };
  127. int step2(int pc, int x) { return (*ops[pc])(x); }
  128. `);
  129. // Field←field propagation (the hook_demo shape) + chained receiver.
  130. write('hook.c', `
  131. typedef void hook_fn(int);
  132. struct entry { const char *nm; hook_fn *fn; };
  133. struct hook { hook_fn *func; };
  134. static void on_commit(int v) { (void)v; }
  135. static struct entry entries[] = { { "commit", on_commit } };
  136. void wire(struct hook *h, struct entry *found) { h->func = found->fn; }
  137. void fire(struct hook *h, int v) { h->func(v); }
  138. `);
  139. // Adversarial text: CRLF, NBSP after 'struct', continuation before a
  140. // #define, decoy syntax inside strings, backtick, unterminated comment,
  141. // and the `static x = {` backtracking shape.
  142. write('nasty.c', [
  143. 'struct weird { int (*go)(int); };',
  144. 'static int impl_go(int a) { return a; }\r',
  145. 'static struct weird w = { impl_go };\r',
  146. // NBSP (U+00A0) between `struct` and the tag: JS `\s` is the Unicode
  147. // class, so the initializer scan crosses it — the native sweep must too.
  148. 'static struct\u00A0weird w2 = { impl_go };',
  149. 'int poke(struct weird *p, int a) { return p->go(a); }',
  150. 'static x = {1};',
  151. 'const char *s = "struct fake { int (*f)(int); } decoy[] = { impl_go };";',
  152. 'int bt = 0; /* unterminated ` tick',
  153. ].join('\n'));
  154. };
  155. it('indexes to identical fn-pointer-dispatch edges with the sweep native vs JS', async () => {
  156. writeFixture();
  157. process.env.CODEGRAPH_KERNEL_CFNPTR = '0';
  158. const js = await indexAndCollect();
  159. delete process.env.CODEGRAPH_KERNEL_CFNPTR;
  160. const native = await indexAndCollect();
  161. expect(native.nodes).toBe(js.nodes);
  162. expect(native.edges).toEqual(js.edges);
  163. // The fixture must actually exercise the synthesizer, not vacuously pass.
  164. expect(js.edges.length).toBeGreaterThanOrEqual(8);
  165. const vias = new Set(js.edges.map((e) => e.via));
  166. expect([...vias].some((v) => v.endsWith('[]'))).toBe(true); // bare-array path
  167. expect([...vias].some((v) => v.includes('.'))).toBe(true); // struct-field path
  168. }, 120_000);
  169. it('native facts match the JS sweep on the raw scanner surface', () => {
  170. // Direct record-level check of one adversarial file (no indexing): the
  171. // kernel's per-file facts vs what the JS sweep's scans produce. Guards
  172. // the scanner surface even for shapes the edge-level fixture might not
  173. // reach (alias names, d-pairs, include capture order).
  174. const text = [
  175. '#define ALIAS realStruct',
  176. '#define NUM 0x10',
  177. '#define FN(x) x',
  178. 'typedef void (*cb_t)(int);',
  179. 'typedef int fnt(int);',
  180. '#include "a.def"',
  181. '#include "b.h"',
  182. 'struct realStruct { cb_t cb; fnt *f; int n; };',
  183. 'void go(struct realStruct *r, struct realStruct *q) {',
  184. ' r->cb = q->cb;',
  185. ' r->cb(1);',
  186. ' tbl[NUM](2);',
  187. '}',
  188. 'static struct ALIAS one = { 0 };',
  189. 'static x = {1};',
  190. ].join('\n');
  191. const out = kernel!.cfnptrScanFiles!([{ text, structs: [] }])[0]!;
  192. expect(out.fnPtrTypedefs).toEqual(['cb_t']);
  193. expect(out.fnTypeTypedefs).toEqual(['fnt']);
  194. expect(out.aliasNames).toEqual(['ALIAS']); // NUM numeric, FN function-like
  195. expect(out.includes).toEqual(['a.def', 'b.h']);
  196. expect(out.dPairs).toEqual(['cb\0cb']);
  197. expect(out.dispatchFields).toContain('cb');
  198. expect(out.arrayDispatchNames).toContain('tbl');
  199. expect(out.initTokens).toContain('ALIAS');
  200. expect(out.initTokens).toContain('static'); // the backtracking shape
  201. // `struct realStruct { … };` is followed by `;`, so it fails the
  202. // inline-TABLE var check (`^\s*(\w+)`) — no candidate, like the JS scan.
  203. expect(out.inlineTags).toEqual([]);
  204. });
  205. });