no-restricted-imports.js 9.35 KB
Newer Older
YazhouChen's avatar
YazhouChen committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
/**
 * @fileoverview Restrict usage of specified node imports.
 * @author Guy Ellis
 */
"use strict";

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

const DEFAULT_MESSAGE_TEMPLATE = "'{{importSource}}' import is restricted from being used.";
const CUSTOM_MESSAGE_TEMPLATE = "'{{importSource}}' import is restricted from being used. {{customMessage}}";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

const ignore = require("ignore");

const arrayOfStrings = {
    type: "array",
    items: { type: "string" },
    uniqueItems: true
};

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
};

module.exports = {
    meta: {
        docs: {
            description: "disallow specified modules when loaded by `import`",
            category: "ECMAScript 6",
            recommended: false,
            url: "https://eslint.org/docs/rules/no-restricted-imports"
        },

        schema: {
            anyOf: [
                arrayOfStringsOrObjects,
                {
                    type: "array",
                    items: {
                        type: "object",
                        properties: {
                            paths: arrayOfStringsOrObjects,
                            patterns: arrayOfStrings
                        },
                        additionalProperties: false
                    },
                    additionalItems: false
                }
            ]
        }
    },

    create(context) {
        const options = Array.isArray(context.options) ? context.options : [];
        const isPathAndPatternsObject =
            typeof options[0] === "object" &&
            (options[0].hasOwnProperty("paths") || options[0].hasOwnProperty("patterns"));

        const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
        const restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];

        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;
        }, {});

        // if no imports are restricted we don"t need to check
        if (Object.keys(restrictedPaths).length === 0 && restrictedPatterns.length === 0) {
            return {};
        }

        const restrictedPatternsMatcher = ignore().add(restrictedPatterns);

        /**
         * Checks to see if "*" is being used to import everything.
         * @param {Set.<string>} importNames - Set of import names that are being imported
         * @returns {boolean} whether everything is imported or not
         */
        function isEverythingImported(importNames) {
            return importNames.has("*");
        }

        /**
         * Report a restricted path.
         * @param {node} node representing the restricted path reference
         * @returns {void}
         * @private
         */
        function reportPath(node) {
            const importSource = node.source.value.trim();
            const customMessage = restrictedPathMessages[importSource] && restrictedPathMessages[importSource].message;
            const message = customMessage
                ? CUSTOM_MESSAGE_TEMPLATE
                : DEFAULT_MESSAGE_TEMPLATE;

            context.report({
                node,
                message,
                data: {
                    importSource,
                    customMessage
                }
            });
        }

        /**
         * Report a restricted path specifically for patterns.
         * @param {node} node - representing the restricted path reference
         * @returns {void}
         * @private
         */
        function reportPathForPatterns(node) {
            const importSource = node.source.value.trim();

            context.report({
                node,
                message: "'{{importSource}}' import is restricted from being used by a pattern.",
                data: {
                    importSource
                }
            });
        }

        /**
         * Report a restricted path specifically when using the '*' import.
         * @param {string} importSource - path of the import
         * @param {node} node - representing the restricted path reference
         * @returns {void}
         * @private
         */
        function reportPathForEverythingImported(importSource, node) {
            const importNames = restrictedPathMessages[importSource].importNames;

            context.report({
                node,
                message: "* import is invalid because '{{importNames}}' from '{{importSource}}' is restricted.",
                data: {
                    importSource,
                    importNames
                }
            });
        }

        /**
         * Check if the given importSource is restricted because '*' is being imported.
         * @param {string} importSource - path of the import
         * @param {Set.<string>} importNames - Set of import names that are being imported
         * @returns {boolean} whether the path is restricted
         * @private
         */
        function isRestrictedForEverythingImported(importSource, importNames) {
            return Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource) &&
                restrictedPathMessages[importSource].importNames &&
                isEverythingImported(importNames);
        }

        /**
         * Check if the given importNames are restricted given a list of restrictedImportNames.
         * @param {Set.<string>} importNames - Set of import names that are being imported
         * @param {[string]} restrictedImportNames - array of import names that are restricted for this import
         * @returns {boolean} whether the objectName is restricted
         * @private
         */
        function isRestrictedObject(importNames, restrictedImportNames) {
            return restrictedImportNames.some(restrictedObjectName => (
                importNames.has(restrictedObjectName)
            ));
        }

        /**
         * Check if the given importSource is a restricted path.
         * @param {string} importSource - path of the import
         * @param {Set.<string>} importNames - Set of import names that are being imported
         * @returns {boolean} whether the variable is a restricted path or not
         * @private
         */
        function isRestrictedPath(importSource, importNames) {
            let isRestricted = false;

            if (Object.prototype.hasOwnProperty.call(restrictedPathMessages, importSource)) {
                if (restrictedPathMessages[importSource].importNames) {
                    isRestricted = isRestrictedObject(importNames, restrictedPathMessages[importSource].importNames);
                } else {
                    isRestricted = true;
                }
            }

            return isRestricted;
        }

        /**
         * Check if the given importSource is restricted by a pattern.
         * @param {string} importSource - path of the import
         * @returns {boolean} whether the variable is a restricted pattern or not
         * @private
         */
        function isRestrictedPattern(importSource) {
            return restrictedPatterns.length > 0 && restrictedPatternsMatcher.ignores(importSource);
        }

        return {
            ImportDeclaration(node) {
                const importSource = node.source.value.trim();
                const importNames = node.specifiers.reduce((set, specifier) => {
                    if (specifier.type === "ImportDefaultSpecifier") {
                        set.add("default");
                    } else if (specifier.type === "ImportNamespaceSpecifier") {
                        set.add("*");
                    } else {
                        set.add(specifier.imported.name);
                    }
                    return set;
                }, new Set());

                if (isRestrictedForEverythingImported(importSource, importNames)) {
                    reportPathForEverythingImported(importSource, node);
                }

                if (isRestrictedPath(importSource, importNames)) {
                    reportPath(node);
                }
                if (isRestrictedPattern(importSource)) {
                    reportPathForPatterns(node);
                }
            }
        };
    }
};