eslint-plugin-perfectionist
Version:
ESLint plugin for sorting various data such as objects, imports, types, enums, JSX props, etc.
361 lines (360 loc) • 12 kB
JavaScript
const commonJsonSchemas = require('../utils/common-json-schemas.js')
const reportErrors = require('../utils/report-errors.js')
const validateNewlinesAndPartitionConfiguration = require('../utils/validate-newlines-and-partition-configuration.js')
const validateGeneratedGroupsConfiguration = require('../utils/validate-generated-groups-configuration.js')
const validateCustomSortConfiguration = require('../utils/validate-custom-sort-configuration.js')
const getCustomGroupsCompareOptions = require('../utils/get-custom-groups-compare-options.js')
const sortNodesByDependencies = require('../utils/sort-nodes-by-dependencies.js')
const getEslintDisabledLines = require('../utils/get-eslint-disabled-lines.js')
const isNodeEslintDisabled = require('../utils/is-node-eslint-disabled.js')
const doesCustomGroupMatch = require('../utils/does-custom-group-match.js')
const sortNodesByGroups = require('../utils/sort-nodes-by-groups.js')
const types = require('./sort-enums/types.js')
const createEslintRule = require('../utils/create-eslint-rule.js')
const reportAllErrors = require('../utils/report-all-errors.js')
const shouldPartition = require('../utils/should-partition.js')
const getEnumMembers = require('../utils/get-enum-members.js')
const rangeToDiff = require('../utils/range-to-diff.js')
const getSettings = require('../utils/get-settings.js')
const isSortable = require('../utils/is-sortable.js')
const useGroups = require('../utils/use-groups.js')
const complete = require('../utils/complete.js')
let defaultOptions = {
fallbackSort: { type: 'unsorted' },
partitionByComment: false,
partitionByNewLine: false,
specialCharacters: 'keep',
newlinesBetween: 'ignore',
forceNumericSort: false,
type: 'alphabetical',
sortByValue: false,
ignoreCase: true,
locales: 'en-US',
customGroups: [],
alphabet: '',
order: 'asc',
groups: [],
}
const sortEnums = createEslintRule.createEslintRule({
create: context => ({
TSEnumDeclaration: enumDeclaration => {
let members = getEnumMembers.getEnumMembers(enumDeclaration)
if (
!isSortable.isSortable(members) ||
!members.every(({ initializer }) => initializer)
) {
return
}
let settings = getSettings.getSettings(context.settings)
let options = complete.complete(
context.options.at(0),
settings,
defaultOptions,
)
validateCustomSortConfiguration.validateCustomSortConfiguration(options)
validateGeneratedGroupsConfiguration.validateGeneratedGroupsConfiguration(
{
selectors: [],
modifiers: [],
options,
},
)
validateNewlinesAndPartitionConfiguration.validateNewlinesAndPartitionConfiguration(
options,
)
let { sourceCode, id } = context
let eslintDisabledLines = getEslintDisabledLines.getEslintDisabledLines({
ruleName: id,
sourceCode,
})
let extractDependencies = (expression, enumName) => {
let dependencies = []
let stack = [expression]
while (stack.length > 0) {
let node = stack.pop()
if (
node.type === 'MemberExpression' &&
node.object.type === 'Identifier' &&
node.object.name === enumName &&
node.property.type === 'Identifier'
) {
dependencies.push(node.property.name)
} else if (node.type === 'Identifier') {
dependencies.push(node.name)
}
if ('left' in node) {
stack.push(node.left)
}
if ('right' in node) {
stack.push(node.right)
}
if ('expressions' in node) {
stack.push(...node.expressions)
}
}
return dependencies
}
let formattedMembers = members.reduce(
(accumulator, member) => {
var _a, _b, _c
let dependencies = []
if (member.initializer) {
dependencies = extractDependencies(
member.initializer,
enumDeclaration.id.name,
)
}
let name =
member.id.type === 'Literal'
? `${member.id.value}`
: sourceCode.getText(member.id)
let { defineGroup, getGroup } = useGroups.useGroups(options)
for (let customGroup of options.customGroups) {
if (
doesCustomGroupMatch.doesCustomGroupMatch({
elementValue: sourceCode.getText(member.initializer),
elementName: name,
selectors: [],
modifiers: [],
customGroup,
})
) {
defineGroup(customGroup.groupName, true)
if (getGroup() === customGroup.groupName) {
break
}
}
}
let lastSortingNode =
(_a = accumulator.at(-1)) == null ? void 0 : _a.at(-1)
let sortingNode = {
numericValue: member.initializer
? getExpressionNumberValue(member.initializer)
: null,
value:
((_b = member.initializer) == null ? void 0 : _b.type) ===
'Literal'
? (((_c = member.initializer.value) == null
? void 0
: _c.toString()) ?? null)
: null,
isEslintDisabled: isNodeEslintDisabled.isNodeEslintDisabled(
member,
eslintDisabledLines,
),
size: rangeToDiff.rangeToDiff(member, sourceCode),
group: getGroup(),
node: member,
dependencies,
name,
}
if (
shouldPartition.shouldPartition({
lastSortingNode,
sortingNode,
sourceCode,
options,
})
) {
accumulator.push([])
}
accumulator.at(-1).push(sortingNode)
return accumulator
},
[[]],
)
let nodes = formattedMembers.flat()
let isNumericEnum = nodes.every(
sortingNode =>
sortingNode.numericValue !== null &&
!Number.isNaN(sortingNode.numericValue),
)
let nodeValueGetter = computeNodeValueGetter({
isNumericEnum,
options,
})
let overriddenOptions = {
...options,
type: computeOptionType({
isNumericEnum,
options,
}),
}
let sortNodesExcludingEslintDisabled = ignoreEslintDisabledNodes => {
let nodesSortedByGroups = formattedMembers.flatMap(sortingNodes =>
sortNodesByGroups.sortNodesByGroups({
getOptionsByGroupNumber: groupNumber => ({
options:
getCustomGroupsCompareOptions.getCustomGroupOverriddenOptions({
options: overriddenOptions,
groupNumber,
}),
nodeValueGetter,
}),
ignoreEslintDisabledNodes,
groups: options.groups,
nodes: sortingNodes,
}),
)
return sortNodesByDependencies.sortNodesByDependencies(
nodesSortedByGroups,
{
ignoreEslintDisabledNodes,
},
)
}
reportAllErrors.reportAllErrors({
availableMessageIds: {
missedSpacingBetweenMembers: 'missedSpacingBetweenEnumsMembers',
extraSpacingBetweenMembers: 'extraSpacingBetweenEnumsMembers',
unexpectedDependencyOrder: 'unexpectedEnumsDependencyOrder',
unexpectedGroupOrder: 'unexpectedEnumsGroupOrder',
unexpectedOrder: 'unexpectedEnumsOrder',
},
sortNodesExcludingEslintDisabled,
sourceCode,
options,
context,
nodes,
})
},
}),
meta: {
schema: [
{
properties: {
...commonJsonSchemas.commonJsonSchemas,
forceNumericSort: {
description:
'Will always sort numeric enums by their value regardless of the sort type specified.',
type: 'boolean',
},
customGroups: {
oneOf: [
commonJsonSchemas.customGroupsJsonSchema,
commonJsonSchemas.buildCustomGroupsArrayJsonSchema({
singleCustomGroupJsonSchema: types.singleCustomGroupJsonSchema,
}),
],
},
sortByValue: {
description: 'Compare enum values instead of names.',
type: 'boolean',
},
partitionByComment: commonJsonSchemas.partitionByCommentJsonSchema,
partitionByNewLine: commonJsonSchemas.partitionByNewLineJsonSchema,
newlinesBetween: commonJsonSchemas.newlinesBetweenJsonSchema,
groups: commonJsonSchemas.groupsJsonSchema,
},
additionalProperties: false,
type: 'object',
},
],
messages: {
unexpectedEnumsDependencyOrder: reportErrors.DEPENDENCY_ORDER_ERROR,
missedSpacingBetweenEnumsMembers: reportErrors.MISSED_SPACING_ERROR,
extraSpacingBetweenEnumsMembers: reportErrors.EXTRA_SPACING_ERROR,
unexpectedEnumsGroupOrder: reportErrors.GROUP_ORDER_ERROR,
unexpectedEnumsOrder: reportErrors.ORDER_ERROR,
},
docs: {
url: 'https://perfectionist.dev/rules/sort-enums',
description: 'Enforce sorted TypeScript enums.',
recommended: true,
},
type: 'suggestion',
fixable: 'code',
},
defaultOptions: [defaultOptions],
name: 'sort-enums',
})
let getExpressionNumberValue = expression => {
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, operator) => {
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,
rightExpression,
operator,
) => {
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
}
}
let computeNodeValueGetter = ({ isNumericEnum, options }) =>
// Get the enum value rather than the name if needed.
options.sortByValue || (isNumericEnum && options.forceNumericSort)
? sortingNode => {
if (isNumericEnum) {
return sortingNode.numericValue.toString()
}
return sortingNode.value ?? ''
}
: null
let computeOptionType = ({ isNumericEnum, options }) =>
/**
* If the enum is numeric, and we sort by value, always use the
* `natural` sort type, which will correctly sort them.
*/
isNumericEnum && (options.forceNumericSort || options.sortByValue)
? 'natural'
: options.type
module.exports = sortEnums