UNPKG

eslint-plugin-perfectionist

Version:

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

448 lines (447 loc) 14.3 kB
'use strict' const utils = require('@typescript-eslint/utils') 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-modules/types.js') const validateCustomSortConfiguration = require('../utils/validate-custom-sort-configuration.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 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 cachedGroupsByModifiersAndSelectors = /* @__PURE__ */ new Map() let defaultOptions = { groups: [ 'declare-enum', 'export-enum', 'enum', ['declare-interface', 'declare-type'], ['export-interface', 'export-type'], ['interface', 'type'], 'declare-class', 'class', 'export-class', 'declare-function', 'export-function', 'function', ], fallbackSort: { type: 'unsorted' }, partitionByComment: false, partitionByNewLine: false, newlinesBetween: 'ignore', specialCharacters: 'keep', type: 'alphabetical', ignoreCase: true, customGroups: [], locales: 'en-US', alphabet: '', order: 'asc', } const sortModules = createEslintRule.createEslintRule({ meta: { schema: [ { properties: { ...commonJsonSchemas.commonJsonSchemas, customGroups: commonJsonSchemas.buildCustomGroupsArrayJsonSchema({ singleCustomGroupJsonSchema: types.singleCustomGroupJsonSchema, }), partitionByComment: commonJsonSchemas.partitionByCommentJsonSchema, partitionByNewLine: commonJsonSchemas.partitionByNewLineJsonSchema, newlinesBetween: commonJsonSchemas.newlinesBetweenJsonSchema, groups: commonJsonSchemas.groupsJsonSchema, }, additionalProperties: false, type: 'object', }, ], messages: { unexpectedModulesDependencyOrder: reportErrors.DEPENDENCY_ORDER_ERROR, missedSpacingBetweenModulesMembers: reportErrors.MISSED_SPACING_ERROR, extraSpacingBetweenModulesMembers: reportErrors.EXTRA_SPACING_ERROR, unexpectedModulesGroupOrder: reportErrors.GROUP_ORDER_ERROR, unexpectedModulesOrder: reportErrors.ORDER_ERROR, }, docs: { url: 'https://perfectionist.dev/rules/sort-modules', description: 'Enforce sorted modules.', recommended: true, }, type: 'suggestion', fixable: 'code', }, create: context => { 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, }) return { Program: program => { if (isSortable.isSortable(program.body)) { return analyzeModule({ eslintDisabledLines, sourceCode, options, program, context, }) } }, } }, defaultOptions: [defaultOptions], name: 'sort-modules', }) let analyzeModule = ({ eslintDisabledLines, sourceCode, options, program, context, }) => { var _a, _b let formattedNodes = [[]] for (let node of program.body) { let selector let name let modifiers = [] let dependencies = [] let decorators = [] let addSafetySemicolonWhenInline = false let parseNode = nodeToParse => { var _a2, _b2 if ('declare' in nodeToParse && nodeToParse.declare) { modifiers.push('declare') } switch (nodeToParse.type) { case utils.AST_NODE_TYPES.ExportDefaultDeclaration: modifiers.push('default', 'export') parseNode(nodeToParse.declaration) break case utils.AST_NODE_TYPES.ExportNamedDeclaration: if (nodeToParse.declaration) { parseNode(nodeToParse.declaration) } modifiers.push('export') break case utils.AST_NODE_TYPES.TSInterfaceDeclaration: selector = 'interface' ;({ name } = nodeToParse.id) break case utils.AST_NODE_TYPES.TSTypeAliasDeclaration: selector = 'type' ;({ name } = nodeToParse.id) addSafetySemicolonWhenInline = true break case utils.AST_NODE_TYPES.FunctionDeclaration: case utils.AST_NODE_TYPES.TSDeclareFunction: selector = 'function' if (nodeToParse.async) { modifiers.push('async') } if (modifiers.includes('declare')) { addSafetySemicolonWhenInline = true } name = (_a2 = nodeToParse.id) == null ? void 0 : _a2.name break case utils.AST_NODE_TYPES.TSModuleDeclaration: formattedNodes.push([]) if (nodeToParse.body) { analyzeModule({ program: nodeToParse.body, eslintDisabledLines, sourceCode, options, context, }) } break case utils.AST_NODE_TYPES.VariableDeclaration: case utils.AST_NODE_TYPES.ExpressionStatement: formattedNodes.push([]) break case utils.AST_NODE_TYPES.TSEnumDeclaration: selector = 'enum' ;({ name } = nodeToParse.id) dependencies = [ ...dependencies, ...getEnumMembers .getEnumMembers(nodeToParse) .flatMap(extractDependencies), ] break case utils.AST_NODE_TYPES.ClassDeclaration: selector = 'class' name = (_b2 = nodeToParse.id) == null ? void 0 : _b2.name let nodeDecorators = getNodeDecorators.getNodeDecorators(nodeToParse) if (nodeDecorators.length > 0) { modifiers.push('decorated') } decorators = nodeDecorators.map(decorator => getDecoratorName.getDecoratorName({ sourceCode, decorator, }), ) dependencies = [ ...dependencies, ...(nodeToParse.superClass && 'name' in nodeToParse.superClass ? [nodeToParse.superClass.name] : []), ...extractDependencies(nodeToParse.body), ] break } } parseNode(node) if (!selector || !name) { continue } if ( selector === 'class' && modifiers.includes('export') && modifiers.includes('decorated') ) { continue } let { defineGroup, getGroup } = useGroups.useGroups(options) for (let predefinedGroup of generatePredefinedGroups.generatePredefinedGroups( { cache: cachedGroupsByModifiersAndSelectors, selectors: [selector], modifiers, }, )) { defineGroup(predefinedGroup) } for (let customGroup of options.customGroups) { if ( doesCustomGroupMatch.doesCustomGroupMatch({ selectors: [selector], elementName: name, customGroup, decorators, modifiers, }) ) { defineGroup(customGroup.groupName, true) if (getGroup() === customGroup.groupName) { break } } } let sortingNode = { isEslintDisabled: isNodeEslintDisabled.isNodeEslintDisabled( node, eslintDisabledLines, ), size: rangeToDiff.rangeToDiff(node, sourceCode), addSafetySemicolonWhenInline, dependencyName: name, group: getGroup(), dependencies, name, node, } let lastSortingNode = (_a = formattedNodes.at(-1)) == null ? void 0 : _a.at(-1) if ( shouldPartition.shouldPartition({ lastSortingNode, sortingNode, sourceCode, options, }) ) { formattedNodes.push([]) } ;(_b = formattedNodes.at(-1)) == null ? void 0 : _b.push(sortingNode) } 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({ availableMessageIds: { missedSpacingBetweenMembers: 'missedSpacingBetweenModulesMembers', extraSpacingBetweenMembers: 'extraSpacingBetweenModulesMembers', unexpectedDependencyOrder: 'unexpectedModulesDependencyOrder', unexpectedGroupOrder: 'unexpectedModulesGroupOrder', unexpectedOrder: 'unexpectedModulesOrder', }, sortNodesExcludingEslintDisabled, sourceCode, options, context, nodes, }) } let extractDependencies = expression => { let dependencies = [] let isPropertyOrAccessor = node => node.type === 'PropertyDefinition' || node.type === 'AccessorProperty' let isArrowFunction = node => isPropertyOrAccessor(node) && node.value !== null && node.value.type === 'ArrowFunctionExpression' let searchStaticMethodsAndFunctionProperties = expression.type === 'ClassBody' && expression.body.some( classElement => classElement.type === 'StaticBlock' || (classElement.static && isPropertyOrAccessor(classElement) && !isArrowFunction(classElement)), ) let checkNode = nodeValue => { if ( (nodeValue.type === 'MethodDefinition' || isArrowFunction(nodeValue)) && (!nodeValue.static || !searchStaticMethodsAndFunctionProperties) ) { return } if ( nodeValue.type === 'NewExpression' && nodeValue.callee.type === 'Identifier' ) { dependencies.push(nodeValue.callee.name) } if (nodeValue.type === 'Identifier') { dependencies.push(nodeValue.name) } if (nodeValue.type === 'ConditionalExpression') { checkNode(nodeValue.test) checkNode(nodeValue.consequent) checkNode(nodeValue.alternate) } if ( 'expression' in nodeValue && typeof nodeValue.expression !== 'boolean' ) { checkNode(nodeValue.expression) } if ('object' in nodeValue) { checkNode(nodeValue.object) } if ('callee' in nodeValue) { checkNode(nodeValue.callee) } if ('init' in nodeValue && nodeValue.init) { checkNode(nodeValue.init) } if ('body' in nodeValue && nodeValue.body) { traverseNode(nodeValue.body) } if ('left' in nodeValue) { checkNode(nodeValue.left) } if ('right' in nodeValue) { checkNode(nodeValue.right) } if ('initializer' in nodeValue && nodeValue.initializer) { checkNode(nodeValue.initializer) } 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) { checkNode(nodeValue.argument) } if ('arguments' in nodeValue) { for (let argument of nodeValue.arguments) { checkNode(argument) } } if ('declarations' in nodeValue) { for (let declaration of nodeValue.declarations) { checkNode(declaration) } } if ('properties' in nodeValue) { for (let property of nodeValue.properties) { checkNode(property) } } if ( 'value' in nodeValue && nodeValue.value && typeof nodeValue.value === 'object' && 'type' in nodeValue.value ) { checkNode(nodeValue.value) } if ('expressions' in nodeValue) { for (let nodeExpression of nodeValue.expressions) { checkNode(nodeExpression) } } } let traverseNode = nodeValue => { if (Array.isArray(nodeValue)) { for (let nodeItem of nodeValue) { traverseNode(nodeItem) } } else { checkNode(nodeValue) } } checkNode(expression) return dependencies } module.exports = sortModules