UNPKG

9.71 kBJavaScriptView Raw
1/**
2 * @fileoverview Restrict usage of specified node imports.
3 * @author Guy Ellis
4 */
5"use strict";
6
7//------------------------------------------------------------------------------
8// Rule Definition
9//------------------------------------------------------------------------------
10
11const ignore = require("ignore");
12
13const arrayOfStrings = {
14 type: "array",
15 items: { type: "string" },
16 uniqueItems: true
17};
18
19const 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
47module.exports = {
48 meta: {
49 type: "suggestion",
50
51 docs: {
52 description: "disallow specified modules when loaded by `import`",
53 category: "ECMAScript 6",
54 recommended: false,
55 url: "https://eslint.org/docs/rules/no-restricted-imports"
56 },
57
58 messages: {
59 path: "'{{importSource}}' import is restricted from being used.",
60 // eslint-disable-next-line eslint-plugin/report-message-format
61 pathWithCustomMessage: "'{{importSource}}' import is restricted from being used. {{customMessage}}",
62
63 patterns: "'{{importSource}}' import is restricted from being used by a pattern.",
64
65 everything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.",
66 // eslint-disable-next-line eslint-plugin/report-message-format
67 everythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted. {{customMessage}}",
68
69 importName: "'{{importName}}' import from '{{importSource}}' is restricted.",
70 // eslint-disable-next-line eslint-plugin/report-message-format
71 importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}"
72 },
73
74 schema: {
75 anyOf: [
76 arrayOfStringsOrObjects,
77 {
78 type: "array",
79 items: [{
80 type: "object",
81 properties: {
82 paths: arrayOfStringsOrObjects,
83 patterns: arrayOfStrings
84 },
85 additionalProperties: false
86 }],
87 additionalItems: false
88 }
89 ]
90 }
91 },
92
93 create(context) {
94 const sourceCode = context.getSourceCode();
95 const options = Array.isArray(context.options) ? context.options : [];
96 const isPathAndPatternsObject =
97 typeof options[0] === "object" &&
98 (Object.prototype.hasOwnProperty.call(options[0], "paths") || Object.prototype.hasOwnProperty.call(options[0], "patterns"));
99
100 const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
101 const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
102
103 // if no imports are restricted we don"t need to check
104 if (Object.keys(restrictedPaths).length === 0 && restrictedPatterns.length === 0) {
105 return {};
106 }
107
108 const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
109 if (typeof importSource === "string") {
110 memo[importSource] = { message: null };
111 } else {
112 memo[importSource.name] = {
113 message: importSource.message,
114 importNames: importSource.importNames
115 };
116 }
117 return memo;
118 }, {});
119
120 const restrictedPatternsMatcher = ignore().add(restrictedPatterns);
121
122 /**
123 * Report a restricted path.
124 * @param {string} importSource path of the import
125 * @param {Map<string,Object[]>} importNames Map of import names that are being imported
126 * @param {node} node representing the restricted path reference
127 * @returns {void}
128 * @private
129 */
130 function checkRestrictedPathAndReport(importSource, importNames, node) {
131 if (!Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) {
132 return;
133 }
134
135 const customMessage = restrictedPathMessages[importSource].message;
136 const restrictedImportNames = restrictedPathMessages[importSource].importNames;
137
138 if (restrictedImportNames) {
139 if (importNames.has("*")) {
140 const specifierData = importNames.get("*")[0];
141
142 context.report({
143 node,
144 messageId: customMessage ? "everythingWithCustomMessage" : "everything",
145 loc: specifierData.loc,
146 data: {
147 importSource,
148 importNames: restrictedImportNames,
149 customMessage
150 }
151 });
152 }
153
154 restrictedImportNames.forEach(importName => {
155 if (importNames.has(importName)) {
156 const specifiers = importNames.get(importName);
157
158 specifiers.forEach(specifier => {
159 context.report({
160 node,
161 messageId: customMessage ? "importNameWithCustomMessage" : "importName",
162 loc: specifier.loc,
163 data: {
164 importSource,
165 customMessage,
166 importName
167 }
168 });
169 });
170 }
171 });
172 } else {
173 context.report({
174 node,
175 messageId: customMessage ? "pathWithCustomMessage" : "path",
176 data: {
177 importSource,
178 customMessage
179 }
180 });
181 }
182 }
183
184 /**
185 * Report a restricted path specifically for patterns.
186 * @param {node} node representing the restricted path reference
187 * @returns {void}
188 * @private
189 */
190 function reportPathForPatterns(node) {
191 const importSource = node.source.value.trim();
192
193 context.report({
194 node,
195 messageId: "patterns",
196 data: {
197 importSource
198 }
199 });
200 }
201
202 /**
203 * Check if the given importSource is restricted by a pattern.
204 * @param {string} importSource path of the import
205 * @returns {boolean} whether the variable is a restricted pattern or not
206 * @private
207 */
208 function isRestrictedPattern(importSource) {
209 return restrictedPatterns.length > 0 && restrictedPatternsMatcher.ignores(importSource);
210 }
211
212 /**
213 * Checks a node to see if any problems should be reported.
214 * @param {ASTNode} node The node to check.
215 * @returns {void}
216 * @private
217 */
218 function checkNode(node) {
219 const importSource = node.source.value.trim();
220 const importNames = new Map();
221
222 if (node.type === "ExportAllDeclaration") {
223 const starToken = sourceCode.getFirstToken(node, 1);
224
225 importNames.set("*", [{ loc: starToken.loc }]);
226 } else if (node.specifiers) {
227 for (const specifier of node.specifiers) {
228 let name;
229 const specifierData = { loc: specifier.loc };
230
231 if (specifier.type === "ImportDefaultSpecifier") {
232 name = "default";
233 } else if (specifier.type === "ImportNamespaceSpecifier") {
234 name = "*";
235 } else if (specifier.imported) {
236 name = specifier.imported.name;
237 } else if (specifier.local) {
238 name = specifier.local.name;
239 }
240
241 if (name) {
242 if (importNames.has(name)) {
243 importNames.get(name).push(specifierData);
244 } else {
245 importNames.set(name, [specifierData]);
246 }
247 }
248 }
249 }
250
251 checkRestrictedPathAndReport(importSource, importNames, node);
252
253 if (isRestrictedPattern(importSource)) {
254 reportPathForPatterns(node);
255 }
256 }
257
258 return {
259 ImportDeclaration: checkNode,
260 ExportNamedDeclaration(node) {
261 if (node.source) {
262 checkNode(node);
263 }
264 },
265 ExportAllDeclaration: checkNode
266 };
267 }
268};