Ignore type-only import cycles

This commit is contained in:
murashit 2026-06-15 15:50:49 +09:00
parent 328832a68e
commit 87cac9ee85
2 changed files with 42 additions and 3 deletions

View file

@ -75,11 +75,16 @@ function importEdges(sourceFile) {
const edges = [];
for (const statement of sourceFile.statements) {
if (ts.isImportDeclaration(statement) && stringLiteralText(statement.moduleSpecifier)) {
if (ts.isImportDeclaration(statement) && hasRuntimeImportEdge(statement) && stringLiteralText(statement.moduleSpecifier)) {
edges.push({
specifier: stringLiteralText(statement.moduleSpecifier),
});
} else if (ts.isExportDeclaration(statement) && statement.moduleSpecifier && stringLiteralText(statement.moduleSpecifier)) {
} else if (
ts.isExportDeclaration(statement) &&
hasRuntimeExportEdge(statement) &&
statement.moduleSpecifier &&
stringLiteralText(statement.moduleSpecifier)
) {
edges.push({
specifier: stringLiteralText(statement.moduleSpecifier),
});
@ -89,6 +94,27 @@ function importEdges(sourceFile) {
return edges;
}
function hasRuntimeImportEdge(statement) {
const importClause = statement.importClause;
if (!importClause) return true;
if (importClause.isTypeOnly) return false;
if (importClause.name) return true;
const namedBindings = importClause.namedBindings;
if (!namedBindings) return false;
if (ts.isNamespaceImport(namedBindings)) return true;
return namedBindings.elements.some((element) => !element.isTypeOnly);
}
function hasRuntimeExportEdge(statement) {
if (statement.isTypeOnly) return false;
const exportClause = statement.exportClause;
if (!exportClause) return true;
if (ts.isNamespaceExport(exportClause)) return true;
return exportClause.elements.some((element) => !element.isTypeOnly);
}
function stringLiteralText(node) {
return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : null;
}

View file

@ -171,7 +171,7 @@ describe("development scripts", () => {
expect(result.stderr).toContain("src/a.ts -> src/b.ts -> src/a.ts");
});
it("detects type-only import cycles", async () => {
it("ignores type-only import cycles", async () => {
const cwd = await tempWorkspace();
await writeImportCycleFixture(cwd, {
"src/a.ts": 'import type { B } from "./b";\nexport interface A { b?: B }\n',
@ -180,6 +180,19 @@ describe("development scripts", () => {
const result = runNodeScript("scripts/check-import-cycles.mjs", [], cwd);
expect(result.status).toBe(0);
expect(result.stdout).toContain("No import cycles found.");
});
it("detects import cycles when mixed imports include runtime values", async () => {
const cwd = await tempWorkspace();
await writeImportCycleFixture(cwd, {
"src/a.ts": 'import { type B, b } from "./b";\nexport interface A { b?: B }\nexport const a = b;\n',
"src/b.ts": 'import { a, type A } from "./a";\nexport interface B { a?: A }\nexport const b = a;\n',
});
const result = runNodeScript("scripts/check-import-cycles.mjs", [], cwd);
expect(result.status).toBe(1);
expect(result.stderr).toContain("found 1 cycle");
expect(result.stderr).toContain("src/a.ts -> src/b.ts -> src/a.ts");