eslint-plugin-perfectionist
Version:
ESLint plugin for sorting various data such as objects, imports, types, enums, JSX props, etc.
282 lines (281 loc) • 9.46 kB
JavaScript
const makeSingleNodeCommentAfterFixes = require('../utils/make-single-node-comment-after-fixes.js')
const validateCustomSortConfiguration = require('../utils/validate-custom-sort-configuration.js')
const reportErrors = require('../utils/report-errors.js')
const createNodeIndexMap = require('../utils/create-node-index-map.js')
const commonJsonSchemas = require('../utils/common-json-schemas.js')
const createEslintRule = require('../utils/create-eslint-rule.js')
const rangeToDiff = require('../utils/range-to-diff.js')
const getSettings = require('../utils/get-settings.js')
const isSortable = require('../utils/is-sortable.js')
const makeFixes = require('../utils/make-fixes.js')
const sortNodes = require('../utils/sort-nodes.js')
const pairwise = require('../utils/pairwise.js')
const complete = require('../utils/complete.js')
const compare = require('../utils/compare.js')
let defaultOptions = {
fallbackSort: { type: 'unsorted' },
specialCharacters: 'keep',
type: 'alphabetical',
ignoreCase: true,
locales: 'en-US',
alphabet: '',
order: 'asc',
}
const sortSwitchCase = createEslintRule.createEslintRule({
create: context => ({
SwitchStatement: switchNode => {
if (!isSortable.isSortable(switchNode.cases)) {
return
}
let settings = getSettings.getSettings(context.settings)
let options = complete.complete(
context.options.at(0),
settings,
defaultOptions,
)
validateCustomSortConfiguration.validateCustomSortConfiguration(options)
let { sourceCode } = context
let isDiscriminantTrue =
switchNode.discriminant.type === 'Literal' &&
switchNode.discriminant.value === true
if (isDiscriminantTrue) {
return
}
let caseNameSortingNodeGroups = switchNode.cases.reduce(
(accumulator, caseNode, index) => {
if (caseNode.test) {
accumulator.at(-1).push({
size: rangeToDiff.rangeToDiff(caseNode.test, sourceCode),
name: getCaseName(sourceCode, caseNode),
isEslintDisabled: false,
node: caseNode.test,
})
}
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.sortNodes({
nodes: caseNodesSortingNodeGroup,
ignoreEslintDisabledNodes: false,
options,
})
hasUnsortedNodes ||
(hasUnsortedNodes = sortedCaseNameSortingNodes.some(
(node, index) => node !== caseNodesSortingNodeGroup[index],
))
let nodeIndexMap = createNodeIndexMap.createNodeIndexMap(
sortedCaseNameSortingNodes,
)
pairwise.pairwise(caseNodesSortingNodeGroup, (left, right) => {
let leftIndex = nodeIndexMap.get(left)
let rightIndex = nodeIndexMap.get(right)
if (leftIndex < rightIndex) {
return
}
reportErrors.reportErrors({
messageIds: ['unexpectedSwitchCaseOrder'],
sortedNodes: sortedCaseNameSortingNodes,
nodes: caseNodesSortingNodeGroup,
sourceCode,
context,
right,
left,
})
})
}
let sortingNodes = switchNode.cases.map(caseNode => ({
size: caseNode.test
? rangeToDiff.rangeToDiff(caseNode.test, sourceCode)
: 'default'.length,
name: getCaseName(sourceCode, caseNode),
addSafetySemicolonWhenInline: true,
isDefaultClause: !caseNode.test,
isEslintDisabled: false,
node: caseNode,
}))
let sortingNodeGroupsForDefaultSort = reduceCaseSortingNodes(
sortingNodes,
caseNode => caseNode.node.consequent.length > 0,
)
let sortingNodesGroupWithDefault = sortingNodeGroupsForDefaultSort.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.makeSingleNodeCommentAfterFixes(
{
sortedNode: punctuatorAfterLastCase,
node: defaultCase.node,
sourceCode,
fixer,
},
),
...makeSingleNodeCommentAfterFixes.makeSingleNodeCommentAfterFixes(
{
node: punctuatorAfterLastCase,
sortedNode: defaultCase.node,
sourceCode,
fixer,
},
),
]
},
data: {
[reportErrors.LEFT]: defaultCase.name,
[reportErrors.RIGHT]: lastCase.name,
},
messageId: 'unexpectedSwitchCaseOrder',
node: defaultCase.node,
})
}
let sortingNodeGroupsForBlockSort = reduceCaseSortingNodes(
sortingNodes,
caseNode => caseHasBreakOrReturn(caseNode.node),
)
let lastNodeGroup = sortingNodeGroupsForBlockSort.at(-1)
let lastBlockCaseShouldStayInPlace = !caseHasBreakOrReturn(
lastNodeGroup.at(-1).node,
)
let sortedSortingNodeGroupsForBlockSort = [
...sortingNodeGroupsForBlockSort,
]
.sort((a, b) => {
if (lastBlockCaseShouldStayInPlace) {
if (a === lastNodeGroup) {
return 1
}
if (b === lastNodeGroup) {
return -1
}
}
if (a.some(node => node.isDefaultClause)) {
return 1
}
if (b.some(node => node.isDefaultClause)) {
return -1
}
return compare.compare({
a: a.at(0),
b: b.at(0),
options,
})
})
.flat()
let sortingNodeGroupsForBlockSortFlat =
sortingNodeGroupsForBlockSort.flat()
pairwise.pairwise(sortingNodeGroupsForBlockSortFlat, (left, right) => {
let indexOfLeft = sortedSortingNodeGroupsForBlockSort.indexOf(left)
let indexOfRight = sortedSortingNodeGroupsForBlockSort.indexOf(right)
if (indexOfLeft < indexOfRight) {
return
}
context.report({
fix: fixer =>
hasUnsortedNodes
? []
: makeFixes.makeFixes({
sortedNodes: sortedSortingNodeGroupsForBlockSort,
nodes: sortingNodeGroupsForBlockSortFlat,
sourceCode,
fixer,
}),
data: {
[reportErrors.RIGHT]: right.name,
[reportErrors.LEFT]: left.name,
},
messageId: 'unexpectedSwitchCaseOrder',
node: right.node,
})
})
},
}),
meta: {
schema: [
{
properties: {
...commonJsonSchemas.commonJsonSchemas,
},
additionalProperties: false,
type: 'object',
},
],
docs: {
url: 'https://perfectionist.dev/rules/sort-switch-case',
description: 'Enforce sorted switch cases.',
recommended: true,
},
messages: {
unexpectedSwitchCaseOrder: reportErrors.ORDER_ERROR,
},
type: 'suggestion',
fixable: 'code',
},
defaultOptions: [defaultOptions],
name: 'sort-switch-case',
})
let getCaseName = (sourceCode, caseNode) => {
var _a
if (((_a = caseNode.test) == null ? void 0 : _a.type) === 'Literal') {
return `${caseNode.test.value}`
} else if (caseNode.test === null) {
return 'default'
}
return sourceCode.getText(caseNode.test)
}
let reduceCaseSortingNodes = (caseNodes, endsBlock) =>
caseNodes.reduce(
(accumulator, caseNode, index) => {
accumulator.at(-1).push(caseNode)
if (endsBlock(caseNode) && index !== caseNodes.length - 1) {
accumulator.push([])
}
return accumulator
},
[[]],
)
let caseHasBreakOrReturn = caseNode => {
var _a
let statements =
((_a = caseNode.consequent[0]) == null ? void 0 : _a.type) ===
'BlockStatement'
? caseNode.consequent[0].body
: caseNode.consequent
return statements.some(statementIsBreakOrReturn)
}
let statementIsBreakOrReturn = statement =>
statement.type === 'BreakStatement' || statement.type === 'ReturnStatement'
module.exports = sortSwitchCase