Skip to content

Commit

Permalink
Merge pull request #19 from mskelton/sort-type-properties
Browse files Browse the repository at this point in the history
Sort type properties
  • Loading branch information
mskelton authored Jan 9, 2022
2 parents f7a24d0 + fbfb39e commit 0af61b0
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 14 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ recommended configuration. This will enable all available rules as warnings.
✔: Enabled in the `recommended` configuration.\
🔧: Fixable with [`eslint --fix`](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).

|| 🔧 | Rule | Description |
| :-: | :-: | ----------------------------------------------------------------------- | -------------------------------- |
|| 🔧 | [sort/destructuring-properties](docs/rules/destructuring-properties.md) | Destructuring Properties Sorting |
|| 🔧 | [sort/import-members](docs/rules/import-members.md) | Import Member Sorting |
|| 🔧 | [sort/imports](docs/rules/imports.md) | Import Sorting |
|| 🔧 | [sort/object-properties](docs/rules/object-properties.md) | Object Property Sorting |
|| 🔧 | Rule | Description |
| :-: | :-: | ----------------------------------------------------------------------- | ------------------------------------- |
|| 🔧 | [sort/destructuring-properties](docs/rules/destructuring-properties.md) | Sorts object destructuring properties |
|| 🔧 | [sort/import-members](docs/rules/import-members.md) | Sorts import members |
|| 🔧 | [sort/imports](docs/rules/imports.md) | Sorts imports |
|| 🔧 | [sort/object-properties](docs/rules/object-properties.md) | Sorts object properties |
| | 🔧 | [sort/type-properties](docs/rules/type-properties.md) | Sorts TypeScript type properties |
51 changes: 51 additions & 0 deletions docs/rules/type-properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# TypeScript Type Property Sorting (sort/type-properties)

🔧 The `--fix` option on the command line can automatically fix the problems
reported by this rule.

Sorts TypeScript type properties alphabetically and case insensitive in
ascending order.

## Rule Details

Examples of **incorrect** code for this rule:

