diff --git a/docs/rules/no-restricted-require.md b/docs/rules/no-restricted-require.md index ea30f80d..3d4192b4 100644 --- a/docs/rules/no-restricted-require.md +++ b/docs/rules/no-restricted-require.md @@ -14,96 +14,107 @@ This rule allows you to specify modules that you don’t want to use in your app ### Options -The rule takes one or more strings as options: the names of restricted modules. +The rule takes an array as options: the names of restricted modules. ```json -"no-restricted-require": ["error", "foo-module", "bar-module"] -``` - -It can also take an object with lists of `paths` and gitignore-style `patterns` strings. - -```json -"no-restricted-require": ["error", { "paths": ["foo-module", "bar-module"] }] -``` - -```json -"no-restricted-require": ["error", { - "paths": ["foo-module", "bar-module"], - "patterns": ["foo-module/private/*", "bar-module/*","!baz-module/good"] -}] -``` - -You may also specify a custom message for any paths you want to restrict as follows: - -```json -"no-restricted-require": ["error", { - "name": "foo-module", - "message": "Please use bar-module instead." - } -] +{ + "no-restricted-require": ["error", [ + "foo-module", + "bar-module" + ]] +} ``` -or like this: +You may also specify a custom message for each module you want to restrict as follows: ```json -"no-restricted-require": ["error",{ -"paths":[{ - "name": "foo-module", - "message": "Please use bar-module instead." - }] -}] +{ + "no-restricted-require": ["error", [ + { + "name": "foo-module", + "message": "Please use foo-module2 instead." + }, + { + "name": "bar-module", + "message": "Please use bar-module2 instead." + } + ]] +} ``` -The custom message will be appended to the default error message. Please note that you may not specify custom error messages for restricted patterns as a particular module may match more than one pattern. - - -To restrict the use of all Node.js core modules (via https://github.com/nodejs/node/tree/master/lib): +And you can use glob patterns in the `name` property. ```json { - "no-restricted-require": ["error", - "assert","buffer","child_process","cluster","crypto","dgram","dns","domain","events","freelist","fs","http","https","module","net","os","path","punycode","querystring","readline","repl","smalloc","stream","string_decoder","sys","timers","tls","tracing","tty","url","util","vm","zlib" - ] + "no-restricted-require": ["error", [ + { + "name": "lodash/*", + "message": "Please use xyz-module instead." + }, + { + "name": ["foo-module/private/*", "bar-module/*", "!baz-module/good"], + "message": "Please use xyz-module instead." + } + ]] } ``` -Examples of **incorrect** code for this rule with sample `"fs", "cluster", "lodash"` restricted modules: +And you can use absolute paths in the `name` property. ```js -/*eslint no-restricted-require: ["error", "fs", "cluster"]*/ - -var fs = require('fs'); -var cluster = require('cluster'); +module.exports = { + overrides: [ + { + files: "client/**", + rules: { + "no-restricted-require": ["error", [ + { + name: path.resolve(__dirname, "server/**"), + message: "Don't use server code from client code." + } + ]] + } + }, + { + files: "server/**", + rules: { + "no-restricted-require": ["error", [ + { + name: path.resolve(__dirname, "client/**"), + message: "Don't use client code from server code." + } + ]] + } + } + ] +} ``` -```js -/*eslint no-restricted-require: ["error", {"paths": ["cluster"] }]*/ +### Examples -var cluster = require('cluster'); -``` +Examples of **incorrect** code for this rule with sample `"fs", "cluster", "lodash"` restricted modules: ```js -/*eslint no-restricted-require: ["error", { "patterns": ["lodash/*"] }]*/ +/*eslint no-restricted-require: ["error", ["fs", "cluster", "lodash/*"]]*/ -var pick = require('lodash/pick'); +const fs = require('fs'); +const cluster = require('cluster'); +const pick = require('lodash/pick'); ``` Examples of **correct** code for this rule with sample `"fs", "cluster", "lodash"` restricted modules: ```js -/*eslint no-restricted-require: ["error", "fs", "cluster"]*/ +/*eslint no-restricted-require: ["error", ["fs", "cluster", "lodash/*"]]*/ -var crypto = require('crypto'); +const crypto = require('crypto'); +const _ = require('lodash'); ``` ```js -/*eslint no-restricted-require: ["error", { - "paths": ["fs", "cluster"], - "patterns": ["lodash/*", "!lodash/pick"] -}]*/ +/*eslint no-restricted-require: ["error", ["fs", "cluster", { "name": ["lodash/*", "!lodash/pick"] }]]*/ -var crypto = require('crypto'); -var pick = require('lodash/pick'); +const pick = require('lodash/pick'); ``` ## 🔎 Implementation diff --git a/lib/rules/no-restricted-require.js b/lib/rules/no-restricted-require.js index 27673bfc..eb14614a 100644 --- a/lib/rules/no-restricted-require.js +++ b/lib/rules/no-restricted-require.js @@ -1,38 +1,12 @@ /** * @author Christian Schulz + * @author Toru Nagashima * See LICENSE file in root directory for full license. */ "use strict" -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, - }, - }, - additionalProperties: false, - required: ["name"], - }, - ], - }, - uniqueItems: true, -} +const check = require("../util/check-restricted") +const visit = require("../util/visit-require") module.exports = { meta: { @@ -45,148 +19,44 @@ module.exports = { "https://github.com/mysticatea/eslint-plugin-node/blob/v11.0.0/docs/rules/no-restricted-require.md", }, fixable: null, - schema: { - anyOf: [ - arrayOfStringsOrObjects, - { - type: "array", - items: { - type: "object", - properties: { - paths: arrayOfStringsOrObjects, - patterns: arrayOfStrings, + schema: [ + { + type: "array", + items: { + anyOf: [ + { type: "string" }, + { + type: "object", + properties: { + name: { + anyOf: [ + { type: "string" }, + { + type: "array", + items: { type: "string" }, + additionalItems: false, + }, + ], + }, + message: { type: "string" }, + }, + additionalProperties: false, + required: ["name"], }, - additionalProperties: false, - }, - additionalItems: false, + ], }, - ], - }, + additionalItems: false, + }, + ], messages: { - defaultMessage: "'{{name}}' module is restricted from being used.", - customMessage: + restricted: // eslint-disable-next-line @mysticatea/eslint-plugin/report-message-format - "'{{name}}' module is restricted from being used. {{customMessage}}", - patternMessage: - "'{{name}}' module is restricted from being used by a pattern.", + "'{{name}}' module is restricted from being used.{{customMessage}}", }, }, create(context) { - 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 restrictedPatterns = - (isPathAndPatternsObject ? options[0].patterns : []) || [] - - const restrictedPathMessages = restrictedPaths.reduce( - (memo, importName) => { - if (typeof importName === "string") { - memo[importName] = null - } else { - memo[importName.name] = importName.message - } - return memo - }, - {} - ) - - // if no imports are restricted we don"t need to check - if ( - Object.keys(restrictedPaths).length === 0 && - restrictedPatterns.length === 0 - ) { - return {} - } - - const ig = ignore().add(restrictedPatterns) - - /** - * Function to check if a node is a string literal. - * @param {ASTNode} node The node to check. - * @returns {boolean} If the node is a string literal. - */ - function isString(node) { - return ( - node && - node.type === "Literal" && - typeof node.value === "string" - ) - } - - /** - * Function to check if a node is a require call. - * @param {ASTNode} node The node to check. - * @returns {boolean} If the node is a require call. - */ - function isRequireCall(node) { - return ( - node.callee.type === "Identifier" && - node.callee.name === "require" - ) - } - - /** - * Report a restricted path. - * @param {node} node representing the restricted path reference - * @returns {void} - * @private - */ - function reportPath(node) { - const name = node.arguments[0].value.trim() - const customMessage = restrictedPathMessages[name] - const messageId = customMessage ? "customMessage" : "defaultMessage" - - context.report({ - node, - messageId, - data: { - name, - customMessage, - }, - }) - } - - /** - * Check if the given name is a restricted path name - * @param {string} name name of a variable - * @returns {boolean} whether the variable is a restricted path or not - * @private - */ - function isRestrictedPath(name) { - return Object.prototype.hasOwnProperty.call( - restrictedPathMessages, - name - ) - } - - return { - CallExpression(node) { - if (isRequireCall(node)) { - // node has arguments and first argument is string - if (node.arguments.length && isString(node.arguments[0])) { - const name = node.arguments[0].value.trim() - - // check if argument value is in restricted modules array - if (isRestrictedPath(name)) { - reportPath(node) - } - - if (restrictedPatterns.length > 0 && ig.ignores(name)) { - context.report({ - node, - messageId: "patternMessage", - data: { name }, - }) - } - } - } - }, - } + const opts = { includeCore: true } + return visit(context, opts, targets => check(context, targets)) }, } diff --git a/lib/util/check-restricted.js b/lib/util/check-restricted.js new file mode 100644 index 00000000..0eae4f86 --- /dev/null +++ b/lib/util/check-restricted.js @@ -0,0 +1,109 @@ +/** + * @author Toru Nagashima + * See LICENSE file in root directory for full license. + */ +"use strict" + +const path = require("path") +const { Minimatch } = require("minimatch") + +/** @typedef {import("../util/import-target")} ImportTarget */ +/** + * @typedef {Object} DefinitionData + * @property {string | string[]} name The name to disallow. + * @property {string} [message] The custom message to show. + */ + +/** + * Check if matched or not. + * @param {InstanceType} matcher The matcher. + * @param {boolean} absolute The flag that the matcher is for absolute paths. + * @param {ImportTarget} importee The importee information. + */ +function match(matcher, absolute, { filePath, name }) { + if (absolute) { + return filePath != null && matcher.match(filePath) + } + return matcher.match(name) +} + +/** Restriction. */ +class Restriction { + /** + * Initialize this restriction. + * @param {DefinitionData} def The definition of a restriction. + */ + constructor({ name, message }) { + const names = Array.isArray(name) ? name : [name] + const matchers = names.map(raw => { + const negate = raw[0] === "!" && raw[1] !== "(" + const pattern = negate ? raw.slice(1) : raw + const absolute = path.isAbsolute(pattern) + const matcher = new Minimatch(pattern, { dot: true }) + return { absolute, matcher, negate } + }) + + this.matchers = matchers + this.message = message ? ` ${message}` : "" + } + + /** + * Check if a given importee is disallowed. + * @param {ImportTarget} importee The importee to check. + * @returns {boolean} `true` if the importee is disallowed. + */ + match(importee) { + return this.matchers.reduce( + (ret, { absolute, matcher, negate }) => + negate + ? ret && !match(matcher, absolute, importee) + : ret || match(matcher, absolute, importee), + false + ) + } +} + +/** + * Create a restriction. + * @param {string | DefinitionData} def A definition. + * @returns {Restriction} Created restriction. + */ +function createRestriction(def) { + if (typeof def === "string") { + return new Restriction({ name: def }) + } + return new Restriction(def) +} + +/** + * Create restrictions. + * @param {(string | DefinitionData | GlobDefinition)[]} defs Definitions. + * @returns {(Restriction | GlobRestriction)[]} Created restrictions. + */ +function createRestrictions(defs) { + return (defs || []).map(createRestriction) +} + +/** + * Checks if given importees are disallowed or not. + * @param {RuleContext} context - A context to report. + * @param {ImportTarget[]} targets - A list of target information to check. + * @returns {void} + */ +module.exports = function checkForRestriction(context, targets) { + const restrictions = createRestrictions(context.options[0]) + + for (const target of targets) { + const restriction = restrictions.find(r => r.match(target)) + if (restriction) { + context.report({ + node: target.node, + messageId: "restricted", + data: { + name: target.name, + customMessage: restriction.message, + }, + }) + } + } +} diff --git a/tests/lib/rules/no-restricted-require.js b/tests/lib/rules/no-restricted-require.js index b1068d32..8970408c 100644 --- a/tests/lib/rules/no-restricted-require.js +++ b/tests/lib/rules/no-restricted-require.js @@ -4,173 +4,201 @@ */ "use strict" +const path = require("path") const RuleTester = require("eslint").RuleTester const rule = require("../../../lib/rules/no-restricted-require") -new RuleTester().run("no-restricted-require", rule, { +new RuleTester({ + globals: { require: "readonly" }, +}).run("no-restricted-require", rule, { valid: [ - { code: 'require("fs")', options: ["crypto"] }, - { code: 'require("path")', options: ["crypto", "stream", "os"] }, + { code: 'require("fs")', options: [["crypto"]] }, + { code: 'require("path")', options: [["crypto", "stream", "os"]] }, 'require("fs ")', - { code: "require(2)", options: ["crypto"] }, - { code: "require(foo)", options: ["crypto"] }, - { code: "var foo = bar('crypto');", options: ["crypto"] }, - { code: 'require("foo/bar");', options: ["foo"] }, + { code: "require(2)", options: [["crypto"]] }, + { code: "require(foo)", options: [["crypto"]] }, + { code: "bar('crypto');", options: [["crypto"]] }, + { code: 'require("foo/bar");', options: [["foo"]] }, { - code: 'var withPaths = require("foo/bar");', - options: [{ paths: ["foo", "bar"] }], + code: 'require("foo/bar");', + options: [[{ name: ["foo", "bar"] }]], + }, + { + code: 'require("foo/bar");', + options: [[{ name: ["foo/c*"] }]], + }, + { + code: 'require("foo/bar");', + options: [[{ name: ["foo"] }, { name: ["foo/c*"] }]], + }, + { + code: 'require("foo/bar");', + options: [[{ name: ["foo"] }, { name: ["foo/*", "!foo/bar"] }]], + }, + { + code: 'require("os ")', + options: [["fs", "crypto ", "stream", "os"]], + }, + { + code: 'require("./foo")', + options: [["foo"]], }, { - code: 'var withPatterns = require("foo/bar");', - options: [{ patterns: ["foo/c*"] }], + code: 'require("foo")', + options: [["./foo"]], }, { - code: 'var withPatternsAndPaths = require("foo/bar");', - options: [{ paths: ["foo"], patterns: ["foo/c*"] }], + code: 'require("foo/bar");', + options: [[{ name: "@foo/bar" }]], }, { - code: 'var withGitignores = require("foo/bar");', - options: [{ paths: ["foo"], patterns: ["foo/*", "!foo/bar"] }], + filename: path.resolve(__dirname, "lib/sub/test.js"), + code: 'require("../foo");', + options: [[{ name: path.resolve(__dirname, "foo") }]], }, ], invalid: [ { code: 'require("fs")', - options: ["fs"], + options: [["fs"]], errors: [ { - messageId: "defaultMessage", - data: { name: "fs" }, - type: "CallExpression", + messageId: "restricted", + data: { name: "fs", customMessage: "" }, }, ], }, { - code: 'require("os ")', - options: ["fs", "crypto ", "stream", "os"], + code: 'require("foo/bar");', + options: [["foo/bar"]], errors: [ { - messageId: "defaultMessage", - data: { name: "os" }, - type: "CallExpression", + messageId: "restricted", + data: { name: "foo/bar", customMessage: "" }, }, ], }, { code: 'require("foo/bar");', - options: ["foo/bar"], + options: [[{ name: ["foo/bar"] }]], errors: [ { - messageId: "defaultMessage", - data: { name: "foo/bar" }, - type: "CallExpression", + messageId: "restricted", + data: { name: "foo/bar", customMessage: "" }, }, ], }, { - code: 'var withPaths = require("foo/bar");', - options: [{ paths: ["foo/bar"] }], + code: 'require("foo/bar");', + options: [[{ name: ["foo/*"] }]], errors: [ { - messageId: "defaultMessage", - data: { name: "foo/bar" }, - type: "CallExpression", + messageId: "restricted", + data: { name: "foo/bar", customMessage: "" }, }, ], }, { - code: 'var withPatterns = require("foo/bar");', - options: [{ patterns: ["foo/*"] }], + code: 'require("foo/bar");', + options: [[{ name: ["foo/*"] }, { name: ["foo"] }]], errors: [ { - messageId: "patternMessage", - data: { name: "foo/bar" }, - type: "CallExpression", + messageId: "restricted", + data: { name: "foo/bar", customMessage: "" }, }, ], }, { - code: 'var withPatternsAndPaths = require("foo/bar");', - options: [{ patterns: ["foo/*"], paths: ["foo"] }], + code: 'require("foo/bar");', + options: [[{ name: ["foo/*", "!foo/baz"] }, { name: ["foo"] }]], errors: [ { - messageId: "patternMessage", - data: { name: "foo/bar" }, - type: "CallExpression", + messageId: "restricted", + data: { name: "foo/bar", customMessage: "" }, }, ], }, { - code: 'var withGitignores = require("foo/bar");', - options: [{ patterns: ["foo/*", "!foo/baz"], paths: ["foo"] }], + code: 'require("foo");', + options: [ + [ + { + name: "foo", + message: "Please use 'bar' module instead.", + }, + ], + ], errors: [ { - messageId: "patternMessage", - data: { name: "foo/bar" }, - type: "CallExpression", + messageId: "restricted", + data: { + name: "foo", + customMessage: " Please use 'bar' module instead.", + }, }, ], }, { - code: 'var withGitignores = require("foo");', + code: 'require("bar");', options: [ - { - name: "foo", - message: "Please use 'bar' module instead.", - }, + [ + "foo", + { + name: "bar", + message: "Please use 'baz' module instead.", + }, + "baz", + ], ], errors: [ { - messageId: "customMessage", + messageId: "restricted", data: { - name: "foo", - customMessage: "Please use 'bar' module instead.", + name: "bar", + customMessage: " Please use 'baz' module instead.", }, - type: "CallExpression", }, ], }, { - code: 'var withGitignores = require("bar");', - options: [ - "foo", + code: 'require("@foo/bar");', + options: [[{ name: "@foo/*" }]], + errors: [ { - name: "bar", - message: "Please use 'baz' module instead.", + messageId: "restricted", + data: { name: "@foo/bar", customMessage: "" }, }, - "baz", ], + }, + { + code: 'require("./foo/bar");', + options: [[{ name: "./foo/*" }]], errors: [ { - messageId: "customMessage", - data: { - name: "bar", - customMessage: "Please use 'baz' module instead.", - }, - type: "CallExpression", + messageId: "restricted", + data: { name: "./foo/bar", customMessage: "" }, }, ], }, { - code: 'var withGitignores = require("foo");', - options: [ + filename: path.resolve(__dirname, "lib/test.js"), + code: 'require("../foo");', + options: [[{ name: path.resolve(__dirname, "foo") }]], + errors: [ { - paths: [ - { - name: "foo", - message: "Please use 'bar' module instead.", - }, - ], + messageId: "restricted", + data: { name: "../foo", customMessage: "" }, }, ], + }, + { + filename: path.resolve(__dirname, "lib/sub/test.js"), + code: 'require("../../foo");', + options: [[{ name: path.resolve(__dirname, "foo") }]], errors: [ { - messageId: "customMessage", - data: { - name: "foo", - customMessage: "Please use 'bar' module instead.", - }, - type: "CallExpression", + messageId: "restricted", + data: { name: "../../foo", customMessage: "" }, }, ], },