Repository: eslint/eslint (js)

Issue:
no-unused-vars with ignoreRestSiblings:true reports false-positives
<!--
    ESLint adheres to the Open JS Foundation Code of Conduct:
    https://eslint.org/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://eslint.org/chat/help

    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:** 7.21.0
* **Node Version:** 12.16.2
* **npm Version:** 6.14.4
* **Operating System:** Windows

**What parser (default, `@babel/eslint-parser`, `@typescript-eslint/parser`, etc.) are you using?**
babel-eslint

**Please show your full configuration:**

<details>
<summary>Configuration</summary>

<!-- Paste your configuration below: -->
```js
module.exports = {
    extends: 'airbnb',
    parser: 'babel-eslint',
    plugins: [
        'jasmine',
        'webdriverio',
        'react-redux',
    ],
    env: {
        'browser': true,
        'node': true,
        'jasmine': true,
        'webdriverio/wdio': true,
    },
    rules: {
        'import/no-cycle': 'off',
        'react/jsx-props-no-spreading': 'off',
        'react/no-access-state-in-setstate': 'off',
        'arrow-parens': ['error', "as-needed", { "requireForBlockBody": true }],
        'function-paren-newline': 'off',
        'indent': ['error', 4, { 'SwitchCase': 1 }],
        'no-undef': 'error',
        'no-underscore-dangle': 'off',
        'no-multiple-empty-lines': ['warn', { "max": 2 } ],
        'no-else-return': ['error', { allowElseIf: true }],
        'no-restricted-globals': [
            "error",
            { name: 'Set', message: 'Use window.Set or add an eslint-ignore if native Set is what you want' },
            { name: 'Map', message: 'Use window.Map or add an eslint-ignore if native Map is what you want' }
        ],
        'max-len': ['error', 120],
        'implicit-arrow-linebreak': 'off',
        'jsx-a11y/click-events-have-key-events': 'off',
        'jsx-a11y/interactive-supports-focus': 'off',
        'jsx-a11y/label-has-for': 'off',
        'jsx-a11y/label-has-associated-control': 'off',
        'jsx-a11y/no-noninteractive-element-interactions': 'off',
        'jsx-a11y/no-static-element-interactions': 'off',
        'jsx-a11y/anchor-is-valid': 'off',
        'jsx-a11y/control-has-associated-label': 'off',
        'class-methods-use-this': 'off',
        'no-plusplus': ['error', { 'allowForLoopAfterthoughts': true }],
        'no-mixed-operators': ['error', { 'allowSamePrecedence': true }],
        'no-multi-spaces': ['error', { ignoreEOLComments: true }],
        'no-console': 'warn',
        'no-continue': 'off',
        'no-unused-vars': ['error', { 'ignoreRestSiblings': true }],
        'object-curly-newline': 'off',
        'operator-linebreak': ["warn", "after"],
        'prefer-destructuring': 'off',
        'prefer-promise-reject-errors': 'warn',
        'react/sort-comp': ['warn', { order: ['static-methods',  'lifecycle', 'everything-else', 'render'] }],
        'react/forbid-prop-types': 'off',
        'react/jsx-fragments': 'off',
        'react/jsx-filename-extension': 'off',
        'react/jsx-indent': ['error', 4],
        'react/jsx-indent-props': ['error', 4],
        'react/no-danger': 'off',
        'react/require-default-props': 'off',
        'react/jsx-boolean-value': 'off',
        'react/no-did-update-set-state': 'off', // recommended way since 16.3.
        'react/destructuring-assignment': 'off',
        'react/static-property-placement': 'off',
        'react-redux/prefer-separate-component-file': 'off',
        'react-redux/connect-prefer-named-arguments': 'off',
        'react/no-unused-prop-types': 'off', // superseded by react-redux/no-unused-prop-types
        'react/jsx-props-no-multi-spaces': 'off',
        // disallow certain syntax forms
        // http://eslint.org/docs/rules/no-restricted-syntax
        'no-restricted-syntax': [
            'error',
            {
                selector: 'ForInStatement',
                message: 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.',
            },
            {
                selector: 'LabeledStatement',
                message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.',
            },
            {
                selector: 'WithStatement',
                message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
            },
        ],
        camelcase: 'off'
    },
    globals: {
        '__BUILD_NR__': true,
        'requestIdleCallback': true,
        'any': true
    },
    settings: {
        'import/core-modules': [
            'native-api',
        ],
        'react': {
            'pragma': 'React',
            'version': '16.3'
        },
    }
};

```