```ts
interface A {
B: number
c: string
a: boolean
}

type A = {
b: number
a: {
y: string
x: boolean
}
}
```
Examples of **correct** code for this rule:
```ts
interface A {
a: boolean
B: number
c: string
}

type A = {
a: {
x: boolean
y: string
}
b: number
}
```
## When Not To Use It
This rule is a formatting preference and not following it won't negatively
affect the quality of your code. If alphabetizing type properties isn't a part
of your coding standards, then you can leave this rule off.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
"peerDependencies": {
"eslint": ">=6"
},
"dependencies": {
"@typescript-eslint/experimental-utils": "^5.9.0"
},
"devDependencies": {
"@babel/cli": "^7.16.7",
"@babel/core": "^7.16.7",
Expand Down
124 changes: 124 additions & 0 deletions src/__tests__/type-properties.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { ESLintUtils } from "@typescript-eslint/experimental-utils"
import rule from "../rules/type-properties"

const ruleTester = new ESLintUtils.RuleTester({
parser: "@typescript-eslint/parser",
})

ruleTester.run("sort/type-properties", rule, {
valid: [
"interface A {a: string, b: number}",
"interface A {a: string}",
"interface A {}",
"type A = {_:string, a:string, b:string}",

// Case insensitive
"type A = {a:string, B:string, c:string, D:string}",
"type A = {_:string, A:string, b:string}",

// Weights
`
interface A {
new(f: string): void
(e: string): void
b: boolean
c: boolean
d(): void
[a: string]: unknown
}
`,

// Comments
`
interface A {
// a
a: string
// b
b: number
}
`.trim(),
],
invalid: [
{
code: "interface A {c:string, a:number, b:boolean}",
output: "interface A {a:number, b:boolean, c:string}",
errors: [{ messageId: "unsorted" }],
},
{
code: "interface A {b:boolean, a:string, _:number}",
output: "interface A {_:number, a:string, b:boolean}",
errors: [{ messageId: "unsorted" }],
},

// Case insensitive
{
code: "type A = {b: symbol; A: number; _: string}",
output: "type A = {_: string; A: number; b: symbol}",
errors: [{ messageId: "unsorted" }],
},
{
code: "type A = {D:number, a:boolean, c:string, B:string}",
output: "type A = {a:boolean, B:string, c:string, D:number}",
errors: [{ messageId: "unsorted" }],
},

// All properties are sorted with a single sort
{
code: "interface A {z:string,y:number,x:boolean,w:symbol,v:string}",
output: "interface A {v:string,w:symbol,x:boolean,y:number,z:string}",
errors: [{ messageId: "unsorted" }],
},

// Weights
{
code: `
interface A {
b: boolean
(e: string): void
[a: string]: unknown
[b: string]: boolean
d(): void
c: boolean
[\`c\${o}g\`]: boolean
new(f: string): void
}
`,
output: `
interface A {
new(f: string): void
(e: string): void
b: boolean
c: boolean
[\`c\${o}g\`]: boolean
d(): void
[a: string]: unknown
[b: string]: boolean
}
`,
errors: [{ messageId: "unsorted" }],
},

// Comments
{
code: `
interface A {
// c
c: boolean
// b
b: number
a: string
}
`.trim(),
output: `
interface A {
a: string
// b
b: number
// c
c: boolean
}
`.trim(),
errors: [{ messageId: "unsorted" }],
},
],
})
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import imports from "./rules/imports"
import importMembers from "./rules/import-members"
import destructuringProperties from "./rules/destructuring-properties"
import objectProperties from "./rules/object-properties"
import typeProperties from "./rules/type-properties"

module.exports = {
configs: {
Expand Down Expand Up @@ -30,5 +31,6 @@ module.exports = {
"import-members": importMembers,
"imports": imports,
"object-properties": objectProperties,
"type-properties": typeProperties,
},
}
103 changes: 103 additions & 0 deletions src/rules/type-properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
AST_NODE_TYPES,
ESLintUtils,
TSESTree,
} from "@typescript-eslint/experimental-utils"
import { getName, getNodeRange, getNodeText } from "../ts-utils"
import { docsURL, enumerate, isUnsorted } from "../utils"

/**
* Returns the node's sort weight. The sort weight is used to separate types
* of nodes into groups and then sort in each individual group.
*/
function getWeight(node: TSESTree.TypeElement) {
const weights = {
[AST_NODE_TYPES.TSConstructSignatureDeclaration]: 0,
[AST_NODE_TYPES.TSCallSignatureDeclaration]: 1,
[AST_NODE_TYPES.TSPropertySignature]: 2,
[AST_NODE_TYPES.TSMethodSignature]: 2,
[AST_NODE_TYPES.TSIndexSignature]: 3,
}

return weights[node.type]
}

function getSortValue(node: TSESTree.TypeElement) {
switch (node.type) {
case AST_NODE_TYPES.TSPropertySignature:
case AST_NODE_TYPES.TSMethodSignature:
return getName(node.key)

case AST_NODE_TYPES.TSIndexSignature:
return getName(node.parameters[0])
}

return ""
}

export default ESLintUtils.RuleCreator.withoutDocs({
create(context) {
const source = context.getSourceCode()

function getRangeWithoutDelimiter(node: TSESTree.Node): TSESTree.Range {
const range = getNodeRange(source, node)

return source.getLastToken(node)?.type === "Punctuator"
? [range[0], range[1] - 1]
: range
}

function sort(nodes: TSESTree.TypeElement[]) {
// If there are one or fewer properties, there is nothing to sort
if (nodes.length < 2) {
return
}

const sorted = nodes.slice().sort(
(a, b) =>
// First sort by weight
getWeight(a) - getWeight(b) ||
// Then sort by name
getSortValue(a).localeCompare(getSortValue(b))
)

if (isUnsorted(nodes, sorted)) {
context.report({
node: nodes[0],
messageId: "unsorted",
*fix(fixer) {
for (const [node, complement] of enumerate(nodes, sorted)) {
yield fixer.replaceTextRange(
getRangeWithoutDelimiter(node),
getNodeText(source, complement).replace(/[;,]$/, "")
)
}
},
})
}
}

return {
TSInterfaceBody(node) {
sort(node.body)
},
TSTypeLiteral(node) {
sort(node.members)
},
}
},
meta: {
fixable: "code",
docs: {
recommended: false,
url: docsURL("type-properties"),
description: `Sorts TypeScript type properties alphabetically and case insensitive in ascending order.`,
},
messages: {
unsorted: "Type properties should be sorted alphabetically.",
},
type: "suggestion",
schema: [],
},
defaultOptions: [],
})
42 changes: 42 additions & 0 deletions src/ts-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
AST_NODE_TYPES,
TSESLint,
TSESTree,
} from "@typescript-eslint/experimental-utils"
import { getTextRange } from "./utils"

/**
* Get's the string name of a node used for sorting or errors.
*/
export function getName(node?: TSESTree.Node): string {
switch (node?.type) {
case AST_NODE_TYPES.Identifier:
return node.name

case AST_NODE_TYPES.Literal:
return node.value!.toString()

// `a${b}c${d}` becomes `abcd`
case AST_NODE_TYPES.TemplateLiteral:
return node.quasis.reduce(
(acc, quasi, i) => acc + quasi.value.raw + getName(node.expressions[i]),
""
)
}

return ""
}

/**
* Returns an AST range for a node and it's preceding comments.
*/
export function getNodeRange(source: TSESLint.SourceCode, node: TSESTree.Node) {
return getTextRange(source.getCommentsBefore(node)[0] ?? node, node)
}

/**
* Returns a node's text with it's preceding comments.
*/
export function getNodeText(source: TSESLint.SourceCode, node: TSESTree.Node) {
return source.getText().slice(...getNodeRange(source, node))
}
Loading

0 comments on commit 0af61b0

Please sign in to comment.