Skip to content

Commit

Permalink
feat(sort-enums): handle numeric operations
Browse files Browse the repository at this point in the history
  • Loading branch information
hugop95 authored Nov 21, 2024
1 parent 936cb05 commit 710cc24
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 48 deletions.
3 changes: 2 additions & 1 deletion rules/sort-classes-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -150,7 +151,7 @@ export let customGroupMatches = (props: CustomGroupMatchesProps): boolean => {
export let getCompareOptions = (
options: Required<SortClassesOptions[0]>,
groupNumber: number,
): CompareOptions | null => {
): CompareOptions<SortingNodeWithDependencies> | null => {
let group = options.groups[groupNumber]
let customGroup =
typeof group === 'string'
Expand Down
130 changes: 110 additions & 20 deletions rules/sort-enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export type Options = [
}>,
]

interface SortEnumsSortingNode
extends SortingNodeWithDependencies<TSESTree.TSEnumMember> {
numericValue: number | null
}

type MESSAGE_ID = 'unexpectedEnumsDependencyOrder' | 'unexpectedEnumsOrder'

let defaultOptions: Required<Options[0]> = {
Expand All @@ -63,8 +68,8 @@ let defaultOptions: Required<Options[0]> = {

export default createEslintRule<Options, MESSAGE_ID>({
create: context => ({
TSEnumDeclaration: node => {
let members = getEnumMembers(node)
TSEnumDeclaration: enumDeclaration => {
let members = getEnumMembers(enumDeclaration)
if (
!isSortable(members) ||
!members.every(({ initializer }) => initializer)
Expand Down Expand Up @@ -116,14 +121,22 @@ export default createEslintRule<Options, MESSAGE_ID>({
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}`
Expand Down Expand Up @@ -152,20 +165,22 @@ export default createEslintRule<Options, MESSAGE_ID>({
},
[[]],
)
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<SortEnumsSortingNode> = {
// 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 ''
Expand All @@ -184,7 +199,7 @@ export default createEslintRule<Options, MESSAGE_ID>({

let sortNodesIgnoringEslintDisabledNodes = (
ignoreEslintDisabledNodes: boolean,
): SortingNodeWithDependencies[] =>
): SortEnumsSortingNode[] =>
sortNodesByDependencies(
formattedMembers.flatMap(nodes =>
sortNodes(nodes, compareOptions, {
Expand All @@ -198,9 +213,8 @@ export default createEslintRule<Options, MESSAGE_ID>({
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 =
Expand All @@ -213,15 +227,15 @@ export default createEslintRule<Options, MESSAGE_ID>({
}

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,
Expand Down Expand Up @@ -281,3 +295,79 @@ export default createEslintRule<Options, MESSAGE_ID>({
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
}
}
3 changes: 2 additions & 1 deletion rules/sort-modules-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -89,7 +90,7 @@ export let customGroupMatches = (props: CustomGroupMatchesProps): boolean => {
export let getCompareOptions = (
options: Required<SortModulesOptions[0]>,
groupNumber: number,
): CompareOptions | null => {
): CompareOptions<SortingNodeWithDependencies> | null => {
let group = options.groups[groupNumber]
let customGroup =
typeof group === 'string'
Expand Down
17 changes: 14 additions & 3 deletions test/sort-enums.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
39 changes: 19 additions & 20 deletions utils/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends SortingNode> =
| AlphabeticalCompareOptions<T>
| LineLengthCompareOptions<T>
| NaturalCompareOptions<T>

interface BaseCompareOptions {
interface BaseCompareOptions<T extends SortingNode> {
/**
* 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<T extends SortingNode>
extends BaseCompareOptions<T> {
specialCharacters: 'remove' | 'trim' | 'keep'
locales: NonNullable<Intl.LocalesArgument>
type: 'alphabetical'
ignoreCase: boolean
}

interface NaturalCompareOptions extends BaseCompareOptions {
interface NaturalCompareOptions<T extends SortingNode>
extends BaseCompareOptions<T> {
specialCharacters: 'remove' | 'trim' | 'keep'
locales: NonNullable<Intl.LocalesArgument>
ignoreCase: boolean
type: 'natural'
}

interface LineLengthCompareOptions extends BaseCompareOptions {
interface LineLengthCompareOptions<T extends SortingNode>
extends BaseCompareOptions<T> {
maxLineLength?: number
type: 'line-length'
}

export let compare = (
a: SortingNode,
b: SortingNode,
options: CompareOptions,
export let compare = <T extends SortingNode>(
a: T,
b: T,
options: CompareOptions<T>,
): 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,
Expand Down Expand Up @@ -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)) {
Expand Down
4 changes: 2 additions & 2 deletions utils/sort-nodes-by-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface ExtraOptions<T extends SortingNode> {
* 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<T> | null
ignoreEslintDisabledNodes?: boolean
isNodeIgnored?(node: T): boolean
}
Expand All @@ -20,7 +20,7 @@ interface GroupOptions {

export let sortNodesByGroups = <T extends SortingNode>(
nodes: T[],
options: CompareOptions & GroupOptions,
options: CompareOptions<T> & GroupOptions,
extraOptions?: ExtraOptions<T>,
): T[] => {
let nodesByNonIgnoredGroupNumber: Record<number, T[]> = {}
Expand Down
2 changes: 1 addition & 1 deletion utils/sort-nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface ExtraOptions {

export let sortNodes = <T extends SortingNode>(
nodes: T[],
options: CompareOptions,
options: CompareOptions<T>,
extraOptions?: ExtraOptions,
): T[] => {
let nonIgnoredNodes: T[] = []
Expand Down

0 comments on commit 710cc24

Please sign in to comment.