Skip to content

Commit 0af61b0

Browse files
authored
Merge pull request #19 from mskelton/sort-type-properties
Sort type properties
2 parents f7a24d0 + fbfb39e commit 0af61b0

File tree

9 files changed

+349
-14
lines changed

9 files changed

+349
-14
lines changed

README.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ recommended configuration. This will enable all available rules as warnings.
3737
✔: Enabled in the `recommended` configuration.\
3838
🔧: Fixable with [`eslint --fix`](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).
3939

40-
|| 🔧 | Rule | Description |
41-
| :-: | :-: | ----------------------------------------------------------------------- | -------------------------------- |
42-
|| 🔧 | [sort/destructuring-properties](docs/rules/destructuring-properties.md) | Destructuring Properties Sorting |
43-
|| 🔧 | [sort/import-members](docs/rules/import-members.md) | Import Member Sorting |
44-
|| 🔧 | [sort/imports](docs/rules/imports.md) | Import Sorting |
45-
|| 🔧 | [sort/object-properties](docs/rules/object-properties.md) | Object Property Sorting |
40+
|| 🔧 | Rule | Description |
41+
| :-: | :-: | ----------------------------------------------------------------------- | ------------------------------------- |
42+
|| 🔧 | [sort/destructuring-properties](docs/rules/destructuring-properties.md) | Sorts object destructuring properties |
43+
|| 🔧 | [sort/import-members](docs/rules/import-members.md) | Sorts import members |
44+
|| 🔧 | [sort/imports](docs/rules/imports.md) | Sorts imports |
45+
|| 🔧 | [sort/object-properties](docs/rules/object-properties.md) | Sorts object properties |
46+
| | 🔧 | [sort/type-properties](docs/rules/type-properties.md) | Sorts TypeScript type properties |

