-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin): [no-circular-imports] add new rule
- Loading branch information
Showing
19 changed files
with
390 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
--- | ||
description: 'Disallow the use of import module that result in circular imports.' | ||
--- | ||
|
||
import Tabs from '@theme/Tabs'; | ||
import TabItem from '@theme/TabItem'; | ||
|
||
> 🛑 This file is source code, not the primary documentation location! 🛑 | ||
> | ||
> See **https://typescript-eslint.io/rules/no-circular-import** for documentation. | ||
This rule disallows the use of import module that result in circular imports except for the type-only imports. | ||
|
||
## Examples | ||
|
||
<Tabs> | ||
<TabItem value="❌ Incorrect"> | ||
|
||
```ts skipValidation | ||
// foo.ts | ||
import { bar } from './bar'; | ||
export const foo = 1; | ||
|
||
// bar.ts | ||
import { foo } from './foo'; | ||
export const bar = 1; | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="✅ Correct"> | ||
|
||
```ts | ||
// foo.ts | ||
import type { bar } from './bar'; | ||
export type baz = number; | ||
|
||
// bar.ts | ||
import { type baz } from './baz'; | ||
export type bar = number; | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
## When Not To Use It | ||
|
||
## Related To |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
import * as ts from 'typescript'; | ||
|
||
import { createRule, getParserServices } from '../util'; | ||
|
||
type Options = []; | ||
type MessageIds = 'noCircularImport'; | ||
|
||
interface Edge { | ||
filename: string; | ||
specifier: string; | ||
} | ||
|
||
class Graph { | ||
private graph = new Map< | ||
string, | ||
{ | ||
filename: string; | ||
specifier: string; | ||
}[] | ||
>(); | ||
|
||
addEdge(start: string, edge: Edge): void { | ||
if (this.graph.has(start)) { | ||
this.graph.get(start)?.push(edge); | ||
} else { | ||
this.graph.set(start, [edge]); | ||
} | ||
} | ||
|
||
hasEdge(name: string): boolean { | ||
return this.graph.has(name); | ||
} | ||
|
||
getEdges(name: string): Edge[] { | ||
return this.graph.get(name) ?? []; | ||
} | ||
} | ||
// imports “a.ts” and is imported from “b.ts”, resulting in a circular reference. | ||
export default createRule<Options, MessageIds>({ | ||
name: 'no-circular-import', | ||
meta: { | ||
docs: { | ||
description: | ||
'Disallow the use of import module that result in circular imports', | ||
requiresTypeChecking: true, | ||
}, | ||
messages: { | ||
noCircularImport: 'Circular import dcetected via {{paths}}.', | ||
}, | ||
schema: [], | ||
type: 'suggestion', | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
const services = getParserServices(context); | ||
const graph = new Graph(); | ||
|
||
function resolveSpecifier( | ||
containingFile: string, | ||
specifier: string, | ||
): ts.ResolvedModuleWithFailedLookupLocations { | ||
return ts.resolveModuleName( | ||
specifier, | ||
containingFile, | ||
services.program.getCompilerOptions(), | ||
ts.sys, | ||
); | ||
} | ||
|
||
function isTypeOnlyImport(node: ts.ImportDeclaration): boolean { | ||
return ( | ||
node.importClause?.isTypeOnly || | ||
(!!node.importClause?.namedBindings && | ||
ts.isNamedImports(node.importClause.namedBindings) && | ||
node.importClause.namedBindings.elements.every( | ||
elem => elem.isTypeOnly, | ||
)) | ||
); | ||
} | ||
|
||
function addEdgesRecursively( | ||
graph: Graph, | ||
containingFile: string, | ||
importDeclaration: ts.ImportDeclaration, | ||
): void { | ||
if (graph.hasEdge(containingFile)) { | ||
return; | ||
} | ||
|
||
if (isTypeOnlyImport(importDeclaration)) { | ||
return; | ||
} | ||
|
||
if (!ts.isStringLiteral(importDeclaration.moduleSpecifier)) { | ||
return; | ||
} | ||
|
||
const specifier = importDeclaration.moduleSpecifier.text; | ||
|
||
const resolved = resolveSpecifier(containingFile, specifier); | ||
|
||
if ( | ||
!resolved.resolvedModule || | ||
resolved.resolvedModule.isExternalLibraryImport | ||
) { | ||
return; | ||
} | ||
|
||
const resolvedFile = resolved.resolvedModule.resolvedFileName; | ||
|
||
graph.addEdge(containingFile, { | ||
filename: resolvedFile, | ||
specifier, | ||
}); | ||
|
||
const sourceCode = services.program.getSourceFile(resolvedFile); | ||
sourceCode?.statements.forEach(statement => { | ||
if (ts.isImportDeclaration(statement)) { | ||
addEdgesRecursively(graph, resolvedFile, statement); | ||
} | ||
}); | ||
} | ||
|
||
function detectCycleWorker( | ||
start: string, | ||
graph: Graph, | ||
filename: string, | ||
visited: Set<string>, | ||
paths: string[], | ||
): string[] { | ||
visited.add(filename); | ||
|
||
for (const edge of graph.getEdges(filename)) { | ||
if (visited.has(edge.filename)) { | ||
if (edge.filename === start) { | ||
return paths.concat(edge.specifier); | ||
} | ||
return []; | ||
} | ||
const detected = detectCycleWorker( | ||
start, | ||
graph, | ||
edge.filename, | ||
visited, | ||
paths.concat(edge.specifier), | ||
); | ||
if (detected.length) { | ||
return detected; | ||
} | ||
} | ||
return []; | ||
} | ||
|
||
function detectCycle(graph: Graph, filename: string): string[] { | ||
const visited = new Set<string>(); | ||
return detectCycleWorker(filename, graph, filename, visited, []); | ||
} | ||
|
||
return { | ||
ImportDeclaration(node): void { | ||
const tsNode = services.esTreeNodeToTSNodeMap.get(node); | ||
const containingFile = tsNode.parent.getSourceFile().fileName; | ||
addEdgesRecursively(graph, containingFile, tsNode); | ||
|
||
const cycle = detectCycle(graph, containingFile); | ||
if (cycle.length > 1) { | ||
context.report({ | ||
messageId: 'noCircularImport', | ||
node, | ||
data: { | ||
paths: | ||
cycle.length === 2 | ||
? cycle[0] | ||
: `${cycle[0]} ... ${cycle[cycle.length - 2]}`, | ||
}, | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}); |
23 changes: 23 additions & 0 deletions
23
packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-circular-import.shot
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
2 changes: 2 additions & 0 deletions
2
packages/eslint-plugin/tests/fixtures/no-circular-import/depth-one.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import { entry } from './entry'; | ||
export const one = entry; |
2 changes: 2 additions & 0 deletions
2
packages/eslint-plugin/tests/fixtures/no-circular-import/depth-three.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import { two } from './depth-two'; | ||
export const three = two; |
2 changes: 2 additions & 0 deletions
2
packages/eslint-plugin/tests/fixtures/no-circular-import/depth-two.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import { one } from './depth-one'; | ||
export const two = one; |
1 change: 1 addition & 0 deletions
1
packages/eslint-plugin/tests/fixtures/no-circular-import/entry.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const entry = 1; |
2 changes: 2 additions & 0 deletions
2
packages/eslint-plugin/tests/fixtures/no-circular-import/isolated-circular-a.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import { b } from './isolated-circular-b'; | ||
export const a = 1; |
2 changes: 2 additions & 0 deletions
2
packages/eslint-plugin/tests/fixtures/no-circular-import/isolated-circular-b.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import { a } from './isolated-circular-a'; | ||
export const b = 1; |
1 change: 1 addition & 0 deletions
1
packages/eslint-plugin/tests/fixtures/no-circular-import/no-circular.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export default {}; |
3 changes: 3 additions & 0 deletions
3
packages/eslint-plugin/tests/fixtures/no-circular-import/type-only.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import type { entry } from './entry'; | ||
import { type entry as EntryType } from './entry'; | ||
export type TypeOnly = typeof entry | typeof EntryType; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.