From c0f418e2476df98519bc156b81d20431984e8704 Mon Sep 17 00:00:00 2001 From: Stephen Wade Date: Fri, 21 May 2021 16:24:19 -0400 Subject: [PATCH] Chore: Remove lodash (#14287) * Chore: Update table to ^6.0.9 * Chore: Remove lodash.last lodash.last(array) -> array[array.length - 1] * Chore: Remove lodash.get v = lodash.get(a, "b.c") -> if (a && a.b && a.b.c) v = a.b.c * Chore: Remove lodash.noop lodash.noop -> () => {} * Chore: Remove lodash.union https://exploringjs.com/impatient-js/ch_sets.html#union-a-b * Chore: Remove lodash.intersection https://exploringjs.com/impatient-js/ch_sets.html#intersection-a-b * Chore: Remove lodash.findLast lodash.findLast(array) -> [...array].reverse().find(_ =>_) * Chore: Remove lodash.overSome * Chore: Remove lodash.isPlainObject * Chore: Remove lodash.isString lodash.isString(str) -> typeof str === "string"; * Chore: Remove lodash.range * Chore: Remove lodash.sortedLastIndex https://www.30secondsofcode.org/js/s/sorted-last-index * Chore: Remove lodash.sortedIndexBy https://www.30secondsofcode.org/js/s/sorted-index-by * Chore: Remove lodash.sample https://www.30secondsofcode.org/js/s/sample * Chore: Remove lodash.flatMap * Chore: Remove lodash.flatten * Chore: Remove lodash.template * Chore: Remove lodash.escapeRegExp Add the escape-string-regexp package * Chore: Remove lodash.isEqual Add the fast-deep-equal package * Chore: Remove lodash.merge Add the deep-extend package * Chore: Remove lodash.cloneDeep Add the clone package * Chore: Remove lodash.omit Add the omit package * Chore: Remove lodash.upperFirst Add the upper-case-first package * Chore: Remove lodash.memoize Add the fast-memoize package * Chore: Remove lodash.mapValues Add the map-values package * Chore: Remove lodash.flatten * Chore: Remove lodash * Chore: Replace arrays.flat() * Chore: Replace clone with rfdc * Chore: Add comment about map-values * Chore: Remove omit dependency * Chore: Remove rfdc dependency * Chore: Remove upper-case-first dependency * Chore: Remove fast-memoize dependency * Chore: Apply suggestions in lib/linter/node-event-generator.js Co-authored-by: Milos Djermanovic * Chore: Add tests for upperCaseFirst * Chore: Remove map-values dependency * Chore: Apply review suggestions * Chore: Upgrade deep-extend to ^0.6.0 * Chore: Replace deep-extend dependency with lodash.merge * Chore: Apply review suggestion * Chore: Simplify search code * Chore: Apply review suggestions Co-authored-by: Milos Djermanovic --- bin/eslint.js | 14 +----- docs/developer-guide/code-path-analysis.md | 12 ++--- .../code-path-analysis/README.md | 12 ++--- lib/cli-engine/file-enumerator.js | 2 +- .../formatters/html-template-message.html | 8 --- .../formatters/html-template-message.js | 25 ++++++++++ ...mplate-page.html => html-template-page.js} | 14 ++++-- .../formatters/html-template-result.html | 6 --- .../formatters/html-template-result.js | 14 ++++++ lib/cli-engine/formatters/html.js | 36 +++++++++----- lib/init/autoconfig.js | 4 +- lib/linter/apply-disable-directives.js | 18 +++++-- lib/linter/linter.js | 10 ++-- lib/linter/node-event-generator.js | 49 ++++++++++++++++--- lib/rule-tester/rule-tester.js | 24 +++++---- lib/rules/comma-dangle.js | 23 ++++++--- lib/rules/complexity.js | 5 +- lib/rules/consistent-return.js | 4 +- lib/rules/eol-last.js | 9 +--- lib/rules/indent.js | 17 +++---- lib/rules/max-lines-per-function.js | 5 +- lib/rules/max-lines.js | 39 ++++++++++++--- lib/rules/max-params.js | 5 +- lib/rules/max-statements.js | 5 +- lib/rules/no-fallthrough.js | 10 +--- lib/rules/no-useless-backreference.js | 3 +- lib/rules/no-useless-computed-key.js | 10 +++- lib/rules/no-warning-comments.js | 2 +- lib/rules/object-curly-newline.js | 23 +++++++-- lib/rules/spaced-comment.js | 4 +- lib/rules/utils/ast-utils.js | 4 +- lib/shared/deprecation-warnings.js | 15 ++++-- lib/shared/string-utils.js | 22 +++++++++ lib/source-code/source-code.js | 11 +++-- lib/source-code/token-store/utils.js | 16 ++---- ...files-ignored.txt => all-files-ignored.js} | 12 ++++- messages/extend-config-missing.js | 13 +++++ messages/extend-config-missing.txt | 5 -- messages/failed-to-read-json.js | 11 +++++ messages/failed-to-read-json.txt | 3 -- messages/file-not-found.js | 10 ++++ messages/file-not-found.txt | 2 - ...no-config-found.txt => no-config-found.js} | 10 +++- messages/plugin-conflict.js | 22 +++++++++ messages/plugin-conflict.txt | 7 --- messages/plugin-invalid.js | 16 ++++++ messages/plugin-invalid.txt | 8 --- messages/plugin-missing.js | 19 +++++++ messages/plugin-missing.txt | 11 ----- ...xt => print-config-with-directory-path.js} | 6 +++ messages/whitespace-found.js | 11 +++++ messages/whitespace-found.txt | 3 -- package.json | 7 +-- tests/lib/rule-tester/rule-tester.js | 12 ++++- tests/lib/rules/no-invalid-this.js | 10 ++-- tests/lib/shared/string-utils.js | 41 ++++++++++++++++ tools/eslint-fuzzer.js | 24 +++++++-- webpack.config.js | 3 +- 58 files changed, 526 insertions(+), 220 deletions(-) delete mode 100644 lib/cli-engine/formatters/html-template-message.html create mode 100644 lib/cli-engine/formatters/html-template-message.js rename lib/cli-engine/formatters/{html-template-page.html => html-template-page.js} (92%) delete mode 100644 lib/cli-engine/formatters/html-template-result.html create mode 100644 lib/cli-engine/formatters/html-template-result.js create mode 100644 lib/shared/string-utils.js rename messages/{all-files-ignored.txt => all-files-ignored.js} (51%) create mode 100644 messages/extend-config-missing.js delete mode 100644 messages/extend-config-missing.txt create mode 100644 messages/failed-to-read-json.js delete mode 100644 messages/failed-to-read-json.txt create mode 100644 messages/file-not-found.js delete mode 100644 messages/file-not-found.txt rename messages/{no-config-found.txt => no-config-found.js} (52%) create mode 100644 messages/plugin-conflict.js delete mode 100644 messages/plugin-conflict.txt create mode 100644 messages/plugin-invalid.js delete mode 100644 messages/plugin-invalid.txt create mode 100644 messages/plugin-missing.js delete mode 100644 messages/plugin-missing.txt rename messages/{print-config-with-directory-path.txt => print-config-with-directory-path.js} (70%) create mode 100644 messages/whitespace-found.js delete mode 100644 messages/whitespace-found.txt create mode 100644 tests/lib/shared/string-utils.js diff --git a/bin/eslint.js b/bin/eslint.js index 86291b0f527..5fa5766828e 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 7762bc5d652..cd3fc29365a 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 c283d51bf91..1c84b2e1f73 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/lib/cli-engine/file-enumerator.js b/lib/cli-engine/file-enumerator.js index bd89ec7334c..ade28517b42 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 93795a1bdc8..00000000000 --- a/lib/cli-engine/formatters/html-template-message.html +++ /dev/null @@ -1,8 +0,0 @@ - - <%= lineNumber %>:<%= columnNumber %> - <%= severityName %> - <%- message %> - - <%= ruleId %> - - diff --git a/lib/cli-engine/formatters/html-template-message.js b/lib/cli-engine/formatters/html-template-message.js new file mode 100644 index 00000000000..c259618bf28 --- /dev/null +++ b/lib/cli-engine/formatters/html-template-message.js @@ -0,0 +1,25 @@ +"use strict"; + +module.exports = function(it, encodeHTML) { + const { + parentIndex, + lineNumber, + columnNumber, + severityNumber, + severityName, + message, + ruleUrl, + ruleId + } = it; + + return ` + + ${lineNumber}:${columnNumber} + ${severityName} + ${encodeHTML(message)} + + ${ruleId ? ruleId : ""} + + +`.trimLeft(); +}; diff --git a/lib/cli-engine/formatters/html-template-page.html b/lib/cli-engine/formatters/html-template-page.js similarity index 92% rename from lib/cli-engine/formatters/html-template-page.html rename to lib/cli-engine/formatters/html-template-page.js index 4016576fa06..e37a71e2d95 100644 --- a/lib/cli-engine/formatters/html-template-page.html +++ b/lib/cli-engine/formatters/html-template-page.js @@ -1,3 +1,9 @@ +"use strict"; + +module.exports = function(it) { + const { reportColor, reportSummary, date, results } = it; + + return ` @@ -88,15 +94,15 @@ -
+

ESLint Report

- <%= reportSummary %> - Generated on <%= date %> + ${reportSummary} - Generated on ${date}
- <%= results %> + ${results}
+`.trimLeft(); +}; 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 f4a55933c20..00000000000 --- a/lib/cli-engine/formatters/html-template-result.html +++ /dev/null @@ -1,6 +0,0 @@ - - - [+] <%- filePath %> - <%- summary %> - - diff --git a/lib/cli-engine/formatters/html-template-result.js b/lib/cli-engine/formatters/html-template-result.js new file mode 100644 index 00000000000..5048f72e928 --- /dev/null +++ b/lib/cli-engine/formatters/html-template-result.js @@ -0,0 +1,14 @@ +"use strict"; + +module.exports = function(it, encodeHTML) { + const { color, index, filePath, summary } = it; + + return ` + + + [+] ${encodeHTML(filePath)} + ${encodeHTML(summary)} + + +`.trimLeft(); +}; diff --git a/lib/cli-engine/formatters/html.js b/lib/cli-engine/formatters/html.js index 5d4b7e56060..b9739f05e2d 100644 --- a/lib/cli-engine/formatters/html.js +++ b/lib/cli-engine/formatters/html.js @@ -4,17 +4,30 @@ */ "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 = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'" + }; + const matchHTML = /[&<>"']/ug; + + return function(code) { + return code + ? code.toString().replace(matchHTML, m => encodeHTMLRules[m] || m) + : ""; + }; +}()); + +const pageTemplate = require("./html-template-page.js"); +const messageTemplate = require("./html-template-message.js"); +const resultTemplate = require("./html-template-result.js"); /** * Given a word and a count, append an s if count is not one. @@ -80,7 +93,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({ @@ -92,7 +107,7 @@ function renderMessages(messages, parentIndex, rulesMeta) { message: message.message, ruleId: message.ruleId, ruleUrl - }); + }, encodeHTML); }).join("\n"); } @@ -108,8 +123,7 @@ 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"); + }, encodeHTML) + renderMessages(result.messages, index, rulesMeta)).join("\n"); } //------------------------------------------------------------------------------ diff --git a/lib/init/autoconfig.js b/lib/init/autoconfig.js index 19d016a54c1..054c538496f 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 41d6934abba..0ba69ca9cc4 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 576816b5b7b..bdc6c1b1d01 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 0b4e50fc4b7..8b619fdff83 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/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 23b78fba279..b08303c62b2 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/lib/rules/comma-dangle.js b/lib/rules/comma-dangle.js index e22b7f3551e..798c111ec3b 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/complexity.js b/lib/rules/complexity.js index 5d62c6ff44b..116c8ad0a63 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 94db253d25b..a250430cb76 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 89c76acb202..24b0c9279c7 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 8f4079d31f9..b1af2a73b33 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 de70cecb47a..60e2e879f54 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 ceb014aff71..8bd5a1c95f4 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 4eebe2d95a3..8fb798401cb 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 437b393a508..65d5539550d 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 dd1f3ed9d9a..e8016e93e59 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-useless-backreference.js b/lib/rules/no-useless-backreference.js index 958e3d5dd4e..529c16439e3 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 e0505a318ef..a1cacc29612 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 0691a31f77e..e5f702bc7d7 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 9b64a1b5c6a..1fbea00c5d7 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 d3221f0ea79..226a2d44798 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 679eebb4c45..6b853001132 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 1438eaa69bf..1a0501ab057 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 00000000000..e4a55d79931 --- /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 6b20495b6fc..c13ce29b877 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 21e1d6ff7c3..a2bd77de71a 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 3f4c8ced4c9..d85828d36e7 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 00000000000..db8a5c64b9f --- /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 4defd7ac4d1..00000000000 --- 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 00000000000..5114de30980 --- /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 b5e2b861cfc..00000000000 --- 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 00000000000..26a5d57eff7 --- /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 639498eb5c6..00000000000 --- 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 b46a7e5a7a6..1042143f9fd 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 00000000000..c8c060e2f05 --- /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 3ab4b340ef2..00000000000 --- 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 00000000000..7913576f000 --- /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 3ee251821be..00000000000 --- 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 00000000000..f58c78ceb38 --- /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 aa25f59ac44..00000000000 --- 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 1afc9b1e88b..6a5d571dd37 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 00000000000..4ce49ca3a4e --- /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 3eed1af5866..00000000000 --- 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 51a76ec31c8..31d96fe28cf 100644 --- a/package.json +++ b/package.json @@ -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,7 +93,6 @@ "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", diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 3f2393621b6..3aaf6f91f3f 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 6e1b757a712..3b19d63696f 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/shared/string-utils.js b/tests/lib/shared/string-utils.js new file mode 100644 index 00000000000..bc48afaab4e --- /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("") === ""); + }); +}); diff --git a/tools/eslint-fuzzer.js b/tools/eslint-fuzzer.js index 4104b8516b2..c9705200eaf 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 b256f442460..a22c99bf861 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 + } } ] },