@borela-tech/eslint-config 1.3.3 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lint +3 -1
- package/bin/test +12 -0
- package/bin/typecheck +8 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +320 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +21 -16
- package/src/rules/__tests__/dedent/countLeadingSpaces.ts +4 -0
- package/src/rules/__tests__/dedent/findMinIndent.ts +7 -0
- package/src/rules/__tests__/dedent/index.ts +17 -0
- package/src/rules/__tests__/dedent/interpolate.ts +11 -0
- package/src/rules/__tests__/dedent/removeEmptyPrefix.ts +6 -0
- package/src/rules/__tests__/dedent/removeEmptySuffix.ts +6 -0
- package/src/rules/__tests__/dedent/removeIndent.ts +3 -0
- package/src/rules/__tests__/individualImports.test.ts +42 -0
- package/src/rules/__tests__/sortedImports.test.ts +148 -0
- package/src/rules/individualImports.ts +3 -3
- package/src/rules/sortedImports/CategorizedImport.ts +8 -0
- package/src/rules/sortedImports/ImportError.ts +6 -0
- package/src/rules/sortedImports/ImportGroup.ts +1 -0
- package/src/rules/sortedImports/areSpecifiersSorted.ts +10 -0
- package/src/rules/sortedImports/categorizeImport.ts +15 -0
- package/src/rules/sortedImports/categorizeImports.ts +12 -0
- package/src/rules/sortedImports/checkAlphabeticalSorting.ts +22 -0
- package/src/rules/sortedImports/checkGroupOrdering.ts +22 -0
- package/src/rules/sortedImports/checkSpecifiersSorting.ts +21 -0
- package/src/rules/sortedImports/createFix/ReplacementRange.ts +4 -0
- package/src/rules/sortedImports/createFix/buildSortedCode.ts +22 -0
- package/src/rules/sortedImports/createFix/findLastImportIndex.ts +12 -0
- package/src/rules/sortedImports/createFix/formatNamedImport.ts +23 -0
- package/src/rules/sortedImports/createFix/getReplacementRange.ts +14 -0
- package/src/rules/sortedImports/createFix/groupImportsByType.ts +18 -0
- package/src/rules/sortedImports/createFix/index.ts +28 -0
- package/src/rules/sortedImports/createFix/sortImportGroups.ts +11 -0
- package/src/rules/sortedImports/getImportDeclarations.ts +8 -0
- package/src/rules/sortedImports/getNamedSpecifiers.ts +8 -0
- package/src/rules/sortedImports/getSortKey.ts +20 -0
- package/src/rules/sortedImports/getSpecifierName.ts +7 -0
- package/src/rules/sortedImports/index.ts +54 -0
- package/src/rules/sortedImports/sortSpecifiersText.ts +14 -0
- package/src/rules/sortedImports.ts +0 -83
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import typescript from 'typescript-eslint'
|
|
2
|
+
import {dedent} from './dedent'
|
|
3
|
+
import {individualImports} from '../individualImports'
|
|
4
|
+
import {RuleTester} from 'eslint'
|
|
5
|
+
|
|
6
|
+
const ruleTester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser: typescript.parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
ruleTester.run('individual-imports', individualImports, {
|
|
17
|
+
valid: [{
|
|
18
|
+
code: "import {foo} from 'bar'",
|
|
19
|
+
}, {
|
|
20
|
+
code: "import foo from 'bar'",
|
|
21
|
+
}, {
|
|
22
|
+
code: "import * as foo from 'bar'",
|
|
23
|
+
}, {
|
|
24
|
+
code: "import 'bar'",
|
|
25
|
+
}],
|
|
26
|
+
invalid: [{
|
|
27
|
+
code: "import {foo, bar} from 'baz'",
|
|
28
|
+
errors: [{messageId: 'individualImports'}],
|
|
29
|
+
output: dedent`
|
|
30
|
+
import {foo} from 'baz'
|
|
31
|
+
import {bar} from 'baz'
|
|
32
|
+
`,
|
|
33
|
+
}, {
|
|
34
|
+
code: "import {foo, bar, baz} from 'qux'",
|
|
35
|
+
errors: [{messageId: 'individualImports'}],
|
|
36
|
+
output: dedent`
|
|
37
|
+
import {foo} from 'qux'
|
|
38
|
+
import {bar} from 'qux'
|
|
39
|
+
import {baz} from 'qux'
|
|
40
|
+
`,
|
|
41
|
+
}],
|
|
42
|
+
})
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import typescript from 'typescript-eslint'
|
|
2
|
+
import {dedent} from './dedent'
|
|
3
|
+
import {RuleTester} from 'eslint'
|
|
4
|
+
import {sortedImports} from '../sortedImports'
|
|
5
|
+
|
|
6
|
+
const ruleTester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser: typescript.parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
ruleTester.run('sorted-imports', sortedImports, {
|
|
17
|
+
valid: [{
|
|
18
|
+
code: "import {foo} from 'bar'",
|
|
19
|
+
}, {
|
|
20
|
+
code: "import foo from 'bar'",
|
|
21
|
+
}, {
|
|
22
|
+
code: "import 'bar'",
|
|
23
|
+
}, {
|
|
24
|
+
code: "import type {Foo} from 'bar'",
|
|
25
|
+
}, {
|
|
26
|
+
code: dedent`
|
|
27
|
+
import 'aaa'
|
|
28
|
+
import 'bbb'
|
|
29
|
+
import bar from 'bbb'
|
|
30
|
+
import foo from 'aaa'
|
|
31
|
+
import {a} from 'aaa'
|
|
32
|
+
import {b} from 'bbb'
|
|
33
|
+
import type {X} from 'xxx'
|
|
34
|
+
import type {Y} from 'yyy'
|
|
35
|
+
`,
|
|
36
|
+
}, {
|
|
37
|
+
code: dedent`
|
|
38
|
+
import {a} from 'ccc'
|
|
39
|
+
import {b} from 'aaa'
|
|
40
|
+
import {c} from 'bbb'
|
|
41
|
+
`,
|
|
42
|
+
}, {
|
|
43
|
+
code: dedent`
|
|
44
|
+
import {a, b, c} from 'bar'
|
|
45
|
+
`,
|
|
46
|
+
}, {
|
|
47
|
+
code: '',
|
|
48
|
+
}, {
|
|
49
|
+
code: 'const x = 1',
|
|
50
|
+
}],
|
|
51
|
+
invalid: [{
|
|
52
|
+
code: dedent`
|
|
53
|
+
import {c, a, b} from 'bar'
|
|
54
|
+
`,
|
|
55
|
+
errors: [{messageId: 'sortedNames'}],
|
|
56
|
+
output: dedent`
|
|
57
|
+
import { a, b, c } from 'bar'
|
|
58
|
+
`,
|
|
59
|
+
}, {
|
|
60
|
+
code: dedent`
|
|
61
|
+
import {z, a} from 'bar'
|
|
62
|
+
`,
|
|
63
|
+
errors: [{messageId: 'sortedNames'}],
|
|
64
|
+
output: dedent`
|
|
65
|
+
import { a, z } from 'bar'
|
|
66
|
+
`,
|
|
67
|
+
}, {
|
|
68
|
+
code: dedent`
|
|
69
|
+
import foo from 'aaa'
|
|
70
|
+
import bar from 'bbb'
|
|
71
|
+
`,
|
|
72
|
+
errors: [{messageId: 'sortedImports'}, {messageId: 'sortedImports'}],
|
|
73
|
+
output: dedent`
|
|
74
|
+
import bar from 'bbb'
|
|
75
|
+
import foo from 'aaa'
|
|
76
|
+
`,
|
|
77
|
+
}, {
|
|
78
|
+
code: dedent`
|
|
79
|
+
import 'bbb'
|
|
80
|
+
import 'aaa'
|
|
81
|
+
`,
|
|
82
|
+
errors: [{messageId: 'sortedImports'}, {messageId: 'sortedImports'}],
|
|
83
|
+
output: dedent`
|
|
84
|
+
import 'aaa'
|
|
85
|
+
import 'bbb'
|
|
86
|
+
`,
|
|
87
|
+
}, {
|
|
88
|
+
code: dedent`
|
|
89
|
+
import foo from 'bar'
|
|
90
|
+
import 'baz'
|
|
91
|
+
`,
|
|
92
|
+
errors: [{messageId: 'wrongGroup'}],
|
|
93
|
+
output: dedent`
|
|
94
|
+
import 'baz'
|
|
95
|
+
import foo from 'bar'
|
|
96
|
+
`,
|
|
97
|
+
}, {
|
|
98
|
+
code: dedent`
|
|
99
|
+
import {a} from 'bar'
|
|
100
|
+
import foo from 'baz'
|
|
101
|
+
`,
|
|
102
|
+
errors: [{messageId: 'wrongGroup'}],
|
|
103
|
+
output: dedent`
|
|
104
|
+
import foo from 'baz'
|
|
105
|
+
import {a} from 'bar'
|
|
106
|
+
`,
|
|
107
|
+
}, {
|
|
108
|
+
code: dedent`
|
|
109
|
+
import {b, a} from 'bar'
|
|
110
|
+
import foo from 'baz'
|
|
111
|
+
`,
|
|
112
|
+
errors: [{messageId: 'sortedNames'}, {messageId: 'wrongGroup'}],
|
|
113
|
+
output: dedent`
|
|
114
|
+
import foo from 'baz'
|
|
115
|
+
import { a, b } from 'bar'
|
|
116
|
+
`,
|
|
117
|
+
}, {
|
|
118
|
+
code: dedent`
|
|
119
|
+
import type {Y} from 'yyy'
|
|
120
|
+
import type {X} from 'xxx'
|
|
121
|
+
`,
|
|
122
|
+
errors: [{messageId: 'sortedImports'}, {messageId: 'sortedImports'}],
|
|
123
|
+
output: dedent`
|
|
124
|
+
import type {X} from 'xxx'
|
|
125
|
+
import type {Y} from 'yyy'
|
|
126
|
+
`,
|
|
127
|
+
}, {
|
|
128
|
+
code: dedent`
|
|
129
|
+
import type {Foo} from 'bar'
|
|
130
|
+
import {baz} from 'qux'
|
|
131
|
+
`,
|
|
132
|
+
errors: [{messageId: 'wrongGroup'}],
|
|
133
|
+
output: dedent`
|
|
134
|
+
import {baz} from 'qux'
|
|
135
|
+
import type {Foo} from 'bar'
|
|
136
|
+
`,
|
|
137
|
+
}, {
|
|
138
|
+
code: dedent`
|
|
139
|
+
import {existsSync} from 'fs'
|
|
140
|
+
import {basename} from 'path'
|
|
141
|
+
`,
|
|
142
|
+
errors: [{messageId: 'sortedImports'}, {messageId: 'sortedImports'}],
|
|
143
|
+
output: dedent`
|
|
144
|
+
import {basename} from 'path'
|
|
145
|
+
import {existsSync} from 'fs'
|
|
146
|
+
`,
|
|
147
|
+
}],
|
|
148
|
+
})
|
|
@@ -24,9 +24,9 @@ export const individualImports: Rule.RuleModule = {
|
|
|
24
24
|
fix(fixer) {
|
|
25
25
|
const source = node.source.raw
|
|
26
26
|
const specifiers = node.specifiers
|
|
27
|
-
.map(
|
|
28
|
-
if (
|
|
29
|
-
return `import {${
|
|
27
|
+
.map(importSpecifier => {
|
|
28
|
+
if (importSpecifier.type === 'ImportSpecifier')
|
|
29
|
+
return `import {${importSpecifier.local.name}} from ${source}`
|
|
30
30
|
return null
|
|
31
31
|
})
|
|
32
32
|
.filter(Boolean)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type ImportGroup = 'side-effect' | 'default' | 'named' | 'type'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {getSpecifierName} from './getSpecifierName'
|
|
2
|
+
import type {ImportSpecifier} from 'estree'
|
|
3
|
+
|
|
4
|
+
export function areSpecifiersSorted(specifiers: ImportSpecifier[]): boolean {
|
|
5
|
+
const names = specifiers.map(s => getSpecifierName(s))
|
|
6
|
+
const sorted = [...names].sort((a, b) =>
|
|
7
|
+
a.toLowerCase().localeCompare(b.toLowerCase()),
|
|
8
|
+
)
|
|
9
|
+
return names.every((name, i) => name === sorted[i])
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type {ImportGroup} from './ImportGroup'
|
|
2
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
3
|
+
|
|
4
|
+
export function categorizeImport(declaration: TSESTree.ImportDeclaration): ImportGroup {
|
|
5
|
+
if (declaration.importKind === 'type')
|
|
6
|
+
return 'type'
|
|
7
|
+
|
|
8
|
+
if (declaration.specifiers.length === 0)
|
|
9
|
+
return 'side-effect'
|
|
10
|
+
|
|
11
|
+
if (declaration.specifiers.some(s => s.type === 'ImportDefaultSpecifier'))
|
|
12
|
+
return 'default'
|
|
13
|
+
|
|
14
|
+
return 'named'
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {categorizeImport} from './categorizeImport'
|
|
2
|
+
import {getSortKey} from './getSortKey'
|
|
3
|
+
import type {CategorizedImport} from './CategorizedImport'
|
|
4
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
5
|
+
|
|
6
|
+
export function categorizeImports(declarations: TSESTree.ImportDeclaration[]): CategorizedImport[] {
|
|
7
|
+
return declarations.map(declaration => ({
|
|
8
|
+
declaration,
|
|
9
|
+
group: categorizeImport(declaration),
|
|
10
|
+
sortKey: getSortKey(declaration),
|
|
11
|
+
}))
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {CategorizedImport} from './CategorizedImport'
|
|
2
|
+
import type {ImportError} from './ImportError'
|
|
3
|
+
import type {ImportGroup} from './ImportGroup'
|
|
4
|
+
|
|
5
|
+
export function checkAlphabeticalSorting(categorized: CategorizedImport[]): ImportError[] {
|
|
6
|
+
const errors: ImportError[] = []
|
|
7
|
+
|
|
8
|
+
for (const group of ['side-effect', 'default', 'named', 'type'] as ImportGroup[]) {
|
|
9
|
+
const groupImports = categorized.filter(c => c.group === group)
|
|
10
|
+
const sorted = [...groupImports].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
|
|
11
|
+
for (let i = 0; i < groupImports.length; i++) {
|
|
12
|
+
if (groupImports[i] !== sorted[i]) {
|
|
13
|
+
errors.push({
|
|
14
|
+
node: groupImports[i].declaration,
|
|
15
|
+
messageId: 'sortedImports',
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return errors
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {CategorizedImport} from './CategorizedImport'
|
|
2
|
+
import type {ImportError} from './ImportError'
|
|
3
|
+
import type {ImportGroup} from './ImportGroup'
|
|
4
|
+
|
|
5
|
+
export function checkGroupOrdering(categorized: CategorizedImport[]): ImportError[] {
|
|
6
|
+
const groupOrder: ImportGroup[] = ['side-effect', 'default', 'named', 'type']
|
|
7
|
+
const errors: ImportError[] = []
|
|
8
|
+
|
|
9
|
+
let currentGroupIndex = -1
|
|
10
|
+
for (const {declaration, group} of categorized) {
|
|
11
|
+
const groupIndex = groupOrder.indexOf(group)
|
|
12
|
+
if (groupIndex < currentGroupIndex) {
|
|
13
|
+
errors.push({
|
|
14
|
+
node: declaration,
|
|
15
|
+
messageId: 'wrongGroup',
|
|
16
|
+
})
|
|
17
|
+
} else
|
|
18
|
+
currentGroupIndex = groupIndex
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return errors
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {areSpecifiersSorted} from './areSpecifiersSorted'
|
|
2
|
+
import {getNamedSpecifiers} from './getNamedSpecifiers'
|
|
3
|
+
import type {CategorizedImport} from './CategorizedImport'
|
|
4
|
+
import type {ImportError} from './ImportError'
|
|
5
|
+
|
|
6
|
+
export function checkSpecifiersSorting(categorized: CategorizedImport[]): ImportError[] {
|
|
7
|
+
const errors: ImportError[] = []
|
|
8
|
+
const namedImports = categorized.filter(c => c.group === 'named')
|
|
9
|
+
|
|
10
|
+
for (const {declaration} of namedImports) {
|
|
11
|
+
const specifiers = getNamedSpecifiers(declaration)
|
|
12
|
+
if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {
|
|
13
|
+
errors.push({
|
|
14
|
+
node: declaration,
|
|
15
|
+
messageId: 'sortedNames',
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return errors
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {formatNamedImport} from './formatNamedImport'
|
|
2
|
+
import type {CategorizedImport} from '../CategorizedImport'
|
|
3
|
+
import type {ImportGroup} from '../ImportGroup'
|
|
4
|
+
|
|
5
|
+
export function buildSortedCode(
|
|
6
|
+
grouped: Record<ImportGroup, CategorizedImport[]>,
|
|
7
|
+
sourceCode: {getText: (node?: unknown) => string},
|
|
8
|
+
): string[] {
|
|
9
|
+
const groupOrder: ImportGroup[] = ['side-effect', 'default', 'named', 'type']
|
|
10
|
+
const sortedCode: string[] = []
|
|
11
|
+
|
|
12
|
+
for (const group of groupOrder) {
|
|
13
|
+
for (const {declaration} of grouped[group]) {
|
|
14
|
+
if (group === 'named' || group === 'type')
|
|
15
|
+
sortedCode.push(formatNamedImport(declaration, sourceCode))
|
|
16
|
+
else
|
|
17
|
+
sortedCode.push(sourceCode.getText(declaration))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return sortedCode
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
2
|
+
|
|
3
|
+
export function findLastImportIndex(programBody: TSESTree.ProgramStatement[]): number {
|
|
4
|
+
let lastIndex = 0
|
|
5
|
+
for (let i = 0; i < programBody.length; i++) {
|
|
6
|
+
if (programBody[i].type === 'ImportDeclaration')
|
|
7
|
+
lastIndex = i
|
|
8
|
+
else
|
|
9
|
+
break
|
|
10
|
+
}
|
|
11
|
+
return lastIndex
|
|
12
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {areSpecifiersSorted} from '../areSpecifiersSorted'
|
|
2
|
+
import {getNamedSpecifiers} from '../getNamedSpecifiers'
|
|
3
|
+
import {sortSpecifiersText} from '../sortSpecifiersText'
|
|
4
|
+
import type {ImportDeclaration} from 'estree'
|
|
5
|
+
|
|
6
|
+
export function formatNamedImport(
|
|
7
|
+
declaration: ImportDeclaration,
|
|
8
|
+
sourceCode: {getText: (node?: unknown) => string},
|
|
9
|
+
): string {
|
|
10
|
+
const specifiers = getNamedSpecifiers(declaration)
|
|
11
|
+
|
|
12
|
+
if (specifiers.length > 1 && !areSpecifiersSorted(specifiers)) {
|
|
13
|
+
const importText = sourceCode.getText(declaration)
|
|
14
|
+
const specifiersStart = importText.indexOf('{')
|
|
15
|
+
const specifiersEnd = importText.lastIndexOf('}')
|
|
16
|
+
const before = importText.substring(0, specifiersStart + 1)
|
|
17
|
+
const after = importText.substring(specifiersEnd)
|
|
18
|
+
const sortedSpecifiers = sortSpecifiersText(specifiers, sourceCode)
|
|
19
|
+
return before + ' ' + sortedSpecifiers + ' ' + after
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return sourceCode.getText(declaration)
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {findLastImportIndex} from './findLastImportIndex'
|
|
2
|
+
import type {ReplacementRange} from './ReplacementRange'
|
|
3
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
4
|
+
|
|
5
|
+
export function getReplacementRange(
|
|
6
|
+
programBody: TSESTree.ProgramStatement[],
|
|
7
|
+
): ReplacementRange {
|
|
8
|
+
const lastIndex = findLastImportIndex(programBody)
|
|
9
|
+
const firstImport = programBody[0] as TSESTree.ImportDeclaration
|
|
10
|
+
const lastImport = programBody[lastIndex] as TSESTree.ImportDeclaration
|
|
11
|
+
const start = firstImport.range![0]
|
|
12
|
+
const end = lastImport.range![1]
|
|
13
|
+
return {start, end}
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type {CategorizedImport} from '../CategorizedImport'
|
|
2
|
+
import type {ImportGroup} from '../ImportGroup'
|
|
3
|
+
|
|
4
|
+
export function groupImportsByType(
|
|
5
|
+
categorized: CategorizedImport[],
|
|
6
|
+
): Record<ImportGroup, CategorizedImport[]> {
|
|
7
|
+
const grouped: Record<ImportGroup, CategorizedImport[]> = {
|
|
8
|
+
'side-effect': [],
|
|
9
|
+
default: [],
|
|
10
|
+
named: [],
|
|
11
|
+
type: [],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (const item of categorized)
|
|
15
|
+
grouped[item.group].push(item)
|
|
16
|
+
|
|
17
|
+
return grouped
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {buildSortedCode} from './buildSortedCode'
|
|
2
|
+
import {categorizeImports} from '../categorizeImports'
|
|
3
|
+
import {getReplacementRange} from './getReplacementRange'
|
|
4
|
+
import {groupImportsByType} from './groupImportsByType'
|
|
5
|
+
import {sortImportGroups} from './sortImportGroups'
|
|
6
|
+
import type {Rule} from 'eslint'
|
|
7
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
8
|
+
|
|
9
|
+
export function createFix(
|
|
10
|
+
fixer: Rule.RuleFixer,
|
|
11
|
+
importDeclarations: TSESTree.ImportDeclaration[],
|
|
12
|
+
sourceCode: {getText: (node?: unknown) => string},
|
|
13
|
+
programBody: TSESTree.ProgramStatement[],
|
|
14
|
+
) {
|
|
15
|
+
const range = getReplacementRange(programBody)
|
|
16
|
+
const categorized = categorizeImports(importDeclarations)
|
|
17
|
+
const grouped = groupImportsByType(categorized)
|
|
18
|
+
|
|
19
|
+
sortImportGroups(grouped)
|
|
20
|
+
|
|
21
|
+
const sortedCode = buildSortedCode(grouped, sourceCode)
|
|
22
|
+
.join('\n')
|
|
23
|
+
|
|
24
|
+
return fixer.replaceTextRange(
|
|
25
|
+
[range.start, range.end],
|
|
26
|
+
sortedCode,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type {CategorizedImport} from '../CategorizedImport'
|
|
2
|
+
import type {ImportGroup} from '../ImportGroup'
|
|
3
|
+
|
|
4
|
+
export function sortImportGroups(
|
|
5
|
+
grouped: Record<ImportGroup, CategorizedImport[]>,
|
|
6
|
+
): void {
|
|
7
|
+
grouped['side-effect'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
|
|
8
|
+
grouped['default'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
|
|
9
|
+
grouped['named'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
|
|
10
|
+
grouped['type'].sort((a, b) => a.sortKey.localeCompare(b.sortKey))
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
2
|
+
|
|
3
|
+
export function getImportDeclarations(programBody: TSESTree.ProgramStatement[]): TSESTree.ImportDeclaration[] {
|
|
4
|
+
return programBody.filter(
|
|
5
|
+
(statement): statement is TSESTree.ImportDeclaration =>
|
|
6
|
+
statement.type === 'ImportDeclaration',
|
|
7
|
+
)
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type {ImportDeclaration} from 'estree'
|
|
2
|
+
import type {ImportSpecifier} from 'estree'
|
|
3
|
+
|
|
4
|
+
export function getNamedSpecifiers(declaration: ImportDeclaration): ImportSpecifier[] {
|
|
5
|
+
return declaration.specifiers.filter(
|
|
6
|
+
(s): s is ImportSpecifier => s.type === 'ImportSpecifier',
|
|
7
|
+
)
|
|
8
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {categorizeImport} from './categorizeImport'
|
|
2
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
3
|
+
|
|
4
|
+
export function getSortKey(declaration: TSESTree.ImportDeclaration): string {
|
|
5
|
+
const group = categorizeImport(declaration)
|
|
6
|
+
|
|
7
|
+
if (group === 'side-effect')
|
|
8
|
+
return (declaration.source.value as string).toLowerCase()
|
|
9
|
+
|
|
10
|
+
if (group === 'default') {
|
|
11
|
+
const defaultSpecifier = declaration.specifiers.find(
|
|
12
|
+
s => s.type === 'ImportDefaultSpecifier',
|
|
13
|
+
) as TSESTree.ImportDefaultSpecifier | undefined
|
|
14
|
+
|
|
15
|
+
return defaultSpecifier?.local.name.toLowerCase() ?? ''
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const specifier = declaration.specifiers[0]
|
|
19
|
+
return specifier.local.name.toLowerCase()
|
|
20
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {categorizeImports} from './categorizeImports'
|
|
2
|
+
import {checkAlphabeticalSorting} from './checkAlphabeticalSorting'
|
|
3
|
+
import {checkGroupOrdering} from './checkGroupOrdering'
|
|
4
|
+
import {checkSpecifiersSorting} from './checkSpecifiersSorting'
|
|
5
|
+
import {createFix} from './createFix'
|
|
6
|
+
import {getImportDeclarations} from './getImportDeclarations'
|
|
7
|
+
import type {ImportError} from './ImportError'
|
|
8
|
+
import type {Rule} from 'eslint'
|
|
9
|
+
import type {TSESTree} from '@typescript-eslint/types'
|
|
10
|
+
|
|
11
|
+
export const sortedImports: Rule.RuleModule = {
|
|
12
|
+
meta: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Enforce sorted imports alphabetically',
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
fixable: 'code',
|
|
18
|
+
messages: {
|
|
19
|
+
sortedImports: 'Imports should be sorted alphabetically',
|
|
20
|
+
sortedNames: 'Named imports should be sorted alphabetically',
|
|
21
|
+
wrongGroup: 'Import is in wrong group',
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
type: 'suggestion',
|
|
25
|
+
},
|
|
26
|
+
create(context) {
|
|
27
|
+
return {
|
|
28
|
+
Program(node) {
|
|
29
|
+
const body = node.body as TSESTree.ProgramStatement[]
|
|
30
|
+
const declarations = getImportDeclarations(body)
|
|
31
|
+
if (declarations.length === 0)
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
const categorized = categorizeImports(declarations)
|
|
35
|
+
const errors: ImportError[] = [
|
|
36
|
+
...checkGroupOrdering(categorized),
|
|
37
|
+
...checkAlphabeticalSorting(categorized),
|
|
38
|
+
...checkSpecifiersSorting(categorized),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
for (const error of errors) {
|
|
42
|
+
context.report({
|
|
43
|
+
node: error.node,
|
|
44
|
+
messageId: error.messageId,
|
|
45
|
+
fix(fixer) {
|
|
46
|
+
const sourceCode = context.sourceCode
|
|
47
|
+
return createFix(fixer, declarations, sourceCode, body)
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {getSpecifierName} from './getSpecifierName'
|
|
2
|
+
import type {ImportSpecifier} from 'estree'
|
|
3
|
+
|
|
4
|
+
export function sortSpecifiersText(
|
|
5
|
+
specifiers: ImportSpecifier[],
|
|
6
|
+
sourceCode: {getText: (node: ImportSpecifier) => string},
|
|
7
|
+
): string {
|
|
8
|
+
const sorted = [...specifiers].sort((a, b) => {
|
|
9
|
+
const lowerA = getSpecifierName(a).toLowerCase()
|
|
10
|
+
const lowerB = getSpecifierName(b).toLowerCase()
|
|
11
|
+
return lowerA.localeCompare(lowerB)
|
|
12
|
+
})
|
|
13
|
+
return sorted.map(s => sourceCode.getText(s)).join(', ')
|
|
14
|
+
}
|