Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions src/lib/drift-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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: [],
};
}
Expand Down
12 changes: 12 additions & 0 deletions tests/unit/lib/drift-sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down