UNPKG

eslint-plugin-perfectionist

Version:

ESLint plugin for sorting various data such as objects, imports, types, enums, JSX props, etc.

319 lines (318 loc) 10.8 kB
import { buildCommonJsonSchemas } from '../utils/json-schemas/common-json-schemas.js' import { makeSingleNodeCommentAfterFixes } from '../utils/make-single-node-comment-after-fixes.js' import { makeFixes } from '../utils/make-fixes.js' import { LEFT, ORDER_ERROR, RIGHT, reportErrors, } from '../utils/report-errors.js' import { defaultComparatorByOptionsComputer } from '../utils/compare/default-comparator-by-options-computer.js' import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration.js' import { sortNodes } from '../utils/sort-nodes.js' import { createNodeIndexMap } from '../utils/create-node-index-map.js' import { pairwise } from '../utils/pairwise.js' import { rangeToDiff } from '../utils/range-to-diff.js' import { getSettings } from '../utils/get-settings.js' import { isSortable } from '../utils/is-sortable.js' import { complete } from '../utils/complete.js' import { createEslintRule } from '../utils/create-eslint-rule.js' import { AST_NODE_TYPES } from '@typescript-eslint/utils' var ORDER_ERROR_ID = 'unexpectedSwitchCaseOrder' var defaultOptions = { fallbackSort: { type: 'unsorted' }, specialCharacters: 'keep', type: 'alphabetical', ignoreCase: true, locales: 'en-US', alphabet: '', order: 'asc', } var sort_switch_case_default = createEslintRule({ create: context => ({ SwitchStatement: switchNode => { if (!isSortable(switchNode.cases)) { return } let settings = getSettings(context.settings) let options = complete(context.options.at(0), settings, defaultOptions) let defaultComparator = defaultComparatorByOptionsComputer(options) validateCustomSortConfiguration(options) let { sourceCode } = context if ( switchNode.discriminant.type === AST_NODE_TYPES.Literal && switchNode.discriminant.value === true ) { return } let caseNameSortingNodeGroups = switchNode.cases.reduce( (accumulator, caseNode, index) => { if (caseNode.test) { accumulator.at(-1).push({ size: rangeToDiff(caseNode.test, sourceCode), name: getCaseName(sourceCode, caseNode), partitionId: accumulator.length, isEslintDisabled: false, node: caseNode.test, group: 'unknown', }) } if ( caseNode.consequent.length > 0 && index !== switchNode.cases.length - 1 ) { accumulator.push([]) } return accumulator }, [[]], ) let hasUnsortedNodes = false for (let caseNodesSortingNodeGroup of caseNameSortingNodeGroups) { let sortedCaseNameSortingNodes = sortNodes({ comparatorByOptionsComputer: defaultComparatorByOptionsComputer, nodes: caseNodesSortingNodeGroup, ignoreEslintDisabledNodes: false, options, }) hasUnsortedNodes ||= sortedCaseNameSortingNodes.some( (node, index) => node !== caseNodesSortingNodeGroup[index], ) let nodeIndexMap = createNodeIndexMap(sortedCaseNameSortingNodes) pairwise(caseNodesSortingNodeGroup, (left, right) => { if (!left) { return } if (nodeIndexMap.get(left) < nodeIndexMap.get(right)) { return } reportErrors({ sortedNodes: sortedCaseNameSortingNodes, nodes: caseNodesSortingNodeGroup, messageIds: [ORDER_ERROR_ID], sourceCode, context, right, left, }) }) } let sortingNodes = switchNode.cases.map(caseNode => ({ size: caseNode.test ? rangeToDiff(caseNode.test, sourceCode) : 7, name: getCaseName(sourceCode, caseNode), addSafetySemicolonWhenInline: true, isDefaultClause: !caseNode.test, isEslintDisabled: false, group: 'unknown', partitionId: 0, node: caseNode, })) let sortingNodesGroupWithDefault = reduceCaseSortingNodes( sortingNodes, caseNode => caseNode.node.consequent.length > 0, ).find(caseNodeGroup => caseNodeGroup.some(node => node.isDefaultClause)) if ( sortingNodesGroupWithDefault && !sortingNodesGroupWithDefault.at(-1).isDefaultClause ) { let defaultCase = sortingNodesGroupWithDefault.find( node => node.isDefaultClause, ) let lastCase = sortingNodesGroupWithDefault.at(-1) context.report({ fix: fixer => { let punctuatorAfterLastCase = sourceCode.getTokenAfter( lastCase.node.test, ) let lastCaseRange = [ lastCase.node.range[0], punctuatorAfterLastCase.range[1], ] return [ fixer.replaceText( defaultCase.node, sourceCode.text.slice(...lastCaseRange), ), fixer.replaceTextRange( lastCaseRange, sourceCode.getText(defaultCase.node), ), ...makeSingleNodeCommentAfterFixes({ sortedNode: punctuatorAfterLastCase, node: defaultCase.node, sourceCode, fixer, }), ...makeSingleNodeCommentAfterFixes({ node: punctuatorAfterLastCase, sortedNode: defaultCase.node, sourceCode, fixer, }), ] }, data: { [LEFT]: defaultCase.name, [RIGHT]: lastCase.name, }, messageId: ORDER_ERROR_ID, node: defaultCase.node, }) } let sortingNodeGroupsForBlockSort = reduceCaseSortingNodes( sortingNodes, caseNode => caseHasBreakOrReturn(caseNode.node), ) /** * If the last case does not have a return/break, leave its group at its * place. */ let lastNodeGroup = sortingNodeGroupsForBlockSort.at(-1) let lastBlockCaseShouldStayInPlace = !caseHasBreakOrReturn( lastNodeGroup.at(-1).node, ) let sortedSortingNodeGroupsForBlockSort = [ ...sortingNodeGroupsForBlockSort, ] .toSorted((a, b) => { if (lastBlockCaseShouldStayInPlace) { if (a === lastNodeGroup) { return 1 } /* v8 ignore if -- @preserve last element might never be b. */ if (b === lastNodeGroup) { return -1 } } if (a.some(node => node.isDefaultClause)) { return 1 } if (b.some(node => node.isDefaultClause)) { return -1 } return defaultComparator(a.at(0), b.at(0)) }) .flat() let sortingNodeGroupsForBlockSortFlat = sortingNodeGroupsForBlockSort.flat() pairwise(sortingNodeGroupsForBlockSortFlat, (left, right) => { if (!left) { return } if ( sortedSortingNodeGroupsForBlockSort.indexOf(left) < sortedSortingNodeGroupsForBlockSort.indexOf(right) ) { return } context.report({ fix: fixer => hasUnsortedNodes ? [] : makeFixes({ sortedNodes: sortedSortingNodeGroupsForBlockSort, nodes: sortingNodeGroupsForBlockSortFlat, hasCommentAboveMissing: false, sourceCode, fixer, }), data: { [RIGHT]: right.name, [LEFT]: left.name, }, messageId: ORDER_ERROR_ID, node: right.node, }) }) }, }), meta: { docs: { url: 'https://perfectionist.dev/rules/sort-switch-case', description: 'Enforce sorted switch cases.', recommended: true, }, schema: [ { properties: buildCommonJsonSchemas(), additionalProperties: false, type: 'object', }, ], messages: { [ORDER_ERROR_ID]: ORDER_ERROR }, type: 'suggestion', fixable: 'code', }, defaultOptions: [defaultOptions], name: 'sort-switch-case', }) /** * Groups consecutive switch case nodes into blocks for sorting. * * Creates partitions of case nodes where each partition ends when the * `endsBlock` predicate returns true (typically when a case has a break or * return statement). * * @param caseNodes - Array of switch case sorting nodes to partition. * @param endsBlock - Predicate function that determines if a case ends a block. * @returns A 2D array where each inner array is a sortable block of cases. */ function reduceCaseSortingNodes(caseNodes, endsBlock) { return caseNodes.reduce( (accumulator, caseNode, index) => { accumulator.at(-1).push(caseNode) if (endsBlock(caseNode) && index !== caseNodes.length - 1) { accumulator.push([]) } return accumulator }, [[]], ) } /** * Extracts the name of a switch case for sorting purposes. * * For literal test values, returns the string representation of the value. For * the default case (null test), returns 'default'. For other expressions, * returns the source code text. * * @param sourceCode - The ESLint source code object. * @param caseNode - The switch case AST node. * @returns The name to use for sorting this case. */ function getCaseName(sourceCode, caseNode) { if (caseNode.test?.type === AST_NODE_TYPES.Literal) { return `${caseNode.test.value}` } else if (caseNode.test === null) { return 'default' } return sourceCode.getText(caseNode.test) } /** * Checks if a switch case contains a break or return statement. * * Examines the case's consequent statements, handling both direct statements * and statements wrapped in a block. * * @param caseNode - The switch case AST node to check. * @returns True if the case contains a break or return statement. */ function caseHasBreakOrReturn(caseNode) { return ( caseNode.consequent[0]?.type === AST_NODE_TYPES.BlockStatement ? caseNode.consequent[0].body : caseNode.consequent).some(statementIsBreakOrReturn) } /** * Type guard that checks if a statement is a break or return statement. * * @param statement - The statement AST node to check. * @returns True if the statement is a BreakStatement or ReturnStatement. */ function statementIsBreakOrReturn(statement) { return ( statement.type === AST_NODE_TYPES.BreakStatement || statement.type === AST_NODE_TYPES.ReturnStatement ) } export { sort_switch_case_default as default }