diff --git a/.eslintrc.js b/.eslintrc.js index 1457d9b553c..668bc2747c5 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/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6a57d0e6b65..f0d6c3e2ed9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,6 +10,11 @@ #### What is the purpose of this pull request? (put an "X" next to an item) + + [ ] Documentation update [ ] Bug fix ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/bug-report.md)) [ ] New rule ([template](https://raw.githubusercontent.com/eslint/eslint/master/templates/rule-proposal.md)) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3de5fc7cd29..6ba0bdc384b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node: [15.x, 14.x, 13.x, 12.x, 10.x, "10.12.0"] + node: [16.x, 15.x, 14.x, 13.x, 12.x, 10.x, "10.12.0"] include: - os: windows-latest node: "12.x" diff --git a/CHANGELOG.md b/CHANGELOG.md index aa02c0b4cff..6eaeb9a9a56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,57 @@ +v7.28.0 - June 4, 2021 + +* [`1237705`](https://github.com/eslint/eslint/commit/1237705dd08c209c5e3136045ec51a4ba87a3abe) Upgrade: @eslint/eslintrc to 0.4.2 (#14672) (Milos Djermanovic) +* [`123fb86`](https://github.com/eslint/eslint/commit/123fb8648731c2c23313c544ffa1872d3024fe68) Docs: Add Feedback Needed triage description (#14670) (Nicholas C. Zakas) +* [`c545163`](https://github.com/eslint/eslint/commit/c5451635b4e89827cfc8d8d77083647c74506e42) Update: support multiline /*eslint-env*/ directives (fixes #14652) (#14660) (薛定谔的猫) +* [`8d1e75a`](https://github.com/eslint/eslint/commit/8d1e75a31b3e3d67130709a219bdd07ce6f3cf74) Upgrade: glob-parent version in package.json (#14658) (Hamza Najeeb) +* [`1f048cb`](https://github.com/eslint/eslint/commit/1f048cb0eec660d2052f1758f4b2ad7b1cb424e1) Fix: no-implicit-coercion false positive with `String()` (fixes #14623) (#14641) (Milos Djermanovic) +* [`d709abf`](https://github.com/eslint/eslint/commit/d709abfdde087325d4578b6709dc61040b8ca9d8) Chore: fix comment location in no-unused-vars (#14648) (Milos Djermanovic) +* [`e44ce0a`](https://github.com/eslint/eslint/commit/e44ce0a8acfaad513c385150c25e76e82a1b8f12) Fix: no-duplicate-imports allow unmergeable (fixes #12758, fixes #12760) (#14238) (Soufiane Boutahlil) +* [`bb66a3d`](https://github.com/eslint/eslint/commit/bb66a3d91af426dac9a7ffdbe47bdbbc0ffd4dd7) New: add `getPhysicalFilename()` method to rule context (fixes #11989) (#14616) (Nitin Kumar) +* [`2e43dac`](https://github.com/eslint/eslint/commit/2e43dacd24337a82d4184fac9b44d497675f46ef) Docs: fix `no-sequences` example (#14643) (Nitin Kumar) +* [`958ff4e`](https://github.com/eslint/eslint/commit/958ff4e8a5102f204f1484d09985e28a79790996) Docs: add note for arrow functions in no-seq rule (#14578) (Nitin Kumar) +* [`e4f111b`](https://github.com/eslint/eslint/commit/e4f111b67d114adbf76a9c9dbb18fa4f49bc91b6) Fix: arrow-body-style crash with object pattern (fixes #14633) (#14635) (Milos Djermanovic) +* [`ec28b5a`](https://github.com/eslint/eslint/commit/ec28b5a2bdc69f34ce29d670f5e84d2446774a00) Chore: upgrade eslint-plugin-eslint-plugin (#14590) (薛定谔的猫) +* [`85a2725`](https://github.com/eslint/eslint/commit/85a2725b1fade5538e727102d9701ccb503e54d4) Docs: Update README team and sponsors (ESLint Jenkins) + +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) +* [`ae6dbd1`](https://github.com/eslint/eslint/commit/ae6dbd148aaca83e4bd04b9351b54029c50fac8a) Fix: track variables, not names in require-atomic-updates (fixes #14208) (#14282) (Patrick Ahmetovic) +* [`6a86e50`](https://github.com/eslint/eslint/commit/6a86e5018a3733049c09261bcabae422fbea893d) Chore: remove loose-parser tests (fixes #14315) (#14569) (Milos Djermanovic) +* [`ee3a3ea`](https://github.com/eslint/eslint/commit/ee3a3ead893d185cc4b1ae9041940cb0968767e1) Fix: create `.eslintrc.cjs` for `module` type (#14304) (Nitin Kumar) +* [`6791dec`](https://github.com/eslint/eslint/commit/6791decfc58b7b09cfd0aabd15a3d14148aae073) Docs: fix example for require-atomic-updates (#14562) (Milos Djermanovic) +* [`388eb7e`](https://github.com/eslint/eslint/commit/388eb7e14039b8951462b311d6121002ca5232cb) Sponsors: Sync README with website (ESLint Jenkins) +* [`f071d1e`](https://github.com/eslint/eslint/commit/f071d1ef91286bf2e3fb63d9b679ff7702819a1e) Update: Add automated suggestion to `radix` rule for parsing decimals (#14291) (Bryan Mishkin) +* [`0b6a3f3`](https://github.com/eslint/eslint/commit/0b6a3f31e6e78825114f82d4e0aed9cd72f784ac) New: Include XO style guide in `eslint --init` (#14193) (Federico Brigante) + +v7.25.0 - April 23, 2021 + +* [`5df5e4a`](https://github.com/eslint/eslint/commit/5df5e4a9976964fcf4dc67e241d4e22ec1370fe0) Update: highlight last write reference for no-unused-vars (fixes #14324) (#14335) (Nitin Kumar) +* [`0023872`](https://github.com/eslint/eslint/commit/00238729329b86b4f8af89ebfe278da3095a6075) Docs: Add deprecated note to `working-with-rules-deprecated` page (#14344) (Michael Novotny) +* [`36fca70`](https://github.com/eslint/eslint/commit/36fca70fa29ab65080076810de98e09133254b8a) Chore: Upgrade eslump to 3.0.0 (#14350) (Stephen Wade) +* [`59b689a`](https://github.com/eslint/eslint/commit/59b689a0b3fa658b8380431007cc1facb4617a3b) Chore: add node v16 (#14355) (薛定谔的猫) +* [`35a1f5e`](https://github.com/eslint/eslint/commit/35a1f5e967e4e87360d3e70d3fca0f7adeeaa1d7) Sponsors: Sync README with website (ESLint Jenkins) +* [`fb0a92b`](https://github.com/eslint/eslint/commit/fb0a92b3d2fed4a17bc39b7f02c540cd1175ec7d) Chore: rename misspelled identifier in test (#14346) (Tobias Nießen) +* [`f2babb1`](https://github.com/eslint/eslint/commit/f2babb1069194166e0ac1afd1269bbd06ac299b6) Docs: update pull request template (#14336) (Nitin Kumar) +* [`02dde29`](https://github.com/eslint/eslint/commit/02dde29eeb523ca24bc4ae7797d38627c3ba9fe9) Docs: Fix anchor in 'docs/developer-guide/working-with-rules.md' (#14332) (Nate-Wilkins) +* [`07d14c3`](https://github.com/eslint/eslint/commit/07d14c304c358fbc9c3d318e1377d2b2bda9179f) Chore: remove extraneous command from lint-staged config (#14314) (James George) +* [`41b3570`](https://github.com/eslint/eslint/commit/41b3570b6c014c534bb3208ed00050fd99842101) Update: lint code block with same extension but different content (#14227) (JounQin) +* [`eb29996`](https://github.com/eslint/eslint/commit/eb299966bdc3920dd2c6f9774d95103d242fc409) Docs: add more examples with arrow functions for no-sequences rule (#14313) (Nitin Kumar) + v7.24.0 - April 9, 2021 * [`0c346c8`](https://github.com/eslint/eslint/commit/0c346c87fa83c6d1184fdafb9c0748c2e15a423d) Chore: ignore `pnpm-lock.yaml` (#14303) (Nitin Kumar) diff --git a/README.md b/README.md index 14ba7e2cb32..2202900d194 100644 --- a/README.md +++ b/README.md @@ -223,11 +223,6 @@ Nicholas C. Zakas Brandon Mills - -
-Toru Nagashima -
-
Milos Djermanovic @@ -240,6 +235,11 @@ Milos Djermanovic The people who review and implement new features. - - - - - 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 4016576fa06..00000000000 --- a/lib/cli-engine/formatters/html-template-page.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - ESLint Report - - - -
-

ESLint Report

-
- <%= reportSummary %> - Generated on <%= date %> -
-
-
+ +
+Toru Nagashima +
+

薛定谔的猫 @@ -281,9 +281,9 @@ The following companies, organizations, and individuals support ESLint's ongoing

Platinum Sponsors

Automattic

Gold Sponsors

-

Nx (by Nrwl) Chrome's Web Framework & Tools Performance Fund Shopify Salesforce Airbnb Microsoft Substack

Silver Sponsors

+

Nx (by Nrwl) Chrome's Web Framework & Tools Performance Fund Salesforce Airbnb Coinbase Substack

Silver Sponsors

Retool Liftoff

Bronze Sponsors

-

Buy.Fineproxy.Org Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks

+

Anagram Solver Bugsnag Stability Monitoring Mixpanel VPS Server Icons8: free icons, photos, illustrations, and music Discord ThemeIsle Fire Stick Tricks Practice Ignition

