Skip to content

Commit

Permalink
feat: Add support for sorting string enums
Browse files Browse the repository at this point in the history
Fixes #50
  • Loading branch information
mskelton committed Apr 16, 2023
1 parent 8fb2928 commit 0d0a131
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ recommended configuration. This will enable all available rules as warnings.
|| 🔧 | [sort/import-members](docs/rules/import-members.md) | Sorts import members |
|| 🔧 | [sort/object-properties](docs/rules/object-properties.md) | Sorts object properties |
| | 🔧 | [sort/type-properties](docs/rules/type-properties.md) | Sorts TypeScript type properties |
| | 🔧 | [sort/string-enums](docs/rules/string-enums.md) | Sorts TypeScript string enums |
| | 🔧 | [sort/string-unions](docs/rules/string-unions.md) | Sorts TypeScript string unions |
56 changes: 56 additions & 0 deletions docs/rules/string-enums.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# TypeScript String Enum Sorting (sort/string-enums)

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

Sorts TypeScript string enums alphabetically and case insensitive in ascending
order.

## Rule Details

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

```typescript
enum Fruit {
Orange = "orange",
Apple = "apple",
Grape = "grape",
}
```

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

```typescript
enum Fruit {
Apple = "apple",
Grape = "grape",
Orange = "orange",
}
```

## Options

This rule has an options object with the following defaults.

```json
{
"sort/string-enums": ["error", { "caseSensitive": false, "natural": true }]
}
```

### `caseSensitive`

If `true`, enforce exports to be in case-sensitive order.

### `natural`

If `true`, enforce imports to be in natural order. Natural order compares
strings containing combination of letters and numbers in the way a human being
would sort. For example, `A10` would come after `A3` when using natural
ordering.

## 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 string enums isn't a part of
your coding standards, then you can leave this rule off.
89 changes: 89 additions & 0 deletions src/__tests__/string-enums.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { TSESLint } from "@typescript-eslint/experimental-utils"
import rule from "../rules/string-enums.js"
import { createTsRuleTester } from "../test-utils.js"

const ruleTester = createTsRuleTester()

const createValidCodeVariants = (
code: string
): TSESLint.RunTests<
"unsorted",
[{ caseSensitive?: boolean; natural?: boolean }]
>["valid"] => [
{ code, options: [{ caseSensitive: false, natural: false }] },
{ code, options: [{ caseSensitive: true, natural: false }] },
{ code, options: [{ caseSensitive: false, natural: true }] },
{ code, options: [{ caseSensitive: true, natural: true }] },
]

