diff --git a/docs/content/rules/sort-enums.mdx b/docs/content/rules/sort-enums.mdx
index 9c072040..ad930d39 100644
--- a/docs/content/rules/sort-enums.mdx
+++ b/docs/content/rules/sort-enums.mdx
@@ -112,6 +112,26 @@ Controls whether sorting should be case-sensitive or not.
- `true` — Ignore case when sorting alphabetically or naturally (e.g., “A” and “a” are the same).
- `false` — Consider case when sorting (e.g., “A” comes before “a”).
+### sortByValue
+
+default: `false`
+
+Controls whether sorting should be done using the enum's values or names.
+
+- `true` — Use enum values.
+- `false` — Use enum names.
+
+When this setting is `true`, numeric enums will have their values sorted numerically regardless of the `type` setting.
+
+### forceNumericSort
+
+default: `false`
+
+Controls whether numeric enums should always be sorted numerically, regardless of the `type` and `sortByValue` settings.
+
+- `true` — Use enum values.
+- `false` — Use enum names.
+
### partitionByComment
default: `false`
@@ -144,6 +164,7 @@ Allows you to use comments to separate the members of enums into logical groups.
order: 'asc',
ignoreCase: true,
partitionByComment: false,
+ sortByValue: false
},
],
},
@@ -168,6 +189,8 @@ Allows you to use comments to separate the members of enums into logical groups.
order: 'asc',
ignoreCase: true,
partitionByComment: false,
+ sortByValue: false,
+ forceNumericSort: false
},
],
},
diff --git a/rules/sort-enums.ts b/rules/sort-enums.ts
index 44a9f96c..d30ba0c2 100644
--- a/rules/sort-enums.ts
+++ b/rules/sort-enums.ts
@@ -1,5 +1,6 @@
import type { TSESTree } from '@typescript-eslint/types'
+import type { CompareOptions } from '../utils/compare'
import type { SortingNode } from '../typings'
import { isPartitionComment } from '../utils/is-partition-comment'
@@ -17,11 +18,13 @@ import { compare } from '../utils/compare'
type MESSAGE_ID = 'unexpectedEnumsOrder'
-type Options = [
+export type Options = [
Partial<{
type: 'alphabetical' | 'line-length' | 'natural'
partitionByComment: string[] | boolean | string
+ forceNumericSort: boolean
order: 'desc' | 'asc'
+ sortByValue: boolean
ignoreCase: boolean
}>,
]
@@ -54,6 +57,15 @@ export default createEslintRule({
'Controls whether sorting should be case-sensitive or not.',
type: 'boolean',
},
+ sortByValue: {
+ description: 'Compare enum values instead of names.',
+ type: 'boolean',
+ },
+ forceNumericSort: {
+ description:
+ 'Will always sort numeric enums by their value regardless of the sort type specified.',
+ type: 'boolean',
+ },
partitionByComment: {
description:
'Allows you to use comments to separate the class members into logical groups.',
@@ -85,7 +97,9 @@ export default createEslintRule({
type: 'alphabetical',
order: 'asc',
ignoreCase: true,
+ sortByValue: false,
partitionByComment: false,
+ forceNumericSort: false,
},
],
create: context => ({
@@ -94,21 +108,24 @@ export default createEslintRule({
/* v8 ignore next 2 */
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
node.body?.members ?? nodeValue.members ?? []
+ let members = getMembers(node)
if (
- getMembers(node).length > 1 &&
- getMembers(node).every(({ initializer }) => initializer)
+ members.length > 1 &&
+ members.every(({ initializer }) => initializer)
) {
let options = complete(context.options.at(0), {
partitionByComment: false,
type: 'alphabetical',
ignoreCase: true,
order: 'asc',
+ sortByValue: false,
+ forceNumericSort: false,
} as const)
let sourceCode = getSourceCode(context)
let partitionComment = options.partitionByComment
- let formattedMembers: SortingNode[][] = getMembers(node).reduce(
+ let formattedMembers: SortingNode[][] = members.reduce(
(accumulator: SortingNode[][], member) => {
let comment = getCommentBefore(member, sourceCode)
@@ -135,10 +152,37 @@ export default createEslintRule({
},
[[]],
)
+ let isNumericEnum = members.every(
+ member =>
+ member.initializer?.type === 'Literal' &&
+ typeof member.initializer.value === 'number',
+ )
+ let compareOptions: CompareOptions = {
+ // If the enum is numeric, and we sort by value, always use the `natural` sort type, which will correctly sort them.
+ type:
+ isNumericEnum && (options.forceNumericSort || options.sortByValue)
+ ? 'natural'
+ : options.type,
+ order: options.order,
+ ignoreCase: options.ignoreCase,
+ // Get the enum value rather than the name if needed
+ nodeValueGetter:
+ options.sortByValue || (isNumericEnum && options.forceNumericSort)
+ ? sortingNode => {
+ if (
+ sortingNode.node.type === 'TSEnumMember' &&
+ sortingNode.node.initializer?.type === 'Literal'
+ ) {
+ return sortingNode.node.initializer.value?.toString() ?? ''
+ }
+ return ''
+ }
+ : undefined,
+ }
for (let nodes of formattedMembers) {
pairwise(nodes, (left, right) => {
- if (isPositive(compare(left, right, options))) {
+ if (isPositive(compare(left, right, compareOptions))) {
context.report({
messageId: 'unexpectedEnumsOrder',
data: {
@@ -150,7 +194,7 @@ export default createEslintRule({
makeFixes(
fixer,
nodes,
- sortNodes(nodes, options),
+ sortNodes(nodes, compareOptions),
sourceCode,
{ partitionComment },
),
diff --git a/test/sort-enums.test.ts b/test/sort-enums.test.ts
index 66629266..76581ca2 100644
--- a/test/sort-enums.test.ts
+++ b/test/sort-enums.test.ts
@@ -2,6 +2,8 @@ import { RuleTester } from '@typescript-eslint/rule-tester'
import { afterAll, describe, it } from 'vitest'
import { dedent } from 'ts-dedent'
+import type { Options } from '../rules/sort-enums'
+
import rule from '../rules/sort-enums'
let ruleName = 'sort-enums'
@@ -396,6 +398,115 @@ describe(ruleName, () => {
],
},
)
+
+ ruleTester.run(`${ruleName}: sort enum values correctly`, rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ enum Enum {
+ 'a' = 'i',
+ 'b' = 'h',
+ 'c' = 'g',
+ 'd' = 'f',
+ 'e' = 'e',
+ 'f' = 'd',
+ 'g' = 'c',
+ 'h' = 'b',
+ 'i' = 'a',
+ 'j' = null,
+ 'k' = undefined,
+ }
+ `,
+ output: dedent`
+ enum Enum {
+ 'j' = null,
+ 'k' = undefined,
+ 'i' = 'a',
+ 'h' = 'b',
+ 'g' = 'c',
+ 'f' = 'd',
+ 'e' = 'e',
+ 'd' = 'f',
+ 'c' = 'g',
+ 'b' = 'h',
+ 'a' = 'i',
+ }
+ `,
+ options: [
+ {
+ ...options,
+ sortByValue: true,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'a',
+ right: 'b',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'b',
+ right: 'c',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'c',
+ right: 'd',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'd',
+ right: 'e',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'e',
+ right: 'f',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'f',
+ right: 'g',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'g',
+ right: 'h',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'h',
+ right: 'i',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'i',
+ right: 'j',
+ },
+ },
+ ],
+ },
+ ],
+ })
})
describe(`${ruleName}: sorting by natural order`, () => {
@@ -778,6 +889,108 @@ describe(ruleName, () => {
],
},
)
+
+ ruleTester.run(`${ruleName}: sort enum values correctly`, rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ enum Enum {
+ 'a' = 'ffffff',
+ 'b' = 4444,
+ 'c' = 6,
+ 'd' = 5,
+ 'e' = 4,
+ 'f' = '3',
+ 'g' = 2,
+ 'h' = 1,
+ 'i' = '',
+ 'j' = null,
+ 'k' = undefined,
+ }
+ `,
+ output: dedent`
+ enum Enum {
+ 'i' = '',
+ 'j' = null,
+ 'k' = undefined,
+ 'h' = 1,
+ 'g' = 2,
+ 'f' = '3',
+ 'e' = 4,
+ 'd' = 5,
+ 'c' = 6,
+ 'b' = 4444,
+ 'a' = 'ffffff',
+ }
+ `,
+ options: [
+ {
+ ...options,
+ sortByValue: true,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'a',
+ right: 'b',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'b',
+ right: 'c',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'c',
+ right: 'd',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'd',
+ right: 'e',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'e',
+ right: 'f',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'f',
+ right: 'g',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'g',
+ right: 'h',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'h',
+ right: 'i',
+ },
+ },
+ ],
+ },
+ ],
+ })
})
describe(`${ruleName}: sorting by line length`, () => {
@@ -1152,9 +1365,291 @@ describe(ruleName, () => {
],
},
)
+
+ ruleTester.run(`${ruleName}: sort enum values correctly`, rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ enum Enum {
+ 'a' = '',
+ 'b' = 'b',
+ 'c' = 'cc',
+ 'd' = 'ddd',
+ 'e' = 'eeee',
+ 'f' = 'fffff',
+ 'g' = 'gggggg',
+ 'h' = 'hhhhhhh',
+ 'i' = 'iiiiiiii',
+ 'j' = 'jj',
+ }
+ `,
+ output: dedent`
+ enum Enum {
+ 'i' = 'iiiiiiii',
+ 'h' = 'hhhhhhh',
+ 'g' = 'gggggg',
+ 'f' = 'fffff',
+ 'e' = 'eeee',
+ 'd' = 'ddd',
+ 'c' = 'cc',
+ 'j' = 'jj',
+ 'b' = 'b',
+ 'a' = '',
+ }
+ `,
+ options: [
+ {
+ ...options,
+ sortByValue: true,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'a',
+ right: 'b',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'b',
+ right: 'c',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'c',
+ right: 'd',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'd',
+ right: 'e',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'e',
+ right: 'f',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'f',
+ right: 'g',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'g',
+ right: 'h',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'h',
+ right: 'i',
+ },
+ },
+ ],
+ },
+ ],
+ })
})
describe(`${ruleName}: misc`, () => {
+ ruleTester.run(`${ruleName}: detects numeric enums`, rule, {
+ valid: [
+ {
+ code: dedent`
+ enum Enum {
+ 'a' = '1',
+ 'b' = 2,
+ 'c' = 0,
+ }
+ `,
+ options: [
+ {
+ forceNumericSort: true,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ enum Enum {
+ 'a' = 1,
+ 'b' = 2,
+ 'c' = 0,
+ d,
+ }
+ `,
+ options: [
+ {
+ forceNumericSort: true,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ enum Enum {
+ 'a' = 1,
+ 'b' = 2,
+ 'c' = 0,
+ d = undefined,
+ }
+ `,
+ options: [
+ {
+ forceNumericSort: true,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ enum Enum {
+ 'a' = 1,
+ 'b' = 2,
+ 'c' = 0,
+ d = null,
+ }
+ `,
+ options: [
+ {
+ forceNumericSort: true,
+ },
+ ],
+ },
+ {
+ code: dedent`
+ enum Enum {
+ 'c' = 0,
+ 'a' = 1,
+ 'b' = 2,
+ }
+ `,
+ options: [
+ {
+ forceNumericSort: true,
+ },
+ ],
+ },
+ ],
+ invalid: [],
+ })
+
+ let sortTypes: Options[0]['type'][] = [
+ 'alphabetical',
+ 'line-length',
+ 'natural',
+ ]
+ for (let type of sortTypes) {
+ ruleTester.run(
+ `${ruleName}: sortByValue = true => sorts numerical enums numerically for type ${type}`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ enum Enum {
+ 'b' = 2,
+ 'a' = 1,
+ 'c' = 0,
+ }
+ `,
+ output: dedent`
+ enum Enum {
+ 'c' = 0,
+ 'a' = 1,
+ 'b' = 2,
+ }
+ `,
+ options: [
+ {
+ type,
+ sortByValue: true,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'b',
+ right: 'a',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'a',
+ right: 'c',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+
+ ruleTester.run(
+ `${ruleName}: forceNumericSort = true => sorts numerical enums numerically regardless for type ${type}`,
+ rule,
+ {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ enum Enum {
+ 'b' = 2,
+ 'a' = 1,
+ 'c' = 0,
+ }
+ `,
+ output: dedent`
+ enum Enum {
+ 'c' = 0,
+ 'a' = 1,
+ 'b' = 2,
+ }
+ `,
+ options: [
+ {
+ type,
+ forceNumericSort: true,
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'b',
+ right: 'a',
+ },
+ },
+ {
+ messageId: 'unexpectedEnumsOrder',
+ data: {
+ left: 'a',
+ right: 'c',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ )
+ }
+
ruleTester.run(
`${ruleName}: sets alphabetical asc sorting as default`,
rule,
diff --git a/utils/compare.ts b/utils/compare.ts
index a233d75f..1cf8e3d9 100644
--- a/utils/compare.ts
+++ b/utils/compare.ts
@@ -2,15 +2,38 @@ import naturalCompare from 'natural-compare-lite'
import type { SortingNode } from '../typings'
+interface BaseCompareOptions {
+ /**
+ * Custom function to get the value of the node. By default, returns the node's name.
+ */
+ nodeValueGetter?: (node: SortingNode) => string
+ order: 'desc' | 'asc'
+}
+
+interface AlphabeticalCompareOptions extends BaseCompareOptions {
+ type: 'alphabetical'
+ ignoreCase?: boolean
+}
+
+interface LineLengthCompareOptions extends BaseCompareOptions {
+ maxLineLength?: number
+ type: 'line-length'
+}
+
+interface NaturalCompareOptions extends BaseCompareOptions {
+ ignoreCase?: boolean
+ type: 'natural'
+}
+
+export type CompareOptions =
+ | AlphabeticalCompareOptions
+ | LineLengthCompareOptions
+ | NaturalCompareOptions
+
export let compare = (
a: SortingNode,
b: SortingNode,
- options: {
- type: 'alphabetical' | 'line-length' | 'natural'
- maxLineLength?: number
- order: 'desc' | 'asc'
- ignoreCase?: boolean
- },
+ options: CompareOptions,
): number => {
if (b.dependencies?.includes(a.name)) {
return -1
@@ -21,12 +44,19 @@ export let compare = (
let orderCoefficient = options.order === 'asc' ? 1 : -1
let sortingFunction: (a: SortingNode, b: SortingNode) => number
- let formatString = (string: string) =>
- options.ignoreCase ? string.toLowerCase() : string
+ let formatString =
+ options.type === 'line-length' || !options.ignoreCase
+ ? (string: string) => string
+ : (string: string) => string.toLowerCase()
+
+ let nodeValueGetter =
+ options.nodeValueGetter ?? ((node: SortingNode) => node.name)
if (options.type === 'alphabetical') {
sortingFunction = (aNode, bNode) =>
- formatString(aNode.name).localeCompare(formatString(bNode.name))
+ formatString(nodeValueGetter(aNode)).localeCompare(
+ formatString(nodeValueGetter(bNode)),
+ )
} else if (options.type === 'natural') {
let prepareNumeric = (string: string) => {
let formattedNumberPattern = /^[+-]?[\d ,_]+(\.[\d ,_]+)?$/
@@ -37,8 +67,8 @@ export let compare = (
}
sortingFunction = (aNode, bNode) =>
naturalCompare(
- prepareNumeric(formatString(aNode.name)),
- prepareNumeric(formatString(bNode.name)),
+ prepareNumeric(formatString(nodeValueGetter(aNode))),
+ prepareNumeric(formatString(nodeValueGetter(bNode))),
)
} else {
sortingFunction = (aNode, bNode) => {
@@ -52,11 +82,11 @@ export let compare = (
size > maxLineLength && node.hasMultipleImportDeclarations
if (isTooLong(aSize, aNode)) {
- aSize = aNode.name.length + 10
+ aSize = nodeValueGetter(aNode).length + 10
}
if (isTooLong(bSize, bNode)) {
- bSize = bNode.name.length + 10
+ bSize = nodeValueGetter(bNode).length + 10
}
}
diff --git a/utils/sort-nodes.ts b/utils/sort-nodes.ts
index da88ffb3..c8c1487d 100644
--- a/utils/sort-nodes.ts
+++ b/utils/sort-nodes.ts
@@ -1,12 +1,9 @@
+import type { CompareOptions } from './compare'
import type { SortingNode } from '../typings'
import { compare } from './compare'
export let sortNodes = (
nodes: T[],
- options: {
- type: 'alphabetical' | 'line-length' | 'natural'
- order: 'desc' | 'asc'
- ignoreCase?: boolean
- },
+ options: CompareOptions,
): T[] => [...nodes].sort((a, b) => compare(a, b, options))