Repository: eslint/eslint (js)

Issue:
no-unused-vars highlights at wrong position
With `no-unused-vars` enabled eslint highlights the last use of the variable. If it is used left and right of an assignment it should mark the one on the left.

**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: -->
Instead of this:
```js
let x = [];
x = x.concat(x);
             ^
```

**What did you expect to happen?**
It should be this:
```js
let x = [];
x = x.concat(x);
^
```

**What actually happened? Please copy-paste the actual, raw output from ESLint.**


**Steps to reproduce this issue:**
1. [eslint demo](https://eslint.org/demo#eyJ0ZXh0IjoibGV0IHggPSBbXTtcbnggPSB4LmNvbmNhdCh4KTsiLCJvcHRpb25zIjp7InBhcnNlck9wdGlvbnMiOnsiZWNtYVZlcnNpb24iOjEyLCJzb3VyY2VUeXBlIjoic2NyaXB0IiwiZWNtYUZlYXR1cmVzIjp7fX0sInJ1bGVzIjp7ImNvbnN0cnVjdG9yLXN1cGVyIjoyLCJmb3ItZGlyZWN0aW9uIjoyLCJnZXR0ZXItcmV0dXJuIjoyLCJuby1hc3luYy1wcm9taXNlLWV4ZWN1dG9yIjoyLCJuby1jYXNlLWRlY2xhcmF0aW9ucyI6Miwibm8tY2xhc3MtYXNzaWduIjoyLCJuby1jb21wYXJlLW5lZy16ZXJvIjoyLCJuby1jb25kLWFzc2lnbiI6Miwibm8tY29uc3QtYXNzaWduIjoyLCJuby1jb25zdGFudC1jb25kaXRpb24iOjIsIm5vLWNvbnRyb2wtcmVnZXgiOjIsIm5vLWRlYnVnZ2VyIjoyLCJuby1kZWxldGUtdmFyIjoyLCJuby1kdXBlLWFyZ3MiOjIsIm5vLWR1cGUtY2xhc3MtbWVtYmVycyI6Miwibm8tZHVwZS1lbHNlLWlmIjoyLCJuby1kdXBlLWtleXMiOjIsIm5vLWR1cGxpY2F0ZS1jYXNlIjoyLCJuby1lbXB0eSI6Miwibm8tZW1wdHktY2hhcmFjdGVyLWNsYXNzIjoyLCJuby1lbXB0eS1wYXR0ZXJuIjoyLCJuby1leC1hc3NpZ24iOjIsIm5vLWV4dHJhLWJvb2xlYW4tY2FzdCI6Miwibm8tZXh0cmEtc2VtaSI6Miwibm8tZmFsbHRocm91Z2giOjIsIm5vLWZ1bmMtYXNzaWduIjoyLCJuby1nbG9iYWwtYXNzaWduIjoyLCJuby1pbXBvcnQtYXNzaWduIjoyLCJuby1pbm5lci1kZWNsYXJhdGlvbnMiOjIsIm5vLWludmFsaWQtcmVnZXhwIjoyLCJuby1pcnJlZ3VsYXItd2hpdGVzcGFjZSI6Miwibm8tbWlzbGVhZGluZy1jaGFyYWN0ZXItY2xhc3MiOjIsIm5vLW1peGVkLXNwYWNlcy1hbmQtdGFicyI6Miwibm8tbmV3LXN5bWJvbCI6Miwibm8tb2JqLWNhbGxzIjoyLCJuby1vY3RhbCI6Miwibm8tcHJvdG90eXBlLWJ1aWx0aW5zIjoyLCJuby1yZWRlY2xhcmUiOjIsIm5vLXJlZ2V4LXNwYWNlcyI6Miwibm8tc2VsZi1hc3NpZ24iOjIsIm5vLXNldHRlci1yZXR1cm4iOjIsIm5vLXNoYWRvdy1yZXN0cmljdGVkLW5hbWVzIjoyLCJuby1zcGFyc2UtYXJyYXlzIjoyLCJuby10aGlzLWJlZm9yZS1zdXBlciI6Miwibm8tdW5kZWYiOjIsIm5vLXVuZXhwZWN0ZWQtbXVsdGlsaW5lIjoyLCJuby11bnJlYWNoYWJsZSI6Miwibm8tdW5zYWZlLWZpbmFsbHkiOjIsIm5vLXVuc2FmZS1uZWdhdGlvbiI6Miwibm8tdW51c2VkLWxhYmVscyI6Miwibm8tdW51c2VkLXZhcnMiOjIsIm5vLXVzZWxlc3MtY2F0Y2giOjIsIm5vLXVzZWxlc3MtZXNjYXBlIjoyLCJuby13aXRoIjoyLCJyZXF1aXJlLXlpZWxkIjoyLCJ1c2UtaXNuYW4iOjIsInZhbGlkLXR5cGVvZiI6Mn0sImVudiI6e319fQ==)

**Are you willing to submit a pull request to fix this bug?**
No

Pull request description:
Update: highlight last write reference for no-unused-vars (fixes #14324)
<!--
    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)

[ ] 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 #14324 
Highlight the last write reference instead of just highlighting the last reference.

#### Is there anything you'd like reviewers to focus on?


Patch:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 6a57d0e6b..f0d6c3e2e 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -10,6 +10,11 @@
 
 #### 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))
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3de5fc7cd..6ba0bdc38 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,7 +26,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest]
-        node: [15.x, 14.x, 13.x, 12.x, 10.x, "10.12.0"]
+        node: [16.x, 15.x, 14.x, 13.x, 12.x, 10.x, "10.12.0"]
         include:
         - os: windows-latest
           node: "12.x"
