Files
codegraph/__tests__/object-registry-synthesizer.test.ts
T
Colby McHenryandClaude Opus 4.8 7f970296cf feat(resolution): synthesize object-literal registry dispatch edges
Adds `objectRegistryEdges` — a dynamic-dispatch synthesizer for the command/handler
registry pattern: an object literal maps string keys → handler classes/functions, then
dispatches by a RUNTIME key static parsing can't follow:

    this.commands = { [Cmd.ADD]: AddObjectCommand, ... }    // registration
    new this.commands[command](args).execute()              // dynamic dispatch

It links each dispatching function → each registered handler's callable entry (a class's
execute/run/handle method — preferring the method chained at the dispatch site — or the
function value), like the gin-middleware-chain fan-out. Same-file registry+dispatch only.

Validated precise on 3 real repos (the discipline that caught redux-thunk's n=1 overfit):
EtherealEngine's CommandManager (64 edges, class registry → .execute), Prebid.js (7:
builder/consent/message dispatch, function registry), warp-drive (1). Zero false positives
after several precision gates found during validation:
- skip minified/generated bundles (avg line length > 200) — draco/three.min were a
  false-positive minefield of `h[x](...)` calls + `{a:b}` literals;
- DEPTH-AWARE entry parsing (top-level `key: Identifier` only) so method-shorthand bodies
  and nested objects don't leak their inner `k: v` pairs as bogus handlers;
- callable-only targets (drop data `constant`s — a `{x: URL}` entry resolving to the global);
- dynamic-dispatch gate (a statically-accessed look-alike object yields nothing).
Handles constructor and field-initializer registry forms (this. normalized). Surfaces in
codegraph_explore via the existing Dynamic-dispatch-links section.

Deferred (recall, documented in dispatch-synthesizer-backlog.md): assign-then-call dispatch,
augmentation registration (reg[k]=H), and the cross-file barrel-namespace variant
(trezor getMethod) — the hard tier.

Full suite green (1606); new __tests__/object-registry-synthesizer.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:17:31 -05:00

84 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Object-literal registry dispatch synthesizer.
*
* A command registry maps keys → handler classes/functions in an object literal, then
* dispatches by a RUNTIME key (`new registry[command]().execute()`) that static parsing
* can't follow. The synthesizer links each dispatching method → each registered handler's
* callable entry. Validates: a class registry resolves to the handler's `.execute` method;
* the field-initializer form (`commands = {…}` matched against a `this.commands[k]` dispatch);
* and the dispatch GATE — a look-alike object literal that is only ever accessed statically
* (never `registry[var]`) yields no edges.
*/
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';
describe('object-registry synthesizer', () => {
let dir: string;
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'obj-registry-')); });
afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
it('links a dispatcher to each registered command classs execute method, gated on dynamic dispatch', async () => {
fs.writeFileSync(
path.join(dir, 'commands.ts'),
`export class AddCommand { execute() { return 'add'; } }
export class RemoveCommand { execute() { return 'remove'; } }
export class MoveCommand { execute() { return 'move'; } }
`
);
fs.writeFileSync(
path.join(dir, 'manager.ts'),
`import { AddCommand, RemoveCommand, MoveCommand } from './commands';
const Cmd = { ADD: 'add', REMOVE: 'remove', MOVE: 'move' };
class CommandManager {
commands = {
[Cmd.ADD]: AddCommand,
[Cmd.REMOVE]: RemoveCommand,
[Cmd.MOVE]: MoveCommand,
};
executeCommand(command: string) {
return new this.commands[command]().execute();
}
}
`
);
// A look-alike registry that is NEVER dynamically dispatched (only a static `.add`
// member access) — must yield NO edges. The dynamic `registry[var]` dispatch is the gate.
fs.writeFileSync(
path.join(dir, 'static.ts'),
`import { AddCommand, RemoveCommand } from './commands';
const table = { add: AddCommand, remove: RemoveCommand };
export function direct() { return new table.add().execute(); }
`
);
const cg = await CodeGraph.init(dir, { silent: true });
await cg.indexAll();
const db = (cg as any).db.db;
const rows = db
.prepare(
`SELECT s.name source_name, t.name target_name, t.kind target_kind, t.file_path target_file
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') = 'object-registry'`
)
.all();
cg.close?.();
// Exactly the 3 dispatcher→handler-entry edges: executeCommand → {Add,Remove,Move}Command.execute.
expect(rows.length).toBe(3);
expect(rows.every((r: any) => r.source_name === 'executeCommand')).toBe(true);
expect(rows.every((r: any) => r.target_kind === 'method' && r.target_name === 'execute')).toBe(true);
expect(rows.every((r: any) => /commands\.ts$/.test(r.target_file))).toBe(true);
// The statically-accessed look-alike registry contributed nothing.
expect(rows.some((r: any) => /static\.ts$/.test(r.target_file))).toBe(false);
});
});