From 18ce12c0c964624b4d69f83a30926e14779c7d89 Mon Sep 17 00:00:00 2001 From: benwu95 Date: Thu, 9 Jul 2026 18:18:34 +0800 Subject: [PATCH] fix: report import-direction skipped for non-JS/TS projects, not a vacuous pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit import-direction only parses JS/TS ES-module imports, but it reported available+0 edges (a PASS) on a project whose module-path dirs exist yet hold no JS/TS source — falsely claiming the dependency direction was verified. - set `available` only when JS/TS source is actually found; otherwise report honest `skipped` with a reason that says the check is JS/TS-only and points to the language's own import linter (import-linter / deptrac / packwerk / …) - JS/TS behavior unchanged (edge collection/attribution/sort); existing tests green - validated on a large real-world Python project: was a vacuous PASS, now skipped - supersedes the abandoned tree-sitter multi-language attempt — native per-language architecture linters do cross-language dependency-direction far better --- src/lib/drift-sources.ts | 31 ++++++++++++++++++---------- tests/unit/lib/drift-sources.test.ts | 12 +++++++++++ 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/lib/drift-sources.ts b/src/lib/drift-sources.ts index d399031..64b2a95 100644 --- a/src/lib/drift-sources.ts +++ b/src/lib/drift-sources.ts @@ -281,7 +281,15 @@ export function collectMarkdownLinks(roots: string[], cwd: string): LinkSource { return { available: true, links }; } -/** Collect cross-module static import edges, attributed via module-map paths. */ +/** + * Collect cross-module static import edges, attributed via module-map paths. + * Deliberately JS/TS-only: it parses ES-module `import … from` / side-effect + * imports. Other languages resolve imports through their own systems (package + * roots, namespaces) a lightweight scan cannot follow, and each has a + * purpose-built architecture linter (import-linter, deptrac, packwerk, + * dependency-cruiser, go-arch-lint, …) that does it properly — so on a project + * with no JS/TS source this reports honest `skipped`, never a vacuous PASS. + */ export function collectImportEdges(cwd: string, moduleMap: ModuleMap): ImportEdgeSource { const toModule = moduleAttributor(moduleMap); // Whole-content matching so multi-line `import { … }\nfrom 'x'` statements are @@ -290,21 +298,21 @@ export function collectImportEdges(cwd: string, moduleMap: ModuleMap): ImportEdg const importPattern = /(?:^|\n)\s*(?:(?:import|export)\s+[^;'"`]*?from\s*|import\s*)['"]([^'"]+)['"]/g; const edges: ImportEdge[] = []; - let anyPathExists = false; + let anyJsTsSource = false; for (const entry of moduleMap.modules) { for (const prefix of entry.paths) { const isGlob = prefix.includes('*'); - // A literal dir prefix is gated by its on-disk existence. A domain glob - // ('**/auth/**') has no literal path to stat — `existsSync` on it is - // always false — so it is scanned directly and counts as available only - // when the glob actually matches files (domain projects relied on the - // import-direction check silently degrading to `skipped` before this). + // A literal dir prefix is gated by its on-disk existence; a domain glob + // ('**/auth/**') has no literal path to stat, so it is scanned directly. if (!isGlob && !existsSync(path.resolve(cwd, prefix))) continue; const pattern = importScanPattern(prefix, cwd); if (pattern === null) continue; // non-source file entry — carries no import edges const { files } = scanDirSync(pattern, { cwd }); - if (isGlob && files.length === 0) continue; - anyPathExists = true; + // The check is `available` only where JS/TS source is actually found — + // a path (or whole project) with no .ts/.js contributes nothing, so a + // non-JS/TS project degrades to honest `skipped`, not a vacuous PASS. + if (files.length === 0) continue; + anyJsTsSource = true; for (const relPath of files) { const fromModule = toModule(relPath); if (fromModule !== entry.name) continue; // longest-prefix owner emits the edge once @@ -331,10 +339,11 @@ export function collectImportEdges(cwd: string, moduleMap: ModuleMap): ImportEdg } } } - if (!anyPathExists) { + if (!anyJsTsSource) { return { available: false, - reason: 'source unavailable: none of the module paths exist on disk', + reason: + "source unavailable: no JavaScript/TypeScript source under module paths (import-direction is JS/TS-only — use your language's import linter, e.g. import-linter / deptrac / packwerk, for others)", edges: [], }; } diff --git a/tests/unit/lib/drift-sources.test.ts b/tests/unit/lib/drift-sources.test.ts index 5371053..0abdb7e 100644 --- a/tests/unit/lib/drift-sources.test.ts +++ b/tests/unit/lib/drift-sources.test.ts @@ -273,6 +273,18 @@ describe('collectImportEdges', () => { expect(r.reason).toContain('source unavailable'); }); + it('reports skipped (not a vacuous PASS) for a non-JS/TS project whose module paths exist but hold no JS/TS source', () => { + // The dirs exist and contain source — but Python, not JS/TS. import-direction + // is JS/TS-only, so it must degrade to skipped, never report available/PASS + // (which would falsely claim the dependency direction was verified). + write('src/services/main.py', 'from ..cli.app import x\n'); + write('src/cli/app.py', 'pass\n'); + const r = collectImportEdges(tmpDir, MODULE_MAP); + expect(r.available).toBe(false); + expect(r.reason).toContain('JavaScript/TypeScript'); + expect(r.edges).toEqual([]); + }); + it('ignores imports inside block comments (commented-out code is not an edge)', () => { write( 'src/types/x.ts',