diff --git a/README.md b/README.md
index 14ba7e2cb..1aa25ee3d 100644
--- a/README.md
+++ b/README.md
@@ -283,7 +283,7 @@ The following companies, organizations, and individuals support ESLint's ongoing
 <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.shopify.com"><img src="https://images.opencollective.com/shopify/e780cd4/logo.png" alt="Shopify" 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://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://discordapp.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://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>
 <!--sponsorsend-->
 
 ## <a name="technology-sponsors"></a>Technology Sponsors
diff --git a/docs/developer-guide/working-with-rules-deprecated.md b/docs/developer-guide/working-with-rules-deprecated.md
index fdd4f9d3e..5265cb50e 100644
--- a/docs/developer-guide/working-with-rules-deprecated.md
+++ b/docs/developer-guide/working-with-rules-deprecated.md
@@ -1,4 +1,6 @@
-# Working with Rules
+# Working with Rules (Deprecated)
+
+**Note:** This page covers the deprecated rule format for ESLint <= 2.13.1. [This is the most recent rule format](./working-with-rules.md).
 
 Each rule in ESLint has two files named with its identifier (for example, `no-extra-semi`).
 
diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md
index 2e699c088..59e275de5 100644
--- a/docs/developer-guide/working-with-rules.md
+++ b/docs/developer-guide/working-with-rules.md
@@ -140,7 +140,7 @@ Additionally, the `context` object has the following methods:
     * Otherwise, if the node does not declare any variables, an empty array is returned.
 * `getFilename()` - returns the filename associated with the source.
 * `getScope()` - returns the [scope](./scope-manager-interface.md#scope-interface) of the currently-traversed node. This information can be used to track references to variables.
-* `getSourceCode()` - returns a [`SourceCode`](#context-getsourcecode) object that you can use to work with the source that was passed to ESLint.
+* `getSourceCode()` - returns a [`SourceCode`](#contextgetsourcecode) object that you can use to work with the source that was passed to ESLint.
 * `markVariableAsUsed(name)` - marks a variable with the given name in the current scope as used. This affects the [no-unused-vars](../rules/no-unused-vars.md) rule. Returns `true` if a variable with the given name was found and marked as used, otherwise `false`.
 * `report(descriptor)` - reports a problem in the code (see the [dedicated section](#contextreport)).
 
diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js
index 32589099c..7619be331 100644
--- a/lib/rules/no-unused-vars.js
+++ b/lib/rules/no-unused-vars.js
@@ -624,10 +624,18 @@ module.exports = {
 
                     // Report the first declaration.
                     if (unusedVar.defs.length > 0) {
+
+                        // report last write reference, https://github.com/eslint/eslint/issues/14324
+                        const writeReferences = unusedVar.references.filter(ref => ref.isWrite() && ref.from.variableScope === unusedVar.scope.variableScope);
+
+                        let referenceToReport;
+
+                        if (writeReferences.length > 0) {
+                            referenceToReport = writeReferences[writeReferences.length - 1];
+                        }
+
                         context.report({
-                            node: unusedVar.references.length ? unusedVar.references[
-                                unusedVar.references.length - 1
-                            ].identifier : unusedVar.identifiers[0],
+                            node: referenceToReport ? referenceToReport.identifier : unusedVar.identifiers[0],
                             messageId: "unusedVar",
                             data: unusedVar.references.some(ref => ref.isWrite())
                                 ? getAssignedMessageData(unusedVar)
diff --git a/package.json b/package.json
index 5c0229174..012a8f4fd 100644
--- a/package.json
+++ b/package.json
@@ -100,7 +100,7 @@
     "eslint-plugin-jsdoc": "^25.4.3",
     "eslint-plugin-node": "^11.1.0",
     "eslint-release": "^2.0.0",
-    "eslump": "^2.0.0",
+    "eslump": "^3.0.0",
     "esprima": "^4.0.1",
     "fs-teardown": "^0.1.0",
     "glob": "^7.1.6",


Test patch:
diff --git a/tests/fixtures/formatters/broken.js b/tests/fixtures/formatters/broken.js
index c467ac736..263143645 100644
--- a/tests/fixtures/formatters/broken.js
+++ b/tests/fixtures/formatters/broken.js
@@ -1,5 +1,5 @@
 /*global module*/
-var nonExistantFormatter = require('this-module-does-not-exist');
+var nonExistentFormatter = require('this-module-does-not-exist');
 module.exports = function(results) {
-    return nonExistantFormatter(results);
+    return nonExistentFormatter(results);
 };
diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js
index db041d46b..423afa167 100644
--- a/tests/lib/rules/no-unused-vars.js
+++ b/tests/lib/rules/no-unused-vars.js
@@ -1062,7 +1062,7 @@ ruleTester.run("no-unused-vars", rule, {
             code: `let myArray = [1,2,3,4].filter((x) => x == 0);
     myArray = myArray.filter((x) => x == 1);`,
             parserOptions: { ecmaVersion: 2015 },
-            errors: [{ ...assignedError("myArray"), line: 2, column: 15 }]
+            errors: [{ ...assignedError("myArray"), line: 2, column: 5 }]
         },
         {
             code: "const a = 1; a += 1;",
@@ -1071,21 +1071,28 @@ ruleTester.run("no-unused-vars", rule, {
         },
         {
             code: "var a = function() { a(); };",
-            errors: [{ ...assignedError("a"), line: 1, column: 22 }]
+            errors: [{ ...assignedError("a"), line: 1, column: 5 }]
         },
         {
             code: "var a = function(){ return function() { a(); } };",
-            errors: [{ ...assignedError("a"), line: 1, column: 41 }]
+            errors: [{ ...assignedError("a"), line: 1, column: 5 }]
         },
         {
             code: "const a = () => { a(); };",
             parserOptions: { ecmaVersion: 2015 },
-            errors: [{ ...assignedError("a"), line: 1, column: 19 }]
+            errors: [{ ...assignedError("a"), line: 1, column: 7 }]
         },
         {
             code: "const a = () => () => { a(); };",
             parserOptions: { ecmaVersion: 2015 },
-            errors: [{ ...assignedError("a"), line: 1, column: 25 }]
+            errors: [{ ...assignedError("a"), line: 1, column: 7 }]
+        },
+
+        // https://github.com/eslint/eslint/issues/14324
+        {
+            code: "let x = [];\nx = x.concat(x);",
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 2, column: 1 }]
         },
         {
 
@@ -1098,7 +1105,25 @@ ruleTester.run("no-unused-vars", rule, {
                 }
             }`,
             parserOptions: { ecmaVersion: 2020 },
-            errors: [{ ...definedError("foo"), line: 3, column: 22 }, { ...assignedError("a"), line: 6, column: 21 }]
+            errors: [{ ...assignedError("a"), line: 2, column: 13 }, { ...definedError("foo"), line: 3, column: 22 }]
+        },
+        {
+            code: `let foo;
+            init();
+            foo = foo + 2;
+            function init() {
+                foo = 1;
+            }`,
+            parserOptions: { ecmaVersion: 2020 },
+            errors: [{ ...assignedError("foo"), line: 3, column: 13 }]
+        },
+        {
+            code: `function foo(n) {
+                if (n < 2) return 1;
+                return n * foo(n - 1);
+            }`,
+            parserOptions: { ecmaVersion: 2020 },
+            errors: [{ ...definedError("foo"), line: 1, column: 10 }]
         },
         {
             code: `let c = 'c'