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