## Technology Sponsors 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/docs/developer-guide/working-with-rules-deprecated.md b/docs/developer-guide/working-with-rules-deprecated.md index fdd4f9d3e14..5265cb50e79 100644 --- a/docs/developer-guide/working-with-rules-deprecated.md +++ b/docs/developer-guide/working-with-rules-deprecated.md @@ -1,4 +1,6 @@ -# Working with Rules +# Working with Rules (Deprecated) + +**Note:** This page covers the deprecated rule format for ESLint <= 2.13.1. [This is the most recent rule format](./working-with-rules.md). Each rule in ESLint has two files named with its identifier (for example, `no-extra-semi`). diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md index 2e699c088d3..6a019aff199 100644 --- a/docs/developer-guide/working-with-rules.md +++ b/docs/developer-guide/working-with-rules.md @@ -66,7 +66,7 @@ The source file for a rule exports an object with the following properties. In a custom rule or plugin, you can omit `docs` or include any properties that you need in it. -* `fixable` (string) is either `"code"` or `"whitespace"` if the `--fix` option on the [command line](../user-guide/command-line-interface.md#fix) automatically fixes problems reported by the rule +* `fixable` (string) is either `"code"` or `"whitespace"` if the `--fix` option on the [command line](../user-guide/command-line-interface.md#-fix) automatically fixes problems reported by the rule **Important:** the `fixable` property is mandatory for fixable rules. If this property isn't specified, ESLint will throw an error whenever the rule attempts to produce a fix. Omit the `fixable` property if the rule is not fixable. @@ -139,8 +139,9 @@ Additionally, the `context` object has the following methods: * If the node is an `ImportSpecifier`, `ImportDefaultSpecifier`, or `ImportNamespaceSpecifier`, the declared variable is returned. * Otherwise, if the node does not declare any variables, an empty array is returned. * `getFilename()` - returns the filename associated with the source. +* `getPhysicalFilename()` - when linting a file, it returns the full path of the file on disk without any code block information. When linting text, it returns the value passed to `—stdin-filename` or `` if not specified. * `getScope()` - returns the [scope](./scope-manager-interface.md#scope-interface) of the currently-traversed node. This information can be used to track references to variables. -* `getSourceCode()` - returns a [`SourceCode`](#context-getsourcecode) object that you can use to work with the source that was passed to ESLint. +* `getSourceCode()` - returns a [`SourceCode`](#contextgetsourcecode) object that you can use to work with the source that was passed to ESLint. * `markVariableAsUsed(name)` - marks a variable with the given name in the current scope as used. This affects the [no-unused-vars](../rules/no-unused-vars.md) rule. Returns `true` if a variable with the given name was found and marked as used, otherwise `false`. * `report(descriptor)` - reports a problem in the code (see the [dedicated section](#contextreport)). diff --git a/docs/maintainer-guide/issues.md b/docs/maintainer-guide/issues.md index 6273016bcdc..2624a4bda08 100644 --- a/docs/maintainer-guide/issues.md +++ b/docs/maintainer-guide/issues.md @@ -28,6 +28,7 @@ All of ESLint's issues, across all GitHub repositories, are managed on our [Tria * **Triaging** - issues that someone has reviewed but has not been able to fully triage yet * **Ready for Dev Team** - issues that have been triaged and have all of the information necessary for the dev team to take a look * **Evaluating** - the dev team is evaluating these issues to determine whether to move forward or not +* **Feedback Needed** - a team member is requesting more input from the rest of the team before proceeding * **Waiting for RFC** - the next step in the process is for an RFC to be written * **RFC Opened** - an RFC is opened to address these issues * **Blocked** - the issue can't move forward due to some dependency diff --git a/docs/rules/no-duplicate-imports.md b/docs/rules/no-duplicate-imports.md index 683f31584a1..437d2c3daa1 100644 --- a/docs/rules/no-duplicate-imports.md +++ b/docs/rules/no-duplicate-imports.md @@ -12,7 +12,7 @@ import { find } from 'module'; ## Rule Details -This rule requires that all imports from a single module exists in a single `import` statement. +This rule requires that all imports from a single module that can be merged exist in a single `import` statement. Example of **incorrect** code for this rule: @@ -33,6 +33,16 @@ import { merge, find } from 'module'; import something from 'another-module'; ``` +Example of **correct** code for this rule: + +```js +/*eslint no-duplicate-imports: "error"*/ + +// not mergeable +import { merge } from 'module'; +import * as something from 'module'; +``` + ## Options This rule takes one optional argument, an object with a single key, `includeExports` which is a `boolean`. It defaults to `false`. @@ -58,3 +68,17 @@ import { merge, find } from 'module'; export { find }; ``` + +Example of **correct** code for this rule with the `{ "includeExports": true }` option: + +```js +/*eslint no-duplicate-imports: ["error", { "includeExports": true }]*/ + +import { merge, find } from 'module'; + +// cannot be merged with the above import +export * as something from 'module'; + +// cannot be written differently +export * from 'module'; +``` diff --git a/docs/rules/no-restricted-imports.md b/docs/rules/no-restricted-imports.md index 0f6841340c9..f1577d7d89d 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/docs/rules/no-sequences.md b/docs/rules/no-sequences.md index d4efc1c5ba5..f12c46573b7 100644 --- a/docs/rules/no-sequences.md +++ b/docs/rules/no-sequences.md @@ -39,8 +39,6 @@ switch (val = foo(), val) {} while (val = foo(), val < 42); with (doSomething(), val) {} - -const foo = (val) => (console.log('bar'), val); ``` Examples of **correct** code for this rule: @@ -63,8 +61,32 @@ switch ((val = foo(), val)) {} while ((val = foo(), val < 42)); with ((doSomething(), val)) {} +``` + +### Note about arrow function bodies + +If an arrow function body is a statement rather than a block, and that statement contains a sequence, you need to use double parentheses around the statement to indicate that the sequence is intentional. + +Examples of **incorrect** code for arrow functions: + +```js +/*eslint no-sequences: "error"*/ +const foo = (val) => (console.log('bar'), val); + +const foo = () => ((bar = 123), 10); + +const foo = () => { return (bar = 123), 10 } +``` + +Examples of **correct** code for arrow functions: +```js +/*eslint no-sequences: "error"*/ const foo = (val) => ((console.log('bar'), val)); + +const foo = () => (((bar = 123), 10)); + +const foo = () => { return ((bar = 123), 10) } ``` ## Options diff --git a/docs/rules/require-atomic-updates.md b/docs/rules/require-atomic-updates.md index 593ccdd6073..8dd313815c8 100644 --- a/docs/rules/require-atomic-updates.md +++ b/docs/rules/require-atomic-updates.md @@ -84,7 +84,7 @@ async function foo() { } function* bar() { - result += yield; + result = (yield) + result; result = (yield somethingElse) + result; diff --git a/docs/user-guide/configuring/language-options.md b/docs/user-guide/configuring/language-options.md index eb3fe8a0afe..08b62aad571 100644 --- a/docs/user-guide/configuring/language-options.md +++ b/docs/user-guide/configuring/language-options.md @@ -187,7 +187,7 @@ For ES6 syntax, use `{ "parserOptions": { "ecmaVersion": 6 } }`; for new ES6 glo Parser options are set in your `.eslintrc.*` file by using the `parserOptions` property. The available options are: -* `ecmaVersion` - set to 3, 5 (default), 6, 7, 8, 9, 10, 11, or 12 to specify the version of ECMAScript syntax you want to use. You can also set to 2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), 2020 (same as 11), or 2021 (same as 12) to use the year-based naming. +* `ecmaVersion` - set to 3, 5 (default), 6, 7, 8, 9, 10, 11, or 12 to specify the version of ECMAScript syntax you want to use. You can also set to 2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), 2020 (same as 11), or 2021 (same as 12) to use the year-based naming. You can also set "latest" to use the most recently supported version. * `sourceType` - set to `"script"` (default) or `"module"` if your code is in ECMAScript modules. * `ecmaFeatures` - an object indicating which additional language features you'd like to use: * `globalReturn` - allow `return` statements in the global scope @@ -199,7 +199,7 @@ Here's an example `.eslintrc.json` file: ```json { "parserOptions": { - "ecmaVersion": 6, + "ecmaVersion": "latest", "sourceType": "module", "ecmaFeatures": { "jsx": true diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index b1befaa04fc..ca298f9c356 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -27,16 +27,11 @@ const { naming, CascadingConfigArrayFactory, IgnorePattern, - getUsedExtractedConfigs + getUsedExtractedConfigs, + ModuleResolver } } = require("@eslint/eslintrc"); -/* - * For some reason, ModuleResolver must be included via filepath instead of by - * API exports in order to work properly. That's why this is separated out onto - * its own require() statement. - */ -const ModuleResolver = require("@eslint/eslintrc/lib/shared/relative-module-resolver"); const { FileEnumerator } = require("./file-enumerator"); const { Linter } = require("../linter"); 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 @@ -
- - <%= results %> - -
- - - 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.js b/lib/cli-engine/formatters/html.js index 5d4b7e56060..baddb63079d 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 = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'" + }; + 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 ` + + + + + ESLint Report + + + +
+

ESLint Report

