Repository: eslint/eslint
Pull request #14590 (2021-05-16 05:47:19)

Chore: upgrade eslint-plugin-eslint-plugin
<!--
    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))
[ ] 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
[ x] 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)
upgrade eslint-plugin-eslint-plugin to latest.

#### Is there anything you'd like reviewers to focus on?
it also rewrite some generators to remove disable directives.


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 1457d9b55..668bc2747 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -71,22 +71,7 @@ module.exports = {
         "eslint-plugin/report-message-format": ["error", "[^a-z].*\\.$"],
         "eslint-plugin/require-meta-docs-description": "error",
         "eslint-plugin/require-meta-type": "error",
-        "eslint-plugin/test-case-property-ordering": [
-            "error",
-
-            // https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/79
-            [
-                "filename",
-                "code",
-                "output",
-                "options",
-                "parser",
-                "parserOptions",
-                "globals",
-                "env",
-                "errors"
-            ]
-        ],
+        "eslint-plugin/test-case-property-ordering": "error",
         "eslint-plugin/test-case-shorthand-strings": "error",
         "internal-rules/multiline-comment-style": "error"
     },
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b976ab2ea..54f33b6d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+v7.27.0 - May 21, 2021
+
+* [`2c0868c`](https://github.com/eslint/eslint/commit/2c0868cbeadc9f42716fa1178ebdc6b4cee6d31e) Chore: merge all html formatter files into `html.js` (#14612) (Milos Djermanovic)
+* [`9e9b5e0`](https://github.com/eslint/eslint/commit/9e9b5e07475564813b62cd1d7562a93c5fb4bc74) Update: no-unused-vars false negative with comma operator (fixes #14325) (#14354) (Nitin Kumar)
+* [`afe9569`](https://github.com/eslint/eslint/commit/afe95693e1e4316a1c6f01d39345061d4c5921c7) Chore: use includes instead of indexOf (#14607) (Mikhail Bodrov)
+* [`c0f418e`](https://github.com/eslint/eslint/commit/c0f418e2476df98519bc156b81d20431984e8704) Chore: Remove lodash (#14287) (Stephen Wade)
+* [`52655dd`](https://github.com/eslint/eslint/commit/52655dd54925ee02af2ba3a0ebc09de959ae3101) Update: no-restricted-imports custom message for patterns (fixes #11843) (#14580) (Alex Holden)
+* [`967b1c4`](https://github.com/eslint/eslint/commit/967b1c4ceca8f5248378477da94ff118dafaa647) Chore: Fix typo in large.js (#14589) (Ikko Ashimine)
+* [`2466a05`](https://github.com/eslint/eslint/commit/2466a05160de60958457d984b79fd445c12ebc98) Sponsors: Sync README with website (ESLint Jenkins)
+* [`fe29f18`](https://github.com/eslint/eslint/commit/fe29f18227fd02fd7c3da033417d621275b00d0a) Sponsors: Sync README with website (ESLint Jenkins)
+* [`086c1d6`](https://github.com/eslint/eslint/commit/086c1d6e8593cf8e7851daa8f2a890c213cf6999) Chore: add more test cases for `no-sequences` (#14579) (Nitin Kumar)
+* [`6a2ced8`](https://github.com/eslint/eslint/commit/6a2ced892c0dc43fa4942293b9f1c4b9151c3741) Docs: Update README team and sponsors (ESLint Jenkins)
+
 v7.26.0 - May 7, 2021
 
 * [`aaf65e6`](https://github.com/eslint/eslint/commit/aaf65e629adb74401092c3ccc9cb4e4bd1c8609b) Upgrade: eslintrc for ModuleResolver fix (#14577) (Brandon Mills)
diff --git a/README.md b/README.md
index 1cb56a9bc..438a087ff 100644
--- a/README.md
+++ b/README.md
@@ -223,11 +223,6 @@ Nicholas C. Zakas
 Brandon Mills
 </a>
 </td><td align="center" valign="top" width="11%">
-<a href="https://github.com/mysticatea">
-<img src="https://github.com/mysticatea.png?s=75" width="75" height="75"><br />
-Toru Nagashima
-</a>
-</td><td align="center" valign="top" width="11%">
 <a href="https://github.com/mdjermanovic">
 <img src="https://github.com/mdjermanovic.png?s=75" width="75" height="75"><br />
 Milos Djermanovic
@@ -288,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.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/bin/eslint.js b/bin/eslint.js
index 86291b0f5..5fa576682 100755
--- a/bin/eslint.js
+++ b/bin/eslint.js
@@ -66,11 +66,8 @@ function readStdin() {
  */
 function getErrorMessage(error) {
 
-    // Lazy loading because those are used only if error happened.
-    const fs = require("fs");
-    const path = require("path");
+    // Lazy loading because this is used only if an error happened.
     const util = require("util");
-    const lodash = require("lodash");
 
     // Foolproof -- thirdparty module might throw non-object.
     if (typeof error !== "object" || error === null) {
@@ -80,14 +77,7 @@ function getErrorMessage(error) {
     // Use templates if `error.messageTemplate` is present.
     if (typeof error.messageTemplate === "string") {
         try {
-            const templateFilePath = path.resolve(
-                __dirname,
-                `../messages/${error.messageTemplate}.txt`
-            );
-
-            // Use sync API because Node.js should exit at this tick.
-            const templateText = fs.readFileSync(templateFilePath, "utf-8");
-            const template = lodash.template(templateText);
+            const template = require(`../messages/${error.messageTemplate}.js`);
 
             return template(error.messageData || {});
         } catch {
diff --git a/docs/developer-guide/code-path-analysis.md b/docs/developer-guide/code-path-analysis.md
index 7762bc5d6..cd3fc2936 100644
--- a/docs/developer-guide/code-path-analysis.md
+++ b/docs/developer-guide/code-path-analysis.md
@@ -195,8 +195,6 @@ bar();
 ### To check whether or not this is reachable
 
 ```js
-var last = require("lodash").last;
-
 function isReachable(segment) {
     return segment.reachable;
 }
@@ -215,7 +213,7 @@ module.exports = function(context) {
 
         // Checks reachable or not.
         "ExpressionStatement": function(node) {
-            var codePath = last(codePathStack);
+            var codePath = codePathStack[codePathStack.length - 1];
 
             // Checks the current code path segments.
             if (!codePath.currentSegments.some(isReachable)) {
@@ -239,8 +237,6 @@ So a rule must not modify those instances.
 Please use a map of information instead.
 
 ```js
-var last = require("lodash").last;
-
 function hasCb(node, context) {
     if (node.type.indexOf("Function") !== -1) {
         return context.getDeclaredVariables(node).some(function(v) {
@@ -285,8 +281,10 @@ module.exports = function(context) {
 
         // Manages state of code paths.
         "onCodePathSegmentStart": function(segment) {
+            var funcInfo = funcInfoStack[funcInfoStack.length - 1];
+
             // Ignores if `cb` doesn't exist.
-            if (!last(funcInfoStack).hasCb) {
+            if (!funcInfo.hasCb) {
                 return;
             }
 
@@ -304,7 +302,7 @@ module.exports = function(context) {
 
         // Checks reachable or not.
         "CallExpression": function(node) {
-            var funcInfo = last(funcInfoStack);
+            var funcInfo = funcInfoStack[funcInfoStack.length - 1];
 
             // Ignores if `cb` doesn't exist.
             if (!funcInfo.hasCb) {
diff --git a/docs/developer-guide/code-path-analysis/README.md b/docs/developer-guide/code-path-analysis/README.md
index c283d51bf..1c84b2e1f 100644
--- a/docs/developer-guide/code-path-analysis/README.md
+++ b/docs/developer-guide/code-path-analysis/README.md
@@ -195,8 +195,6 @@ bar();
 ### To check whether or not this is reachable
 
 ```js
-var last = require("lodash").last;
-
 function isReachable(segment) {
     return segment.reachable;
 }
@@ -215,7 +213,7 @@ module.exports = function(context) {
 
         // Checks reachable or not.
         "ExpressionStatement": function(node) {
-            var codePath = last(codePathStack);
+            var codePath = codePathStack[codePathStack.length - 1];
 
             // Checks the current code path segments.
             if (!codePath.currentSegments.some(isReachable)) {
@@ -239,8 +237,6 @@ So a rule must not modify those instances.
 Please use a map of information instead.
 
 ```js
-var last = require("lodash").last;
-
 function hasCb(node, context) {
     if (node.type.indexOf("Function") !== -1) {
         return context.getDeclaredVariables(node).some(function(v) {
@@ -285,8 +281,10 @@ module.exports = function(context) {
 
         // Manages state of code paths.
         "onCodePathSegmentStart": function(segment) {
+            var funcInfo = funcInfoStack[funcInfoStack - 1];
+
             // Ignores if `cb` doesn't exist.
-            if (!last(funcInfoStack).hasCb) {
+            if (!funcInfo.hasCb) {
                 return;
             }
 
@@ -304,7 +302,7 @@ module.exports = function(context) {
 
         // Checks reachable or not.
         "CallExpression": function(node) {
-            var funcInfo = last(funcInfoStack);
+            var funcInfo = funcInfoStack[funcInfoStack - 1];
 
             // Ignores if `cb` doesn't exist.
             if (!funcInfo.hasCb) {
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/cli-engine/file-enumerator.js b/lib/cli-engine/file-enumerator.js
index bd89ec733..ade28517b 100644
--- a/lib/cli-engine/file-enumerator.js
+++ b/lib/cli-engine/file-enumerator.js
@@ -38,7 +38,7 @@ const fs = require("fs");
 const path = require("path");
 const getGlobParent = require("glob-parent");
 const isGlob = require("is-glob");
-const { escapeRegExp } = require("lodash");
+const escapeRegExp = require("escape-string-regexp");
 const { Minimatch } = require("minimatch");
 
 const {
diff --git a/lib/cli-engine/formatters/html-template-message.html b/lib/cli-engine/formatters/html-template-message.html
deleted file mode 100644
index 93795a1bd..000000000
--- a/lib/cli-engine/formatters/html-template-message.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<tr style="display:none" class="f-<%= parentIndex %>">
-    <td><%= lineNumber %>:<%= columnNumber %></td>
-    <td class="clr-<%= severityNumber %>"><%= severityName %></td>
-    <td><%- message %></td>
-    <td>
-        <a href="<%= ruleUrl %>" target="_blank" rel="noopener noreferrer"><%= ruleId %></a>
-    </td>
-</tr>
diff --git a/lib/cli-engine/formatters/html-template-page.html b/lib/cli-engine/formatters/html-template-page.html
deleted file mode 100644
index 4016576fa..000000000
--- a/lib/cli-engine/formatters/html-template-page.html
+++ /dev/null
@@ -1,115 +0,0 @@
-<!DOCTYPE html>
-<html>
-    <head>
-        <meta charset="UTF-8">
-        <title>ESLint Report</title>
-        <style>
-            body {
-                font-family:Arial, "Helvetica Neue", Helvetica, sans-serif;
-                font-size:16px;
-                font-weight:normal;
-                margin:0;
-                padding:0;
-                color:#333
-            }
-            #overview {
-                padding:20px 30px
-            }
-            td, th {
-                padding:5px 10px
-            }
-            h1 {
-                margin:0
-            }
-            table {
-                margin:30px;
-                width:calc(100% - 60px);
-                max-width:1000px;
-                border-radius:5px;
-                border:1px solid #ddd;
-                border-spacing:0px;
-            }
-            th {
-                font-weight:400;
-                font-size:medium;
-                text-align:left;
-                cursor:pointer
-            }
-            td.clr-1, td.clr-2, th span {
-                font-weight:700
-            }
-            th span {
-                float:right;
-                margin-left:20px
-            }
-            th span:after {
-                content:"";
-                clear:both;
-                display:block
-            }
-            tr:last-child td {
-                border-bottom:none
-            }
-            tr td:first-child, tr td:last-child {
-                color:#9da0a4
-            }
-            #overview.bg-0, tr.bg-0 th {
-                color:#468847;
-                background:#dff0d8;
-                border-bottom:1px solid #d6e9c6
-            }
-            #overview.bg-1, tr.bg-1 th {
-                color:#f0ad4e;
-                background:#fcf8e3;
-                border-bottom:1px solid #fbeed5
-            }
-            #overview.bg-2, tr.bg-2 th {
-                color:#b94a48;
-                background:#f2dede;
-                border-bottom:1px solid #eed3d7
-            }
-            td {
-                border-bottom:1px solid #ddd
-            }
-            td.clr-1 {
-                color:#f0ad4e
-            }
-            td.clr-2 {
-                color:#b94a48
-            }
-            td a {
-                color:#3a33d1;
-                text-decoration:none
-            }
-            td a:hover {
-                color:#272296;
-                text-decoration:underline
-            }
-        </style>
-    </head>
-    <body>
-        <div id="overview" class="bg-<%= reportColor %>">
-            <h1>ESLint Report</h1>
-            <div>
-                <span><%= reportSummary %></span> - Generated on <%= date %>
-            </div>
-        </div>
-        <table>
-            <tbody>
-                <%= results %>
-            </tbody>
-        </table>
-        <script type="text/javascript">
-            var groups = document.querySelectorAll("tr[data-group]");
-            for (i = 0; i < groups.length; i++) {
-                groups[i].addEventListener("click", function() {
-                    var inGroup = document.getElementsByClassName(this.getAttribute("data-group"));
-                    this.innerHTML = (this.innerHTML.indexOf("+") > -1) ? this.innerHTML.replace("+", "-") : this.innerHTML.replace("-", "+");
-                    for (var j = 0; j < inGroup.length; j++) {
-                        inGroup[j].style.display = (inGroup[j].style.display !== "none") ? "none" : "table-row";
-                    }
-                });
-            }
-        </script>
-    </body>
-</html>
diff --git a/lib/cli-engine/formatters/html-template-result.html b/lib/cli-engine/formatters/html-template-result.html
deleted file mode 100644
index f4a55933c..000000000
--- a/lib/cli-engine/formatters/html-template-result.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<tr class="bg-<%- color %>" data-group="f-<%- index %>">
-    <th colspan="4">
-        [+] <%- filePath %>
-        <span><%- summary %></span>
-    </th>
-</tr>
diff --git a/lib/cli-engine/formatters/html.js b/lib/cli-engine/formatters/html.js
index 5d4b7e560..baddb6307 100644
--- a/lib/cli-engine/formatters/html.js
+++ b/lib/cli-engine/formatters/html.js
@@ -4,17 +4,153 @@
  */
 "use strict";
 
-const lodash = require("lodash");
-const fs = require("fs");
-const path = require("path");
-
 //------------------------------------------------------------------------------
 // Helpers
 //------------------------------------------------------------------------------
 
-const pageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-page.html"), "utf-8"));
-const messageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-message.html"), "utf-8"));
-const resultTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-result.html"), "utf-8"));
+const encodeHTML = (function() {
+    const encodeHTMLRules = {
+        "&": "&#38;",
+        "<": "&#60;",
+        ">": "&#62;",
+        '"': "&#34;",
+        "'": "&#39;"
+    };
+    const matchHTML = /[&<>"']/ug;
+
+    return function(code) {
+        return code
+            ? code.toString().replace(matchHTML, m => encodeHTMLRules[m] || m)
+            : "";
+    };
+}());
+
+/**
+ * Get the final HTML document.
+ * @param {Object} it data for the document.
+ * @returns {string} HTML document.
+ */
+function pageTemplate(it) {
+    const { reportColor, reportSummary, date, results } = it;
+
+    return `
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="UTF-8">
+        <title>ESLint Report</title>
+        <style>
+            body {
+                font-family:Arial, "Helvetica Neue", Helvetica, sans-serif;
+                font-size:16px;
+                font-weight:normal;
+                margin:0;
+                padding:0;
+                color:#333
+            }
+            #overview {
+                padding:20px 30px
+            }
+            td, th {
+                padding:5px 10px
+            }
+            h1 {
+                margin:0
+            }
+            table {
+                margin:30px;
+                width:calc(100% - 60px);
+                max-width:1000px;
+                border-radius:5px;
+                border:1px solid #ddd;
+                border-spacing:0px;
+            }
+            th {
+                font-weight:400;
+                font-size:medium;
+                text-align:left;
+                cursor:pointer
+            }
+            td.clr-1, td.clr-2, th span {
+                font-weight:700
+            }
+            th span {
+                float:right;
+                margin-left:20px
+            }
+            th span:after {
+                content:"";
+                clear:both;
+                display:block
+            }
+            tr:last-child td {
+                border-bottom:none
+            }
+            tr td:first-child, tr td:last-child {
+                color:#9da0a4
+            }
+            #overview.bg-0, tr.bg-0 th {
+                color:#468847;
+                background:#dff0d8;
+                border-bottom:1px solid #d6e9c6
+            }
+            #overview.bg-1, tr.bg-1 th {
+                color:#f0ad4e;
+                background:#fcf8e3;
+                border-bottom:1px solid #fbeed5
+            }
+            #overview.bg-2, tr.bg-2 th {
+                color:#b94a48;
+                background:#f2dede;
+                border-bottom:1px solid #eed3d7
+            }
+            td {
+                border-bottom:1px solid #ddd
+            }
+            td.clr-1 {
+                color:#f0ad4e
+            }
+            td.clr-2 {
+                color:#b94a48
+            }
+            td a {
+                color:#3a33d1;
+                text-decoration:none
+            }
+            td a:hover {
+                color:#272296;
+                text-decoration:underline
+            }
+        </style>
+    </head>
+    <body>
+        <div id="overview" class="bg-${reportColor}">
+            <h1>ESLint Report</h1>
+            <div>
+                <span>${reportSummary}</span> - Generated on ${date}
+            </div>
+        </div>
+        <table>
+            <tbody>
+                ${results}
+            </tbody>
+        </table>
+        <script type="text/javascript">
+            var groups = document.querySelectorAll("tr[data-group]");
+            for (i = 0; i < groups.length; i++) {
+                groups[i].addEventListener("click", function() {
+                    var inGroup = document.getElementsByClassName(this.getAttribute("data-group"));
+                    this.innerHTML = (this.innerHTML.indexOf("+") > -1) ? this.innerHTML.replace("+", "-") : this.innerHTML.replace("-", "+");
+                    for (var j = 0; j < inGroup.length; j++) {
+                        inGroup[j].style.display = (inGroup[j].style.display !== "none") ? "none" : "table-row";
+                    }
+                });
+            }
+        </script>
+    </body>
+</html>
+`.trimLeft();
+}
 
 /**
  * Given a word and a count, append an s if count is not one.
@@ -58,6 +194,35 @@ function renderColor(totalErrors, totalWarnings) {
     return 0;
 }
 
+/**
+ * Get HTML (table row) describing a single message.
+ * @param {Object} it data for the message.
+ * @returns {string} HTML (table row) describing the message.
+ */
+function messageTemplate(it) {
+    const {
+        parentIndex,
+        lineNumber,
+        columnNumber,
+        severityNumber,
+        severityName,
+        message,
+        ruleUrl,
+        ruleId
+    } = it;
+
+    return `
+<tr style="display:none" class="f-${parentIndex}">
+    <td>${lineNumber}:${columnNumber}</td>
+    <td class="clr-${severityNumber}">${severityName}</td>
+    <td>${encodeHTML(message)}</td>
+    <td>
+        <a href="${ruleUrl ? ruleUrl : ""}" target="_blank" rel="noopener noreferrer">${ruleId ? ruleId : ""}</a>
+    </td>
+</tr>
+`.trimLeft();
+}
+
 /**
  * Get HTML (table rows) describing the messages.
  * @param {Array} messages Messages.
@@ -80,7 +245,9 @@ function renderMessages(messages, parentIndex, rulesMeta) {
         if (rulesMeta) {
             const meta = rulesMeta[message.ruleId];
 
-            ruleUrl = lodash.get(meta, "docs.url", null);
+            if (meta && meta.docs && meta.docs.url) {
+                ruleUrl = meta.docs.url;
+            }
         }
 
         return messageTemplate({
@@ -96,6 +263,24 @@ function renderMessages(messages, parentIndex, rulesMeta) {
     }).join("\n");
 }
 
+/**
+ * Get HTML (table row) describing the result for a single file.
+ * @param {Object} it data for the file.
+ * @returns {string} HTML (table row) describing the result for the file.
+ */
+function resultTemplate(it) {
+    const { color, index, filePath, summary } = it;
+
+    return `
+<tr class="bg-${color}" data-group="f-${index}">
+    <th colspan="4">
+        [+] ${encodeHTML(filePath)}
+        <span>${encodeHTML(summary)}</span>
+    </th>
+</tr>
+`.trimLeft();
+}
+
 // eslint-disable-next-line jsdoc/require-description
 /**
  * @param {Array} results Test results.
@@ -108,7 +293,6 @@ function renderResults(results, rulesMeta) {
         color: renderColor(result.errorCount, result.warningCount),
         filePath: result.filePath,
         summary: renderSummary(result.errorCount, result.warningCount)
-
     }) + renderMessages(result.messages, index, rulesMeta)).join("\n");
 }
 
diff --git a/lib/init/autoconfig.js b/lib/init/autoconfig.js
index 19d016a54..054c53849 100644
--- a/lib/init/autoconfig.js
+++ b/lib/init/autoconfig.js
@@ -9,7 +9,7 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const lodash = require("lodash"),
+const equal = require("fast-deep-equal"),
     recConfig = require("../../conf/eslint-recommended"),
     ConfigOps = require("@eslint/eslintrc/lib/shared/config-ops"),
     { Linter } = require("../linter"),
@@ -329,7 +329,7 @@ function extendFromRecommended(config) {
     const recRules = Object.keys(recConfig.rules).filter(ruleId => ConfigOps.isErrorSeverity(recConfig.rules[ruleId]));
 
     recRules.forEach(ruleId => {
-        if (lodash.isEqual(recConfig.rules[ruleId], newConfig.rules[ruleId])) {
+        if (equal(recConfig.rules[ruleId], newConfig.rules[ruleId])) {
             delete newConfig.rules[ruleId];
         }
     });
diff --git a/lib/linter/apply-disable-directives.js b/lib/linter/apply-disable-directives.js
index 41d6934ab..0ba69ca9c 100644
--- a/lib/linter/apply-disable-directives.js
+++ b/lib/linter/apply-disable-directives.js
@@ -5,8 +5,6 @@
 
 "use strict";
 
-const lodash = require("lodash");
-
 /**
  * Compares the locations of two objects in a source file
  * @param {{line: number, column: number}} itemA The first object
@@ -124,7 +122,21 @@ module.exports = ({ directives, problems, reportUnusedDisableDirectives = "off"
         .map(directive => Object.assign({}, directive, { unprocessedDirective: directive }))
         .sort(compareLocations);
 
-    const lineDirectives = lodash.flatMap(directives, directive => {
+    /**
+     * Returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level.
+     * TODO(stephenwade): Replace this with array.flatMap when we drop support for Node v10
+     * @param {any[]} array The array to process
+     * @param {Function} fn The function to use
+     * @returns {any[]} The result array
+     */
+    function flatMap(array, fn) {
+        const mapped = array.map(fn);
+        const flattened = [].concat(...mapped);
+
+        return flattened;
+    }
+
+    const lineDirectives = flatMap(directives, directive => {
         switch (directive.type) {
             case "disable":
             case "enable":
diff --git a/lib/linter/linter.js b/lib/linter/linter.js
index 576816b5b..bdc6c1b1d 100644
--- a/lib/linter/linter.js
+++ b/lib/linter/linter.js
@@ -15,7 +15,7 @@ const
     eslintScope = require("eslint-scope"),
     evk = require("eslint-visitor-keys"),
     espree = require("espree"),
-    lodash = require("lodash"),
+    merge = require("lodash.merge"),
     BuiltInEnvironments = require("@eslint/eslintrc/conf/environments"),
     pkg = require("../../package.json"),
     astUtils = require("../shared/ast-utils"),
@@ -529,8 +529,8 @@ function normalizeVerifyOptions(providedOptions, config) {
 function resolveParserOptions(parserName, providedOptions, enabledEnvironments) {
     const parserOptionsFromEnv = enabledEnvironments
         .filter(env => env.parserOptions)
-        .reduce((parserOptions, env) => lodash.merge(parserOptions, env.parserOptions), {});
-    const mergedParserOptions = lodash.merge(parserOptionsFromEnv, providedOptions || {});
+        .reduce((parserOptions, env) => merge(parserOptions, env.parserOptions), {});
+    const mergedParserOptions = merge(parserOptionsFromEnv, providedOptions || {});
     const isModule = mergedParserOptions.sourceType === "module";
 
     if (isModule) {
@@ -1286,7 +1286,9 @@ class Linter {
         const filenameToExpose = normalizeFilename(filename);
         const text = ensureText(textOrSourceCode);
         const preprocess = options.preprocess || (rawText => [rawText]);
-        const postprocess = options.postprocess || lodash.flatten;
+
+        // TODO(stephenwade): Replace this with array.flat() when we drop support for Node v10
+        const postprocess = options.postprocess || (array => [].concat(...array));
         const filterCodeBlock =
             options.filterCodeBlock ||
             (blockFilename => blockFilename.endsWith(".js"));
diff --git a/lib/linter/node-event-generator.js b/lib/linter/node-event-generator.js
index 0b4e50fc4..8b619fdff 100644
--- a/lib/linter/node-event-generator.js
+++ b/lib/linter/node-event-generator.js
@@ -10,7 +10,6 @@
 //------------------------------------------------------------------------------
 
 const esquery = require("esquery");
-const lodash = require("lodash");
 
 //------------------------------------------------------------------------------
 // Typedefs
@@ -32,6 +31,35 @@ const lodash = require("lodash");
 // Helpers
 //------------------------------------------------------------------------------
 
+/**
+ * Computes the union of one or more arrays
+ * @param {...any[]} arrays One or more arrays to union
+ * @returns {any[]} The union of the input arrays
+ */
+function union(...arrays) {
+
+    // TODO(stephenwade): Replace this with arrays.flat() when we drop support for Node v10
+    return [...new Set([].concat(...arrays))];
+}
+
+/**
+ * Computes the intersection of one or more arrays
+ * @param {...any[]} arrays One or more arrays to intersect
+ * @returns {any[]} The intersection of the input arrays
+ */
+function intersection(...arrays) {
+    if (arrays.length === 0) {
+        return [];
+    }
+
+    let result = [...new Set(arrays[0])];
+
+    for (const array of arrays.slice(1)) {
+        result = result.filter(x => array.includes(x));
+    }
+    return result;
+}
+
 /**
  * Gets the possible types of a selector
  * @param {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
@@ -46,7 +74,7 @@ function getPossibleTypes(parsedSelector) {
             const typesForComponents = parsedSelector.selectors.map(getPossibleTypes);
 
             if (typesForComponents.every(Boolean)) {
-                return lodash.union(...typesForComponents);
+                return union(...typesForComponents);
             }
             return null;
         }
@@ -63,7 +91,7 @@ function getPossibleTypes(parsedSelector) {
              * If at least one of the components could only match a particular type, the compound could only match
              * the intersection of those types.
              */
-            return lodash.intersection(...typesForComponents);
+            return intersection(...typesForComponents);
         }
 
         case "child":
@@ -166,15 +194,21 @@ function tryParseSelector(rawSelector) {
     }
 }
 
+const selectorCache = new Map();
+
 /**
  * Parses a raw selector string, and returns the parsed selector along with specificity and type information.
  * @param {string} rawSelector A raw AST selector
  * @returns {ASTSelector} A selector descriptor
  */
-const parseSelector = lodash.memoize(rawSelector => {
+function parseSelector(rawSelector) {
+    if (selectorCache.has(rawSelector)) {
+        return selectorCache.get(rawSelector);
+    }
+
     const parsedSelector = tryParseSelector(rawSelector);
 
-    return {
+    const result = {
         rawSelector,
         isExit: rawSelector.endsWith(":exit"),
         parsedSelector,
@@ -182,7 +216,10 @@ const parseSelector = lodash.memoize(rawSelector => {
         attributeCount: countClassAttributes(parsedSelector),
         identifierCount: countIdentifiers(parsedSelector)
     };
-});
+
+    selectorCache.set(rawSelector, result);
+    return result;
+}
 
 //------------------------------------------------------------------------------
 // Public Interface
diff --git a/lib/rules/comma-dangle.js b/lib/rules/comma-dangle.js
index e22b7f355..798c111ec 100644
--- a/lib/rules/comma-dangle.js
+++ b/lib/rules/comma-dangle.js
@@ -9,7 +9,6 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const lodash = require("lodash");
 const astUtils = require("./utils/ast-utils");
 
 //------------------------------------------------------------------------------
@@ -144,23 +143,33 @@ module.exports = {
          * @returns {ASTNode|null} The last node or null.
          */
         function getLastItem(node) {
+
+            /**
+             * Returns the last element of an array
+             * @param {any[]} array The input array
+             * @returns {any} The last element
+             */
+            function last(array) {
+                return array[array.length - 1];
+            }
+
             switch (node.type) {
                 case "ObjectExpression":
                 case "ObjectPattern":
-                    return lodash.last(node.properties);
+                    return last(node.properties);
                 case "ArrayExpression":
                 case "ArrayPattern":
-                    return lodash.last(node.elements);
+                    return last(node.elements);
                 case "ImportDeclaration":
                 case "ExportNamedDeclaration":
-                    return lodash.last(node.specifiers);
+                    return last(node.specifiers);
                 case "FunctionDeclaration":
                 case "FunctionExpression":
                 case "ArrowFunctionExpression":
-                    return lodash.last(node.params);
+                    return last(node.params);
                 case "CallExpression":
                 case "NewExpression":
-                    return lodash.last(node.arguments);
+                    return last(node.arguments);
                 default:
                     return null;
             }
@@ -316,7 +325,7 @@ module.exports = {
             "always-multiline": forceTrailingCommaIfMultiline,
             "only-multiline": allowTrailingCommaIfMultiline,
             never: forbidTrailingComma,
-            ignore: lodash.noop
+            ignore: () => {}
         };
 
         return {
diff --git a/lib/rules/comma-spacing.js b/lib/rules/comma-spacing.js
index 73c10a771..2bf41a00b 100644
--- a/lib/rules/comma-spacing.js
+++ b/lib/rules/comma-spacing.js
@@ -181,7 +181,7 @@ module.exports = {
 
                     validateCommaItemSpacing({
                         comma: token,
-                        left: astUtils.isCommaToken(previousToken) || commaTokensToIgnore.indexOf(token) > -1 ? null : previousToken,
+                        left: astUtils.isCommaToken(previousToken) || commaTokensToIgnore.includes(token) ? null : previousToken,
                         right: astUtils.isCommaToken(nextToken) ? null : nextToken
                     }, token);
                 });
diff --git a/lib/rules/complexity.js b/lib/rules/complexity.js
index 5d62c6ff4..116c8ad0a 100644
--- a/lib/rules/complexity.js
+++ b/lib/rules/complexity.js
@@ -10,9 +10,8 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const lodash = require("lodash");
-
 const astUtils = require("./utils/ast-utils");
+const { upperCaseFirst } = require("../shared/string-utils");
 
 //------------------------------------------------------------------------------
 // Rule Definition
@@ -95,7 +94,7 @@ module.exports = {
          * @private
          */
         function endFunction(node) {
-            const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(node));
+            const name = upperCaseFirst(astUtils.getFunctionNameWithKind(node));
             const complexity = fns.pop();
 
             if (complexity > THRESHOLD) {
diff --git a/lib/rules/consistent-return.js b/lib/rules/consistent-return.js
index 94db253d2..a250430cb 100644
--- a/lib/rules/consistent-return.js
+++ b/lib/rules/consistent-return.js
@@ -8,8 +8,8 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const lodash = require("lodash");
 const astUtils = require("./utils/ast-utils");
+const { upperCaseFirst } = require("../shared/string-utils");
 
 //------------------------------------------------------------------------------
 // Helpers
@@ -164,7 +164,7 @@ module.exports = {
                     funcInfo.data = {
                         name: funcInfo.node.type === "Program"
                             ? "Program"
-                            : lodash.upperFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
+                            : upperCaseFirst(astUtils.getFunctionNameWithKind(funcInfo.node))
                     };
                 } else if (funcInfo.hasReturnValue !== hasReturnValue) {
                     context.report({
diff --git a/lib/rules/eol-last.js b/lib/rules/eol-last.js
index 89c76acb2..24b0c9279 100644
--- a/lib/rules/eol-last.js
+++ b/lib/rules/eol-last.js
@@ -4,12 +4,6 @@
  */
 "use strict";
 
-//------------------------------------------------------------------------------
-// Requirements
-//------------------------------------------------------------------------------
-
-const lodash = require("lodash");
-
 //------------------------------------------------------------------------------
 // Rule Definition
 //------------------------------------------------------------------------------
@@ -48,8 +42,9 @@ module.exports = {
             Program: function checkBadEOF(node) {
                 const sourceCode = context.getSourceCode(),
                     src = sourceCode.getText(),
+                    lastLine = sourceCode.lines[sourceCode.lines.length - 1],
                     location = {
-                        column: lodash.last(sourceCode.lines).length,
+                        column: lastLine.length,
                         line: sourceCode.lines.length
                     },
                     LF = "\n",
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index 8f4079d31..b1af2a73b 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -12,10 +12,10 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const lodash = require("lodash");
-const astUtils = require("./utils/ast-utils");
 const createTree = require("functional-red-black-tree");
 
+const astUtils = require("./utils/ast-utils");
+
 //------------------------------------------------------------------------------
 // Rule Definition
 //------------------------------------------------------------------------------
@@ -1068,7 +1068,7 @@ module.exports = {
         const baseOffsetListeners = {
             "ArrayExpression, ArrayPattern"(node) {
                 const openingBracket = sourceCode.getFirstToken(node);
-                const closingBracket = sourceCode.getTokenAfter(lodash.findLast(node.elements) || openingBracket, astUtils.isClosingBracketToken);
+                const closingBracket = sourceCode.getTokenAfter([...node.elements].reverse().find(_ => _) || openingBracket, astUtils.isClosingBracketToken);
 
                 addElementListIndent(node.elements, openingBracket, closingBracket, options.ArrayExpression);
             },
@@ -1560,8 +1560,9 @@ module.exports = {
          * 2. Don't set any offsets against the first token of the node.
          * 3. Call `ignoreNode` on the node sometime after exiting it and before validating offsets.
          */
-        const offsetListeners = lodash.mapValues(
-            baseOffsetListeners,
+        const offsetListeners = {};
+
+        for (const [selector, listener] of Object.entries(baseOffsetListeners)) {
 
             /*
              * Offset listener calls are deferred until traversal is finished, and are called as
@@ -1579,10 +1580,8 @@ module.exports = {
              * To avoid this, the `Identifier` listener isn't called until traversal finishes and all
              * ignored nodes are known.
              */
-            listener =>
-                node =>
-                    listenerCallQueue.push({ listener, node })
-        );
+            offsetListeners[selector] = node => listenerCallQueue.push({ listener, node });
+        }
 
         // For each ignored node selector, set up a listener to collect it into the `ignoredNodes` set.
         const ignoredNodes = new Set();
diff --git a/lib/rules/max-lines-per-function.js b/lib/rules/max-lines-per-function.js
index de70cecb4..60e2e879f 100644
--- a/lib/rules/max-lines-per-function.js
+++ b/lib/rules/max-lines-per-function.js
@@ -9,8 +9,7 @@
 //------------------------------------------------------------------------------
 
 const astUtils = require("./utils/ast-utils");
-
-const lodash = require("lodash");
+const { upperCaseFirst } = require("../shared/string-utils");
 
 //------------------------------------------------------------------------------
 // Constants
@@ -191,7 +190,7 @@ module.exports = {
             }
 
             if (lineCount > maxLines) {
-                const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(funcNode));
+                const name = upperCaseFirst(astUtils.getFunctionNameWithKind(funcNode));
 
                 context.report({
                     node,
diff --git a/lib/rules/max-lines.js b/lib/rules/max-lines.js
index ceb014aff..8bd5a1c95 100644
--- a/lib/rules/max-lines.js
+++ b/lib/rules/max-lines.js
@@ -8,9 +8,22 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const lodash = require("lodash");
 const astUtils = require("./utils/ast-utils");
 
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+/**
+ * Creates an array of numbers from `start` up to, but not including, `end`
+ * @param {number} start The start of the range
+ * @param {number} end The end of the range
+ * @returns {number[]} The range of numbers
+ */
+function range(start, end) {
+    return [...Array(end - start).keys()].map(x => x + start);
+}
+
 //------------------------------------------------------------------------------
 // Rule Definition
 //------------------------------------------------------------------------------
@@ -119,11 +132,25 @@ module.exports = {
             }
 
             if (start <= end) {
-                return lodash.range(start, end + 1);
+                return range(start, end + 1);
             }
             return [];
         }
 
+        /**
+         * Returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level.
+         * TODO(stephenwade): Replace this with array.flatMap when we drop support for Node v10
+         * @param {any[]} array The array to process
+         * @param {Function} fn The function to use
+         * @returns {any[]} The result array
+         */
+        function flatMap(array, fn) {
+            const mapped = array.map(fn);
+            const flattened = [].concat(...mapped);
+
+            return flattened;
+        }
+
         return {
             "Program:exit"() {
                 let lines = sourceCode.lines.map((text, i) => ({
@@ -135,7 +162,7 @@ module.exports = {
                  * If file ends with a linebreak, `sourceCode.lines` will have one extra empty line at the end.
                  * That isn't a real line, so we shouldn't count it.
                  */
-                if (lines.length > 1 && lodash.last(lines).text === "") {
+                if (lines.length > 1 && lines[lines.length - 1].text === "") {
                     lines.pop();
                 }
 
@@ -146,9 +173,7 @@ module.exports = {
                 if (skipComments) {
                     const comments = sourceCode.getAllComments();
 
-                    const commentLines = lodash.flatten(
-                        comments.map(comment => getLinesWithoutCode(comment))
-                    );
+                    const commentLines = flatMap(comments, comment => getLinesWithoutCode(comment));
 
                     lines = lines.filter(
                         l => !commentLines.includes(l.lineNumber)
@@ -163,7 +188,7 @@ module.exports = {
                         },
                         end: {
                             line: sourceCode.lines.length,
-                            column: lodash.last(sourceCode.lines).length
+                            column: sourceCode.lines[sourceCode.lines.length - 1].length
                         }
                     };
 
diff --git a/lib/rules/max-params.js b/lib/rules/max-params.js
index 4eebe2d95..8fb798401 100644
--- a/lib/rules/max-params.js
+++ b/lib/rules/max-params.js
@@ -9,9 +9,8 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const lodash = require("lodash");
-
 const astUtils = require("./utils/ast-utils");
+const { upperCaseFirst } = require("../shared/string-utils");
 
 //------------------------------------------------------------------------------
 // Rule Definition
@@ -85,7 +84,7 @@ module.exports = {
                     node,
                     messageId: "exceed",
                     data: {
-                        name: lodash.upperFirst(astUtils.getFunctionNameWithKind(node)),
+                        name: upperCaseFirst(astUtils.getFunctionNameWithKind(node)),
                         count: node.params.length,
                         max: numParams
                     }
diff --git a/lib/rules/max-statements.js b/lib/rules/max-statements.js
index 437b393a5..65d553955 100644
--- a/lib/rules/max-statements.js
+++ b/lib/rules/max-statements.js
@@ -9,9 +9,8 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const lodash = require("lodash");
-
 const astUtils = require("./utils/ast-utils");
+const { upperCaseFirst } = require("../shared/string-utils");
 
 //------------------------------------------------------------------------------
 // Rule Definition
@@ -97,7 +96,7 @@ module.exports = {
          */
         function reportIfTooManyStatements(node, count, max) {
             if (count > max) {
-                const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(node));
+                const name = upperCaseFirst(astUtils.getFunctionNameWithKind(node));
 
                 context.report({
                     node,
diff --git a/lib/rules/no-fallthrough.js b/lib/rules/no-fallthrough.js
index dd1f3ed9d..e8016e93e 100644
--- a/lib/rules/no-fallthrough.js
+++ b/lib/rules/no-fallthrough.js
@@ -4,12 +4,6 @@
  */
 "use strict";
 
-//------------------------------------------------------------------------------
-// Requirements
-//------------------------------------------------------------------------------
-
-const lodash = require("lodash");
-
 //------------------------------------------------------------------------------
 // Helpers
 //------------------------------------------------------------------------------
@@ -25,7 +19,7 @@ const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/iu;
  */
 function hasFallthroughComment(node, context, fallthroughCommentPattern) {
     const sourceCode = context.getSourceCode();
-    const comment = lodash.last(sourceCode.getCommentsBefore(node));
+    const comment = sourceCode.getCommentsBefore(node).pop();
 
     return Boolean(comment && fallthroughCommentPattern.test(comment.value));
 }
@@ -133,7 +127,7 @@ module.exports = {
                  */
                 if (currentCodePath.currentSegments.some(isReachable) &&
                     (node.consequent.length > 0 || hasBlankLinesBetween(node, nextToken)) &&
-                    lodash.last(node.parent.cases) !== node) {
+                    node.parent.cases[node.parent.cases.length - 1] !== node) {
                     fallthroughCase = node;
                 }
             }
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 {
diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js
index 7619be331..adf465905 100644
--- a/lib/rules/no-unused-vars.js
+++ b/lib/rules/no-unused-vars.js
@@ -410,6 +410,31 @@ module.exports = {
             );
         }
 
+        /**
+         * Checks whether a given node is unused expression or not.
+         * @param {ASTNode} node The node itself
+         * @returns {boolean} The node is an unused expression.
+         * @private
+         */
+        function isUnusedExpression(node) {
+            const parent = node.parent;
+
+            if (parent.type === "ExpressionStatement") {
+                return true;
+            }
+
+            if (parent.type === "SequenceExpression") {
+                const isLastExpression = parent.expressions[parent.expressions.length - 1] === node;
+
+                if (!isLastExpression) {
+                    return true;
+                }
+                return isUnusedExpression(parent);
+            }
+
+            return false;
+        }
+
         /**
          * Checks whether a given reference is a read to update itself or not.
          * @param {eslint-scope.Reference} ref A reference to check.
@@ -420,7 +445,6 @@ module.exports = {
         function isReadForItself(ref, rhsNode) {
             const id = ref.identifier;
             const parent = id.parent;
-            const grandparent = parent.parent;
 
             return ref.isRead() && (
 
@@ -428,12 +452,12 @@ module.exports = {
                 (// in RHS of an assignment for itself. e.g. `a = a + 1`
                     ((
                         parent.type === "AssignmentExpression" &&
-                    grandparent.type === "ExpressionStatement" &&
+                    isUnusedExpression(parent) &&
                     parent.left === id
                     ) ||
                 (
                     parent.type === "UpdateExpression" &&
-                    grandparent.type === "ExpressionStatement"
+                    isUnusedExpression(parent)
                 ) || rhsNode &&
                 isInside(id, rhsNode) &&
                 !isInsideOfStorableFunction(id, rhsNode)))
diff --git a/lib/rules/no-useless-backreference.js b/lib/rules/no-useless-backreference.js
index 958e3d5dd..529c16439 100644
--- a/lib/rules/no-useless-backreference.js
+++ b/lib/rules/no-useless-backreference.js
@@ -11,7 +11,6 @@
 
 const { CALL, CONSTRUCT, ReferenceTracker, getStringIfConstant } = require("eslint-utils");
 const { RegExpParser, visitRegExpAST } = require("regexpp");
-const lodash = require("lodash");
 
 //------------------------------------------------------------------------------
 // Helpers
@@ -137,7 +136,7 @@ module.exports = {
 
                             // the opposite of the previous when the regex is matching backward in a lookbehind context.
                             messageId = "backward";
-                        } else if (lodash.last(groupCut).type === "Alternative") {
+                        } else if (groupCut[groupCut.length - 1].type === "Alternative") {
 
                             // group's and bref's ancestor nodes below the lowest common ancestor are sibling alternatives => they're disjunctive.
                             messageId = "disjunctive";
diff --git a/lib/rules/no-useless-computed-key.js b/lib/rules/no-useless-computed-key.js
index e0505a318..a1cacc296 100644
--- a/lib/rules/no-useless-computed-key.js
+++ b/lib/rules/no-useless-computed-key.js
@@ -8,7 +8,6 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const lodash = require("lodash");
 const astUtils = require("./utils/ast-utils");
 
 //------------------------------------------------------------------------------
@@ -95,9 +94,16 @@ module.exports = {
             }
         }
 
+        /**
+         * A no-op function to act as placeholder for checking a node when the `enforceForClassMembers` option is `false`.
+         * @returns {void}
+         * @private
+         */
+        function noop() {}
+
         return {
             Property: check,
-            MethodDefinition: enforceForClassMembers ? check : lodash.noop
+            MethodDefinition: enforceForClassMembers ? check : noop
         };
     }
 };
diff --git a/lib/rules/no-warning-comments.js b/lib/rules/no-warning-comments.js
index 0691a31f7..e5f702bc7 100644
--- a/lib/rules/no-warning-comments.js
+++ b/lib/rules/no-warning-comments.js
@@ -5,7 +5,7 @@
 
 "use strict";
 
-const { escapeRegExp } = require("lodash");
+const escapeRegExp = require("escape-string-regexp");
 const astUtils = require("./utils/ast-utils");
 
 const CHAR_LIMIT = 40;
diff --git a/lib/rules/object-curly-newline.js b/lib/rules/object-curly-newline.js
index 9b64a1b5c..1fbea00c5 100644
--- a/lib/rules/object-curly-newline.js
+++ b/lib/rules/object-curly-newline.js
@@ -10,7 +10,6 @@
 //------------------------------------------------------------------------------
 
 const astUtils = require("./utils/ast-utils");
-const lodash = require("lodash");
 
 //------------------------------------------------------------------------------
 // Helpers
@@ -69,6 +68,24 @@ function normalizeOptionValue(value) {
     return { multiline, minProperties, consistent };
 }
 
+/**
+ * Checks if a value is an object.
+ * @param {any} value The value to check
+ * @returns {boolean} `true` if the value is an object, otherwise `false`
+ */
+function isObject(value) {
+    return typeof value === "object" && value !== null;
+}
+
+/**
+ * Checks if an option is a node-specific option
+ * @param {any} option The option to check
+ * @returns {boolean} `true` if the option is node-specific, otherwise `false`
+ */
+function isNodeSpecificOption(option) {
+    return isObject(option) || typeof option === "string";
+}
+
 /**
  * Normalizes a given option value.
  * @param {string|Object|undefined} options An option value to parse.
@@ -80,9 +97,7 @@ function normalizeOptionValue(value) {
  * }} Normalized option object.
  */
 function normalizeOptions(options) {
-    const isNodeSpecificOption = lodash.overSome([lodash.isPlainObject, lodash.isString]);
-
-    if (lodash.isPlainObject(options) && Object.values(options).some(isNodeSpecificOption)) {
+    if (isObject(options) && Object.values(options).some(isNodeSpecificOption)) {
         return {
             ObjectExpression: normalizeOptionValue(options.ObjectExpression),
             ObjectPattern: normalizeOptionValue(options.ObjectPattern),
diff --git a/lib/rules/spaced-comment.js b/lib/rules/spaced-comment.js
index d3221f0ea..226a2d447 100644
--- a/lib/rules/spaced-comment.js
+++ b/lib/rules/spaced-comment.js
@@ -4,7 +4,7 @@
  */
 "use strict";
 
-const lodash = require("lodash");
+const escapeRegExp = require("escape-string-regexp");
 const astUtils = require("./utils/ast-utils");
 
 //------------------------------------------------------------------------------
@@ -17,7 +17,7 @@ const astUtils = require("./utils/ast-utils");
  * @returns {string} An escaped string.
  */
 function escape(s) {
-    return `(?:${lodash.escapeRegExp(s)})`;
+    return `(?:${escapeRegExp(s)})`;
 }
 
 /**
diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js
index 679eebb4c..6b8530011 100644
--- a/lib/rules/utils/ast-utils.js
+++ b/lib/rules/utils/ast-utils.js
@@ -11,7 +11,7 @@
 
 const esutils = require("esutils");
 const espree = require("espree");
-const lodash = require("lodash");
+const escapeRegExp = require("escape-string-regexp");
 const {
     breakableTypePattern,
     createGlobalLinebreakMatcher,
@@ -1756,7 +1756,7 @@ module.exports = {
      * @returns {SourceLocation} The `loc` object.
      */
     getNameLocationInGlobalDirectiveComment(sourceCode, comment, name) {
-        const namePattern = new RegExp(`[\\s,]${lodash.escapeRegExp(name)}(?:$|[\\s,:])`, "gu");
+        const namePattern = new RegExp(`[\\s,]${escapeRegExp(name)}(?:$|[\\s,:])`, "gu");
 
         // To ignore the first text "global".
         namePattern.lastIndex = comment.value.indexOf("global") + 6;
diff --git a/lib/shared/deprecation-warnings.js b/lib/shared/deprecation-warnings.js
index 1438eaa69..1a0501ab0 100644
--- a/lib/shared/deprecation-warnings.js
+++ b/lib/shared/deprecation-warnings.js
@@ -9,7 +9,6 @@
 //------------------------------------------------------------------------------
 
 const path = require("path");
-const lodash = require("lodash");
 
 //------------------------------------------------------------------------------
 // Private
@@ -28,6 +27,8 @@ const deprecationWarningMessages = {
         "projects in order to avoid loading '~/.eslintrc.*' accidentally."
 };
 
+const sourceFileErrorCache = new Set();
+
 /**
  * Emits a deprecation warning containing a given filepath. A new deprecation warning is emitted
  * for each unique file path, but repeated invocations with the same file path have no effect.
@@ -36,7 +37,15 @@ const deprecationWarningMessages = {
  * @param {string} errorCode The warning message to show.
  * @returns {void}
  */
-const emitDeprecationWarning = lodash.memoize((source, errorCode) => {
+function emitDeprecationWarning(source, errorCode) {
+    const cacheKey = JSON.stringify({ source, errorCode });
+
+    if (sourceFileErrorCache.has(cacheKey)) {
+        return;
+    }
+
+    sourceFileErrorCache.add(cacheKey);
+
     const rel = path.relative(process.cwd(), source);
     const message = deprecationWarningMessages[errorCode];
 
@@ -45,7 +54,7 @@ const emitDeprecationWarning = lodash.memoize((source, errorCode) => {
         "DeprecationWarning",
         errorCode
     );
-}, (...args) => JSON.stringify(args));
+}
 
 //------------------------------------------------------------------------------
 // Public Interface
diff --git a/lib/shared/string-utils.js b/lib/shared/string-utils.js
new file mode 100644
index 000000000..e4a55d799
--- /dev/null
+++ b/lib/shared/string-utils.js
@@ -0,0 +1,22 @@
+/**
+ * @fileoverview Utilities to operate on strings.
+ * @author Stephen Wade
+ */
+
+"use strict";
+
+/**
+ * Converts the first letter of a string to uppercase.
+ * @param {string} string The string to operate on
+ * @returns {string} The converted string
+ */
+function upperCaseFirst(string) {
+    if (string.length <= 1) {
+        return string.toUpperCase();
+    }
+    return string[0].toUpperCase() + string.slice(1);
+}
+
+module.exports = {
+    upperCaseFirst
+};
diff --git a/lib/source-code/source-code.js b/lib/source-code/source-code.js
index 6b20495b6..c13ce29b8 100644
--- a/lib/source-code/source-code.js
+++ b/lib/source-code/source-code.js
@@ -12,8 +12,7 @@ const
     { isCommentToken } = require("eslint-utils"),
     TokenStore = require("./token-store"),
     astUtils = require("../shared/ast-utils"),
-    Traverser = require("../shared/traverser"),
-    lodash = require("lodash");
+    Traverser = require("../shared/traverser");
 
 //------------------------------------------------------------------------------
 // Private
@@ -531,10 +530,12 @@ class SourceCode extends TokenStore {
         }
 
         /*
-         * To figure out which line rangeIndex is on, determine the last index at which rangeIndex could
-         * be inserted into lineIndices to keep the list sorted.
+         * To figure out which line index is on, determine the last place at which index could
+         * be inserted into lineStartIndices to keep the list sorted.
          */
-        const lineNumber = lodash.sortedLastIndex(this.lineStartIndices, index);
+        const lineNumber = index >= this.lineStartIndices[this.lineStartIndices.length - 1]
+            ? this.lineStartIndices.length
+            : this.lineStartIndices.findIndex(el => index < el);
 
         return { line: lineNumber, column: index - this.lineStartIndices[lineNumber - 1] };
     }
diff --git a/lib/source-code/token-store/utils.js b/lib/source-code/token-store/utils.js
index 21e1d6ff7..a2bd77de7 100644
--- a/lib/source-code/token-store/utils.js
+++ b/lib/source-code/token-store/utils.js
@@ -4,12 +4,6 @@
  */
 "use strict";
 
-//------------------------------------------------------------------------------
-// Requirements
-//------------------------------------------------------------------------------
-
-const lodash = require("lodash");
-
 //------------------------------------------------------------------------------
 // Helpers
 //------------------------------------------------------------------------------
@@ -29,18 +23,16 @@ function getStartLocation(token) {
 //------------------------------------------------------------------------------
 
 /**
- * Binary-searches the index of the first token which is after the given location.
+ * Finds the index of the first token which is after the given location.
  * If it was not found, this returns `tokens.length`.
  * @param {(Token|Comment)[]} tokens It searches the token in this list.
  * @param {number} location The location to search.
  * @returns {number} The found index or `tokens.length`.
  */
 exports.search = function search(tokens, location) {
-    return lodash.sortedIndexBy(
-        tokens,
-        { range: [location] },
-        getStartLocation
-    );
+    const index = tokens.findIndex(el => location <= getStartLocation(el));
+
+    return index === -1 ? tokens.length : index;
 };
 
 /**
diff --git a/messages/all-files-ignored.txt b/messages/all-files-ignored.js
similarity index 51%
rename from messages/all-files-ignored.txt
rename to messages/all-files-ignored.js
index 3f4c8ced4..d85828d36 100644
--- a/messages/all-files-ignored.txt
+++ b/messages/all-files-ignored.js
@@ -1,8 +1,16 @@
-You are linting "<%= pattern %>", but all of the files matching the glob pattern "<%= pattern %>" are ignored.
+"use strict";
 
-If you don't want to lint these files, remove the pattern "<%= pattern %>" from the list of arguments passed to ESLint.
+module.exports = function(it) {
+    const { pattern } = it;
+
+    return `
+You are linting "${pattern}", but all of the files matching the glob pattern "${pattern}" are ignored.
+
+If you don't want to lint these files, remove the pattern "${pattern}" from the list of arguments passed to ESLint.
 
 If you do want to lint these files, try the following solutions:
 
 * Check your .eslintignore file, or the eslintIgnore property in package.json, to ensure that the files are not configured to be ignored.
 * Explicitly list the files from this glob that you'd like to lint on the command-line, rather than providing a glob as an argument.
+`.trimLeft();
+};
diff --git a/messages/extend-config-missing.js b/messages/extend-config-missing.js
new file mode 100644
index 000000000..db8a5c64b
--- /dev/null
+++ b/messages/extend-config-missing.js
@@ -0,0 +1,13 @@
+"use strict";
+
+module.exports = function(it) {
+    const { configName, importerName } = it;
+
+    return `
+ESLint couldn't find the config "${configName}" to extend from. Please check that the name of the config is correct.
+
+The config "${configName}" was referenced from the config file in "${importerName}".
+
+If you still have problems, please stop by https://eslint.org/chat/help to chat with the team.
+`.trimLeft();
+};
diff --git a/messages/extend-config-missing.txt b/messages/extend-config-missing.txt
deleted file mode 100644
index 4defd7ac4..000000000
--- a/messages/extend-config-missing.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-ESLint couldn't find the config "<%- configName %>" to extend from. Please check that the name of the config is correct.
-
-The config "<%- configName %>" was referenced from the config file in "<%- importerName %>".
-
-If you still have problems, please stop by https://eslint.org/chat/help to chat with the team.
diff --git a/messages/failed-to-read-json.js b/messages/failed-to-read-json.js
new file mode 100644
index 000000000..5114de309
--- /dev/null
+++ b/messages/failed-to-read-json.js
@@ -0,0 +1,11 @@
+"use strict";
+
+module.exports = function(it) {
+    const { path, message } = it;
+
+    return `
+Failed to read JSON file at ${path}:
+
+${message}
+`.trimLeft();
+};
diff --git a/messages/failed-to-read-json.txt b/messages/failed-to-read-json.txt
deleted file mode 100644
index b5e2b861c..000000000
--- a/messages/failed-to-read-json.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Failed to read JSON file at <%= path %>:
-
-<%= message %>
diff --git a/messages/file-not-found.js b/messages/file-not-found.js
new file mode 100644
index 000000000..26a5d57ef
--- /dev/null
+++ b/messages/file-not-found.js
@@ -0,0 +1,10 @@
+"use strict";
+
+module.exports = function(it) {
+    const { pattern, globDisabled } = it;
+
+    return `
+No files matching the pattern "${pattern}"${globDisabled ? " (with disabling globs)" : ""} were found.
+Please check for typing mistakes in the pattern.
+`.trimLeft();
+};
diff --git a/messages/file-not-found.txt b/messages/file-not-found.txt
deleted file mode 100644
index 639498eb5..000000000
--- a/messages/file-not-found.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-No files matching the pattern "<%= pattern %>"<% if (globDisabled) { %> (with disabling globs)<% } %> were found.
-Please check for typing mistakes in the pattern.
diff --git a/messages/no-config-found.txt b/messages/no-config-found.js
similarity index 52%
rename from messages/no-config-found.txt
rename to messages/no-config-found.js
index b46a7e5a7..1042143f9 100644
--- a/messages/no-config-found.txt
+++ b/messages/no-config-found.js
@@ -1,7 +1,15 @@
+"use strict";
+
+module.exports = function(it) {
+    const { directoryPath } = it;
+
+    return `
 ESLint couldn't find a configuration file. To set up a configuration file for this project, please run:
 
     eslint --init
 
-ESLint looked for configuration files in <%= directoryPath %> and its ancestors. If it found none, it then looked in your home directory.
+ESLint looked for configuration files in ${directoryPath} and its ancestors. If it found none, it then looked in your home directory.
 
 If you think you already have a configuration file or if you need more help, please stop by the ESLint chat room: https://eslint.org/chat/help
+`.trimLeft();
+};
diff --git a/messages/plugin-conflict.js b/messages/plugin-conflict.js
new file mode 100644
index 000000000..c8c060e2f
--- /dev/null
+++ b/messages/plugin-conflict.js
@@ -0,0 +1,22 @@
+"use strict";
+
+module.exports = function(it) {
+    const { pluginId, plugins } = it;
+
+    let result = `ESLint couldn't determine the plugin "${pluginId}" uniquely.
+`;
+
+    for (const { filePath, importerName } of plugins) {
+        result += `
+- ${filePath} (loaded in "${importerName}")`;
+    }
+
+    result += `
+
+Please remove the "plugins" setting from either config or remove either plugin installation.
+
+If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.
+`;
+
+    return result;
+};
diff --git a/messages/plugin-conflict.txt b/messages/plugin-conflict.txt
deleted file mode 100644
index 3ab4b340e..000000000
--- a/messages/plugin-conflict.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-ESLint couldn't determine the plugin "<%- pluginId %>" uniquely.
-<% for (const { filePath, importerName } of plugins) { %>
-- <%= filePath %> (loaded in "<%= importerName %>")<% } %>
-
-Please remove the "plugins" setting from either config or remove either plugin installation.
-
-If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.
diff --git a/messages/plugin-invalid.js b/messages/plugin-invalid.js
new file mode 100644
index 000000000..7913576f0
--- /dev/null
+++ b/messages/plugin-invalid.js
@@ -0,0 +1,16 @@
+"use strict";
+
+module.exports = function(it) {
+    const { configName, importerName } = it;
+
+    return `
+"${configName}" is invalid syntax for a config specifier.
+
+* If your intention is to extend from a configuration exported from the plugin, add the configuration name after a slash: e.g. "${configName}/myConfig".
+* If this is the name of a shareable config instead of a plugin, remove the "plugin:" prefix: i.e. "${configName.slice("plugin:".length)}".
+
+"${configName}" was referenced from the config file in "${importerName}".
+
+If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.
+`.trimLeft();
+};
diff --git a/messages/plugin-invalid.txt b/messages/plugin-invalid.txt
deleted file mode 100644
index 3ee251821..000000000
--- a/messages/plugin-invalid.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-"<%- configName %>" is invalid syntax for a config specifier.
-
-* If your intention is to extend from a configuration exported from the plugin, add the configuration name after a slash: e.g. "<%- configName %>/myConfig".
-* If this is the name of a shareable config instead of a plugin, remove the "plugin:" prefix: i.e. "<%- configName.slice("plugin:".length) %>".
-
-"<%- configName %>" was referenced from the config file in "<%- importerName %>".
-
-If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.
diff --git a/messages/plugin-missing.js b/messages/plugin-missing.js
new file mode 100644
index 000000000..f58c78ceb
--- /dev/null
+++ b/messages/plugin-missing.js
@@ -0,0 +1,19 @@
+"use strict";
+
+module.exports = function(it) {
+    const { pluginName, resolvePluginsRelativeTo, importerName } = it;
+
+    return `
+ESLint couldn't find the plugin "${pluginName}".
+
+(The package "${pluginName}" was not found when loaded as a Node module from the directory "${resolvePluginsRelativeTo}".)
+
+It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:
+
+    npm install ${pluginName}@latest --save-dev
+
+The plugin "${pluginName}" was referenced from the config file in "${importerName}".
+
+If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.
+`.trimLeft();
+};
diff --git a/messages/plugin-missing.txt b/messages/plugin-missing.txt
deleted file mode 100644
index aa25f59ac..000000000
--- a/messages/plugin-missing.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-ESLint couldn't find the plugin "<%- pluginName %>".
-
-(The package "<%- pluginName %>" was not found when loaded as a Node module from the directory "<%- resolvePluginsRelativeTo %>".)
-
-It's likely that the plugin isn't installed correctly. Try reinstalling by running the following:
-
-    npm install <%- pluginName %>@latest --save-dev
-
-The plugin "<%- pluginName %>" was referenced from the config file in "<%- importerName %>".
-
-If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.
diff --git a/messages/print-config-with-directory-path.txt b/messages/print-config-with-directory-path.js
similarity index 70%
rename from messages/print-config-with-directory-path.txt
rename to messages/print-config-with-directory-path.js
index 1afc9b1e8..6a5d571dd 100644
--- a/messages/print-config-with-directory-path.txt
+++ b/messages/print-config-with-directory-path.js
@@ -1,2 +1,8 @@
+"use strict";
+
+module.exports = function() {
+    return `
 The '--print-config' CLI option requires a path to a source code file rather than a directory.
 See also: https://eslint.org/docs/user-guide/command-line-interface#--print-config
+`.trimLeft();
+};
diff --git a/messages/whitespace-found.js b/messages/whitespace-found.js
new file mode 100644
index 000000000..4ce49ca3a
--- /dev/null
+++ b/messages/whitespace-found.js
@@ -0,0 +1,11 @@
+"use strict";
+
+module.exports = function(it) {
+    const { pluginName } = it;
+
+    return `
+ESLint couldn't find the plugin "${pluginName}". because there is whitespace in the name. Please check your configuration and remove all whitespace from the plugin name.
+
+If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.
+`.trimLeft();
+};
diff --git a/messages/whitespace-found.txt b/messages/whitespace-found.txt
deleted file mode 100644
index 3eed1af58..000000000
--- a/messages/whitespace-found.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-ESLint couldn't find the plugin "<%- pluginName %>". because there is whitespace in the name. Please check your configuration and remove all whitespace from the plugin name.
-
-If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team.
diff --git a/package.json b/package.json
index 51a76ec31..46c086010 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint",
-  "version": "7.26.0",
+  "version": "7.27.0",
   "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
   "description": "An AST-based pattern checker for JavaScript.",
   "bin": {
@@ -51,12 +51,14 @@
     "debug": "^4.0.1",
     "doctrine": "^3.0.0",
     "enquirer": "^2.3.5",
+    "escape-string-regexp": "^4.0.0",
     "eslint-scope": "^5.1.1",
     "eslint-utils": "^2.1.0",
     "eslint-visitor-keys": "^2.0.0",
     "espree": "^7.3.1",
     "esquery": "^1.4.0",
     "esutils": "^2.0.2",
+    "fast-deep-equal": "^3.1.3",
     "file-entry-cache": "^6.0.1",
     "functional-red-black-tree": "^1.0.1",
     "glob-parent": "^5.0.0",
@@ -68,7 +70,7 @@
     "js-yaml": "^3.13.1",
     "json-stable-stringify-without-jsonify": "^1.0.1",
     "levn": "^0.4.1",
-    "lodash": "^4.17.21",
+    "lodash.merge": "^4.6.2",
     "minimatch": "^3.0.4",
     "natural-compare": "^1.4.0",
     "optionator": "^0.9.1",
@@ -77,7 +79,7 @@
     "semver": "^7.2.1",
     "strip-ansi": "^6.0.0",
     "strip-json-comments": "^3.1.0",
-    "table": "^6.0.4",
+    "table": "^6.0.9",
     "text-table": "^0.2.0",
     "v8-compile-cache": "^2.0.3"
   },
@@ -91,10 +93,9 @@
     "core-js": "^3.1.3",
     "dateformat": "^3.0.3",
     "ejs": "^3.0.2",
-    "escape-string-regexp": "^3.0.0",
     "eslint": "file:.",
     "eslint-config-eslint": "file:packages/eslint-config-eslint",
-    "eslint-plugin-eslint-plugin": "^2.2.1",
+    "eslint-plugin-eslint-plugin": "^3.0.3",
     "eslint-plugin-internal-rules": "file:tools/internal-rules",
     "eslint-plugin-jsdoc": "^25.4.3",
     "eslint-plugin-node": "^11.1.0",
diff --git a/tools/eslint-fuzzer.js b/tools/eslint-fuzzer.js
index 4104b8516..c9705200e 100644
--- a/tools/eslint-fuzzer.js
+++ b/tools/eslint-fuzzer.js
@@ -10,13 +10,25 @@
 //------------------------------------------------------------------------------
 
 const assert = require("assert");
-const lodash = require("lodash");
 const eslump = require("eslump");
 const espree = require("espree");
 const SourceCodeFixer = require("../lib/linter/source-code-fixer");
 const ruleConfigs = require("../lib/init/config-rule").createCoreRuleConfigs(true);
 const sampleMinimizer = require("./code-sample-minimizer");
 
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+/**
+ * Gets a random item from an array
+ * @param {any[]} array The array to sample
+ * @returns {any} The random item
+ */
+function sample(array) {
+    return array[Math.floor(Math.random() * array.length)];
+}
+
 //------------------------------------------------------------------------------
 // Public API
 //------------------------------------------------------------------------------
@@ -125,10 +137,16 @@ function fuzz(options) {
     }
 
     for (let i = 0; i < options.count; progressCallback(problems.length), i++) {
-        const sourceType = lodash.sample(["script", "module"]);
+        const rules = {};
+
+        for (const [id, configs] of Object.entries(ruleConfigs)) {
+            rules[id] = sample(configs);
+        }
+
+        const sourceType = sample(["script", "module"]);
         const text = codeGenerator({ sourceType });
         const config = {
-            rules: lodash.mapValues(ruleConfigs, lodash.sample),
+            rules,
             parserOptions: {
                 sourceType,
                 ecmaVersion: espree.latestEcmaVersion
diff --git a/webpack.config.js b/webpack.config.js
index b256f4424..a22c99bf8 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -39,8 +39,7 @@ module.exports = {
                             targets: ">0.5%, not chrome 49, not ie 11, not safari 5.1"
                         }]
                     ]
-                },
-                exclude: /node_modules[\\/]lodash/u
+                }
             }
         ]
     },


Test patch:
diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js
index 23b78fba2..b08303c62 100644
--- a/lib/rule-tester/rule-tester.js
+++ b/lib/rule-tester/rule-tester.js
@@ -44,7 +44,8 @@ const
     assert = require("assert"),
     path = require("path"),
     util = require("util"),
-    lodash = require("lodash"),
+    merge = require("lodash.merge"),
+    equal = require("fast-deep-equal"),
     Traverser = require("../../lib/shared/traverser"),
     { getRuleOptionsSchema, validate } = require("../shared/config-validator"),
     { Linter, SourceCodeFixer, interpolate } = require("../linter");
@@ -324,10 +325,9 @@ class RuleTester {
          * configuration and the default configuration.
          * @type {Object}
          */
-        this.testerConfig = lodash.merge(
-
-            // we have to clone because merge uses the first argument for recipient
-            lodash.cloneDeep(defaultConfig),
+        this.testerConfig = merge(
+            {},
+            defaultConfig,
             testerConfig,
             { rules: { "rule-tester/validate-ast": "error" } }
         );
@@ -369,7 +369,7 @@ class RuleTester {
      * @returns {void}
      */
     static resetDefaultConfig() {
-        defaultConfig = lodash.cloneDeep(testerDefaultConfig);
+        defaultConfig = merge({}, testerDefaultConfig);
     }
 
 
@@ -465,7 +465,7 @@ class RuleTester {
          * @private
          */
         function runRuleForItem(item) {
-            let config = lodash.cloneDeep(testerConfig),
+            let config = merge({}, testerConfig),
                 code, filename, output, beforeAST, afterAST;
 
             if (typeof item === "string") {
@@ -477,13 +477,17 @@ class RuleTester {
                  * Assumes everything on the item is a config except for the
                  * parameters used by this tester
                  */
-                const itemConfig = lodash.omit(item, RuleTesterParameters);
+                const itemConfig = { ...item };
+
+                for (const parameter of RuleTesterParameters) {
+                    delete itemConfig[parameter];
+                }
 
                 /*
                  * Create the config object from the tester config and this item
                  * specific configurations.
                  */
-                config = lodash.merge(
+                config = merge(
                     config,
                     itemConfig
                 );
@@ -589,7 +593,7 @@ class RuleTester {
          * @private
          */
         function assertASTDidntChange(beforeAST, afterAST) {
-            if (!lodash.isEqual(beforeAST, afterAST)) {
+            if (!equal(beforeAST, afterAST)) {
                 assert.fail("Rule should not modify AST.");
             }
         }
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/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js
index 3f2393621..3aaf6f91f 100644
--- a/tests/lib/rule-tester/rule-tester.js
+++ b/tests/lib/rule-tester/rule-tester.js
@@ -11,8 +11,7 @@ const sinon = require("sinon"),
     EventEmitter = require("events"),
     { RuleTester } = require("../../../lib/rule-tester"),
     assert = require("chai").assert,
-    nodeAssert = require("assert"),
-    { noop } = require("lodash");
+    nodeAssert = require("assert");
 
 const NODE_ASSERT_STRICT_EQUAL_OPERATOR = (() => {
     try {
@@ -23,6 +22,15 @@ const NODE_ASSERT_STRICT_EQUAL_OPERATOR = (() => {
     throw new Error("unexpected successful assertion");
 })();
 
+/**
+ * Do nothing.
+ * @returns {void}
+ */
+function noop() {
+
+    // do nothing.
+}
+
 //------------------------------------------------------------------------------
 // Rewire Things
 //------------------------------------------------------------------------------
diff --git a/tests/lib/rules/no-invalid-this.js b/tests/lib/rules/no-invalid-this.js
index 6e1b757a7..3b19d6369 100644
--- a/tests/lib/rules/no-invalid-this.js
+++ b/tests/lib/rules/no-invalid-this.js
@@ -9,7 +9,8 @@
 // Requirements
 //------------------------------------------------------------------------------
 
-const lodash = require("lodash");
+const merge = require("lodash.merge");
+
 const rule = require("../../../lib/rules/no-invalid-this");
 const { RuleTester } = require("../../../lib/rule-tester");
 
@@ -69,7 +70,7 @@ function extractPatterns(patterns, type) {
 
     // Clone and apply the pattern environment.
     const patternsList = patterns.map(pattern => pattern[type].map(applyCondition => {
-        const thisPattern = lodash.cloneDeep(pattern);
+        const thisPattern = merge({}, pattern);
 
         applyCondition(thisPattern);
 
@@ -79,7 +80,10 @@ function extractPatterns(patterns, type) {
             thisPattern.code += " /* should error */";
         }
 
-        return lodash.omit(thisPattern, ["valid", "invalid"]);
+        delete thisPattern.valid;
+        delete thisPattern.invalid;
+
+        return thisPattern;
     }));
 
     // Flatten.
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-unused-vars.js b/tests/lib/rules/no-unused-vars.js
index 423afa167..48ccdb1d4 100644
--- a/tests/lib/rules/no-unused-vars.js
+++ b/tests/lib/rules/no-unused-vars.js
@@ -177,6 +177,10 @@ ruleTester.run("no-unused-vars", rule, {
         { code: "(function(obj) { for ( const name in obj ) { return true } })({})", parserOptions: { ecmaVersion: 6 } },
         { code: "(function(obj) { for ( const name in obj ) return true })({})", parserOptions: { ecmaVersion: 6 } },
 
+        // Sequence Expressions (See https://github.com/eslint/eslint/issues/14325)
+        { code: "let x = 0; foo = (0, x++);", parserOptions: { ecmaVersion: 6 } },
+        { code: "let x = 0; foo = (0, x += 1);", parserOptions: { ecmaVersion: 6 } },
+
         // caughtErrors
         {
             code: "try{}catch(err){console.error(err);}",
@@ -995,6 +999,71 @@ ruleTester.run("no-unused-vars", rule, {
                 definedError("c")
             ]
         },
+
+        // https://github.com/eslint/eslint/issues/14325
+        {
+            code: `let x = 0;
+            x++, x = 0;`,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 2, column: 18 }]
+        },
+        {
+            code: `let x = 0;
+            x++, x = 0;
+            x=3;`,
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 3, column: 13 }]
+        },
+        {
+            code: "let x = 0; x++, 0;",
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 1, column: 12 }]
+        },
+        {
+            code: "let x = 0; 0, x++;",
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 1, column: 15 }]
+        },
+        {
+            code: "let x = 0; 0, (1, x++);",
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 1, column: 19 }]
+        },
+        {
+            code: "let x = 0; foo = (x++, 0);",
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 1, column: 19 }]
+        },
+        {
+            code: "let x = 0; foo = ((0, x++), 0);",
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 1, column: 23 }]
+        },
+        {
+            code: "let x = 0; x += 1, 0;",
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 1, column: 12 }]
+        },
+        {
+            code: "let x = 0; 0, x += 1;",
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 1, column: 15 }]
+        },
+        {
+            code: "let x = 0; 0, (1, x += 1);",
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 1, column: 19 }]
+        },
+        {
+            code: "let x = 0; foo = (x += 1, 0);",
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 1, column: 19 }]
+        },
+        {
+            code: "let x = 0; foo = ((0, x += 1), 0);",
+            parserOptions: { ecmaVersion: 2015 },
+            errors: [{ ...assignedError("x"), line: 1, column: 23 }]
+        },
         {
             code: "(function ({ a, b }, { c } ) { return b; })();",
             parserOptions: { ecmaVersion: 2015 },
diff --git a/tests/lib/shared/string-utils.js b/tests/lib/shared/string-utils.js
new file mode 100644
index 000000000..bc48afaab
--- /dev/null
+++ b/tests/lib/shared/string-utils.js
@@ -0,0 +1,41 @@
+/**
+ * @fileoverview Tests for string utils.
+ * @author Stephen Wade
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const assert = require("chai").assert;
+
+const { upperCaseFirst } = require("../../../lib/shared/string-utils");
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+describe("upperCaseFirst", () => {
+    it("uppercases the first letter of a string", () => {
+        assert(upperCaseFirst("e") === "E");
+        assert(upperCaseFirst("alphabet") === "Alphabet");
+        assert(upperCaseFirst("one two three") === "One two three");
+    });
+
+    it("only changes the case of the first letter", () => {
+        assert(upperCaseFirst("alphaBet") === "AlphaBet");
+        assert(upperCaseFirst("one TWO three") === "One TWO three");
+    });
+
+    it("does not change the case if the first letter is already uppercase", () => {
+        assert(upperCaseFirst("E") === "E");
+        assert(upperCaseFirst("Alphabet") === "Alphabet");
+        assert(upperCaseFirst("One Two Three") === "One Two Three");
+    });
+
+    it("properly handles an empty string", () => {
+        assert(upperCaseFirst("") === "");
+    });
+});