|
1 | 1 | import type { TSESTree, TSESLint, ParserServicesWithTypeInformation } from '@typescript-eslint/utils'; |
2 | 2 | import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils'; |
| 3 | +import * as path from 'node:path'; |
3 | 4 | import * as ts from 'typescript'; |
4 | 5 |
|
5 | 6 | // NOTE: The rule will be available in ESLint configs as "@nx/workspace-base-hook-no-forbidden-runtime" |
@@ -100,8 +101,6 @@ interface Reach { |
100 | 101 | all: ReadonlySet<string>; |
101 | 102 | } |
102 | 103 |
|
103 | | -const EMPTY_REACH: Reach = { value: new Set(), all: new Set() }; |
104 | | - |
105 | 104 | /** |
106 | 105 | * Per-Program cache: source file path → reach sets transitively computed from that file. |
107 | 106 | * Both `value` and `all` sets are filled in a single DFS pass to share resolution work. |
@@ -497,58 +496,60 @@ function transitiveReach(program: ts.Program, sourceFile: ts.SourceFile): Reach |
497 | 496 | /** |
498 | 497 | * Recursive worker for `transitiveReach`. Walks the import graph DFS, recording every bare |
499 | 498 | * specifier encountered (separately for value-only vs value+type follow modes) and recursing into |
500 | | - * each resolved source file. Uses `inProgress` to break cycles (cycle hits return empty sets |
501 | | - * without caching, so the originating call still commits the complete result). |
| 499 | + * each resolved source file. Uses `inProgress` to break cycles by returning the already-cached, |
| 500 | + * in-progress reach object for the cycle participant. |
502 | 501 | */ |
503 | 502 | function computeReach( |
504 | 503 | program: ts.Program, |
505 | 504 | sourceFile: ts.SourceFile, |
506 | 505 | cache: Map<string, Reach>, |
507 | 506 | inProgress: Set<string>, |
508 | 507 | ): Reach { |
509 | | - const cached = cache.get(sourceFile.fileName); |
| 508 | + const fileName = sourceFile.fileName; |
| 509 | + const cached = cache.get(fileName); |
510 | 510 | if (cached) { |
511 | 511 | return cached; |
512 | 512 | } |
513 | | - if (inProgress.has(sourceFile.fileName)) { |
514 | | - // Cycle: return empty sets without caching so the eventual full result is committed by the originator. |
515 | | - return EMPTY_REACH; |
516 | | - } |
517 | | - inProgress.add(sourceFile.fileName); |
518 | | - |
519 | 513 | const value = new Set<string>(); |
520 | 514 | const all = new Set<string>(); |
521 | | - for (const imp of collectImports(sourceFile)) { |
522 | | - if (isBareSpecifier(imp.specifier)) { |
523 | | - const pkg = packageNameOf(imp.specifier); |
524 | | - all.add(pkg); |
525 | | - if (!imp.typeOnly) { |
526 | | - value.add(pkg); |
| 515 | + const result: Reach = { value, all }; |
| 516 | + cache.set(fileName, result); |
| 517 | + if (inProgress.has(fileName)) { |
| 518 | + return result; |
| 519 | + } |
| 520 | + inProgress.add(fileName); |
| 521 | + |
| 522 | + try { |
| 523 | + for (const imp of collectImports(sourceFile)) { |
| 524 | + if (isBareSpecifier(imp.specifier)) { |
| 525 | + const pkg = packageNameOf(imp.specifier); |
| 526 | + all.add(pkg); |
| 527 | + if (!imp.typeOnly) { |
| 528 | + value.add(pkg); |
| 529 | + } |
527 | 530 | } |
528 | | - } |
529 | | - const resolved = resolveModule(program, sourceFile, imp.specifier, imp.literal); |
530 | | - if (!resolved) { |
531 | | - continue; |
532 | | - } |
533 | | - const childSourceFile = program.getSourceFile(resolved); |
534 | | - if (!childSourceFile) { |
535 | | - continue; |
536 | | - } |
537 | | - const childReach = computeReach(program, childSourceFile, cache, inProgress); |
538 | | - for (const pkg of childReach.all) { |
539 | | - all.add(pkg); |
540 | | - } |
541 | | - if (!imp.typeOnly) { |
542 | | - // A type-only edge does not propagate runtime reach: it can only widen the `all` set. |
543 | | - for (const pkg of childReach.value) { |
544 | | - value.add(pkg); |
| 531 | + const resolved = resolveModule(program, sourceFile, imp.specifier, imp.literal); |
| 532 | + if (!resolved) { |
| 533 | + continue; |
| 534 | + } |
| 535 | + const childSourceFile = program.getSourceFile(resolved); |
| 536 | + if (!childSourceFile) { |
| 537 | + continue; |
| 538 | + } |
| 539 | + const childReach = computeReach(program, childSourceFile, cache, inProgress); |
| 540 | + for (const pkg of childReach.all) { |
| 541 | + all.add(pkg); |
| 542 | + } |
| 543 | + if (!imp.typeOnly) { |
| 544 | + // A type-only edge does not propagate runtime reach: it can only widen the `all` set. |
| 545 | + for (const pkg of childReach.value) { |
| 546 | + value.add(pkg); |
| 547 | + } |
545 | 548 | } |
546 | 549 | } |
| 550 | + } finally { |
| 551 | + inProgress.delete(fileName); |
547 | 552 | } |
548 | | - |
549 | | - inProgress.delete(sourceFile.fileName); |
550 | | - const result: Reach = { value, all }; |
551 | | - cache.set(sourceFile.fileName, result); |
552 | 553 | return result; |
553 | 554 | } |
554 | 555 |
|
@@ -691,14 +692,22 @@ function packageNameOf(specifier: string): string { |
691 | 692 | * path workspace-relative when inside the current working directory. |
692 | 693 | */ |
693 | 694 | function shortenPath(absolute: string): string { |
694 | | - const marker = '/node_modules/'; |
695 | | - const idx = absolute.lastIndexOf(marker); |
696 | | - if (idx !== -1) { |
697 | | - return absolute.slice(idx + marker.length); |
| 695 | + const resolvedAbsolute = path.resolve(absolute); |
| 696 | + const normalizedAbsolute = toPosixPath(resolvedAbsolute); |
| 697 | + const segments = normalizedAbsolute.split('/'); |
| 698 | + const nodeModulesIdx = segments.lastIndexOf('node_modules'); |
| 699 | + if (nodeModulesIdx !== -1 && nodeModulesIdx + 1 < segments.length) { |
| 700 | + return segments.slice(nodeModulesIdx + 1).join('/'); |
698 | 701 | } |
699 | | - const cwd = process.cwd(); |
700 | | - if (absolute.startsWith(cwd)) { |
701 | | - return absolute.slice(cwd.length + 1); |
| 702 | + |
| 703 | + const relative = path.relative(path.resolve(process.cwd()), resolvedAbsolute); |
| 704 | + if (relative.length > 0 && !relative.startsWith('..') && !path.isAbsolute(relative)) { |
| 705 | + return toPosixPath(relative); |
702 | 706 | } |
703 | | - return absolute; |
| 707 | + |
| 708 | + return normalizedAbsolute; |
| 709 | +} |
| 710 | + |
| 711 | +function toPosixPath(value: string): string { |
| 712 | + return value.replace(/\\/g, '/'); |
704 | 713 | } |
0 commit comments