eslint-plugin-perfectionist
Version:
ESLint plugin for sorting various data such as objects, imports, types, enums, JSX props, etc.
581 lines (580 loc) • 20.2 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 validateGeneratedGroupsConfiguration = require('../utils/validate-generated-groups-configuration.js')
const validateSideEffectsConfiguration = require('./sort-imports/validate-side-effects-configuration.js')
const validateCustomSortConfiguration = require('../utils/validate-custom-sort-configuration.js')
const getCustomGroupsCompareOptions = require('../utils/get-custom-groups-compare-options.js')
const readClosestTsConfigByPath = require('./sort-imports/read-closest-ts-config-by-path.js')
const getOptionsWithCleanGroups = require('../utils/get-options-with-clean-groups.js')
const computeCommonSelectors = require('./sort-imports/compute-common-selectors.js')
const isSideEffectOnlyGroup = require('./sort-imports/is-side-effect-only-group.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 types = require('./sort-imports/types.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 computeGroup = require('../utils/compute-group.js')
const rangeToDiff = require('../utils/range-to-diff.js')
const getSettings = require('../utils/get-settings.js')
const isSortable = require('../utils/is-sortable.js')
const complete = require('../utils/complete.js')
let cachedGroupsByModifiersAndSelectors = /* @__PURE__ */ new Map()
let defaultOptions = {
groups: [
'type-import',
['value-builtin', 'value-external'],
'type-internal',
'value-internal',
['type-parent', 'type-sibling', 'type-index'],
['value-parent', 'value-sibling', 'value-index'],
'ts-equals-import',
'unknown',
],
internalPattern: ['^~/.+', '^@/.+'],
fallbackSort: { type: 'unsorted' },
partitionByComment: false,
partitionByNewLine: false,
specialCharacters: 'keep',
sortSideEffects: false,
type: 'alphabetical',
environment: 'node',
newlinesBetween: 1,
customGroups: [],
ignoreCase: true,
locales: 'en-US',
alphabet: '',
order: 'asc',
}
const sortImports = createEslintRule.createEslintRule({
create: context => {
var _a, _b
let settings = getSettings.getSettings(context.settings)
let userOptions = context.options.at(0)
let options = getOptionsWithCleanGroups.getOptionsWithCleanGroups(
complete.complete(userOptions, settings, defaultOptions),
)
validateGeneratedGroupsConfiguration.validateGeneratedGroupsConfiguration({
options: {
...options,
customGroups: Array.isArray(options.customGroups)
? options.customGroups
: {
...options.customGroups.type,
...options.customGroups.value,
},
},
selectors: [...types.allSelectors, ...types.allDeprecatedSelectors],
modifiers: types.allModifiers,
})
validateCustomSortConfiguration.validateCustomSortConfiguration(options)
validateNewlinesAndPartitionConfiguration.validateNewlinesAndPartitionConfiguration(
options,
)
validateSideEffectsConfiguration.validateSideEffectsConfiguration(options)
let tsconfigRootDirectory =
((_a = options.tsconfig) == null ? void 0 : _a.rootDir) ??
options.tsconfigRootDir
let tsConfigOutput = tsconfigRootDirectory
? readClosestTsConfigByPath.readClosestTsConfigByPath({
tsconfigFilename:
((_b = options.tsconfig) == null ? void 0 : _b.filename) ??
'tsconfig.json',
tsconfigRootDir: tsconfigRootDirectory,
filePath: context.physicalFilename,
contextCwd: context.cwd,
})
: null
let { sourceCode, filename, id } = context
let eslintDisabledLines = getEslintDisabledLines.getEslintDisabledLines({
ruleName: id,
sourceCode,
})
let sortingNodesWithoutPartitionId = []
let flatGroups = new Set(options.groups.flat())
let shouldRegroupSideEffectNodes = flatGroups.has('side-effect')
let shouldRegroupSideEffectStyleNodes = flatGroups.has('side-effect-style')
let registerNode = node => {
let name = getNodeName({
sourceCode,
node,
})
let commonSelectors = computeCommonSelectors.computeCommonSelectors({
tsConfigOutput,
filename,
options,
name,
})
let selectors = []
let modifiers = []
let group = null
if (node.type !== 'VariableDeclaration' && node.importKind === 'type') {
if (node.type === 'ImportDeclaration') {
if (!Array.isArray(options.customGroups)) {
group = computeGroupExceptUnknown({
customGroups: options.customGroups.type,
options,
name,
})
}
for (let selector of commonSelectors) {
if (selector !== 'subpath' && selector !== 'tsconfig-path') {
selectors.push(`${selector}-type`)
}
}
}
selectors.push('type')
modifiers.push('type')
if (!group && !Array.isArray(options.customGroups)) {
group = computeGroupExceptUnknown({
customGroups: [],
selectors,
modifiers,
options,
name,
})
}
}
let isSideEffect = isSideEffectImport({ sourceCode, node })
let isStyleValue = isStyle(name)
let isStyleSideEffect = isSideEffect && isStyleValue
if (!group && !Array.isArray(options.customGroups)) {
group = computeGroupExceptUnknown({
customGroups: options.customGroups.value,
options,
name,
})
}
if (!isNonExternalReferenceTsImportEquals(node)) {
if (isStyleSideEffect) {
selectors.push('side-effect-style')
}
if (isSideEffect) {
selectors.push('side-effect')
modifiers.push('side-effect')
}
if (isStyleValue) {
selectors.push('style')
}
for (let selector of commonSelectors) {
selectors.push(selector)
}
}
selectors.push('import')
if (!modifiers.includes('type')) {
modifiers.push('value')
}
if (node.type === 'TSImportEqualsDeclaration') {
modifiers.push('ts-equals')
}
if (node.type === 'VariableDeclaration') {
modifiers.push('require')
}
if (hasSpecifier(node, 'ImportDefaultSpecifier')) {
modifiers.push('default')
}
if (hasSpecifier(node, 'ImportNamespaceSpecifier')) {
modifiers.push('wildcard')
}
if (hasSpecifier(node, 'ImportSpecifier')) {
modifiers.push('named')
}
group ??
(group =
computeGroupExceptUnknown({
customGroups: Array.isArray(options.customGroups)
? options.customGroups
: [],
selectors,
modifiers,
options,
name,
}) ?? 'unknown')
let hasMultipleImportDeclarations = isSortable.isSortable(node.specifiers)
let size = rangeToDiff.rangeToDiff(node, sourceCode)
if (
hasMultipleImportDeclarations &&
options.maxLineLength &&
size > options.maxLineLength
) {
size = name.length + 10
}
sortingNodesWithoutPartitionId.push({
isIgnored:
!options.sortSideEffects &&
isSideEffect &&
!shouldRegroupSideEffectNodes &&
(!isStyleSideEffect || !shouldRegroupSideEffectStyleNodes),
isEslintDisabled: isNodeEslintDisabled.isNodeEslintDisabled(
node,
eslintDisabledLines,
),
dependencyNames: computeDependencyNames({ sourceCode, node }),
dependencies: computeDependencies(node),
addSafetySemicolonWhenInline: true,
group,
size,
name,
node,
})
}
return {
'Program:exit': () => {
let contentSeparatedSortingNodeGroups = [[[]]]
for (let sortingNodeWithoutPartitionId of sortingNodesWithoutPartitionId) {
let lastGroupWithNoContentBetween =
contentSeparatedSortingNodeGroups.at(-1)
let lastGroup = lastGroupWithNoContentBetween.at(-1)
let lastSortingNode = lastGroup.at(-1)
if (
lastSortingNode &&
hasContentBetweenNodes(
sourceCode,
lastSortingNode,
sortingNodeWithoutPartitionId,
)
) {
lastGroup = []
lastGroupWithNoContentBetween = [lastGroup]
contentSeparatedSortingNodeGroups.push(
lastGroupWithNoContentBetween,
)
} else if (
shouldPartition.shouldPartition({
sortingNode: sortingNodeWithoutPartitionId,
lastSortingNode,
sourceCode,
options,
})
) {
lastGroup = []
lastGroupWithNoContentBetween.push(lastGroup)
}
lastGroup.push({
...sortingNodeWithoutPartitionId,
partitionId: lastGroupWithNoContentBetween.length,
})
}
for (let sortingNodeGroups of contentSeparatedSortingNodeGroups) {
let sortNodesExcludingEslintDisabled = ignoreEslintDisabledNodes => {
let nodesSortedByGroups = sortingNodeGroups.flatMap(nodes2 =>
sortNodesByGroups.sortNodesByGroups({
getOptionsByGroupIndex: groupIndex => {
let customGroupOverriddenOptions =
getCustomGroupsCompareOptions.getCustomGroupOverriddenOptions(
{
options: {
...options,
customGroups: Array.isArray(options.customGroups)
? options.customGroups
: [],
},
groupIndex,
},
)
if (options.sortSideEffects) {
return {
options: {
...options,
...customGroupOverriddenOptions,
},
}
}
let overriddenOptions = {
...options,
...customGroupOverriddenOptions,
}
return {
options: {
...overriddenOptions,
type:
overriddenOptions.groups[groupIndex] &&
isSideEffectOnlyGroup.isSideEffectOnlyGroup(
overriddenOptions.groups[groupIndex],
)
? 'unsorted'
: overriddenOptions.type,
},
}
},
isNodeIgnored: node => node.isIgnored,
ignoreEslintDisabledNodes,
groups: options.groups,
nodes: nodes2,
}),
)
return sortNodesByDependencies.sortNodesByDependencies(
nodesSortedByGroups,
{
ignoreEslintDisabledNodes,
},
)
}
let nodes = sortingNodeGroups.flat()
reportAllErrors.reportAllErrors({
availableMessageIds: {
unexpectedDependencyOrder: 'unexpectedImportsDependencyOrder',
missedSpacingBetweenMembers: 'missedSpacingBetweenImports',
extraSpacingBetweenMembers: 'extraSpacingBetweenImports',
unexpectedGroupOrder: 'unexpectedImportsGroupOrder',
missedCommentAbove: 'missedCommentAboveImport',
unexpectedOrder: 'unexpectedImportsOrder',
},
options: {
...options,
customGroups: Array.isArray(options.customGroups)
? options.customGroups
: [],
},
sortNodesExcludingEslintDisabled,
sourceCode,
context,
nodes,
})
}
},
VariableDeclaration: node => {
var _a2
if (
node.declarations[0].init &&
node.declarations[0].init.type === 'CallExpression' &&
node.declarations[0].init.callee.type === 'Identifier' &&
node.declarations[0].init.callee.name === 'require' &&
((_a2 = node.declarations[0].init.arguments[0]) == null
? void 0
: _a2.type) === 'Literal'
) {
registerNode(node)
}
},
TSImportEqualsDeclaration: registerNode,
ImportDeclaration: registerNode,
}
},
meta: {
schema: {
items: {
properties: {
...commonJsonSchemas.commonJsonSchemas,
customGroups: {
oneOf: [
{
properties: {
value: {
...commonJsonSchemas.deprecatedCustomGroupsJsonSchema,
description: 'Specifies custom groups for value imports.',
},
type: {
...commonJsonSchemas.deprecatedCustomGroupsJsonSchema,
description: 'Specifies custom groups for type imports.',
},
},
description: 'Specifies custom groups.',
additionalProperties: false,
type: 'object',
},
commonJsonSchemas.buildCustomGroupsArrayJsonSchema({
singleCustomGroupJsonSchema: types.singleCustomGroupJsonSchema,
}),
],
},
tsconfig: {
properties: {
rootDir: {
description: 'Specifies the tsConfig root directory.',
type: 'string',
},
filename: {
description: 'Specifies the tsConfig filename.',
type: 'string',
},
},
additionalProperties: false,
required: ['rootDir'],
type: 'object',
},
maxLineLength: {
description: 'Specifies the maximum line length.',
exclusiveMinimum: true,
type: 'integer',
minimum: 0,
},
sortSideEffects: {
description:
'Controls whether side-effect imports should be sorted.',
type: 'boolean',
},
environment: {
description: 'Specifies the environment.',
enum: ['node', 'bun'],
type: 'string',
},
tsconfigRootDir: {
description: 'Specifies the tsConfig root directory.',
type: 'string',
},
partitionByComment: commonJsonSchemas.partitionByCommentJsonSchema,
partitionByNewLine: commonJsonSchemas.partitionByNewLineJsonSchema,
newlinesBetween: commonJsonSchemas.newlinesBetweenJsonSchema,
internalPattern: commonJsonSchemas.regexJsonSchema,
groups: commonJsonSchemas.groupsJsonSchema,
},
additionalProperties: false,
type: 'object',
},
uniqueItems: true,
type: 'array',
},
messages: {
unexpectedImportsDependencyOrder: reportErrors.DEPENDENCY_ORDER_ERROR,
missedCommentAboveImport: reportErrors.MISSED_COMMENT_ABOVE_ERROR,
missedSpacingBetweenImports: reportErrors.MISSED_SPACING_ERROR,
extraSpacingBetweenImports: reportErrors.EXTRA_SPACING_ERROR,
unexpectedImportsGroupOrder: reportErrors.GROUP_ORDER_ERROR,
unexpectedImportsOrder: reportErrors.ORDER_ERROR,
},
docs: {
url: 'https://perfectionist.dev/rules/sort-imports',
description: 'Enforce sorted imports.',
recommended: true,
},
type: 'suggestion',
fixable: 'code',
},
defaultOptions: [defaultOptions],
name: 'sort-imports',
})
let hasContentBetweenNodes = (sourceCode, left, right) =>
sourceCode.getTokensBetween(left.node, right.node, {
includeComments: false,
}).length > 0
let hasSpecifier = (node, specifier) =>
node.type === 'ImportDeclaration' &&
node.specifiers.some(nodeSpecifier => nodeSpecifier.type === specifier)
let styleExtensions = [
'.less',
'.scss',
'.sass',
'.styl',
'.pcss',
'.css',
'.sss',
]
let isStyle = value => {
let [cleanedValue] = value.split('?')
return styleExtensions.some(extension =>
cleanedValue == null ? void 0 : cleanedValue.endsWith(extension),
)
}
let isSideEffectImport = ({ sourceCode, node }) =>
node.type === 'ImportDeclaration' &&
node.specifiers.length ===
0 /* Avoid matching on named imports without specifiers */ &&
!/\}\s*from\s+/u.test(sourceCode.getText(node))
let getNodeName = ({ sourceCode, node }) => {
if (node.type === 'ImportDeclaration') {
return node.source.value
}
if (node.type === 'TSImportEqualsDeclaration') {
if (node.moduleReference.type === 'TSExternalModuleReference') {
return node.moduleReference.expression.value
}
return sourceCode.getText(node.moduleReference)
}
let callExpression = node.declarations[0].init
let { value } = callExpression.arguments[0]
return value.toString()
}
let computeGroupExceptUnknown = ({
customGroups,
selectors,
modifiers,
options,
name,
}) => {
let predefinedGroups =
modifiers && selectors
? generatePredefinedGroups.generatePredefinedGroups({
cache: cachedGroupsByModifiersAndSelectors,
selectors,
modifiers,
})
: []
let computedCustomGroup = computeGroup.computeGroup({
customGroupMatcher: customGroup =>
doesCustomGroupMatch.doesCustomGroupMatch({
modifiers,
selectors,
elementName: name,
customGroup,
}),
options: {
...options,
customGroups,
},
predefinedGroups,
name,
})
if (computedCustomGroup === 'unknown') {
return null
}
return computedCustomGroup
}
let computeDependencies = node => {
if (node.type !== 'TSImportEqualsDeclaration') {
return []
}
if (node.moduleReference.type !== 'TSQualifiedName') {
return []
}
let qualifiedName = getQualifiedNameDependencyName(node.moduleReference)
if (!qualifiedName) {
return []
}
return [qualifiedName]
}
let getQualifiedNameDependencyName = node => {
switch (node.type) {
case 'TSQualifiedName':
return getQualifiedNameDependencyName(node.left)
case 'Identifier':
return node.name
}
return null
}
let computeDependencyNames = ({ sourceCode, node }) => {
if (node.type === 'VariableDeclaration') {
return []
}
if (node.type === 'TSImportEqualsDeclaration') {
return [node.id.name]
}
let returnValue = []
for (let specifier of node.specifiers) {
switch (specifier.type) {
case 'ImportNamespaceSpecifier':
returnValue.push(sourceCode.getText(specifier.local))
break
case 'ImportDefaultSpecifier':
returnValue.push(sourceCode.getText(specifier.local))
break
case 'ImportSpecifier':
returnValue.push(sourceCode.getText(specifier.imported))
break
}
}
return returnValue
}
let isNonExternalReferenceTsImportEquals = node => {
if (node.type !== 'TSImportEqualsDeclaration') {
return false
}
return node.moduleReference.type !== 'TSExternalModuleReference'
}
module.exports = sortImports