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