]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/no-duplicate-imports.js
947bb30c2e175a887f38ab7feae5ad2518183257
[pve-eslint.git] / eslint / lib / rules / no-duplicate-imports.js
1 /**
2 * @fileoverview Restrict usage of duplicate imports.
3 * @author Simen Bekkhus
4 */
5 "use strict";
6
7 //------------------------------------------------------------------------------
8 // Helpers
9 //------------------------------------------------------------------------------
10
11 const NAMED_TYPES = ["ImportSpecifier", "ExportSpecifier"];
12 const NAMESPACE_TYPES = [
13 "ImportNamespaceSpecifier",
14 "ExportNamespaceSpecifier"
15 ];
16
17 //------------------------------------------------------------------------------
18 // Rule Definition
19 //------------------------------------------------------------------------------
20
21 /**
22 * Check if an import/export type belongs to (ImportSpecifier|ExportSpecifier) or (ImportNamespaceSpecifier|ExportNamespaceSpecifier).
23 * @param {string} importExportType An import/export type to check.
24 * @param {string} type Can be "named" or "namespace"
25 * @returns {boolean} True if import/export type belongs to (ImportSpecifier|ExportSpecifier) or (ImportNamespaceSpecifier|ExportNamespaceSpecifier) and false if it doesn't.
26 */
27 function isImportExportSpecifier(importExportType, type) {
28 const arrayToCheck = type === "named" ? NAMED_TYPES : NAMESPACE_TYPES;
29
30 return arrayToCheck.includes(importExportType);
31 }
32
33 /**
34 * Return the type of (import|export).
35 * @param {ASTNode} node A node to get.
36 * @returns {string} The type of the (import|export).
37 */
38 function getImportExportType(node) {
39 if (node.specifiers && node.specifiers.length > 0) {
40 const nodeSpecifiers = node.specifiers;
41 const index = nodeSpecifiers.findIndex(
42 ({ type }) =>
43 isImportExportSpecifier(type, "named") ||
44 isImportExportSpecifier(type, "namespace")
45 );
46 const i = index > -1 ? index : 0;
47
48 return nodeSpecifiers[i].type;
49 }
50 if (node.type === "ExportAllDeclaration") {
51 if (node.exported) {
52 return "ExportNamespaceSpecifier";
53 }
54 return "ExportAll";
55 }
56 return "SideEffectImport";
57 }
58
59 /**
60 * Returns a boolean indicates if two (import|export) can be merged
61 * @param {ASTNode} node1 A node to check.
62 * @param {ASTNode} node2 A node to check.
63 * @returns {boolean} True if two (import|export) can be merged, false if they can't.
64 */
65 function isImportExportCanBeMerged(node1, node2) {
66 const importExportType1 = getImportExportType(node1);
67 const importExportType2 = getImportExportType(node2);
68
69 if (
70 (importExportType1 === "ExportAll" &&
71 importExportType2 !== "ExportAll" &&
72 importExportType2 !== "SideEffectImport") ||
73 (importExportType1 !== "ExportAll" &&
74 importExportType1 !== "SideEffectImport" &&
75 importExportType2 === "ExportAll")
76 ) {
77 return false;
78 }
79 if (
80 (isImportExportSpecifier(importExportType1, "namespace") &&
81 isImportExportSpecifier(importExportType2, "named")) ||
82 (isImportExportSpecifier(importExportType2, "namespace") &&
83 isImportExportSpecifier(importExportType1, "named"))
84 ) {
85 return false;
86 }
87 return true;
88 }
89
90 /**
91 * Returns a boolean if we should report (import|export).
92 * @param {ASTNode} node A node to be reported or not.
93 * @param {[ASTNode]} previousNodes An array contains previous nodes of the module imported or exported.
94 * @returns {boolean} True if the (import|export) should be reported.
95 */
96 function shouldReportImportExport(node, previousNodes) {
97 let i = 0;
98
99 while (i < previousNodes.length) {
100 if (isImportExportCanBeMerged(node, previousNodes[i])) {
101 return true;
102 }
103 i++;
104 }
105 return false;
106 }
107
108 /**
109 * Returns array contains only nodes with declarations types equal to type.
110 * @param {[{node: ASTNode, declarationType: string}]} nodes An array contains objects, each object contains a node and a declaration type.
111 * @param {string} type Declaration type.
112 * @returns {[ASTNode]} An array contains only nodes with declarations types equal to type.
113 */
114 function getNodesByDeclarationType(nodes, type) {
115 return nodes
116 .filter(({ declarationType }) => declarationType === type)
117 .map(({ node }) => node);
118 }
119
120 /**
121 * Returns the name of the module imported or re-exported.
122 * @param {ASTNode} node A node to get.
123 * @returns {string} The name of the module, or empty string if no name.
124 */
125 function getModule(node) {
126 if (node && node.source && node.source.value) {
127 return node.source.value.trim();
128 }
129 return "";
130 }
131
132 /**
133 * Checks if the (import|export) can be merged with at least one import or one export, and reports if so.
134 * @param {RuleContext} context The ESLint rule context object.
135 * @param {ASTNode} node A node to get.
136 * @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.
137 * @param {string} declarationType A declaration type can be an import or export.
138 * @param {boolean} includeExports Whether or not to check for exports in addition to imports.
139 * @returns {void} No return value.
140 */
141 function checkAndReport(
142 context,
143 node,
144 modules,
145 declarationType,
146 includeExports
147 ) {
148 const module = getModule(node);
149
150 if (modules.has(module)) {
151 const previousNodes = modules.get(module);
152 const messagesIds = [];
153 const importNodes = getNodesByDeclarationType(previousNodes, "import");
154 let exportNodes;
155
156 if (includeExports) {
157 exportNodes = getNodesByDeclarationType(previousNodes, "export");
158 }
159 if (declarationType === "import") {
160 if (shouldReportImportExport(node, importNodes)) {
161 messagesIds.push("import");
162 }
163 if (includeExports) {
164 if (shouldReportImportExport(node, exportNodes)) {
165 messagesIds.push("importAs");
166 }
167 }
168 } else if (declarationType === "export") {
169 if (shouldReportImportExport(node, exportNodes)) {
170 messagesIds.push("export");
171 }
172 if (shouldReportImportExport(node, importNodes)) {
173 messagesIds.push("exportAs");
174 }
175 }
176 messagesIds.forEach(messageId =>
177 context.report({
178 node,
179 messageId,
180 data: {
181 module
182 }
183 }));
184 }
185 }
186
187 /**
188 * @callback nodeCallback
189 * @param {ASTNode} node A node to handle.
190 */
191
192 /**
193 * Returns a function handling the (imports|exports) of a given file
194 * @param {RuleContext} context The ESLint rule context object.
195 * @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.
196 * @param {string} declarationType A declaration type can be an import or export.
197 * @param {boolean} includeExports Whether or not to check for exports in addition to imports.
198 * @returns {nodeCallback} A function passed to ESLint to handle the statement.
199 */
200 function handleImportsExports(
201 context,
202 modules,
203 declarationType,
204 includeExports
205 ) {
206 return function(node) {
207 const module = getModule(node);
208
209 if (module) {
210 checkAndReport(
211 context,
212 node,
213 modules,
214 declarationType,
215 includeExports
216 );
217 const currentNode = { node, declarationType };
218 let nodes = [currentNode];
219
220 if (modules.has(module)) {
221 const previousNodes = modules.get(module);
222
223 nodes = [...previousNodes, currentNode];
224 }
225 modules.set(module, nodes);
226 }
227 };
228 }
229
230 /** @type {import('../shared/types').Rule} */
231 module.exports = {
232 meta: {
233 type: "problem",
234
235 docs: {
236 description: "disallow duplicate module imports",
237 recommended: false,
238 url: "https://eslint.org/docs/rules/no-duplicate-imports"
239 },
240
241 schema: [
242 {
243 type: "object",
244 properties: {
245 includeExports: {
246 type: "boolean",
247 default: false
248 }
249 },
250 additionalProperties: false
251 }
252 ],
253
254 messages: {
255 import: "'{{module}}' import is duplicated.",
256 importAs: "'{{module}}' import is duplicated as export.",
257 export: "'{{module}}' export is duplicated.",
258 exportAs: "'{{module}}' export is duplicated as import."
259 }
260 },
261
262 create(context) {
263 const includeExports = (context.options[0] || {}).includeExports,
264 modules = new Map();
265 const handlers = {
266 ImportDeclaration: handleImportsExports(
267 context,
268 modules,
269 "import",
270 includeExports
271 )
272 };
273
274 if (includeExports) {
275 handlers.ExportNamedDeclaration = handleImportsExports(
276 context,
277 modules,
278 "export",
279 includeExports
280 );
281 handlers.ExportAllDeclaration = handleImportsExports(
282 context,
283 modules,
284 "export",
285 includeExports
286 );
287 }
288 return handlers;
289 }
290 };