| /** |
| * @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; |
| }, "") |
| ); |
| } |
| }); |
| } |
| } |
| } |
| }; |
| } |
| }; |