Skip to content

Commit 1c517f3

Browse files
committed
fix(layering): preserve import-type source symbols
1 parent aa8e691 commit 1c517f3

3 files changed

Lines changed: 55 additions & 3 deletions

File tree

‎scripts/layering/model.test.ts‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ test('parseImports records TSImportType edges separately from dynamic imports',
6161
"type Imported = import('./type-only.ts').Imported;",
6262
"type Queried = typeof import('./type-query.ts').value;",
6363
"type SameTarget = import('./same-target.ts').Target; void import('./same-target.ts');",
64+
"type Nested = import('./nested.ts').namespace.SessionState;",
65+
"type WholeModule = import('./whole-module.ts');",
6466
].join('\n'),
6567
);
6668

@@ -78,14 +80,14 @@ test('parseImports records TSImportType edges separately from dynamic imports',
7880
dynamic: false,
7981
typeOnly: true,
8082
line: 1,
81-
symbols: [],
83+
symbols: ['Imported'],
8284
},
8385
{
8486
spec: './type-query.ts',
8587
dynamic: false,
8688
typeOnly: true,
8789
line: 2,
88-
symbols: [],
90+
symbols: ['value'],
8991
},
9092
{
9193
spec: './same-target.ts',
@@ -99,6 +101,20 @@ test('parseImports records TSImportType edges separately from dynamic imports',
99101
dynamic: false,
100102
typeOnly: true,
101103
line: 3,
104+
symbols: ['Target'],
105+
},
106+
{
107+
spec: './nested.ts',
108+
dynamic: false,
109+
typeOnly: true,
110+
line: 4,
111+
symbols: ['namespace'],
112+
},
113+
{
114+
spec: './whole-module.ts',
115+
dynamic: false,
116+
typeOnly: true,
117+
line: 5,
102118
symbols: [],
103119
},
104120
],
@@ -220,6 +236,7 @@ test('parseImports retains named source symbols without changing edge-kind detec
220236
"import type { TypeB as RenamedType } from './types.ts';",
221237
"export { reExport as publicName } from './re-export.ts';",
222238
"export type { ExportedType } from './exported-types.ts';",
239+
"export * from './re-export-all.ts';",
223240
"import * as namespace from './namespace.ts';",
224241
"void import('./dynamic.ts');",
225242
].join('\n'),
@@ -237,6 +254,7 @@ test('parseImports retains named source symbols without changing edge-kind detec
237254
{ spec: './types.ts', dynamic: false, typeOnly: true, symbols: ['TypeB'] },
238255
{ spec: './re-export.ts', dynamic: false, typeOnly: false, symbols: ['reExport'] },
239256
{ spec: './exported-types.ts', dynamic: false, typeOnly: true, symbols: ['ExportedType'] },
257+
{ spec: './re-export-all.ts', dynamic: false, typeOnly: false, symbols: [] },
240258
{ spec: './namespace.ts', dynamic: false, typeOnly: false, symbols: [] },
241259
{ spec: './dynamic.ts', dynamic: true, typeOnly: false, symbols: [] },
242260
],

‎scripts/layering/model.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,13 @@ function namedSymbols(
190190
return [...new Set(symbols)];
191191
}
192192

193+
function importTypeSourceName(qualifier: unknown): string | undefined {
194+
if (qualifier === null || typeof qualifier !== 'object') return undefined;
195+
const record = qualifier as Record<string, unknown>;
196+
if (record.type === 'Identifier') return importedName(qualifier);
197+
return record.type === 'TSQualifiedName' ? importTypeSourceName(record.left) : undefined;
198+
}
199+
193200
function specifierTypeOnly(
194201
declaration: Record<string, unknown>,
195202
specifiers: readonly Record<string, unknown>[],
@@ -272,7 +279,14 @@ export function parseImports(source: string): ImportEdge[] {
272279
} else if (node.type === 'ExportAllDeclaration') {
273280
edge = staticImportEdge(source, node, node.source, node.exportKind === 'type', []);
274281
} else if (node.type === 'TSImportType') {
275-
edge = staticImportEdge(source, node, node.source, true, []);
282+
const sourceName = importTypeSourceName(node.qualifier);
283+
edge = staticImportEdge(
284+
source,
285+
node,
286+
node.source,
287+
true,
288+
sourceName === undefined ? [] : [sourceName],
289+
);
276290
}
277291
if (edge) located.push({ edge, start, order: order++ });
278292
});

‎scripts/layering/session-authority-overlay.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,23 @@ test('the end-to-end measurement feeds the ratchet: a new handler importer is re
135135
assert.equal(violations.length, 1);
136136
assert.equal(violations[0]!.file, 'src/daemon/handlers/new.ts');
137137
});
138+
139+
test('R75 catches a new handler shape edge expressed as an import type', () => {
140+
const base = {
141+
'src/daemon/session-state.ts': STATE_STUB,
142+
'src/daemon/session-store.ts': STORE_STUB,
143+
};
144+
const reference = handlerOwnedOverlay(overlayOf(base));
145+
const measured = handlerOwnedOverlay(
146+
overlayOf({
147+
...base,
148+
'src/daemon/handlers/new.ts': "type State = import('../session-state.ts').SessionState;\n",
149+
}),
150+
);
151+
152+
const violations = checkSessionAuthorityOverlay(measured, reference);
153+
assert.equal(violations.length, 1);
154+
assert.equal(violations[0]!.rule, SESSION_AUTHORITY_OVERLAY_RULE);
155+
assert.equal(violations[0]!.file, 'src/daemon/handlers/new.ts');
156+
assert.match(violations[0]!.message, /new handler-owned SessionState shape edge/);
157+
});

0 commit comments

Comments
 (0)