Skip to content

Commit ee85250

Browse files
authored
feat: Sort object properties (#7)
* Reorder functions * Simpify * Simplify lastUnsortedNode * Sort properties * Start on object property tests * Fix case insensitivity * Fix sorting spread elements * Comment tests * Get sort value of template literals * Add nested property spec * Fix test * Update docs * Add rule
1 parent 98ce69c commit ee85250

10 files changed

+427
-44
lines changed

.eslintrc

+9-1
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,13 @@
2424
}
2525
],
2626
"no-useless-constructor": "off"
27-
}
27+
},
28+
"overrides": [
29+
{
30+
"files": "**/__tests__/**",
31+
"rules": {
32+
"@typescript-eslint/no-var-requires": "off"
33+
}
34+
}
35+
]
2836
}

README.md

+22-2
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,29 @@ After installing, add `sort` to your list of ESLint plugins and extend the recom
3232

3333
While the recommended configuration is the simplest way to use this plugin, you can also configure the rules manually based on your needs.
3434

35+
### `sort/object-properties` 🔧
36+
37+
Sorts object properties alphabetically and case insensitive in ascending order.
38+
39+
Examples of **incorrect** code for this rule.
40+
41+
```js
42+
var a = { b: 1, c: 2, a: 3 }
43+
var a = { C: 1, b: 2 }
44+
var a = { C: 1, b: { y: 1, x: 2 } }
45+
```
46+
47+
Examples of **correct** code for this rule.
48+
49+
```js
50+
var a = { a: 1, b: 2, c: 3 }
51+
var a = { b: 1, C: 2 }
52+
var a = { b: { x: 1, y: 2 }, C: 1 }
53+
```
54+
3555
### `sort/destructured-properties` 🔧
3656

37-
Sorts properties in object destructuring patterns.
57+
Sorts properties in object destructuring patterns alphabetically and case insensitive in ascending order.
3858

3959
Examples of **incorrect** code for this rule.
4060

@@ -54,7 +74,7 @@ let { a: b, b: a } = {}
5474

5575
### `sort/imported-variables` 🔧
5676

57-
Sorts imported variable names alphabetically and case insensitive.
77+
Sorts imported variable names alphabetically and case insensitive in ascending order.
5878

