diff --git a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl index be1409553..151f4997f 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl +++ b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl @@ -919,11 +919,18 @@ parent_up(q, a, b, "protocol") :- reach(q, a, d), d > 0, reach(q, b, d1), d1 = d // (`imports()` in axiomcode-impact, per language). Only a target that resolves to a file IN THIS REPOSITORY is // kept: a stdlib or third-party import names no file here and is dropped rather than guessed. .decl imports_fact(f:symbol, g:symbol) .input imports_fact +// ASKED FOR, NOT PRECOMPUTED (#1033). `import_hop` only ever reads the files that import a module the change +// reaches, so the closure is walked backwards from those files and nowhere else. The whole-project closure this +// replaces was the same for every question and quadratic to build (imports_file joined to itself): on a +// 3.6k-file TypeScript graph it was 1.2M rows and 27 s of a 38 s solve, the flat floor under every `impact`. +.decl import_goal(g:symbol) +import_goal(g) :- up_running(_, mod, _), kind(mod, "module"), decl_file(mod, g). .decl imports_file(f:symbol, g:symbol) -imports_file(f, g) :- imports_fact(f, g). +imports_file(f, g) :- import_goal(g), imports_fact(f, g). // and transitively: a test file imports the module that imports the settings object. The chain is what makes -// the rule worth anything — the file that breaks is almost never the file the test names. -imports_file(f, h) :- imports_file(f, g), imports_file(g, h), f != h. +// the rule worth anything — the file that breaks is almost never the file the test names. Linear in the chain: +// any path of imports from f to g with f != g yields the pair, the same set the self-join derived. +imports_file(f, g) :- imports_fact(f, h), imports_file(h, g), f != g. .output fw_edge // ── tests: a test's own body reaches the change, or a fixture its framework runs before it does ─────────────── diff --git a/tests/cases/typescript/runs-at-import-through-a-chain/case.json b/tests/cases/typescript/runs-at-import-through-a-chain/case.json new file mode 100644 index 000000000..92dde0cd5 --- /dev/null +++ b/tests/cases/typescript/runs-at-import-through-a-chain/case.json @@ -0,0 +1,10 @@ +{"lang": "typescript", "src": "src", + "checks": [ + {"why": "a module-level call breaks every file that imports its module, however many imports away: page.spec reaches the table only through page -> escape -> render -> tags, and escape and render import each other", + "run": ["impact", "makeMap", "--tests"], + "want": ["page.spec.ts", "at import"], + "avoid": ["sum.spec.ts"]}, + {"why": "control: a chain of imports that never reaches a module doing work at import propagates nothing", + "run": ["impact", "double", "--tests"], + "want": ["sum.spec.ts"], + "avoid": ["page.spec.ts"]}]} diff --git a/tests/cases/typescript/runs-at-import-through-a-chain/src/__tests__/page.spec.ts b/tests/cases/typescript/runs-at-import-through-a-chain/src/__tests__/page.spec.ts new file mode 100644 index 000000000..2dc97fd31 --- /dev/null +++ b/tests/cases/typescript/runs-at-import-through-a-chain/src/__tests__/page.spec.ts @@ -0,0 +1,8 @@ +import { describe, it, expect } from 'vitest' +import { page } from '../page' + +describe('page', () => { + it('wraps', () => { + expect(page('x')).toBe('x') + }) +}) diff --git a/tests/cases/typescript/runs-at-import-through-a-chain/src/__tests__/sum.spec.ts b/tests/cases/typescript/runs-at-import-through-a-chain/src/__tests__/sum.spec.ts new file mode 100644 index 000000000..0c05ab21f --- /dev/null +++ b/tests/cases/typescript/runs-at-import-through-a-chain/src/__tests__/sum.spec.ts @@ -0,0 +1,8 @@ +import { describe, it, expect } from 'vitest' +import { sum } from '../sum' + +describe('sum', () => { + it('adds', () => { + expect(sum(1, 2)).toBe(4) + }) +}) diff --git a/tests/cases/typescript/runs-at-import-through-a-chain/src/escape.ts b/tests/cases/typescript/runs-at-import-through-a-chain/src/escape.ts new file mode 100644 index 000000000..0a61ce59d --- /dev/null +++ b/tests/cases/typescript/runs-at-import-through-a-chain/src/escape.ts @@ -0,0 +1,9 @@ +import { open } from './render' + +export function escape(s: string): string { + return s.replace(/ { + const out: Record = {} + for (const k of list.split(',')) out[k] = true + return out +} diff --git a/tests/cases/typescript/runs-at-import-through-a-chain/src/page.ts b/tests/cases/typescript/runs-at-import-through-a-chain/src/page.ts new file mode 100644 index 000000000..6bea8b6a0 --- /dev/null +++ b/tests/cases/typescript/runs-at-import-through-a-chain/src/page.ts @@ -0,0 +1,6 @@ +import { wrap } from './escape' + +// two imports away from the table module, and calls nothing in it +export function page(s: string): string { + return wrap(s) +} diff --git a/tests/cases/typescript/runs-at-import-through-a-chain/src/plain.ts b/tests/cases/typescript/runs-at-import-through-a-chain/src/plain.ts new file mode 100644 index 000000000..a1ba9813d --- /dev/null +++ b/tests/cases/typescript/runs-at-import-through-a-chain/src/plain.ts @@ -0,0 +1,4 @@ +// imports nothing that reaches the table module +export function double(n: number): number { + return n * 2 +} diff --git a/tests/cases/typescript/runs-at-import-through-a-chain/src/render.ts b/tests/cases/typescript/runs-at-import-through-a-chain/src/render.ts new file mode 100644 index 000000000..17781fc3f --- /dev/null +++ b/tests/cases/typescript/runs-at-import-through-a-chain/src/render.ts @@ -0,0 +1,9 @@ +import { isVoidTag } from './tags' +import { escape } from './escape' + +// imports the table module, and is imported by `escape`, which it imports: a cycle +export function open(tag: string): string { + return '<' + escape(tag) + '>' +} + +export const voids = Object.keys(isVoidTag) diff --git a/tests/cases/typescript/runs-at-import-through-a-chain/src/sum.ts b/tests/cases/typescript/runs-at-import-through-a-chain/src/sum.ts new file mode 100644 index 000000000..eeec51971 --- /dev/null +++ b/tests/cases/typescript/runs-at-import-through-a-chain/src/sum.ts @@ -0,0 +1,5 @@ +import { double } from './plain' + +export function sum(a: number, b: number): number { + return double(a) + b +} diff --git a/tests/cases/typescript/runs-at-import-through-a-chain/src/tags.ts b/tests/cases/typescript/runs-at-import-through-a-chain/src/tags.ts new file mode 100644 index 000000000..3d466c18f --- /dev/null +++ b/tests/cases/typescript/runs-at-import-through-a-chain/src/tags.ts @@ -0,0 +1,4 @@ +import { makeMap } from './maps' + +// the module body runs this while being imported +export const isVoidTag = makeMap('br,hr,img')