-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #49 from mskelton/string-unions
feat: Add `string-unions` rule to sort string unions
- Loading branch information
Showing
12 changed files
with
234 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# TypeScript String Union Sorting (sort/string-unions) | ||
|
||
🔧 The `--fix` option on the command line can automatically fix the problems | ||
reported by this rule. | ||
|
||
Sorts TypeScript string unions alphabetically and case insensitive in ascending | ||
order. This only applies to union types that are made up of entirely string | ||
keys, so mixed type unions will be ignored. | ||
|
||
## Rule Details | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```typescript | ||
type Fruit = "orange" | "apple" | "grape" | ||
``` | ||
Examples of **correct** code for this rule: | ||
```typescript | ||
type Fruit = "apple" | "grape" | "orange" | ||
``` | ||
## Options | ||
```json | ||
{ | ||
"sort/string-unions": ["error", { "caseSensitive": false, "natural": true }] | ||
} | ||
``` | ||
- `caseSensitive` (default `false`) - if `true`, enforce properties to be in | ||
case-sensitive order. | ||
- `natural` (default `true`) - if `true`, enforce properties to be in natural | ||
order. Natural order compares strings containing combination of letters and | ||
numbers in the way a human being would sort. It basically sorts numerically, | ||
instead of sorting alphabetically. So the number 10 comes after the number 3 | ||
in natural sorting. | ||
## 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 unions isn't a part of | ||
your coding standards, then you can leave this rule off. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { TSESLint } from "@typescript-eslint/experimental-utils" | ||
import rule from "../rules/string-unions.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-unions", rule, { | ||
valid: [ | ||
...createValidCodeVariants("type A = 'a'"), | ||
...createValidCodeVariants("type A = 'a' | 'b'"), | ||
...createValidCodeVariants("type A = '_' | 'a' | 'b'"), | ||
|
||
// Ignores mixed types | ||
...createValidCodeVariants("type A = 'b' | 'a' | boolean"), | ||
...createValidCodeVariants("type A = 'b' | {type:string} | 'a'"), | ||
|
||
// Options | ||
{ | ||
code: "type A = 'a1' | 'A1' | 'a12' | 'a2' | 'B2'", | ||
options: [{ caseSensitive: false, natural: false }], | ||
}, | ||
{ | ||
code: "type A = 'A1' | 'B1' | 'a1' | 'a12' | 'a2'", | ||
options: [{ caseSensitive: true, natural: false }], | ||
}, | ||
{ | ||
code: "type A = 'a1' | 'A1' | 'a2' | 'a12' | 'B2'", | ||
options: [{ caseSensitive: false, natural: true }], | ||
}, | ||
{ | ||
code: "type A = 'A1' | 'B2' | 'a1' | 'a2' | 'a12'", | ||
options: [{ caseSensitive: true, natural: true }], | ||
}, | ||
], | ||
invalid: [ | ||
{ | ||
code: "type A = 'b' | 'a'", | ||
output: "type A = 'a' | 'b'", | ||
errors: [{ messageId: "unsorted" }], | ||
}, | ||
{ | ||
code: "type A = 'b' | 'a' | 'c'", | ||
output: "type A = 'a' | 'b' | 'c'", | ||
errors: [{ messageId: "unsorted" }], | ||
}, | ||
{ | ||
code: "type A = 'b' | '_' | 'c'", | ||
output: "type A = '_' | 'b' | 'c'", | ||
errors: [{ messageId: "unsorted" }], | ||
}, | ||
|
||
// Options | ||
{ | ||
code: "type A = 'a12' | 'B2' | 'a1' | 'a2'", | ||
output: "type A = 'a1' | 'a12' | 'a2' | 'B2'", | ||
options: [{ caseSensitive: false, natural: false }], | ||
errors: [{ messageId: "unsorted" }], | ||
}, | ||
{ | ||
code: "type A = 'a1' | 'B2' | 'a2' | 'a12'", | ||
output: "type A = 'B2' | 'a1' | 'a12' | 'a2'", | ||
options: [{ caseSensitive: true, natural: false }], | ||
errors: [{ messageId: "unsorted" }], | ||
}, | ||
{ | ||
code: "type A = 'a2' | 'a1' | 'a12' | 'B2'", | ||
output: "type A = 'a1' | 'a2' | 'a12' | 'B2'", | ||
options: [{ caseSensitive: false, natural: true }], | ||
errors: [{ messageId: "unsorted" }], | ||
}, | ||
{ | ||
code: "type A = 'a12' | 'a2' | 'B2' | 'a1'", | ||
output: "type A = 'B2' | 'a1' | 'a2' | 'a12'", | ||
options: [{ caseSensitive: true, natural: true }], | ||
errors: [{ messageId: "unsorted" }], | ||
}, | ||
], | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
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.Node) { | ||
return node.type === TSESTree.AST_NODE_TYPES.TSLiteralType && | ||
node.literal.type === TSESTree.AST_NODE_TYPES.Literal && | ||
typeof node.literal.value === "string" | ||
? node.literal.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({ | ||
caseSensitive: options?.caseSensitive, | ||
natural: options?.natural, | ||
}) | ||
|
||
return { | ||
TSUnionType(node) { | ||
const nodes = node.types | ||
|
||
// If there are one or fewer properties, there is nothing to sort | ||
if (nodes.length < 2) return | ||
|
||
// Ignore mixed type unions | ||
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-unions"), | ||
description: `Sorts TypeScript string unions alphabetically and case insensitive in ascending order.`, | ||
}, | ||
fixable: "code", | ||
messages: { | ||
unsorted: "String unions 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[]> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters