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