eslint-plugin-perfectionist
Version:
ESLint plugin for sorting various data such as objects, imports, types, enums, JSX props, etc.
537 lines (536 loc) • 18.8 kB
JavaScript
'use strict'
const types$1 = require('@typescript-eslint/types')
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-objects/types.js')
const validateCustomSortConfiguration = require('../utils/validate-custom-sort-configuration.js')
const getFirstNodeParentWithType = require('./sort-objects/get-first-node-parent-with-type.js')
const getMatchingContextOptions = require('../utils/get-matching-context-options.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 createEslintRule = require('../utils/create-eslint-rule.js')
const reportAllErrors = require('../utils/report-all-errors.js')
const shouldPartition = require('../utils/should-partition.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 sortNodes = require('../utils/sort-nodes.js')
const complete = require('../utils/complete.js')
const matches = require('../utils/matches.js')
let cachedGroupsByModifiersAndSelectors = /* @__PURE__ */ new Map()
let defaultOptions = {
fallbackSort: { type: 'unsorted' },
partitionByNewLine: false,
partitionByComment: false,
newlinesBetween: 'ignore',
specialCharacters: 'keep',
destructuredObjects: true,
objectDeclarations: true,
styledComponents: true,
destructureOnly: false,
useConfigurationIf: {},
type: 'alphabetical',
ignorePattern: [],
ignoreCase: true,
customGroups: {},
locales: 'en-US',
alphabet: '',
order: 'asc',
groups: [],
}
const sortObjects = createEslintRule.createEslintRule({
create: context => {
let settings = getSettings.getSettings(context.settings)
let { sourceCode, id } = context
let sortObject = nodeObject => {
if (!isSortable.isSortable(nodeObject.properties)) {
return
}
let objectParent = getObjectParent({
onlyFirstParent: true,
node: nodeObject,
})
let matchedContextOptions = getMatchingContextOptions
.getMatchingContextOptions({
nodeNames: nodeObject.properties
.filter(
property =>
property.type !== 'SpreadElement' &&
property.type !== 'RestElement',
)
.map(property => getNodeName({ sourceCode, property })),
contextOptions: context.options,
})
.find(options2 => {
var _a
if (
!((_a = options2.useConfigurationIf) == null
? void 0
: _a.callingFunctionNamePattern)
) {
return true
}
if (
(objectParent == null ? void 0 : objectParent.type) ===
'VariableDeclarator' ||
!(objectParent == null ? void 0 : objectParent.name)
) {
return false
}
return matches.matches(
objectParent.name,
options2.useConfigurationIf.callingFunctionNamePattern,
)
})
let options = complete.complete(
matchedContextOptions,
settings,
defaultOptions,
)
validateCustomSortConfiguration.validateCustomSortConfiguration(options)
validateGeneratedGroupsConfiguration.validateGeneratedGroupsConfiguration(
{
selectors: types.allSelectors,
modifiers: types.allModifiers,
options,
},
)
validateNewlinesAndPartitionConfiguration.validateNewlinesAndPartitionConfiguration(
options,
)
let isDestructuredObject = nodeObject.type === 'ObjectPattern'
if (isDestructuredObject) {
if (!options.destructuredObjects) {
return
}
} else if (options.destructureOnly || !options.objectDeclarations) {
return
}
let objectParentForIgnorePattern = getObjectParent({
onlyFirstParent: false,
node: nodeObject,
})
if (
(objectParentForIgnorePattern == null
? void 0
: objectParentForIgnorePattern.name) &&
matches.matches(
objectParentForIgnorePattern.name,
options.ignorePattern,
)
) {
return
}
let isStyledCallExpression = identifier =>
identifier.type === 'Identifier' && identifier.name === 'styled'
let isCssCallExpression = identifier =>
identifier.type === 'Identifier' && identifier.name === 'css'
let isStyledComponents = styledNode =>
!!styledNode &&
((styledNode.type === 'CallExpression' &&
(isCssCallExpression(styledNode.callee) ||
(styledNode.callee.type === 'MemberExpression' &&
isStyledCallExpression(styledNode.callee.object)) ||
(styledNode.callee.type === 'CallExpression' &&
isStyledCallExpression(styledNode.callee.callee)))) ||
(styledNode.type === 'JSXExpressionContainer' &&
styledNode.parent.type === 'JSXAttribute' &&
styledNode.parent.name.name === 'style'))
if (
!options.styledComponents &&
(isStyledComponents(nodeObject.parent) ||
(nodeObject.parent.type === 'ArrowFunctionExpression' &&
isStyledComponents(nodeObject.parent.parent)))
) {
return
}
let eslintDisabledLines = getEslintDisabledLines.getEslintDisabledLines({
ruleName: id,
sourceCode,
})
let extractDependencies = init => {
let dependencies = []
let checkNode = nodeValue => {
if (
nodeValue.type === 'ArrowFunctionExpression' ||
nodeValue.type === 'FunctionExpression'
) {
return
}
if (nodeValue.type === 'Identifier') {
dependencies.push(nodeValue.name)
}
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 ('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) {
for (let argument of nodeValue.arguments) {
traverseNode(argument)
}
}
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 => {
checkNode(nodeValue)
}
traverseNode(init)
return dependencies
}
let formatProperties = props =>
props.reduce(
(accumulator, property) => {
var _a
if (
property.type === 'SpreadElement' ||
property.type === 'RestElement'
) {
accumulator.push([])
return accumulator
}
let lastSortingNode =
(_a = accumulator.at(-1)) == null ? void 0 : _a.at(-1)
let dependencies = []
let { setCustomGroups, defineGroup, getGroup } =
useGroups.useGroups(options)
let selectors = []
let modifiers = []
if (property.value.type === 'AssignmentPattern') {
dependencies = extractDependencies(property.value.right)
}
if (
property.value.type === 'ArrowFunctionExpression' ||
property.value.type === 'FunctionExpression'
) {
selectors.push('method')
} else {
selectors.push('property')
}
selectors.push('member')
if (property.loc.start.line !== property.loc.end.line) {
modifiers.push('multiline')
selectors.push('multiline')
}
let predefinedGroups =
generatePredefinedGroups.generatePredefinedGroups({
cache: cachedGroupsByModifiersAndSelectors,
selectors,
modifiers,
})
for (let predefinedGroup of predefinedGroups) {
defineGroup(predefinedGroup)
}
let name = getNodeName({ sourceCode, property })
if (Array.isArray(options.customGroups)) {
for (let customGroup of options.customGroups) {
if (
doesCustomGroupMatch.doesCustomGroupMatch({
elementValue: getNodeValue({
sourceCode,
property,
}),
elementName: name,
customGroup,
selectors,
modifiers,
})
) {
defineGroup(customGroup.groupName, true)
if (getGroup() === customGroup.groupName) {
break
}
}
}
} else {
setCustomGroups(options.customGroups, name, {
override: true,
})
}
let dependencyName = name
if (isDestructuredObject && property.value.type === 'Identifier') {
dependencyName = property.value.name
}
let sortingNode = {
isEslintDisabled: isNodeEslintDisabled.isNodeEslintDisabled(
property,
eslintDisabledLines,
),
size: rangeToDiff.rangeToDiff(property, sourceCode),
group: getGroup(),
dependencyName,
node: property,
dependencies,
name,
}
if (
shouldPartition.shouldPartition({
lastSortingNode,
sortingNode,
sourceCode,
options,
})
) {
accumulator.push([])
}
accumulator.at(-1).push(sortingNode)
return accumulator
},
[[]],
)
let formattedMembers = formatProperties(nodeObject.properties)
let sortingOptions = options
let nodesSortingFunction =
isDestructuredObject &&
typeof options.destructuredObjects === 'object' &&
!options.destructuredObjects.groups
? 'sortNodes'
: 'sortNodesByGroups'
let sortNodesExcludingEslintDisabled = ignoreEslintDisabledNodes => {
let nodesSortedByGroups = formattedMembers.flatMap(nodes2 =>
nodesSortingFunction === 'sortNodes'
? sortNodes.sortNodes({
ignoreEslintDisabledNodes,
options: sortingOptions,
nodes: nodes2,
})
: sortNodesByGroups.sortNodesByGroups({
getOptionsByGroupNumber:
getCustomGroupsCompareOptions.buildGetCustomGroupOverriddenOptionsFunction(
options,
),
ignoreEslintDisabledNodes,
groups: options.groups,
nodes: nodes2,
}),
)
return sortNodesByDependencies.sortNodesByDependencies(
nodesSortedByGroups,
{
ignoreEslintDisabledNodes,
},
)
}
let nodes = formattedMembers.flat()
reportAllErrors.reportAllErrors({
availableMessageIds: {
missedSpacingBetweenMembers: 'missedSpacingBetweenObjectMembers',
extraSpacingBetweenMembers: 'extraSpacingBetweenObjectMembers',
unexpectedDependencyOrder: 'unexpectedObjectsDependencyOrder',
unexpectedGroupOrder: 'unexpectedObjectsGroupOrder',
unexpectedOrder: 'unexpectedObjectsOrder',
},
sortNodesExcludingEslintDisabled,
sourceCode,
options,
context,
nodes,
})
}
return {
ObjectExpression: sortObject,
ObjectPattern: sortObject,
}
},
meta: {
schema: {
items: {
properties: {
...commonJsonSchemas.commonJsonSchemas,
destructuredObjects: {
oneOf: [
{
type: 'boolean',
},
{
properties: {
groups: {
description:
'Controls whether to use groups to sort destructured objects.',
type: 'boolean',
},
},
additionalProperties: false,
type: 'object',
},
],
description: 'Controls whether to sort destructured objects.',
},
customGroups: {
oneOf: [
commonJsonSchemas.customGroupsJsonSchema,
commonJsonSchemas.buildCustomGroupsArrayJsonSchema({
singleCustomGroupJsonSchema: types.singleCustomGroupJsonSchema,
}),
],
},
useConfigurationIf:
commonJsonSchemas.buildUseConfigurationIfJsonSchema({
additionalProperties: {
callingFunctionNamePattern: commonJsonSchemas.regexJsonSchema,
},
}),
destructureOnly: {
description:
'[DEPRECATED] Controls whether to sort only destructured objects.',
type: 'boolean',
},
objectDeclarations: {
description: 'Controls whether to sort object declarations.',
type: 'boolean',
},
styledComponents: {
description: 'Controls whether to sort styled components.',
type: 'boolean',
},
partitionByComment: commonJsonSchemas.partitionByCommentJsonSchema,
partitionByNewLine: commonJsonSchemas.partitionByNewLineJsonSchema,
newlinesBetween: commonJsonSchemas.newlinesBetweenJsonSchema,
ignorePattern: commonJsonSchemas.regexJsonSchema,
groups: commonJsonSchemas.groupsJsonSchema,
},
additionalProperties: false,
type: 'object',
},
uniqueItems: true,
type: 'array',
},
messages: {
unexpectedObjectsDependencyOrder: reportErrors.DEPENDENCY_ORDER_ERROR,
missedSpacingBetweenObjectMembers: reportErrors.MISSED_SPACING_ERROR,
extraSpacingBetweenObjectMembers: reportErrors.EXTRA_SPACING_ERROR,
unexpectedObjectsGroupOrder: reportErrors.GROUP_ORDER_ERROR,
unexpectedObjectsOrder: reportErrors.ORDER_ERROR,
},
docs: {
url: 'https://perfectionist.dev/rules/sort-objects',
description: 'Enforce sorted objects.',
recommended: true,
},
type: 'suggestion',
fixable: 'code',
},
defaultOptions: [defaultOptions],
name: 'sort-objects',
})
let getNodeName = ({ sourceCode, property }) => {
if (property.key.type === 'Identifier') {
return property.key.name
} else if (property.key.type === 'Literal') {
return `${property.key.value}`
}
return sourceCode.getText(property.key)
}
let getNodeValue = ({ sourceCode, property }) => {
if (
property.value.type === 'ArrowFunctionExpression' ||
property.value.type === 'FunctionExpression'
) {
return null
}
return sourceCode.getText(property.value)
}
let getObjectParent = ({ onlyFirstParent, node }) => {
let variableParentName = getVariableParentName({ onlyFirstParent, node })
if (variableParentName) {
return {
type: 'VariableDeclarator',
name: variableParentName,
}
}
let callParentName = getCallExpressionParentName({
onlyFirstParent,
node,
})
if (callParentName) {
return {
type: 'CallExpression',
name: callParentName,
}
}
return null
}
let getVariableParentName = ({ onlyFirstParent, node }) => {
let variableParent = getFirstNodeParentWithType.getFirstNodeParentWithType({
allowedTypes: [
types$1.TSESTree.AST_NODE_TYPES.VariableDeclarator,
types$1.TSESTree.AST_NODE_TYPES.Property,
],
onlyFirstParent,
node,
})
if (!variableParent) {
return null
}
let parentId
if (variableParent.type === 'VariableDeclarator') {
parentId = variableParent.id
} else if ('key' in variableParent) {
parentId = variableParent.key
} else {
return null
}
return parentId.type === 'Identifier' ? parentId.name : null
}
let getCallExpressionParentName = ({ onlyFirstParent, node }) => {
let callParent = getFirstNodeParentWithType.getFirstNodeParentWithType({
allowedTypes: [types$1.TSESTree.AST_NODE_TYPES.CallExpression],
onlyFirstParent,
node,
})
if (!callParent) {
return null
}
return callParent.callee.type === 'Identifier' ? callParent.callee.name : null
}
module.exports = sortObjects