</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: -->
```
function buildLibraryStructure(folders, entries) {
    const structureMap = {
        root: {
            id: 'root',
            title: 'Library',
            type: navItemTypes.folder,
            parent: null,
            children: [],
            weight: 1,
        },
    };

    // reused dummy variables used to spread data out of entries/folders
    /* eslint-disable no-unused-vars */
    let folder;
    let validFrom;
    let validUntil;
    let parent;
    /* eslint-enable no-unused-vars */
    let rest;

    // Add folders to map of items
    folders.forEach((folderItem) => {
        ({ folder, validFrom, validUntil, ...rest } = folderItem);

        structureMap[folderItem.id] = {
            ...rest,
            oid: folderItem.id,
            isFolder: true,
        };
    });
    return structureMap;
}
```

**What did you expect to happen?**
I believe the rule `no-unused-vars` should not be triggered.

The rule is configured as `'no-unused-vars': ['error', { 'ignoreRestSiblings': true }],`.
The above code splits definition and spreading of the two variables `folder` and `rest`. The definition of the variables are even enclosed in a `disable no-unused-vars`

**What actually happened? Please copy-paste the actual, raw output from ESLint.**

>   38:16  error  'folder' is assigned a value but never used      no-unused-vars
>   38:20  error  'validFrom' is assigned a value but never used   no-unused-vars
>   38:31  error  'validUntil' is assigned a value but never used  no-unused-vars

**Steps to reproduce this issue:**

<!-- Please tell us exactly how to see the issue you're describing -->

1. split definition and usage of variables
1. use the rest operator with a throw-away variables on an other line
1. no-unsed-vars is triggered

**Are you willing to submit a pull request to fix this bug?**
No, I don't think I could


Pull request:
Fix: no-unused-vars ignoreRestSiblings check assignments (fixes #14163)
<!--
    Thank you for contributing!

    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

#### Prerequisites checklist

- [ ] 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)

[ ] Documentation update
[x] 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))
[ ] 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)


Fixes #14163 by checking references also.

#### Is there anything you'd like reviewers to focus on?

no.

Patch:
diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md
index cbfa30ae2..9b46d2944 100644
--- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md
+++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md
@@ -2,7 +2,7 @@
 name: "\U0001F41E  Bug report"
 about: Report an issue with ESLint or rules bundled with ESLint
 title: ''
-labels: bug, triage
+labels: bug, repro:needed
 assignees: ''
 
 ---
@@ -35,6 +35,8 @@ assignees: ''
 
 **What parser (default, `@babel/eslint-parser`, `@typescript-eslint/parser`, etc.) are you using?**
 
+<!-- if you are using a parser other than ESLint's default, please try using the default parser to verify if the issue is parser-specific -->
+
 **Please show your full configuration:**
 
 <details>
@@ -74,4 +76,3 @@ assignees: ''
 1. 
 
 **Are you willing to submit a pull request to fix this bug?**
-
diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md
index 708d1fa82..ce7271357 100644
--- a/docs/developer-guide/nodejs-api.md
+++ b/docs/developer-guide/nodejs-api.md
@@ -353,6 +353,8 @@ The `LintMessage` value is the information of each linting error. The `messages`
   The rule name that generates this lint message. If this message is generated by the ESLint core rather than rules, this is `null`.
 * `severity` (`1 | 2`)<br>
   The severity of this message. `1` means warning and `2` means error.
+* `fatal` (`boolean | undefined`)<br>
+  `true` if this is a fatal error unrelated to a rule, like a parsing error.
 * `message` (`string`)<br>
   The error message.
 * `line` (`number`)<br>