5979
Examples of **incorrect** code for this rule.
6080

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export const input = `
2+
var a = {
3+
// d
4+
d: 2,
5+
// c
6+
c: 1,
7+
...spread1,
8+
e: 3,
9+
// spread 2
10+
...spread2,
11+
b: 5,
12+
a: 4
13+
}
14+
`
15+
16+
export const output = `
17+
var a = {
18+
// c
19+
c: 1,
20+
// d
21+
d: 2,
22+
...spread1,
23+
e: 3,
24+
// spread 2
25+
...spread2,
26+
a: 4,
27+
b: 5
28+
}
29+
`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const input = `
2+
var a = {
3+
// c
4+
c: 1,
5+
// d
6+
d: 2,
7+
...spread1,
8+
// spread 2
9+
...spread2,
10+
a: 3,
11+
b: 4
12+
}
13+
`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../rules/sort-object-properties"
3+
import { invalidFixture, validFixture } from "./utils"
4+
5+
const messages = rule.meta!.messages! as Record<
6+
"unsorted" | "unsortedProperties",
7+
string
8+
>
9+
10+
const valid = (input: string) => ({ code: `var a = ${input}` })
11+
12+
const invalid = (
13+
input: string,
14+
output: string,
15+
...errors: string[]
16+
): RuleTester.InvalidTestCase => ({
17+
code: `var a = ${input}`,
18+
errors,
19+
output: `var a = ${output}`,
20+
})
21+
22+
const error = (a: string, b: string) =>
23+
messages.unsorted.replace("{{a}}", a).replace("{{b}}", b)
24+
25+
const ruleTester = new RuleTester({
26+
parserOptions: {
27+
ecmaVersion: 2018,
28+
},
29+
})
30+
31+
ruleTester.run("sort/destructured-properties", rule, {
32+
valid: [
33+
// Basic
34+
valid("{}"),
35+
valid("{a: 1}"),
36+
valid("{a: 1, b: 2, c: 3}"),
37+
valid("{_:1, a:2, b:3}"),
38+
valid("{1:'a', 2:'b'}"),
39+
40+
// Case insensitive
41+
valid("{a:1, B:2, c:3, D:4}"),
42+
valid("{_:1, A:2, b:3}"),
43+
44+
// Bracket notation
45+
valid("{['a']: 1, ['b']: 2}"),
46+
valid("{[1]: 'a', [2]: 'b'}"),
47+
48+
// Template literals
49+
valid("{[`a`]: 1, [`b`]: 2}"),
50+
valid("{[`a${b}c${d}`]: 1, [`a${c}e${g}`]: 2}"),
51+
valid("{[`${a}b${c}d`]: 1, [`${a}c${e}g`]: 2}"),
52+
53+
// Spread elements
54+
valid("{a:1, b:2, ...c, d:3, e:4}"),
55+
valid("{d:1, e:2, ...c, a:3, b:4}"),
56+
valid("{e:1, f:2, ...d, ...c, a:3, b:4}"),
57+
valid("{f:1, g:2, ...d, a:3, ...c, b:4, c:5}"),
58+
59+
// Nested properties
60+
valid("{a:1, b:{x:2, y:3}, c:4}"),
61+
62+
// Comments
63+
validFixture("object-properties/valid-comments"),
64+
],
65+
invalid: [
66+
invalid(
67+
"{b:2, a:1}",
68+
"{a:1, b:2}",
69+
messages.unsortedProperties,
70+
error("a", "b")
71+
),
72+
invalid(
73+
"{b:3, a:2, _:1}",
74+
"{_:1, a:2, b:3}",
75+
messages.unsortedProperties,
76+
error("a", "b"),
77+
error("_", "a")
78+
),
79+
invalid(
80+
"{2:'b', 1:'a'}",
81+
"{1:'a', 2:'b'}",
82+
messages.unsortedProperties,
83+
error("1", "2")
84+
),
85+
86+
// Case insensitive
87+
invalid(
88+
"{D:4, B:2, a:1, c:3}",
89+
"{a:1, B:2, c:3, D:4}",
90+
messages.unsortedProperties,
91+
error("B", "D"),
92+
error("a", "B")
93+
),
94+
invalid(
95+
"{b:3, A:2, _:1}",
96+
"{_:1, A:2, b:3}",
97+
messages.unsortedProperties,
98+
error("A", "b"),
99+
error("_", "A")
100+
),
101+
102+
// Spread elements
103+
invalid(
104+
"{e:2, d:1, ...c, b:4, a:3}",
105+
"{d:1, e:2, ...c, a:3, b:4}",
106+
messages.unsortedProperties,
107+
error("d", "e"),
108+
error("a", "b")
109+
),
110+
invalid(
111+
"{b:2, a:1, ...c, e:4, d:3}",
112+
"{a:1, b:2, ...c, d:3, e:4}",
113+
messages.unsortedProperties,
114+
error("a", "b"),
115+
error("d", "e")
116+
),
117+
invalid(
118+
"{f:2, e:1, ...d, ...c, b:4, a:3}",
119+
"{e:1, f:2, ...d, ...c, a:3, b:4}",
120+
messages.unsortedProperties,
121+
error("e", "f"),
122+
error("a", "b")
123+
),
124+
invalid(
125+
"{g:2, f:1, ...d, a:3, ...c, c:5, b:4}",
126+
"{f:1, g:2, ...d, a:3, ...c, b:4, c:5}",
127+
messages.unsortedProperties,
128+
error("f", "g"),
129+
error("b", "c")
130+
),
131+
132+
// Bracket notation
133+
invalid(
134+
"{['b']: 2, ['a']: 1}",
135+
"{['a']: 1, ['b']: 2}",
136+
messages.unsortedProperties,
137+
error("a", "b")
138+
),
139+
invalid(
140+
"{[2]: 'b', [1]: 'a'}",
141+
"{[1]: 'a', [2]: 'b'}",
142+
messages.unsortedProperties,
143+
error("1", "2")
144+
),
145+
146+
// Template literals
147+
invalid(
148+
"{[`b`]: 2, [`a`]: 1}",
149+
"{[`a`]: 1, [`b`]: 2}",
150+
messages.unsortedProperties,
151+
error("a", "b")
152+
),
153+
invalid(
154+
"{[`a${c}e${g}`]: 2, [`a${b}c${d}`]: 1}",
155+
"{[`a${b}c${d}`]: 1, [`a${c}e${g}`]: 2}",
156+
messages.unsortedProperties,
157+
error("abcd", "aceg")
158+
),
159+
invalid(
160+
"{[`${a}c${e}g`]: 2, [`${a}b${c}d`]: 1}",
161+
"{[`${a}b${c}d`]: 1, [`${a}c${e}g`]: 2}",
162+
messages.unsortedProperties,
163+
error("abcd", "aceg")
164+
),
165+
166+
// Nested properties
167+
invalid(
168+
"{c:4, b:{y:3, x:2}, a:1}",
169+
// Because RuleTester runs autofixing only once, nested properties don't
170+
// appear to be fixed even though they will be fixed in real world usage.
171+
"{a:1, b:{y:3, x:2}, c:4}",
172+
messages.unsortedProperties,
173+
error("b", "c"),
174+
messages.unsortedProperties,
175+
error("x", "y"),
176+
error("a", "b")
177+
),
178+
179+
// Comments
180+
invalidFixture(
181+
"object-properties/invalid-comments",
182+
messages.unsortedProperties,
183+
error("c", "d"),
184+
error("a", "b")
185+
),
186+
],
187+
})