ruleTester.run("sort/string-enums", rule, {
valid: [
...createValidCodeVariants("enum Foo {A='a'}"),
...createValidCodeVariants("enum Foo {a='a', b='b'}"),
...createValidCodeVariants("enum Foo {_='_', a='a', b='b'}"),

// Ignores mixed types
...createValidCodeVariants("enum Foo {b='b', a='a', c=1}"),

// Options
{
code: "enum Foo {a1='a1', A1='A1', a12='a12', a2='a2', B2='B2'}",
options: [{ caseSensitive: false, natural: false }],
},
{
code: "enum Foo {A1='A1', B1='B1', a1='a1', a12='a12', a2='a2'}",
options: [{ caseSensitive: true, natural: false }],
},
{
code: "enum Foo {a1='a1', A1='A1', a2='a2', a12='a12', B2='B2'}",
options: [{ caseSensitive: false, natural: true }],
},
{
code: "enum Foo {A1='A1', B2='B2', a1='a1', a2='a2', a12='a12'}",
options: [{ caseSensitive: true, natural: true }],
},
],
invalid: [
{
code: "enum Foo {b='b', a='a'}",
output: "enum Foo {a='a', b='b'}",
errors: [{ messageId: "unsorted" }],
},
{
code: "enum Foo {b='b', a='a', c='c'}",
output: "enum Foo {a='a', b='b', c='c'}",
errors: [{ messageId: "unsorted" }],
},
{
code: "enum Foo {b='b', _='_', c='c'}",
output: "enum Foo {_='_', b='b', c='c'}",
errors: [{ messageId: "unsorted" }],
},

// Options
{
code: "enum Foo {a12='a12', B2='B2', a1='a1', a2='a2'}",
output: "enum Foo {a1='a1', a12='a12', a2='a2', B2='B2'}",
options: [{ caseSensitive: false, natural: false }],
errors: [{ messageId: "unsorted" }],
},
{
code: "enum Foo {a1='a1', B2='B2', a2='a2', a12='a12'}",
output: "enum Foo {B2='B2', a1='a1', a12='a12', a2='a2'}",
options: [{ caseSensitive: true, natural: false }],
errors: [{ messageId: "unsorted" }],
},
{
code: "enum Foo {a2='a2', a1='a1', a12='a12', B2='B2'}",
output: "enum Foo {a1='a1', a2='a2', a12='a12', B2='B2'}",
options: [{ caseSensitive: false, natural: true }],
errors: [{ messageId: "unsorted" }],
},
{
code: "enum Foo {a12='a12', a2='a2', B2='B2', a1='a1'}",
output: "enum Foo {B2='B2', a1='a1', a2='a2', a12='a12'}",
options: [{ caseSensitive: true, natural: true }],
errors: [{ messageId: "unsorted" }],
},
],
})
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import sortImportMembers from "./rules/import-members.js"
import sortObjectProperties from "./rules/object-properties.js"
import sortTypeProperties from "./rules/type-properties.js"
import sortStringUnions from "./rules/string-unions.js"
import sortStringEnums from "./rules/string-enums.js"

const config = {
configs: {
Expand Down Expand Up @@ -51,6 +52,7 @@ const config = {
"object-properties": sortObjectProperties,
"type-properties": sortTypeProperties,
"string-unions": sortStringUnions,
"string-enums": sortStringEnums,
},
}

Expand Down
84 changes: 84 additions & 0 deletions src/rules/string-enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
ESLintUtils,
TSESLint,
TSESTree,
} from "@typescript-eslint/experimental-utils"
import { getNodeText } from "../ts-utils.js"
import { docsURL, enumerate, getSorter, isUnsorted } from "../utils.js"

function getSortValue(node: TSESTree.TSEnumMember) {
return node.initializer?.type === TSESTree.AST_NODE_TYPES.Literal &&
typeof node.initializer.value === "string"
? node.initializer.value
: null
}

export default ESLintUtils.RuleCreator.withoutDocs<
[{ caseSensitive?: boolean; natural?: boolean }],
"unsorted"
>({
create(context) {
const source = context.getSourceCode()
const options = context.options[0]
const sorter = getSorter(options)

return {
TSEnumDeclaration(node) {
const nodes = node.members

// If there are one or fewer properties, there is nothing to sort
if (nodes.length < 2) return

// Ignore mixed type enums
if (nodes.map(getSortValue).some((value) => value === null)) return

const sorted = nodes
.slice()
.sort((a, b) => sorter(getSortValue(a) ?? "", getSortValue(b) ?? ""))

const firstUnsortedNode = isUnsorted(nodes, sorted)
if (firstUnsortedNode) {
context.report({
node: firstUnsortedNode,
messageId: "unsorted",
*fix(fixer) {
for (const [node, complement] of enumerate(nodes, sorted)) {
yield fixer.replaceText(node, getNodeText(source, complement))
}
},
})
}
},
}
},
meta: {
docs: {
recommended: false,
url: docsURL("string-enums"),
description: `Sorts TypeScript string enums alphabetically and case insensitive in ascending order.`,
},
fixable: "code",
messages: {
unsorted: "String enums should be sorted alphabetically.",
},
schema: [
{
additionalProperties: false,
default: { caseSensitive: false, natural: true },
properties: {
caseSensitive: {
type: "boolean",
default: false,
},
natural: {
type: "boolean",
default: true,
},
},
type: "object",
},
],
type: "suggestion",
},
defaultOptions: [{}],
}) as TSESLint.RuleModule<string, unknown[]>

0 comments on commit 0d0a131

Please sign in to comment.