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