• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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};
269