diff --git a/docs/rules/no-multi-assign.md b/docs/rules/no-multi-assign.md
index 41bba9ad6..7ef705675 100644
--- a/docs/rules/no-multi-assign.md
+++ b/docs/rules/no-multi-assign.md
@@ -43,6 +43,38 @@ let a = c;
 let b = c;
 ```
 
+## Options
+
+This rule has an object option:
+
+* `"ignoreNonDeclaration"`: When set to `true`, the rule allows chains that don't include initializing a variable in a declaration. Default is `false`.
+
+### ignoreNonDeclaration
+
+Examples of **correct** code for the `{ "ignoreNonDeclaration": true }` option:
+
+```js
+/*eslint no-multi-assign: ["error", { "ignoreNonDeclaration": true }]*/
+
+let a;
+let b;
+a = b = "baz";
+
+const x = {};
+const y = {};
+x.one = y.one = 1;
+```
+
+Examples of **incorrect** code for the `{ "ignoreNonDeclaration": true }` option:
+
+```js
+/*eslint no-multi-assign: ["error", { "ignoreNonDeclaration": true }]*/
+
+let a = b = "baz";
+
+const foo = bar = 1;
+```
+
 ## Related Rules
 
 * [max-statements-per-line](max-statements-per-line.md)
diff --git a/docs/rules/no-unused-vars.md b/docs/rules/no-unused-vars.md
index f730e6661..30647031e 100644
--- a/docs/rules/no-unused-vars.md
+++ b/docs/rules/no-unused-vars.md
@@ -214,8 +214,11 @@ Examples of **correct** code for the `{ "ignoreRestSiblings": true }` option:
 
 ```js
 /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/
