From 0f3e9da42ea77fbf5869bfd6dc1dfbcefc5310e3 Mon Sep 17 00:00:00 2001 From: hugop95 Date: Sun, 22 Sep 2024 12:23:12 +0200 Subject: [PATCH 1/3] fix(sort-switch-case): adds `partitionByNewLine` --- rules/sort-switch-case.ts | 308 +++++++++++++++++++++----------------- 1 file changed, 167 insertions(+), 141 deletions(-) diff --git a/rules/sort-switch-case.ts b/rules/sort-switch-case.ts index 7cab1e7b..ef3ee071 100644 --- a/rules/sort-switch-case.ts +++ b/rules/sort-switch-case.ts @@ -4,6 +4,7 @@ import type { TSESLint } from '@typescript-eslint/utils' import type { SortingNode } from '../typings' import { createEslintRule } from '../utils/create-eslint-rule' +import { getLinesBetween } from '../utils/get-lines-between' import { getSourceCode } from '../utils/get-source-code' import { rangeToDiff } from '../utils/range-to-diff' import { getSettings } from '../utils/get-settings' @@ -19,6 +20,7 @@ type MESSAGE_ID = 'unexpectedSwitchCaseOrder' type Options = [ Partial<{ type: 'alphabetical' | 'line-length' | 'natural' + partitionByNewLine: boolean order: 'desc' | 'asc' ignoreCase: boolean }>, @@ -56,6 +58,11 @@ export default createEslintRule({ 'Controls whether sorting should be case-sensitive or not.', type: 'boolean', }, + partitionByNewLine: { + description: + 'Allows to use spaces to separate the nodes into logical groups.', + type: 'boolean', + }, }, additionalProperties: false, }, @@ -70,6 +77,7 @@ export default createEslintRule({ type: 'alphabetical', order: 'asc', ignoreCase: true, + partitionByNewLine: false, }, ], create: context => ({ @@ -80,6 +88,7 @@ export default createEslintRule({ type: 'alphabetical', ignoreCase: true, order: 'asc', + partitionByNewLine: false, } as const) let sourceCode = getSourceCode(context) @@ -98,171 +107,188 @@ export default createEslintRule({ ) if (isDiscriminantIdentifier && isCasesHasBreak) { - let nodes = node.cases.map( - (caseNode: TSESTree.SwitchCase) => { - let name: string - let isDefaultClause = false - if (caseNode.test?.type === 'Literal') { - name = `${caseNode.test.value}` - } else if (caseNode.test === null) { - name = 'default' - isDefaultClause = true - } else { - name = sourceCode.text.slice(...caseNode.test.range) - } + let formattedMembers: SortSwitchCaseSortingNode[][] = [[]] + for (let caseNode of node.cases) { + let name: string + let isDefaultClause = false + if (caseNode.test?.type === 'Literal') { + name = `${caseNode.test.value}` + } else if (caseNode.test === null) { + name = 'default' + isDefaultClause = true + } else { + name = sourceCode.text.slice(...caseNode.test.range) + } - return { - size: rangeToDiff(caseNode.test?.range ?? caseNode.range), - node: caseNode, - isDefaultClause, - name, - } - }, - ) + let lastSortingNode = formattedMembers.at(-1)?.at(-1) + let sortingNode: SortSwitchCaseSortingNode = { + size: rangeToDiff(caseNode.test?.range ?? caseNode.range), + node: caseNode, + isDefaultClause, + name, + } - pairwise(nodes, (left, right, iteration) => { - let compareValue: boolean - let lefter = nodes.at(iteration - 1) - let isCaseGrouped = - lefter?.node.consequent.length === 0 && - left.node.consequent.length !== 0 + if ( + options.partitionByNewLine && + lastSortingNode && + getLinesBetween(sourceCode, lastSortingNode, sortingNode) + ) { + formattedMembers.push([]) + } - let isGroupContainsDefault = (group: SortSwitchCaseSortingNode[]) => - group.some(currentNode => currentNode.isDefaultClause) + formattedMembers.at(-1)!.push(sortingNode) + } - let leftCaseGroup = [left] - let rightCaseGroup = [right] - for (let i = iteration - 1; i >= 0; i--) { - if (nodes.at(i)!.node.consequent.length === 0) { - leftCaseGroup.unshift(nodes.at(i)!) - } else { - break - } - } - if (right.node.consequent.length === 0) { - for (let i = iteration + 1; i < nodes.length; i++) { + for (let nodes of formattedMembers) { + pairwise(nodes, (left, right, iteration) => { + let compareValue: boolean + let lefter = nodes.at(iteration - 1) + let isCaseGrouped = + lefter?.node.consequent.length === 0 && + left.node.consequent.length !== 0 + + let isGroupContainsDefault = (group: SortSwitchCaseSortingNode[]) => + group.some(currentNode => currentNode.isDefaultClause) + + let leftCaseGroup = [left] + let rightCaseGroup = [right] + for (let i = iteration - 1; i >= 0; i--) { if (nodes.at(i)!.node.consequent.length === 0) { - rightCaseGroup.push(nodes.at(i)!) + leftCaseGroup.unshift(nodes.at(i)!) } else { - rightCaseGroup.push(nodes.at(i)!) break } } - } - - if (isGroupContainsDefault(leftCaseGroup)) { - compareValue = true - } else if (isGroupContainsDefault(rightCaseGroup)) { - compareValue = false - } else if (isCaseGrouped) { - compareValue = isPositive(compare(leftCaseGroup[0], right, options)) - } else { - compareValue = isPositive(compare(left, right, options)) - } + if (right.node.consequent.length === 0) { + for (let i = iteration + 1; i < nodes.length; i++) { + if (nodes.at(i)!.node.consequent.length === 0) { + rightCaseGroup.push(nodes.at(i)!) + } else { + rightCaseGroup.push(nodes.at(i)!) + break + } + } + } - if (compareValue) { - context.report({ - messageId: 'unexpectedSwitchCaseOrder', - data: { - left: left.name, - right: right.name, - }, - node: right.node, - fix: fixer => { - let additionalFixes: TSESLint.RuleFix[] = [] - let nodeGroups = nodes.reduce( - ( - accumulator: SortSwitchCaseSortingNode[][], - currentNode: SortSwitchCaseSortingNode, - index, - ) => { - if (index === 0) { - accumulator.at(-1)!.push(currentNode) - } else if ( - accumulator.at(-1)!.at(-1)?.node.consequent.length === 0 - ) { - accumulator.at(-1)!.push(currentNode) - } else { - accumulator.push([currentNode]) - } - return accumulator - }, - [[]], - ) + if (isGroupContainsDefault(leftCaseGroup)) { + compareValue = true + } else if (isGroupContainsDefault(rightCaseGroup)) { + compareValue = false + } else if (isCaseGrouped) { + compareValue = isPositive( + compare(leftCaseGroup[0], right, options), + ) + } else { + compareValue = isPositive(compare(left, right, options)) + } - let sortedNodeGroups = nodeGroups - .map(group => { - let sortedGroup = sortNodes(group, options).sort((a, b) => { - if (b.isDefaultClause) { - return -1 + if (compareValue) { + context.report({ + messageId: 'unexpectedSwitchCaseOrder', + data: { + left: left.name, + right: right.name, + }, + node: right.node, + fix: fixer => { + let additionalFixes: TSESLint.RuleFix[] = [] + let nodeGroups = nodes.reduce( + ( + accumulator: SortSwitchCaseSortingNode[][], + currentNode: SortSwitchCaseSortingNode, + index, + ) => { + if (index === 0) { + accumulator.at(-1)!.push(currentNode) + } else if ( + accumulator.at(-1)!.at(-1)?.node.consequent.length === 0 + ) { + accumulator.at(-1)!.push(currentNode) + } else { + accumulator.push([currentNode]) } - return 1 - }) + return accumulator + }, + [[]], + ) - if (group.at(-1)!.name !== sortedGroup.at(-1)!.name) { - let consequentNodeIndex = sortedGroup.findIndex( - currentNode => currentNode.node.consequent.length !== 0, + let sortedNodeGroups = nodeGroups + .map(group => { + let sortedGroup = sortNodes(group, options).sort( + (a, b) => { + if (b.isDefaultClause) { + return -1 + } + return 1 + }, ) - let firstSortedNodeConsequent = - sortedGroup.at(consequentNodeIndex)!.node.consequent - let consequentStart = firstSortedNodeConsequent - .at(0) - ?.range.at(0) - let consequentEnd = firstSortedNodeConsequent - .at(-1) - ?.range.at(1) - let lastNode = group.at(-1)!.node - if (consequentStart && consequentEnd && lastNode.test) { - lastNode.range = [ - lastNode.range.at(0)!, - lastNode.test.range.at(1)! + 1, - ] - additionalFixes.push( - ...makeFixes(fixer, group, sortedGroup, sourceCode), - fixer.removeRange([ - lastNode.range.at(1)!, - consequentEnd, - ]), - fixer.insertTextAfter( - lastNode, - sourceCode.text.slice( - lastNode.range.at(1), + if (group.at(-1)!.name !== sortedGroup.at(-1)!.name) { + let consequentNodeIndex = sortedGroup.findIndex( + currentNode => + currentNode.node.consequent.length !== 0, + ) + let firstSortedNodeConsequent = + sortedGroup.at(consequentNodeIndex)!.node.consequent + let consequentStart = firstSortedNodeConsequent + .at(0) + ?.range.at(0) + let consequentEnd = firstSortedNodeConsequent + .at(-1) + ?.range.at(1) + let lastNode = group.at(-1)!.node + + if (consequentStart && consequentEnd && lastNode.test) { + lastNode.range = [ + lastNode.range.at(0)!, + lastNode.test.range.at(1)! + 1, + ] + additionalFixes.push( + ...makeFixes(fixer, group, sortedGroup, sourceCode), + fixer.removeRange([ + lastNode.range.at(1)!, consequentEnd, + ]), + fixer.insertTextAfter( + lastNode, + sourceCode.text.slice( + lastNode.range.at(1), + consequentEnd, + ), ), - ), - ) + ) + } } - } - return sortedGroup - }) - .sort((a, b) => { - if (isGroupContainsDefault(a)) { - return 1 - } else if (isGroupContainsDefault(b)) { - return -1 - } - return compare(a.at(0)!, b.at(0)!, options) - }) + return sortedGroup + }) + .sort((a, b) => { + if (isGroupContainsDefault(a)) { + return 1 + } else if (isGroupContainsDefault(b)) { + return -1 + } + return compare(a.at(0)!, b.at(0)!, options) + }) - let sortedNodes = sortedNodeGroups.flat() + let sortedNodes = sortedNodeGroups.flat() - for (let max = sortedNodes.length, i = 0; i < max; i++) { - if (sortedNodes.at(i)!.isDefaultClause) { - sortedNodes.push(sortedNodes.splice(i, 1).at(0)!) + for (let max = sortedNodes.length, i = 0; i < max; i++) { + if (sortedNodes.at(i)!.isDefaultClause) { + sortedNodes.push(sortedNodes.splice(i, 1).at(0)!) + } } - } - if (additionalFixes.length) { - return additionalFixes - } + if (additionalFixes.length) { + return additionalFixes + } - return makeFixes(fixer, nodes, sortedNodes, sourceCode) - }, - }) - } - }) + return makeFixes(fixer, nodes, sortedNodes, sourceCode) + }, + }) + } + }) + } } }, }), From 85c35bc484be80272c9cf650394737fe514b8525 Mon Sep 17 00:00:00 2001 From: hugop95 Date: Sun, 22 Sep 2024 12:23:20 +0200 Subject: [PATCH 2/3] fix(sort-switch-case): adds tests --- test/sort-switch-case.test.ts | 58 +++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/test/sort-switch-case.test.ts b/test/sort-switch-case.test.ts index ad2c98e9..40eb61b8 100644 --- a/test/sort-switch-case.test.ts +++ b/test/sort-switch-case.test.ts @@ -531,6 +531,64 @@ describe(ruleName, () => { ], }, ) + + ruleTester.run( + `${ruleName}(${type}): allows to use new line as partition`, + rule, + { + valid: [], + invalid: [ + { + code: dedent` + switch (x) { + case 'd': + case 'a': + + case 'c': + + case 'e': + case 'b': + break + } + `, + output: dedent` + switch (x) { + case 'a': + case 'd': + + case 'c': + + case 'b': + case 'e': + break + } + `, + options: [ + { + ...options, + partitionByNewLine: true, + }, + ], + errors: [ + { + messageId: 'unexpectedSwitchCaseOrder', + data: { + left: 'd', + right: 'a', + }, + }, + { + messageId: 'unexpectedSwitchCaseOrder', + data: { + left: 'e', + right: 'b', + }, + }, + ], + }, + ], + }, + ) }) describe(`${ruleName}: sorting by natural order`, () => { From 6f420e6187e3f42391b70c143503a6830ccd415f Mon Sep 17 00:00:00 2001 From: hugop95 Date: Sun, 22 Sep 2024 12:23:28 +0200 Subject: [PATCH 3/3] fix(sort-switch-case): adds documentation --- docs/content/rules/sort-switch-case.mdx | 28 +++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/content/rules/sort-switch-case.mdx b/docs/content/rules/sort-switch-case.mdx index d524b1cc..13262a43 100644 --- a/docs/content/rules/sort-switch-case.mdx +++ b/docs/content/rules/sort-switch-case.mdx @@ -174,6 +174,32 @@ 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”). +### partitionByNewLine + +default: `false` + +When `true`, the rule will not sort the members of an enum if there is an empty line between them. This can be useful for keeping logically separated groups of members in their defined order. + +```ts +switch (action) { + // Group 1 + case 'Drone': + case 'Keyboard': + case 'Mouse': + case 'Smartphone': + + // Group 2 + case 'Laptop': + case 'Monitor': + case 'Smartwatch': + case 'Tablet': + + // Group 3 + case 'Headphones': + case 'Router': +} +``` + ## Usage