]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Restrict usage of specified node imports. | |
3 | * @author Guy Ellis | |
4 | */ | |
5 | "use strict"; | |
6 | ||
7 | //------------------------------------------------------------------------------ | |
8 | // Rule Definition | |
9 | //------------------------------------------------------------------------------ | |
10 | ||
11 | const ignore = require("ignore"); | |
12 | ||
eb39fafa DC |
13 | const arrayOfStringsOrObjects = { |
14 | type: "array", | |
15 | items: { | |
16 | anyOf: [ | |
17 | { type: "string" }, | |
18 | { | |
19 | type: "object", | |
20 | properties: { | |
21 | name: { type: "string" }, | |
22 | message: { | |
23 | type: "string", | |
24 | minLength: 1 | |
25 | }, | |
26 | importNames: { | |
27 | type: "array", | |
28 | items: { | |
29 | type: "string" | |
30 | } | |
31 | } | |
32 | }, | |
33 | additionalProperties: false, | |
34 | required: ["name"] | |
35 | } | |
36 | ] | |
37 | }, | |
38 | uniqueItems: true | |
39 | }; | |
40 | ||
5422a9cc TL |
41 | const arrayOfStringsOrObjectPatterns = { |
42 | anyOf: [ | |
43 | { | |
44 | type: "array", | |
45 | items: { | |
46 | type: "string" | |
47 | }, | |
48 | uniqueItems: true | |
49 | }, | |
50 | { | |
51 | type: "array", | |
52 | items: { | |
53 | type: "object", | |
54 | properties: { | |
55 | group: { | |
56 | type: "array", | |
57 | items: { | |
58 | type: "string" | |
59 | }, | |
60 | minItems: 1, | |
61 | uniqueItems: true | |
62 | }, | |
63 | message: { | |
64 | type: "string", | |
65 | minLength: 1 | |
66 | } | |
67 | }, | |
68 | additionalProperties: false, | |
69 | required: ["group"] | |
70 | }, | |
71 | uniqueItems: true | |
72 | } | |
73 | ] | |
74 | }; | |
75 | ||
34eeec05 | 76 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
77 | module.exports = { |
78 | meta: { | |
79 | type: "suggestion", | |
80 | ||
81 | docs: { | |
82 | description: "disallow specified modules when loaded by `import`", | |
eb39fafa DC |
83 | recommended: false, |
84 | url: "https://eslint.org/docs/rules/no-restricted-imports" | |
85 | }, | |
86 | ||
87 | messages: { | |
88 | path: "'{{importSource}}' import is restricted from being used.", | |
609c276f | 89 | // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period |
eb39fafa DC |
90 | pathWithCustomMessage: "'{{importSource}}' import is restricted from being used. {{customMessage}}", |
91 | ||
92 | patterns: "'{{importSource}}' import is restricted from being used by a pattern.", | |
609c276f | 93 | // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period |
5422a9cc | 94 | patternWithCustomMessage: "'{{importSource}}' import is restricted from being used by a pattern. {{customMessage}}", |
eb39fafa DC |
95 | |
96 | everything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.", | |
609c276f | 97 | // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period |
eb39fafa DC |
98 | everythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted. {{customMessage}}", |
99 | ||
100 | importName: "'{{importName}}' import from '{{importSource}}' is restricted.", | |
609c276f | 101 | // eslint-disable-next-line eslint-plugin/report-message-format -- Custom message might not end in a period |
eb39fafa DC |
102 | importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}" |
103 | }, | |
104 | ||
105 | schema: { | |
106 | anyOf: [ | |
107 | arrayOfStringsOrObjects, | |
108 | { | |
109 | type: "array", | |
110 | items: [{ | |
111 | type: "object", | |
112 | properties: { | |
113 | paths: arrayOfStringsOrObjects, | |
5422a9cc | 114 | patterns: arrayOfStringsOrObjectPatterns |
eb39fafa DC |
115 | }, |
116 | additionalProperties: false | |
117 | }], | |
118 | additionalItems: false | |
119 | } | |
120 | ] | |
121 | } | |
122 | }, | |
123 | ||
124 | create(context) { | |
125 | const sourceCode = context.getSourceCode(); | |
126 | const options = Array.isArray(context.options) ? context.options : []; | |
127 | const isPathAndPatternsObject = | |
128 | typeof options[0] === "object" && | |
129 | (Object.prototype.hasOwnProperty.call(options[0], "paths") || Object.prototype.hasOwnProperty.call(options[0], "patterns")); | |
130 | ||
131 | const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || []; | |
eb39fafa DC |
132 | const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => { |
133 | if (typeof importSource === "string") { | |
134 | memo[importSource] = { message: null }; | |
135 | } else { | |
136 | memo[importSource.name] = { | |
137 | message: importSource.message, | |
138 | importNames: importSource.importNames | |
139 | }; | |
140 | } | |
141 | return memo; | |
142 | }, {}); | |
143 | ||
5422a9cc TL |
144 | // Handle patterns too, either as strings or groups |
145 | const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || []; | |
146 | const restrictedPatternGroups = restrictedPatterns.length > 0 && typeof restrictedPatterns[0] === "string" | |
147 | ? [{ matcher: ignore().add(restrictedPatterns) }] | |
148 | : restrictedPatterns.map(({ group, message }) => ({ matcher: ignore().add(group), customMessage: message })); | |
149 | ||
609c276f | 150 | // if no imports are restricted we don't need to check |
5422a9cc TL |
151 | if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) { |
152 | return {}; | |
153 | } | |
eb39fafa DC |
154 | |
155 | /** | |
156 | * Report a restricted path. | |
157 | * @param {string} importSource path of the import | |
158 | * @param {Map<string,Object[]>} importNames Map of import names that are being imported | |
159 | * @param {node} node representing the restricted path reference | |
160 | * @returns {void} | |
161 | * @private | |
162 | */ | |
163 | function checkRestrictedPathAndReport(importSource, importNames, node) { | |
164 | if (!Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) { | |
165 | return; | |
166 | } | |
167 | ||
168 | const customMessage = restrictedPathMessages[importSource].message; | |
169 | const restrictedImportNames = restrictedPathMessages[importSource].importNames; | |
170 | ||
171 | if (restrictedImportNames) { | |
172 | if (importNames.has("*")) { | |
173 | const specifierData = importNames.get("*")[0]; | |
174 | ||
175 | context.report({ | |
176 | node, | |
177 | messageId: customMessage ? "everythingWithCustomMessage" : "everything", | |
178 | loc: specifierData.loc, | |
179 | data: { | |
180 | importSource, | |
181 | importNames: restrictedImportNames, | |
182 | customMessage | |
183 | } | |
184 | }); | |
185 | } | |
186 | ||
187 | restrictedImportNames.forEach(importName => { | |
188 | if (importNames.has(importName)) { | |
189 | const specifiers = importNames.get(importName); | |
190 | ||
191 | specifiers.forEach(specifier => { | |
192 | context.report({ | |
193 | node, | |
194 | messageId: customMessage ? "importNameWithCustomMessage" : "importName", | |
195 | loc: specifier.loc, | |
196 | data: { | |
197 | importSource, | |
198 | customMessage, | |
199 | importName | |
200 | } | |
201 | }); | |
202 | }); | |
203 | } | |
204 | }); | |
205 | } else { | |
206 | context.report({ | |
207 | node, | |
208 | messageId: customMessage ? "pathWithCustomMessage" : "path", | |
209 | data: { | |
210 | importSource, | |
211 | customMessage | |
212 | } | |
213 | }); | |
214 | } | |
215 | } | |
216 | ||
217 | /** | |
218 | * Report a restricted path specifically for patterns. | |
219 | * @param {node} node representing the restricted path reference | |
5422a9cc | 220 | * @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails |
eb39fafa DC |
221 | * @returns {void} |
222 | * @private | |
223 | */ | |
5422a9cc | 224 | function reportPathForPatterns(node, group) { |
eb39fafa DC |
225 | const importSource = node.source.value.trim(); |
226 | ||
227 | context.report({ | |
228 | node, | |
5422a9cc | 229 | messageId: group.customMessage ? "patternWithCustomMessage" : "patterns", |
eb39fafa | 230 | data: { |
5422a9cc TL |
231 | importSource, |
232 | customMessage: group.customMessage | |
eb39fafa DC |
233 | } |
234 | }); | |
235 | } | |
236 | ||
237 | /** | |
238 | * Check if the given importSource is restricted by a pattern. | |
239 | * @param {string} importSource path of the import | |
5422a9cc | 240 | * @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails |
eb39fafa DC |
241 | * @returns {boolean} whether the variable is a restricted pattern or not |
242 | * @private | |
243 | */ | |
5422a9cc TL |
244 | function isRestrictedPattern(importSource, group) { |
245 | return group.matcher.ignores(importSource); | |
eb39fafa DC |
246 | } |
247 | ||
248 | /** | |
249 | * Checks a node to see if any problems should be reported. | |
250 | * @param {ASTNode} node The node to check. | |
251 | * @returns {void} | |
252 | * @private | |
253 | */ | |
254 | function checkNode(node) { | |
255 | const importSource = node.source.value.trim(); | |
256 | const importNames = new Map(); | |
257 | ||
258 | if (node.type === "ExportAllDeclaration") { | |
259 | const starToken = sourceCode.getFirstToken(node, 1); | |
260 | ||
261 | importNames.set("*", [{ loc: starToken.loc }]); | |
262 | } else if (node.specifiers) { | |
263 | for (const specifier of node.specifiers) { | |
264 | let name; | |
265 | const specifierData = { loc: specifier.loc }; | |
266 | ||
267 | if (specifier.type === "ImportDefaultSpecifier") { | |
268 | name = "default"; | |
269 | } else if (specifier.type === "ImportNamespaceSpecifier") { | |
270 | name = "*"; | |
271 | } else if (specifier.imported) { | |
272 | name = specifier.imported.name; | |
273 | } else if (specifier.local) { | |
274 | name = specifier.local.name; | |
275 | } | |
276 | ||
277 | if (name) { | |
278 | if (importNames.has(name)) { | |
279 | importNames.get(name).push(specifierData); | |
280 | } else { | |
281 | importNames.set(name, [specifierData]); | |
282 | } | |
283 | } | |
284 | } | |
285 | } | |
286 | ||
287 | checkRestrictedPathAndReport(importSource, importNames, node); | |
5422a9cc TL |
288 | restrictedPatternGroups.forEach(group => { |
289 | if (isRestrictedPattern(importSource, group)) { | |
290 | reportPathForPatterns(node, group); | |
291 | } | |
292 | }); | |
eb39fafa DC |
293 | } |
294 | ||
295 | return { | |
296 | ImportDeclaration: checkNode, | |
297 | ExportNamedDeclaration(node) { | |
298 | if (node.source) { | |
299 | checkNode(node); | |
300 | } | |
301 | }, | |
302 | ExportAllDeclaration: checkNode | |
303 | }; | |
304 | } | |
305 | }; |