]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/no-restricted-imports.js
import 8.23.1 source
[pve-eslint.git] / eslint / lib / rules / no-restricted-imports.js
1 /**
2 * @fileoverview Restrict usage of specified node imports.
3 * @author Guy Ellis
4 */
5 "use strict";
6
7 //------------------------------------------------------------------------------
8 // Requirements
9 //------------------------------------------------------------------------------
10
11 const astUtils = require("./utils/ast-utils");
12
13 //------------------------------------------------------------------------------
14 // Rule Definition
15 //------------------------------------------------------------------------------
16
17 const ignore = require("ignore");
18
19 const arrayOfStringsOrObjects = {
20 type: "array",
21 items: {
22 anyOf: [
23 { type: "string" },
24 {
25 type: "object",
26 properties: {
27 name: { type: "string" },
28 message: {
29 type: "string",
30 minLength: 1
31 },
32 importNames: {
33 type: "array",
34 items: {
35 type: "string"
36 }
37 }
38 },
39 additionalProperties: false,
40 required: ["name"]
41 }
42 ]
43 },
44 uniqueItems: true
45 };
46
47 const arrayOfStringsOrObjectPatterns = {
48 anyOf: [
49 {
50 type: "array",
51 items: {
52 type: "string"
53 },
54 uniqueItems: true
55 },
56 {
57 type: "array",
58 items: {
59 type: "object",
60 properties: {
61 importNames: {
62 type: "array",
63 items: {
64 type: "string"
65 },
66 minItems: 1,
67 uniqueItems: true
68 },
69 group: {
70 type: "array",
71 items: {
72 type: "string"
73 },
74 minItems: 1,
75 uniqueItems: true
76 },
77 message: {
78 type: "string",
79 minLength: 1
80 },
81 caseSensitive: {
82 type: "boolean"
83 }
84 },
85 additionalProperties: false,
86 required: ["group"]
87 },
88 uniqueItems: true
89 }
90 ]
91 };
92
93 /** @type {import('../shared/types').Rule} */
94 module.exports = {
95 meta: {
96 type: "suggestion",
97
98 docs: {
99 description: "Disallow specified modules when loaded by `import`",
100 recommended: false,
101 url: "https://eslint.org/docs/rules/no-restricted-imports"
102 },
103
104 messages: {
105 path: "'{{importSource}}' import is restricted from being used.",
106 // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
107 pathWithCustomMessage: "'{{importSource}}' import is restricted from being used. {{customMessage}}",
108
109 patterns: "'{{importSource}}' import is restricted from being used by a pattern.",
110 // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
111 patternWithCustomMessage: "'{{importSource}}' import is restricted from being used by a pattern. {{customMessage}}",
112
113 patternAndImportName: "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern.",
114 // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
115 patternAndImportNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}",
116
117 patternAndEverything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern.",
118 // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
119 patternAndEverythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted from being used by a pattern. {{customMessage}}",
120
121 everything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.",
122 // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
123 everythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted. {{customMessage}}",
124
125 importName: "'{{importName}}' import from '{{importSource}}' is restricted.",
126 // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period
127 importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}"
128 },
129
130 schema: {
131 anyOf: [
132 arrayOfStringsOrObjects,
133 {
134 type: "array",
135 items: [{
136 type: "object",
137 properties: {
138 paths: arrayOfStringsOrObjects,
139 patterns: arrayOfStringsOrObjectPatterns
140 },
141 additionalProperties: false
142 }],
143 additionalItems: false
144 }
145 ]
146 }
147 },
148
149 create(context) {
150 const sourceCode = context.getSourceCode();
151 const options = Array.isArray(context.options) ? context.options : [];
152 const isPathAndPatternsObject =
153 typeof options[0] === "object" &&
154 (Object.prototype.hasOwnProperty.call(options[0], "paths") || Object.prototype.hasOwnProperty.call(options[0], "patterns"));
155
156 const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
157 const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
158 if (typeof importSource === "string") {
159 memo[importSource] = { message: null };
160 } else {
161 memo[importSource.name] = {
162 message: importSource.message,
163 importNames: importSource.importNames
164 };
165 }
166 return memo;
167 }, {});
168
169 // Handle patterns too, either as strings or groups
170 let restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
171
172 // standardize to array of objects if we have an array of strings
173 if (restrictedPatterns.length > 0 && typeof restrictedPatterns[0] === "string") {
174 restrictedPatterns = [{ group: restrictedPatterns }];
175 }
176
177 // relative paths are supported for this rule
178 const restrictedPatternGroups = restrictedPatterns.map(({ group, message, caseSensitive, importNames }) => ({
179 matcher: ignore({ allowRelativePaths: true, ignorecase: !caseSensitive }).add(group),
180 customMessage: message,
181 importNames
182 }));
183
184 // if no imports are restricted we don't need to check
185 if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) {
186 return {};
187 }
188
189 /**
190 * Report a restricted path.
191 * @param {string} importSource path of the import
192 * @param {Map<string,Object[]>} importNames Map of import names that are being imported
193 * @param {node} node representing the restricted path reference
194 * @returns {void}
195 * @private
196 */
197 function checkRestrictedPathAndReport(importSource, importNames, node) {
198 if (!Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) {
199 return;
200 }
201
202 const customMessage = restrictedPathMessages[importSource].message;
203 const restrictedImportNames = restrictedPathMessages[importSource].importNames;
204
205 if (restrictedImportNames) {
206 if (importNames.has("*")) {
207 const specifierData = importNames.get("*")[0];
208
209 context.report({
210 node,
211 messageId: customMessage ? "everythingWithCustomMessage" : "everything",
212 loc: specifierData.loc,
213 data: {
214 importSource,
215 importNames: restrictedImportNames,
216 customMessage
217 }
218 });
219 }
220
221 restrictedImportNames.forEach(importName => {
222 if (importNames.has(importName)) {
223 const specifiers = importNames.get(importName);
224
225 specifiers.forEach(specifier => {
226 context.report({
227 node,
228 messageId: customMessage ? "importNameWithCustomMessage" : "importName",
229 loc: specifier.loc,
230 data: {
231 importSource,
232 customMessage,
233 importName
234 }
235 });
236 });
237 }
238 });
239 } else {
240 context.report({
241 node,
242 messageId: customMessage ? "pathWithCustomMessage" : "path",
243 data: {
244 importSource,
245 customMessage
246 }
247 });
248 }
249 }
250
251 /**
252 * Report a restricted path specifically for patterns.
253 * @param {node} node representing the restricted path reference
254 * @param {Object} group contains an Ignore instance for paths, the customMessage to show on failure,
255 * and any restricted import names that have been specified in the config
256 * @param {Map<string,Object[]>} importNames Map of import names that are being imported
257 * @returns {void}
258 * @private
259 */
260 function reportPathForPatterns(node, group, importNames) {
261 const importSource = node.source.value.trim();
262
263 const customMessage = group.customMessage;
264 const restrictedImportNames = group.importNames;
265
266 /*
267 * If we are not restricting to any specific import names and just the pattern itself,
268 * report the error and move on
269 */
270 if (!restrictedImportNames) {
271 context.report({
272 node,
273 messageId: customMessage ? "patternWithCustomMessage" : "patterns",
274 data: {
275 importSource,
276 customMessage
277 }
278 });
279 return;
280 }
281
282 if (importNames.has("*")) {
283 const specifierData = importNames.get("*")[0];
284
285 context.report({
286 node,
287 messageId: customMessage ? "patternAndEverythingWithCustomMessage" : "patternAndEverything",
288 loc: specifierData.loc,
289 data: {
290 importSource,
291 importNames: restrictedImportNames,
292 customMessage
293 }
294 });
295 }
296
297 restrictedImportNames.forEach(importName => {
298 if (!importNames.has(importName)) {
299 return;
300 }
301
302 const specifiers = importNames.get(importName);
303
304 specifiers.forEach(specifier => {
305 context.report({
306 node,
307 messageId: customMessage ? "patternAndImportNameWithCustomMessage" : "patternAndImportName",
308 loc: specifier.loc,
309 data: {
310 importSource,
311 customMessage,
312 importName
313 }
314 });
315 });
316 });
317 }
318
319 /**
320 * Check if the given importSource is restricted by a pattern.
321 * @param {string} importSource path of the import
322 * @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails
323 * @returns {boolean} whether the variable is a restricted pattern or not
324 * @private
325 */
326 function isRestrictedPattern(importSource, group) {
327 return group.matcher.ignores(importSource);
328 }
329
330 /**
331 * Checks a node to see if any problems should be reported.
332 * @param {ASTNode} node The node to check.
333 * @returns {void}
334 * @private
335 */
336 function checkNode(node) {
337 const importSource = node.source.value.trim();
338 const importNames = new Map();
339
340 if (node.type === "ExportAllDeclaration") {
341 const starToken = sourceCode.getFirstToken(node, 1);
342
343 importNames.set("*", [{ loc: starToken.loc }]);
344 } else if (node.specifiers) {
345 for (const specifier of node.specifiers) {
346 let name;
347 const specifierData = { loc: specifier.loc };
348
349 if (specifier.type === "ImportDefaultSpecifier") {
350 name = "default";
351 } else if (specifier.type === "ImportNamespaceSpecifier") {
352 name = "*";
353 } else if (specifier.imported) {
354 name = astUtils.getModuleExportName(specifier.imported);
355 } else if (specifier.local) {
356 name = astUtils.getModuleExportName(specifier.local);
357 }
358
359 if (typeof name === "string") {
360 if (importNames.has(name)) {
361 importNames.get(name).push(specifierData);
362 } else {
363 importNames.set(name, [specifierData]);
364 }
365 }
366 }
367 }
368
369 checkRestrictedPathAndReport(importSource, importNames, node);
370 restrictedPatternGroups.forEach(group => {
371 if (isRestrictedPattern(importSource, group)) {
372 reportPathForPatterns(node, group, importNames);
373 }
374 });
375 }
376
377 return {
378 ImportDeclaration: checkNode,
379 ExportNamedDeclaration(node) {
380 if (node.source) {
381 checkNode(node);
382 }
383 },
384 ExportAllDeclaration: checkNode
385 };
386 }
387 };