Repository: eslint/eslint (JavaScript)

Issue:
no-restricted-modules and TemplateLiteral
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).

    This template is for bug reports. If you are here for another reason, please see below:

    1. To propose a new rule: https://eslint.org/docs/developer-guide/contributing/new-rules
    2. To request a rule change: https://eslint.org/docs/developer-guide/contributing/rule-changes
    3. To request a change that is not a bug fix, rule change, or new rule: https://eslint.org/docs/developer-guide/contributing/changes
    4. If you have any questions, please stop by our chatroom: https://gitter.im/eslint/eslint

    Note that leaving sections blank will make it difficult for us to troubleshoot and we may have to close the issue.
-->

**Tell us about your environment**

<!--
    If you are using ESLint v6.5.0 or later, you can run ESLint with the `--env-info` flag and paste the output here.
-->

* **ESLint Version:** `5.16.0` (in production), but issue exist also in `6.8.0`
* **Node Version:** `10.16.3`
* **npm Version:** `6.13.4`

**What parser (default, Babel-ESLint, etc.) are you using?**

`babel-eslint`

**Please show your full configuration:**

<details>
<summary>Configuration</summary>

Pasted full config below - but relevant part is in overrides configuration for `packages/gatsby-cli/src/**/*.js`

<!-- Paste your configuration below: -->
```js
const TSEslint = require("@typescript-eslint/eslint-plugin")

const gatsbyCLILocalReactImportPath = `packages/gatsby-cli/src/reporter/loggers/ink/react-instance-used-by-ink`

module.exports = {
  parser: "babel-eslint",
  extends: [
    "google",
    "eslint:recommended",
    "plugin:flowtype/recommended",
    "plugin:react/recommended",
    "prettier",
    "prettier/flowtype",
    "prettier/react",
  ],
  plugins: ["flowtype", "prettier", "react", "filenames"],
  parserOptions: {
    ecmaVersion: 2016,
    sourceType: "module",
    ecmaFeatures: {
      jsx: true,
    },
  },
  env: {
    browser: true,
    es6: true,
    node: true,
    jest: true,
  },
  globals: {
    before: true,
    after: true,
    spyOn: true,
    __PATH_PREFIX__: true,
    __BASE_PATH__: true,
    __ASSET_PREFIX__: true,
  },
  rules: {
    "arrow-body-style": [
      "error",
      "as-needed",
      { requireReturnForObjectLiteral: true },
    ],
    "no-unused-expressions": [
      "error",
      {
        allowTaggedTemplates: true,
      },
    ],
    "consistent-return": ["error"],
    "filenames/match-regex": ["error", "^[a-z-\\d\\.]+$", true],
    "no-console": "off",
    "no-inner-declarations": "off",
    "prettier/prettier": "error",
    quotes: ["error", "backtick"],
    "react/display-name": "off",
    "react/jsx-key": "warn",
    "react/no-unescaped-entities": "off",
    "react/prop-types": "off",
    "require-jsdoc": "off",
    "valid-jsdoc": "off",
  },
  overrides: [
    {
      files: [
        "packages/**/gatsby-browser.js",
        "packages/gatsby/cache-dir/**/*",
      ],
      env: {
        browser: true,
      },
      globals: {
        ___loader: false,
        ___emitter: false,
      },
    },
    {
      files: ["**/cypress/integration/**/*", "**/cypress/support/**/*"],
      globals: {
        cy: false,
        Cypress: false,
      },
    },
    {
      files: ["*.ts", "*.tsx"],
      parser: "@typescript-eslint/parser",
      plugins: ["@typescript-eslint/eslint-plugin"],
      rules: {
        ...TSEslint.configs.recommended.rules,
        // This rule ensures that typescript types do not have semicolons
        // at the end of their lines, since our prettier setup is to have no semicolons
        // e.g.,
        // interface Foo {
        // -  baz: string;
        // +  baz: string
        // }
        "@typescript-eslint/member-delimiter-style": [
          "error",
          {
            multiline: {
              delimiter: "none",
            },
          },
        ],
        // This ensures all interfaces are named with an I as a prefix
        // e.g.,
        // interface IFoo {}
        "@typescript-eslint/interface-name-prefix": [
          "error",
          { prefixWithI: "always" },
        ],
        "@typescript-eslint/no-empty-function": "off",
        // This ensures that we always type the return type of functions
        // a high level focus of our TS setup is typing fn inputs and outputs.
        "@typescript-eslint/explicit-function-return-type": "error",
        // This forces us to use interfaces over types aliases for object defintions.
        // Type is still useful for opaque types
        // e.g.,
        // type UUID = string
        "@typescript-eslint/consistent-type-definitions": [
          "error",
          "interface",
        ],

        // Allows us to write unions like `type Foo = "baz" | "bar"`
        // otherwise eslint will want to switch the strings to backticks,
        // which then crashes the ts compiler
        quotes: "off",
        "@typescript-eslint/quotes": [
          2,
          "backtick",
          {
            avoidEscape: true,
          },
        ],
      },
    },
    {
      files: [`packages/gatsby-cli/src/**/*.js`],
      rules: {
        "no-restricted-imports": [
          "error",
          {
            paths: [
              {
                name: "react",
                message: `Please import React from ${gatsbyCLILocalReactImportPath} (using relative local path) instead.`,
              },
            ],
          },
        ],
        "no-restricted-modules": [
          "error",
          {
            paths: [
              {
                name: "react",
                message: `Please import React from ${gatsbyCLILocalReactImportPath} (using relative local path) instead.`,
              },
            ],
          },
        ],
      },
    },
  ],
  settings: {
    react: {
      version: "16.4.2",
    },
  },
}

```

