Skip to content

Commit 0ba0387

Browse files
committed
fix(layering): reject incomplete AST parses and isolate parity fixtures
1 parent 903d5f1 commit 0ba0387

2 files changed

Lines changed: 112 additions & 33 deletions

File tree

‎scripts/layering/model.test.ts‎

Lines changed: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { readFileSync } from 'node:fs';
33
import path from 'node:path';
44
import { test } from 'node:test';
55
import { listSourceFiles } from './check.ts';
6-
import { workspaceSpecifierTargets } from './package-boundaries.ts';
76
import {
87
fieldClassificationDrift,
98
findSessionStateWrites,
@@ -26,6 +25,26 @@ import {
2625
unclassifiedZones,
2726
} from './model.ts';
2827

28+
test('parseImports rejects malformed source instead of returning an incomplete graph', () => {
29+
assert.throws(
30+
() => parseImports("import { Broken from './broken.ts';"),
31+
/Cannot parse imports.*Expected/,
32+
);
33+
});
34+
35+
test('parseImports ignores import-like declarations inside comments and template text', () => {
36+
const source = [
37+
'const documentation = `',
38+
"import { phantom } from './phantom.ts';",
39+
"export { phantom } from './also-phantom.ts';",
40+
'`;',
41+
'/*',
42+
"import './comment.ts';",
43+
'*/',
44+
].join('\n');
45+
assert.deepEqual(parseImports(source), []);
46+
});
47+
2948
test('parseImports distinguishes value, type-only, dynamic, and value re-export edges', () => {
3049
const edges = parseImports(
3150
[
@@ -148,32 +167,90 @@ test('parseImports records TSImportType edges separately from dynamic imports',
148167
);
149168
});
150169

151-
test('the nine recorded TSImportType file pairs resolve as type edges', () => {
152-
const expected = [
153-
'packages/contracts/src/platform-runtime.ts\0packages/contracts/src/device-shutdown-runtime.ts',
154-
'packages/platform-apple/src/runner/runner-provider.ts\0packages/platform-apple/src/runner/runner-artifact.ts',
155-
'src/daemon/interaction/index.ts\0src/daemon/gesture-runtime.ts',
156-
'src/daemon/interaction/index.ts\0src/daemon/touch-runtime.ts',
157-
'src/daemon/interaction/internal/interaction.ts\0packages/contracts/src/android-observation.ts',
158-
'src/daemon/snapshot-runtime-binding.ts\0packages/contracts/src/focus-runtime.ts',
159-
'src/daemon/snapshot-runtime-binding.ts\0packages/contracts/src/interactor-types.ts',
160-
'src/daemon/snapshot-runtime-binding.ts\0packages/contracts/src/type-text-runtime.ts',
161-
'src/sdk/artifacts.ts\0packages/platform-android/src/mechanics.ts',
162-
].sort();
163-
const expectedPairs = new Set(expected);
164-
const sources = new Map(
165-
listSourceFiles().map((file) => [file, readFileSync(path.resolve(file), 'utf8')]),
166-
);
167-
const actual = [
168-
...new Set(
169-
resolveImportEdges(sources, workspaceSpecifierTargets(process.cwd()))
170-
.filter((edge) => !edge.dynamic && edge.typeOnly)
171-
.map((edge) => `${edge.file}\0${edge.target}`)
172-
.filter((pair) => expectedPairs.has(pair)),
173-
),
174-
].sort();
175-
176-
assert.deepEqual(actual, expected);
170+
test('the nine recorded TSImportType shapes resolve with source-side symbols', () => {
171+
const cases = [
172+
[
173+
'packages/contracts/src/platform-runtime.ts',
174+
'packages/contracts/src/device-shutdown-runtime.ts',
175+
'./device-shutdown-runtime.ts',
176+
'DeviceShutdownRuntime',
177+
],
178+
[
179+
'packages/platform-apple/src/runner/runner-provider.ts',
180+
'packages/platform-apple/src/runner/runner-artifact.ts',
181+
'./runner-artifact.ts',
182+
'RunnerStartAdmission',
183+
],
184+
[
185+
'src/daemon/interaction/index.ts',
186+
'src/daemon/gesture-runtime.ts',
187+
'../gesture-runtime.ts',
188+
'BoundGestureExecutor',
189+
],
190+
[
191+
'src/daemon/interaction/index.ts',
192+
'src/daemon/touch-runtime.ts',
193+
'../touch-runtime.ts',
194+
'BoundTouchExecutor',
195+
],
196+
[
197+
'src/daemon/interaction/internal/interaction.ts',
198+
'packages/contracts/src/android-observation.ts',
199+
'@agent-device/contracts/android-observation',
200+
'AndroidObservationAdapter',
201+
],
202+
[
203+
'src/daemon/snapshot-runtime-binding.ts',
204+
'packages/contracts/src/focus-runtime.ts',
205+
'@agent-device/contracts/focus-runtime',
206+
'FocusPointInput',
207+
],
208+
[
209+
'src/daemon/snapshot-runtime-binding.ts',
210+
'packages/contracts/src/interactor-types.ts',
211+
'@agent-device/contracts/interactor-types',
212+
'TypeTextBackendResult',
213+
],
214+
[
215+
'src/daemon/snapshot-runtime-binding.ts',
216+
'packages/contracts/src/type-text-runtime.ts',
217+
'@agent-device/contracts/type-text-runtime',
218+
'TypeTextInput',
219+
],
220+
[
221+
'src/sdk/artifacts.ts',
222+
'packages/platform-android/src/mechanics.ts',
223+
'@agent-device/platform-android/mechanics',
224+
'resolveAndroidArchivePackageName',
225+
],
226+
] as const;
227+
const sources = new Map<string, string>();
228+
const exports = new Map<string, string>();
229+
for (const [index, [file, target, spec, symbol]] of cases.entries()) {
230+
const query = file === 'src/sdk/artifacts.ts' ? 'typeof ' : '';
231+
sources.set(
232+
file,
233+
`${sources.get(file) ?? ''}type Dependency${index} = ${query}import('${spec}').${symbol};\n`,
234+
);
235+
sources.set(target, `export type ${symbol} = unknown;`);
236+
if (spec.startsWith('@')) exports.set(spec, target);
237+
}
238+
assert.deepEqual(
239+
resolveImportEdges(sources, exports).map(({ file, target, dynamic, typeOnly, symbols }) => ({
240+
file,
241+
target,
242+
dynamic,
243+
typeOnly,
244+
symbols,
245+
})),
246+
cases.map(([file, target, , symbol]) => ({
247+
file,
248+
target,
249+
dynamic: false,
250+
typeOnly: true,
251+
symbols: [symbol],
252+
})),
253+
);
177254
});
178255

179256
test('parseImports detects multiline dynamic imports', () => {

‎scripts/layering/model.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ function literalSpecifier(node: unknown): string | undefined {
168168
return undefined;
169169
}
170170

171-
type LocatedImportEdge = { edge: ImportEdge; start: number; order: number };
171+
type LocatedImportEdge = { edge: ImportEdge; start: number };
172172

173173
function importedName(node: unknown): string | undefined {
174174
if (node === null || typeof node !== 'object') return undefined;
@@ -228,9 +228,13 @@ function staticImportEdge(
228228

229229
export function parseImports(source: string): ImportEdge[] {
230230
const parsed = parseSync('layering-imports.ts', source);
231+
if (parsed.errors.length > 0) {
232+
throw new SyntaxError(
233+
`Cannot parse imports: ${parsed.errors.map(({ message }) => message).join('; ')}`,
234+
);
235+
}
231236
const destructured = destructuredDynamicImportBindings(parsed.program);
232237
const located: LocatedImportEdge[] = [];
233-
let order = 0;
234238

235239
visitAst(parsed.program, (node) => {
236240
const start = typeof node.start === 'number' ? node.start : 0;
@@ -240,7 +244,6 @@ export function parseImports(source: string): ImportEdge[] {
240244
const capture = destructured.get(start);
241245
located.push({
242246
start,
243-
order: order++,
244247
edge: {
245248
spec,
246249
dynamic: true,
@@ -288,16 +291,15 @@ export function parseImports(source: string): ImportEdge[] {
288291
sourceName === undefined ? [] : [sourceName],
289292
);
290293
}
291-
if (edge) located.push({ edge, start, order: order++ });
294+
if (edge) located.push({ edge, start });
292295
});
293296

294297
return located
295298
.sort(
296299
(left, right) =>
297300
left.edge.line - right.edge.line ||
298301
Number(right.edge.dynamic) - Number(left.edge.dynamic) ||
299-
left.start - right.start ||
300-
left.order - right.order,
302+
left.start - right.start,
301303
)
302304
.map(({ edge }) => edge);
303305
}

0 commit comments

Comments
 (0)