Skip to content
Open
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
10 changes: 9 additions & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,17 @@ export async function analyze(opts: AnalysisOptions): Promise<TSApplication> {
const extraction = opts.analysisLevel >= 3 ? startExtraction(project, symbol_table, mat.tsConfigFilePath, opts, log) : null;

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

const app: TSApplication = {
Expand Down
38 changes: 37 additions & 1 deletion test/schema-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* bindings as module `fields{}`, and `body` `call` nodes at L1 — while remaining a superset of
* every v1 symbol-table fact.
*/
import { describe, expect, test } from "bun:test";
import { describe, expect, spyOn, test } from "bun:test";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
Expand All @@ -15,6 +15,7 @@ import type { AnalysisOptions } from "../src/options";
import type { GraphSelector, TSApplication } from "../src/schema";
import { type V2Application, type V2Callable, type V2Module, type V2Type, toV2Detailed } from "../src/schema/v2";
import { type GraphRows, project } from "../src/build/neo4j";
import { tscProvider } from "../src/semantic_analysis";

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

Expand Down Expand Up @@ -188,6 +189,41 @@ describe("schema v2 — L1 superset", () => {
});
});

// ---- L1: the call-graph solve is skipped entirely (issue #31) -------------------------------
// The solve (including the heavier Jelly leg) is only useful from L2 up — the v2 emitter never
// reads call_graph/external_symbols/synthesized_callables at L1 (homeExternals/homeSynthesized
// are gated to `level >= 2` in src/schema/v2/emit.ts). This locks BOTH the output invariant
// (already true before the -a 1 gating fix) AND, via a spy, that the provider itself is skipped.
describe("schema v2 — L1 skips the call-graph solve (issue #31)", () => {
test("provider.build is not invoked and no call-graph/external/synth data appears at -a 1", async () => {
const spy = spyOn(tscProvider, "build");
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "cants-v2-l1-guard-"));
try {
const v1L1 = await analyze({ ...options(), analysisLevel: 1, callGraphProvider: "tsc", cacheDir });
expect(spy).not.toHaveBeenCalled();
expect(v1L1.call_graph).toEqual([]);
expect(Object.keys(v1L1.external_symbols)).toEqual([]);
expect(Object.keys(v1L1.synthesized_callables)).toEqual([]);
} finally {
spy.mockRestore();
fs.rmSync(cacheDir, { recursive: true, force: true });
}
});

test("provider.build IS invoked at -a 2, and produces edges (baseline: L2 unaffected)", async () => {
const spy = spyOn(tscProvider, "build");
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "cants-v2-l1-guard-l2-"));
try {
const v1L2guard = await analyze({ ...options(), analysisLevel: 2, callGraphProvider: "tsc", cacheDir });
expect(spy).toHaveBeenCalledTimes(1);
expect(v1L2guard.call_graph.length).toBeGreaterThan(0);
} finally {
spy.mockRestore();
fs.rmSync(cacheDir, { recursive: true, force: true });
}
});
});

// ---- L2: call graph -------------------------------------------------------------------------
async function runL2(): Promise<TSApplication> {
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "cants-v2-l2-"));
Expand Down