*/
"use strict";
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+const NAMED_TYPES = ["ImportSpecifier", "ExportSpecifier"];
+const NAMESPACE_TYPES = [
+ "ImportNamespaceSpecifier",
+ "ExportNamespaceSpecifier"
+];
+
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/**
- * Returns the name of the module imported or re-exported.
+ * Check if an import/export type belongs to (ImportSpecifier|ExportSpecifier) or (ImportNamespaceSpecifier|ExportNamespaceSpecifier).
+ * @param {string} importExportType An import/export type to check.
+ * @param {string} type Can be "named" or "namespace"
+ * @returns {boolean} True if import/export type belongs to (ImportSpecifier|ExportSpecifier) or (ImportNamespaceSpecifier|ExportNamespaceSpecifier) and false if it doesn't.
+ */
+function isImportExportSpecifier(importExportType, type) {
+ const arrayToCheck = type === "named" ? NAMED_TYPES : NAMESPACE_TYPES;
+
+ return arrayToCheck.includes(importExportType);
+}
+
+/**
+ * Return the type of (import|export).
* @param {ASTNode} node A node to get.
- * @returns {string} the name of the module, or empty string if no name.
+ * @returns {string} The type of the (import|export).
*/
-function getValue(node) {
- if (node && node.source && node.source.value) {
- return node.source.value.trim();
+function getImportExportType(node) {
+ if (node.specifiers && node.specifiers.length > 0) {
+ const nodeSpecifiers = node.specifiers;
+ const index = nodeSpecifiers.findIndex(
+ ({ type }) =>
+ isImportExportSpecifier(type, "named") ||
+ isImportExportSpecifier(type, "namespace")
+ );
+ const i = index > -1 ? index : 0;
+
+ return nodeSpecifiers[i].type;
}
+ if (node.type === "ExportAllDeclaration") {
+ if (node.exported) {
+ return "ExportNamespaceSpecifier";
+ }
+ return "ExportAll";
+ }
+ return "SideEffectImport";
+}
- return "";
+/**
+ * Returns a boolean indicates if two (import|export) can be merged
+ * @param {ASTNode} node1 A node to check.
+ * @param {ASTNode} node2 A node to check.
+ * @returns {boolean} True if two (import|export) can be merged, false if they can't.
+ */
+function isImportExportCanBeMerged(node1, node2) {
+ const importExportType1 = getImportExportType(node1);
+ const importExportType2 = getImportExportType(node2);
+
+ if (
+ (importExportType1 === "ExportAll" &&
+ importExportType2 !== "ExportAll" &&
+ importExportType2 !== "SideEffectImport") ||
+ (importExportType1 !== "ExportAll" &&
+ importExportType1 !== "SideEffectImport" &&
+ importExportType2 === "ExportAll")
+ ) {
+ return false;
+ }
+ if (
+ (isImportExportSpecifier(importExportType1, "namespace") &&
+ isImportExportSpecifier(importExportType2, "named")) ||
+ (isImportExportSpecifier(importExportType2, "namespace") &&
+ isImportExportSpecifier(importExportType1, "named"))
+ ) {
+ return false;
+ }
+ return true;
}
/**
- * Checks if the name of the import or export exists in the given array, and reports if so.
- * @param {RuleContext} context The ESLint rule context object.
- * @param {ASTNode} node A node to get.
- * @param {string} value The name of the imported or exported module.
- * @param {string[]} array The array containing other imports or exports in the file.
- * @param {string} messageId A messageId to be reported after the name of the module
- *
- * @returns {void} No return value
+ * Returns a boolean if we should report (import|export).
+ * @param {ASTNode} node A node to be reported or not.
+ * @param {[ASTNode]} previousNodes An array contains previous nodes of the module imported or exported.
+ * @returns {boolean} True if the (import|export) should be reported.
*/
-function checkAndReport(context, node, value, array, messageId) {
- if (array.indexOf(value) !== -1) {
- context.report({
- node,
- messageId,
- data: {
- module: value
- }
- });
+function shouldReportImportExport(node, previousNodes) {
+ let i = 0;
+
+ while (i < previousNodes.length) {
+ if (isImportExportCanBeMerged(node, previousNodes[i])) {
+ return true;
+ }
+ i++;
}
+ return false;
}
/**
- * @callback nodeCallback
- * @param {ASTNode} node A node to handle.
+ * Returns array contains only nodes with declarations types equal to type.
+ * @param {[{node: ASTNode, declarationType: string}]} nodes An array contains objects, each object contains a node and a declaration type.
+ * @param {string} type Declaration type.
+ * @returns {[ASTNode]} An array contains only nodes with declarations types equal to type.
+ */
+function getNodesByDeclarationType(nodes, type) {
+ return nodes
+ .filter(({ declarationType }) => declarationType === type)
+ .map(({ node }) => node);
+}
+
+/**
+ * Returns the name of the module imported or re-exported.
+ * @param {ASTNode} node A node to get.
+ * @returns {string} The name of the module, or empty string if no name.
*/
+function getModule(node) {
+ if (node && node.source && node.source.value) {
+ return node.source.value.trim();
+ }
+ return "";
+}
/**
- * Returns a function handling the imports of a given file
+ * Checks if the (import|export) can be merged with at least one import or one export, and reports if so.
* @param {RuleContext} context The ESLint rule context object.
+ * @param {ASTNode} node A node to get.
+ * @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type.
+ * @param {string} declarationType A declaration type can be an import or export.
* @param {boolean} includeExports Whether or not to check for exports in addition to imports.
- * @param {string[]} importsInFile The array containing other imports in the file.
- * @param {string[]} exportsInFile The array containing other exports in the file.
- *
- * @returns {nodeCallback} A function passed to ESLint to handle the statement.
+ * @returns {void} No return value.
*/
-function handleImports(context, includeExports, importsInFile, exportsInFile) {
- return function(node) {
- const value = getValue(node);
+function checkAndReport(
+ context,
+ node,
+ modules,
+ declarationType,
+ includeExports
+) {
+ const module = getModule(node);
- if (value) {
- checkAndReport(context, node, value, importsInFile, "import");
+ if (modules.has(module)) {
+ const previousNodes = modules.get(module);
+ const messagesIds = [];
+ const importNodes = getNodesByDeclarationType(previousNodes, "import");
+ let exportNodes;
+ if (includeExports) {
+ exportNodes = getNodesByDeclarationType(previousNodes, "export");
+ }
+ if (declarationType === "import") {
+ if (shouldReportImportExport(node, importNodes)) {
+ messagesIds.push("import");
+ }
if (includeExports) {
- checkAndReport(context, node, value, exportsInFile, "importAs");
+ if (shouldReportImportExport(node, exportNodes)) {
+ messagesIds.push("importAs");
+ }
+ }
+ } else if (declarationType === "export") {
+ if (shouldReportImportExport(node, exportNodes)) {
+ messagesIds.push("export");
+ }
+ if (shouldReportImportExport(node, importNodes)) {
+ messagesIds.push("exportAs");
}
-
- importsInFile.push(value);
}
- };
+ messagesIds.forEach(messageId =>
+ context.report({
+ node,
+ messageId,
+ data: {
+ module
+ }
+ }));
+ }
}
/**
- * Returns a function handling the exports of a given file
+ * @callback nodeCallback
+ * @param {ASTNode} node A node to handle.
+ */
+
+/**
+ * Returns a function handling the (imports|exports) of a given file
* @param {RuleContext} context The ESLint rule context object.
- * @param {string[]} importsInFile The array containing other imports in the file.
- * @param {string[]} exportsInFile The array containing other exports in the file.
- *
+ * @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type.
+ * @param {string} declarationType A declaration type can be an import or export.
+ * @param {boolean} includeExports Whether or not to check for exports in addition to imports.
* @returns {nodeCallback} A function passed to ESLint to handle the statement.
*/
-function handleExports(context, importsInFile, exportsInFile) {
+function handleImportsExports(
+ context,
+ modules,
+ declarationType,
+ includeExports
+) {
return function(node) {
- const value = getValue(node);
+ const module = getModule(node);
+
+ if (module) {
+ checkAndReport(
+ context,
+ node,
+ modules,
+ declarationType,
+ includeExports
+ );
+ const currentNode = { node, declarationType };
+ let nodes = [currentNode];
- if (value) {
- checkAndReport(context, node, value, exportsInFile, "export");
- checkAndReport(context, node, value, importsInFile, "exportAs");
+ if (modules.has(module)) {
+ const previousNodes = modules.get(module);
- exportsInFile.push(value);
+ nodes = [...previousNodes, currentNode];
+ }
+ modules.set(module, nodes);
}
};
}
url: "https://eslint.org/docs/rules/no-duplicate-imports"
},
- schema: [{
- type: "object",
- properties: {
- includeExports: {
- type: "boolean",
- default: false
- }
- },
- additionalProperties: false
- }],
+ schema: [
+ {
+ type: "object",
+ properties: {
+ includeExports: {
+ type: "boolean",
+ default: false
+ }
+ },
+ additionalProperties: false
+ }
+ ],
+
messages: {
import: "'{{module}}' import is duplicated.",
importAs: "'{{module}}' import is duplicated as export.",
create(context) {
const includeExports = (context.options[0] || {}).includeExports,
- importsInFile = [],
- exportsInFile = [];
-
+ modules = new Map();
const handlers = {
- ImportDeclaration: handleImports(context, includeExports, importsInFile, exportsInFile)
+ ImportDeclaration: handleImportsExports(
+ context,
+ modules,
+ "import",
+ includeExports
+ )
};
if (includeExports) {
- handlers.ExportNamedDeclaration = handleExports(context, importsInFile, exportsInFile);
- handlers.ExportAllDeclaration = handleExports(context, importsInFile, exportsInFile);
+ handlers.ExportNamedDeclaration = handleImportsExports(
+ context,
+ modules,
+ "export",
+ includeExports
+ );
+ handlers.ExportAllDeclaration = handleImportsExports(
+ context,
+ modules,
+ "export",
+ includeExports
+ );
}
-
return handlers;
}
};