docs/rules/type-properties.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# TypeScript Type Property Sorting (sort/type-properties)
2+
3+
🔧 The `--fix` option on the command line can automatically fix the problems
4+
reported by this rule.
5+
6+
Sorts TypeScript type properties alphabetically and case insensitive in
7+
ascending order.
8+
9+
## Rule Details
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```ts
14+
interface A {
15+
B: number
16+
c: string
17+
a: boolean
18+
}
19+
20+
type A = {
21+
b: number
22+
a: {
23+
y: string
24+
x: boolean
25+
}
26+
}
27+
```
28+
29+
Examples of **correct** code for this rule:
30+
31+
```ts
32+
interface A {
33+
a: boolean
34+
B: number
35+
c: string
36+
}
37+
38+
type A = {
39+
a: {
40+
x: boolean
41+
y: string
42+
}
43+
b: number
44+
}
45+
```
46+
47+
## When Not To Use It
48+
49+
This rule is a formatting preference and not following it won't negatively
50+
affect the quality of your code. If alphabetizing type properties isn't a part
51+
of your coding standards, then you can leave this rule off.

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
"peerDependencies": {
3131
"eslint": ">=6"
3232
},
33+
"dependencies": {
34+
"@typescript-eslint/experimental-utils": "^5.9.0"
35+
},
3336
"devDependencies": {
3437
"@babel/cli": "^7.16.7",
3538
"@babel/core": "^7.16.7",

src/__tests__/type-properties.spec.ts

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { ESLintUtils } from "@typescript-eslint/experimental-utils"
2+
import rule from "../rules/type-properties"
3+
4+
const ruleTester = new ESLintUtils.RuleTester({
5+
parser: "@typescript-eslint/parser",
6+
})
7+
8+
ruleTester.run("sort/type-properties", rule, {
9+
valid: [
10+
"interface A {a: string, b: number}",
11+
"interface A {a: string}",
12+
"interface A {}",
13+
"type A = {_:string, a:string, b:string}",
14+
15+
// Case insensitive
16+
"type A = {a:string, B:string, c:string, D:string}",
17+
"type A = {_:string, A:string, b:string}",
18+
19+
// Weights
20+
`
21+
interface A {
22+
new(f: string): void
23+
(e: string): void
24+
b: boolean
25+
c: boolean
26+
d(): void
27+
[a: string]: unknown
28+
}
29+
`,
30+
31+
// Comments
32+
`
33+
interface A {
34+
// a
35+
a: string
36+
// b
37+
b: number
38+
}
39+
`.trim(),
40+
],
41+
invalid: [
42+
{
43+
code: "interface A {c:string, a:number, b:boolean}",
44+
output: "interface A {a:number, b:boolean, c:string}",
45+
errors: [{ messageId: "unsorted" }],
46+
},
47+
{
48+
code: "interface A {b:boolean, a:string, _:number}",
49+
output: "interface A {_:number, a:string, b:boolean}",
50+
errors: [{ messageId: "unsorted" }],
51+
},
52+
53+
// Case insensitive
54+
{
55+
code: "type A = {b: symbol; A: number; _: string}",
56+
output: "type A = {_: string; A: number; b: symbol}",
57+
errors: [{ messageId: "unsorted" }],
58+
},
59+
{
60+
code: "type A = {D:number, a:boolean, c:string, B:string}",
61+
output: "type A = {a:boolean, B:string, c:string, D:number}",
62+
errors: [{ messageId: "unsorted" }],
63+
},
64+
65+
// All properties are sorted with a single sort
66+
{
67+
code: "interface A {z:string,y:number,x:boolean,w:symbol,v:string}",
68+
output: "interface A {v:string,w:symbol,x:boolean,y:number,z:string}",
69+
errors: [{ messageId: "unsorted" }],
70+
},
71+
72+
// Weights
73+
{
74+
code: `
75+
interface A {
76+
b: boolean
77+
(e: string): void
78+
[a: string]: unknown
79+
[b: string]: boolean
80+
d(): void
81+
c: boolean
82+
[\`c\${o}g\`]: boolean
83+
new(f: string): void
84+
}
85+
`,
86+
output: `
87+
interface A {
88+
new(f: string): void
89+
(e: string): void
90+
b: boolean
91+
c: boolean
92+
[\`c\${o}g\`]: boolean
93+
d(): void
94+
[a: string]: unknown
95+
[b: string]: boolean
96+
}
97+
`,
98+
errors: [{ messageId: "unsorted" }],
99+
},
100+
101+
// Comments
102+
{
103+
code: `
104+
interface A {
105+
// c
106+
c: boolean
107+
// b
108+
b: number
109+
a: string
110+
}
111+
`.trim(),
112+
output: `
113+
interface A {
114+
a: string
115+
// b
116+
b: number
117+
// c
118+
c: boolean
119+
}
120+
`.trim(),
121+
errors: [{ messageId: "unsorted" }],
122+
},
123+
],
124+
})

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import imports from "./rules/imports"
22
import importMembers from "./rules/import-members"
33
import destructuringProperties from "./rules/destructuring-properties"
44
import objectProperties from "./rules/object-properties"
5+
import typeProperties from "./rules/type-properties"
56

67
module.exports = {
78
configs: {
@@ -30,5 +31,6 @@ module.exports = {
3031
"import-members": importMembers,
3132
"imports": imports,
3233
"object-properties": objectProperties,
34+
"type-properties": typeProperties,
3335
},
3436
}

src/rules/type-properties.ts

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {
2+
AST_NODE_TYPES,
3+
ESLintUtils,
4+
TSESTree,
5+
} from "@typescript-eslint/experimental-utils"
6+
import { getName, getNodeRange, getNodeText } from "../ts-utils"
7+
import { docsURL, enumerate, isUnsorted } from "../utils"
8+
9+
/**
10+
* Returns the node's sort weight. The sort weight is used to separate types
11+
* of nodes into groups and then sort in each individual group.
12+
*/
13+
function getWeight(node: TSESTree.TypeElement) {
14+
const weights = {
15+
[AST_NODE_TYPES.TSConstructSignatureDeclaration]: 0,
16+
[AST_NODE_TYPES.TSCallSignatureDeclaration]: 1,
17+
[AST_NODE_TYPES.TSPropertySignature]: 2,
18+
[AST_NODE_TYPES.TSMethodSignature]: 2,
19+
[AST_NODE_TYPES.TSIndexSignature]: 3,
20+
}
21+
22+
return weights[node.type]
23+
}
24+
25+
function getSortValue(node: TSESTree.TypeElement) {
26+
switch (node.type) {
27+
case AST_NODE_TYPES.TSPropertySignature:
28+
case AST_NODE_TYPES.TSMethodSignature:
29+
return getName(node.key)
30+
31+
case AST_NODE_TYPES.TSIndexSignature:
32+
return getName(node.parameters[0])
33+
}
34+
35+
return ""
36+
}
37+
38+
export default ESLintUtils.RuleCreator.withoutDocs({
39+
create(context) {
40+
const source = context.getSourceCode()
41+
42+
function getRangeWithoutDelimiter(node: TSESTree.Node): TSESTree.Range {
43+
const range = getNodeRange(source, node)
44+
45+
return source.getLastToken(node)?.type === "Punctuator"
46+
? [range[0], range[1] - 1]
47+
: range
48+
}
49+
50+
function sort(nodes: TSESTree.TypeElement[]) {
51+
// If there are one or fewer properties, there is nothing to sort
52+
if (nodes.length < 2) {
53+
return
54+
}
55+
56+
const sorted = nodes.slice().sort(
57+
(a, b) =>
58+
// First sort by weight
59+
getWeight(a) - getWeight(b) ||
60+
// Then sort by name
61+
getSortValue(a).localeCompare(getSortValue(b))
62+
)
63+
64+
if (isUnsorted(nodes, sorted)) {
65+
context.report({
66+
node: nodes[0],
67+
messageId: "unsorted",
68+
*fix(fixer) {
69+
for (const [node, complement] of enumerate(nodes, sorted)) {
70+
yield fixer.replaceTextRange(
71+
getRangeWithoutDelimiter(node),
72+
getNodeText(source, complement).replace(/[;,]$/, "")
73+
)
74+
}
75+
},
76+
})
77+
}
78+
}
79+
80+
return {
81+
TSInterfaceBody(node) {
82+
sort(node.body)
83+
},
84+
TSTypeLiteral(node) {
85+
sort(node.members)
86+
},
87+
}
88+
},
89+
meta: {
90+
fixable: "code",
91+
docs: {
92+
recommended: false,
93+
url: docsURL("type-properties"),
94+
description: `Sorts TypeScript type properties alphabetically and case insensitive in ascending order.`,
95+
},
96+
messages: {
97+
unsorted: "Type properties should be sorted alphabetically.",
98+
},
99+
type: "suggestion",
100+
schema: [],
101+
},
102+
defaultOptions: [],
103+
})

src/ts-utils.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
AST_NODE_TYPES,
3+
TSESLint,
4+
TSESTree,
5+
} from "@typescript-eslint/experimental-utils"
6+
import { getTextRange } from "./utils"
7+
8+
/**
9+
* Get's the string name of a node used for sorting or errors.
10+
*/
11+
export function getName(node?: TSESTree.Node): string {
12+
switch (node?.type) {
13+
case AST_NODE_TYPES.Identifier:
14+
return node.name
15+
16+
case AST_NODE_TYPES.Literal:
17+
return node.value!.toString()
18+
19+
// `a${b}c${d}` becomes `abcd`
20+
case AST_NODE_TYPES.TemplateLiteral:
21+
return node.quasis.reduce(
22+
(acc, quasi, i) => acc + quasi.value.raw + getName(node.expressions[i]),
23+
""
24+
)
25+
}
26+
27+
return ""
28+
}
29+
30+
/**
31+
* Returns an AST range for a node and it's preceding comments.
32+
*/
33+
export function getNodeRange(source: TSESLint.SourceCode, node: TSESTree.Node) {
34+
return getTextRange(source.getCommentsBefore(node)[0] ?? node, node)
35+
}
36+
37+
/**
38+
* Returns a node's text with it's preceding comments.
39+
*/
40+
export function getNodeText(source: TSESLint.SourceCode, node: TSESTree.Node) {
41+
return source.getText().slice(...getNodeRange(source, node))
42+
}

0 commit comments

Comments
 (0)