| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- /**
- * cFnPtr native extraction sweep — differential gate (task #5 step 2).
- *
- * The synthesizer's extraction sweep has two implementations: the JS regex
- * sweep and the kernel's `cfnptrScanFiles` (codegraph-kernel/src/cfnptr.rs).
- * They must be record-identical, which this suite pins end-to-end: the same
- * adversarial project is indexed twice — CODEGRAPH_KERNEL_CFNPTR toggled —
- * and the synthesized fn-pointer-dispatch edges must match EXACTLY, including
- * order (edge order is observable through FANOUT_CAP truncation).
- *
- * The fixture deliberately stacks the sweep's edge cases: macro-built tables
- * behind a non-indexed include, `#ifdef`-guarded inline structs, object-macro
- * type aliases, bare fn-pointer arrays with casts and designators, chained
- * receivers, field←field propagation, CRLF line endings, NBSP whitespace,
- * `\`-continuations, strings containing decoy syntax, an unterminated block
- * comment, and modifier/type backtracking shapes (`static x = {…}`).
- */
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
- import * as fs from 'node:fs';
- import * as path from 'node:path';
- import * as os from 'node:os';
- import { CodeGraph } from '../src';
- import { getKernel } from '../src/extraction/kernel/loader';
- const kernel = getKernel();
- const nativeAvailable = !!kernel && typeof kernel.cfnptrScanFiles === 'function';
- interface EdgeRow {
- src: string;
- tgt: string;
- via: string;
- line: number;
- }
- describe.runIf(nativeAvailable)('cFnPtr sweep: native vs JS differential', () => {
- let dir: string;
- beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cfp-k-')); });
- afterEach(() => {
- delete process.env.CODEGRAPH_KERNEL_CFNPTR;
- fs.rmSync(dir, { recursive: true, force: true });
- });
- const write = (rel: string, body: string) => {
- const p = path.join(dir, rel);
- fs.mkdirSync(path.dirname(p), { recursive: true });
- fs.writeFileSync(p, body);
- };
- const indexAndCollect = async (): Promise<{ edges: EdgeRow[]; nodes: number }> => {
- fs.rmSync(path.join(dir, '.codegraph'), { recursive: true, force: true });
- const cg = await CodeGraph.init(dir, { silent: true });
- await cg.indexAll();
- const db = (cg as any).db.db;
- const edges: EdgeRow[] = db
- .prepare(
- `SELECT s.name src, t.name tgt, json_extract(e.metadata,'$.via') via, e.line line
- 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') = 'fn-pointer-dispatch'
- ORDER BY e.id`
- )
- .all();
- const nodes = db.prepare('SELECT count(*) c FROM nodes').get().c as number;
- cg.close?.();
- return { edges, nodes };
- };
- const writeFixture = () => {
- // The git shape + designated init + assignment registration.
- write('cmd.c', `
- struct cmd { const char *name; int (*fn)(int argc); };
- static int cmd_add(int argc) { return argc + 1; }
- static int cmd_rm(int argc) { return argc - 1; }
- static struct cmd commands[] = {
- { "add", cmd_add },
- { "rm", cmd_rm },
- };
- int run(int i, int argc) { return commands[i].fn(argc); }
- `);
- // Macro-built table with an object-macro struct alias and a non-indexed
- // include, redis-style; plus a typedef'd fn-TYPE field.
- write('table.c', `
- #include "table.h"
- #include "cmds.def"
- int dispatch(struct client *c, int a) { return c->cur->proc(a); }
- `);
- write('table.h', `
- typedef int cmdProc(int a);
- #define TBL_STRUCT redisCmd
- struct redisCmd { const char *name; cmdProc *proc; };
- struct client { struct redisCmd *cur; };
- #define MK(nm, fn) { nm, fn }
- static int getCmd(int a);
- static int setCmd(int a);
- `);
- write('cmds.def', `
- struct TBL_STRUCT tbl[] = {
- MK("get", getCmd),
- MK("set", setCmd),
- };
- `);
- write('impl.c', `
- #include "table.h"
- static int getCmd(int a) { return a; }
- static int setCmd(int a) { return a + 1; }
- `);
- // #ifdef-guarded inline struct table + parenthesized subscript dispatch
- // (the vim shape), switched on by the includer.
- write('ex.c', `
- #define WANT_TABLE
- #include "ex_cmds.h"
- int exec(int i, int a) { return (cmdtab[i].cmd_fn)(a); }
- `);
- write('ex_cmds.h', `
- #ifdef WANT_TABLE
- static int ex_quit(int a);
- struct excmd { char *nm; int (*cmd_fn)(int); } cmdtab[] = { { "q", ex_quit } };
- #endif
- `);
- write('ex_impl.c', `static int ex_quit(int a) { return -a; }\n`);
- // Bare arrays: fn-TYPE typedef with star, casts, index designators, and a
- // same-named file-local collision (the SameBoy/Zend shapes).
- write('ops.c', `
- typedef int op_t(int);
- static int nop(int x) { return x; }
- static int halt(int x) { return -x; }
- static op_t *ops[4] = { nop, [2] = (op_t *)halt };
- int step(int pc, int x) { return ops[pc](x); }
- `);
- write('ops2.c', `
- typedef int op_t(int);
- static int trace(int x) { return x * 2; }
- static op_t *ops[4] = { trace };
- int step2(int pc, int x) { return (*ops[pc])(x); }
- `);
- // Field←field propagation (the hook_demo shape) + chained receiver.
- write('hook.c', `
- typedef void hook_fn(int);
- struct entry { const char *nm; hook_fn *fn; };
- struct hook { hook_fn *func; };
- static void on_commit(int v) { (void)v; }
- static struct entry entries[] = { { "commit", on_commit } };
- void wire(struct hook *h, struct entry *found) { h->func = found->fn; }
- void fire(struct hook *h, int v) { h->func(v); }
- `);
- // Adversarial text: CRLF, NBSP after 'struct', continuation before a
- // #define, decoy syntax inside strings, backtick, unterminated comment,
- // and the `static x = {` backtracking shape.
- write('nasty.c', [
- 'struct weird { int (*go)(int); };',
- 'static int impl_go(int a) { return a; }\r',
- 'static struct weird w = { impl_go };\r',
- // NBSP (U+00A0) between `struct` and the tag: JS `\s` is the Unicode
- // class, so the initializer scan crosses it — the native sweep must too.
- 'static struct\u00A0weird w2 = { impl_go };',
- 'int poke(struct weird *p, int a) { return p->go(a); }',
- 'static x = {1};',
- 'const char *s = "struct fake { int (*f)(int); } decoy[] = { impl_go };";',
- 'int bt = 0; /* unterminated ` tick',
- ].join('\n'));
- };
- it('indexes to identical fn-pointer-dispatch edges with the sweep native vs JS', async () => {
- writeFixture();
- process.env.CODEGRAPH_KERNEL_CFNPTR = '0';
- const js = await indexAndCollect();
- delete process.env.CODEGRAPH_KERNEL_CFNPTR;
- const native = await indexAndCollect();
- expect(native.nodes).toBe(js.nodes);
- expect(native.edges).toEqual(js.edges);
- // The fixture must actually exercise the synthesizer, not vacuously pass.
- expect(js.edges.length).toBeGreaterThanOrEqual(8);
- const vias = new Set(js.edges.map((e) => e.via));
- expect([...vias].some((v) => v.endsWith('[]'))).toBe(true); // bare-array path
- expect([...vias].some((v) => v.includes('.'))).toBe(true); // struct-field path
- }, 120_000);
- it('native facts match the JS sweep on the raw scanner surface', () => {
- // Direct record-level check of one adversarial file (no indexing): the
- // kernel's per-file facts vs what the JS sweep's scans produce. Guards
- // the scanner surface even for shapes the edge-level fixture might not
- // reach (alias names, d-pairs, include capture order).
- const text = [
- '#define ALIAS realStruct',
- '#define NUM 0x10',
- '#define FN(x) x',
- 'typedef void (*cb_t)(int);',
- 'typedef int fnt(int);',
- '#include "a.def"',
- '#include "b.h"',
- 'struct realStruct { cb_t cb; fnt *f; int n; };',
- 'void go(struct realStruct *r, struct realStruct *q) {',
- ' r->cb = q->cb;',
- ' r->cb(1);',
- ' tbl[NUM](2);',
- '}',
- 'static struct ALIAS one = { 0 };',
- 'static x = {1};',
- ].join('\n');
- const out = kernel!.cfnptrScanFiles!([{ text, structs: [] }])[0]!;
- expect(out.fnPtrTypedefs).toEqual(['cb_t']);
- expect(out.fnTypeTypedefs).toEqual(['fnt']);
- expect(out.aliasNames).toEqual(['ALIAS']); // NUM numeric, FN function-like
- expect(out.includes).toEqual(['a.def', 'b.h']);
- expect(out.dPairs).toEqual(['cb\0cb']);
- expect(out.dispatchFields).toContain('cb');
- expect(out.arrayDispatchNames).toContain('tbl');
- expect(out.initTokens).toContain('ALIAS');
- expect(out.initTokens).toContain('static'); // the backtracking shape
- // `struct realStruct { … };` is followed by `;`, so it fails the
- // inline-TABLE var check (`^\s*(\w+)`) — no candidate, like the JS scan.
- expect(out.inlineTags).toEqual([]);
- });
- });
|