UNPKG

eslint-plugin-perfectionist

Version:

ESLint plugin for sorting various data such as objects, imports, types, enums, JSX props, etc.

458 lines (457 loc) 16.3 kB
import { buildCommonJsonSchemas, buildRegexJsonSchema, useExperimentalDependencyDetectionJsonSchema, } from '../utils/json-schemas/common-json-schemas.js' import { buildCommonGroupsJsonSchemas } from '../utils/json-schemas/common-groups-json-schemas.js' import { DEPENDENCY_ORDER_ERROR, EXTRA_SPACING_ERROR, GROUP_ORDER_ERROR, MISSED_COMMENT_ABOVE_ERROR, MISSED_SPACING_ERROR, ORDER_ERROR, } from '../utils/report-errors.js' import { partitionByCommentJsonSchema, partitionByNewLineJsonSchema, } from '../utils/json-schemas/common-partition-json-schemas.js' import { computeDependenciesBySortingNode } from '../utils/compute-dependencies-by-sorting-node.js' import { populateSortingNodeGroupsWithDependencies } from '../utils/populate-sorting-node-groups-with-dependencies.js' import { validateNewlinesAndPartitionConfiguration } from '../utils/validate-newlines-and-partition-configuration.js' import { buildOptionsByGroupIndexComputer } from '../utils/build-options-by-group-index-computer.js' import { validateCustomSortConfiguration } from '../utils/validate-custom-sort-configuration.js' import { validateGroupsConfiguration } from '../utils/validate-groups-configuration.js' import { generatePredefinedGroups } from '../utils/generate-predefined-groups.js' import { sortNodesByDependencies } from '../utils/sort-nodes-by-dependencies.js' import { getEslintDisabledLines } from '../utils/get-eslint-disabled-lines.js' import { doesCustomGroupMatch } from '../utils/does-custom-group-match.js' import { isNodeEslintDisabled } from '../utils/is-node-eslint-disabled.js' import { sortNodesByGroups } from '../utils/sort-nodes-by-groups.js' import { reportAllErrors } from '../utils/report-all-errors.js' import { shouldPartition } from '../utils/should-partition.js' import { computeGroup } from '../utils/compute-group.js' import { rangeToDiff } from '../utils/range-to-diff.js' import { getSettings } from '../utils/get-settings.js' import { isSortable } from '../utils/is-sortable.js' import { complete } from '../utils/complete.js' import { createEslintRule } from '../utils/create-eslint-rule.js' import { isNodeOnSingleLine } from '../utils/is-node-on-single-line.js' import { TYPE_IMPORT_FIRST_TYPE_OPTION, additionalCustomGroupMatchOptionsJsonSchema, additionalSortOptionsJsonSchema, allModifiers, allSelectors, } from './sort-imports/types.js' import { isNonExternalReferenceTsImportEquals } from './sort-imports/is-non-external-reference-ts-import-equals.js' import { isSideEffectOnlyGroup } from './sort-imports/is-side-effect-only-group.js' import { validateSideEffectsConfiguration } from './sort-imports/validate-side-effects-configuration.js' import { comparatorByOptionsComputer } from './sort-imports/comparator-by-options-computer.js' import { readClosestTsConfigByPath } from './sort-imports/read-closest-ts-config-by-path.js' import { computeSpecifierModifiers } from './sort-imports/compute-specifier-modifiers.js' import { getOptionsWithCleanGroups } from '../utils/get-options-with-clean-groups.js' import { computeCommonSelectors } from './sort-imports/compute-common-selectors.js' import { computeDependencyNames } from './sort-imports/compute-dependency-names.js' import { computeSpecifierName } from './sort-imports/compute-specifier-name.js' import { computeDependencies } from './sort-imports/compute-dependencies.js' import { isSideEffectImport } from './sort-imports/is-side-effect-import.js' import { computeNodeName } from './sort-imports/compute-node-name.js' import { AST_NODE_TYPES } from '@typescript-eslint/utils' /** * Cache computed groups by modifiers and selectors for performance. */ var cachedGroupsByModifiersAndSelectors = /* @__PURE__ */ new Map() var ORDER_ERROR_ID = 'unexpectedImportsOrder' var GROUP_ORDER_ERROR_ID = 'unexpectedImportsGroupOrder' var EXTRA_SPACING_ERROR_ID = 'extraSpacingBetweenImports' var MISSED_SPACING_ERROR_ID = 'missedSpacingBetweenImports' var MISSED_COMMENT_ABOVE_ERROR_ID = 'missedCommentAboveImport' var DEPENDENCY_ORDER_ERROR_ID = 'unexpectedImportsDependencyOrder' var 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: ['^~/.+', '^@/.+', '^#.+'], useExperimentalDependencyDetection: true, fallbackSort: { type: 'unsorted' }, partitionByComment: false, partitionByNewLine: false, specialCharacters: 'keep', tsconfig: { rootDir: '' }, maxLineLength: Infinity, sortSideEffects: false, type: 'alphabetical', environment: 'node', newlinesBetween: 1, newlinesInside: 0, customGroups: [], ignoreCase: true, locales: 'en-US', sortBy: 'path', alphabet: '', order: 'asc', } var sort_imports_default = createEslintRule({ create: context => { let settings = getSettings(context.settings) let options = getOptionsWithCleanGroups( complete(context.options.at(0), settings, defaultOptions), ) validateGroupsConfiguration({ selectors: allSelectors, modifiers: allModifiers, options, }) validateCustomSortConfiguration(options) validateNewlinesAndPartitionConfiguration(options) validateSideEffectsConfiguration(options) let tsconfigRootDirectory = options.tsconfig.rootDir let tsConfigOutput = tsconfigRootDirectory ? readClosestTsConfigByPath({ tsconfigFilename: options.tsconfig.filename ?? 'tsconfig.json', tsconfigRootDir: tsconfigRootDirectory, filePath: context.physicalFilename, contextCwd: context.cwd, }) : null let { sourceCode, filename, id } = context let eslintDisabledLines = 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') function registerNode(node) { let name = computeNodeName({ sourceCode, node, }) let commonSelectors = computeCommonSelectors({ tsConfigOutput, filename, options, name, }) let selectors = [] let modifiers = [] let group = null if ( node.type !== AST_NODE_TYPES.VariableDeclaration && node.importKind === 'type' ) { selectors.push('type') modifiers.push('type') } let isSideEffect = isSideEffectImport({ sourceCode, node, }) let isStyleValue = isStyle(name) let isStyleSideEffect = isSideEffect && isStyleValue 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 === AST_NODE_TYPES.TSImportEqualsDeclaration) { modifiers.push('ts-equals') } if (node.type === AST_NODE_TYPES.VariableDeclaration) { modifiers.push('require') } modifiers.push(...computeSpecifierModifiers(node)) if (isNodeOnSingleLine(node)) { modifiers.push('singleline') } else { modifiers.push('multiline') } group ??= computeGroupExceptUnknown({ selectors, modifiers, options, name, }) ?? 'unknown' let hasMultipleImportDeclarations = node.type === AST_NODE_TYPES.ImportDeclaration && isSortable(node.specifiers) let size = rangeToDiff(node, sourceCode) if (hasMultipleImportDeclarations && size > options.maxLineLength) { size = name.length + 10 } sortingNodesWithoutPartitionId.push({ isIgnored: !options.sortSideEffects && isSideEffect && !shouldRegroupSideEffectNodes && (!isStyleSideEffect || !shouldRegroupSideEffectStyleNodes), dependencies: options.useExperimentalDependencyDetection ? [] : computeDependencies(node), isEslintDisabled: isNodeEslintDisabled(node, eslintDisabledLines), dependencyNames: computeDependencyNames({ sourceCode, node, }), specifierName: computeSpecifierName({ sourceCode, node, }), isTypeImport: modifiers.includes('type'), addSafetySemicolonWhenInline: true, group, size, name, node, }) } return { VariableDeclaration: node => { if ( node.declarations[0].init?.type === AST_NODE_TYPES.CallExpression && node.declarations[0].init.callee.type === AST_NODE_TYPES.Identifier && node.declarations[0].init.callee.name === 'require' && node.declarations[0].init.arguments[0]?.type === AST_NODE_TYPES.Literal ) { registerNode(node) } }, 'Program:exit': () => { sortImportNodes({ sortingNodesWithoutPartitionId, context, options, }) }, TSImportEqualsDeclaration: registerNode, ImportDeclaration: registerNode, } }, meta: { schema: { items: { properties: { ...buildCommonJsonSchemas({ allowedAdditionalTypeValues: [TYPE_IMPORT_FIRST_TYPE_OPTION], additionalSortProperties: additionalSortOptionsJsonSchema, }), ...buildCommonGroupsJsonSchemas({ additionalCustomGroupMatchProperties: additionalCustomGroupMatchOptionsJsonSchema, allowedAdditionalTypeValues: [TYPE_IMPORT_FIRST_TYPE_OPTION], additionalSortProperties: additionalSortOptionsJsonSchema, }), 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', }, useExperimentalDependencyDetection: useExperimentalDependencyDetectionJsonSchema, partitionByComment: partitionByCommentJsonSchema, partitionByNewLine: partitionByNewLineJsonSchema, internalPattern: buildRegexJsonSchema(), }, additionalProperties: false, type: 'object', }, uniqueItems: true, type: 'array', }, messages: { [MISSED_COMMENT_ABOVE_ERROR_ID]: MISSED_COMMENT_ABOVE_ERROR, [DEPENDENCY_ORDER_ERROR_ID]: DEPENDENCY_ORDER_ERROR, [MISSED_SPACING_ERROR_ID]: MISSED_SPACING_ERROR, [EXTRA_SPACING_ERROR_ID]: EXTRA_SPACING_ERROR, [GROUP_ORDER_ERROR_ID]: GROUP_ORDER_ERROR, [ORDER_ERROR_ID]: 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', }) function sortImportNodes({ sortingNodesWithoutPartitionId, options, context }) { let { sourceCode } = context let optionsByGroupIndexComputer = buildOptionsByGroupIndexComputer(options) 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(lastSortingNode, sortingNodeWithoutPartitionId) ) { lastGroup = [] lastGroupWithNoContentBetween = [lastGroup] contentSeparatedSortingNodeGroups.push(lastGroupWithNoContentBetween) } else if ( shouldPartition({ sortingNode: sortingNodeWithoutPartitionId, lastSortingNode, sourceCode, options, }) ) { lastGroup = [] lastGroupWithNoContentBetween.push(lastGroup) } lastGroup.push({ ...sortingNodeWithoutPartitionId, partitionId: lastGroupWithNoContentBetween.length, }) } for (let contentSeparatedSortingNodeGroup of contentSeparatedSortingNodeGroups) { let sortingNodeGroups = [...contentSeparatedSortingNodeGroup] if (options.useExperimentalDependencyDetection) { sortingNodeGroups = populateSortingNodeGroupsWithDependencies({ dependenciesBySortingNode: computeDependenciesBySortingNode({ sortingNodes: sortingNodeGroups.flat(), sourceCode, }), sortingNodeGroups, }) } let sortingNodes = sortingNodeGroups.flat() reportAllErrors({ availableMessageIds: { unexpectedDependencyOrder: DEPENDENCY_ORDER_ERROR_ID, missedSpacingBetweenMembers: MISSED_SPACING_ERROR_ID, extraSpacingBetweenMembers: EXTRA_SPACING_ERROR_ID, missedCommentAbove: MISSED_COMMENT_ABOVE_ERROR_ID, unexpectedGroupOrder: GROUP_ORDER_ERROR_ID, unexpectedOrder: ORDER_ERROR_ID, }, sortNodesExcludingEslintDisabled: createSortNodesExcludingEslintDisabled(sortingNodeGroups), nodes: sortingNodes, options, context, }) } function createSortNodesExcludingEslintDisabled(nodeGroups) { return function (ignoreEslintDisabledNodes) { return sortNodesByDependencies( nodeGroups.flatMap(nodes => sortNodesByGroups({ isNodeIgnoredForGroup: ({ groupIndex }) => { if (options.sortSideEffects) { return false } return isSideEffectOnlyGroup(options.groups[groupIndex]) }, isNodeIgnored: node => node.isIgnored, optionsByGroupIndexComputer, comparatorByOptionsComputer, ignoreEslintDisabledNodes, groups: options.groups, nodes, }), ), { ignoreEslintDisabledNodes }, ) } } function hasContentBetweenNodes(left, right) { return ( sourceCode.getTokensBetween(left.node, right.node, { includeComments: false, }).length > 0 ) } } function computeGroupExceptUnknown({ selectors, modifiers, options, name }) { let computedCustomGroup = computeGroup({ customGroupMatcher: customGroup => doesCustomGroupMatch({ elementName: name, customGroup, modifiers, selectors, }), predefinedGroups: generatePredefinedGroups({ cache: cachedGroupsByModifiersAndSelectors, selectors, modifiers, }), options, }) if (computedCustomGroup === 'unknown') { return null } return computedCustomGroup } var styleExtensions = [ '.less', '.scss', '.sass', '.styl', '.pcss', '.css', '.sss', ] function isStyle(value) { let [cleanedValue] = value.split('?') return styleExtensions.some(extension => cleanedValue?.endsWith(extension)) } export { sort_imports_default as default }