-// 'type' is ignored because it has a rest property sibling.
-var { type, ...coords } = data;
+// 'foo' and 'bar' were ignored because they have a rest property sibling.
+var { foo, ...coords } = data;
+
+var bar;
+({ bar, ...coords } = data);
 ```
 
 ### argsIgnorePattern
diff --git a/lib/rules/no-multi-assign.js b/lib/rules/no-multi-assign.js
index ab6430c19..d2606a150 100644
--- a/lib/rules/no-multi-assign.js
+++ b/lib/rules/no-multi-assign.js
@@ -21,7 +21,16 @@ module.exports = {
             url: "https://eslint.org/docs/rules/no-multi-assign"
         },
 
-        schema: [],
+        schema: [{
+            type: "object",
+            properties: {
+                ignoreNonDeclaration: {
+                    type: "boolean",
+                    default: false
+                }
+            },
+            additionalProperties: false
+        }],
 
         messages: {
             unexpectedChain: "Unexpected chained assignment."
@@ -33,10 +42,14 @@ module.exports = {
         //--------------------------------------------------------------------------
         // Public
         //--------------------------------------------------------------------------
+        const options = context.options[0] || {
+            ignoreNonDeclaration: false
+        };
+        const targetParent = options.ignoreNonDeclaration ? ["VariableDeclarator"] : ["AssignmentExpression", "VariableDeclarator"];
 
         return {
             AssignmentExpression(node) {
-                if (["AssignmentExpression", "VariableDeclarator"].indexOf(node.parent.type) !== -1) {
+                if (targetParent.indexOf(node.parent.type) !== -1) {
                     context.report({
                         node,
                         messageId: "unexpectedChain"
diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js
index 4dc6dc2ba..32589099c 100644
--- a/lib/rules/no-unused-vars.js
+++ b/lib/rules/no-unused-vars.js
@@ -196,6 +196,17 @@ module.exports = {
 
         }
 
+        /**
+         * Checks whether a node is a sibling of the rest property or not.
+         * @param {ASTNode} node a node to check
+         * @returns {boolean} True if the node is a sibling of the rest property, otherwise false.
+         */
+        function hasRestSibling(node) {
+            return node.type === "Property" &&
+                node.parent.type === "ObjectPattern" &&
+                REST_PROPERTY_TYPE.test(node.parent.properties[node.parent.properties.length - 1].type);
+        }
+
         /**
          * Determines if a variable has a sibling rest property
          * @param {Variable} variable eslint-scope variable object.
@@ -204,16 +215,10 @@ module.exports = {
          */
         function hasRestSpreadSibling(variable) {
             if (config.ignoreRestSiblings) {
-                return variable.defs.some(def => {
-                    const propertyNode = def.name.parent;
-                    const patternNode = propertyNode.parent;
-
-                    return (
-                        propertyNode.type === "Property" &&
-                        patternNode.type === "ObjectPattern" &&
-                        REST_PROPERTY_TYPE.test(patternNode.properties[patternNode.properties.length - 1].type)
-                    );
-                });
+                const hasRestSiblingDefinition = variable.defs.some(def => hasRestSibling(def.name.parent));
+                const hasRestSiblingReference = variable.references.some(ref => hasRestSibling(ref.identifier.parent));
+
+                return hasRestSiblingDefinition || hasRestSiblingReference;
             }
 
             return false;
diff --git a/package.json b/package.json
index ebbc13d59..8f1308965 100644
--- a/package.json
+++ b/package.json
@@ -118,8 +118,8 @@
     "markdownlint": "^0.19.0",
     "markdownlint-cli": "^0.22.0",
     "memfs": "^3.0.1",
-    "mocha": "^7.1.1",
-    "mocha-junit-reporter": "^1.23.0",
+    "mocha": "^8.3.2",
+    "mocha-junit-reporter": "^2.0.0",
     "node-polyfill-webpack-plugin": "^1.0.3",
     "npm-license": "^0.3.3",
     "nyc": "^15.0.1",


Test patch:
diff --git a/tests/lib/rules/no-multi-assign.js b/tests/lib/rules/no-multi-assign.js
index dc907465c..c534f55b3 100644
--- a/tests/lib/rules/no-multi-assign.js
+++ b/tests/lib/rules/no-multi-assign.js
@@ -51,7 +51,9 @@ ruleTester.run("no-mutli-assign", rule, {
         { code: "for(let a = 0, b = 0;;){}", parserOptions: { ecmaVersion: 6 } },
         { code: "for(const a = 0, b = 0;;){}", parserOptions: { ecmaVersion: 6 } },
         { code: "export let a, b;", parserOptions: { ecmaVersion: 6, sourceType: "module" } },
-        { code: "export let a,\n b = 0;", parserOptions: { ecmaVersion: 6, sourceType: "module" } }
+        { code: "export let a,\n b = 0;", parserOptions: { ecmaVersion: 6, sourceType: "module" } },
+        { code: "const x = {};const y = {};x.one = y.one = 1;", options: [{ ignoreNonDeclaration: true }], parserOptions: { ecmaVersion: 6 } },
+        { code: "let a, b;a = b = 1", options: [{ ignoreNonDeclaration: true }], parserOptions: { ecmaVersion: 6 } }
     ],
 
     invalid: [
@@ -137,6 +139,39 @@ ruleTester.run("no-mutli-assign", rule, {
             errors: [
                 errorAt(1, 5, "AssignmentExpression")
             ]
+        },
+        {
+            code: "const x = {};\nconst y = x.one = 1;",
+            options: [{ ignoreNonDeclaration: true }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                errorAt(2, 11, "AssignmentExpression")
+            ]
+
+        },
+        {
+            code: "let a, b;a = b = 1",
+            options: [{}],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                errorAt(1, 14, "AssignmentExpression")
+            ]
+        },
+        {
+            code: "let x, y;x = y = 'baz'",
+            options: [{ ignoreNonDeclaration: false }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                errorAt(1, 14, "AssignmentExpression")
+            ]
+        },
+        {
+            code: "const a = b = 1",
+            options: [{ ignoreNonDeclaration: true }],
+            parserOptions: { ecmaVersion: 6 },
+            errors: [
+                errorAt(1, 11, "AssignmentExpression")
+            ]
         }
     ]
 });
diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js
index 061ac08d5..db041d46b 100644
--- a/tests/lib/rules/no-unused-vars.js
+++ b/tests/lib/rules/no-unused-vars.js
@@ -291,6 +291,13 @@ ruleTester.run("no-unused-vars", rule, {
             parserOptions: { ecmaVersion: 2018 }
         },
 
+        // https://github.com/eslint/eslint/issues/14163
+        {
+            code: "let foo, rest;\n({ foo, ...rest } = something);\nconsole.log(rest);",
+            options: [{ ignoreRestSiblings: true }],
+            parserOptions: { ecmaVersion: 2020 }
+        },
+
         // https://github.com/eslint/eslint/issues/10952
         "/*eslint use-every-a:1*/ !function(b, a) { return 1 }",
 
@@ -588,6 +595,23 @@ ruleTester.run("no-unused-vars", rule, {
                 }
             ]
         },
+        {
+            code: "let type, coords;\n({ type, ...coords } = data);\n console.log(type)",
+            options: [{ ignoreRestSiblings: true }],
+            parserOptions: { ecmaVersion: 2018 },
+            errors: [
+                {
+                    line: 2,
+                    column: 13,
+                    messageId: "unusedVar",
+                    data: {
+                        varName: "coords",
+                        action: "assigned a value",
+                        additional: ""
+                    }
+                }
+            ]
+        },
 
         // Unused rest property without ignoreRestSiblings
         {