/** * @fileoverview Restrict usage of specified node imports. * @author Guy Ellis */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ const ignore = require("ignore"); const arrayOfStringsOrObjects = { type: "array", items: { anyOf: [ { type: "string" }, { type: "object", properties: { name: { type: "string" }, message: { type: "string", minLength: 1 }, importNames: { type: "array", items: { type: "string" } } }, additionalProperties: false, required: ["name"] } ] }, uniqueItems: true }; const arrayOfStringsOrObjectPatterns = { anyOf: [ { type: "array", items: { type: "string" }, uniqueItems: true }, { type: "array", items: { type: "object", properties: { group: { type: "array", items: { type: "string" }, minItems: 1, uniqueItems: true }, message: { type: "string", minLength: 1 } }, additionalProperties: false, required: ["group"] }, uniqueItems: true } ] }; module.exports = { meta: { type: "suggestion", docs: { description: "disallow specified modules when loaded by `import`", category: "ECMAScript 6", recommended: false, url: "https://eslint.org/docs/rules/no-restricted-imports" }, messages: { path: "'{{importSource}}' import is restricted from being used.", // eslint-disable-next-line eslint-plugin/report-message-format pathWithCustomMessage: "'{{importSource}}' import is restricted from being used. {{customMessage}}", patterns: "'{{importSource}}' import is restricted from being used by a pattern.", // eslint-disable-next-line eslint-plugin/report-message-format patternWithCustomMessage: "'{{importSource}}' import is restricted from being used by a pattern. {{customMessage}}", everything: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.", // eslint-disable-next-line eslint-plugin/report-message-format everythingWithCustomMessage: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted. {{customMessage}}", importName: "'{{importName}}' import from '{{importSource}}' is restricted.", // eslint-disable-next-line eslint-plugin/report-message-format importNameWithCustomMessage: "'{{importName}}' import from '{{importSource}}' is restricted. {{customMessage}}" }, schema: { anyOf: [ arrayOfStringsOrObjects, { type: "array", items: [{ type: "object", properties: { paths: arrayOfStringsOrObjects, patterns: arrayOfStringsOrObjectPatterns }, additionalProperties: false }], additionalItems: false } ] } }, create(context) { const sourceCode = context.getSourceCode(); const options = Array.isArray(context.options) ? context.options : []; const isPathAndPatternsObject = typeof options[0] === "object" && (Object.prototype.hasOwnProperty.call(options[0], "paths") || Object.prototype.hasOwnProperty.call(options[0], "patterns")); const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || []; const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => { if (typeof importSource === "string") { memo[importSource] = { message: null }; } else { memo[importSource.name] = { message: importSource.message, importNames: importSource.importNames }; } return memo; }, {}); // Handle patterns too, either as strings or groups const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || []; const restrictedPatternGroups = restrictedPatterns.length > 0 && typeof restrictedPatterns[0] === "string" ? [{ matcher: ignore().add(restrictedPatterns) }] : restrictedPatterns.map(({ group, message }) => ({ matcher: ignore().add(group), customMessage: message })); // if no imports are restricted we don"t need to check if (Object.keys(restrictedPaths).length === 0 && restrictedPatternGroups.length === 0) { return {}; } /** * Report a restricted path. * @param {string} importSource path of the import * @param {Map} importNames Map of import names that are being imported * @param {node} node representing the restricted path reference * @returns {void} * @private */ function checkRestrictedPathAndReport(importSource, importNames, node) { if (!Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) { return; } const customMessage = restrictedPathMessages[importSource].message; const restrictedImportNames = restrictedPathMessages[importSource].importNames; if (restrictedImportNames) { if (importNames.has("*")) { const specifierData = importNames.get("*")[0]; context.report({ node, messageId: customMessage ? "everythingWithCustomMessage" : "everything", loc: specifierData.loc, data: { importSource, importNames: restrictedImportNames, customMessage } }); } restrictedImportNames.forEach(importName => { if (importNames.has(importName)) { const specifiers = importNames.get(importName); specifiers.forEach(specifier => { context.report({ node, messageId: customMessage ? "importNameWithCustomMessage" : "importName", loc: specifier.loc, data: { importSource, customMessage, importName } }); }); } }); } else { context.report({ node, messageId: customMessage ? "pathWithCustomMessage" : "path", data: { importSource, customMessage } }); } } /** * Report a restricted path specifically for patterns. * @param {node} node representing the restricted path reference * @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails * @returns {void} * @private */ function reportPathForPatterns(node, group) { const importSource = node.source.value.trim(); context.report({ node, messageId: group.customMessage ? "patternWithCustomMessage" : "patterns", data: { importSource, customMessage: group.customMessage } }); } /** * Check if the given importSource is restricted by a pattern. * @param {string} importSource path of the import * @param {Object} group contains a Ignore instance for paths, and the customMessage to show if it fails * @returns {boolean} whether the variable is a restricted pattern or not * @private */ function isRestrictedPattern(importSource, group) { return group.matcher.ignores(importSource); } /** * Checks a node to see if any problems should be reported. * @param {ASTNode} node The node to check. * @returns {void} * @private */ function checkNode(node) { const importSource = node.source.value.trim(); const importNames = new Map(); if (node.type === "ExportAllDeclaration") { const starToken = sourceCode.getFirstToken(node, 1); importNames.set("*", [{ loc: starToken.loc }]); } else if (node.specifiers) { for (const specifier of node.specifiers) { let name; const specifierData = { loc: specifier.loc }; if (specifier.type === "ImportDefaultSpecifier") { name = "default"; } else if (specifier.type === "ImportNamespaceSpecifier") { name = "*"; } else if (specifier.imported) { name = specifier.imported.name; } else if (specifier.local) { name = specifier.local.name; } if (name) { if (importNames.has(name)) { importNames.get(name).push(specifierData); } else { importNames.set(name, [specifierData]); } } } } checkRestrictedPathAndReport(importSource, importNames, node); restrictedPatternGroups.forEach(group => { if (isRestrictedPattern(importSource, group)) { reportPathForPatterns(node, group); } }); } return { ImportDeclaration: checkNode, ExportNamedDeclaration(node) { if (node.source) { checkNode(node); } }, ExportAllDeclaration: checkNode }; } };