Repository: eslint/eslint (js)

Issue:
Support for "message" with "patterns" in `no-restricted-imports`
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).

    This template is for requesting a rule change. If you are here for another reason, please see below:

    1. To report a bug: https://eslint.org/docs/developer-guide/contributing/reporting-bugs
    2. To propose a new rule: https://eslint.org/docs/developer-guide/contributing/new-rules
    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.
-->

Creating a new request for a [closed issue](https://github.com/eslint/eslint/issues/11134), as suggested [here](https://github.com/eslint/eslint/issues/11134#issuecomment-498474195).

**What rule do you want to change?** no-restricted-imports

**Does this change cause the rule to produce more or fewer warnings?** Neither

**How will the change be implemented? (New option, new default behavior, etc.)?** Allow a new format for an existing option

**Please provide some example code that this change will affect:**

<!-- Put your code examples here -->
```js
// I can currently customize the error message for restricted path imports like this
import * as moment from 'moment';
// But I can't customize the error message for restricted pattern imports like this
import { Button } from '../../framework/buttons';
```

What I'd like to do in my ESLint config:

```json
{
  "rules": {
    "no-restricted-imports": ["error", {
      "paths": [{"name": "moment", "message": "Don't import moment"}],
      "patterns": [{"name": "**/framework/*", "message": "Only import from the root of /framework"}]
    }]
  }
}
```

**What does the rule currently do for this code?** I can't customize the error message for the restricted pattern import

**What will the rule do after it's changed?** It should allow me to customize the error message for the restricted pattern import

**Are you willing to submit a pull request to implement this change?** Sure, though I'd like to get some clarity on how difficult the "large option schema change" mentioned [here](https://github.com/eslint/eslint/issues/11134#issuecomment-442213289) would be.


Pull request:
Update: no-restricted-imports custom message for patterns (fixes #11843)
…

<!--
    Thank you for contributing!

    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

#### Prerequisites checklist

- [x] I have read the [contributing guidelines](https://github.com/eslint/eslint/blob/master/CONTRIBUTING.md).

#### What is the purpose of this pull request? (put an "X" next to an item)

<!--
    The following template is intentionally not a markdown checkbox list for the reasons
    explained in https://github.com/eslint/eslint/pull/12848#issuecomment-580302888
-->

[ ] Documentation update
[ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md))
[ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md))
[x] Changes an existing rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-change-proposal.md))
[ ] Add autofixing to a rule
[ ] Add a CLI option
[ ] Add something to the core
[ ] Other, please explain:

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (https://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

#### What changes did you make? (Give an overview)

As per the spec agreed upon in https://github.com/eslint/eslint/issues/11843 - the rule `no-restricted-imports` now allows multiple patterns to be grouped together with a message that is logged if any match fails

For example, `{ patterns: [{ group: ["foo/*", "bar/*"], message: "'foo' and 'bar' are deprecated, please use 'baz' instead." }] }` would log error `'foo/some/subimport' import is restricted from being used by a pattern. 'foo' and 'bar' are deprecated, please use 'baz' instead.`

#### Is there anything you'd like reviewers to focus on?

Wrote this for a personal project, but happy to tweak anything required to shepherd it through any part of the community process that I've missed from the contributor guidelines or add some more robust test edge cases if proposed.


Patch:
diff --git a/README.md b/README.md
index a4f4a04dd..544da51eb 100644
--- a/README.md
+++ b/README.md
@@ -286,9 +286,9 @@ The following companies, organizations, and individuals support ESLint's ongoing
 <!--sponsorsstart-->
 <h3>Platinum Sponsors</h3>
 <p><a href="https://automattic.com"><img src="https://images.opencollective.com/photomatt/d0ef3e1/logo.png" alt="Automattic" height="undefined"></a></p><h3>Gold Sponsors</h3>
-<p><a href="https://nx.dev"><img src="https://images.opencollective.com/nx/0efbe42/logo.png" alt="Nx (by Nrwl)" height="96"></a> <a href="https://google.com/chrome"><img src="https://images.opencollective.com/chrome/dc55bd4/logo.png" alt="Chrome's Web Framework & Tools Performance Fund" height="96"></a> <a href="https://www.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a> <a href="https://opensource.microsoft.com"><img src="https://avatars.githubusercontent.com/u/6154722?v=4" alt="Microsoft" height="96"></a> <a href="https://substack.com/"><img src="https://avatars.githubusercontent.com/u/53023767?v=4" alt="Substack" height="96"></a></p><h3>Silver Sponsors</h3>
+<p><a href="https://nx.dev"><img src="https://images.opencollective.com/nx/0efbe42/logo.png" alt="Nx (by Nrwl)" height="96"></a> <a href="https://google.com/chrome"><img src="https://images.opencollective.com/chrome/dc55bd4/logo.png" alt="Chrome's Web Framework & Tools Performance Fund" height="96"></a> <a href="https://www.salesforce.com"><img src="https://images.opencollective.com/salesforce/ca8f997/logo.png" alt="Salesforce" height="96"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="96"></a> <a href="https://substack.com/"><img src="https://avatars.githubusercontent.com/u/53023767?v=4" alt="Substack" height="96"></a></p><h3>Silver Sponsors</h3>
 <p><a href="https://retool.com/"><img src="https://images.opencollective.com/retool/98ea68e/logo.png" alt="Retool" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a></p><h3>Bronze Sponsors</h3>
-<p><a href="https://buy.fineproxy.org/eng/"><img src="https://images.opencollective.com/buy-fineproxy-org/b282e39/logo.png" alt="Buy.Fineproxy.Org" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="null"><img src="https://images.opencollective.com/bugsnag-stability-monitoring/c2cef36/logo.png" alt="Bugsnag Stability Monitoring" height="32"></a> <a href="https://mixpanel.com"><img src="https://images.opencollective.com/mixpanel/cd682f7/logo.png" alt="Mixpanel" height="32"></a> <a href="https://www.vpsserver.com"><img src="https://images.opencollective.com/vpsservercom/logo.png" alt="VPS Server" height="32"></a> <a href="https://icons8.com"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8: free icons, photos, illustrations, and music" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/7e3d9a9/logo.png" alt="Discord" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.firesticktricks.com"><img src="https://images.opencollective.com/fire-stick-tricks/b8fbe2c/logo.png" alt="Fire Stick Tricks" height="32"></a></p>
+<p><a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" height="32"></a> <a href="null"><img src="https://images.opencollective.com/bugsnag-stability-monitoring/c2cef36/logo.png" alt="Bugsnag Stability Monitoring" height="32"></a> <a href="https://mixpanel.com"><img src="https://images.opencollective.com/mixpanel/cd682f7/logo.png" alt="Mixpanel" height="32"></a> <a href="https://www.vpsserver.com"><img src="https://images.opencollective.com/vpsservercom/logo.png" alt="VPS Server" height="32"></a> <a href="https://icons8.com"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8: free icons, photos, illustrations, and music" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/7e3d9a9/logo.png" alt="Discord" height="32"></a> <a href="https://themeisle.com"><img src="https://images.opencollective.com/themeisle/d5592fe/logo.png" alt="ThemeIsle" height="32"></a> <a href="https://www.firesticktricks.com"><img src="https://images.opencollective.com/fire-stick-tricks/b8fbe2c/logo.png" alt="Fire Stick Tricks" height="32"></a></p>
 <!--sponsorsend-->
 
 ## <a name="technology-sponsors"></a>Technology Sponsors
diff --git a/docs/rules/no-restricted-imports.md b/docs/rules/no-restricted-imports.md
index 0f6841340..f1577d7d8 100644
--- a/docs/rules/no-restricted-imports.md
+++ b/docs/rules/no-restricted-imports.md
@@ -75,7 +75,21 @@ or like this if you need to restrict only certain imports from a module:
 }]
 ```
 
-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 import may match more than one pattern.
+or like this if you want to apply a custom message to pattern matches:
+
+```json
+"no-restricted-imports": ["error", {
+    "patterns": [{
+      "group": ["import1/private/*"],
+      "message": "usage of import1 private modules not allowed."
+    }, {
+      "group": ["import2/*", "!import2/good"],
+      "message": "import2 is deprecated, except the modules in import2/good."
+    }]
+}]
+```
+
+The custom message will be appended to the default error message.
 
 To restrict the use of all Node.js core imports (via https://github.com/nodejs/node/tree/master/lib):
 
@@ -149,6 +163,15 @@ import { DisallowedObject as AllowedObject } from "foo";
 import * as Foo from "foo";
 ```
 
