From 710cc249df52f831f55903f41ff1736e6b62882f Mon Sep 17 00:00:00 2001 From: Hugo <60015232+hugop95@users.noreply.github.com> Date: Fri, 22 Nov 2024 00:21:45 +0100 Subject: [PATCH] feat(sort-enums): handle numeric operations --- rules/sort-classes-utils.ts | 3 +- rules/sort-enums.ts | 130 ++++++++++++++++++++++++++++------ rules/sort-modules-utils.ts | 3 +- test/sort-enums.test.ts | 17 ++++- utils/compare.ts | 39 +++++----- utils/sort-nodes-by-groups.ts | 4 +- utils/sort-nodes.ts | 2 +- 7 files changed, 150 insertions(+), 48 deletions(-) diff --git a/rules/sort-classes-utils.ts b/rules/sort-classes-utils.ts index 1dd177fbf..3df9c7ff7 100644 --- a/rules/sort-classes-utils.ts +++ b/rules/sort-classes-utils.ts @@ -7,6 +7,7 @@ import type { Modifier, Selector, } from './sort-classes.types' +import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies' import type { CompareOptions } from '../utils/compare' import { isSortable } from '../utils/is-sortable' @@ -150,7 +151,7 @@ export let customGroupMatches = (props: CustomGroupMatchesProps): boolean => { export let getCompareOptions = ( options: Required, groupNumber: number, -): CompareOptions | null => { +): CompareOptions | null => { let group = options.groups[groupNumber] let customGroup = typeof group === 'string' diff --git a/rules/sort-enums.ts b/rules/sort-enums.ts index 385b6e96b..d0fcdd147 100644 --- a/rules/sort-enums.ts +++ b/rules/sort-enums.ts @@ -47,6 +47,11 @@ export type Options = [ }>, ] +interface SortEnumsSortingNode + extends SortingNodeWithDependencies { + numericValue: number | null +} + type MESSAGE_ID = 'unexpectedEnumsDependencyOrder' | 'unexpectedEnumsOrder' let defaultOptions: Required = { @@ -63,8 +68,8 @@ let defaultOptions: Required = { export default createEslintRule({ create: context => ({ - TSEnumDeclaration: node => { - let members = getEnumMembers(node) + TSEnumDeclaration: enumDeclaration => { + let members = getEnumMembers(enumDeclaration) if ( !isSortable(members) || !members.every(({ initializer }) => initializer) @@ -116,14 +121,22 @@ export default createEslintRule({ checkNode(expression) return dependencies } - let formattedMembers: SortingNodeWithDependencies[][] = members.reduce( - (accumulator: SortingNodeWithDependencies[][], member) => { + let formattedMembers: SortEnumsSortingNode[][] = members.reduce( + (accumulator: SortEnumsSortingNode[][], member) => { let dependencies: string[] = [] if (member.initializer) { - dependencies = extractDependencies(member.initializer, node.id.name) + dependencies = extractDependencies( + member.initializer, + enumDeclaration.id.name, + ) } let lastSortingNode = accumulator.at(-1)?.at(-1) - let sortingNode: SortingNodeWithDependencies = { + let sortingNode: SortEnumsSortingNode = { + numericValue: member.initializer + ? getExpressionNumberValue( + member.initializer, + ) /* v8 ignore next - Unsure how we can reach that case */ + : null, name: member.id.type === 'Literal' ? `${member.id.value}` @@ -152,20 +165,22 @@ export default createEslintRule({ }, [[]], ) - let isNumericEnum = members.every( - member => - member.initializer?.type === 'Literal' && - typeof member.initializer.value === 'number', + + let sortingNodes = formattedMembers.flat() + let isNumericEnum = sortingNodes.every( + sortingNode => + sortingNode.numericValue !== null && + !Number.isNaN(sortingNode.numericValue), ) - let compareOptions: CompareOptions = { + let compareOptions: CompareOptions = { // 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' - ) { + if (isNumericEnum) { + return sortingNode.numericValue!.toString() + } + if (sortingNode.node.initializer?.type === 'Literal') { return sortingNode.node.initializer.value?.toString() ?? '' } return '' @@ -184,7 +199,7 @@ export default createEslintRule({ let sortNodesIgnoringEslintDisabledNodes = ( ignoreEslintDisabledNodes: boolean, - ): SortingNodeWithDependencies[] => + ): SortEnumsSortingNode[] => sortNodesByDependencies( formattedMembers.flatMap(nodes => sortNodes(nodes, compareOptions, { @@ -198,9 +213,8 @@ export default createEslintRule({ let sortedNodes = sortNodesIgnoringEslintDisabledNodes(false) let sortedNodesExcludingEslintDisabled = sortNodesIgnoringEslintDisabledNodes(true) - let nodes = formattedMembers.flat() - pairwise(nodes, (left, right) => { + pairwise(sortingNodes, (left, right) => { let indexOfLeft = sortedNodes.indexOf(left) let indexOfRight = sortedNodes.indexOf(right) let indexOfRightExcludingEslintDisabled = @@ -213,15 +227,15 @@ export default createEslintRule({ } let firstUnorderedNodeDependentOnRight = - getFirstUnorderedNodeDependentOn(right, nodes) + getFirstUnorderedNodeDependentOn(right, sortingNodes) context.report({ fix: fixer => makeFixes({ sortedNodes: sortedNodesExcludingEslintDisabled, + nodes: sortingNodes, sourceCode, options, fixer, - nodes, }), data: { nodeDependentOnRight: firstUnorderedNodeDependentOnRight?.name, @@ -281,3 +295,79 @@ export default createEslintRule({ defaultOptions: [defaultOptions], name: 'sort-enums', }) + +let getExpressionNumberValue = (expression: TSESTree.Node): number => { + switch (expression.type) { + case 'BinaryExpression': + return getBinaryExpressionNumberValue( + expression.left, + expression.right, + expression.operator, + ) + case 'UnaryExpression': + return getUnaryExpressionNumberValue( + expression.argument, + expression.operator, + ) + case 'Literal': + return typeof expression.value === 'number' + ? expression.value + : Number.NaN + default: + return Number.NaN + } +} + +let getUnaryExpressionNumberValue = ( + argumentExpression: TSESTree.Expression, + operator: string, +): number => { + let argument = getExpressionNumberValue(argumentExpression) + switch (operator) { + case '+': + return argument + case '-': + return -argument + case '~': + return ~argument + /* v8 ignore next 2 - Unsure if we can reach it */ + default: + return Number.NaN + } +} + +let getBinaryExpressionNumberValue = ( + leftExpression: TSESTree.PrivateIdentifier | TSESTree.Expression, + rightExpression: TSESTree.Expression, + operator: string, +): number => { + let left = getExpressionNumberValue(leftExpression) + let right = getExpressionNumberValue(rightExpression) + switch (operator) { + case '**': + return left ** right + case '>>': + return left >> right + case '<<': + return left << right + case '+': + return left + right + case '-': + return left - right + case '*': + return left * right + case '/': + return left / right + case '%': + return left % right + case '|': + return left | right + case '&': + return left & right + case '^': + return left ^ right + /* v8 ignore next 2 - Unsure if we can reach it */ + default: + return Number.NaN + } +} diff --git a/rules/sort-modules-utils.ts b/rules/sort-modules-utils.ts index 3cc9edea9..a0a22cfad 100644 --- a/rules/sort-modules-utils.ts +++ b/rules/sort-modules-utils.ts @@ -5,6 +5,7 @@ import type { Modifier, Selector, } from './sort-modules.types' +import type { SortingNodeWithDependencies } from '../utils/sort-nodes-by-dependencies' import type { CompareOptions } from '../utils/compare' import { matches } from '../utils/matches' @@ -89,7 +90,7 @@ export let customGroupMatches = (props: CustomGroupMatchesProps): boolean => { export let getCompareOptions = ( options: Required, groupNumber: number, -): CompareOptions | null => { +): CompareOptions | null => { let group = options.groups[groupNumber] let customGroup = typeof group === 'string' diff --git a/test/sort-enums.test.ts b/test/sort-enums.test.ts index dd11d60df..fd8628e5d 100644 --- a/test/sort-enums.test.ts +++ b/test/sort-enums.test.ts @@ -1681,9 +1681,20 @@ describe(ruleName, () => { { code: dedent` enum Enum { - 'c' = 0, - 'a' = 1, - 'b' = 2, + 'i' = ~2, // -3 + 'k' = -1, + 'j' = - 0.1, + 'e' = - (((1 + 1) * 2) ** 2) / 4 % 2, // 0 + 'f' = 0, + 'h' = +1, + 'g' = 3 - 1, // 2 + 'b' = 5^6, // 3 + 'l' = 1 + 3, // 4 + 'm' = 2.1 ** 2, // 4.41 + 'a' = 20 >> 2, // 5 + 'm' = 7 & 6, // 6 + 'c' = 5 | 6, // 7 + 'd' = 2 << 2, // 8 } `, options: [ diff --git a/utils/compare.ts b/utils/compare.ts index 106d6ac53..cade523f8 100644 --- a/utils/compare.ts +++ b/utils/compare.ts @@ -2,48 +2,50 @@ import { compare as createNaturalCompare } from 'natural-orderby' import type { SortingNode } from '../typings' -export type CompareOptions = - | AlphabeticalCompareOptions - | LineLengthCompareOptions - | NaturalCompareOptions +export type CompareOptions = + | AlphabeticalCompareOptions + | LineLengthCompareOptions + | NaturalCompareOptions -interface BaseCompareOptions { +interface BaseCompareOptions { /** * Custom function to get the value of the node. By default, returns the * node's name. */ - nodeValueGetter?: ((node: SortingNode) => string) | null + nodeValueGetter?: ((node: T) => string) | null order: 'desc' | 'asc' } -interface AlphabeticalCompareOptions extends BaseCompareOptions { +interface AlphabeticalCompareOptions + extends BaseCompareOptions { specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable type: 'alphabetical' ignoreCase: boolean } -interface NaturalCompareOptions extends BaseCompareOptions { +interface NaturalCompareOptions + extends BaseCompareOptions { specialCharacters: 'remove' | 'trim' | 'keep' locales: NonNullable ignoreCase: boolean type: 'natural' } -interface LineLengthCompareOptions extends BaseCompareOptions { +interface LineLengthCompareOptions + extends BaseCompareOptions { maxLineLength?: number type: 'line-length' } -export let compare = ( - a: SortingNode, - b: SortingNode, - options: CompareOptions, +export let compare = ( + a: T, + b: T, + options: CompareOptions, ): number => { let orderCoefficient = options.order === 'asc' ? 1 : -1 - let sortingFunction: (a: SortingNode, b: SortingNode) => number - let nodeValueGetter = - options.nodeValueGetter ?? ((node: SortingNode) => node.name) + let sortingFunction: (a: T, b: T) => number + let nodeValueGetter = options.nodeValueGetter ?? ((node: T) => node.name) if (options.type === 'alphabetical') { let formatString = getFormatStringFunction( options.ignoreCase, @@ -76,10 +78,7 @@ export let compare = ( let { maxLineLength } = options if (maxLineLength) { - let isTooLong = ( - size: number, - node: SortingNode, - ): undefined | boolean => + let isTooLong = (size: number, node: T): undefined | boolean => size > maxLineLength && node.hasMultipleImportDeclarations if (isTooLong(aSize, aNode)) { diff --git a/utils/sort-nodes-by-groups.ts b/utils/sort-nodes-by-groups.ts index 822ef6372..72ce9e76d 100644 --- a/utils/sort-nodes-by-groups.ts +++ b/utils/sort-nodes-by-groups.ts @@ -9,7 +9,7 @@ interface ExtraOptions { * If not provided, `options` will be used. If function returns null, nodes * will not be sorted within the group. */ - getGroupCompareOptions?(groupNumber: number): CompareOptions | null + getGroupCompareOptions?(groupNumber: number): CompareOptions | null ignoreEslintDisabledNodes?: boolean isNodeIgnored?(node: T): boolean } @@ -20,7 +20,7 @@ interface GroupOptions { export let sortNodesByGroups = ( nodes: T[], - options: CompareOptions & GroupOptions, + options: CompareOptions & GroupOptions, extraOptions?: ExtraOptions, ): T[] => { let nodesByNonIgnoredGroupNumber: Record = {} diff --git a/utils/sort-nodes.ts b/utils/sort-nodes.ts index dc92bb32e..9d591dd88 100644 --- a/utils/sort-nodes.ts +++ b/utils/sort-nodes.ts @@ -9,7 +9,7 @@ interface ExtraOptions { export let sortNodes = ( nodes: T[], - options: CompareOptions, + options: CompareOptions, extraOptions?: ExtraOptions, ): T[] => { let nonIgnoredNodes: T[] = []