Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions src/features/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ type BatchSig = 'name' | 'target' | 'file' | 'dbOnly';
interface BatchCommandEntry {
fn: (...args: unknown[]) => unknown;
sig: BatchSig;
/**
* For `sig: 'dbOnly'` commands only: the `opts` key each target should be
* written to before calling `fn(customDbPath, opts)`. Defaults to `'target'`
* (a symbol/name filter). Commands whose underlying query function expects a
* *file path* target (rather than a symbol name) — e.g. `complexityData`,
* which treats `opts.target` as a name filter and `opts.file` as the file
* scope — must override this so the batch target lands in the right filter.
*/
targetKey?: string;
}

export const BATCH_COMMANDS: Record<string, BatchCommandEntry> = {
Expand All @@ -31,9 +40,22 @@ export const BATCH_COMMANDS: Record<string, BatchCommandEntry> = {
exports: { fn: exportsData as (...args: unknown[]) => unknown, sig: 'file' },
flow: { fn: flowData as (...args: unknown[]) => unknown, sig: 'name' },
dataflow: { fn: dataflowData as (...args: unknown[]) => unknown, sig: 'name' },
complexity: { fn: complexityData as (...args: unknown[]) => unknown, sig: 'dbOnly' },
complexity: {
fn: complexityData as (...args: unknown[]) => unknown,
sig: 'dbOnly',
targetKey: 'file',
},
};

/** Resolve the opts key a `dbOnly` command's target should be written to. */
function dbOnlyTargetOpts(
entry: BatchCommandEntry,
opts: Record<string, unknown>,
target: string,
): Record<string, unknown> {
return { ...opts, [entry.targetKey ?? 'target']: target };
}

interface BatchResultOk {
target: string;
ok: true;
Expand Down Expand Up @@ -77,7 +99,7 @@ export function batchData(
try {
let data: unknown;
if (entry.sig === 'dbOnly') {
data = entry.fn(customDbPath, { ...opts, target });
data = entry.fn(customDbPath, dbOnlyTargetOpts(entry, opts, target));
} else {
data = entry.fn(target, customDbPath, opts);
}
Expand Down Expand Up @@ -166,7 +188,7 @@ export function multiBatchData(
try {
let data: unknown;
if (entry.sig === 'dbOnly') {
data = entry.fn(customDbPath, { ...merged, target });
data = entry.fn(customDbPath, dbOnlyTargetOpts(entry, merged, target));
} else {
data = entry.fn(target, customDbPath, merged);
}
Expand Down
57 changes: 52 additions & 5 deletions tests/integration/batch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ function insertEdge(db, sourceId, targetId, kind = 'calls', confidence = 1.0) {
).run(sourceId, targetId, kind, confidence);
}

function insertComplexity(db, nodeId, cognitive, cyclomatic, maxNesting = 0) {
db.prepare(
'INSERT INTO function_complexity (node_id, cognitive, cyclomatic, max_nesting) VALUES (?, ?, ?, ?)',
).run(nodeId, cognitive, cyclomatic, maxNesting);
}

// ─── Fixture DB ────────────────────────────────────────────────────────

let tmpDir: string, dbPath: string;
Expand Down Expand Up @@ -71,6 +77,13 @@ beforeAll(() => {
insertEdge(db, fnRoute, fnAuth);
insertEdge(db, fnRoute, fnFormat);

// Complexity data, distinct per file, so batch complexity can be asserted
// to scope results to the requested file rather than the whole repo.
insertComplexity(db, fnAuth, 5, 2);
insertComplexity(db, fnValidate, 3, 1);
insertComplexity(db, fnRoute, 10, 4);
insertComplexity(db, fnFormat, 1, 1);

db.close();
});

Expand Down Expand Up @@ -197,12 +210,46 @@ describe('batchData — edge cases', () => {
// ─── complexity (dbOnly sig) ─────────────────────────────────────────

describe('batchData — complexity (dbOnly signature)', () => {
test('complexity command uses target as opts.target', () => {
const data = batchData('complexity', ['authenticate'], dbPath);
expect(data.total).toBe(1);
// complexityData returns functions array — it won't error for unknown targets
test('complexity command scopes each target to a file, not a symbol name (#1721)', () => {
Comment thread
carlos-alm marked this conversation as resolved.
// Regression test for #1721: batch previously forwarded the file-path
// target as opts.target (a symbol-name filter), so complexityData never
// matched anything and silently fell back to identical, unfiltered
// whole-repo results for every target. It must land in opts.file instead.
const data = batchData('complexity', ['src/auth.js', 'src/routes.js'], dbPath);
expect(data.total).toBe(2);
expect(data.succeeded).toBe(2);

const auth = data.results.find((r) => r.target === 'src/auth.js');
const routes = data.results.find((r) => r.target === 'src/routes.js');
expect(auth.ok).toBe(true);
expect(routes.ok).toBe(true);

// src/auth.js has authenticate (cognitive 5) + validateToken (cognitive 3)
const authNames = auth.data.functions.map((f) => f.name).sort();
expect(authNames).toEqual(['authenticate', 'validateToken']);

// src/routes.js has only handleRoute (cognitive 10)
const routeNames = routes.data.functions.map((f) => f.name);
expect(routeNames).toEqual(['handleRoute']);

// The two targets must not collapse into identical, unfiltered results.
// (Note: `summary` is a whole-repo health metric independent of the file
// filter, by design of complexityData — see #1807 for a separate,
// pre-existing gap where it should but doesn't reflect the file scope.
// The bug this test guards against is that `functions` came back empty
// for every target because the file path was routed into a symbol-name
// filter instead of the file filter.)
expect(auth.data.functions).not.toEqual(routes.data.functions);
});

test('complexity command still accepts a bare symbol name via opts.target passthrough', () => {
// BATCH_COMMANDS commands other than complexity keep symbol-name targets;
// this guards that complexity's own --target-style filtering (via shared
// opts, not the batch target) continues to work for direct callers.
const data = batchData('complexity', ['src/auth.js'], dbPath, { target: 'valid' });
expect(data.results[0].ok).toBe(true);
expect(data.results[0].data).toHaveProperty('functions');
const names = data.results[0].data.functions.map((f) => f.name);
expect(names).toEqual(['validateToken']);
});
});

Expand Down
Loading