+```js
+/*eslint no-restricted-imports: ["error", { patterns: [{
+    group: ["lodash/*"],
+    message: "Please use the default import from 'lodash' instead."
+}]}]*/
+
+import pick from 'lodash/pick';
+```
+
 Examples of **correct** code for this rule:
 
 ```js
@@ -182,6 +205,15 @@ import DisallowedObject from "foo"
 import { AllowedObject as DisallowedObject } from "foo";
 ```
 
+```js
+/*eslint no-restricted-imports: ["error", { patterns: [{
+    group: ["lodash/*"],
+    message: "Please use the default import from 'lodash' instead."
+}]}]*/
+
+import lodash from 'lodash';
+```
+
 ## When Not To Use It
 
 Don't use this rule or don't include a module in the list for this rule if you want to be able to import a module in your project without an ESLint error or warning.
diff --git a/lib/rules/no-restricted-imports.js b/lib/rules/no-restricted-imports.js
index c205dad8b..414164d29 100644
--- a/lib/rules/no-restricted-imports.js
+++ b/lib/rules/no-restricted-imports.js
@@ -10,12 +10,6 @@
 
 const ignore = require("ignore");
 
-const arrayOfStrings = {
-    type: "array",
-    items: { type: "string" },
-    uniqueItems: true
-};
-
 const arrayOfStringsOrObjects = {
     type: "array",
     items: {
@@ -44,6 +38,41 @@ const arrayOfStringsOrObjects = {
     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",
@@ -61,6 +90,8 @@ module.exports = {
             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
@@ -80,7 +111,7 @@ module.exports = {
                         type: "object",
                         properties: {
                             paths: arrayOfStringsOrObjects,
-                            patterns: arrayOfStrings
+                            patterns: arrayOfStringsOrObjectPatterns
                         },
                         additionalProperties: false
                     }],
@@ -98,13 +129,6 @@ module.exports = {
             (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 : []) || [];
-
-        // if no imports are restricted we don"t need to check
-        if (Object.keys(restrictedPaths).length === 0 && restrictedPatterns.length === 0) {
-            return {};
-        }
-
         const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
             if (typeof importSource === "string") {
                 memo[importSource] = { message: null };
@@ -117,7 +141,16 @@ module.exports = {
             return memo;
         }, {});
 
-        const restrictedPatternsMatcher = ignore().add(restrictedPatterns);
+        // 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.
@@ -184,17 +217,19 @@ module.exports = {
         /**
          * 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) {
+        function reportPathForPatterns(node, group) {
             const importSource = node.source.value.trim();
 
             context.report({
                 node,
-                messageId: "patterns",
+                messageId: group.customMessage ? "patternWithCustomMessage" : "patterns",
                 data: {
-                    importSource
+                    importSource,
+                    customMessage: group.customMessage
                 }
             });
         }
@@ -202,11 +237,12 @@ module.exports = {
         /**
          * 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) {
-            return restrictedPatterns.length > 0 && restrictedPatternsMatcher.ignores(importSource);
+        function isRestrictedPattern(importSource, group) {
+            return group.matcher.ignores(importSource);
         }
 
         /**
@@ -249,10 +285,11 @@ module.exports = {
             }
 
             checkRestrictedPathAndReport(importSource, importNames, node);
-
-            if (isRestrictedPattern(importSource)) {
-                reportPathForPatterns(node);
-            }
+            restrictedPatternGroups.forEach(group => {
+                if (isRestrictedPattern(importSource, group)) {
+                    reportPathForPatterns(node, group);
+                }
+            });
         }
 
         return {


Test patch:
diff --git a/tests/bench/large.js b/tests/bench/large.js
index c94e88568..a3db1ac6c 100644
--- a/tests/bench/large.js
+++ b/tests/bench/large.js
@@ -57078,7 +57078,7 @@ var JSHINT = (function () {
   FutureReservedWord("transient");
   FutureReservedWord("volatile");
 
-  // this function is used to determine wether a squarebracket or a curlybracket
+  // this function is used to determine whether a squarebracket or a curlybracket
   // expression is a comprehension array, destructuring assignment or a json value.
 
   var lookupBlockType = function () {
@@ -60568,4 +60568,4 @@ exports.yui = {
 },{}]},{},["nr+AlQ"])
 JSHINT = require('jshint').JSHINT;
 if (typeof exports === 'object' && exports) exports.JSHINT = JSHINT;
-}());
\ No newline at end of file
+}());
diff --git a/tests/lib/rules/no-restricted-imports.js b/tests/lib/rules/no-restricted-imports.js
index 363229712..f54d4df03 100644
--- a/tests/lib/rules/no-restricted-imports.js
+++ b/tests/lib/rules/no-restricted-imports.js
@@ -37,6 +37,10 @@ ruleTester.run("no-restricted-imports", rule, {
             code: "import withGitignores from \"foo/bar\";",
             options: [{ patterns: ["foo/*", "!foo/bar"] }]
         },
+        {
+            code: "import withPatterns from \"foo/bar\";",
+            options: [{ patterns: [{ group: ["foo/*", "!foo/bar"], message: "foo is forbidden, use bar instead" }] }]
+        },
         {
             code: "import AllowedObject from \"foo\";",
             options: [{
@@ -241,6 +245,36 @@ ruleTester.run("no-restricted-imports", rule, {
             column: 1,
             endColumn: 36
         }]
+    }, {
+        code: "import withPatterns from \"foo/baz\";",
+        options: [{ patterns: [{ group: ["foo/*", "!foo/bar"], message: "foo is forbidden, use foo/bar instead" }] }],
+        errors: [{
+            message: "'foo/baz' import is restricted from being used by a pattern. foo is forbidden, use foo/bar instead",
+            type: "ImportDeclaration",
+            line: 1,
+            column: 1,
+            endColumn: 36
+        }]
+    }, {
+        code: "import withPatterns from \"foo/baz\";",
+        options: [{ patterns: [{ group: ["foo/bar", "foo/baz"], message: "some foo subimports are restricted" }] }],
+        errors: [{
+            message: "'foo/baz' import is restricted from being used by a pattern. some foo subimports are restricted",
+            type: "ImportDeclaration",
+            line: 1,
+            column: 1,
+            endColumn: 36
+        }]
+    }, {
+        code: "import withPatterns from \"foo/bar\";",
+        options: [{ patterns: [{ group: ["foo/bar"] }] }],
+        errors: [{
+            message: "'foo/bar' import is restricted from being used by a pattern.",
+            type: "ImportDeclaration",
+            line: 1,
+            column: 1,
+            endColumn: 36
+        }]
     }, {
         code: "import withGitignores from \"foo/bar\";",
         options: [{ patterns: ["foo/*", "!foo/baz"] }],
diff --git a/tests/lib/rules/no-sequences.js b/tests/lib/rules/no-sequences.js
index f9eb8a6a3..1ac7c4a6c 100644
--- a/tests/lib/rules/no-sequences.js
+++ b/tests/lib/rules/no-sequences.js
@@ -59,7 +59,11 @@ ruleTester.run("no-sequences", rule, {
 
         // valid code with "allowInParentheses" set to `false`
         { code: "for ((i = 0, j = 0); test; );", options: [{ allowInParentheses: false }] },
-        { code: "for (; test; (i++, j++));", options: [{ allowInParentheses: false }] }
+        { code: "for (; test; (i++, j++));", options: [{ allowInParentheses: false }] },
+
+        // https://github.com/eslint/eslint/issues/14572
+        { code: "const foo = () => { return ((bar = 123), 10) }", env: { es6: true } },
+        { code: "const foo = () => (((bar = 123), 10));", env: { es6: true } }
     ],
 
     // Examples of code that should trigger the rule