+
+ ${reportSummary} - Generated on ${date} +
+
+ + + ${results} + +
+ + + +`.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 ` + + ${lineNumber}:${columnNumber} + ${severityName} + ${encodeHTML(message)} + +
${ruleId ? ruleId : ""} + + +`.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 ` + + + [+] ${encodeHTML(filePath)} + ${encodeHTML(summary)} + + +`.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/eslint/eslint.js b/lib/eslint/eslint.js index ae2d2100861..c387ca72c3a 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -552,9 +552,12 @@ class ESLint { ...unknownOptions } = options || {}; - for (const key of Object.keys(unknownOptions)) { - throw new Error(`'options' must not include the unknown option '${key}'`); + const unknownOptionKeys = Object.keys(unknownOptions); + + if (unknownOptionKeys.length > 0) { + throw new Error(`'options' must not include the unknown option(s): ${unknownOptionKeys.join(", ")}`); } + if (filePath !== void 0 && !isNonEmptyString(filePath)) { throw new Error("'options.filePath' must be a non-empty string or undefined"); } 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/init/config-file.js b/lib/init/config-file.js index fc62b81525e..4c648ac0551 100644 --- a/lib/init/config-file.js +++ b/lib/init/config-file.js @@ -117,6 +117,7 @@ function writeJSConfigFile(config, filePath) { function write(config, filePath) { switch (path.extname(filePath)) { case ".js": + case ".cjs": writeJSConfigFile(config, filePath); break; diff --git a/lib/init/config-initializer.js b/lib/init/config-initializer.js index 6f62e7db87e..3c7f2ba0944 100644 --- a/lib/init/config-initializer.js +++ b/lib/init/config-initializer.js @@ -12,6 +12,7 @@ const util = require("util"), path = require("path"), + fs = require("fs"), enquirer = require("enquirer"), ProgressBar = require("progress"), semver = require("semver"), @@ -48,6 +49,16 @@ function writeFile(config, format) { extname = ".yml"; } else if (format === "JSON") { extname = ".json"; + } else if (format === "JavaScript") { + const pkgJSONPath = npmUtils.findPackageJson(); + + if (pkgJSONPath) { + const pkgJSONContents = JSON.parse(fs.readFileSync(pkgJSONPath, "utf8")); + + if (pkgJSONContents.type === "module") { + extname = ".cjs"; + } + } } const installedESLint = config.installedESLint; @@ -531,7 +542,8 @@ function promptUser() { choices: [ { message: "Airbnb: https://github.com/airbnb/javascript", name: "airbnb" }, { message: "Standard: https://github.com/standard/standard", name: "standard" }, - { message: "Google: https://github.com/google/eslint-config-google", name: "google" } + { message: "Google: https://github.com/google/eslint-config-google", name: "google" }, + { message: "XO: https://github.com/xojs/eslint-config-xo", name: "xo" } ], skip() { this.state.answers.packageJsonExists = npmUtils.checkPackageJson(); @@ -683,6 +695,7 @@ const init = { hasESLintVersionConflict, installModules, processAnswers, + writeFile, /* istanbul ignore next */initializeConfig() { return promptUser(); } diff --git a/lib/init/npm-utils.js b/lib/init/npm-utils.js index 555ea2b2b28..b91a824b126 100644 --- a/lib/init/npm-utils.js +++ b/lib/init/npm-utils.js @@ -50,8 +50,7 @@ function findPackageJson(startDir) { */ function installSyncSaveDev(packages) { const packageList = Array.isArray(packages) ? packages : [packages]; - const npmProcess = spawn.sync("npm", ["i", "--save-dev"].concat(packageList), - { stdio: "inherit" }); + const npmProcess = spawn.sync("npm", ["i", "--save-dev"].concat(packageList), { stdio: "inherit" }); const error = npmProcess.error; if (error && error.code === "ENOENT") { @@ -172,6 +171,7 @@ function checkPackageJson(startDir) { module.exports = { installSyncSaveDev, fetchPeerDependencies, + findPackageJson, checkDeps, checkDevDeps, checkPackageJson 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..77682ea9548 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"), @@ -37,6 +37,7 @@ const const debug = require("debug")("eslint:linter"); const MAX_AUTOFIX_PASSES = 10; const DEFAULT_PARSER_NAME = "espree"; +const DEFAULT_ECMA_VERSION = 5; const commentParser = new ConfigCommentParser(); const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }; @@ -432,10 +433,20 @@ function getDirectiveComments(filename, ast, ruleMapper, warnInlineConfig) { /** * Normalize ECMAScript version from the initial config - * @param {number} ecmaVersion ECMAScript version from the initial config + * @param {Parser} parser The parser which uses this options. + * @param {number} ecmaVersion ECMAScript version from the initial config * @returns {number} normalized ECMAScript version */ -function normalizeEcmaVersion(ecmaVersion) { +function normalizeEcmaVersion(parser, ecmaVersion) { + if (parser === espree) { + if (ecmaVersion === void 0) { + return DEFAULT_ECMA_VERSION; + } + + if (ecmaVersion === "latest") { + return espree.latestEcmaVersion; + } + } /* * Calculate ECMAScript edition number from official year version starting with @@ -444,7 +455,7 @@ function normalizeEcmaVersion(ecmaVersion) { return ecmaVersion >= 2015 ? ecmaVersion - 2009 : ecmaVersion; } -const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//gu; +const eslintEnvPattern = /\/\*\s*eslint-env\s(.+?)\*\//gsu; /** * Checks whether or not there is a comment which has "eslint-env *" in a given text. @@ -521,16 +532,17 @@ function normalizeVerifyOptions(providedOptions, config) { /** * Combines the provided parserOptions with the options from environments - * @param {string} parserName The parser name which uses this options. + * @param {Parser} parser The parser which uses this options. * @param {ParserOptions} providedOptions The provided 'parserOptions' key in a config * @param {Environment[]} enabledEnvironments The environments enabled in configuration and with inline comments * @returns {ParserOptions} Resulting parser options after merge */ -function resolveParserOptions(parserName, providedOptions, enabledEnvironments) { +function resolveParserOptions(parser, 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) { @@ -542,12 +554,7 @@ function resolveParserOptions(parserName, providedOptions, enabledEnvironments) mergedParserOptions.ecmaFeatures = Object.assign({}, mergedParserOptions.ecmaFeatures, { globalReturn: false }); } - /* - * TODO: @aladdin-add - * 1. for a 3rd-party parser, do not normalize parserOptions - * 2. for espree, no need to do this (espree will do it) - */ - mergedParserOptions.ecmaVersion = normalizeEcmaVersion(mergedParserOptions.ecmaVersion); + mergedParserOptions.ecmaVersion = normalizeEcmaVersion(parser, mergedParserOptions.ecmaVersion); return mergedParserOptions; } @@ -606,7 +613,7 @@ function getRuleOptions(ruleConfig) { */ function analyzeScope(ast, parserOptions, visitorKeys) { const ecmaFeatures = parserOptions.ecmaFeatures || {}; - const ecmaVersion = parserOptions.ecmaVersion || 5; + const ecmaVersion = parserOptions.ecmaVersion || DEFAULT_ECMA_VERSION; return eslintScope.analyze(ast, { ignoreEval: true, @@ -828,9 +835,10 @@ const BASE_TRAVERSAL_CONTEXT = Object.freeze( * @param {string} filename The reported filename of the code * @param {boolean} disableFixes If true, it doesn't make `fix` properties. * @param {string | undefined} cwd cwd of the cli + * @param {string} physicalFilename The full path of the file on disk without any code block information * @returns {Problem[]} An array of reported problems */ -function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd) { +function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename, disableFixes, cwd, physicalFilename) { const emitter = createEmitter(); const nodeQueue = []; let currentNode = sourceCode.ast; @@ -859,6 +867,7 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parser getDeclaredVariables: sourceCode.scopeManager.getDeclaredVariables.bind(sourceCode.scopeManager), getCwd: () => cwd, getFilename: () => filename, + getPhysicalFilename: () => physicalFilename || filename, getScope: () => getScope(sourceCode.scopeManager, currentNode), getSourceCode: () => sourceCode, markVariableAsUsed: name => markVariableAsUsed(sourceCode.scopeManager, currentNode, parserOptions, name), @@ -1121,7 +1130,7 @@ class Linter { .map(envName => getEnv(slots, envName)) .filter(env => env); - const parserOptions = resolveParserOptions(parserName, config.parserOptions || {}, enabledEnvs); + const parserOptions = resolveParserOptions(parser, config.parserOptions || {}, enabledEnvs); const configuredGlobals = resolveGlobals(config.globals || {}, enabledEnvs); const settings = config.settings || {}; @@ -1181,7 +1190,8 @@ class Linter { settings, options.filename, options.disableFixes, - slots.cwd + slots.cwd, + providedOptions.physicalFilename ); } catch (err) { err.message += `\nOccurred while linting ${options.filename}`; @@ -1284,9 +1294,12 @@ class Linter { _verifyWithProcessor(textOrSourceCode, config, options, configForRecursive) { const filename = options.filename || ""; const filenameToExpose = normalizeFilename(filename); + const physicalFilename = options.physicalFilename || filenameToExpose; 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")); @@ -1314,7 +1327,7 @@ class Linter { return this._verifyWithConfigArray( blockText, configForRecursive, - { ...options, filename: blockName } + { ...options, filename: blockName, physicalFilename } ); } @@ -1322,7 +1335,7 @@ class Linter { return this._verifyWithoutProcessors( blockText, config, - { ...options, filename: blockName } + { ...options, filename: blockName, physicalFilename } ); }); 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..f177ae3cc17 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."); } } @@ -606,7 +610,8 @@ class RuleTester { const messages = result.messages; assert.strictEqual(messages.length, 0, util.format("Should have no errors but had %d: %s", - messages.length, util.inspect(messages))); + messages.length, + util.inspect(messages))); assertASTDidntChange(result.beforeAST, result.afterAST); } @@ -661,13 +666,18 @@ class RuleTester { } assert.strictEqual(messages.length, item.errors, util.format("Should have %d error%s but had %d: %s", - item.errors, item.errors === 1 ? "" : "s", messages.length, util.inspect(messages))); + item.errors, + item.errors === 1 ? "" : "s", + messages.length, + util.inspect(messages))); } else { assert.strictEqual( - messages.length, item.errors.length, - util.format( + messages.length, item.errors.length, util.format( "Should have %d error%s but had %d: %s", - item.errors.length, item.errors.length === 1 ? "" : "s", messages.length, util.inspect(messages) + item.errors.length, + item.errors.length === 1 ? "" : "s", + messages.length, + util.inspect(messages) ) ); diff --git a/lib/rules/arrow-body-style.js b/lib/rules/arrow-body-style.js index b2167fde77b..5b8a5f01167 100644 --- a/lib/rules/arrow-body-style.js +++ b/lib/rules/arrow-body-style.js @@ -87,17 +87,17 @@ module.exports = { } /** - * Gets the closing parenthesis which is the pair of the given opening parenthesis. - * @param {Token} token The opening parenthesis token to get. + * Gets the closing parenthesis by the given node. + * @param {ASTNode} node first node after an opening parenthesis. * @returns {Token} The found closing parenthesis token. */ - function findClosingParen(token) { - let node = sourceCode.getNodeByRangeIndex(token.range[0]); + function findClosingParen(node) { + let nodeToCheck = node; - while (!astUtils.isParenthesised(sourceCode, node)) { - node = node.parent; + while (!astUtils.isParenthesised(sourceCode, nodeToCheck)) { + nodeToCheck = nodeToCheck.parent; } - return sourceCode.getTokenAfter(node); + return sourceCode.getTokenAfter(nodeToCheck); } /** @@ -226,12 +226,22 @@ module.exports = { const arrowToken = sourceCode.getTokenBefore(arrowBody, astUtils.isArrowToken); const [firstTokenAfterArrow, secondTokenAfterArrow] = sourceCode.getTokensAfter(arrowToken, { count: 2 }); const lastToken = sourceCode.getLastToken(node); - const isParenthesisedObjectLiteral = + + let parenthesisedObjectLiteral = null; + + if ( astUtils.isOpeningParenToken(firstTokenAfterArrow) && - astUtils.isOpeningBraceToken(secondTokenAfterArrow); + astUtils.isOpeningBraceToken(secondTokenAfterArrow) + ) { + const braceNode = sourceCode.getNodeByRangeIndex(secondTokenAfterArrow.range[0]); + + if (braceNode.type === "ObjectExpression") { + parenthesisedObjectLiteral = braceNode; + } + } // If the value is object literal, remove parentheses which were forced by syntax. - if (isParenthesisedObjectLiteral) { + if (parenthesisedObjectLiteral) { const openingParenToken = firstTokenAfterArrow; const openingBraceToken = secondTokenAfterArrow; @@ -247,7 +257,7 @@ module.exports = { } // Closing paren for the object doesn't have to be lastToken, e.g.: () => ({}).foo() - fixes.push(fixer.remove(findClosingParen(openingBraceToken))); + fixes.push(fixer.remove(findClosingParen(parenthesisedObjectLiteral))); fixes.push(fixer.insertTextAfter(lastToken, "}")); } else { 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/comma-spacing.js b/lib/rules/comma-spacing.js index 73c10a7711b..2bf41a00bb6 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/comma-style.js b/lib/rules/comma-style.js index f1a23d63b78..fbdecccaac2 100644 --- a/lib/rules/comma-style.js +++ b/lib/rules/comma-style.js @@ -207,8 +207,7 @@ module.exports = { * they are always valid regardless of an undefined item. */ if (astUtils.isCommaToken(commaToken)) { - validateCommaItemSpacing(previousItemToken, commaToken, - currentItemToken, reportItem); + validateCommaItemSpacing(previousItemToken, commaToken, currentItemToken, reportItem); } if (item) { 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..04f41db9e26 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); }, @@ -1177,8 +1177,7 @@ module.exports = { offsets.setDesiredOffset(questionMarkToken, firstToken, 1); offsets.setDesiredOffset(colonToken, firstToken, 1); - offsets.setDesiredOffset(firstConsequentToken, firstToken, - firstConsequentToken.type === "Punctuator" && + offsets.setDesiredOffset(firstConsequentToken, firstToken, firstConsequentToken.type === "Punctuator" && options.offsetTernaryExpressions ? 2 : 1); /* @@ -1204,8 +1203,7 @@ module.exports = { * If `baz` were aligned with `bar` rather than being offset by 1 from `foo`, `baz` would end up * having no expected indentation. */ - offsets.setDesiredOffset(firstAlternateToken, firstToken, - firstAlternateToken.type === "Punctuator" && + offsets.setDesiredOffset(firstAlternateToken, firstToken, firstAlternateToken.type === "Punctuator" && options.offsetTernaryExpressions ? 2 : 1); } } @@ -1560,8 +1558,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 +1578,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-duplicate-imports.js b/lib/rules/no-duplicate-imports.js index 7218dc64add..cc3da1d5a68 100644 --- a/lib/rules/no-duplicate-imports.js +++ b/lib/rules/no-duplicate-imports.js @@ -4,92 +4,225 @@ */ "use strict"; +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const NAMED_TYPES = ["ImportSpecifier", "ExportSpecifier"]; +const NAMESPACE_TYPES = [ + "ImportNamespaceSpecifier", + "ExportNamespaceSpecifier" +]; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** - * Returns the name of the module imported or re-exported. + * Check if an import/export type belongs to (ImportSpecifier|ExportSpecifier) or (ImportNamespaceSpecifier|ExportNamespaceSpecifier). + * @param {string} importExportType An import/export type to check. + * @param {string} type Can be "named" or "namespace" + * @returns {boolean} True if import/export type belongs to (ImportSpecifier|ExportSpecifier) or (ImportNamespaceSpecifier|ExportNamespaceSpecifier) and false if it doesn't. + */ +function isImportExportSpecifier(importExportType, type) { + const arrayToCheck = type === "named" ? NAMED_TYPES : NAMESPACE_TYPES; + + return arrayToCheck.includes(importExportType); +} + +/** + * Return the type of (import|export). * @param {ASTNode} node A node to get. - * @returns {string} the name of the module, or empty string if no name. + * @returns {string} The type of the (import|export). */ -function getValue(node) { - if (node && node.source && node.source.value) { - return node.source.value.trim(); +function getImportExportType(node) { + if (node.specifiers && node.specifiers.length > 0) { + const nodeSpecifiers = node.specifiers; + const index = nodeSpecifiers.findIndex( + ({ type }) => + isImportExportSpecifier(type, "named") || + isImportExportSpecifier(type, "namespace") + ); + const i = index > -1 ? index : 0; + + return nodeSpecifiers[i].type; } + if (node.type === "ExportAllDeclaration") { + if (node.exported) { + return "ExportNamespaceSpecifier"; + } + return "ExportAll"; + } + return "SideEffectImport"; +} - return ""; +/** + * Returns a boolean indicates if two (import|export) can be merged + * @param {ASTNode} node1 A node to check. + * @param {ASTNode} node2 A node to check. + * @returns {boolean} True if two (import|export) can be merged, false if they can't. + */ +function isImportExportCanBeMerged(node1, node2) { + const importExportType1 = getImportExportType(node1); + const importExportType2 = getImportExportType(node2); + + if ( + (importExportType1 === "ExportAll" && + importExportType2 !== "ExportAll" && + importExportType2 !== "SideEffectImport") || + (importExportType1 !== "ExportAll" && + importExportType1 !== "SideEffectImport" && + importExportType2 === "ExportAll") + ) { + return false; + } + if ( + (isImportExportSpecifier(importExportType1, "namespace") && + isImportExportSpecifier(importExportType2, "named")) || + (isImportExportSpecifier(importExportType2, "namespace") && + isImportExportSpecifier(importExportType1, "named")) + ) { + return false; + } + return true; } /** - * Checks if the name of the import or export exists in the given array, and reports if so. - * @param {RuleContext} context The ESLint rule context object. - * @param {ASTNode} node A node to get. - * @param {string} value The name of the imported or exported module. - * @param {string[]} array The array containing other imports or exports in the file. - * @param {string} messageId A messageId to be reported after the name of the module - * - * @returns {void} No return value + * Returns a boolean if we should report (import|export). + * @param {ASTNode} node A node to be reported or not. + * @param {[ASTNode]} previousNodes An array contains previous nodes of the module imported or exported. + * @returns {boolean} True if the (import|export) should be reported. */ -function checkAndReport(context, node, value, array, messageId) { - if (array.indexOf(value) !== -1) { - context.report({ - node, - messageId, - data: { - module: value - } - }); +function shouldReportImportExport(node, previousNodes) { + let i = 0; + + while (i < previousNodes.length) { + if (isImportExportCanBeMerged(node, previousNodes[i])) { + return true; + } + i++; } + return false; } /** - * @callback nodeCallback - * @param {ASTNode} node A node to handle. + * Returns array contains only nodes with declarations types equal to type. + * @param {[{node: ASTNode, declarationType: string}]} nodes An array contains objects, each object contains a node and a declaration type. + * @param {string} type Declaration type. + * @returns {[ASTNode]} An array contains only nodes with declarations types equal to type. + */ +function getNodesByDeclarationType(nodes, type) { + return nodes + .filter(({ declarationType }) => declarationType === type) + .map(({ node }) => node); +} + +/** + * Returns the name of the module imported or re-exported. + * @param {ASTNode} node A node to get. + * @returns {string} The name of the module, or empty string if no name. */ +function getModule(node) { + if (node && node.source && node.source.value) { + return node.source.value.trim(); + } + return ""; +} /** - * Returns a function handling the imports of a given file + * Checks if the (import|export) can be merged with at least one import or one export, and reports if so. * @param {RuleContext} context The ESLint rule context object. + * @param {ASTNode} node A node to get. + * @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type. + * @param {string} declarationType A declaration type can be an import or export. * @param {boolean} includeExports Whether or not to check for exports in addition to imports. - * @param {string[]} importsInFile The array containing other imports in the file. - * @param {string[]} exportsInFile The array containing other exports in the file. - * - * @returns {nodeCallback} A function passed to ESLint to handle the statement. + * @returns {void} No return value. */ -function handleImports(context, includeExports, importsInFile, exportsInFile) { - return function(node) { - const value = getValue(node); +function checkAndReport( + context, + node, + modules, + declarationType, + includeExports +) { + const module = getModule(node); - if (value) { - checkAndReport(context, node, value, importsInFile, "import"); + if (modules.has(module)) { + const previousNodes = modules.get(module); + const messagesIds = []; + const importNodes = getNodesByDeclarationType(previousNodes, "import"); + let exportNodes; + if (includeExports) { + exportNodes = getNodesByDeclarationType(previousNodes, "export"); + } + if (declarationType === "import") { + if (shouldReportImportExport(node, importNodes)) { + messagesIds.push("import"); + } if (includeExports) { - checkAndReport(context, node, value, exportsInFile, "importAs"); + if (shouldReportImportExport(node, exportNodes)) { + messagesIds.push("importAs"); + } + } + } else if (declarationType === "export") { + if (shouldReportImportExport(node, exportNodes)) { + messagesIds.push("export"); + } + if (shouldReportImportExport(node, importNodes)) { + messagesIds.push("exportAs"); } - - importsInFile.push(value); } - }; + messagesIds.forEach(messageId => + context.report({ + node, + messageId, + data: { + module + } + })); + } } /** - * Returns a function handling the exports of a given file + * @callback nodeCallback + * @param {ASTNode} node A node to handle. + */ + +/** + * Returns a function handling the (imports|exports) of a given file * @param {RuleContext} context The ESLint rule context object. - * @param {string[]} importsInFile The array containing other imports in the file. - * @param {string[]} exportsInFile The array containing other exports in the file. - * + * @param {Map} modules A Map object contains as a key a module name and as value an array contains objects, each object contains a node and a declaration type. + * @param {string} declarationType A declaration type can be an import or export. + * @param {boolean} includeExports Whether or not to check for exports in addition to imports. * @returns {nodeCallback} A function passed to ESLint to handle the statement. */ -function handleExports(context, importsInFile, exportsInFile) { +function handleImportsExports( + context, + modules, + declarationType, + includeExports +) { return function(node) { - const value = getValue(node); + const module = getModule(node); + + if (module) { + checkAndReport( + context, + node, + modules, + declarationType, + includeExports + ); + const currentNode = { node, declarationType }; + let nodes = [currentNode]; - if (value) { - checkAndReport(context, node, value, exportsInFile, "export"); - checkAndReport(context, node, value, importsInFile, "exportAs"); + if (modules.has(module)) { + const previousNodes = modules.get(module); - exportsInFile.push(value); + nodes = [...previousNodes, currentNode]; + } + modules.set(module, nodes); } }; } @@ -105,16 +238,19 @@ module.exports = { url: "https://eslint.org/docs/rules/no-duplicate-imports" }, - schema: [{ - type: "object", - properties: { - includeExports: { - type: "boolean", - default: false - } - }, - additionalProperties: false - }], + schema: [ + { + type: "object", + properties: { + includeExports: { + type: "boolean", + default: false + } + }, + additionalProperties: false + } + ], + messages: { import: "'{{module}}' import is duplicated.", importAs: "'{{module}}' import is duplicated as export.", @@ -125,18 +261,30 @@ module.exports = { create(context) { const includeExports = (context.options[0] || {}).includeExports, - importsInFile = [], - exportsInFile = []; - + modules = new Map(); const handlers = { - ImportDeclaration: handleImports(context, includeExports, importsInFile, exportsInFile) + ImportDeclaration: handleImportsExports( + context, + modules, + "import", + includeExports + ) }; if (includeExports) { - handlers.ExportNamedDeclaration = handleExports(context, importsInFile, exportsInFile); - handlers.ExportAllDeclaration = handleExports(context, importsInFile, exportsInFile); + handlers.ExportNamedDeclaration = handleImportsExports( + context, + modules, + "export", + includeExports + ); + handlers.ExportAllDeclaration = handleImportsExports( + context, + modules, + "export", + includeExports + ); } - return handlers; } }; 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-implicit-coercion.js b/lib/rules/no-implicit-coercion.js index b9bb4548986..993b8d1f1c8 100644 --- a/lib/rules/no-implicit-coercion.js +++ b/lib/rules/no-implicit-coercion.js @@ -109,6 +109,20 @@ function getNonNumericOperand(node) { return null; } +/** + * Checks whether an expression evaluates to a string. + * @param {ASTNode} node node that represents the expression to check. + * @returns {boolean} Whether or not the expression evaluates to a string. + */ +function isStringType(node) { + return astUtils.isStringLiteral(node) || + ( + node.type === "CallExpression" && + node.callee.type === "Identifier" && + node.callee.name === "String" + ); +} + /** * Checks whether a node is an empty string literal or not. * @param {ASTNode} node The node to check. @@ -126,8 +140,8 @@ function isEmptyString(node) { */ function isConcatWithEmptyString(node) { return node.operator === "+" && ( - (isEmptyString(node.left) && !astUtils.isStringLiteral(node.right)) || - (isEmptyString(node.right) && !astUtils.isStringLiteral(node.left)) + (isEmptyString(node.left) && !isStringType(node.right)) || + (isEmptyString(node.right) && !isStringType(node.left)) ); } @@ -332,6 +346,11 @@ module.exports = { return; } + // if the expression is already a string, then this isn't a coercion + if (isStringType(node.expressions[0])) { + return; + } + const code = sourceCode.getText(node.expressions[0]); const recommendation = `String(${code})`; diff --git a/lib/rules/no-restricted-imports.js b/lib/rules/no-restricted-imports.js index c205dad8bdb..414164d29f7 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 32589099cf4..f04818f8e9d 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,23 +445,28 @@ module.exports = { function isReadForItself(ref, rhsNode) { const id = ref.identifier; const parent = id.parent; - const grandparent = parent.parent; return ref.isRead() && ( // self update. e.g. `a += 1`, `a++` - (// in RHS of an assignment for itself. e.g. `a = a + 1` - (( + ( + ( parent.type === "AssignmentExpression" && - grandparent.type === "ExpressionStatement" && - parent.left === id + parent.left === id && + isUnusedExpression(parent) ) || + ( + parent.type === "UpdateExpression" && + isUnusedExpression(parent) + ) + ) || + + // in RHS of an assignment for itself. e.g. `a = a + 1` ( - parent.type === "UpdateExpression" && - grandparent.type === "ExpressionStatement" - ) || rhsNode && - isInside(id, rhsNode) && - !isInsideOfStorableFunction(id, rhsNode))) + rhsNode && + isInside(id, rhsNode) && + !isInsideOfStorableFunction(id, rhsNode) + ) ); } @@ -624,10 +654,18 @@ module.exports = { // Report the first declaration. if (unusedVar.defs.length > 0) { + + // report last write reference, https://github.com/eslint/eslint/issues/14324 + const writeReferences = unusedVar.references.filter(ref => ref.isWrite() && ref.from.variableScope === unusedVar.scope.variableScope); + + let referenceToReport; + + if (writeReferences.length > 0) { + referenceToReport = writeReferences[writeReferences.length - 1]; + } + context.report({ - node: unusedVar.references.length ? unusedVar.references[ - unusedVar.references.length - 1 - ].identifier : unusedVar.identifiers[0], + node: referenceToReport ? referenceToReport.identifier : unusedVar.identifiers[0], messageId: "unusedVar", data: unusedVar.references.some(ref => ref.isWrite()) ? getAssignedMessageData(unusedVar) diff --git a/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/radix.js b/lib/rules/radix.js index e3225662388..d1b67e5e280 100644 --- a/lib/rules/radix.js +++ b/lib/rules/radix.js @@ -82,7 +82,8 @@ module.exports = { description: "enforce the consistent use of the radix argument when using `parseInt()`", category: "Best Practices", recommended: false, - url: "https://eslint.org/docs/rules/radix" + url: "https://eslint.org/docs/rules/radix", + suggestion: true }, schema: [ @@ -95,7 +96,8 @@ module.exports = { missingParameters: "Missing parameters.", redundantRadix: "Redundant radix parameter.", missingRadix: "Missing radix parameter.", - invalidRadix: "Invalid radix parameter, must be an integer between 2 and 36." + invalidRadix: "Invalid radix parameter, must be an integer between 2 and 36.", + addRadixParameter10: "Add radix parameter `10` for parsing decimal numbers." } }, @@ -123,7 +125,21 @@ module.exports = { if (mode === MODE_ALWAYS) { context.report({ node, - messageId: "missingRadix" + messageId: "missingRadix", + suggest: [ + { + messageId: "addRadixParameter10", + fix(fixer) { + const sourceCode = context.getSourceCode(); + const tokens = sourceCode.getTokens(node); + const lastToken = tokens[tokens.length - 1]; // Parenthesis. + const secondToLastToken = tokens[tokens.length - 2]; // May or may not be a comma. + const hasTrailingComma = secondToLastToken.type === "Punctuator" && secondToLastToken.value === ","; + + return fixer.insertTextBefore(lastToken, hasTrailingComma ? " 10," : ", 10"); + } + } + ] }); } break; diff --git a/lib/rules/require-atomic-updates.js b/lib/rules/require-atomic-updates.js index c552f1bd825..b3df907420c 100644 --- a/lib/rules/require-atomic-updates.js +++ b/lib/rules/require-atomic-updates.js @@ -13,6 +13,10 @@ */ function createReferenceMap(scope, outReferenceMap = new Map()) { for (const reference of scope.references) { + if (reference.resolved === null) { + continue; + } + outReferenceMap.set(reference.identifier, reference); } for (const childScope of scope.childScopes) { @@ -86,42 +90,42 @@ class SegmentInfo { * @returns {void} */ initialize(segment) { - const outdatedReadVariableNames = new Set(); - const freshReadVariableNames = new Set(); + const outdatedReadVariables = new Set(); + const freshReadVariables = new Set(); for (const prevSegment of segment.prevSegments) { const info = this.info.get(prevSegment); if (info) { - info.outdatedReadVariableNames.forEach(Set.prototype.add, outdatedReadVariableNames); - info.freshReadVariableNames.forEach(Set.prototype.add, freshReadVariableNames); + info.outdatedReadVariables.forEach(Set.prototype.add, outdatedReadVariables); + info.freshReadVariables.forEach(Set.prototype.add, freshReadVariables); } } - this.info.set(segment, { outdatedReadVariableNames, freshReadVariableNames }); + this.info.set(segment, { outdatedReadVariables, freshReadVariables }); } /** * Mark a given variable as read on given segments. * @param {PathSegment[]} segments The segments that it read the variable on. - * @param {string} variableName The variable name to be read. + * @param {Variable} variable The variable to be read. * @returns {void} */ - markAsRead(segments, variableName) { + markAsRead(segments, variable) { for (const segment of segments) { const info = this.info.get(segment); if (info) { - info.freshReadVariableNames.add(variableName); + info.freshReadVariables.add(variable); // If a variable is freshly read again, then it's no more out-dated. - info.outdatedReadVariableNames.delete(variableName); + info.outdatedReadVariables.delete(variable); } } } /** - * Move `freshReadVariableNames` to `outdatedReadVariableNames`. + * Move `freshReadVariables` to `outdatedReadVariables`. * @param {PathSegment[]} segments The segments to process. * @returns {void} */ @@ -130,8 +134,8 @@ class SegmentInfo { const info = this.info.get(segment); if (info) { - info.freshReadVariableNames.forEach(Set.prototype.add, info.outdatedReadVariableNames); - info.freshReadVariableNames.clear(); + info.freshReadVariables.forEach(Set.prototype.add, info.outdatedReadVariables); + info.freshReadVariables.clear(); } } } @@ -139,14 +143,14 @@ class SegmentInfo { /** * Check if a given variable is outdated on the current segments. * @param {PathSegment[]} segments The current segments. - * @param {string} variableName The variable name to check. + * @param {Variable} variable The variable to check. * @returns {boolean} `true` if the variable is outdated on the segments. */ - isOutdated(segments, variableName) { + isOutdated(segments, variable) { for (const segment of segments) { const info = this.info.get(segment); - if (info && info.outdatedReadVariableNames.has(variableName)) { + if (info && info.outdatedReadVariables.has(variable)) { return true; } } @@ -214,14 +218,13 @@ module.exports = { if (!reference) { return; } - const name = reference.identifier.name; const variable = reference.resolved; const writeExpr = getWriteExpr(reference); const isMemberAccess = reference.identifier.parent.type === "MemberExpression"; // Add a fresh read variable. if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) { - segmentInfo.markAsRead(codePath.currentSegments, name); + segmentInfo.markAsRead(codePath.currentSegments, variable); } /* @@ -245,7 +248,7 @@ module.exports = { /* * Verify assignments. - * If the reference exists in `outdatedReadVariableNames` list, report it. + * If the reference exists in `outdatedReadVariables` list, report it. */ ":expression:exit"(node) { const { codePath, referenceMap } = stack; @@ -267,9 +270,9 @@ module.exports = { assignmentReferences.delete(node); for (const reference of references) { - const name = reference.identifier.name; + const variable = reference.resolved; - if (segmentInfo.isOutdated(codePath.currentSegments, name)) { + if (segmentInfo.isOutdated(codePath.currentSegments, variable)) { context.report({ node: node.parent, messageId: "nonAtomicUpdate", 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 d11478d1d55..2ca7f2cba9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "7.24.0", + "version": "7.28.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { @@ -27,10 +27,7 @@ "pre-commit": "lint-staged" }, "lint-staged": { - "*.js": [ - "eslint --fix", - "git add" - ], + "*.js": "eslint --fix", "*.md": "markdownlint" }, "files": [ @@ -47,7 +44,7 @@ "bugs": "https://github.com/eslint/eslint/issues/", "dependencies": { "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.0", + "@eslint/eslintrc": "^0.4.2", "@humanwhocodes/config-array": "^0.5.0", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -55,15 +52,17 @@ "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", + "glob-parent": "^5.1.2", "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", @@ -72,7 +71,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", @@ -81,14 +80,13 @@ "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" }, "devDependencies": { "@babel/core": "^7.4.3", "@babel/preset-env": "^7.4.3", - "acorn": "^7.2.0", "babel-loader": "^8.0.5", "chai": "^4.0.1", "cheerio": "^0.22.0", @@ -96,15 +94,14 @@ "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", "eslint-release": "^2.0.0", - "eslump": "^2.0.0", + "eslump": "^3.0.0", "esprima": "^4.0.1", "fs-teardown": "^0.1.0", "glob": "^7.1.6", diff --git a/packages/eslint-config-eslint/default.yml b/packages/eslint-config-eslint/default.yml index 8a85313045c..d2b31d9ed77 100644 --- a/packages/eslint-config-eslint/default.yml +++ b/packages/eslint-config-eslint/default.yml @@ -29,6 +29,7 @@ rules: consistent-return: "error" curly: ["error", "all"] default-case: "error" + default-case-last: "error" default-param-last: "error" dot-location: ["error", "property"] dot-notation: ["error", { allowKeywords: true }] @@ -36,8 +37,10 @@ rules: eqeqeq: "error" func-call-spacing: "error" func-style: ["error", "declaration"] + function-call-argument-newline: ["error", "consistent"] function-paren-newline: ["error", "consistent"] generator-star-spacing: "error" + grouped-accessor-pairs: "error" guard-for-in: "error" jsdoc/check-alignment: "error" jsdoc/check-param-names: "error" @@ -79,6 +82,7 @@ rules: no-caller: "error" no-confusing-arrow: "error" no-console: "error" + no-constructor-return: "error" no-delete-var: "error" no-else-return: ["error", { allowElseIf: false }] no-eval: "error" @@ -133,6 +137,7 @@ rules: no-underscore-dangle: ["error", {allowAfterThis: true}] no-unmodified-loop-condition: "error" no-unneeded-ternary: "error" + no-unreachable-loop: "error" no-unused-expressions: "error" no-unused-vars: ["error", {vars: "all", args: "after-used", caughtErrors: "all"}] no-use-before-define: "error" @@ -174,6 +179,7 @@ rules: ] prefer-arrow-callback: "error" prefer-const: "error" + prefer-exponentiation-operator: "error" prefer-numeric-literals: "error" prefer-promise-reject-errors: "error" prefer-regex-literals: "error" diff --git a/tests/bench/large.js b/tests/bench/large.js index c94e88568d4..a3db1ac6c8d 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/fixtures/formatters/broken.js b/tests/fixtures/formatters/broken.js index c467ac7367f..263143645b4 100644 --- a/tests/fixtures/formatters/broken.js +++ b/tests/fixtures/formatters/broken.js @@ -1,5 +1,5 @@ /*global module*/ -var nonExistantFormatter = require('this-module-does-not-exist'); +var nonExistentFormatter = require('this-module-does-not-exist'); module.exports = function(results) { - return nonExistantFormatter(results); + return nonExistentFormatter(results); }; diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index c7783a6c059..0845d697083 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -846,7 +846,7 @@ describe("ESLint", () => { it("should throw if 'options' argument contains unknown key", async () => { eslint = new ESLint(); - await assert.rejects(() => eslint.lintText("var a = 0", { filename: "foo.js" }), /'options' must not include the unknown option 'filename'/u); + await assert.rejects(() => eslint.lintText("var a = 0", { filename: "foo.js" }), /'options' must not include the unknown option\(s\): filename/u); }); it("should throw if non-string value is given to 'options.filePath' option", async () => { diff --git a/tests/lib/init/config-initializer.js b/tests/lib/init/config-initializer.js index cb9162e2d5c..81e4e52faaa 100644 --- a/tests/lib/init/config-initializer.js +++ b/tests/lib/init/config-initializer.js @@ -27,6 +27,8 @@ const proxyquire = require("proxyquire").noPreserveCache(); //------------------------------------------------------------------------------ let answers = {}; +let pkgJSONContents = {}; +let pkgJSONPath = ""; describe("configInitializer", () => { @@ -240,6 +242,14 @@ describe("configInitializer", () => { assert.include(modules, "eslint-config-standard@latest"); }); + it("should support the xo style guide", () => { + const config = { extends: "xo" }; + const modules = init.getModulesList(config); + + assert.deepStrictEqual(config, { extends: "xo", installedESLint: true }); + assert.include(modules, "eslint-config-xo@latest"); + }); + it("should install required sharable config", () => { const config = { extends: "google" }; @@ -447,4 +457,121 @@ describe("configInitializer", () => { }); }); }); + + describe("writeFile()", () => { + + beforeEach(() => { + answers = { + purpose: "style", + source: "prompt", + extendDefault: true, + indent: 2, + quotes: "single", + linebreak: "unix", + semi: true, + moduleType: "esm", + es6Globals: true, + env: ["browser"], + format: "JSON" + }; + + pkgJSONContents = { + name: "config-initializer", + version: "1.0.0" + }; + + process.chdir(fixtureDir); + + pkgJSONPath = path.resolve(fixtureDir, "package.json"); + }); + + afterEach(() => { + process.chdir(originalDir); + }); + + it("should create .eslintrc.json", () => { + const config = init.processAnswers(answers); + const filePath = path.resolve(fixtureDir, ".eslintrc.json"); + + fs.writeFileSync(pkgJSONPath, JSON.stringify(pkgJSONContents)); + + init.writeFile(config, answers.format); + + assert.isTrue(fs.existsSync(filePath)); + + fs.unlinkSync(filePath); + fs.unlinkSync(pkgJSONPath); + }); + + it("should create .eslintrc.js", () => { + answers.format = "JavaScript"; + + const config = init.processAnswers(answers); + const filePath = path.resolve(fixtureDir, ".eslintrc.js"); + + fs.writeFileSync(pkgJSONPath, JSON.stringify(pkgJSONContents)); + + init.writeFile(config, answers.format); + + assert.isTrue(fs.existsSync(filePath)); + + fs.unlinkSync(filePath); + fs.unlinkSync(pkgJSONPath); + }); + + it("should create .eslintrc.yml", () => { + answers.format = "YAML"; + + const config = init.processAnswers(answers); + const filePath = path.resolve(fixtureDir, ".eslintrc.yml"); + + fs.writeFileSync(pkgJSONPath, JSON.stringify(pkgJSONContents)); + + init.writeFile(config, answers.format); + + assert.isTrue(fs.existsSync(filePath)); + + fs.unlinkSync(filePath); + fs.unlinkSync(pkgJSONPath); + }); + + // For https://github.com/eslint/eslint/issues/14137 + it("should create .eslintrc.cjs", () => { + answers.format = "JavaScript"; + + // create package.json with "type": "module" + pkgJSONContents.type = "module"; + + fs.writeFileSync(pkgJSONPath, JSON.stringify(pkgJSONContents)); + + const config = init.processAnswers(answers); + const filePath = path.resolve(fixtureDir, ".eslintrc.cjs"); + + init.writeFile(config, answers.format); + + assert.isTrue(fs.existsSync(filePath)); + + fs.unlinkSync(filePath); + fs.unlinkSync(pkgJSONPath); + }); + + it("should create .eslintrc.json even with type: 'module'", () => { + answers.format = "JSON"; + + // create package.json with "type": "module" + pkgJSONContents.type = "module"; + + fs.writeFileSync(pkgJSONPath, JSON.stringify(pkgJSONContents)); + + const config = init.processAnswers(answers); + const filePath = path.resolve(fixtureDir, ".eslintrc.json"); + + init.writeFile(config, answers.format); + + assert.isTrue(fs.existsSync(filePath)); + + fs.unlinkSync(filePath); + fs.unlinkSync(pkgJSONPath); + }); + }); }); diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 976bd765755..15d310c9ab9 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -11,6 +11,7 @@ const assert = require("chai").assert, sinon = require("sinon"), + espree = require("espree"), esprima = require("esprima"), testParsers = require("../../fixtures/parsers/linter-test-parsers"); @@ -1559,6 +1560,22 @@ describe("Linter", () => { assert.strictEqual(messages[0].message, filename); }); + it("has access to the physicalFilename", () => { + linter.defineRule(code, context => ({ + Literal(node) { + context.report(node, context.getPhysicalFilename()); + } + })); + + const config = { rules: {} }; + + config.rules[code] = 1; + + const messages = linter.verify("0", config, filename); + + assert.strictEqual(messages[0].message, filename); + }); + it("defaults filename to ''", () => { linter.defineRule(code, context => ({ Literal(node) { @@ -2957,6 +2974,23 @@ var a = "test2"; assert.strictEqual(messages.length, 0); }); + // https://github.com/eslint/eslint/issues/14652 + it("should not report a violation", () => { + const codes = [ + "/*eslint-env es6\n */ new Promise();", + "/*eslint-env browser,\nes6 */ window;Promise;", + "/*eslint-env\nbrowser,es6 */ window;Promise;" + ]; + const config = { rules: { "no-undef": 1 } }; + + for (const code of codes) { + const messages = linter.verify(code, config, filename); + + assert.strictEqual(messages.length, 0); + } + + }); + it("should not report a violation", () => { const code = `/*${ESLINT_ENV} mocha,node */ require();describe();`; @@ -3408,6 +3442,41 @@ var a = "test2"; }); }); + describe("physicalFilenames", () => { + it("should be same as `filename` passed on options object, if no processors are used", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), "foo.js"); + return {}; + }); + + linter.defineRule("checker", physicalFilenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }, { filename: "foo.js" }); + assert(physicalFilenameChecker.calledOnce); + }); + + it("should default physicalFilename to when options object doesn't have filename", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), ""); + return {}; + }); + + linter.defineRule("checker", physicalFilenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }, {}); + assert(physicalFilenameChecker.calledOnce); + }); + + it("should default physicalFilename to when only two arguments are passed", () => { + const physicalFilenameChecker = sinon.spy(context => { + assert.strictEqual(context.getPhysicalFilename(), ""); + return {}; + }); + + linter.defineRule("checker", physicalFilenameChecker); + linter.verify("foo;", { rules: { checker: "error" } }); + assert(physicalFilenameChecker.calledOnce); + }); + }); + it("should report warnings in order by line and column when called", () => { const code = "foo()\n alert('test')"; @@ -3425,6 +3494,70 @@ var a = "test2"; }); describe("ecmaVersion", () => { + + it("should not support ES6 when no ecmaVersion provided", () => { + const messages = linter.verify("let x = 0;"); + + assert.strictEqual(messages.length, 1); + }); + + it("the default ECMAScript version is 5", () => { + let ecmaVersion = null; + const config = { rules: { "ecma-version": 2 } }; + + linter.defineRule("ecma-version", context => ({ + Program() { + ecmaVersion = context.parserOptions.ecmaVersion; + } + })); + linter.verify("", config); + assert.strictEqual(ecmaVersion, 5); + }); + + it("supports ECMAScript version 'latest'", () => { + const messages = linter.verify("let x = 5 ** 7;", { + parserOptions: { ecmaVersion: "latest" } + }); + + assert.strictEqual(messages.length, 0); + }); + + it("the 'latest' is equal to espree.lastEcmaVersion", () => { + let ecmaVersion = null; + const config = { rules: { "ecma-version": 2 }, parserOptions: { ecmaVersion: "latest" } }; + + linter.defineRule("ecma-version", context => ({ + Program() { + ecmaVersion = context.parserOptions.ecmaVersion; + } + })); + linter.verify("", config); + assert.strictEqual(ecmaVersion, espree.latestEcmaVersion); + }); + + it("should pass normalized ecmaVersion to eslint-scope", () => { + let blockScope = null; + + linter.defineRule("block-scope", context => ({ + BlockStatement() { + blockScope = context.getScope(); + } + })); + + linter.verify("{}", { + rules: { "block-scope": 2 }, + parserOptions: { ecmaVersion: "latest" } + }); + + assert.strictEqual(blockScope.type, "block"); + + linter.verify("{}", { + rules: { "block-scope": 2 }, + parserOptions: {} // ecmaVersion defaults to 5 + }); + assert.strictEqual(blockScope.type, "global"); + }); + describe("it should properly parse let declaration when", () => { it("the ECMAScript version number is 6", () => { const messages = linter.verify("let x = 5;", { @@ -4783,14 +4916,17 @@ var a = "test2"; describe("processors", () => { let receivedFilenames = []; + let receivedPhysicalFilenames = []; beforeEach(() => { receivedFilenames = []; + receivedPhysicalFilenames = []; // A rule that always reports the AST with a message equal to the source text linter.defineRule("report-original-text", context => ({ Program(ast) { receivedFilenames.push(context.getFilename()); + receivedPhysicalFilenames.push(context.getPhysicalFilename()); context.report({ node: ast, message: context.getSourceCode().text }); } })); @@ -4845,10 +4981,16 @@ var a = "test2"; assert.strictEqual(problems.length, 3); assert.deepStrictEqual(problems.map(problem => problem.message), ["foo", "bar", "baz"]); + + // filename assert.strictEqual(receivedFilenames.length, 3); assert(/^filename\.js[/\\]0_block\.js/u.test(receivedFilenames[0])); assert(/^filename\.js[/\\]1_block\.js/u.test(receivedFilenames[1])); assert(/^filename\.js[/\\]2_block\.js/u.test(receivedFilenames[2])); + + // physical filename + assert.strictEqual(receivedPhysicalFilenames.length, 3); + assert.strictEqual(receivedPhysicalFilenames.every(name => name === filename), true); }); it("should receive text even if a SourceCode object was given.", () => { 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/arrow-body-style.js b/tests/lib/rules/arrow-body-style.js index 7a8de4fe5ef..0ccb440d392 100644 --- a/tests/lib/rules/arrow-body-style.js +++ b/tests/lib/rules/arrow-body-style.js @@ -810,6 +810,14 @@ ruleTester.run("arrow-body-style", rule, { `, options: ["always"], errors: [{ messageId: "expectedBlock" }] + }, + + // https://github.com/eslint/eslint/issues/14633 + { + code: "const createMarker = (color) => ({ latitude, longitude }, index) => {};", + output: "const createMarker = (color) => {return ({ latitude, longitude }, index) => {}};", + options: ["always"], + errors: [{ messageId: "expectedBlock" }] } ] }); diff --git a/tests/lib/rules/no-duplicate-imports.js b/tests/lib/rules/no-duplicate-imports.js index 42d35c85751..e200bbc704d 100644 --- a/tests/lib/rules/no-duplicate-imports.js +++ b/tests/lib/rules/no-duplicate-imports.js @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/no-duplicate-imports"), // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6, sourceType: "module" } }); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 12, sourceType: "module" } }); ruleTester.run("no-duplicate-imports", rule, { valid: [ @@ -26,6 +26,9 @@ ruleTester.run("no-duplicate-imports", rule, { "import * as Foobar from \"async\";", "import \"foo\"", "import os from \"os\";\nexport { something } from \"os\";", + "import * as bar from \"os\";\nimport { baz } from \"os\";", + "import foo, * as bar from \"os\";\nimport { baz } from \"os\";", + "import foo, { bar } from \"os\";\nimport * as baz from \"os\";", { code: "import os from \"os\";\nexport { hello } from \"hello\";", options: [{ includeExports: true }] @@ -45,6 +48,26 @@ ruleTester.run("no-duplicate-imports", rule, { { code: "import { merge } from \"lodash-es\";\nexport { merge as lodashMerge }", options: [{ includeExports: true }] + }, + { + code: "export { something } from \"os\";\nexport * as os from \"os\";", + options: [{ includeExports: true }] + }, + { + code: "import { something } from \"os\";\nexport * as os from \"os\";", + options: [{ includeExports: true }] + }, + { + code: "import * as os from \"os\";\nexport { something } from \"os\";", + options: [{ includeExports: true }] + }, + { + code: "import os from \"os\";\nexport * from \"os\";", + options: [{ includeExports: true }] + }, + { + code: "export { something } from \"os\";\nexport * from \"os\";", + options: [{ includeExports: true }] } ], invalid: [ @@ -53,11 +76,22 @@ ruleTester.run("no-duplicate-imports", rule, { errors: [{ messageId: "import", data: { module: "fs" }, type: "ImportDeclaration" }] }, { - code: "import { merge } from \"lodash-es\";import { find } from \"lodash-es\";", + code: "import { merge } from \"lodash-es\";\nimport { find } from \"lodash-es\";", errors: [{ messageId: "import", data: { module: "lodash-es" }, type: "ImportDeclaration" }] }, { - code: "import { merge } from \"lodash-es\";import _ from \"lodash-es\";", + code: "import { merge } from \"lodash-es\";\nimport _ from \"lodash-es\";", + errors: [{ messageId: "import", data: { module: "lodash-es" }, type: "ImportDeclaration" }] + }, + { + code: "import os from \"os\";\nimport { something } from \"os\";\nimport * as foobar from \"os\";", + errors: [ + { messageId: "import", data: { module: "os" }, type: "ImportDeclaration" }, + { messageId: "import", data: { module: "os" }, type: "ImportDeclaration" } + ] + }, + { + code: "import * as modns from \"lodash-es\";\nimport { merge } from \"lodash-es\";\nimport { baz } from \"lodash-es\";", errors: [{ messageId: "import", data: { module: "lodash-es" }, type: "ImportDeclaration" }] }, { @@ -66,7 +100,7 @@ ruleTester.run("no-duplicate-imports", rule, { errors: [{ messageId: "export", data: { module: "os" }, type: "ExportNamedDeclaration" }] }, { - code: "import os from \"os\"; export { os as foobar } from \"os\";\nexport { something } from \"os\";", + code: "import os from \"os\";\nexport { os as foobar } from \"os\";\nexport { something } from \"os\";", options: [{ includeExports: true }], errors: [ { messageId: "exportAs", data: { module: "os" }, type: "ExportNamedDeclaration" }, @@ -80,7 +114,27 @@ ruleTester.run("no-duplicate-imports", rule, { errors: [{ messageId: "exportAs", data: { module: "os" }, type: "ExportNamedDeclaration" }] }, { - code: "import os from \"os\";\nexport * from \"os\";", + code: "import os from \"os\";\nexport * as os from \"os\";", + options: [{ includeExports: true }], + errors: [{ messageId: "exportAs", data: { module: "os" }, type: "ExportAllDeclaration" }] + }, + { + code: "export * as os from \"os\";\nimport os from \"os\";", + options: [{ includeExports: true }], + errors: [{ messageId: "importAs", data: { module: "os" }, type: "ImportDeclaration" }] + }, + { + code: "import * as modns from \"mod\";\nexport * as modns from \"mod\";", + options: [{ includeExports: true }], + errors: [{ messageId: "exportAs", data: { module: "mod" }, type: "ExportAllDeclaration" }] + }, + { + code: "export * from \"os\";\nexport * from \"os\";", + options: [{ includeExports: true }], + errors: [{ messageId: "export", data: { module: "os" }, type: "ExportAllDeclaration" }] + }, + { + code: "import \"os\";\nexport * from \"os\";", options: [{ includeExports: true }], errors: [{ messageId: "exportAs", data: { module: "os" }, type: "ExportAllDeclaration" }] } diff --git a/tests/lib/rules/no-implicit-coercion.js b/tests/lib/rules/no-implicit-coercion.js index fa0a63824bb..f7ca9dcff3f 100644 --- a/tests/lib/rules/no-implicit-coercion.js +++ b/tests/lib/rules/no-implicit-coercion.js @@ -95,7 +95,16 @@ ruleTester.run("no-implicit-coercion", rule, { { code: "`${foo}`", parserOptions: { ecmaVersion: 6 } }, { code: "`${foo}`", options: [{ }], parserOptions: { ecmaVersion: 6 } }, { code: "`${foo}`", options: [{ disallowTemplateShorthand: false }], parserOptions: { ecmaVersion: 6 } }, - "+42" + "+42", + + // https://github.com/eslint/eslint/issues/14623 + "'' + String(foo)", + "String(foo) + ''", + { code: "`` + String(foo)", parserOptions: { ecmaVersion: 6 } }, + { code: "String(foo) + ``", parserOptions: { ecmaVersion: 6 } }, + { code: "`${'foo'}`", options: [{ disallowTemplateShorthand: true }], parserOptions: { ecmaVersion: 6 } }, + { code: "`${`foo`}`", options: [{ disallowTemplateShorthand: true }], parserOptions: { ecmaVersion: 6 } }, + { code: "`${String(foo)}`", options: [{ disallowTemplateShorthand: true }], parserOptions: { ecmaVersion: 6 } } ], invalid: [ { 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/rules/no-redeclare.js b/tests/lib/rules/no-redeclare.js index f6293b7e152..f89d6853fa3 100644 --- a/tests/lib/rules/no-redeclare.js +++ b/tests/lib/rules/no-redeclare.js @@ -9,7 +9,6 @@ // Requirements //------------------------------------------------------------------------------ -const path = require("path"); const rule = require("../../../lib/rules/no-redeclare"); const { RuleTester } = require("../../../lib/rule-tester"); @@ -17,7 +16,6 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const looseParserPath = path.resolve(__dirname, "../../tools/loose-parser.js"); const ruleTester = new RuleTester(); ruleTester.run("no-redeclare", rule, { @@ -205,112 +203,6 @@ ruleTester.run("no-redeclare", rule, { ] }, - // let/const - { - code: "let a; let a;", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "let a; let a;", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015, sourceType: "module" }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "let a; let a;", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015, ecmaFeatures: { globalReturn: true } }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "let a; const a = 0;", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "const a = 0; const a = 0;", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "if (test) { let a; let a; }", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "switch (test) { case 0: let a; let a; }", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "for (let a, a;;);", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "for (let [a, a] in xs);", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "for (let [a, a] of xs);", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "function f() { let a; let a; }", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "function f(a) { let a; }", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - { - code: "function f() { if (test) { let a; let a; } }", - parser: looseParserPath, - parserOptions: { ecmaVersion: 2015 }, - errors: [ - { message: "'a' is already defined.", type: "Identifier" } - ] - }, - // Comments and built-ins. { code: "/*globals Array */", diff --git a/tests/lib/rules/no-restricted-imports.js b/tests/lib/rules/no-restricted-imports.js index 363229712c9..f54d4df0354 100644 --- a/tests/lib/rules/no-restricted-imports.js +++ b/tests/lib/rules/no-restricted-imports.js @@ -37,6 +37,10 @@ ruleTester.run("no-restricted-imports", rule, { code: "import withGitignores from \"foo/bar\";", options: [{ patterns: ["foo/*", "!foo/bar"] }] }, + { + code: "import withPatterns from \"foo/bar\";", + options: [{ patterns: [{ group: ["foo/*", "!foo/bar"], message: "foo is forbidden, use bar instead" }] }] + }, { code: "import AllowedObject from \"foo\";", options: [{ @@ -241,6 +245,36 @@ ruleTester.run("no-restricted-imports", rule, { column: 1, endColumn: 36 }] + }, { + code: "import withPatterns from \"foo/baz\";", + options: [{ patterns: [{ group: ["foo/*", "!foo/bar"], message: "foo is forbidden, use foo/bar instead" }] }], + errors: [{ + message: "'foo/baz' import is restricted from being used by a pattern. foo is forbidden, use foo/bar instead", + type: "ImportDeclaration", + line: 1, + column: 1, + endColumn: 36 + }] + }, { + code: "import withPatterns from \"foo/baz\";", + options: [{ patterns: [{ group: ["foo/bar", "foo/baz"], message: "some foo subimports are restricted" }] }], + errors: [{ + message: "'foo/baz' import is restricted from being used by a pattern. some foo subimports are restricted", + type: "ImportDeclaration", + line: 1, + column: 1, + endColumn: 36 + }] + }, { + code: "import withPatterns from \"foo/bar\";", + options: [{ patterns: [{ group: ["foo/bar"] }] }], + errors: [{ + message: "'foo/bar' import is restricted from being used by a pattern.", + type: "ImportDeclaration", + line: 1, + column: 1, + endColumn: 36 + }] }, { code: "import withGitignores from \"foo/bar\";", options: [{ patterns: ["foo/*", "!foo/baz"] }], diff --git a/tests/lib/rules/no-sequences.js b/tests/lib/rules/no-sequences.js index f9eb8a6a30f..1ac7c4a6c6e 100644 --- a/tests/lib/rules/no-sequences.js +++ b/tests/lib/rules/no-sequences.js @@ -59,7 +59,11 @@ ruleTester.run("no-sequences", rule, { // valid code with "allowInParentheses" set to `false` { code: "for ((i = 0, j = 0); test; );", options: [{ allowInParentheses: false }] }, - { code: "for (; test; (i++, j++));", options: [{ allowInParentheses: false }] } + { code: "for (; test; (i++, j++));", options: [{ allowInParentheses: false }] }, + + // https://github.com/eslint/eslint/issues/14572 + { code: "const foo = () => { return ((bar = 123), 10) }", env: { es6: true } }, + { code: "const foo = () => (((bar = 123), 10));", env: { es6: true } } ], // Examples of code that should trigger the rule diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js index db041d46b89..48ccdb1d42f 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 }, @@ -1062,7 +1131,7 @@ ruleTester.run("no-unused-vars", rule, { code: `let myArray = [1,2,3,4].filter((x) => x == 0); myArray = myArray.filter((x) => x == 1);`, parserOptions: { ecmaVersion: 2015 }, - errors: [{ ...assignedError("myArray"), line: 2, column: 15 }] + errors: [{ ...assignedError("myArray"), line: 2, column: 5 }] }, { code: "const a = 1; a += 1;", @@ -1071,21 +1140,28 @@ ruleTester.run("no-unused-vars", rule, { }, { code: "var a = function() { a(); };", - errors: [{ ...assignedError("a"), line: 1, column: 22 }] + errors: [{ ...assignedError("a"), line: 1, column: 5 }] }, { code: "var a = function(){ return function() { a(); } };", - errors: [{ ...assignedError("a"), line: 1, column: 41 }] + errors: [{ ...assignedError("a"), line: 1, column: 5 }] }, { code: "const a = () => { a(); };", parserOptions: { ecmaVersion: 2015 }, - errors: [{ ...assignedError("a"), line: 1, column: 19 }] + errors: [{ ...assignedError("a"), line: 1, column: 7 }] }, { code: "const a = () => () => { a(); };", parserOptions: { ecmaVersion: 2015 }, - errors: [{ ...assignedError("a"), line: 1, column: 25 }] + errors: [{ ...assignedError("a"), line: 1, column: 7 }] + }, + + // https://github.com/eslint/eslint/issues/14324 + { + code: "let x = [];\nx = x.concat(x);", + parserOptions: { ecmaVersion: 2015 }, + errors: [{ ...assignedError("x"), line: 2, column: 1 }] }, { @@ -1098,7 +1174,25 @@ ruleTester.run("no-unused-vars", rule, { } }`, parserOptions: { ecmaVersion: 2020 }, - errors: [{ ...definedError("foo"), line: 3, column: 22 }, { ...assignedError("a"), line: 6, column: 21 }] + errors: [{ ...assignedError("a"), line: 2, column: 13 }, { ...definedError("foo"), line: 3, column: 22 }] + }, + { + code: `let foo; + init(); + foo = foo + 2; + function init() { + foo = 1; + }`, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ ...assignedError("foo"), line: 3, column: 13 }] + }, + { + code: `function foo(n) { + if (n < 2) return 1; + return n * foo(n - 1); + }`, + parserOptions: { ecmaVersion: 2020 }, + errors: [{ ...definedError("foo"), line: 1, column: 10 }] }, { code: `let c = 'c' diff --git a/tests/lib/rules/radix.js b/tests/lib/rules/radix.js index 52801c3d183..ddb8363c1d9 100644 --- a/tests/lib/rules/radix.js +++ b/tests/lib/rules/radix.js @@ -76,7 +76,34 @@ ruleTester.run("radix", rule, { code: "parseInt(\"10\");", errors: [{ messageId: "missingRadix", - type: "CallExpression" + type: "CallExpression", + suggestions: [{ messageId: "addRadixParameter10", output: "parseInt(\"10\", 10);" }] + }] + }, + { + code: "parseInt(\"10\",);", // Function parameter with trailing comma + parserOptions: { ecmaVersion: 2017 }, + errors: [{ + messageId: "missingRadix", + type: "CallExpression", + suggestions: [{ messageId: "addRadixParameter10", output: "parseInt(\"10\", 10,);" }] + }] + }, + { + code: "parseInt((0, \"10\"));", // Sequence expression (no trailing comma). + errors: [{ + messageId: "missingRadix", + type: "CallExpression", + suggestions: [{ messageId: "addRadixParameter10", output: "parseInt((0, \"10\"), 10);" }] + }] + }, + { + code: "parseInt((0, \"10\"),);", // Sequence expression (with trailing comma). + parserOptions: { ecmaVersion: 2017 }, + errors: [{ + messageId: "missingRadix", + type: "CallExpression", + suggestions: [{ messageId: "addRadixParameter10", output: "parseInt((0, \"10\"), 10,);" }] }] }, { @@ -154,7 +181,8 @@ ruleTester.run("radix", rule, { code: "Number.parseInt(\"10\");", errors: [{ messageId: "missingRadix", - type: "CallExpression" + type: "CallExpression", + suggestions: [{ messageId: "addRadixParameter10", output: "Number.parseInt(\"10\", 10);" }] }] }, { @@ -191,22 +219,46 @@ ruleTester.run("radix", rule, { { code: "parseInt?.(\"10\");", parserOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "missingRadix" }] + errors: [ + { + messageId: "missingRadix", + type: "CallExpression", + suggestions: [{ messageId: "addRadixParameter10", output: "parseInt?.(\"10\", 10);" }] + } + ] }, { code: "Number.parseInt?.(\"10\");", parserOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "missingRadix" }] + errors: [ + { + messageId: "missingRadix", + type: "CallExpression", + suggestions: [{ messageId: "addRadixParameter10", output: "Number.parseInt?.(\"10\", 10);" }] + } + ] }, { code: "Number?.parseInt(\"10\");", parserOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "missingRadix" }] + errors: [ + { + messageId: "missingRadix", + type: "CallExpression", + suggestions: [{ messageId: "addRadixParameter10", output: "Number?.parseInt(\"10\", 10);" }] + } + ] }, { code: "(Number?.parseInt)(\"10\");", parserOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "missingRadix" }] + errors: [ + { + messageId: "missingRadix", + type: "CallExpression", + suggestions: [{ messageId: "addRadixParameter10", output: "(Number?.parseInt)(\"10\", 10);" }] + } + ] } ] }); diff --git a/tests/lib/rules/require-atomic-updates.js b/tests/lib/rules/require-atomic-updates.js index cd51e38ffbd..bd3738ac979 100644 --- a/tests/lib/rules/require-atomic-updates.js +++ b/tests/lib/rules/require-atomic-updates.js @@ -53,6 +53,7 @@ ruleTester.run("require-atomic-updates", rule, { "let foo; async function x() { foo = condition ? foo : await bar; }", "async function x() { let foo; bar(() => { let foo; blah(foo); }); foo += await result; }", "let foo; async function x() { foo = foo + 1; await bar; }", + "async function x() { foo += await bar; }", /* @@ -162,6 +163,52 @@ ruleTester.run("require-atomic-updates", rule, { count -= 1 return } + `, + + // https://github.com/eslint/eslint/issues/14208 + ` + async function foo(e) { + } + + async function run() { + const input = []; + const props = []; + + for(const entry of input) { + const prop = props.find(a => a.id === entry.id) || null; + await foo(entry); + } + + for(const entry of input) { + const prop = props.find(a => a.id === entry.id) || null; + } + + for(const entry2 of input) { + const prop = props.find(a => a.id === entry2.id) || null; + } + } + `, + + ` + async function run() { + { + let entry; + await entry; + } + { + let entry; + () => entry; + + entry = 1; + } + } + `, + + ` + async function run() { + await a; + b = 1; + } ` ], @@ -251,7 +298,7 @@ ruleTester.run("require-atomic-updates", rule, { errors: [COMPUTED_PROPERTY_ERROR, STATIC_PROPERTY_ERROR] }, { - code: "async function x() { foo += await bar; }", + code: "let foo = ''; async function x() { foo += await bar; }", errors: [VARIABLE_ERROR] }, { 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/tests/tools/loose-parser.js b/tests/tools/loose-parser.js deleted file mode 100644 index 98e97cc3687..00000000000 --- a/tests/tools/loose-parser.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @fileoverview Define a custom parser to ignore recoverable syntax errors. - * @author Toru Nagashima - * - * no-redeclare rule uses this parser to check redeclarations. - */ -"use strict"; - -const acorn = require("acorn"); -const espree = require("espree/lib/espree"); - -/** - * Define the parser which ignores recoverable errors. - * @returns {(parser:acorn.Parser) => acorn.Parser} The function that defines loose parser. - */ -function loose() { - return Parser => class LooseParser extends Parser { - raiseRecoverable() { // eslint-disable-line class-methods-use-this - // ignore - } - }; -} - -const LooseEspree = acorn.Parser.extend(espree(), loose()); - -module.exports = { - parse(code, options) { - return new LooseEspree(options, code).parse(); - } -}; 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 + } } ] },