eslint-plugin-perfectionist
Version:
ESLint plugin for sorting various data such as objects, imports, types, enums, JSX props, etc.
568 lines (567 loc) • 21.4 kB
JavaScript
'use strict'
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 getCustomGroupsCompareOptions = require('../utils/get-custom-groups-compare-options.js')
const validateGeneratedGroupsConfiguration = require('../utils/validate-generated-groups-configuration.js')
const types = require('./sort-classes/types.js')
const validateCustomSortConfiguration = require('../utils/validate-custom-sort-configuration.js')
const getOverloadSignatureGroups = require('./sort-classes/get-overload-signature-groups.js')
const generatePredefinedGroups = require('../utils/generate-predefined-groups.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 getNodeDecorators = require('../utils/get-node-decorators.js')
const createEslintRule = require('../utils/create-eslint-rule.js')
const getDecoratorName = require('../utils/get-decorator-name.js')
const reportAllErrors = require('../utils/report-all-errors.js')
const shouldPartition = require('../utils/should-partition.js')
const getGroupNumber = require('../utils/get-group-number.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')
const matches = require('../utils/matches.js')
let cachedGroupsByModifiersAndSelectors = /* @__PURE__ */ new Map()
let defaultOptions = {
groups: [
'index-signature',
['static-property', 'static-accessor-property'],
['static-get-method', 'static-set-method'],
['protected-static-property', 'protected-static-accessor-property'],
['protected-static-get-method', 'protected-static-set-method'],
['private-static-property', 'private-static-accessor-property'],
['private-static-get-method', 'private-static-set-method'],
'static-block',
['property', 'accessor-property'],
['get-method', 'set-method'],
['protected-property', 'protected-accessor-property'],
['protected-get-method', 'protected-set-method'],
['private-property', 'private-accessor-property'],
['private-get-method', 'private-set-method'],
'constructor',
['static-method', 'static-function-property'],
['protected-static-method', 'protected-static-function-property'],
['private-static-method', 'private-static-function-property'],
['method', 'function-property'],
['protected-method', 'protected-function-property'],
['private-method', 'private-function-property'],
'unknown',
],
ignoreCallbackDependenciesPatterns: [],
fallbackSort: { type: 'unsorted' },
partitionByComment: false,
partitionByNewLine: false,
newlinesBetween: 'ignore',
specialCharacters: 'keep',
type: 'alphabetical',
ignoreCase: true,
customGroups: [],
locales: 'en-US',
alphabet: '',
order: 'asc',
}
const sortClasses = createEslintRule.createEslintRule({
create: context => ({
ClassBody: node => {
var _a
if (!isSortable.isSortable(node.body)) {
return
}
let settings = getSettings.getSettings(context.settings)
let options = complete.complete(
context.options.at(0),
settings,
defaultOptions,
)
validateCustomSortConfiguration.validateCustomSortConfiguration(options)
validateGeneratedGroupsConfiguration.validateGeneratedGroupsConfiguration(
{
modifiers: types.allModifiers,
selectors: types.allSelectors,
options,
},
)
validateNewlinesAndPartitionConfiguration.validateNewlinesAndPartitionConfiguration(
options,
)
let { sourceCode, id } = context
let eslintDisabledLines = getEslintDisabledLines.getEslintDisabledLines({
ruleName: id,
sourceCode,
})
let className = (_a = node.parent.id) == null ? void 0 : _a.name
let getDependencyName = props =>
`${props.isStatic ? 'static ' : ''}${props.isPrivateHash ? '#' : ''}${props.nodeNameWithoutStartingHash}`
let classMethodsDependencyNames = new Set(
node.body
.map(member => {
if (
(member.type === 'MethodDefinition' ||
member.type === 'TSAbstractMethodDefinition') &&
'name' in member.key
) {
return getDependencyName({
isPrivateHash: member.key.type === 'PrivateIdentifier',
nodeNameWithoutStartingHash: member.key.name,
isStatic: member.static,
})
}
return null
})
.filter(Boolean),
)
let extractDependencies = (expression, isMemberStatic) => {
let dependencies = []
let checkNode = nodeValue => {
if (
nodeValue.type === 'MemberExpression' &&
(nodeValue.object.type === 'ThisExpression' ||
(nodeValue.object.type === 'Identifier' &&
nodeValue.object.name === className)) &&
(nodeValue.property.type === 'Identifier' ||
nodeValue.property.type === 'PrivateIdentifier')
) {
let isStaticDependency =
isMemberStatic || nodeValue.object.type === 'Identifier'
let dependencyName = getDependencyName({
isPrivateHash: nodeValue.property.type === 'PrivateIdentifier',
nodeNameWithoutStartingHash: nodeValue.property.name,
isStatic: isStaticDependency,
})
if (!classMethodsDependencyNames.has(dependencyName)) {
dependencies.push(dependencyName)
}
}
if (nodeValue.type === 'Property') {
traverseNode(nodeValue.key)
traverseNode(nodeValue.value)
}
if (nodeValue.type === 'ConditionalExpression') {
traverseNode(nodeValue.test)
traverseNode(nodeValue.consequent)
traverseNode(nodeValue.alternate)
}
if (
'expression' in nodeValue &&
typeof nodeValue.expression !== 'boolean'
) {
traverseNode(nodeValue.expression)
}
if ('object' in nodeValue) {
traverseNode(nodeValue.object)
}
if ('callee' in nodeValue) {
traverseNode(nodeValue.callee)
}
if ('init' in nodeValue && nodeValue.init) {
traverseNode(nodeValue.init)
}
if ('body' in nodeValue && nodeValue.body) {
traverseNode(nodeValue.body)
}
if ('left' in nodeValue) {
traverseNode(nodeValue.left)
}
if ('right' in nodeValue) {
traverseNode(nodeValue.right)
}
if ('elements' in nodeValue) {
let elements = nodeValue.elements.filter(
currentNode => currentNode !== null,
)
for (let element of elements) {
traverseNode(element)
}
}
if ('argument' in nodeValue && nodeValue.argument) {
traverseNode(nodeValue.argument)
}
if ('arguments' in nodeValue) {
let shouldIgnore = false
if (nodeValue.type === 'CallExpression') {
let functionName =
'name' in nodeValue.callee ? nodeValue.callee.name : null
shouldIgnore =
functionName !== null &&
matches.matches(
functionName,
options.ignoreCallbackDependenciesPatterns,
)
}
if (!shouldIgnore) {
for (let argument of nodeValue.arguments) {
traverseNode(argument)
}
}
}
if ('declarations' in nodeValue) {
for (let declaration of nodeValue.declarations) {
traverseNode(declaration)
}
}
if ('properties' in nodeValue) {
for (let property of nodeValue.properties) {
traverseNode(property)
}
}
if ('expressions' in nodeValue) {
for (let nodeExpression of nodeValue.expressions) {
traverseNode(nodeExpression)
}
}
}
let traverseNode = nodeValue => {
if (Array.isArray(nodeValue)) {
for (let nodeItem of nodeValue) {
traverseNode(nodeItem)
}
} else {
checkNode(nodeValue)
}
}
traverseNode(expression)
return dependencies
}
let overloadSignatureGroups =
getOverloadSignatureGroups.getOverloadSignatureGroups(node.body)
let formattedNodes = node.body.reduce(
(accumulator, member) => {
var _a2, _b, _c, _d, _e
let name
let dependencies = []
let { defineGroup, getGroup } = useGroups.useGroups(options)
if (member.type === 'StaticBlock') {
name = 'static'
} else if (member.type === 'TSIndexSignature') {
name = sourceCode.text.slice(
member.range.at(0),
((_a2 = member.typeAnnotation) == null
? void 0
: _a2.range.at(0)) ?? member.range.at(1),
)
} else if (member.key.type === 'Identifier') {
;({ name } = member.key)
} else {
name = sourceCode.getText(member.key)
}
let isPrivateHash =
'key' in member && member.key.type === 'PrivateIdentifier'
let decorated = false
let decorators = []
if ('decorators' in member) {
decorators = getNodeDecorators
.getNodeDecorators(member)
.map(decorator =>
getDecoratorName.getDecoratorName({ sourceCode, decorator }),
)
decorated = decorators.length > 0
}
let memberValue
let modifiers = []
let selectors = []
let addSafetySemicolonWhenInline = true
switch (member.type) {
case 'TSAbstractPropertyDefinition':
case 'PropertyDefinition':
if ('static' in member && member.static) {
modifiers.push('static')
}
if ('declare' in member && member.declare) {
modifiers.push('declare')
}
if (member.type === 'TSAbstractPropertyDefinition') {
modifiers.push('abstract')
}
if (decorated) {
modifiers.push('decorated')
}
if ('override' in member && member.override) {
modifiers.push('override')
}
if ('readonly' in member && member.readonly) {
modifiers.push('readonly')
}
if (
'accessibility' in member &&
member.accessibility === 'protected'
) {
modifiers.push('protected')
} else if (
('accessibility' in member &&
member.accessibility === 'private') ||
isPrivateHash
) {
modifiers.push('private')
} else {
modifiers.push('public')
}
if ('optional' in member && member.optional) {
modifiers.push('optional')
}
if (
((_b = member.value) == null ? void 0 : _b.type) ===
'ArrowFunctionExpression' ||
((_c = member.value) == null ? void 0 : _c.type) ===
'FunctionExpression'
) {
if (member.value.async) {
modifiers.push('async')
}
selectors.push('function-property')
} else if (member.value) {
memberValue = sourceCode.getText(member.value)
dependencies = extractDependencies(member.value, member.static)
}
selectors.push('property')
break
case 'TSAbstractMethodDefinition':
case 'MethodDefinition':
if (member.static) {
modifiers.push('static')
}
if (member.type === 'TSAbstractMethodDefinition') {
modifiers.push('abstract')
} else if (!node.parent.declare) {
addSafetySemicolonWhenInline = false
}
if (decorated) {
modifiers.push('decorated')
}
if (member.override) {
modifiers.push('override')
}
if (member.accessibility === 'protected') {
modifiers.push('protected')
} else if (member.accessibility === 'private' || isPrivateHash) {
modifiers.push('private')
} else {
modifiers.push('public')
}
if (member.optional) {
modifiers.push('optional')
}
if (member.value.async) {
modifiers.push('async')
}
if (member.kind === 'constructor') {
selectors.push('constructor')
}
if (member.kind === 'get') {
selectors.push('get-method')
}
if (member.kind === 'set') {
selectors.push('set-method')
}
selectors.push('method')
break
case 'TSAbstractAccessorProperty':
case 'AccessorProperty':
if (member.static) {
modifiers.push('static')
}
if (member.type === 'TSAbstractAccessorProperty') {
modifiers.push('abstract')
}
if (decorated) {
modifiers.push('decorated')
}
if (member.override) {
modifiers.push('override')
}
if (member.accessibility === 'protected') {
modifiers.push('protected')
} else if (member.accessibility === 'private' || isPrivateHash) {
modifiers.push('private')
} else {
modifiers.push('public')
}
selectors.push('accessor-property')
break
case 'TSIndexSignature':
if (member.static) {
modifiers.push('static')
}
if (member.readonly) {
modifiers.push('readonly')
}
selectors.push('index-signature')
break
case 'StaticBlock':
addSafetySemicolonWhenInline = false
selectors.push('static-block')
dependencies = extractDependencies(member, true)
break
}
let predefinedGroups =
generatePredefinedGroups.generatePredefinedGroups({
cache: cachedGroupsByModifiersAndSelectors,
selectors,
modifiers,
})
for (let predefinedGroup of predefinedGroups) {
defineGroup(predefinedGroup)
}
for (let customGroup of options.customGroups) {
if (
doesCustomGroupMatch.doesCustomGroupMatch({
elementValue: memberValue,
elementName: name,
customGroup,
decorators,
modifiers,
selectors,
})
) {
defineGroup(customGroup.groupName, true)
if (getGroup() === customGroup.groupName) {
break
}
}
}
let overloadSignatureGroupMemberIndex =
overloadSignatureGroups.findIndex(overloadSignatures =>
overloadSignatures.includes(member),
)
let overloadSignatureGroupMember =
(_d = overloadSignatureGroups[overloadSignatureGroupMemberIndex]) ==
null
? void 0
: _d.at(-1)
let sortingNode = {
dependencyName: getDependencyName({
nodeNameWithoutStartingHash: name.startsWith('#')
? name.slice(1)
: name,
isStatic: modifiers.includes('static'),
isPrivateHash,
}),
overloadSignaturesGroupId:
overloadSignatureGroupMemberIndex === -1
? null
: overloadSignatureGroupMemberIndex,
size: overloadSignatureGroupMember
? rangeToDiff.rangeToDiff(
overloadSignatureGroupMember,
sourceCode,
)
: rangeToDiff.rangeToDiff(member, sourceCode),
isEslintDisabled: isNodeEslintDisabled.isNodeEslintDisabled(
member,
eslintDisabledLines,
),
addSafetySemicolonWhenInline,
group: getGroup(),
node: member,
dependencies,
name,
}
let lastSortingNode =
(_e = accumulator.at(-1)) == null ? void 0 : _e.at(-1)
if (
shouldPartition.shouldPartition({
lastSortingNode,
sortingNode,
sourceCode,
options,
})
) {
accumulator.push([])
}
accumulator.at(-1).push(sortingNode)
return accumulator
},
[[]],
)
let sortNodesExcludingEslintDisabled = ignoreEslintDisabledNodes => {
let nodesSortedByGroups = formattedNodes.flatMap(nodes2 =>
sortNodesByGroups.sortNodesByGroups({
isNodeIgnored: sortingNode =>
getGroupNumber.getGroupNumber(options.groups, sortingNode) ===
options.groups.length,
getOptionsByGroupNumber:
getCustomGroupsCompareOptions.buildGetCustomGroupOverriddenOptionsFunction(
options,
),
ignoreEslintDisabledNodes,
groups: options.groups,
nodes: nodes2,
}),
)
return sortNodesByDependencies.sortNodesByDependencies(
nodesSortedByGroups,
{
ignoreEslintDisabledNodes,
},
)
}
let nodes = formattedNodes.flat()
reportAllErrors.reportAllErrors({
newlinesBetweenValueGetter: ({
computedNewlinesBetween,
right,
left,
}) => {
if (
left.overloadSignaturesGroupId !== null &&
left.overloadSignaturesGroupId === right.overloadSignaturesGroupId
) {
return 'never'
}
return computedNewlinesBetween
},
availableMessageIds: {
missedSpacingBetweenMembers: 'missedSpacingBetweenClassMembers',
extraSpacingBetweenMembers: 'extraSpacingBetweenClassMembers',
unexpectedDependencyOrder: 'unexpectedClassesDependencyOrder',
unexpectedGroupOrder: 'unexpectedClassesGroupOrder',
unexpectedOrder: 'unexpectedClassesOrder',
},
sortNodesExcludingEslintDisabled,
sourceCode,
options,
context,
nodes,
})
},
}),
meta: {
schema: [
{
properties: {
...commonJsonSchemas.commonJsonSchemas,
customGroups: commonJsonSchemas.buildCustomGroupsArrayJsonSchema({
singleCustomGroupJsonSchema: types.singleCustomGroupJsonSchema,
}),
ignoreCallbackDependenciesPatterns: commonJsonSchemas.regexJsonSchema,
partitionByComment: commonJsonSchemas.partitionByCommentJsonSchema,
partitionByNewLine: commonJsonSchemas.partitionByNewLineJsonSchema,
newlinesBetween: commonJsonSchemas.newlinesBetweenJsonSchema,
groups: commonJsonSchemas.groupsJsonSchema,
},
additionalProperties: false,
type: 'object',
},
],
messages: {
unexpectedClassesDependencyOrder: reportErrors.DEPENDENCY_ORDER_ERROR,
missedSpacingBetweenClassMembers: reportErrors.MISSED_SPACING_ERROR,
extraSpacingBetweenClassMembers: reportErrors.EXTRA_SPACING_ERROR,
unexpectedClassesGroupOrder: reportErrors.GROUP_ORDER_ERROR,
unexpectedClassesOrder: reportErrors.ORDER_ERROR,
},
docs: {
url: 'https://perfectionist.dev/rules/sort-classes',
description: 'Enforce sorted classes.',
recommended: true,
},
type: 'suggestion',
fixable: 'code',
},
defaultOptions: [defaultOptions],
name: 'sort-classes',
})
module.exports = sortClasses