</details>

**What did you do? Please include the actual source code causing the issue, as well as the command that you used to run ESLint.**

<!-- Paste the source code below: -->
```js
// notice using TemplateLiteral as argument for `require`
// eslint-disable-next-line no-unused-vars, quotes
const React = require(`React`)
```

<!-- Paste the command you used to run ESLint: -->
```bash
yarn eslint packages/gatsby-cli/src/reporter/loggers/ink/test.js
```

**What did you expect to happen?**

For `no-restricted-modules` to raise linting error

**What actually happened? Please include the actual, raw output from ESLint.**

No linting error

**Are you willing to submit a pull request to fix this bug?**

Yes!

### Extra information / motivation:

The `no-restricted-modules` works well when using regular single or double quoted strings as argument for `require` calls - it results in expected linting error then.

However `gatsbyjs/gatsby` repository uses `quotes` rule with `backticks` ( https://github.com/gatsbyjs/gatsby/blob/e720d8efe58eba0f6fae9f26ec8879128967d0b5/.eslintrc.js#L53 ) which automatically will turn all `require` call arguments to use backticks, which makes it so the `no-restricted-modules` rule is ineffective.

Code in rule: https://github.com/eslint/eslint/blob/41de9df41a30a4300243bfe4ca26f716a787b2fc/lib/rules/no-restricted-modules.js#L159-L175

only handle cases of using `Literal` as argument, but it doesn't have handling for `TemplateLiteral` - see AST for:
  - `require("react")` (`Literal` case):

     ```json
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 16,
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 16,
        "callee": {
          "type": "Identifier",
          "start": 0,
          "end": 7,
          "name": "require"
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 8,
            "end": 15,
            "value": "react",
            "raw": "\"react\""
          }
        ]
      }
    }
     ```
  - ``require(`react`)`` (`TemplateLiteral` case):

     ```json
    {
      "type": "ExpressionStatement",
      "start": 17,
      "end": 33,
      "expression": {
        "type": "CallExpression",
        "start": 17,
        "end": 33,
        "callee": {
          "type": "Identifier",
          "start": 17,
          "end": 24,
          "name": "require"
        },
        "arguments": [
          {
            "type": "TemplateLiteral",
            "start": 25,
            "end": 32,
            "expressions": [],
            "quasis": [
              {
                "type": "TemplateElement",
                "start": 26,
                "end": 31,
                "value": {
                  "raw": "react",
                  "cooked": "react"
                },
                "tail": true
              }
            ]
          }
        ]
      }
    }
     ```

As mentioned in one of questions - I'm happy to provide pull request to adjust rule and tests to handle `TemplateLiteral` case (if it is desirable for the rule to handle it as well). 


Discussion:
I have verified this using [our demo](https://eslint.org/demo#eyJ0ZXh0IjoiLyogZXNsaW50IFwibm8tcmVzdHJpY3RlZC1tb2R1bGVzXCI6IFtcbiAgXCJlcnJvclwiLFxuICB7XG4gICAgcGF0aHM6IFtcbiAgICAgIHtcbiAgICAgICAgbmFtZTogXCJyZWFjdFwiLFxuICAgICAgICBtZXNzYWdlOiBcIlBsZWFzZSBpbXBvcnQgUmVhY3QgZnJvbSByZWxhdGl2ZSBsb2NhbCBwYXRoKSBpbnN0ZWFkLlwiXG4gICAgICB9XG4gICAgXVxuICB9XG5dICovXG5cbmNvbnN0IFJlYWN0ID0gcmVxdWlyZShcInJlYWN0XCIpO1xuY29uc3QgUmVhY3QyID0gcmVxdWlyZShgcmVhY3RgKTsgLy8gU2hvdWxkIHdhcm5cbiIsIm9wdGlvbnMiOnsicGFyc2VyT3B0aW9ucyI6eyJlY21hVmVyc2lvbiI6MTEsInNvdXJjZVR5cGUiOiJzY3JpcHQiLCJlY21hRmVhdHVyZXMiOnsianN4Ijp0cnVlfX0sInJ1bGVzIjp7fSwiZW52Ijp7ImJyb3dzZXIiOmZhbHNlfX19).

I think this is worth fixing, though please note that we are in the process of deprecating this rule (see https://github.com/eslint/eslint/issues/12835 for more info).
Thanks for quick response!

I don't want to add more work for you folks as I see there are already deprecation pull requests and migration of rules into `eslint-plugin-node` pull request. I personally can work around this (with vendored rule), but if you think it's still worthwile to make pull request here - I'll do it.
Thanks for being willing! I think it would be great to get this in before we deprecate it, since at that point we won't be making bug fixes (though the rule will live on in `eslint-plugin-node`). We can then either update the `eslint-plugin-node` PR or follow up with a second PR to add this to the plugin :) Please let us know if you have any questions while working on this!
I opened pull request ( #12927 ) and only questions I have are
> Does this change needs any documentation changes?
> Are added test fixtures enough?

But I included them in the PR description so maybe those could be answered there



Patch:
diff --git a/lib/rules/no-restricted-modules.js b/lib/rules/no-restricted-modules.js
index abbc30a05fb5..abd8d5cbe293 100644
--- a/lib/rules/no-restricted-modules.js
+++ b/lib/rules/no-restricted-modules.js
@@ -106,10 +106,19 @@ module.exports = {
          * @param {ASTNode} node The node to check.
          * @returns {boolean} If the node is a string literal.
          */
-        function isString(node) {
+        function isStringLiteral(node) {
             return node && node.type === "Literal" && typeof node.value === "string";
         }
 
+        /**
+         * Function to check if a node is a static string template literal.
+         * @param {ASTNode} node The node to check.
+         * @returns {boolean} If the node is a string template literal.
+         */
+        function isStaticTemplateLiteral(node) {
+            return node && node.type === "TemplateLiteral" && node.expressions.length === 0;
+        }
+
         /**
          * Function to check if a node is a require call.
          * @param {ASTNode} node The node to check.
@@ -119,14 +128,31 @@ module.exports = {
             return node.callee.type === "Identifier" && node.callee.name === "require";
         }
 
+        /**
+         * Extract string from Literal or TemplateLiteral node
+         * @param {ASTNode} node The node to extract from
+         * @returns {string|null} Extracted string or null if node doesn't represent a string
+         */
+        function getFirstArgumentString(node) {
+            if (isStringLiteral(node)) {
+                return node.value.trim();
+            }
+
+            if (isStaticTemplateLiteral(node)) {
+                return node.quasis[0].value.cooked.trim();
+            }
+
+            return null;
+        }
+
         /**
          * Report a restricted path.
          * @param {node} node representing the restricted path reference
+         * @param {string} name restricted path
          * @returns {void}
          * @private
          */
-        function reportPath(node) {
-            const name = node.arguments[0].value.trim();
+        function reportPath(node, name) {
             const customMessage = restrictedPathMessages[name];
             const messageId = customMessage
                 ? "customMessage"
@@ -156,21 +182,25 @@ module.exports = {
             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 }
-                            });
+                    // node has arguments
+                    if (node.arguments.length) {
+                        const name = getFirstArgumentString(node.arguments[0]);
+
+                        // if first argument is a string literal or a static string template literal
+                        if (name) {
+
+                            // check if argument value is in restricted modules array
+                            if (isRestrictedPath(name)) {
+                                reportPath(node, name);
+                            }
+
+                            if (restrictedPatterns.length > 0 && ig.ignores(name)) {
+                                context.report({
+                                    node,
+                                    messageId: "patternMessage",
+                                    data: { name }
+                                });
+                            }
                         }
                     }
                 }


Test patch:
diff --git a/tests/lib/rules/no-restricted-modules.js b/tests/lib/rules/no-restricted-modules.js
index f8b0a026bb38..08cc7c092f52 100644
--- a/tests/lib/rules/no-restricted-modules.js
+++ b/tests/lib/rules/no-restricted-modules.js
@@ -30,7 +30,9 @@ ruleTester.run("no-restricted-modules", rule, {
         { code: "var withPaths = require(\"foo/bar\");", options: [{ paths: ["foo", "bar"] }] },
         { code: "var withPatterns = require(\"foo/bar\");", options: [{ patterns: ["foo/c*"] }] },
         { code: "var withPatternsAndPaths = require(\"foo/bar\");", options: [{ paths: ["foo"], patterns: ["foo/c*"] }] },
-        { code: "var withGitignores = require(\"foo/bar\");", options: [{ paths: ["foo"], patterns: ["foo/*", "!foo/bar"] }] }
+        { code: "var withGitignores = require(\"foo/bar\");", options: [{ paths: ["foo"], patterns: ["foo/*", "!foo/bar"] }] },
+        { code: "require(`fs`)", options: ["crypto"], parserOptions: { ecmaVersion: 6 } },
+        { code: "require(`foo${bar}`)", options: ["foo"], parserOptions: { ecmaVersion: 6 } }
     ],
     invalid: [{
         code: "require(\"fs\")",
@@ -99,5 +101,15 @@ ruleTester.run("no-restricted-modules", rule, {
             data: { name: "foo", customMessage: "Please use 'bar' module instead." },
             type: "CallExpression"
         }]
+    }, {
+        code: "require(`fs`)",
+        options: ["fs"],
+        parserOptions: { ecmaVersion: 6 },
+        errors: [{ messageId: "defaultMessage", data: { name: "fs" }, type: "CallExpression" }]
+    }, {
+        code: "require(`crypt\\o`);",
+        options: ["crypto"],
+        parserOptions: { ecmaVersion: 6 },
+        errors: [{ messageId: "defaultMessage", data: { name: "crypto" }, type: "CallExpression" }]
     }]
 });