Permalink
Cannot retrieve contributors at this time
| /** | |
| * @fileoverview Rule to require sorting of import declarations | |
| * @author Christian Schuller | |
| */ | |
| "use strict"; | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
| module.exports = { | |
| meta: { | |
| type: "suggestion", | |
| docs: { | |
| description: "enforce sorted import declarations within modules", | |
| category: "ECMAScript 6", | |
| recommended: false, | |
| url: "https://eslint.org/docs/rules/sort-imports" | |
| }, | |
| schema: [ | |
| { | |
| type: "object", | |
| properties: { | |
| ignoreCase: { | |
| type: "boolean", | |
| default: false | |
| }, | |
| memberSyntaxSortOrder: { | |
| type: "array", | |
| items: { | |
| enum: ["none", "all", "multiple", "single"] | |
| }, | |
| uniqueItems: true, | |
| minItems: 4, | |
| maxItems: 4 | |
| }, | |
| ignoreDeclarationSort: { | |
| type: "boolean", | |
| default: false | |
| }, | |
| ignoreMemberSort: { | |
| type: "boolean", | |
| default: false | |
| } | |
| }, | |
| additionalProperties: false | |
| } | |
| ], | |
| fixable: "code" | |
| }, | |
| create(context) { | |
| const configuration = context.options[0] || {}, | |
| ignoreCase = configuration.ignoreCase || false, | |
| ignoreDeclarationSort = configuration.ignoreDeclarationSort || false, | |
| ignoreMemberSort = configuration.ignoreMemberSort || false, | |
| memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"], | |
| sourceCode = context.getSourceCode(); | |
| 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; | |
| } | |
| return { | |
| ImportDeclaration(node) { | |
| if (!ignoreDeclarationSort) { | |
| 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, | |
| message: "Expected '{{syntaxA}}' syntax before '{{syntaxB}}' syntax.", | |
| data: { | |
| syntaxA: memberSyntaxSortOrder[currentMemberSyntaxGroupIndex], | |
| syntaxB: memberSyntaxSortOrder[previousMemberSyntaxGroupIndex] | |
| } | |
| }); | |
| } | |
| } else { | |
| if (previousLocalMemberName && | |
| currentLocalMemberName && | |
| currentLocalMemberName < previousLocalMemberName | |
| ) { | |
| context.report({ | |
| node, | |
| message: "Imports should be sorted alphabetically." | |
| }); | |
| } | |
| } | |
| } | |
| 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], | |
| message: "Member '{{memberName}}' of the import declaration should be sorted alphabetically.", | |
| 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[importSpecifiers.length - 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; | |
| }, "") | |
| ); | |
| } | |
| }); | |
| } | |
| } | |
| } | |
| }; | |
| } | |
| }; |