src/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// import sortImports from "./rules/sort-imports"
22
import sortImportSpecifiers from "./rules/sort-import-specifiers"
33
import sortObjectPatterns from "./rules/sort-object-patterns"
4-
// import sortObjectProperties from "./rules/sort-object-properties"
4+
import sortObjectProperties from "./rules/sort-object-properties"
55

66
module.exports = {
77
configs: {
@@ -11,14 +11,14 @@ module.exports = {
1111
"sort/destructured-properties": "warn",
1212
"sort/imported-variables": "warn",
1313
// "sort/imports": "warn",
14-
// "sort/object-properties": "warn",
14+
"sort/object-properties": "warn",
1515
},
1616
},
1717
},
1818
rules: {
1919
"destructured-properties": sortObjectPatterns,
2020
"imported-variables": sortImportSpecifiers,
2121
// "imports": sortImports,
22-
// "object-properties": sortObjectProperties,
22+
"object-properties": sortObjectProperties,
2323
},
2424
}

src/rules/sort-import-specifiers.ts

+8-11
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@ type Specifier =
1717
| ImportDefaultSpecifier
1818
| ImportNamespaceSpecifier
1919

20-
function getNodeText(node: ImportSpecifier) {
21-
return node.imported.name
22-
}
23-
2420
const isImportSpecifier = (node: Specifier): node is ImportSpecifier =>
2521
node.type === "ImportSpecifier"
2622

27-
const getNodeSortValue = (node: Specifier) =>
23+
const getNodeText = (node: ImportSpecifier) => node.imported.name
24+
25+
const sortFn = (node: Specifier) =>
2826
isImportSpecifier(node) ? getNodeText(node).toLowerCase() : -Infinity
2927

3028
function autofix(context: Rule.RuleContext, node: ImportDeclaration) {
@@ -36,7 +34,7 @@ function autofix(context: Rule.RuleContext, node: ImportDeclaration) {
3634
fix(fixer) {
3735
const text = node.specifiers
3836
.slice()
39-
.sort(getSorter(getNodeSortValue))
37+
.sort(getSorter(sortFn))
4038
.reduce((acc, currentNode, index) => {
4139
return (
4240
acc +
@@ -59,16 +57,15 @@ function autofix(context: Rule.RuleContext, node: ImportDeclaration) {
5957

6058
function sort(node: ImportDeclaration, context: Rule.RuleContext) {
6159
const specifiers = node.specifiers.filter(isImportSpecifier)
60+
let unsorted = false
6261

6362
// If there are less than two specifiers, there is nothing to sort.
6463
if (specifiers.length < 2) {
6564
return
6665
}
6766

68-
let lastUnsortedNode: ImportSpecifier | null = null
69-
7067
specifiers.reduce((previousNode, currentNode) => {
71-
if (getNodeSortValue(currentNode) < getNodeSortValue(previousNode)) {
68+
if (sortFn(currentNode) < sortFn(previousNode)) {
7269
context.report({
7370
node: currentNode,
7471
messageId: "unsorted",
@@ -78,7 +75,7 @@ function sort(node: ImportDeclaration, context: Rule.RuleContext) {
7875
},
7976
})
8077

81-
lastUnsortedNode = currentNode
78+
unsorted = true
8279
}
8380

8481
return currentNode
@@ -87,7 +84,7 @@ function sort(node: ImportDeclaration, context: Rule.RuleContext) {
8784
// If we fixed each set of unsorted nodes, it would require multiple runs to
8885
// fix if there are multiple unsorted nodes. Instead, we add a add special
8986
// error with an autofix rule which will sort all specifiers at once.
90-
if (lastUnsortedNode) {
87+
if (unsorted) {
9188
autofix(context, node)
9289
}
9390
}

0 commit comments

Comments
 (0)