Skip to content
Permalink
9bfb9ba527
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time
241 lines (211 sloc) 10.6 KB
/**
* @fileoverview Rule to require sorting of import declarations
* @author Christian Schuller
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Enforce sorted import declarations within modules",
recommended: false,
url: "https://eslint.org/docs/latest/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.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[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;
}, "")
);
}
});
}
}
}
};
}
};