UNPKG

eslint

Version:

An AST-based pattern checker for JavaScript.

320 lines (293 loc) 8.6 kB
/** * @fileoverview Rule to enforce sorted `import` declarations within modules * @author Christian Schuller */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../types').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", defaultOptions: [ { allowSeparatedGroups: false, ignoreCase: false, ignoreDeclarationSort: false, ignoreMemberSort: false, memberSyntaxSortOrder: ["none", "all", "multiple", "single"], }, ], docs: { description: "Enforce sorted `import` declarations within modules", recommended: false, frozen: true, url: "https://eslint.org/docs/latest/rules/sort-imports", }, schema: [ { type: "object", properties: { ignoreCase: { type: "boolean", }, memberSyntaxSortOrder: { type: "array", items: { enum: ["none", "all", "multiple", "single"], }, uniqueItems: true, minItems: 4, maxItems: 4, }, ignoreDeclarationSort: { type: "boolean", }, ignoreMemberSort: { type: "boolean", }, allowSeparatedGroups: { type: "boolean", }, }, additionalProperties: false, }, ], fixable: "code", messages: { sortImportsAlphabetically: "Imports should be sorted alphabetically.", sortMembersAlphabetically: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.", unexpectedSyntaxOrder: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax.", }, }, create(context) { const [ { ignoreCase, ignoreDeclarationSort, ignoreMemberSort, memberSyntaxSortOrder, allowSeparatedGroups, }, ] = context.options; const sourceCode = context.sourceCode; let previousDeclaration = null; /** * Gets the used member syntax style. * * import "my-module.js" --> none * import * as myModule from "my-module.js" --> all * import {myMember} from "my-module.js" --> single * import {foo, bar} from "my-module.js" --> multiple * @param {ASTNode} node the ImportDeclaration node. * @returns {string} used member parameter style, ["all", "multiple", "single"] */ function usedMemberSyntax(node) { if (node.specifiers.length === 0) { return "none"; } if (node.specifiers[0].type === "ImportNamespaceSpecifier") { return "all"; } if (node.specifiers.length === 1) { return "single"; } return "multiple"; } /** * Gets the group by member parameter index for given declaration. * @param {ASTNode} node the ImportDeclaration node. * @returns {number} the declaration group by member index. */ function getMemberParameterGroupIndex(node) { return memberSyntaxSortOrder.indexOf(usedMemberSyntax(node)); } /** * Gets the local name of the first imported module. * @param {ASTNode} node the ImportDeclaration node. * @returns {?string} the local name of the first imported module. */ function getFirstLocalMemberName(node) { if (node.specifiers[0]) { return node.specifiers[0].local.name; } return null; } /** * Calculates number of lines between two nodes. It is assumed that the given `left` node appears before * the given `right` node in the source code. Lines are counted from the end of the `left` node till the * start of the `right` node. If the given nodes are on the same line, it returns `0`, same as if they were * on two consecutive lines. * @param {ASTNode} left node that appears before the given `right` node. * @param {ASTNode} right node that appears after the given `left` node. * @returns {number} number of lines between nodes. */ function getNumberOfLinesBetween(left, right) { return Math.max(right.loc.start.line - left.loc.end.line - 1, 0); } return { ImportDeclaration(node) { if (!ignoreDeclarationSort) { if ( previousDeclaration && allowSeparatedGroups && getNumberOfLinesBetween(previousDeclaration, node) > 0 ) { // reset declaration sort previousDeclaration = null; } if (previousDeclaration) { const currentMemberSyntaxGroupIndex = getMemberParameterGroupIndex(node), previousMemberSyntaxGroupIndex = getMemberParameterGroupIndex( previousDeclaration, ); let currentLocalMemberName = getFirstLocalMemberName(node), previousLocalMemberName = getFirstLocalMemberName(previousDeclaration); if (ignoreCase) { previousLocalMemberName = previousLocalMemberName && previousLocalMemberName.toLowerCase(); currentLocalMemberName = currentLocalMemberName && currentLocalMemberName.toLowerCase(); } /* * When the current declaration uses a different member syntax, * then check if the ordering is correct. * Otherwise, make a default string compare (like rule sort-vars to be consistent) of the first used local member name. */ if ( currentMemberSyntaxGroupIndex !== previousMemberSyntaxGroupIndex ) { if ( currentMemberSyntaxGroupIndex < previousMemberSyntaxGroupIndex ) { context.report({ node, messageId: "unexpectedSyntaxOrder", data: { syntaxA: memberSyntaxSortOrder[ currentMemberSyntaxGroupIndex ], syntaxB: memberSyntaxSortOrder[ previousMemberSyntaxGroupIndex ], }, }); } } else { if ( previousLocalMemberName && currentLocalMemberName && currentLocalMemberName < previousLocalMemberName ) { context.report({ node, messageId: "sortImportsAlphabetically", }); } } } previousDeclaration = node; } if (!ignoreMemberSort) { const importSpecifiers = node.specifiers.filter( specifier => specifier.type === "ImportSpecifier", ); const getSortableName = ignoreCase ? specifier => specifier.local.name.toLowerCase() : specifier => specifier.local.name; const firstUnsortedIndex = importSpecifiers .map(getSortableName) .findIndex( (name, index, array) => array[index - 1] > name, ); if (firstUnsortedIndex !== -1) { context.report({ node: importSpecifiers[firstUnsortedIndex], messageId: "sortMembersAlphabetically", data: { memberName: importSpecifiers[firstUnsortedIndex].local .name, }, fix(fixer) { if ( importSpecifiers.some( specifier => sourceCode.getCommentsBefore( specifier, ).length || sourceCode.getCommentsAfter( specifier, ).length, ) ) { // If there are comments in the ImportSpecifier list, don't rearrange the specifiers. return null; } return fixer.replaceTextRange( [ importSpecifiers[0].range[0], importSpecifiers.at(-1).range[1], ], importSpecifiers // Clone the importSpecifiers array to avoid mutating it .slice() // Sort the array into the desired order .sort((specifierA, specifierB) => { const aName = getSortableName(specifierA); const bName = getSortableName(specifierB); return aName > bName ? 1 : -1; }) // Build a string out of the sorted list of import specifiers and the text between the originals .reduce( (sourceText, specifier, index) => { const textAfterSpecifier = index === importSpecifiers.length - 1 ? "" : sourceCode .getText() .slice( importSpecifiers[ index ].range[1], importSpecifiers[ index + 1 ].range[0], ); return ( sourceText + sourceCode.getText( specifier, ) + textAfterSpecifier ); }, "", ), ); }, }); } } }, }; }, };