Skip to content

Commit 730e73d

Browse files
committed
fix: skip the call-graph (incl. jelly) solve at -a 1 — the result is discarded
The v2 emitter only reads external_symbols/synthesized_callables and populates call_graph at level >= 2 (homeExternals/homeSynthesized in src/schema/v2/emit.ts), so running the full provider solve — including the heavier Jelly leg — at -a 1 computed a result that was thrown away. Gate provider.build to analysisLevel >= 2; levels 3/4 already require >= 2 for callee resolution, so this is safe.
1 parent 6ac743e commit 730e73d

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

src/core.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,17 @@ export async function analyze(opts: AnalysisOptions): Promise<TSApplication> {
2929
const extraction = opts.analysisLevel >= 3 ? startExtraction(project, symbol_table, mat.tsConfigFilePath, opts, log) : null;
3030

3131
// Call graph via the selected provider (union of tsc+jelly by default; --tsc-only / jelly opt-in).
32+
// Only worth running at level >= 2: the v2 emitter discards call_graph/external_symbols/
33+
// synthesized_callables at -a 1 (homeExternals/homeSynthesized in src/schema/v2/emit.ts are
34+
// gated to `level >= 2`), so running the solve — including the heavier Jelly leg — at -a 1
35+
// would compute a result that's thrown away. Levels 3/4 need the provider for callee
36+
// resolution and are always >= 2, so this gate is safe.
3237
const provider = selectProvider(opts.callGraphProvider);
3338
log.info(`call graph provider: ${provider.name}`);
34-
const cg = provider.build({ project, symbol_table, root: opts.input, log, phantoms: opts.phantoms });
39+
const cg =
40+
opts.analysisLevel >= 2
41+
? provider.build({ project, symbol_table, root: opts.input, log, phantoms: opts.phantoms })
42+
: { edges: [], external_symbols: {}, synthesized_callables: {} };
3543
const call_graph = cg.edges;
3644

3745
const app: TSApplication = {

test/schema-v2.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* bindings as module `fields{}`, and `body` `call` nodes at L1 — while remaining a superset of
66
* every v1 symbol-table fact.
77
*/
8-
import { describe, expect, test } from "bun:test";
8+
import { describe, expect, spyOn, test } from "bun:test";
99
import * as fs from "node:fs";
1010
import * as os from "node:os";
1111
import * as path from "node:path";
@@ -15,6 +15,7 @@ import type { AnalysisOptions } from "../src/options";
1515
import type { GraphSelector, TSApplication } from "../src/schema";
1616
import { type V2Application, type V2Callable, type V2Module, type V2Type, toV2Detailed } from "../src/schema/v2";
1717
import { type GraphRows, project } from "../src/build/neo4j";
18+
import { tscProvider } from "../src/semantic_analysis";
1819

1920
const FIXTURE = path.resolve(import.meta.dir, "fixtures/sample-app");
2021

@@ -189,6 +190,41 @@ describe("schema v2 — L1 superset", () => {
189190
});
190191
});
191192

193+
// ---- L1: the call-graph solve is skipped entirely (issue #31) -------------------------------
194+
// The solve (including the heavier Jelly leg) is only useful from L2 up — the v2 emitter never
195+
// reads call_graph/external_symbols/synthesized_callables at L1 (homeExternals/homeSynthesized
196+
// are gated to `level >= 2` in src/schema/v2/emit.ts). This locks BOTH the output invariant
197+
// (already true before the -a 1 gating fix) AND, via a spy, that the provider itself is skipped.
198+
describe("schema v2 — L1 skips the call-graph solve (issue #31)", () => {
199+
test("provider.build is not invoked and no call-graph/external/synth data appears at -a 1", async () => {
200+
const spy = spyOn(tscProvider, "build");
201+
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "cants-v2-l1-guard-"));
202+
try {
203+
const v1L1 = await analyze({ ...options(), analysisLevel: 1, callGraphProvider: "tsc", cacheDir });
204+
expect(spy).not.toHaveBeenCalled();
205+
expect(v1L1.call_graph).toEqual([]);
206+
expect(Object.keys(v1L1.external_symbols)).toEqual([]);
207+
expect(Object.keys(v1L1.synthesized_callables)).toEqual([]);
208+
} finally {
209+
spy.mockRestore();
210+
fs.rmSync(cacheDir, { recursive: true, force: true });
211+
}
212+
});
213+
214+
test("provider.build IS invoked at -a 2, and produces edges (baseline: L2 unaffected)", async () => {
215+
const spy = spyOn(tscProvider, "build");
216+
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "cants-v2-l1-guard-l2-"));
217+
try {
218+
const v1L2guard = await analyze({ ...options(), analysisLevel: 2, callGraphProvider: "tsc", cacheDir });
219+
expect(spy).toHaveBeenCalledTimes(1);
220+
expect(v1L2guard.call_graph.length).toBeGreaterThan(0);
221+
} finally {
222+
spy.mockRestore();
223+
fs.rmSync(cacheDir, { recursive: true, force: true });
224+
}
225+
});
226+
});
227+
192228
// ---- L2: call graph -------------------------------------------------------------------------
193229
async function runL2(): Promise<TSApplication> {
194230
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "cants-v2-l2-"));

0 commit comments

Comments
 (0)