123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- /**
- * @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
- },
- allowSeparatedGroups: {
- type: "boolean",
- default: false
- }
- },
- 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 configuration = context.options[0] || {},
- ignoreCase = configuration.ignoreCase || false,
- ignoreDeclarationSort = configuration.ignoreDeclarationSort || false,
- ignoreMemberSort = configuration.ignoreMemberSort || false,
- memberSyntaxSortOrder = configuration.memberSyntaxSortOrder || ["none", "all", "multiple", "single"],
- allowSeparatedGroups = configuration.allowSeparatedGroups || false,
- 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;
- }
- /**
- * 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[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;
- }, "")
- );
- }
- });
- }
- }
- }
- };
- }
- };
|