Skip to content

Commit 30da61f

Browse files
authored
feat: Sort import specifiers (#4)
* Improve test * Rename files * Add spec and file * Start import specifier rule * default import * Comment tests
1 parent 3df8a47 commit 30da61f

10 files changed

+273
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export const input = `
2+
import {
3+
c,
4+
// b
5+
b,
6+
// a
7+
a
8+
} from 'a'
9+
`
10+
11+
export const output = `
12+
import {
13+
// a
14+
a,
15+
// b
16+
b,
17+
c
18+
} from 'a'
19+
`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const input = `
2+
import {
3+
// a
4+
a,
5+
// b
6+
b,
7+
c
8+
} from 'a'
9+
`

src/__fixtures__/destructured-properties/invalid-comments.ts renamed to src/__fixtures__/object-patterns/invalid-comments.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
export const input = `
22
let {
3+
// c
4+
c,
35
// b
46
b,
5-
c,
6-
// a
77
a,
88
// rest
99
...rest
@@ -12,10 +12,10 @@ let {
1212

1313
export const output = `
1414
let {
15-
// a
1615
a,
1716
// b
1817
b,
18+
// c
1919
c,
2020
// rest
2121
...rest
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../rules/sort-import-specifiers"
3+
import { invalidFixture, validFixture } from "./utils"
4+
5+
const messages = rule.meta!.messages! as Record<
6+
"unsorted" | "unsortedSpecifiers",
7+
string
8+
>
9+
10+
const valid = (input: string) => ({ code: `import ${input} from 'a'` })
11+
12+
const invalid = (input: string, output: string, ...errors: string[]) => ({
13+
code: `import ${input} from 'a'`,
14+
errors,
15+
output: `import ${output} from 'a'`,
16+
})
17+
18+
const error = (a: string, b: string) =>
19+
messages.unsorted.replace("{{a}}", a).replace("{{b}}", b)
20+
21+
const ruleTester = new RuleTester({
22+
parserOptions: {
23+
ecmaVersion: 2018,
24+
sourceType: "module",
25+
},
26+
})
27+
28+
ruleTester.run("sort/imported-variables", rule, {
29+
valid: [
30+
// Basic
31+
valid("{}"),
32+
valid("{a}"),
33+
valid("{a, b, c}"),
34+
valid("{_, a, b}"),
35+
valid("{p,q,r,s,t,u,v,w,x,y,z}"),
36+
37+
// Case insensitive
38+
valid("{a, B, c, D}"),
39+
valid("{_, A, b}"),
40+
41+
// Default and namespace imports
42+
valid("React"),
43+
valid("* as React"),
44+
valid("React, {a, b}"),
45+
46+
//
47+
valid("{a as b, b as a}"),
48+
49+
// Comments
50+
validFixture("import-specifiers/valid-comments"),
51+
],
52+
invalid: [
53+
// Basic
54+
invalid(
55+
"{c, a, b}",
56+
"{a, b, c}",
57+
messages.unsortedSpecifiers,
58+
error("a", "c")
59+
),
60+
invalid(
61+
"{b, a, _}",
62+
"{_, a, b}",
63+
messages.unsortedSpecifiers,
64+
error("a", "b"),
65+
error("_", "a")
66+
),
67+
68+
// Case insensitive
69+
invalid(
70+
"{b, A, _}",
71+
"{_, A, b}",
72+
messages.unsortedSpecifiers,
73+
error("A", "b"),
74+
error("_", "A")
75+
),
76+
invalid(
77+
"{D, a, c, B}",
78+
"{a, B, c, D}",
79+
messages.unsortedSpecifiers,
80+
error("a", "D"),
81+
error("B", "c")
82+
),
83+
84+
// Default and namespace imports
85+
invalid(
86+
"React, {c, a, b}",
87+
"React, {a, b, c}",
88+
messages.unsortedSpecifiers,
89+
error("a", "c")
90+
),
91+
92+
// All properties are sorted with a single sort
93+
invalid(
94+
"{z,y,x,w,v,u,t,s,r,q,p}",
95+
"{p,q,r,s,t,u,v,w,x,y,z}",
96+
messages.unsortedSpecifiers,
97+
error("y", "z"),
98+
error("x", "y"),
99+
error("w", "x"),
100+
error("v", "w"),
101+
error("u", "v"),
102+
error("t", "u"),
103+
error("s", "t"),
104+
error("r", "s"),
105+
error("q", "r"),
106+
error("p", "q")
107+
),
108+
109+
// Comments
110+
invalidFixture(
111+
"import-specifiers/invalid-comments",
112+
messages.unsortedSpecifiers,
113+
error("b", "c"),
114+
error("a", "b")
115+
),
116+
],
117+
})

src/__tests__/sort-destructured-properties.spec.ts renamed to src/__tests__/sort-object-patterns.spec.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { RuleTester } from "eslint"
2-
import rule from "../rules/sort-destructured-properties"
2+
import rule from "../rules/sort-object-patterns"
33
import { invalidFixture, validFixture } from "./utils"
44

55
const messages = rule.meta!.messages! as Record<
@@ -46,7 +46,7 @@ ruleTester.run("sort/destructured-properties", rule, {
4646
valid("{...rest}"),
4747

4848
// Comments
49-
validFixture("destructured-properties/valid-comments"),
49+
validFixture("object-patterns/valid-comments"),
5050
],
5151
invalid: [
5252
// Basic
@@ -107,9 +107,10 @@ ruleTester.run("sort/destructured-properties", rule, {
107107

108108
// Comments
109109
invalidFixture(
110-
"destructured-properties/invalid-comments",
110+
"object-patterns/invalid-comments",
111111
messages.unsortedPattern,
112-
error("a", "c")
112+
error("b", "c"),
113+
error("a", "b")
113114
),
114115
],
115116
})

src/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import sortImports from "./rules/sort-imports"
2+
import sortImportSpecifiers from "./rules/sort-imports"
3+
import sortObjectPatterns from "./rules/sort-object-patterns"
24
import sortObjectProperties from "./rules/sort-object-properties"
3-
import sortObjectPatterns from "./rules/sort-destructured-properties"
45

56
module.exports = {
67
configs: {
78
recommended: {
89
plugins: ["sort"],
910
rules: {
1011
"sort/imports": "warn",
12+
"sort/imported-variables": "warn",
1113
"sort/destructured-properties": "warn",
1214
"sort/object-properties": "warn",
1315
},
1416
},
1517
},
1618
rules: {
1719
"sort/imports": sortImports,
20+
"sort/imported-variables": sortImportSpecifiers,
1821
"sort/destructured-properties": sortObjectPatterns,
1922
"sort/object-properties": sortObjectProperties,
2023
},

src/rules/sort-import-specifiers.ts

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Rule } from "eslint"
2+
import {
3+
ImportDeclaration,
4+
ImportSpecifier,
5+
ImportDefaultSpecifier,
6+
ImportNamespaceSpecifier,
7+
} from "estree"
8+
import {
9+
getSorter,
10+
getTextWithComments,
11+
getNodeGroupRange,
12+
getTextBetweenNodes,
13+
} from "./utils"
14+
15+
type Specifier =
16+
| ImportSpecifier
17+
| ImportDefaultSpecifier
18+
| ImportNamespaceSpecifier
19+
20+
function getNodeText(node: ImportSpecifier) {
21+
return node.imported.name
22+
}
23+
24+
const isImportSpecifier = (node: Specifier): node is ImportSpecifier =>
25+
node.type === "ImportSpecifier"
26+
27+
const getNodeSortValue = (node: Specifier) =>
28+
isImportSpecifier(node) ? getNodeText(node).toLowerCase() : -Infinity
29+
30+
function autofix(context: Rule.RuleContext, node: ImportDeclaration) {
31+
const source = context.getSourceCode()
32+
33+
context.report({
34+
node,
35+
messageId: "unsortedSpecifiers",
36+
fix(fixer) {
37+
const text = node.specifiers
38+
.slice()
39+
.sort(getSorter(getNodeSortValue))
40+
.reduce((acc, currentNode, index) => {
41+
return (
42+
acc +
43+
getTextWithComments(source, currentNode) +
44+
getTextBetweenNodes(
45+
source,
46+
node.specifiers[index],
47+
node.specifiers[index + 1]
48+
)
49+
)
50+
}, "")
51+
52+
return fixer.replaceTextRange(
53+
getNodeGroupRange(source, node.specifiers),
54+
text
55+
)
56+
},
57+
})
58+
}
59+
60+
function sort(node: ImportDeclaration, context: Rule.RuleContext) {
61+
const specifiers = node.specifiers.filter(isImportSpecifier)
62+
63+
// If there are less than two specifiers, there is nothing to sort.
64+
if (specifiers.length < 2) {
65+
return
66+
}
67+
68+
let lastUnsortedNode: ImportSpecifier | null = null
69+
70+
specifiers.reduce((previousNode, currentNode) => {
71+
if (getNodeSortValue(currentNode) < getNodeSortValue(previousNode)) {
72+
context.report({
73+
node: currentNode,
74+
messageId: "unsorted",
75+
data: {
76+
a: getNodeText(currentNode),
77+
b: getNodeText(previousNode),
78+
},
79+
})
80+
81+
lastUnsortedNode = currentNode
82+
}
83+
84+
return currentNode
85+
})
86+
87+
// If we fixed each set of unsorted nodes, it would require multiple runs to
88+
// fix if there are multiple unsorted nodes. Instead, we add a add special
89+
// error with an autofix rule which will sort all specifiers at once.
90+
if (lastUnsortedNode) {
91+
autofix(context, node)
92+
}
93+
}
94+
95+
export default {
96+
create(context) {
97+
return {
98+
ImportDeclaration(node) {
99+
sort(node as ImportDeclaration, context)
100+
},
101+
}
102+
},
103+
meta: {
104+
fixable: "code",
105+
messages: {
106+
unsorted: "Expected '{{a}}' to be before '{{b}}'.",
107+
unsortedSpecifiers: "Expected imported variables to be sorted.",
108+
},
109+
},
110+
} as Rule.RuleModule

src/rules/sort-destructured-properties.ts renamed to src/rules/sort-object-patterns.ts

-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ export default {
109109
messages: {
110110
unsorted: "Expected '{{a}}' to be before '{{b}}'.",
111111
unsortedPattern: "Expected destructured properties to be sorted.",
112-
invalidRest: "Expected rest element to be the last property.",
113112
},
114113
},
115114
} as Rule.RuleModule

src/rules/utils.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { AST, SourceCode } from "eslint"
2-
import { AssignmentProperty, Comment, Node, RestElement } from "estree"
2+
import { Comment, Node } from "estree"
33

4-
type Property = AssignmentProperty | RestElement
5-
6-
export function getSorter(sortFn: (node: Property) => string | number) {
7-
return (a: Property, b: Property) => {
4+
export function getSorter<T extends Node>(
5+
sortFn: (node: T) => string | number
6+
) {
7+
return (a: T, b: T) => {
88
const aText = sortFn(a)
99
const bText = sortFn(b)
1010

1111
if (aText === Infinity) return 1
1212
if (aText === -Infinity) return -1
1313

14-
if (aText < bText) return -1
1514
if (aText > bText) return 1
15+
if (aText < bText) return -1
1616

1717
return 0
1818
}

0 commit comments

Comments
 (0)