diff --git a/CHANGELOG.md b/CHANGELOG.md index efb9cc93035a..61aa641c59f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +v8.51.0 - October 6, 2023 + +* [`1ef39ea`](https://github.com/eslint/eslint/commit/1ef39ea5b884453be717ebc929155d7eb584dcbf) chore: upgrade @eslint/js@8.51.0 (#17624) (Milos Djermanovic) +* [`f8c7403`](https://github.com/eslint/eslint/commit/f8c7403255c11e99c402860aef3c0179f2b16628) chore: package.json update for @eslint/js release (ESLint Jenkins) +* [`f976b2f`](https://github.com/eslint/eslint/commit/f976b2f7bfe7cc78bb649f8b37e90fd519ff3bcc) fix: make rule severity case-sensitive in flat config (#17619) (Milos Djermanovic) +* [`0edfe36`](https://github.com/eslint/eslint/commit/0edfe369aa5bd80a98053022bb4c6b1ea0155f44) fix: Ensure crash error messages are not duplicated (#17584) (Nicholas C. Zakas) +* [`ee5be81`](https://github.com/eslint/eslint/commit/ee5be81fa3c4fe801c2f653854f098ed6a84dcef) docs: default to `sourceType: "module"` in rule examples (#17615) (Francesco Trotta) +* [`dd79abc`](https://github.com/eslint/eslint/commit/dd79abc0c1857b1d765acc312c0d6518e40d31c9) fix: `eslint-disable` to be able to parse quoted rule names (#17612) (Yosuke Ota) +* [`d2f6801`](https://github.com/eslint/eslint/commit/d2f68019b8882278877801c5ef2f74d55e2a10c1) fix: Ensure correct code path for && followed by ?? (#17618) (Nicholas C. Zakas) +* [`2665552`](https://github.com/eslint/eslint/commit/2665552ba0057e8603f9fbece0fd236f189f5cf3) test: fix flat config linter tests to use Linter in flat config mode (#17616) (Milos Djermanovic) +* [`1aa26df`](https://github.com/eslint/eslint/commit/1aa26df9fbcfdf5b895743c6d2d3a216479544b1) docs: Add more examples for multiline-ternary (#17610) (George Ashiotis) +* [`47d0b44`](https://github.com/eslint/eslint/commit/47d0b446964f44d70b9457ecc368e721e1dc7c11) docs: Update README (GitHub Actions Bot) +* [`dbf831e`](https://github.com/eslint/eslint/commit/dbf831e31f8eea0bc94df96cd33255579324b66e) docs: use generated og image (#17601) (Percy Ma) +* [`0a9c433`](https://github.com/eslint/eslint/commit/0a9c43339a4adef24ef83034d0b078dd279cc977) feat: Add `--no-warn-ignored` CLI option for flat config (#17569) (Domantas Petrauskas) +* [`1866da5`](https://github.com/eslint/eslint/commit/1866da5e1d931787256ecb825a803cac5835b71c) docs: Update README (GitHub Actions Bot) +* [`7b77bcc`](https://github.com/eslint/eslint/commit/7b77bccbb51bd36b2d20fea61bc782545c4029b3) chore: Refactor CodePathState (#17510) (Nicholas C. Zakas) +* [`977e67e`](https://github.com/eslint/eslint/commit/977e67ec274a05cb7391665b5e3453e7f72f72b2) feat: logical-assignment-operators to report expressions with 3 operands (#17600) (Yosuke Ota) +* [`bc77c9a`](https://github.com/eslint/eslint/commit/bc77c9af12539f350ef19e30611a153a5b869c6b) chore: Document and refactor ForkContext (#17566) (Nicholas C. Zakas) +* [`24e1f14`](https://github.com/eslint/eslint/commit/24e1f140ec68659e55c1ace0d7500addb135a2b4) chore: Refactor and document CodePath (#17558) (Nicholas C. Zakas) + v8.50.0 - September 22, 2023 * [`f8a8a2d`](https://github.com/eslint/eslint/commit/f8a8a2d6b45c82f94a574623759b6e3d2af193f3) chore: upgrade @eslint/js@8.50.0 (#17599) (Milos Djermanovic) diff --git a/README.md b/README.md index 0b8296193241..7aa4019ed87a 100644 --- a/README.md +++ b/README.md @@ -288,8 +288,8 @@ The following companies, organizations, and individuals support ESLint's ongoing

Platinum Sponsors

Chrome Frameworks Fund Automattic

Gold Sponsors

Salesforce Airbnb

Silver Sponsors

-

Sentry Liftoff Siemens American Express

Bronze Sponsors

-

ThemeIsle Nx (by Nrwl) Anagram Solver Icons8 Discord Transloadit Ignition HeroCoders QuickBooks Tool hub

+

Liftoff Siemens American Express

Bronze Sponsors

+

ThemeIsle Anagram Solver Icons8 Discord Transloadit Ignition HeroCoders QuickBooks Tool hub

## Technology Sponsors diff --git a/bin/eslint.js b/bin/eslint.js index 7094ac77bc4b..5c7972cc086e 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -92,6 +92,14 @@ function getErrorMessage(error) { return util.format("%o", error); } +/** + * Tracks error messages that are shown to the user so we only ever show the + * same message once. + * @type {Set} + */ + +const displayedErrors = new Set(); + /** * Catch and report unexpected error. * @param {any} error The thrown error object. @@ -101,14 +109,17 @@ function onFatalError(error) { process.exitCode = 2; const { version } = require("../package.json"); - const message = getErrorMessage(error); - - console.error(` + const message = ` Oops! Something went wrong! :( ESLint: ${version} -${message}`); +${getErrorMessage(error)}`; + + if (!displayedErrors.has(message)) { + console.error(message); + displayedErrors.add(message); + } } //------------------------------------------------------------------------------ diff --git a/docs/.eleventy.js b/docs/.eleventy.js index 94a202112543..68c6604cf8d0 100644 --- a/docs/.eleventy.js +++ b/docs/.eleventy.js @@ -205,11 +205,12 @@ module.exports = function(eleventyConfig) { } // See https://github.com/eslint/eslint.org/blob/ac38ab41f99b89a8798d374f74e2cce01171be8b/src/playground/App.js#L44 - const parserOptions = tokens[index].info?.split("correct ")[1]?.trim(); + const parserOptionsJSON = tokens[index].info?.split("correct ")[1]?.trim(); + const parserOptions = { sourceType: "module", ...(parserOptionsJSON && JSON.parse(parserOptionsJSON)) }; const { content } = tokens[index + 1]; const state = encodeToBase64( JSON.stringify({ - ...(parserOptions && { options: { parserOptions: JSON.parse(parserOptions) } }), + options: { parserOptions }, text: content }) ); diff --git a/docs/package.json b/docs/package.json index 120aa91c60c3..1b2019bb4cb6 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,7 +1,7 @@ { "name": "docs-eslint", "private": true, - "version": "8.50.0", + "version": "8.51.0", "description": "", "main": "index.js", "keywords": [], diff --git a/docs/src/_includes/layouts/base.html b/docs/src/_includes/layouts/base.html index 49bdf8522c89..86ea680c3cb0 100644 --- a/docs/src/_includes/layouts/base.html +++ b/docs/src/_includes/layouts/base.html @@ -13,11 +13,19 @@ {% set page_title = site[hook].title %} {% endif %} + {% set rule_meta = rules_meta[title] %} {% set page_title = site.shared.title_format | replace("PAGE_TITLE", page_title) %} - {% set cover_image = ["https://", site.hostname, "/icon-512.png" ] | join %} - {% set cover_image_alt = site.shared.eslint_logo_alt %} {% set page_desc = site.shared.description %} {% set relative_page_url = page.url | url | prettyURL %} + {% set cover_image = [ + "https://", site.hostname, "/og", + "?title=", title | urlencode, "&summary=", page_desc | urlencode, + "&is_rule=", rule_meta !== undefined, + "&recommended=", rule_meta.docs.recommended, + "&fixable=", rule_meta.fixable, + "&suggestions=", rule_meta.hasSuggestions + ] | join %} + {% set cover_image_alt = site.shared.eslint_logo_alt %} {% set page_url = ["https://", site.hostname, relative_page_url ] | join %} diff --git a/docs/src/rules/camelcase.md b/docs/src/rules/camelcase.md index 46757fd53b06..aa75b231becd 100644 --- a/docs/src/rules/camelcase.md +++ b/docs/src/rules/camelcase.md @@ -328,7 +328,7 @@ function UNSAFE_componentWillMount() { // ... } -function UNSAFE_componentWillMount() { +function UNSAFE_componentWillReceiveProps() { // ... } ``` diff --git a/docs/src/rules/comma-spacing.md b/docs/src/rules/comma-spacing.md index b69378563211..9bc7f82dd246 100644 --- a/docs/src/rules/comma-spacing.md +++ b/docs/src/rules/comma-spacing.md @@ -60,7 +60,7 @@ var arr = [1 , 2]; var obj = {"foo": "bar" ,"baz": "qur"}; foo(a ,b); new Foo(a ,b); -function foo(a ,b){} +function baz(a ,b){} a ,b ``` @@ -80,7 +80,7 @@ var arr = [1,, 3] var obj = {"foo": "bar", "baz": "qur"}; foo(a, b); new Foo(a, b); -function foo(a, b){} +function qur(a, b){} a, b ``` @@ -131,7 +131,7 @@ var foo = 1, bar = 2; var arr = [1 , 2]; var obj = {"foo": "bar", "baz": "qur"}; new Foo(a,b); -function foo(a,b){} +function baz(a,b){} a, b ``` @@ -151,7 +151,7 @@ var arr = [1 ,,3] var obj = {"foo": "bar" ,"baz": "qur"}; foo(a ,b); new Foo(a ,b); -function foo(a ,b){} +function qur(a ,b){} a ,b ``` diff --git a/docs/src/rules/comma-style.md b/docs/src/rules/comma-style.md index 99d14b27f4fa..6936cdbcb244 100644 --- a/docs/src/rules/comma-style.md +++ b/docs/src/rules/comma-style.md @@ -69,7 +69,7 @@ var foo = 1 var foo = ["apples" , "oranges"]; -function bar() { +function baz() { return { "a": 1 ,"b:": 2 @@ -94,7 +94,7 @@ var foo = 1, var foo = ["apples", "oranges"]; -function bar() { +function baz() { return { "a": 1, "b:": 2 @@ -119,7 +119,7 @@ var foo = 1, var foo = ["apples", "oranges"]; -function bar() { +function baz() { return { "a": 1, "b:": 2 @@ -144,7 +144,7 @@ var foo = 1 var foo = ["apples" ,"oranges"]; -function bar() { +function baz() { return { "a": 1 ,"b:": 2 diff --git a/docs/src/rules/consistent-return.md b/docs/src/rules/consistent-return.md index 2d5f4e3f2ca7..797c9b08ca01 100644 --- a/docs/src/rules/consistent-return.md +++ b/docs/src/rules/consistent-return.md @@ -48,7 +48,7 @@ function doSomething(condition) { } } -function doSomething(condition) { +function doSomethingElse(condition) { if (condition) { return true; } diff --git a/docs/src/rules/default-param-last.md b/docs/src/rules/default-param-last.md index 7f64f7c4464d..63672a9103ec 100644 --- a/docs/src/rules/default-param-last.md +++ b/docs/src/rules/default-param-last.md @@ -28,7 +28,7 @@ Examples of **incorrect** code for this rule: function f(a = 0, b) {} -function f(a, b = 0, c) {} +function g(a, b = 0, c) {} ``` ::: diff --git a/docs/src/rules/function-paren-newline.md b/docs/src/rules/function-paren-newline.md index 4ba5a922c8f4..e6a067cf5f23 100644 --- a/docs/src/rules/function-paren-newline.md +++ b/docs/src/rules/function-paren-newline.md @@ -49,9 +49,9 @@ Examples of **incorrect** code for this rule with the `"always"` option: function foo(bar, baz) {} -var foo = function(bar, baz) {}; +var qux = function(bar, baz) {}; -var foo = (bar, baz) => {}; +var qux = (bar, baz) => {}; foo(bar, baz); ``` @@ -70,11 +70,11 @@ function foo( baz ) {} -var foo = function( +var qux = function( bar, baz ) {}; -var foo = ( +var qux = ( bar, baz ) => {}; @@ -99,11 +99,11 @@ function foo( baz ) {} -var foo = function( +var qux = function( bar, baz ) {}; -var foo = ( +var qux = ( bar, baz ) => {}; @@ -125,12 +125,12 @@ Examples of **correct** code for this rule with the `"never"` option: function foo(bar, baz) {} -function foo(bar, +function qux(bar, baz) {} -var foo = function(bar, baz) {}; +var foobar = function(bar, baz) {}; -var foo = (bar, baz) => {}; +var foobar = (bar, baz) => {}; foo(bar, baz); @@ -151,11 +151,11 @@ function foo(bar, baz ) {} -var foo = function( +var qux = function( bar, baz ) {}; -var foo = ( +var qux = ( bar, baz) => {}; @@ -180,12 +180,12 @@ Examples of **correct** code for this rule with the default `"multiline"` option function foo(bar, baz) {} -var foo = function( +var foobar = function( bar, baz ) {}; -var foo = (bar, baz) => {}; +var foobar = (bar, baz) => {}; foo(bar, baz, qux); @@ -213,11 +213,11 @@ function foo(bar, baz ) {} -var foo = function(bar, +var qux = function(bar, baz ) {}; -var foo = ( +var qux = ( bar, baz) => {}; @@ -243,9 +243,9 @@ Examples of **correct** code for this rule with the `"consistent"` option: function foo(bar, baz) {} -var foo = function(bar, baz) {}; +var qux = function(bar, baz) {}; -var foo = ( +var qux = ( bar, baz ) => {}; @@ -274,11 +274,11 @@ function foo(bar, baz ) {} -var foo = function(bar, +var foobar = function(bar, baz ) {}; -var foo = ( +var foobar = ( bar, baz) => {}; @@ -306,9 +306,9 @@ function foo( baz ) {} -var foo = function(bar, baz) {}; +var qux = function(bar, baz) {}; -var foo = ( +var qux = ( bar ) => {}; @@ -333,13 +333,13 @@ function foo( baz ) {} -function foo(bar, baz, qux) {} +function foobar(bar, baz, qux) {} -var foo = function( +var barbaz = function( bar, baz ) {}; -var foo = (bar, +var barbaz = (bar, baz) => {}; foo(bar, @@ -357,13 +357,13 @@ Examples of **correct** code for this rule with the `{ "minItems": 3 }` option: function foo(bar, baz) {} -var foo = function( +var foobar = function( bar, baz, qux ) {}; -var foo = ( +var foobar = ( bar, baz, qux ) => {}; diff --git a/docs/src/rules/global-require.md b/docs/src/rules/global-require.md index e960468b3807..8460acb89ad1 100644 --- a/docs/src/rules/global-require.md +++ b/docs/src/rules/global-require.md @@ -32,7 +32,7 @@ This rule requires all calls to `require()` to be at the top level of the module Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint global-require: "error"*/ @@ -76,7 +76,7 @@ try { Examples of **correct** code for this rule: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint global-require: "error"*/ diff --git a/docs/src/rules/id-length.md b/docs/src/rules/id-length.md index adf5e8978175..aed5ada0a7c6 100644 --- a/docs/src/rules/id-length.md +++ b/docs/src/rules/id-length.md @@ -160,10 +160,10 @@ try { } var myObj = { apple: 1 }; (value) => { value * value }; -function foobar(value = 0) { } +function foobaz(value = 0) { } class MyClass { } class Foobar { method() {} } -function foobar(...args) { } +function barbaz(...args) { } var { prop } = {}; var [longName] = foo; var { a: [prop] } = {}; diff --git a/docs/src/rules/lines-around-directive.md b/docs/src/rules/lines-around-directive.md index fb71775c7f16..8a83df1afd7c 100644 --- a/docs/src/rules/lines-around-directive.md +++ b/docs/src/rules/lines-around-directive.md @@ -60,7 +60,7 @@ This is the default option. Examples of **incorrect** code for this rule with the `"always"` option: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /* eslint lines-around-directive: ["error", "always"] */ @@ -92,7 +92,7 @@ function foo() { Examples of **correct** code for this rule with the `"always"` option: -::: correct +::: correct { "sourceType": "script" } ```js /* eslint lines-around-directive: ["error", "always"] */ @@ -132,7 +132,7 @@ function foo() { Examples of **incorrect** code for this rule with the `"never"` option: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /* eslint lines-around-directive: ["error", "never"] */ @@ -171,7 +171,7 @@ function foo() { Examples of **correct** code for this rule with the `"never"` option: -::: correct +::: correct { "sourceType": "script" } ```js /* eslint lines-around-directive: ["error", "never"] */ @@ -205,7 +205,7 @@ function foo() { Examples of **incorrect** code for this rule with the `{ "before": "never", "after": "always" }` option: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /* eslint lines-around-directive: ["error", { "before": "never", "after": "always" }] */ @@ -240,7 +240,7 @@ function foo() { Examples of **correct** code for this rule with the `{ "before": "never", "after": "always" }` option: -::: correct +::: correct { "sourceType": "script" } ```js /* eslint lines-around-directive: ["error", { "before": "never", "after": "always" }] */ @@ -276,7 +276,7 @@ function foo() { Examples of **incorrect** code for this rule with the `{ "before": "always", "after": "never" }` option: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /* eslint lines-around-directive: ["error", { "before": "always", "after": "never" }] */ @@ -312,7 +312,7 @@ function foo() { Examples of **correct** code for this rule with the `{ "before": "always", "after": "never" }` option: -::: correct +::: correct { "sourceType": "script" } ```js /* eslint lines-around-directive: ["error", { "before": "always", "after": "never" }] */ diff --git a/docs/src/rules/logical-assignment-operators.md b/docs/src/rules/logical-assignment-operators.md index 058afcd3ad78..1b59cfa70bba 100644 --- a/docs/src/rules/logical-assignment-operators.md +++ b/docs/src/rules/logical-assignment-operators.md @@ -10,7 +10,7 @@ For example `a = a || b` can be shortened to `a ||= b`. ## Rule Details -This rule requires or disallows logical assignment operator shorthand. +This rule requires or disallows logical assignment operator shorthand. ### Options @@ -27,6 +27,9 @@ Object option (only available if string option is set to `"always"`): #### always +This option checks for expressions that can be shortened using logical assignment operator. For example, `a = a || b` can be shortened to `a ||= b`. +Expressions with associativity such as `a = a || b || c` are reported as being able to be shortened to `a ||= b || c` unless the evaluation order is explicitly defined using parentheses, such as `a = (a || b) || c`. + Examples of **incorrect** code for this rule with the default `"always"` option: ::: incorrect @@ -40,6 +43,9 @@ a = a ?? b a || (a = b) a && (a = b) a ?? (a = b) +a = a || b || c +a = a && b && c +a = a ?? b ?? c ``` ::: @@ -58,6 +64,8 @@ a = b || c a || (b = c) if (a) a = b + +a = (a || b) || c ``` ::: diff --git a/docs/src/rules/max-statements-per-line.md b/docs/src/rules/max-statements-per-line.md index 238ade787e86..76a49e3a09b1 100644 --- a/docs/src/rules/max-statements-per-line.md +++ b/docs/src/rules/max-statements-per-line.md @@ -40,7 +40,7 @@ if (condition) { bar = 1; } for (var i = 0; i < length; ++i) { bar = 1; } switch (discriminant) { default: break; } function foo() { bar = 1; } -var foo = function foo() { bar = 1; }; +var qux = function qux() { bar = 1; }; (function foo() { bar = 1; })(); ``` @@ -58,7 +58,7 @@ if (condition) bar = 1; for (var i = 0; i < length; ++i); switch (discriminant) { default: } function foo() { } -var foo = function foo() { }; +var qux = function qux() { }; (function foo() { })(); ``` @@ -76,7 +76,7 @@ if (condition) { bar = 1; } else { baz = 2; } for (var i = 0; i < length; ++i) { bar = 1; baz = 2; } switch (discriminant) { case 'test': break; default: break; } function foo() { bar = 1; baz = 2; } -var foo = function foo() { bar = 1; }; +var qux = function qux() { bar = 1; }; (function foo() { bar = 1; baz = 2; })(); ``` @@ -94,7 +94,7 @@ if (condition) bar = 1; if (condition) baz = 2; for (var i = 0; i < length; ++i) { bar = 1; } switch (discriminant) { default: break; } function foo() { bar = 1; } -var foo = function foo() { bar = 1; }; +var qux = function qux() { bar = 1; }; (function foo() { var bar = 1; })(); ``` diff --git a/docs/src/rules/multiline-ternary.md b/docs/src/rules/multiline-ternary.md index 7e4688d637ae..f828bc1cd40a 100644 --- a/docs/src/rules/multiline-ternary.md +++ b/docs/src/rules/multiline-ternary.md @@ -18,9 +18,14 @@ var foo = bar > baz ? value1 : value2; The above can be rewritten as the following to improve readability and more clearly delineate the operands: ```js + var foo = bar > baz ? value1 : value2; + +var foo = bar > baz + ? value1 + : value2; ``` ## Rule Details @@ -74,6 +79,12 @@ foo > bar ? value1 : value2) : value3; + +foo > bar + ? (baz > qux + ? value1 + : value2) + : value3; ``` ::: @@ -126,6 +137,12 @@ foo > bar && bar > baz ? value1 : value2; + +foo > bar + ? baz > qux + ? value1 + : value2 + : value3; ``` ::: diff --git a/docs/src/rules/newline-before-return.md b/docs/src/rules/newline-before-return.md index 41fb7726c7b2..1980f5bd3463 100644 --- a/docs/src/rules/newline-before-return.md +++ b/docs/src/rules/newline-before-return.md @@ -49,14 +49,14 @@ Examples of **incorrect** code for this rule: ```js /*eslint newline-before-return: "error"*/ -function foo(bar) { +function foo1(bar) { if (!bar) { return; } return bar; } -function foo(bar) { +function foo2(bar) { if (!bar) { return; } @@ -75,30 +75,30 @@ Examples of **correct** code for this rule: ```js /*eslint newline-before-return: "error"*/ -function foo() { +function foo1() { return; } -function foo() { +function foo2() { return; } -function foo(bar) { +function foo3(bar) { if (!bar) return; } -function foo(bar) { +function foo4(bar) { if (!bar) { return }; } -function foo(bar) { +function foo5(bar) { if (!bar) { return; } } -function foo(bar) { +function foo6(bar) { if (!bar) { return; } @@ -106,14 +106,14 @@ function foo(bar) { return bar; } -function foo(bar) { +function foo7(bar) { if (!bar) { return; } } -function foo() { +function foo8() { // comment return; diff --git a/docs/src/rules/no-catch-shadow.md b/docs/src/rules/no-catch-shadow.md index 23328e42fef2..c875f9fe4a92 100644 --- a/docs/src/rules/no-catch-shadow.md +++ b/docs/src/rules/no-catch-shadow.md @@ -39,13 +39,13 @@ try { } -function err() { +function error() { // ... }; try { throw "problem"; -} catch (err) { +} catch (error) { } ``` @@ -67,7 +67,7 @@ try { } -function err() { +function error() { // ... }; diff --git a/docs/src/rules/no-cond-assign.md b/docs/src/rules/no-cond-assign.md index b72c2d09c672..7cb02d7c32fb 100644 --- a/docs/src/rules/no-cond-assign.md +++ b/docs/src/rules/no-cond-assign.md @@ -45,8 +45,7 @@ if (x = 0) { } // Practical example that is similar to an error -function setHeight(someNode) { - "use strict"; +var setHeight = function (someNode) { do { someNode.height = "100px"; } while (someNode = someNode.parentNode); @@ -69,16 +68,14 @@ if (x === 0) { } // Practical example that wraps the assignment in parentheses -function setHeight(someNode) { - "use strict"; +var setHeight = function (someNode) { do { someNode.height = "100px"; } while ((someNode = someNode.parentNode)); } // Practical example that wraps the assignment and tests for 'null' -function setHeight(someNode) { - "use strict"; +var setHeight = function (someNode) { do { someNode.height = "100px"; } while ((someNode = someNode.parentNode) !== null); @@ -103,24 +100,21 @@ if (x = 0) { } // Practical example that is similar to an error -function setHeight(someNode) { - "use strict"; +var setHeight = function (someNode) { do { someNode.height = "100px"; } while (someNode = someNode.parentNode); } // Practical example that wraps the assignment in parentheses -function setHeight(someNode) { - "use strict"; +var setHeight = function (someNode) { do { someNode.height = "100px"; } while ((someNode = someNode.parentNode)); } // Practical example that wraps the assignment and tests for 'null' -function setHeight(someNode) { - "use strict"; +var setHeight = function (someNode) { do { someNode.height = "100px"; } while ((someNode = someNode.parentNode) !== null); diff --git a/docs/src/rules/no-delete-var.md b/docs/src/rules/no-delete-var.md index 65a5aa1614dd..f66aec5879fb 100644 --- a/docs/src/rules/no-delete-var.md +++ b/docs/src/rules/no-delete-var.md @@ -15,7 +15,7 @@ If ESLint parses code in strict mode, the parser (instead of this rule) reports Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-delete-var: "error"*/ diff --git a/docs/src/rules/no-dupe-args.md b/docs/src/rules/no-dupe-args.md index 3cb9133c83fa..748d52c2e21f 100644 --- a/docs/src/rules/no-dupe-args.md +++ b/docs/src/rules/no-dupe-args.md @@ -16,7 +16,7 @@ If ESLint parses code in strict mode, the parser (instead of this rule) reports Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-dupe-args: "error"*/ @@ -34,7 +34,7 @@ var bar = function (a, b, a) { Examples of **correct** code for this rule: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-dupe-args: "error"*/ diff --git a/docs/src/rules/no-else-return.md b/docs/src/rules/no-else-return.md index 917c44553908..79b4fe5fb01d 100644 --- a/docs/src/rules/no-else-return.md +++ b/docs/src/rules/no-else-return.md @@ -37,7 +37,7 @@ Examples of **incorrect** code for this rule: ```js /*eslint no-else-return: "error"*/ -function foo() { +function foo1() { if (x) { return y; } else { @@ -45,7 +45,7 @@ function foo() { } } -function foo() { +function foo2() { if (x) { return y; } else if (z) { @@ -55,7 +55,7 @@ function foo() { } } -function foo() { +function foo3() { if (x) { return y; } else { @@ -65,7 +65,7 @@ function foo() { return t; } -function foo() { +function foo4() { if (error) { return 'It failed'; } else { @@ -76,7 +76,7 @@ function foo() { } // Two warnings for nested occurrences -function foo() { +function foo5() { if (x) { if (y) { return y; @@ -98,7 +98,7 @@ Examples of **correct** code for this rule: ```js /*eslint no-else-return: "error"*/ -function foo() { +function foo1() { if (x) { return y; } @@ -106,7 +106,7 @@ function foo() { return z; } -function foo() { +function foo2() { if (x) { return y; } else if (z) { @@ -116,7 +116,7 @@ function foo() { } } -function foo() { +function foo3() { if (x) { if (z) { return y; @@ -126,7 +126,7 @@ function foo() { } } -function foo() { +function foo4() { if (error) { return 'It failed'; } else if (loading) { diff --git a/docs/src/rules/no-empty-function.md b/docs/src/rules/no-empty-function.md index a9be4bb05d51..68aa2ad887b6 100644 --- a/docs/src/rules/no-empty-function.md +++ b/docs/src/rules/no-empty-function.md @@ -38,13 +38,13 @@ Examples of **incorrect** code for this rule: function foo() {} -var foo = function() {}; +var bar = function() {}; -var foo = () => {}; +var bar = () => {}; -function* foo() {} +function* baz() {} -var foo = function*() {}; +var bar = function*() {}; var obj = { foo: function() {}, @@ -95,19 +95,19 @@ function foo() { // do nothing. } -var foo = function() { +var baz = function() { // any clear comments. }; -var foo = () => { +var baz = () => { bar(); }; -function* foo() { +function* foobar() { // do nothing. } -var foo = function*() { +var baz = function*() { // do nothing. }; @@ -205,7 +205,7 @@ Examples of **correct** code for the `{ "allow": ["functions"] }` option: function foo() {} -var foo = function() {}; +var bar = function() {}; var obj = { foo: function() {} @@ -241,7 +241,7 @@ Examples of **correct** code for the `{ "allow": ["generatorFunctions"] }` optio function* foo() {} -var foo = function*() {}; +var bar = function*() {}; var obj = { foo: function*() {} diff --git a/docs/src/rules/no-empty-pattern.md b/docs/src/rules/no-empty-pattern.md index 7edd933731a9..92000add1857 100644 --- a/docs/src/rules/no-empty-pattern.md +++ b/docs/src/rules/no-empty-pattern.md @@ -44,9 +44,9 @@ var [] = foo; var {a: {}} = foo; var {a: []} = foo; function foo({}) {} -function foo([]) {} -function foo({a: {}}) {} -function foo({a: []}) {} +function bar([]) {} +function baz({a: {}}) {} +function qux({a: []}) {} ``` ::: @@ -61,7 +61,7 @@ Examples of **correct** code for this rule: var {a = {}} = foo; var {a = []} = foo; function foo({a = {}}) {} -function foo({a = []}) {} +function bar({a = []}) {} ``` ::: @@ -84,12 +84,12 @@ Examples of **incorrect** code for this rule with the `{"allowObjectPatternsAsPa /*eslint no-empty-pattern: ["error", { "allowObjectPatternsAsParameters": true }]*/ function foo({a: {}}) {} -var foo = function({a: {}}) {}; -var foo = ({a: {}}) => {}; -var foo = ({} = bar) => {}; -var foo = ({} = { bar: 1 }) => {}; +var bar = function({a: {}}) {}; +var bar = ({a: {}}) => {}; +var bar = ({} = bar) => {}; +var bar = ({} = { bar: 1 }) => {}; -function foo([]) {} +function baz([]) {} ``` ::: @@ -102,10 +102,10 @@ Examples of **correct** code for this rule with the `{"allowObjectPatternsAsPara /*eslint no-empty-pattern: ["error", { "allowObjectPatternsAsParameters": true }]*/ function foo({}) {} -var foo = function({}) {}; -var foo = ({}) => {}; +var bar = function({}) {}; +var bar = ({}) => {}; -function foo({} = {}) {} +function baz({} = {}) {} ``` ::: diff --git a/docs/src/rules/no-extra-boolean-cast.md b/docs/src/rules/no-extra-boolean-cast.md index e45509890f50..2408d6171746 100644 --- a/docs/src/rules/no-extra-boolean-cast.md +++ b/docs/src/rules/no-extra-boolean-cast.md @@ -75,7 +75,7 @@ Examples of **correct** code for this rule: var foo = !!bar; var foo = Boolean(bar); -function foo() { +function qux() { return !!bar; } diff --git a/docs/src/rules/no-extra-parens.md b/docs/src/rules/no-extra-parens.md index 77bef24a7286..1a4d8e2d48a0 100644 --- a/docs/src/rules/no-extra-parens.md +++ b/docs/src/rules/no-extra-parens.md @@ -159,11 +159,11 @@ Examples of **correct** code for this rule with the `"all"` and `{ "returnAssign ```js /* eslint no-extra-parens: ["error", "all", { "returnAssign": false }] */ -function a(b) { +function a1(b) { return (b = 1); } -function a(b) { +function a2(b) { return b ? (c = d) : (c = e); } diff --git a/docs/src/rules/no-func-assign.md b/docs/src/rules/no-func-assign.md index a0f146203742..6346e2b8064f 100644 --- a/docs/src/rules/no-func-assign.md +++ b/docs/src/rules/no-func-assign.md @@ -27,8 +27,8 @@ Examples of **incorrect** code for this rule: function foo() {} foo = bar; -function foo() { - foo = bar; +function baz() { + baz = bar; } var a = function hello() { @@ -61,12 +61,12 @@ Examples of **correct** code for this rule: var foo = function () {} foo = bar; -function foo(foo) { // `foo` is shadowed. - foo = bar; +function baz(baz) { // `baz` is shadowed. + baz = bar; } -function foo() { - var foo = bar; // `foo` is shadowed. +function qux() { + var qux = bar; // `qux` is shadowed. } ``` diff --git a/docs/src/rules/no-inner-declarations.md b/docs/src/rules/no-inner-declarations.md index 8828fabe7d2f..a81a45a61fa1 100644 --- a/docs/src/rules/no-inner-declarations.md +++ b/docs/src/rules/no-inner-declarations.md @@ -74,7 +74,7 @@ This rule has a string option: Examples of **incorrect** code for this rule with the default `"functions"` option: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-inner-declarations: "error"*/ @@ -104,7 +104,7 @@ class C { Examples of **correct** code for this rule with the default `"functions"` option: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-inner-declarations: "error"*/ @@ -139,7 +139,7 @@ if (foo) var a; Examples of **incorrect** code for this rule with the `"both"` option: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-inner-declarations: ["error", "both"]*/ @@ -171,7 +171,7 @@ class C { Examples of **correct** code for this rule with the `"both"` option: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-inner-declarations: ["error", "both"]*/ diff --git a/docs/src/rules/no-invalid-this.md b/docs/src/rules/no-invalid-this.md index 9e4a2aedffae..0c529a970650 100644 --- a/docs/src/rules/no-invalid-this.md +++ b/docs/src/rules/no-invalid-this.md @@ -49,7 +49,7 @@ With `"parserOptions": { "sourceType": "module" }` in the ESLint configuration, Examples of **incorrect** code for this rule in strict mode: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-invalid-this: "error"*/ @@ -97,7 +97,7 @@ foo.forEach(function() { Examples of **correct** code for this rule in strict mode: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-invalid-this: "error"*/ @@ -243,7 +243,7 @@ Set `"capIsConstructor"` to `false` if you want those functions to be treated as Examples of **incorrect** code for this rule with `"capIsConstructor"` option set to `false`: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-invalid-this: ["error", { "capIsConstructor": false }]*/ @@ -271,7 +271,7 @@ Baz = function() { Examples of **correct** code for this rule with `"capIsConstructor"` option set to `false`: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-invalid-this: ["error", { "capIsConstructor": false }]*/ diff --git a/docs/src/rules/no-irregular-whitespace.md b/docs/src/rules/no-irregular-whitespace.md index 137c588d9b15..c0683645628e 100644 --- a/docs/src/rules/no-irregular-whitespace.md +++ b/docs/src/rules/no-irregular-whitespace.md @@ -74,31 +74,31 @@ Examples of **incorrect** code for this rule with the default `{ "skipStrings": ```js /*eslint no-irregular-whitespace: "error"*/ -function thing() /**/{ +var thing = function() /**/{ return 'test'; } -function thing( /**/){ +var thing = function( /**/){ return 'test'; } -function thing /**/(){ +var thing = function /**/(){ return 'test'; } -function thing᠎/**/(){ +var thing = function᠎/**/(){ return 'test'; } -function thing() { +var thing = function() { return 'test'; /**/ } -function thing() { +var thing = function() { return 'test'; /**/ } -function thing() { +var thing = function() { // Description : some descriptive text } @@ -106,12 +106,12 @@ function thing() { Description : some descriptive text */ -function thing() { +var thing = function() { return / regexp/; } /*eslint-env es6*/ -function thing() { +var thing = function() { return `template string`; } ``` @@ -125,15 +125,15 @@ Examples of **correct** code for this rule with the default `{ "skipStrings": tr ```js /*eslint no-irregular-whitespace: "error"*/ -function thing() { +var thing = function() { return ' thing'; } -function thing() { +var thing = function() { return '​thing'; } -function thing() { +var thing = function() { return 'th ing'; } ``` diff --git a/docs/src/rules/no-nonoctal-decimal-escape.md b/docs/src/rules/no-nonoctal-decimal-escape.md index 6ca59fdf79a4..43d0282635af 100644 --- a/docs/src/rules/no-nonoctal-decimal-escape.md +++ b/docs/src/rules/no-nonoctal-decimal-escape.md @@ -30,7 +30,7 @@ This rule disallows `\8` and `\9` escape sequences in string literals. Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-nonoctal-decimal-escape: "error"*/ @@ -52,7 +52,7 @@ var quux = "\0\8"; Examples of **correct** code for this rule: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-nonoctal-decimal-escape: "error"*/ diff --git a/docs/src/rules/no-octal-escape.md b/docs/src/rules/no-octal-escape.md index ae8fecdce32c..755bc56bc84f 100644 --- a/docs/src/rules/no-octal-escape.md +++ b/docs/src/rules/no-octal-escape.md @@ -18,7 +18,7 @@ If ESLint parses code in strict mode, the parser (instead of this rule) reports Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-octal-escape: "error"*/ @@ -30,7 +30,7 @@ var foo = "Copyright \251"; Examples of **correct** code for this rule: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-octal-escape: "error"*/ diff --git a/docs/src/rules/no-octal.md b/docs/src/rules/no-octal.md index 6a8fa7709a4d..c40d9a977b23 100644 --- a/docs/src/rules/no-octal.md +++ b/docs/src/rules/no-octal.md @@ -21,7 +21,7 @@ If ESLint parses code in strict mode, the parser (instead of this rule) reports Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-octal: "error"*/ @@ -34,7 +34,7 @@ var result = 5 + 07; Examples of **correct** code for this rule: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-octal: "error"*/ diff --git a/docs/src/rules/no-param-reassign.md b/docs/src/rules/no-param-reassign.md index 9c23c45f981a..a3cc558cf451 100644 --- a/docs/src/rules/no-param-reassign.md +++ b/docs/src/rules/no-param-reassign.md @@ -21,19 +21,19 @@ Examples of **incorrect** code for this rule: ```js /*eslint no-param-reassign: "error"*/ -function foo(bar) { +var foo = function(bar) { bar = 13; } -function foo(bar) { +var foo = function(bar) { bar++; } -function foo(bar) { +var foo = function(bar) { for (bar in baz) {} } -function foo(bar) { +var foo = function(bar) { for (bar of baz) {} } ``` @@ -47,7 +47,7 @@ Examples of **correct** code for this rule: ```js /*eslint no-param-reassign: "error"*/ -function foo(bar) { +var foo = function(bar) { var baz = bar; } ``` @@ -67,23 +67,23 @@ Examples of **correct** code for the default `{ "props": false }` option: ```js /*eslint no-param-reassign: ["error", { "props": false }]*/ -function foo(bar) { +var foo = function(bar) { bar.prop = "value"; } -function foo(bar) { +var foo = function(bar) { delete bar.aaa; } -function foo(bar) { +var foo = function(bar) { bar.aaa++; } -function foo(bar) { +var foo = function(bar) { for (bar.aaa in baz) {} } -function foo(bar) { +var foo = function(bar) { for (bar.aaa of baz) {} } ``` @@ -97,23 +97,23 @@ Examples of **incorrect** code for the `{ "props": true }` option: ```js /*eslint no-param-reassign: ["error", { "props": true }]*/ -function foo(bar) { +var foo = function(bar) { bar.prop = "value"; } -function foo(bar) { +var foo = function(bar) { delete bar.aaa; } -function foo(bar) { +var foo = function(bar) { bar.aaa++; } -function foo(bar) { +var foo = function(bar) { for (bar.aaa in baz) {} } -function foo(bar) { +var foo = function(bar) { for (bar.aaa of baz) {} } ``` @@ -127,23 +127,23 @@ Examples of **correct** code for the `{ "props": true }` option with `"ignorePro ```js /*eslint no-param-reassign: ["error", { "props": true, "ignorePropertyModificationsFor": ["bar"] }]*/ -function foo(bar) { +var foo = function(bar) { bar.prop = "value"; } -function foo(bar) { +var foo = function(bar) { delete bar.aaa; } -function foo(bar) { +var foo = function(bar) { bar.aaa++; } -function foo(bar) { +var foo = function(bar) { for (bar.aaa in baz) {} } -function foo(bar) { +var foo = function(bar) { for (bar.aaa of baz) {} } ``` @@ -157,23 +157,23 @@ Examples of **correct** code for the `{ "props": true }` option with `"ignorePro ```js /*eslint no-param-reassign: ["error", { "props": true, "ignorePropertyModificationsForRegex": ["^bar"] }]*/ -function foo(barVar) { +var foo = function(barVar) { barVar.prop = "value"; } -function foo(barrito) { +var foo = function(barrito) { delete barrito.aaa; } -function foo(bar_) { +var foo = function(bar_) { bar_.aaa++; } -function foo(barBaz) { +var foo = function(barBaz) { for (barBaz.aaa in baz) {} } -function foo(barBaz) { +var foo = function(barBaz) { for (barBaz.aaa of baz) {} } ``` diff --git a/docs/src/rules/no-plusplus.md b/docs/src/rules/no-plusplus.md index ba787f1968fd..4f564b51cb90 100644 --- a/docs/src/rules/no-plusplus.md +++ b/docs/src/rules/no-plusplus.md @@ -43,7 +43,7 @@ var bar = 42; bar--; for (i = 0; i < l; i++) { - return; + doSomething(i); } ``` @@ -63,7 +63,7 @@ var bar = 42; bar -= 1; for (i = 0; i < l; i += 1) { - return; + doSomething(i); } ``` diff --git a/docs/src/rules/no-restricted-syntax.md b/docs/src/rules/no-restricted-syntax.md index 25d419c149d7..a3f51cfef809 100644 --- a/docs/src/rules/no-restricted-syntax.md +++ b/docs/src/rules/no-restricted-syntax.md @@ -57,7 +57,7 @@ The string and object formats can be freely mixed in the configuration as needed Examples of **incorrect** code for this rule with the `"FunctionExpression", "WithStatement", BinaryExpression[operator='in']` options: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /* eslint no-restricted-syntax: ["error", "FunctionExpression", "WithStatement", "BinaryExpression[operator='in']"] */ @@ -75,7 +75,7 @@ foo in bar; Examples of **correct** code for this rule with the `"FunctionExpression", "WithStatement", BinaryExpression[operator='in']` options: -::: correct +::: correct { "sourceType": "script" } ```js /* eslint no-restricted-syntax: ["error", "FunctionExpression", "WithStatement", "BinaryExpression[operator='in']"] */ diff --git a/docs/src/rules/no-return-assign.md b/docs/src/rules/no-return-assign.md index 89277580f98e..b134fe8a1821 100644 --- a/docs/src/rules/no-return-assign.md +++ b/docs/src/rules/no-return-assign.md @@ -43,7 +43,7 @@ function doSomething() { return foo = bar + 2; } -function doSomething() { +function doSomethingElse() { return foo += 2; } @@ -51,7 +51,7 @@ const foo = (a, b) => a = b const bar = (a, b, c) => (a = b, c == b) -function doSomething() { +function doSomethingMore() { return foo = bar && foo > 0; } ``` @@ -69,11 +69,11 @@ function doSomething() { return foo == bar + 2; } -function doSomething() { +function doSomethingElse() { return foo === bar + 2; } -function doSomething() { +function doSomethingMore() { return (foo = bar + 2); } @@ -81,7 +81,7 @@ const foo = (a, b) => (a = b) const bar = (a, b, c) => ((a = b), c == b) -function doSomething() { +function doAnotherThing() { return (foo = bar) && foo > 0; } ``` @@ -104,11 +104,11 @@ function doSomething() { return foo = bar + 2; } -function doSomething() { +function doSomethingElse() { return foo += 2; } -function doSomething() { +function doSomethingMore() { return (foo = bar + 2); } ``` @@ -126,7 +126,7 @@ function doSomething() { return foo == bar + 2; } -function doSomething() { +function doSomethingElse() { return foo === bar + 2; } ``` diff --git a/docs/src/rules/no-return-await.md b/docs/src/rules/no-return-await.md index 80328c09d8d6..2ee5a8b3659c 100644 --- a/docs/src/rules/no-return-await.md +++ b/docs/src/rules/no-return-await.md @@ -38,23 +38,23 @@ Examples of **correct** code for this rule: ```js /*eslint no-return-await: "error"*/ -async function foo() { +async function foo1() { return bar(); } -async function foo() { +async function foo2() { await bar(); return; } // This is essentially the same as `return await bar();`, but the rule checks only `await` in `return` statements -async function foo() { +async function foo3() { const x = await bar(); return x; } // In this example the `await` is necessary to be able to catch errors thrown from `bar()` -async function foo() { +async function foo4() { try { return await bar(); } catch (error) {} diff --git a/docs/src/rules/no-sequences.md b/docs/src/rules/no-sequences.md index cb2b99c50e1f..811dae491968 100644 --- a/docs/src/rules/no-sequences.md +++ b/docs/src/rules/no-sequences.md @@ -25,7 +25,7 @@ This rule forbids the use of the comma operator, with the following exceptions: Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-sequences: "error"*/ @@ -51,7 +51,7 @@ with (doSomething(), val) {} Examples of **correct** code for this rule: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-sequences: "error"*/ @@ -119,7 +119,7 @@ This rule takes one option, an object, with the following properties: Examples of **incorrect** code for this rule with the `{ "allowInParentheses": false }` option: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-sequences: ["error", { "allowInParentheses": false }]*/ diff --git a/docs/src/rules/no-shadow-restricted-names.md b/docs/src/rules/no-shadow-restricted-names.md index 308772edaf36..7a5e1b0403d9 100644 --- a/docs/src/rules/no-shadow-restricted-names.md +++ b/docs/src/rules/no-shadow-restricted-names.md @@ -22,7 +22,7 @@ Then any code used within the same scope would not get the global `undefined`, b Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-shadow-restricted-names: "error"*/ @@ -40,7 +40,7 @@ try {} catch(eval){} Examples of **correct** code for this rule: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-shadow-restricted-names: "error"*/ diff --git a/docs/src/rules/no-shadow.md b/docs/src/rules/no-shadow.md index e4a081f0a309..9b9a27ee7d38 100644 --- a/docs/src/rules/no-shadow.md +++ b/docs/src/rules/no-shadow.md @@ -36,14 +36,14 @@ function b() { var a = 10; } -var b = function () { +var c = function () { var a = 10; } -function b(a) { +function d(a) { a = 10; } -b(a); +d(a); if (true) { let a = 5; diff --git a/docs/src/rules/no-throw-literal.md b/docs/src/rules/no-throw-literal.md index 621bb11a09c3..1ab939ff3a00 100644 --- a/docs/src/rules/no-throw-literal.md +++ b/docs/src/rules/no-throw-literal.md @@ -84,10 +84,10 @@ throw foo("error"); throw new String("error"); -var foo = { +var baz = { bar: "error" }; -throw foo.bar; +throw baz.bar; ``` ::: diff --git a/docs/src/rules/no-undefined.md b/docs/src/rules/no-undefined.md index 9b0be204b2f9..10096533a646 100644 --- a/docs/src/rules/no-undefined.md +++ b/docs/src/rules/no-undefined.md @@ -54,7 +54,7 @@ if (foo === undefined) { // ... } -function foo(undefined) { +function baz(undefined) { // ... } diff --git a/docs/src/rules/no-unsafe-optional-chaining.md b/docs/src/rules/no-unsafe-optional-chaining.md index e4cc6f831664..57ad6de87805 100644 --- a/docs/src/rules/no-unsafe-optional-chaining.md +++ b/docs/src/rules/no-unsafe-optional-chaining.md @@ -32,7 +32,7 @@ This rule aims to detect some cases where the use of optional chaining doesn't p Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-unsafe-optional-chaining: "error"*/ diff --git a/docs/src/rules/no-useless-escape.md b/docs/src/rules/no-useless-escape.md index cd28fda75a8f..ff173c229c2f 100644 --- a/docs/src/rules/no-useless-escape.md +++ b/docs/src/rules/no-useless-escape.md @@ -43,7 +43,7 @@ Examples of **incorrect** code for this rule: Examples of **correct** code for this rule: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-useless-escape: "error"*/ diff --git a/docs/src/rules/no-useless-return.md b/docs/src/rules/no-useless-return.md index 961f0aef5ca0..de7045347c2e 100644 --- a/docs/src/rules/no-useless-return.md +++ b/docs/src/rules/no-useless-return.md @@ -18,14 +18,14 @@ Examples of **incorrect** code for this rule: ```js /* eslint no-useless-return: "error" */ -function foo() { return; } +var foo = function() { return; } -function foo() { +var foo = function() { doSomething(); return; } -function foo() { +var foo = function() { if (condition) { bar(); return; @@ -34,7 +34,7 @@ function foo() { } } -function foo() { +var foo = function() { switch (bar) { case 1: doSomething(); @@ -55,13 +55,13 @@ Examples of **correct** code for this rule: ```js /* eslint no-useless-return: "error" */ -function foo() { return 5; } +var foo = function() { return 5; } -function foo() { +var foo = function() { return doSomething(); } -function foo() { +var foo = function() { if (condition) { bar(); return; @@ -71,7 +71,7 @@ function foo() { qux(); } -function foo() { +var foo = function() { switch (bar) { case 1: doSomething(); @@ -81,7 +81,7 @@ function foo() { } } -function foo() { +var foo = function() { for (const foo of bar) { return; } diff --git a/docs/src/rules/no-with.md b/docs/src/rules/no-with.md index 4fc3f75841d2..c4e880f812d8 100644 --- a/docs/src/rules/no-with.md +++ b/docs/src/rules/no-with.md @@ -17,7 +17,7 @@ If ESLint parses code in strict mode, the parser (instead of this rule) reports Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-with: "error"*/ @@ -31,7 +31,7 @@ with (point) { Examples of **correct** code for this rule: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-with: "error"*/ diff --git a/docs/src/rules/one-var.md b/docs/src/rules/one-var.md index d9a50ed86715..9c41bcd81b21 100644 --- a/docs/src/rules/one-var.md +++ b/docs/src/rules/one-var.md @@ -74,21 +74,21 @@ Examples of **incorrect** code for this rule with the default `"always"` option: ```js /*eslint one-var: ["error", "always"]*/ -function foo() { +function foo1() { var bar; var baz; let qux; let norf; } -function foo(){ +function foo2(){ const bar = false; const baz = true; let qux; let norf; } -function foo() { +function foo3() { var bar; if (baz) { @@ -125,21 +125,21 @@ Examples of **correct** code for this rule with the default `"always"` option: ```js /*eslint one-var: ["error", "always"]*/ -function foo() { +function foo1() { var bar, baz; let qux, norf; } -function foo(){ +function foo2(){ const bar = true, baz = false; let qux, norf; } -function foo() { +function foo3() { var bar, qux; @@ -148,7 +148,7 @@ function foo() { } } -function foo(){ +function foo4(){ let bar; if (baz) { @@ -230,12 +230,12 @@ Examples of **correct** code for this rule with the `"never"` option: ```js /*eslint one-var: ["error", "never"]*/ -function foo() { +function foo1() { var bar; var baz; } -function foo() { +function foo2() { var bar; if (baz) { @@ -243,7 +243,7 @@ function foo() { } } -function foo() { +function foo3() { let bar; if (baz) { @@ -277,12 +277,12 @@ Examples of **incorrect** code for this rule with the `"consecutive"` option: ```js /*eslint one-var: ["error", "consecutive"]*/ -function foo() { +function foo1() { var bar; var baz; } -function foo(){ +function foo2(){ var bar = 1; var baz = 2; @@ -311,12 +311,12 @@ Examples of **correct** code for this rule with the `"consecutive"` option: ```js /*eslint one-var: ["error", "consecutive"]*/ -function foo() { +function foo1() { var bar, baz; } -function foo(){ +function foo2(){ var bar = 1, baz = 2; @@ -349,14 +349,14 @@ Examples of **incorrect** code for this rule with the `{ var: "always", let: "ne /*eslint one-var: ["error", { var: "always", let: "never", const: "never" }]*/ /*eslint-env es6*/ -function foo() { +function foo1() { var bar; var baz; let qux, norf; } -function foo() { +function foo2() { const bar = 1, baz = 2; let qux, @@ -374,14 +374,14 @@ Examples of **correct** code for this rule with the `{ var: "always", let: "neve /*eslint one-var: ["error", { var: "always", let: "never", const: "never" }]*/ /*eslint-env es6*/ -function foo() { +function foo1() { var bar, baz; let qux; let norf; } -function foo() { +function foo2() { const bar = 1; const baz = 2; let qux; @@ -472,7 +472,7 @@ Examples of **incorrect** code for this rule with the `{ var: "never", let: "con /*eslint one-var: ["error", { var: "never", let: "consecutive", const: "consecutive" }]*/ /*eslint-env es6*/ -function foo() { +function foo1() { let a, b; let c; @@ -481,7 +481,7 @@ function foo() { e; } -function foo() { +function foo2() { const a = 1, b = 2; const c = 3; @@ -501,7 +501,7 @@ Examples of **correct** code for this rule with the `{ var: "never", let: "conse /*eslint one-var: ["error", { var: "never", let: "consecutive", const: "consecutive" }]*/ /*eslint-env es6*/ -function foo() { +function foo1() { let a, b; @@ -511,7 +511,7 @@ function foo() { let f; } -function foo() { +function foo2() { const a = 1, b = 2; diff --git a/docs/src/rules/padding-line-between-statements.md b/docs/src/rules/padding-line-between-statements.md index 1b16881c3eee..710a6116d636 100644 --- a/docs/src/rules/padding-line-between-statements.md +++ b/docs/src/rules/padding-line-between-statements.md @@ -120,13 +120,13 @@ Examples of **correct** code for the `[{ blankLine: "always", prev: "*", next: " { blankLine: "always", prev: "*", next: "return" } ]*/ -function foo() { +function foo1() { bar(); return; } -function foo() { +function foo2() { return; } ``` @@ -148,17 +148,17 @@ Examples of **incorrect** code for the `[{ blankLine: "always", prev: ["const", { blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"]} ]*/ -function foo() { +function foo1() { var a = 0; bar(); } -function foo() { +function foo2() { let a = 0; bar(); } -function foo() { +function foo3() { const a = 0; bar(); } @@ -184,21 +184,21 @@ Examples of **correct** code for the `[{ blankLine: "always", prev: ["const", "l { blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"]} ]*/ -function foo() { +function foo1() { var a = 0; var b = 0; bar(); } -function foo() { +function foo2() { let a = 0; const b = 0; bar(); } -function foo() { +function foo3() { const a = 0; const b = 0; diff --git a/docs/src/rules/prefer-reflect.md b/docs/src/rules/prefer-reflect.md index e2a632eb6c66..d6e3d87330a1 100644 --- a/docs/src/rules/prefer-reflect.md +++ b/docs/src/rules/prefer-reflect.md @@ -380,7 +380,7 @@ delete foo.bar; // deleting object property Examples of **correct** code for this rule when used without exceptions: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint prefer-reflect: "error"*/ @@ -395,7 +395,7 @@ Note: For a rule preventing deletion of variables, see [no-delete-var instead](n Examples of **correct** code for this rule with the `{ "exceptions": ["delete"] }` option: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint prefer-reflect: ["error", { "exceptions": ["delete"] }]*/ diff --git a/docs/src/rules/prefer-rest-params.md b/docs/src/rules/prefer-rest-params.md index cf234260290d..4406b5468dcd 100644 --- a/docs/src/rules/prefer-rest-params.md +++ b/docs/src/rules/prefer-rest-params.md @@ -19,7 +19,7 @@ This rule is aimed to flag usage of `arguments` variables. Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint prefer-rest-params: "error"*/ @@ -43,7 +43,7 @@ function foo(action) { Examples of **correct** code for this rule: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint prefer-rest-params: "error"*/ diff --git a/docs/src/rules/require-await.md b/docs/src/rules/require-await.md index a17eb972dcad..e0371dfb6ade 100644 --- a/docs/src/rules/require-await.md +++ b/docs/src/rules/require-await.md @@ -63,7 +63,7 @@ bar(async () => { await doSomething(); }); -function foo() { +function baz() { doSomething(); } diff --git a/docs/src/rules/require-jsdoc.md b/docs/src/rules/require-jsdoc.md index 20516fb01883..82ea4b432222 100644 --- a/docs/src/rules/require-jsdoc.md +++ b/docs/src/rules/require-jsdoc.md @@ -77,7 +77,7 @@ function foo() { return 10; } -var foo = () => { +var bar = () => { return 10; }; @@ -87,11 +87,11 @@ class Foo { } } -var foo = function() { +var bar = function() { return 10; }; -var foo = { +var bar = { bar: function() { return 10; }, @@ -131,21 +131,21 @@ function foo() { * @params {int} test - some number * @returns {int} sum of test and 10 */ -var foo = (test) => { +var bar = (test) => { return test + 10; } /** * It returns 10 */ -var foo = () => { +var bar = () => { return 10; } /** * It returns 10 */ -var foo = function() { +var bar = function() { return 10; } @@ -169,11 +169,11 @@ class Foo { /** * It returns 10 */ -var foo = function() { +var bar = function() { return 10; }; -var foo = { +var bar = { /** * It returns 10 */ diff --git a/docs/src/rules/require-yield.md b/docs/src/rules/require-yield.md index e4f975faa3e4..eec9d0cfc941 100644 --- a/docs/src/rules/require-yield.md +++ b/docs/src/rules/require-yield.md @@ -41,12 +41,12 @@ function* foo() { return 10; } -function foo() { +function bar() { return 10; } // This rule does not warn on empty generator functions. -function* foo() { } +function* baz() { } ``` ::: diff --git a/docs/src/rules/space-before-function-paren.md b/docs/src/rules/space-before-function-paren.md index 1948d903cf0e..a132105ac415 100644 --- a/docs/src/rules/space-before-function-paren.md +++ b/docs/src/rules/space-before-function-paren.md @@ -85,13 +85,13 @@ class Foo { } } -var foo = { +var baz = { bar() { // ... } }; -var foo = async() => 1 +var baz = async() => 1 ``` ::: @@ -122,13 +122,13 @@ class Foo { } } -var foo = { +var baz = { bar () { // ... } }; -var foo = async () => 1 +var baz = async () => 1 ``` ::: @@ -161,13 +161,13 @@ class Foo { } } -var foo = { +var baz = { bar () { // ... } }; -var foo = async () => 1 +var baz = async () => 1 ``` ::: @@ -198,13 +198,13 @@ class Foo { } } -var foo = { +var baz = { bar() { // ... } }; -var foo = async() => 1 +var baz = async() => 1 ``` ::: @@ -233,13 +233,13 @@ class Foo { } } -var foo = { +var baz = { bar () { // ... } }; -var foo = async(a) => await a +var baz = async(a) => await a ``` ::: @@ -266,13 +266,13 @@ class Foo { } } -var foo = { +var baz = { bar() { // ... } }; -var foo = async (a) => await a +var baz = async (a) => await a ``` ::: @@ -301,7 +301,7 @@ class Foo { } } -var foo = { +var baz = { bar() { // ... } @@ -332,7 +332,7 @@ class Foo { } } -var foo = { +var baz = { bar () { // ... } @@ -361,7 +361,7 @@ class Foo { } } -var foo = { +var baz = { bar() { // ... } @@ -396,7 +396,7 @@ class Foo { } } -var foo = { +var baz = { bar () { // ... } diff --git a/docs/src/rules/space-before-function-parentheses.md b/docs/src/rules/space-before-function-parentheses.md index a5c0eceb01fa..0b6b9b60d610 100644 --- a/docs/src/rules/space-before-function-parentheses.md +++ b/docs/src/rules/space-before-function-parentheses.md @@ -59,7 +59,7 @@ class Foo { } } -var foo = { +var baz = { bar() { // ... } @@ -93,7 +93,7 @@ class Foo { } } -var foo = { +var baz = { bar () { // ... } @@ -127,7 +127,7 @@ class Foo { } } -var foo = { +var baz = { bar () { // ... } @@ -161,7 +161,7 @@ class Foo { } } -var foo = { +var baz = { bar() { // ... } @@ -191,7 +191,7 @@ class Foo { } } -var foo = { +var baz = { bar () { // ... } @@ -221,7 +221,7 @@ class Foo { } } -var foo = { +var baz = { bar() { // ... } @@ -251,7 +251,7 @@ class Foo { } } -var foo = { +var baz = { bar() { // ... } @@ -281,7 +281,7 @@ class Foo { } } -var foo = { +var baz = { bar () { // ... } diff --git a/docs/src/rules/strict.md b/docs/src/rules/strict.md index c559b00710d4..731ddba2c20c 100644 --- a/docs/src/rules/strict.md +++ b/docs/src/rules/strict.md @@ -82,7 +82,7 @@ Otherwise the `"safe"` option corresponds to the `"function"` option. Note that Examples of **incorrect** code for this rule with the `"global"` option: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint strict: ["error", "global"]*/ @@ -93,7 +93,7 @@ function foo() { ::: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint strict: ["error", "global"]*/ @@ -105,7 +105,7 @@ function foo() { ::: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint strict: ["error", "global"]*/ @@ -121,7 +121,7 @@ function foo() { Examples of **correct** code for this rule with the `"global"` option: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint strict: ["error", "global"]*/ @@ -140,7 +140,7 @@ This option ensures that all function bodies are strict mode code, while global Examples of **incorrect** code for this rule with the `"function"` option: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint strict: ["error", "function"]*/ @@ -153,7 +153,7 @@ function foo() { ::: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint strict: ["error", "function"]*/ @@ -170,7 +170,7 @@ function foo() { ::: -::: incorrect +::: incorrect { "ecmaVersion": 6, "sourceType": "script" } ```js /*eslint strict: ["error", "function"]*/ @@ -192,7 +192,7 @@ function foo(a = 1) { Examples of **correct** code for this rule with the `"function"` option: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint strict: ["error", "function"]*/ @@ -225,7 +225,7 @@ var foo = (function() { Examples of **incorrect** code for this rule with the `"never"` option: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint strict: ["error", "never"]*/ @@ -238,7 +238,7 @@ function foo() { ::: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint strict: ["error", "never"]*/ @@ -252,7 +252,7 @@ function foo() { Examples of **correct** code for this rule with the `"never"` option: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint strict: ["error", "never"]*/ @@ -271,7 +271,7 @@ This option ensures that all functions are executed in strict mode. A strict mod Examples of **incorrect** code for this rule with the earlier default option which has been removed: -::: incorrect +::: incorrect { "sourceType": "script" } ```js // "strict": "error" @@ -282,7 +282,7 @@ function foo() { ::: -::: incorrect +::: incorrect { "sourceType": "script" } ```js // "strict": "error" @@ -298,7 +298,7 @@ function foo() { Examples of **correct** code for this rule with the earlier default option which has been removed: -::: correct +::: correct { "sourceType": "script" } ```js // "strict": "error" @@ -311,7 +311,7 @@ function foo() { ::: -::: correct +::: correct { "sourceType": "script" } ```js // "strict": "error" @@ -323,7 +323,7 @@ function foo() { ::: -::: correct +::: correct { "sourceType": "script" } ```js // "strict": "error" diff --git a/docs/src/rules/vars-on-top.md b/docs/src/rules/vars-on-top.md index 20d1440aee4a..5fdb2f160489 100644 --- a/docs/src/rules/vars-on-top.md +++ b/docs/src/rules/vars-on-top.md @@ -34,7 +34,7 @@ function doSomething() { } // Variable declaration in for initializer: -function doSomething() { +function doSomethingElse() { for (var i=0; i<10; i++) {} } ``` @@ -95,7 +95,7 @@ function doSomething() { } } -function doSomething() { +function doSomethingElse() { var i; for (i=0; i<10; i++) {} } diff --git a/docs/src/use/command-line-interface.md b/docs/src/use/command-line-interface.md index d88e35cfa04c..da4faf70ebae 100644 --- a/docs/src/use/command-line-interface.md +++ b/docs/src/use/command-line-interface.md @@ -110,6 +110,7 @@ Miscellaneous: --env-info Output execution environment information - default: false --no-error-on-unmatched-pattern Prevent errors when pattern is unmatched --exit-on-fatal-error Exit with exit code 2 in case of fatal error - default: false + --no-warn-ignored Suppress warnings when the file list includes ignored files. *Flat Config Mode Only* --debug Output debugging information -h, --help Show help -v, --version Output the version number @@ -703,6 +704,18 @@ This option causes ESLint to exit with exit code 2 if one or more fatal parsing npx eslint --exit-on-fatal-error file.js ``` +#### `--no-warn-ignored` + +**Flat Config Mode Only.** This option suppresses both `File ignored by default` and `File ignored because of a matching ignore pattern` warnings when an ignored filename is passed explicitly. It is useful when paired with `--max-warnings 0` as it will prevent exit code 1 due to the aforementioned warning. + +* **Argument Type**: No argument. + +##### `--no-warn-ignored` example + +```shell +npx eslint --no-warn-ignored --max-warnings 0 ignored-file.js +``` + #### `--debug` This option outputs debugging information to the console. Add this flag to an ESLint command line invocation in order to get extra debugging information while the command runs. diff --git a/docs/src/use/configure/ignore.md b/docs/src/use/configure/ignore.md index ffc23428ee02..16f1bfbcdc92 100644 --- a/docs/src/use/configure/ignore.md +++ b/docs/src/use/configure/ignore.md @@ -149,7 +149,7 @@ You'll see this warning: ```text foo.js - 0:0 warning File ignored because of a matching ignore pattern. Use "--no-ignore" to override. + 0:0 warning File ignored because of a matching ignore pattern. Use "--no-ignore" to disable file ignore settings or use "--no-warn-ignored" to suppress this warning. ✖ 1 problem (0 errors, 1 warning) ``` diff --git a/docs/src/use/formatters/html-formatter-example.html b/docs/src/use/formatters/html-formatter-example.html index 9545c0914c4b..2981958fab76 100644 --- a/docs/src/use/formatters/html-formatter-example.html +++ b/docs/src/use/formatters/html-formatter-example.html @@ -118,7 +118,7 @@

ESLint Report

- 9 problems (5 errors, 4 warnings) - Generated on Fri Sep 22 2023 17:03:30 GMT-0400 (Eastern Daylight Time) + 9 problems (5 errors, 4 warnings) - Generated on Fri Oct 06 2023 16:14:35 GMT-0400 (Eastern Daylight Time)
diff --git a/lib/cli.js b/lib/cli.js index a14930e9b0f2..807d28a0d1bc 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -91,7 +91,8 @@ async function translateOptions({ reportUnusedDisableDirectives, resolvePluginsRelativeTo, rule, - rulesdir + rulesdir, + warnIgnored }, configType) { let overrideConfig, overrideConfigFile; @@ -182,6 +183,7 @@ async function translateOptions({ if (configType === "flat") { options.ignorePatterns = ignorePattern; + options.warnIgnored = warnIgnored; } else { options.resolvePluginsRelativeTo = resolvePluginsRelativeTo; options.rulePaths = rulesdir; @@ -385,7 +387,9 @@ const cli = { if (useStdin) { results = await engine.lintText(text, { filePath: options.stdinFilename, - warnIgnored: true + + // flatConfig respects CLI flag and constructor warnIgnored, eslintrc forces true for backwards compatibility + warnIgnored: usingFlatConfig ? void 0 : true }); } else { results = await engine.lintFiles(files); diff --git a/lib/config/flat-config-schema.js b/lib/config/flat-config-schema.js index a79c02d663b5..df850995d87f 100644 --- a/lib/config/flat-config-schema.js +++ b/lib/config/flat-config-schema.js @@ -179,9 +179,7 @@ class InvalidRuleSeverityError extends Error { * @throws {InvalidRuleSeverityError} If the value isn't a valid rule severity. */ function assertIsRuleSeverity(ruleId, value) { - const severity = typeof value === "string" - ? ruleSeverities.get(value.toLowerCase()) - : ruleSeverities.get(value); + const severity = ruleSeverities.get(value); if (typeof severity === "undefined") { throw new InvalidRuleSeverityError(ruleId, value); diff --git a/lib/eslint/eslint-helpers.js b/lib/eslint/eslint-helpers.js index e25b10e8bc4e..72828363c3da 100644 --- a/lib/eslint/eslint-helpers.js +++ b/lib/eslint/eslint-helpers.js @@ -594,9 +594,9 @@ function createIgnoreResult(filePath, baseDir) { const isInNodeModules = baseDir && path.dirname(path.relative(baseDir, filePath)).split(path.sep).includes("node_modules"); if (isInNodeModules) { - message = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to override."; + message = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."; } else { - message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."; + message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."; } return { @@ -676,6 +676,7 @@ function processOptions({ overrideConfigFile = null, plugins = {}, reportUnusedDisableDirectives = null, // ← should be null by default because if it's a string then it overrides the 'reportUnusedDisableDirectives' setting in config files. And we cannot use `overrideConfig.reportUnusedDisableDirectives` instead because we cannot configure the `error` severity with that. + warnIgnored = true, ...unknownOptions }) { const errors = []; @@ -781,6 +782,9 @@ function processOptions({ ) { errors.push("'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null."); } + if (typeof warnIgnored !== "boolean") { + errors.push("'warnIgnored' must be a boolean."); + } if (errors.length > 0) { throw new ESLintInvalidOptionsError(errors); } @@ -802,7 +806,8 @@ function processOptions({ globInputPaths, ignore, ignorePatterns, - reportUnusedDisableDirectives + reportUnusedDisableDirectives, + warnIgnored }; } diff --git a/lib/eslint/flat-eslint.js b/lib/eslint/flat-eslint.js index 4ef386113616..306c80de1d65 100644 --- a/lib/eslint/flat-eslint.js +++ b/lib/eslint/flat-eslint.js @@ -84,6 +84,7 @@ const LintResultCache = require("../cli-engine/lint-result-cache"); * when a string. * @property {Record} [plugins] An array of plugin implementations. * @property {"error" | "warn" | "off"} [reportUnusedDisableDirectives] the severity to report unused eslint-disable directives. + * @property {boolean} warnIgnored Show warnings when the file list includes ignored files */ //------------------------------------------------------------------------------ @@ -749,7 +750,8 @@ class FlatESLint { fixTypes, reportUnusedDisableDirectives, globInputPaths, - errorOnUnmatchedPattern + errorOnUnmatchedPattern, + warnIgnored } = eslintOptions; const startTime = Date.now(); const fixTypesSet = fixTypes ? new Set(fixTypes) : null; @@ -795,7 +797,11 @@ class FlatESLint { * pattern, then notify the user. */ if (ignored) { - return createIgnoreResult(filePath, cwd); + if (warnIgnored) { + return createIgnoreResult(filePath, cwd); + } + + return void 0; } const config = configs.getConfig(filePath); @@ -908,7 +914,7 @@ class FlatESLint { const { filePath, - warnIgnored = false, + warnIgnored, ...unknownOptions } = options || {}; @@ -922,7 +928,7 @@ class FlatESLint { throw new Error("'options.filePath' must be a non-empty string or undefined"); } - if (typeof warnIgnored !== "boolean") { + if (typeof warnIgnored !== "boolean" && typeof warnIgnored !== "undefined") { throw new Error("'options.warnIgnored' must be a boolean or undefined"); } @@ -937,7 +943,8 @@ class FlatESLint { allowInlineConfig, cwd, fix, - reportUnusedDisableDirectives + reportUnusedDisableDirectives, + warnIgnored: constructorWarnIgnored } = eslintOptions; const results = []; const startTime = Date.now(); @@ -945,7 +952,9 @@ class FlatESLint { // Clear the last used config arrays. if (resolvedFilename && await this.isPathIgnored(resolvedFilename)) { - if (warnIgnored) { + const shouldWarnIgnored = typeof warnIgnored === "boolean" ? warnIgnored : constructorWarnIgnored; + + if (shouldWarnIgnored) { results.push(createIgnoreResult(resolvedFilename, cwd)); } } else { diff --git a/lib/linter/apply-disable-directives.js b/lib/linter/apply-disable-directives.js index 13ced990ff48..55f7683f3f53 100644 --- a/lib/linter/apply-disable-directives.js +++ b/lib/linter/apply-disable-directives.js @@ -87,7 +87,7 @@ function createIndividualDirectivesRemoval(directives, commentToken) { return directives.map(directive => { const { ruleId } = directive; - const regex = new RegExp(String.raw`(?:^|\s*,\s*)${escapeRegExp(ruleId)}(?:\s*,\s*|$)`, "u"); + const regex = new RegExp(String.raw`(?:^|\s*,\s*)(?['"]?)${escapeRegExp(ruleId)}\k(?:\s*,\s*|$)`, "u"); const match = regex.exec(listText); const matchedText = match[0]; const matchStartOffset = listStartOffset + match.index; diff --git a/lib/linter/code-path-analysis/code-path-state.js b/lib/linter/code-path-analysis/code-path-state.js index d187297d32b0..2b0dc2bfca04 100644 --- a/lib/linter/code-path-analysis/code-path-state.js +++ b/lib/linter/code-path-analysis/code-path-state.js @@ -12,13 +12,622 @@ const CodePathSegment = require("./code-path-segment"), ForkContext = require("./fork-context"); +//----------------------------------------------------------------------------- +// Contexts +//----------------------------------------------------------------------------- + +/** + * Represents the context in which a `break` statement can be used. + * + * A `break` statement without a label is only valid in a few places in + * JavaScript: any type of loop or a `switch` statement. Otherwise, `break` + * without a label causes a syntax error. For these contexts, `breakable` is + * set to `true` to indicate that a `break` without a label is valid. + * + * However, a `break` statement with a label is also valid inside of a labeled + * statement. For example, this is valid: + * + * a : { + * break a; + * } + * + * The `breakable` property is set false for labeled statements to indicate + * that `break` without a label is invalid. + */ +class BreakContext { + + /** + * Creates a new instance. + * @param {BreakContext} upperContext The previous `BreakContext`. + * @param {boolean} breakable Indicates if we are inside a statement where + * `break` without a label will exit the statement. + * @param {string|null} label The label for the statement. + * @param {ForkContext} forkContext The current fork context. + */ + constructor(upperContext, breakable, label, forkContext) { + + /** + * The previous `BreakContext` + * @type {BreakContext} + */ + this.upper = upperContext; + + /** + * Indicates if we are inside a statement where `break` without a label + * will exit the statement. + * @type {boolean} + */ + this.breakable = breakable; + + /** + * The label associated with the statement. + * @type {string|null} + */ + this.label = label; + + /** + * The fork context for the `break`. + * @type {ForkContext} + */ + this.brokenForkContext = ForkContext.newEmpty(forkContext); + } +} + +/** + * Represents the context for `ChainExpression` nodes. + */ +class ChainContext { + + /** + * Creates a new instance. + * @param {ChainContext} upperContext The previous `ChainContext`. + */ + constructor(upperContext) { + + /** + * The previous `ChainContext` + * @type {ChainContext} + */ + this.upper = upperContext; + + /** + * The number of choice contexts inside of the `ChainContext`. + * @type {number} + */ + this.choiceContextCount = 0; + + } +} + +/** + * Represents a choice in the code path. + * + * Choices are created by logical operators such as `&&`, loops, conditionals, + * and `if` statements. This is the point at which the code path has a choice of + * which direction to go. + * + * The result of a choice might be in the left (test) expression of another choice, + * and in that case, may create a new fork. For example, `a || b` is a choice + * but does not create a new fork because the result of the expression is + * not used as the test expression in another expression. In this case, + * `isForkingAsResult` is false. In the expression `a || b || c`, the `a || b` + * expression appears as the test expression for `|| c`, so the + * result of `a || b` creates a fork because execution may or may not + * continue to `|| c`. `isForkingAsResult` for `a || b` in this case is true + * while `isForkingAsResult` for `|| c` is false. (`isForkingAsResult` is always + * false for `if` statements, conditional expressions, and loops.) + * + * All of the choices except one (`??`) operate on a true/false fork, meaning if + * true go one way and if false go the other (tracked by `trueForkContext` and + * `falseForkContext`). The `??` operator doesn't operate on true/false because + * the left expression is evaluated to be nullish or not, so only if nullish do + * we fork to the right expression (tracked by `nullishForkContext`). + */ +class ChoiceContext { + + /** + * Creates a new instance. + * @param {ChoiceContext} upperContext The previous `ChoiceContext`. + * @param {string} kind The kind of choice. If it's a logical or assignment expression, this + * is `"&&"` or `"||"` or `"??"`; if it's an `if` statement or + * conditional expression, this is `"test"`; otherwise, this is `"loop"`. + * @param {boolean} isForkingAsResult Indicates if the result of the choice + * creates a fork. + * @param {ForkContext} forkContext The containing `ForkContext`. + */ + constructor(upperContext, kind, isForkingAsResult, forkContext) { + + /** + * The previous `ChoiceContext` + * @type {ChoiceContext} + */ + this.upper = upperContext; + + /** + * The kind of choice. If it's a logical or assignment expression, this + * is `"&&"` or `"||"` or `"??"`; if it's an `if` statement or + * conditional expression, this is `"test"`; otherwise, this is `"loop"`. + * @type {string} + */ + this.kind = kind; + + /** + * Indicates if the result of the choice forks the code path. + * @type {boolean} + */ + this.isForkingAsResult = isForkingAsResult; + + /** + * The fork context for the `true` path of the choice. + * @type {ForkContext} + */ + this.trueForkContext = ForkContext.newEmpty(forkContext); + + /** + * The fork context for the `false` path of the choice. + * @type {ForkContext} + */ + this.falseForkContext = ForkContext.newEmpty(forkContext); + + /** + * The fork context for when the choice result is `null` or `undefined`. + * @type {ForkContext} + */ + this.nullishForkContext = ForkContext.newEmpty(forkContext); + + /** + * Indicates if any of `trueForkContext`, `falseForkContext`, or + * `nullishForkContext` have been updated with segments from a child context. + * @type {boolean} + */ + this.processed = false; + } + +} + +/** + * Base class for all loop contexts. + */ +class LoopContextBase { + + /** + * Creates a new instance. + * @param {LoopContext|null} upperContext The previous `LoopContext`. + * @param {string} type The AST node's `type` for the loop. + * @param {string|null} label The label for the loop from an enclosing `LabeledStatement`. + * @param {BreakContext} breakContext The context for breaking the loop. + */ + constructor(upperContext, type, label, breakContext) { + + /** + * The previous `LoopContext`. + * @type {LoopContext} + */ + this.upper = upperContext; + + /** + * The AST node's `type` for the loop. + * @type {string} + */ + this.type = type; + + /** + * The label for the loop from an enclosing `LabeledStatement`. + * @type {string|null} + */ + this.label = label; + + /** + * The fork context for when `break` is encountered. + * @type {ForkContext} + */ + this.brokenForkContext = breakContext.brokenForkContext; + } +} + +/** + * Represents the context for a `while` loop. + */ +class WhileLoopContext extends LoopContextBase { + + /** + * Creates a new instance. + * @param {LoopContext|null} upperContext The previous `LoopContext`. + * @param {string|null} label The label for the loop from an enclosing `LabeledStatement`. + * @param {BreakContext} breakContext The context for breaking the loop. + */ + constructor(upperContext, label, breakContext) { + super(upperContext, "WhileStatement", label, breakContext); + + /** + * The hardcoded literal boolean test condition for + * the loop. Used to catch infinite or skipped loops. + * @type {boolean|undefined} + */ + this.test = void 0; + + /** + * The segments representing the test condition where `continue` will + * jump to. The test condition will typically have just one segment but + * it's possible for there to be more than one. + * @type {Array|null} + */ + this.continueDestSegments = null; + } +} + +/** + * Represents the context for a `do-while` loop. + */ +class DoWhileLoopContext extends LoopContextBase { + + /** + * Creates a new instance. + * @param {LoopContext|null} upperContext The previous `LoopContext`. + * @param {string|null} label The label for the loop from an enclosing `LabeledStatement`. + * @param {BreakContext} breakContext The context for breaking the loop. + * @param {ForkContext} forkContext The enclosing fork context. + */ + constructor(upperContext, label, breakContext, forkContext) { + super(upperContext, "DoWhileStatement", label, breakContext); + + /** + * The hardcoded literal boolean test condition for + * the loop. Used to catch infinite or skipped loops. + * @type {boolean|undefined} + */ + this.test = void 0; + + /** + * The segments at the start of the loop body. This is the only loop + * where the test comes at the end, so the first iteration always + * happens and we need a reference to the first statements. + * @type {Array|null} + */ + this.entrySegments = null; + + /** + * The fork context to follow when a `continue` is found. + * @type {ForkContext} + */ + this.continueForkContext = ForkContext.newEmpty(forkContext); + } +} + +/** + * Represents the context for a `for` loop. + */ +class ForLoopContext extends LoopContextBase { + + /** + * Creates a new instance. + * @param {LoopContext|null} upperContext The previous `LoopContext`. + * @param {string|null} label The label for the loop from an enclosing `LabeledStatement`. + * @param {BreakContext} breakContext The context for breaking the loop. + */ + constructor(upperContext, label, breakContext) { + super(upperContext, "ForStatement", label, breakContext); + + /** + * The hardcoded literal boolean test condition for + * the loop. Used to catch infinite or skipped loops. + * @type {boolean|undefined} + */ + this.test = void 0; + + /** + * The end of the init expression. This may change during the lifetime + * of the instance as we traverse the loop because some loops don't have + * an init expression. + * @type {Array|null} + */ + this.endOfInitSegments = null; + + /** + * The start of the test expression. This may change during the lifetime + * of the instance as we traverse the loop because some loops don't have + * a test expression. + * @type {Array|null} + */ + this.testSegments = null; + + /** + * The end of the test expression. This may change during the lifetime + * of the instance as we traverse the loop because some loops don't have + * a test expression. + * @type {Array|null} + */ + this.endOfTestSegments = null; + + /** + * The start of the update expression. This may change during the lifetime + * of the instance as we traverse the loop because some loops don't have + * an update expression. + * @type {Array|null} + */ + this.updateSegments = null; + + /** + * The end of the update expresion. This may change during the lifetime + * of the instance as we traverse the loop because some loops don't have + * an update expression. + * @type {Array|null} + */ + this.endOfUpdateSegments = null; + + /** + * The segments representing the test condition where `continue` will + * jump to. The test condition will typically have just one segment but + * it's possible for there to be more than one. This may change during the + * lifetime of the instance as we traverse the loop because some loops + * don't have an update expression. When there is an update expression, this + * will end up pointing to that expression; otherwise it will end up pointing + * to the test expression. + * @type {Array|null} + */ + this.continueDestSegments = null; + } +} + +/** + * Represents the context for a `for-in` loop. + * + * Terminology: + * - "left" means the part of the loop to the left of the `in` keyword. For + * example, in `for (var x in y)`, the left is `var x`. + * - "right" means the part of the loop to the right of the `in` keyword. For + * example, in `for (var x in y)`, the right is `y`. + */ +class ForInLoopContext extends LoopContextBase { + + /** + * Creates a new instance. + * @param {LoopContext|null} upperContext The previous `LoopContext`. + * @param {string|null} label The label for the loop from an enclosing `LabeledStatement`. + * @param {BreakContext} breakContext The context for breaking the loop. + */ + constructor(upperContext, label, breakContext) { + super(upperContext, "ForInStatement", label, breakContext); + + /** + * The segments that came immediately before the start of the loop. + * This allows you to traverse backwards out of the loop into the + * surrounding code. This is necessary to evaluate the right expression + * correctly, as it must be evaluated in the same way as the left + * expression, but the pointer to these segments would otherwise be + * lost if not stored on the instance. Once the right expression has + * been evaluated, this property is no longer used. + * @type {Array|null} + */ + this.prevSegments = null; + + /** + * Segments representing the start of everything to the left of the + * `in` keyword. This can be used to move forward towards + * `endOfLeftSegments`. `leftSegments` and `endOfLeftSegments` are + * effectively the head and tail of a doubly-linked list. + * @type {Array|null} + */ + this.leftSegments = null; + + /** + * Segments representing the end of everything to the left of the + * `in` keyword. This can be used to move backward towards `leftSegments`. + * `leftSegments` and `endOfLeftSegments` are effectively the head + * and tail of a doubly-linked list. + * @type {Array|null} + */ + this.endOfLeftSegments = null; + + /** + * The segments representing the left expression where `continue` will + * jump to. In `for-in` loops, `continue` must always re-execute the + * left expression each time through the loop. This contains the same + * segments as `leftSegments`, but is duplicated here so each loop + * context has the same property pointing to where `continue` should + * end up. + * @type {Array|null} + */ + this.continueDestSegments = null; + } +} + +/** + * Represents the context for a `for-of` loop. + */ +class ForOfLoopContext extends LoopContextBase { + + /** + * Creates a new instance. + * @param {LoopContext|null} upperContext The previous `LoopContext`. + * @param {string|null} label The label for the loop from an enclosing `LabeledStatement`. + * @param {BreakContext} breakContext The context for breaking the loop. + */ + constructor(upperContext, label, breakContext) { + super(upperContext, "ForOfStatement", label, breakContext); + + /** + * The segments that came immediately before the start of the loop. + * This allows you to traverse backwards out of the loop into the + * surrounding code. This is necessary to evaluate the right expression + * correctly, as it must be evaluated in the same way as the left + * expression, but the pointer to these segments would otherwise be + * lost if not stored on the instance. Once the right expression has + * been evaluated, this property is no longer used. + * @type {Array|null} + */ + this.prevSegments = null; + + /** + * Segments representing the start of everything to the left of the + * `of` keyword. This can be used to move forward towards + * `endOfLeftSegments`. `leftSegments` and `endOfLeftSegments` are + * effectively the head and tail of a doubly-linked list. + * @type {Array|null} + */ + this.leftSegments = null; + + /** + * Segments representing the end of everything to the left of the + * `of` keyword. This can be used to move backward towards `leftSegments`. + * `leftSegments` and `endOfLeftSegments` are effectively the head + * and tail of a doubly-linked list. + * @type {Array|null} + */ + this.endOfLeftSegments = null; + + /** + * The segments representing the left expression where `continue` will + * jump to. In `for-in` loops, `continue` must always re-execute the + * left expression each time through the loop. This contains the same + * segments as `leftSegments`, but is duplicated here so each loop + * context has the same property pointing to where `continue` should + * end up. + * @type {Array|null} + */ + this.continueDestSegments = null; + } +} + +/** + * Represents the context for any loop. + * @typedef {WhileLoopContext|DoWhileLoopContext|ForLoopContext|ForInLoopContext|ForOfLoopContext} LoopContext + */ + +/** + * Represents the context for a `switch` statement. + */ +class SwitchContext { + + /** + * Creates a new instance. + * @param {SwitchContext} upperContext The previous context. + * @param {boolean} hasCase Indicates if there is at least one `case` statement. + * `default` doesn't count. + */ + constructor(upperContext, hasCase) { + + /** + * The previous context. + * @type {SwitchContext} + */ + this.upper = upperContext; + + /** + * Indicates if there is at least one `case` statement. `default` doesn't count. + * @type {boolean} + */ + this.hasCase = hasCase; + + /** + * The `default` keyword. + * @type {Array|null} + */ + this.defaultSegments = null; + + /** + * The default case body starting segments. + * @type {Array|null} + */ + this.defaultBodySegments = null; + + /** + * Indicates if a `default` case and is empty exists. + * @type {boolean} + */ + this.foundEmptyDefault = false; + + /** + * Indicates that a `default` exists and is the last case. + * @type {boolean} + */ + this.lastIsDefault = false; + + /** + * The number of fork contexts created. This is equivalent to the + * number of `case` statements plus a `default` statement (if present). + * @type {number} + */ + this.forkCount = 0; + } +} + +/** + * Represents the context for a `try` statement. + */ +class TryContext { + + /** + * Creates a new instance. + * @param {TryContext} upperContext The previous context. + * @param {boolean} hasFinalizer Indicates if the `try` statement has a + * `finally` block. + * @param {ForkContext} forkContext The enclosing fork context. + */ + constructor(upperContext, hasFinalizer, forkContext) { + + /** + * The previous context. + * @type {TryContext} + */ + this.upper = upperContext; + + /** + * Indicates if the `try` statement has a `finally` block. + * @type {boolean} + */ + this.hasFinalizer = hasFinalizer; + + /** + * Tracks the traversal position inside of the `try` statement. This is + * used to help determine the context necessary to create paths because + * a `try` statement may or may not have `catch` or `finally` blocks, + * and code paths behave differently in those blocks. + * @type {"try"|"catch"|"finally"} + */ + this.position = "try"; + + /** + * If the `try` statement has a `finally` block, this affects how a + * `return` statement behaves in the `try` block. Without `finally`, + * `return` behaves as usual and doesn't require a fork; with `finally`, + * `return` forks into the `finally` block, so we need a fork context + * to track it. + * @type {ForkContext|null} + */ + this.returnedForkContext = hasFinalizer + ? ForkContext.newEmpty(forkContext) + : null; + + /** + * When a `throw` occurs inside of a `try` block, the code path forks + * into the `catch` or `finally` blocks, and this fork context tracks + * that path. + * @type {ForkContext} + */ + this.thrownForkContext = ForkContext.newEmpty(forkContext); + + /** + * Indicates if the last segment in the `try` block is reachable. + * @type {boolean} + */ + this.lastOfTryIsReachable = false; + + /** + * Indicates if the last segment in the `catch` block is reachable. + * @type {boolean} + */ + this.lastOfCatchIsReachable = false; + } +} + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ /** * Adds given segments into the `dest` array. - * If the `others` array does not includes the given segments, adds to the `all` + * If the `others` array does not include the given segments, adds to the `all` * array as well. * * This adds only reachable and used segments. @@ -40,9 +649,9 @@ function addToReturnedOrThrown(dest, others, all, segments) { } /** - * Gets a loop-context for a `continue` statement. - * @param {CodePathState} state A state to get. - * @param {string} label The label of a `continue` statement. + * Gets a loop context for a `continue` statement based on a given label. + * @param {CodePathState} state The state to search within. + * @param {string|null} label The label of a `continue` statement. * @returns {LoopContext} A loop-context for a `continue` statement. */ function getContinueContext(state, label) { @@ -65,9 +674,9 @@ function getContinueContext(state, label) { /** * Gets a context for a `break` statement. - * @param {CodePathState} state A state to get. - * @param {string} label The label of a `break` statement. - * @returns {LoopContext|SwitchContext} A context for a `break` statement. + * @param {CodePathState} state The state to search within. + * @param {string|null} label The label of a `break` statement. + * @returns {BreakContext} A context for a `break` statement. */ function getBreakContext(state, label) { let context = state.breakContext; @@ -84,8 +693,10 @@ function getBreakContext(state, label) { } /** - * Gets a context for a `return` statement. - * @param {CodePathState} state A state to get. + * Gets a context for a `return` statement. There is just one special case: + * if there is a `try` statement with a `finally` block, because that alters + * how `return` behaves; otherwise, this just passes through the given state. + * @param {CodePathState} state The state to search within * @returns {TryContext|CodePathState} A context for a `return` statement. */ function getReturnContext(state) { @@ -102,8 +713,11 @@ function getReturnContext(state) { } /** - * Gets a context for a `throw` statement. - * @param {CodePathState} state A state to get. + * Gets a context for a `throw` statement. There is just one special case: + * if there is a `try` statement with a `finally` block and we are inside of + * a `catch` because that changes how `throw` behaves; otherwise, this just + * passes through the given state. + * @param {CodePathState} state The state to search within. * @returns {TryContext|CodePathState} A context for a `throw` statement. */ function getThrowContext(state) { @@ -122,13 +736,13 @@ function getThrowContext(state) { } /** - * Removes a given element from a given array. - * @param {any[]} xs An array to remove the specific element. - * @param {any} x An element to be removed. + * Removes a given value from a given array. + * @param {any[]} elements An array to remove the specific element. + * @param {any} value The value to be removed. * @returns {void} */ -function remove(xs, x) { - xs.splice(xs.indexOf(x), 1); +function removeFromArray(elements, value) { + elements.splice(elements.indexOf(value), 1); } /** @@ -141,48 +755,77 @@ function remove(xs, x) { * @param {CodePathSegment[]} nextSegments Backward segments to disconnect. * @returns {void} */ -function removeConnection(prevSegments, nextSegments) { +function disconnectSegments(prevSegments, nextSegments) { for (let i = 0; i < prevSegments.length; ++i) { const prevSegment = prevSegments[i]; const nextSegment = nextSegments[i]; - remove(prevSegment.nextSegments, nextSegment); - remove(prevSegment.allNextSegments, nextSegment); - remove(nextSegment.prevSegments, prevSegment); - remove(nextSegment.allPrevSegments, prevSegment); + removeFromArray(prevSegment.nextSegments, nextSegment); + removeFromArray(prevSegment.allNextSegments, nextSegment); + removeFromArray(nextSegment.prevSegments, prevSegment); + removeFromArray(nextSegment.allPrevSegments, prevSegment); } } /** - * Creates looping path. - * @param {CodePathState} state The instance. + * Creates looping path between two arrays of segments, ensuring that there are + * paths going between matching segments in the arrays. + * @param {CodePathState} state The state to operate on. * @param {CodePathSegment[]} unflattenedFromSegments Segments which are source. * @param {CodePathSegment[]} unflattenedToSegments Segments which are destination. * @returns {void} */ function makeLooped(state, unflattenedFromSegments, unflattenedToSegments) { + const fromSegments = CodePathSegment.flattenUnusedSegments(unflattenedFromSegments); const toSegments = CodePathSegment.flattenUnusedSegments(unflattenedToSegments); - const end = Math.min(fromSegments.length, toSegments.length); + /* + * This loop effectively updates a doubly-linked list between two collections + * of segments making sure that segments in the same array indices are + * combined to create a path. + */ for (let i = 0; i < end; ++i) { + + // get the segments in matching array indices const fromSegment = fromSegments[i]; const toSegment = toSegments[i]; + /* + * If the destination segment is reachable, then create a path from the + * source segment to the destination segment. + */ if (toSegment.reachable) { fromSegment.nextSegments.push(toSegment); } + + /* + * If the source segment is reachable, then create a path from the + * destination segment back to the source segment. + */ if (fromSegment.reachable) { toSegment.prevSegments.push(fromSegment); } + + /* + * Also update the arrays that don't care if the segments are reachable + * or not. This should always happen regardless of anything else. + */ fromSegment.allNextSegments.push(toSegment); toSegment.allPrevSegments.push(fromSegment); + /* + * If the destination segment has at least two previous segments in its + * path then that means there was one previous segment before this iteration + * of the loop was executed. So, we need to mark the source segment as + * looped. + */ if (toSegment.allPrevSegments.length >= 2) { CodePathSegment.markPrevSegmentAsLooped(toSegment, fromSegment); } + // let the code path analyzer know that there's been a loop created state.notifyLooped(fromSegment, toSegment); } } @@ -198,15 +841,27 @@ function makeLooped(state, unflattenedFromSegments, unflattenedToSegments) { * @returns {void} */ function finalizeTestSegmentsOfFor(context, choiceContext, head) { + + /* + * If this choice context doesn't already contain paths from a + * child context, then add the current head to each potential path. + */ if (!choiceContext.processed) { choiceContext.trueForkContext.add(head); choiceContext.falseForkContext.add(head); - choiceContext.qqForkContext.add(head); + choiceContext.nullishForkContext.add(head); } + /* + * If the test condition isn't a hardcoded truthy value, then `break` + * must follow the same path as if the test condition is false. To represent + * that, we append the path for when the loop test is false (represented by + * `falseForkContext`) to the `brokenForkContext`. + */ if (context.test !== true) { context.brokenForkContext.addAll(choiceContext.falseForkContext); } + context.endOfTestSegments = choiceContext.trueForkContext.makeNext(0, -1); } @@ -220,35 +875,124 @@ function finalizeTestSegmentsOfFor(context, choiceContext, head) { class CodePathState { /** + * Creates a new instance. * @param {IdGenerator} idGenerator An id generator to generate id for code * path segments. * @param {Function} onLooped A callback function to notify looping. */ constructor(idGenerator, onLooped) { + + /** + * The ID generator to use when creating new segments. + * @type {IdGenerator} + */ this.idGenerator = idGenerator; + + /** + * A callback function to call when there is a loop. + * @type {Function} + */ this.notifyLooped = onLooped; + + /** + * The root fork context for this state. + * @type {ForkContext} + */ this.forkContext = ForkContext.newRoot(idGenerator); + + /** + * Context for logical expressions, conditional expressions, `if` statements, + * and loops. + * @type {ChoiceContext} + */ this.choiceContext = null; + + /** + * Context for `switch` statements. + * @type {SwitchContext} + */ this.switchContext = null; + + /** + * Context for `try` statements. + * @type {TryContext} + */ this.tryContext = null; + + /** + * Context for loop statements. + * @type {LoopContext} + */ this.loopContext = null; + + /** + * Context for `break` statements. + * @type {BreakContext} + */ this.breakContext = null; + + /** + * Context for `ChainExpression` nodes. + * @type {ChainContext} + */ this.chainContext = null; + /** + * An array that tracks the current segments in the state. The array + * starts empty and segments are added with each `onCodePathSegmentStart` + * event and removed with each `onCodePathSegmentEnd` event. Effectively, + * this is tracking the code path segment traversal as the state is + * modified. + * @type {Array} + */ this.currentSegments = []; + + /** + * Tracks the starting segment for this path. This value never changes. + * @type {CodePathSegment} + */ this.initialSegment = this.forkContext.head[0]; - // returnedSegments and thrownSegments push elements into finalSegments also. - const final = this.finalSegments = []; - const returned = this.returnedForkContext = []; - const thrown = this.thrownForkContext = []; + /** + * The final segments of the code path which are either `return` or `throw`. + * This is a union of the segments in `returnedForkContext` and `thrownForkContext`. + * @type {Array} + */ + this.finalSegments = []; + + /** + * The final segments of the code path which are `return`. These + * segments are also contained in `finalSegments`. + * @type {Array} + */ + this.returnedForkContext = []; + + /** + * The final segments of the code path which are `throw`. These + * segments are also contained in `finalSegments`. + * @type {Array} + */ + this.thrownForkContext = []; + + /* + * We add an `add` method so that these look more like fork contexts and + * can be used interchangeably when a fork context is needed to add more + * segments to a path. + * + * Ultimately, we want anything added to `returned` or `thrown` to also + * be added to `final`. We only add reachable and used segments to these + * arrays. + */ + const final = this.finalSegments; + const returned = this.returnedForkContext; + const thrown = this.thrownForkContext; returned.add = addToReturnedOrThrown.bind(null, returned, thrown, final); thrown.add = addToReturnedOrThrown.bind(null, thrown, returned, final); } /** - * The head segments. + * A passthrough property exposing the current pointer as part of the API. * @type {CodePathSegment[]} */ get headSegments() { @@ -341,77 +1085,72 @@ class CodePathState { * If the new context is LogicalExpression's or AssignmentExpression's, this is `"&&"` or `"||"` or `"??"`. * If it's IfStatement's or ConditionalExpression's, this is `"test"`. * Otherwise, this is `"loop"`. - * @param {boolean} isForkingAsResult A flag that shows that goes different - * paths between `true` and `false`. + * @param {boolean} isForkingAsResult Indicates if the result of the choice + * creates a fork. * @returns {void} */ pushChoiceContext(kind, isForkingAsResult) { - this.choiceContext = { - upper: this.choiceContext, - kind, - isForkingAsResult, - trueForkContext: ForkContext.newEmpty(this.forkContext), - falseForkContext: ForkContext.newEmpty(this.forkContext), - qqForkContext: ForkContext.newEmpty(this.forkContext), - processed: false - }; + this.choiceContext = new ChoiceContext(this.choiceContext, kind, isForkingAsResult, this.forkContext); } /** * Pops the last choice context and finalizes it. + * This is called upon leaving a node that represents a choice. * @throws {Error} (Unreachable.) * @returns {ChoiceContext} The popped context. */ popChoiceContext() { - const context = this.choiceContext; - - this.choiceContext = context.upper; - + const poppedChoiceContext = this.choiceContext; const forkContext = this.forkContext; - const headSegments = forkContext.head; + const head = forkContext.head; + + this.choiceContext = poppedChoiceContext.upper; - switch (context.kind) { + switch (poppedChoiceContext.kind) { case "&&": case "||": case "??": /* - * If any result were not transferred from child contexts, - * this sets the head segments to both cases. - * The head segments are the path of the right-hand operand. + * The `head` are the path of the right-hand operand. + * If we haven't previously added segments from child contexts, + * then we add these segments to all possible forks. */ - if (!context.processed) { - context.trueForkContext.add(headSegments); - context.falseForkContext.add(headSegments); - context.qqForkContext.add(headSegments); + if (!poppedChoiceContext.processed) { + poppedChoiceContext.trueForkContext.add(head); + poppedChoiceContext.falseForkContext.add(head); + poppedChoiceContext.nullishForkContext.add(head); } /* - * Transfers results to upper context if this context is in - * test chunk. + * If this context is the left (test) expression for another choice + * context, such as `a || b` in the expression `a || b || c`, + * then we take the segments for this context and move them up + * to the parent context. */ - if (context.isForkingAsResult) { + if (poppedChoiceContext.isForkingAsResult) { const parentContext = this.choiceContext; - parentContext.trueForkContext.addAll(context.trueForkContext); - parentContext.falseForkContext.addAll(context.falseForkContext); - parentContext.qqForkContext.addAll(context.qqForkContext); + parentContext.trueForkContext.addAll(poppedChoiceContext.trueForkContext); + parentContext.falseForkContext.addAll(poppedChoiceContext.falseForkContext); + parentContext.nullishForkContext.addAll(poppedChoiceContext.nullishForkContext); parentContext.processed = true; - return context; + // Exit early so we don't collapse all paths into one. + return poppedChoiceContext; } break; case "test": - if (!context.processed) { + if (!poppedChoiceContext.processed) { /* * The head segments are the path of the `if` block here. * Updates the `true` path with the end of the `if` block. */ - context.trueForkContext.clear(); - context.trueForkContext.add(headSegments); + poppedChoiceContext.trueForkContext.clear(); + poppedChoiceContext.trueForkContext.add(head); } else { /* @@ -419,8 +1158,8 @@ class CodePathState { * Updates the `false` path with the end of the `else` * block. */ - context.falseForkContext.clear(); - context.falseForkContext.add(headSegments); + poppedChoiceContext.falseForkContext.clear(); + poppedChoiceContext.falseForkContext.add(head); } break; @@ -428,82 +1167,129 @@ class CodePathState { case "loop": /* - * Loops are addressed in popLoopContext(). - * This is called from popLoopContext(). + * Loops are addressed in `popLoopContext()` so just return + * the context without modification. */ - return context; + return poppedChoiceContext; /* c8 ignore next */ default: throw new Error("unreachable"); } - // Merges all paths. - const prevForkContext = context.trueForkContext; + /* + * Merge the true path with the false path to create a single path. + */ + const combinedForkContext = poppedChoiceContext.trueForkContext; - prevForkContext.addAll(context.falseForkContext); - forkContext.replaceHead(prevForkContext.makeNext(0, -1)); + combinedForkContext.addAll(poppedChoiceContext.falseForkContext); + forkContext.replaceHead(combinedForkContext.makeNext(0, -1)); - return context; + return poppedChoiceContext; } /** - * Makes a code path segment of the right-hand operand of a logical + * Creates a code path segment to represent right-hand operand of a logical * expression. + * This is called in the preprocessing phase when entering a node. * @throws {Error} (Unreachable.) * @returns {void} */ makeLogicalRight() { - const context = this.choiceContext; + const currentChoiceContext = this.choiceContext; const forkContext = this.forkContext; - if (context.processed) { + if (currentChoiceContext.processed) { /* - * This got segments already from the child choice context. - * Creates the next path from own true/false fork context. + * This context was already assigned segments from a child + * choice context. In this case, we are concerned only about + * the path that does not short-circuit and so ends up on the + * right-hand operand of the logical expression. */ let prevForkContext; - switch (context.kind) { + switch (currentChoiceContext.kind) { case "&&": // if true then go to the right-hand side. - prevForkContext = context.trueForkContext; + prevForkContext = currentChoiceContext.trueForkContext; break; case "||": // if false then go to the right-hand side. - prevForkContext = context.falseForkContext; + prevForkContext = currentChoiceContext.falseForkContext; break; - case "??": // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's qqForkContext. - prevForkContext = context.qqForkContext; + case "??": // Both true/false can short-circuit, so needs the third path to go to the right-hand side. That's nullishForkContext. + prevForkContext = currentChoiceContext.nullishForkContext; break; default: throw new Error("unreachable"); } + /* + * Create the segment for the right-hand operand of the logical expression + * and adjust the fork context pointer to point there. The right-hand segment + * is added at the end of all segments in `prevForkContext`. + */ forkContext.replaceHead(prevForkContext.makeNext(0, -1)); + + /* + * We no longer need this list of segments. + * + * Reset `processed` because we've removed the segments from the child + * choice context. This allows `popChoiceContext()` to continue adding + * segments later. + */ prevForkContext.clear(); - context.processed = false; + currentChoiceContext.processed = false; + } else { /* - * This did not get segments from the child choice context. - * So addresses the head segments. - * The head segments are the path of the left-hand operand. + * This choice context was not assigned segments from a child + * choice context, which means that it's a terminal logical + * expression. + * + * `head` is the segments for the left-hand operand of the + * logical expression. + * + * Each of the fork contexts below are empty at this point. We choose + * the path(s) that will short-circuit and add the segment for the + * left-hand operand to it. Ultimately, this will be the only segment + * in that path due to the short-circuting, so we are just seeding + * these paths to start. */ - switch (context.kind) { - case "&&": // the false path can short-circuit. - context.falseForkContext.add(forkContext.head); + switch (currentChoiceContext.kind) { + case "&&": + + /* + * In most contexts, when a && expression evaluates to false, + * it short circuits, so we need to account for that by setting + * the `falseForkContext` to the left operand. + * + * When a && expression is the left-hand operand for a ?? + * expression, such as `(a && b) ?? c`, a nullish value will + * also short-circuit in a different way than a false value, + * so we also set the `nullishForkContext` to the left operand. + * This path is only used with a ?? expression and is thrown + * away for any other type of logical expression, so it's safe + * to always add. + */ + currentChoiceContext.falseForkContext.add(forkContext.head); + currentChoiceContext.nullishForkContext.add(forkContext.head); break; case "||": // the true path can short-circuit. - context.trueForkContext.add(forkContext.head); + currentChoiceContext.trueForkContext.add(forkContext.head); break; case "??": // both can short-circuit. - context.trueForkContext.add(forkContext.head); - context.falseForkContext.add(forkContext.head); + currentChoiceContext.trueForkContext.add(forkContext.head); + currentChoiceContext.falseForkContext.add(forkContext.head); break; default: throw new Error("unreachable"); } + /* + * Create the segment for the right-hand operand of the logical expression + * and adjust the fork context pointer to point there. + */ forkContext.replaceHead(forkContext.makeNext(-1, -1)); } } @@ -524,7 +1310,7 @@ class CodePathState { if (!context.processed) { context.trueForkContext.add(forkContext.head); context.falseForkContext.add(forkContext.head); - context.qqForkContext.add(forkContext.head); + context.nullishForkContext.add(forkContext.head); } context.processed = false; @@ -562,22 +1348,20 @@ class CodePathState { //-------------------------------------------------------------------------- /** - * Push a new `ChainExpression` context to the stack. - * This method is called on entering to each `ChainExpression` node. - * This context is used to count forking in the optional chain then merge them on the exiting from the `ChainExpression` node. + * Pushes a new `ChainExpression` context to the stack. This method is + * called when entering a `ChainExpression` node. A chain context is used to + * count forking in the optional chain then merge them on the exiting from the + * `ChainExpression` node. * @returns {void} */ pushChainContext() { - this.chainContext = { - upper: this.chainContext, - countChoiceContexts: 0 - }; + this.chainContext = new ChainContext(this.chainContext); } /** - * Pop a `ChainExpression` context from the stack. - * This method is called on exiting from each `ChainExpression` node. - * This merges all forks of the last optional chaining. + * Pop a `ChainExpression` context from the stack. This method is called on + * exiting from each `ChainExpression` node. This merges all forks of the + * last optional chaining. * @returns {void} */ popChainContext() { @@ -586,7 +1370,7 @@ class CodePathState { this.chainContext = context.upper; // pop all choice contexts of this. - for (let i = context.countChoiceContexts; i > 0; --i) { + for (let i = context.choiceContextCount; i > 0; --i) { this.popChoiceContext(); } } @@ -599,7 +1383,7 @@ class CodePathState { */ makeOptionalNode() { if (this.chainContext) { - this.chainContext.countChoiceContexts += 1; + this.chainContext.choiceContextCount += 1; this.pushChoiceContext("??", false); } } @@ -627,16 +1411,7 @@ class CodePathState { * @returns {void} */ pushSwitchContext(hasCase, label) { - this.switchContext = { - upper: this.switchContext, - hasCase, - defaultSegments: null, - defaultBodySegments: null, - foundDefault: false, - lastIsDefault: false, - countForks: 0 - }; - + this.switchContext = new SwitchContext(this.switchContext, hasCase); this.pushBreakContext(true, label); } @@ -657,7 +1432,7 @@ class CodePathState { const forkContext = this.forkContext; const brokenForkContext = this.popBreakContext().brokenForkContext; - if (context.countForks === 0) { + if (context.forkCount === 0) { /* * When there is only one `default` chunk and there is one or more @@ -684,47 +1459,54 @@ class CodePathState { brokenForkContext.add(lastSegments); /* - * A path which is failed in all case test should be connected to path - * of `default` chunk. + * Any value that doesn't match a `case` test should flow to the default + * case. That happens normally when the default case is last in the `switch`, + * but if it's not, we need to rewire some of the paths to be correct. */ if (!context.lastIsDefault) { if (context.defaultBodySegments) { /* - * Remove a link from `default` label to its chunk. - * It's false route. + * There is a non-empty default case, so remove the path from the `default` + * label to its body for an accurate representation. + */ + disconnectSegments(context.defaultSegments, context.defaultBodySegments); + + /* + * Connect the path from the last non-default case to the body of the + * default case. */ - removeConnection(context.defaultSegments, context.defaultBodySegments); makeLooped(this, lastCaseSegments, context.defaultBodySegments); + } else { /* - * It handles the last case body as broken if `default` chunk - * does not exist. + * There is no default case, so we treat this as if the last case + * had a `break` in it. */ brokenForkContext.add(lastCaseSegments); } } - // Pops the segment context stack until the entry segment. - for (let i = 0; i < context.countForks; ++i) { + // Traverse up to the original fork context for the `switch` statement + for (let i = 0; i < context.forkCount; ++i) { this.forkContext = this.forkContext.upper; } /* - * Creates a path from all brokenForkContext paths. - * This is a path after switch statement. + * Creates a path from all `brokenForkContext` paths. + * This is a path after `switch` statement. */ this.forkContext.replaceHead(brokenForkContext.makeNext(0, -1)); } /** * Makes a code path segment for a `SwitchCase` node. - * @param {boolean} isEmpty `true` if the body is empty. - * @param {boolean} isDefault `true` if the body is the default case. + * @param {boolean} isCaseBodyEmpty `true` if the body is empty. + * @param {boolean} isDefaultCase `true` if the body is the default case. * @returns {void} */ - makeSwitchCaseBody(isEmpty, isDefault) { + makeSwitchCaseBody(isCaseBodyEmpty, isDefaultCase) { const context = this.switchContext; if (!context.hasCase) { @@ -734,7 +1516,7 @@ class CodePathState { /* * Merge forks. * The parent fork context has two segments. - * Those are from the current case and the body of the previous case. + * Those are from the current `case` and the body of the previous case. */ const parentForkContext = this.forkContext; const forkContext = this.pushForkContext(); @@ -742,26 +1524,53 @@ class CodePathState { forkContext.add(parentForkContext.makeNext(0, -1)); /* - * Save `default` chunk info. - * If the `default` label is not at the last, we must make a path from - * the last `case` to the `default` chunk. + * Add information about the default case. + * + * The purpose of this is to identify the starting segments for the + * default case to make sure there is a path there. */ - if (isDefault) { + if (isDefaultCase) { + + /* + * This is the default case in the `switch`. + * + * We first save the current pointer as `defaultSegments` to point + * to the `default` keyword. + */ context.defaultSegments = parentForkContext.head; - if (isEmpty) { - context.foundDefault = true; + + /* + * If the body of the case is empty then we just set + * `foundEmptyDefault` to true; otherwise, we save a reference + * to the current pointer as `defaultBodySegments`. + */ + if (isCaseBodyEmpty) { + context.foundEmptyDefault = true; } else { context.defaultBodySegments = forkContext.head; } + } else { - if (!isEmpty && context.foundDefault) { - context.foundDefault = false; + + /* + * This is not the default case in the `switch`. + * + * If it's not empty and there is already an empty default case found, + * that means the default case actually comes before this case, + * and that it will fall through to this case. So, we can now + * ignore the previous default case (reset `foundEmptyDefault` to false) + * and set `defaultBodySegments` to the current segments because this is + * effectively the new default case. + */ + if (!isCaseBodyEmpty && context.foundEmptyDefault) { + context.foundEmptyDefault = false; context.defaultBodySegments = forkContext.head; } } - context.lastIsDefault = isDefault; - context.countForks += 1; + // keep track if the default case ends up last + context.lastIsDefault = isDefaultCase; + context.forkCount += 1; } //-------------------------------------------------------------------------- @@ -775,19 +1584,7 @@ class CodePathState { * @returns {void} */ pushTryContext(hasFinalizer) { - this.tryContext = { - upper: this.tryContext, - position: "try", - hasFinalizer, - - returnedForkContext: hasFinalizer - ? ForkContext.newEmpty(this.forkContext) - : null, - - thrownForkContext: ForkContext.newEmpty(this.forkContext), - lastOfTryIsReachable: false, - lastOfCatchIsReachable: false - }; + this.tryContext = new TryContext(this.tryContext, hasFinalizer, this.forkContext); } /** @@ -799,25 +1596,35 @@ class CodePathState { this.tryContext = context.upper; + /* + * If we're inside the `catch` block, that means there is no `finally`, + * so we can process the `try` and `catch` blocks the simple way and + * merge their two paths. + */ if (context.position === "catch") { - - // Merges two paths from the `try` block and `catch` block merely. this.popForkContext(); return; } /* - * The following process is executed only when there is the `finally` + * The following process is executed only when there is a `finally` * block. */ - const returned = context.returnedForkContext; - const thrown = context.thrownForkContext; + const originalReturnedForkContext = context.returnedForkContext; + const originalThrownForkContext = context.thrownForkContext; - if (returned.empty && thrown.empty) { + // no `return` or `throw` in `try` or `catch` so there's nothing left to do + if (originalReturnedForkContext.empty && originalThrownForkContext.empty) { return; } + /* + * The following process is executed only when there is a `finally` + * block and there was a `return` or `throw` in the `try` or `catch` + * blocks. + */ + // Separate head to normal paths and leaving paths. const headSegments = this.forkContext.head; @@ -826,10 +1633,10 @@ class CodePathState { const leavingSegments = headSegments.slice(headSegments.length / 2 | 0); // Forwards the leaving path to upper contexts. - if (!returned.empty) { + if (!originalReturnedForkContext.empty) { getReturnContext(this).returnedForkContext.add(leavingSegments); } - if (!thrown.empty) { + if (!originalThrownForkContext.empty) { getThrowContext(this).thrownForkContext.add(leavingSegments); } @@ -852,16 +1659,20 @@ class CodePathState { makeCatchBlock() { const context = this.tryContext; const forkContext = this.forkContext; - const thrown = context.thrownForkContext; + const originalThrownForkContext = context.thrownForkContext; - // Update state. + /* + * We are now in a catch block so we need to update the context + * with that information. This includes creating a new fork + * context in case we encounter any `throw` statements here. + */ context.position = "catch"; context.thrownForkContext = ForkContext.newEmpty(forkContext); context.lastOfTryIsReachable = forkContext.reachable; - // Merge thrown paths. - thrown.add(forkContext.head); - const thrownSegments = thrown.makeNext(0, -1); + // Merge the thrown paths from the `try` and `catch` blocks + originalThrownForkContext.add(forkContext.head); + const thrownSegments = originalThrownForkContext.makeNext(0, -1); // Fork to a bypass and the merged thrown path. this.pushForkContext(); @@ -880,8 +1691,8 @@ class CodePathState { makeFinallyBlock() { const context = this.tryContext; let forkContext = this.forkContext; - const returned = context.returnedForkContext; - const thrown = context.thrownForkContext; + const originalReturnedForkContext = context.returnedForkContext; + const originalThrownForContext = context.thrownForkContext; const headOfLeavingSegments = forkContext.head; // Update state. @@ -895,9 +1706,15 @@ class CodePathState { } else { context.lastOfTryIsReachable = forkContext.reachable; } + + context.position = "finally"; - if (returned.empty && thrown.empty) { + /* + * If there was no `return` or `throw` in either the `try` or `catch` + * blocks, then there's no further code paths to create for `finally`. + */ + if (originalReturnedForkContext.empty && originalThrownForContext.empty) { // This path does not leave. return; @@ -905,18 +1722,18 @@ class CodePathState { /* * Create a parallel segment from merging returned and thrown. - * This segment will leave at the end of this finally block. + * This segment will leave at the end of this `finally` block. */ const segments = forkContext.makeNext(-1, -1); for (let i = 0; i < forkContext.count; ++i) { const prevSegsOfLeavingSegment = [headOfLeavingSegments[i]]; - for (let j = 0; j < returned.segmentsList.length; ++j) { - prevSegsOfLeavingSegment.push(returned.segmentsList[j][i]); + for (let j = 0; j < originalReturnedForkContext.segmentsList.length; ++j) { + prevSegsOfLeavingSegment.push(originalReturnedForkContext.segmentsList[j][i]); } - for (let j = 0; j < thrown.segmentsList.length; ++j) { - prevSegsOfLeavingSegment.push(thrown.segmentsList[j][i]); + for (let j = 0; j < originalThrownForContext.segmentsList.length; ++j) { + prevSegsOfLeavingSegment.push(originalThrownForContext.segmentsList[j][i]); } segments.push( @@ -971,63 +1788,32 @@ class CodePathState { */ pushLoopContext(type, label) { const forkContext = this.forkContext; + + // All loops need a path to account for `break` statements const breakContext = this.pushBreakContext(true, label); switch (type) { case "WhileStatement": this.pushChoiceContext("loop", false); - this.loopContext = { - upper: this.loopContext, - type, - label, - test: void 0, - continueDestSegments: null, - brokenForkContext: breakContext.brokenForkContext - }; + this.loopContext = new WhileLoopContext(this.loopContext, label, breakContext); break; case "DoWhileStatement": this.pushChoiceContext("loop", false); - this.loopContext = { - upper: this.loopContext, - type, - label, - test: void 0, - entrySegments: null, - continueForkContext: ForkContext.newEmpty(forkContext), - brokenForkContext: breakContext.brokenForkContext - }; + this.loopContext = new DoWhileLoopContext(this.loopContext, label, breakContext, forkContext); break; case "ForStatement": this.pushChoiceContext("loop", false); - this.loopContext = { - upper: this.loopContext, - type, - label, - test: void 0, - endOfInitSegments: null, - testSegments: null, - endOfTestSegments: null, - updateSegments: null, - endOfUpdateSegments: null, - continueDestSegments: null, - brokenForkContext: breakContext.brokenForkContext - }; + this.loopContext = new ForLoopContext(this.loopContext, label, breakContext); break; case "ForInStatement": + this.loopContext = new ForInLoopContext(this.loopContext, label, breakContext); + break; + case "ForOfStatement": - this.loopContext = { - upper: this.loopContext, - type, - label, - prevSegments: null, - leftSegments: null, - endOfLeftSegments: null, - continueDestSegments: null, - brokenForkContext: breakContext.brokenForkContext - }; + this.loopContext = new ForOfLoopContext(this.loopContext, label, breakContext); break; /* c8 ignore next */ @@ -1054,6 +1840,11 @@ class CodePathState { case "WhileStatement": case "ForStatement": this.popChoiceContext(); + + /* + * Creates the path from the end of the loop body up to the + * location where `continue` would jump to. + */ makeLooped( this, forkContext.head, @@ -1068,11 +1859,21 @@ class CodePathState { choiceContext.trueForkContext.add(forkContext.head); choiceContext.falseForkContext.add(forkContext.head); } + + /* + * If this isn't a hardcoded `true` condition, then `break` + * should continue down the path as if the condition evaluated + * to false. + */ if (context.test !== true) { brokenForkContext.addAll(choiceContext.falseForkContext); } - // `true` paths go to looping. + /* + * When the condition is true, the loop continues back to the top, + * so create a path from each possible true condition back to the + * top of the loop. + */ const segmentsList = choiceContext.trueForkContext.segmentsList; for (let i = 0; i < segmentsList.length; ++i) { @@ -1088,6 +1889,11 @@ class CodePathState { case "ForInStatement": case "ForOfStatement": brokenForkContext.add(forkContext.head); + + /* + * Creates the path from the end of the loop body up to the + * left expression (left of `in` or `of`) of the loop. + */ makeLooped( this, forkContext.head, @@ -1100,7 +1906,14 @@ class CodePathState { throw new Error("unreachable"); } - // Go next. + /* + * If there wasn't a `break` statement in the loop, then we're at + * the end of the loop's path, so we make an unreachable segment + * to mark that. + * + * If there was a `break` statement, then we continue on into the + * `brokenForkContext`. + */ if (brokenForkContext.empty) { forkContext.replaceHead(forkContext.makeUnreachable(-1, -1)); } else { @@ -1138,7 +1951,11 @@ class CodePathState { choiceContext.falseForkContext.add(forkContext.head); } - // Update state. + /* + * If this isn't a hardcoded `true` condition, then `break` + * should continue down the path as if the condition evaluated + * to false. + */ if (context.test !== true) { context.brokenForkContext.addAll(choiceContext.falseForkContext); } @@ -1170,7 +1987,11 @@ class CodePathState { context.test = test; - // Creates paths of `continue` statements. + /* + * If there is a `continue` statement in the loop then `continueForkContext` + * won't be empty. We wire up the path from `continue` to the loop + * test condition and then continue the traversal in the root fork context. + */ if (!context.continueForkContext.empty) { context.continueForkContext.add(forkContext.head); const testSegments = context.continueForkContext.makeNext(0, -1); @@ -1190,7 +2011,14 @@ class CodePathState { const endOfInitSegments = forkContext.head; const testSegments = forkContext.makeNext(-1, -1); - // Update state. + /* + * Update the state. + * + * The `continueDestSegments` are set to `testSegments` because we + * don't yet know if there is an update expression in this loop. So, + * from what we already know at this point, a `continue` statement + * will jump back to the test expression. + */ context.test = test; context.endOfInitSegments = endOfInitSegments; context.continueDestSegments = context.testSegments = testSegments; @@ -1217,7 +2045,14 @@ class CodePathState { context.endOfInitSegments = forkContext.head; } - // Update state. + /* + * Update the state. + * + * The `continueDestSegments` are now set to `updateSegments` because we + * know there is an update expression in this loop. So, a `continue` statement + * in the loop will jump to the update expression first, and then to any + * test expression the loop might have. + */ const updateSegments = forkContext.makeDisconnected(-1, -1); context.continueDestSegments = context.updateSegments = updateSegments; @@ -1233,11 +2068,30 @@ class CodePathState { const choiceContext = this.choiceContext; const forkContext = this.forkContext; - // Update state. + /* + * Determine what to do based on which part of the `for` loop are present. + * 1. If there is an update expression, then `updateSegments` is not null and + * we need to assign `endOfUpdateSegments`, and if there is a test + * expression, we then need to create the looped path to get back to + * the test condition. + * 2. If there is no update expression but there is a test expression, + * then we only need to update the test segment information. + * 3. If there is no update expression and no test expression, then we + * just save `endOfInitSegments`. + */ if (context.updateSegments) { context.endOfUpdateSegments = forkContext.head; - // `update` -> `test` + /* + * In a `for` loop that has both an update expression and a test + * condition, execution flows from the test expression into the + * loop body, to the update expression, and then back to the test + * expression to determine if the loop should continue. + * + * To account for that, we need to make a path from the end of the + * update expression to the start of the test expression. This is + * effectively what creates the loop in the code path. + */ if (context.testSegments) { makeLooped( this, @@ -1257,12 +2111,18 @@ class CodePathState { let bodySegments = context.endOfTestSegments; + /* + * If there is a test condition, then there `endOfTestSegments` is also + * the start of the loop body. If there isn't a test condition then + * `bodySegments` will be null and we need to look elsewhere to find + * the start of the body. + * + * The body starts at the end of the init expression and ends at the end + * of the update expression, so we use those locations to determine the + * body segments. + */ if (!bodySegments) { - /* - * If there is not the `test` part, the `body` path comes from the - * `init` part and the `update` part. - */ const prevForkContext = ForkContext.newEmpty(forkContext); prevForkContext.add(context.endOfInitSegments); @@ -1272,7 +2132,16 @@ class CodePathState { bodySegments = prevForkContext.makeNext(0, -1); } + + /* + * If there was no test condition and no update expression, then + * `continueDestSegments` will be null. In that case, a + * `continue` should skip directly to the body of the loop. + * Otherwise, we want to keep the current `continueDestSegments`. + */ context.continueDestSegments = context.continueDestSegments || bodySegments; + + // move pointer to the body forkContext.replaceHead(bodySegments); } @@ -1336,19 +2205,15 @@ class CodePathState { //-------------------------------------------------------------------------- /** - * Creates new context for BreakStatement. - * @param {boolean} breakable The flag to indicate it can break by - * an unlabeled BreakStatement. - * @param {string|null} label The label of this context. - * @returns {Object} The new context. + * Creates new context in which a `break` statement can be used. This occurs inside of a loop, + * labeled statement, or switch statement. + * @param {boolean} breakable Indicates if we are inside a statement where + * `break` without a label will exit the statement. + * @param {string|null} label The label associated with the statement. + * @returns {BreakContext} The new context. */ pushBreakContext(breakable, label) { - this.breakContext = { - upper: this.breakContext, - breakable, - label, - brokenForkContext: ForkContext.newEmpty(this.forkContext) - }; + this.breakContext = new BreakContext(this.breakContext, breakable, label, this.forkContext); return this.breakContext; } @@ -1380,7 +2245,7 @@ class CodePathState { * * It registers the head segment to a context of `break`. * It makes new unreachable segment, then it set the head with the segment. - * @param {string} label A label of the break statement. + * @param {string|null} label A label of the break statement. * @returns {void} */ makeBreak(label) { @@ -1406,7 +2271,7 @@ class CodePathState { * * It makes a looping path. * It makes new unreachable segment, then it set the head with the segment. - * @param {string} label A label of the continue statement. + * @param {string|null} label A label of the continue statement. * @returns {void} */ makeContinue(label) { @@ -1422,7 +2287,7 @@ class CodePathState { if (context.continueDestSegments) { makeLooped(this, forkContext.head, context.continueDestSegments); - // If the context is a for-in/of loop, this effects a break also. + // If the context is a for-in/of loop, this affects a break also. if (context.type === "ForInStatement" || context.type === "ForOfStatement" ) { diff --git a/lib/linter/code-path-analysis/code-path.js b/lib/linter/code-path-analysis/code-path.js index f6a88a00af92..3bf570d754bf 100644 --- a/lib/linter/code-path-analysis/code-path.js +++ b/lib/linter/code-path-analysis/code-path.js @@ -80,7 +80,9 @@ class CodePath { } /** - * The initial code path segment. + * The initial code path segment. This is the segment that is at the head + * of the code path. + * This is a passthrough to the underlying `CodePathState`. * @type {CodePathSegment} */ get initialSegment() { @@ -88,8 +90,10 @@ class CodePath { } /** - * Final code path segments. - * This array is a mix of `returnedSegments` and `thrownSegments`. + * Final code path segments. These are the terminal (tail) segments in the + * code path, which is the combination of `returnedSegments` and `thrownSegments`. + * All segments in this array are reachable. + * This is a passthrough to the underlying `CodePathState`. * @type {CodePathSegment[]} */ get finalSegments() { @@ -97,9 +101,14 @@ class CodePath { } /** - * Final code path segments which is with `return` statements. - * This array contains the last path segment if it's reachable. - * Since the reachable last path returns `undefined`. + * Final code path segments that represent normal completion of the code path. + * For functions, this means both explicit `return` statements and implicit returns, + * such as the last reachable segment in a function that does not have an + * explicit `return` as this implicitly returns `undefined`. For scripts, + * modules, class field initializers, and class static blocks, this means + * all lines of code have been executed. + * These segments are also present in `finalSegments`. + * This is a passthrough to the underlying `CodePathState`. * @type {CodePathSegment[]} */ get returnedSegments() { @@ -107,7 +116,9 @@ class CodePath { } /** - * Final code path segments which is with `throw` statements. + * Final code path segments that represent `throw` statements. + * This is a passthrough to the underlying `CodePathState`. + * These segments are also present in `finalSegments`. * @type {CodePathSegment[]} */ get thrownSegments() { @@ -115,7 +126,12 @@ class CodePath { } /** - * Current code path segments. + * Tracks the traversal of the code path through each segment. This array + * starts empty and segments are added or removed as the code path is + * traversed. This array always ends up empty at the end of a code path + * traversal. The `CodePathState` uses this to track its progress through + * the code path. + * This is a passthrough to the underlying `CodePathState`. * @type {CodePathSegment[]} * @deprecated */ @@ -126,46 +142,70 @@ class CodePath { /** * Traverses all segments in this code path. * - * codePath.traverseSegments(function(segment, controller) { + * codePath.traverseSegments((segment, controller) => { * // do something. * }); * * This method enumerates segments in order from the head. * - * The `controller` object has two methods. + * The `controller` argument has two methods: * - * - `controller.skip()` - Skip the following segments in this branch. - * - `controller.break()` - Skip all following segments. - * @param {Object} [options] Omittable. - * @param {CodePathSegment} [options.first] The first segment to traverse. - * @param {CodePathSegment} [options.last] The last segment to traverse. + * - `skip()` - skips the following segments in this branch + * - `break()` - skips all following segments in the traversal + * + * A note on the parameters: the `options` argument is optional. This means + * the first argument might be an options object or the callback function. + * @param {Object} [optionsOrCallback] Optional first and last segments to traverse. + * @param {CodePathSegment} [optionsOrCallback.first] The first segment to traverse. + * @param {CodePathSegment} [optionsOrCallback.last] The last segment to traverse. * @param {Function} callback A callback function. * @returns {void} */ - traverseSegments(options, callback) { + traverseSegments(optionsOrCallback, callback) { + + // normalize the arguments into a callback and options let resolvedOptions; let resolvedCallback; - if (typeof options === "function") { - resolvedCallback = options; + if (typeof optionsOrCallback === "function") { + resolvedCallback = optionsOrCallback; resolvedOptions = {}; } else { - resolvedOptions = options || {}; + resolvedOptions = optionsOrCallback || {}; resolvedCallback = callback; } + // determine where to start traversing from based on the options const startSegment = resolvedOptions.first || this.internal.initialSegment; const lastSegment = resolvedOptions.last; - let item = null; + // set up initial location information + let record = null; let index = 0; let end = 0; let segment = null; - const visited = Object.create(null); + + // segments that have already been visited during traversal + const visited = new Set(); + + // tracks the traversal steps const stack = [[startSegment, 0]]; + + // tracks the last skipped segment during traversal let skippedSegment = null; + + // indicates if we exited early from the traversal let broken = false; + + /** + * Maintains traversal state. + */ const controller = { + + /** + * Skip the following segments in this branch. + * @returns {void} + */ skip() { if (stack.length <= 1) { broken = true; @@ -173,32 +213,52 @@ class CodePath { skippedSegment = stack[stack.length - 2][0]; } }, + + /** + * Stop traversal completely - do not traverse to any + * other segments. + * @returns {void} + */ break() { broken = true; } }; /** - * Checks a given previous segment has been visited. + * Checks if a given previous segment has been visited. * @param {CodePathSegment} prevSegment A previous segment to check. * @returns {boolean} `true` if the segment has been visited. */ function isVisited(prevSegment) { return ( - visited[prevSegment.id] || + visited.has(prevSegment) || segment.isLoopedPrevSegment(prevSegment) ); } + // the traversal while (stack.length > 0) { - item = stack[stack.length - 1]; - segment = item[0]; - index = item[1]; + + /* + * This isn't a pure stack. We use the top record all the time + * but don't always pop it off. The record is popped only if + * one of the following is true: + * + * 1) We have already visited the segment. + * 2) We have not visited *all* of the previous segments. + * 3) We have traversed past the available next segments. + * + * Otherwise, we just read the value and sometimes modify the + * record as we traverse. + */ + record = stack[stack.length - 1]; + segment = record[0]; + index = record[1]; if (index === 0) { // Skip if this segment has been visited already. - if (visited[segment.id]) { + if (visited.has(segment)) { stack.pop(); continue; } @@ -212,18 +272,29 @@ class CodePath { continue; } - // Reset the flag of skipping if all branches have been skipped. + // Reset the skipping flag if all branches have been skipped. if (skippedSegment && segment.prevSegments.includes(skippedSegment)) { skippedSegment = null; } - visited[segment.id] = true; + visited.add(segment); - // Call the callback when the first time. + /* + * If the most recent segment hasn't been skipped, then we call + * the callback, passing in the segment and the controller. + */ if (!skippedSegment) { resolvedCallback.call(this, segment, controller); + + // exit if we're at the last segment if (segment === lastSegment) { controller.skip(); } + + /* + * If the previous statement was executed, or if the callback + * called a method on the controller, we might need to exit the + * loop, so check for that and break accordingly. + */ if (broken) { break; } @@ -233,12 +304,35 @@ class CodePath { // Update the stack. end = segment.nextSegments.length - 1; if (index < end) { - item[1] += 1; + + /* + * If we haven't yet visited all of the next segments, update + * the current top record on the stack to the next index to visit + * and then push a record for the current segment on top. + * + * Setting the current top record's index lets us know how many + * times we've been here and ensures that the segment won't be + * reprocessed (because we only process segments with an index + * of 0). + */ + record[1] += 1; stack.push([segment.nextSegments[index], 0]); } else if (index === end) { - item[0] = segment.nextSegments[index]; - item[1] = 0; + + /* + * If we are at the last next segment, then reset the top record + * in the stack to next segment and set its index to 0 so it will + * be processed next. + */ + record[0] = segment.nextSegments[index]; + record[1] = 0; } else { + + /* + * If index > end, that means we have no more segments that need + * processing. So, we pop that record off of the stack in order to + * continue traversing at the next level up. + */ stack.pop(); } } diff --git a/lib/linter/code-path-analysis/fork-context.js b/lib/linter/code-path-analysis/fork-context.js index 04c59b5e4172..33140272f53b 100644 --- a/lib/linter/code-path-analysis/fork-context.js +++ b/lib/linter/code-path-analysis/fork-context.js @@ -21,8 +21,8 @@ const assert = require("assert"), //------------------------------------------------------------------------------ /** - * Gets whether or not a given segment is reachable. - * @param {CodePathSegment} segment A segment to get. + * Determines whether or not a given segment is reachable. + * @param {CodePathSegment} segment The segment to check. * @returns {boolean} `true` if the segment is reachable. */ function isReachable(segment) { @@ -30,32 +30,64 @@ function isReachable(segment) { } /** - * Creates new segments from the specific range of `context.segmentsList`. + * Creates a new segment for each fork in the given context and appends it + * to the end of the specified range of segments. Ultimately, this ends up calling + * `new CodePathSegment()` for each of the forks using the `create` argument + * as a wrapper around special behavior. + * + * The `startIndex` and `endIndex` arguments specify a range of segments in + * `context` that should become `allPrevSegments` for the newly created + * `CodePathSegment` objects. * * When `context.segmentsList` is `[[a, b], [c, d], [e, f]]`, `begin` is `0`, and - * `end` is `-1`, this creates `[g, h]`. This `g` is from `a`, `c`, and `e`. - * This `h` is from `b`, `d`, and `f`. - * @param {ForkContext} context An instance. - * @param {number} begin The first index of the previous segments. - * @param {number} end The last index of the previous segments. - * @param {Function} create A factory function of new segments. - * @returns {CodePathSegment[]} New segments. + * `end` is `-1`, this creates two new segments, `[g, h]`. This `g` is appended to + * the end of the path from `a`, `c`, and `e`. This `h` is appended to the end of + * `b`, `d`, and `f`. + * @param {ForkContext} context An instance from which the previous segments + * will be obtained. + * @param {number} startIndex The index of the first segment in the context + * that should be specified as previous segments for the newly created segments. + * @param {number} endIndex The index of the last segment in the context + * that should be specified as previous segments for the newly created segments. + * @param {Function} create A function that creates new `CodePathSegment` + * instances in a particular way. See the `CodePathSegment.new*` methods. + * @returns {Array} An array of the newly-created segments. */ -function makeSegments(context, begin, end, create) { +function createSegments(context, startIndex, endIndex, create) { + + /** @type {Array>} */ const list = context.segmentsList; - const normalizedBegin = begin >= 0 ? begin : list.length + begin; - const normalizedEnd = end >= 0 ? end : list.length + end; + /* + * Both `startIndex` and `endIndex` work the same way: if the number is zero + * or more, then the number is used as-is. If the number is negative, + * then that number is added to the length of the segments list to + * determine the index to use. That means -1 for either argument + * is the last element, -2 is the second to last, and so on. + * + * So if `startIndex` is 0, `endIndex` is -1, and `list.length` is 3, the + * effective `startIndex` is 0 and the effective `endIndex` is 2, so this function + * will include items at indices 0, 1, and 2. + * + * Therefore, if `startIndex` is -1 and `endIndex` is -1, that means we'll only + * be using the last segment in `list`. + */ + const normalizedBegin = startIndex >= 0 ? startIndex : list.length + startIndex; + const normalizedEnd = endIndex >= 0 ? endIndex : list.length + endIndex; + /** @type {Array} */ const segments = []; for (let i = 0; i < context.count; ++i) { + + // this is passed into `new CodePathSegment` to add to code path. const allPrevSegments = []; for (let j = normalizedBegin; j <= normalizedEnd; ++j) { allPrevSegments.push(list[j][i]); } + // note: `create` is just a wrapper that augments `new CodePathSegment`. segments.push(create(context.idGenerator.next(), allPrevSegments)); } @@ -63,28 +95,57 @@ function makeSegments(context, begin, end, create) { } /** - * `segments` becomes doubly in a `finally` block. Then if a code path exits by a - * control statement (such as `break`, `continue`) from the `finally` block, the - * destination's segments may be half of the source segments. In that case, this - * merges segments. - * @param {ForkContext} context An instance. - * @param {CodePathSegment[]} segments Segments to merge. - * @returns {CodePathSegment[]} The merged segments. + * Inside of a `finally` block we end up with two parallel paths. If the code path + * exits by a control statement (such as `break` or `continue`) from the `finally` + * block, then we need to merge the remaining parallel paths back into one. + * @param {ForkContext} context The fork context to work on. + * @param {Array} segments Segments to merge. + * @returns {Array} The merged segments. */ function mergeExtraSegments(context, segments) { let currentSegments = segments; + /* + * We need to ensure that the array returned from this function contains no more + * than the number of segments that the context allows. `context.count` indicates + * how many items should be in the returned array to ensure that the new segment + * entries will line up with the already existing segment entries. + */ while (currentSegments.length > context.count) { const merged = []; - for (let i = 0, length = currentSegments.length / 2 | 0; i < length; ++i) { + /* + * Because `context.count` is a factor of 2 inside of a `finally` block, + * we can divide the segment count by 2 to merge the paths together. + * This loops through each segment in the list and creates a new `CodePathSegment` + * that has the segment and the segment two slots away as previous segments. + * + * If `currentSegments` is [a,b,c,d], this will create new segments e and f, such + * that: + * + * When `i` is 0: + * a->e + * c->e + * + * When `i` is 1: + * b->f + * d->f + */ + for (let i = 0, length = Math.floor(currentSegments.length / 2); i < length; ++i) { merged.push(CodePathSegment.newNext( context.idGenerator.next(), [currentSegments[i], currentSegments[i + length]] )); } + + /* + * Go through the loop condition one more time to see if we have the + * number of segments for the context. If not, we'll keep merging paths + * of the merged segments until we get there. + */ currentSegments = merged; } + return currentSegments; } @@ -93,25 +154,55 @@ function mergeExtraSegments(context, segments) { //------------------------------------------------------------------------------ /** - * A class to manage forking. + * Manages the forking of code paths. */ class ForkContext { /** + * Creates a new instance. * @param {IdGenerator} idGenerator An identifier generator for segments. - * @param {ForkContext|null} upper An upper fork context. - * @param {number} count A number of parallel segments. + * @param {ForkContext|null} upper The preceding fork context. + * @param {number} count The number of parallel segments in each element + * of `segmentsList`. */ constructor(idGenerator, upper, count) { + + /** + * The ID generator that will generate segment IDs for any new + * segments that are created. + * @type {IdGenerator} + */ this.idGenerator = idGenerator; + + /** + * The preceding fork context. + * @type {ForkContext|null} + */ this.upper = upper; + + /** + * The number of elements in each element of `segmentsList`. In most + * cases, this is 1 but can be 2 when there is a `finally` present, + * which forks the code path outside of normal flow. In the case of nested + * `finally` blocks, this can be a multiple of 2. + * @type {number} + */ this.count = count; + + /** + * The segments within this context. Each element in this array has + * `count` elements that represent one step in each fork. For example, + * when `segmentsList` is `[[a, b], [c, d], [e, f]]`, there is one path + * a->c->e and one path b->d->f, and `count` is 2 because each element + * is an array with two elements. + * @type {Array>} + */ this.segmentsList = []; } /** - * The head segments. - * @type {CodePathSegment[]} + * The segments that begin this fork context. + * @type {Array} */ get head() { const list = this.segmentsList; @@ -120,7 +211,7 @@ class ForkContext { } /** - * A flag which shows empty. + * Indicates if the context contains no segments. * @type {boolean} */ get empty() { @@ -128,7 +219,7 @@ class ForkContext { } /** - * A flag which shows reachable. + * Indicates if there are any segments that are reachable. * @type {boolean} */ get reachable() { @@ -138,75 +229,82 @@ class ForkContext { } /** - * Creates new segments from this context. - * @param {number} begin The first index of previous segments. - * @param {number} end The last index of previous segments. - * @returns {CodePathSegment[]} New segments. + * Creates new segments in this context and appends them to the end of the + * already existing `CodePathSegment`s specified by `startIndex` and + * `endIndex`. + * @param {number} startIndex The index of the first segment in the context + * that should be specified as previous segments for the newly created segments. + * @param {number} endIndex The index of the last segment in the context + * that should be specified as previous segments for the newly created segments. + * @returns {Array} An array of the newly created segments. */ - makeNext(begin, end) { - return makeSegments(this, begin, end, CodePathSegment.newNext); + makeNext(startIndex, endIndex) { + return createSegments(this, startIndex, endIndex, CodePathSegment.newNext); } /** - * Creates new segments from this context. - * The new segments is always unreachable. - * @param {number} begin The first index of previous segments. - * @param {number} end The last index of previous segments. - * @returns {CodePathSegment[]} New segments. + * Creates new unreachable segments in this context and appends them to the end of the + * already existing `CodePathSegment`s specified by `startIndex` and + * `endIndex`. + * @param {number} startIndex The index of the first segment in the context + * that should be specified as previous segments for the newly created segments. + * @param {number} endIndex The index of the last segment in the context + * that should be specified as previous segments for the newly created segments. + * @returns {Array} An array of the newly created segments. */ - makeUnreachable(begin, end) { - return makeSegments(this, begin, end, CodePathSegment.newUnreachable); + makeUnreachable(startIndex, endIndex) { + return createSegments(this, startIndex, endIndex, CodePathSegment.newUnreachable); } /** - * Creates new segments from this context. - * The new segments don't have connections for previous segments. - * But these inherit the reachable flag from this context. - * @param {number} begin The first index of previous segments. - * @param {number} end The last index of previous segments. - * @returns {CodePathSegment[]} New segments. + * Creates new segments in this context and does not append them to the end + * of the already existing `CodePathSegment`s specified by `startIndex` and + * `endIndex`. The `startIndex` and `endIndex` are only used to determine if + * the new segments should be reachable. If any of the segments in this range + * are reachable then the new segments are also reachable; otherwise, the new + * segments are unreachable. + * @param {number} startIndex The index of the first segment in the context + * that should be considered for reachability. + * @param {number} endIndex The index of the last segment in the context + * that should be considered for reachability. + * @returns {Array} An array of the newly created segments. */ - makeDisconnected(begin, end) { - return makeSegments(this, begin, end, CodePathSegment.newDisconnected); + makeDisconnected(startIndex, endIndex) { + return createSegments(this, startIndex, endIndex, CodePathSegment.newDisconnected); } /** - * Adds segments into this context. - * The added segments become the head. - * @param {CodePathSegment[]} segments Segments to add. + * Adds segments to the head of this context. + * @param {Array} segments The segments to add. * @returns {void} */ add(segments) { assert(segments.length >= this.count, `${segments.length} >= ${this.count}`); - this.segmentsList.push(mergeExtraSegments(this, segments)); } /** - * Replaces the head segments with given segments. + * Replaces the head segments with the given segments. * The current head segments are removed. - * @param {CodePathSegment[]} segments Segments to add. + * @param {Array} replacementHeadSegments The new head segments. * @returns {void} */ - replaceHead(segments) { - assert(segments.length >= this.count, `${segments.length} >= ${this.count}`); - - this.segmentsList.splice(-1, 1, mergeExtraSegments(this, segments)); + replaceHead(replacementHeadSegments) { + assert( + replacementHeadSegments.length >= this.count, + `${replacementHeadSegments.length} >= ${this.count}` + ); + this.segmentsList.splice(-1, 1, mergeExtraSegments(this, replacementHeadSegments)); } /** * Adds all segments of a given fork context into this context. - * @param {ForkContext} context A fork context to add. + * @param {ForkContext} otherForkContext The fork context to add from. * @returns {void} */ - addAll(context) { - assert(context.count === this.count); - - const source = context.segmentsList; - - for (let i = 0; i < source.length; ++i) { - this.segmentsList.push(source[i]); - } + addAll(otherForkContext) { + assert(otherForkContext.count === this.count); + this.segmentsList.push(...otherForkContext.segmentsList); } /** @@ -218,7 +316,8 @@ class ForkContext { } /** - * Creates the root fork context. + * Creates a new root context, meaning that there are no parent + * fork contexts. * @param {IdGenerator} idGenerator An identifier generator for segments. * @returns {ForkContext} New fork context. */ @@ -233,14 +332,16 @@ class ForkContext { /** * Creates an empty fork context preceded by a given context. * @param {ForkContext} parentContext The parent fork context. - * @param {boolean} forkLeavingPath A flag which shows inside of `finally` block. + * @param {boolean} shouldForkLeavingPath Indicates that we are inside of + * a `finally` block and should therefore fork the path that leaves + * `finally`. * @returns {ForkContext} New fork context. */ - static newEmpty(parentContext, forkLeavingPath) { + static newEmpty(parentContext, shouldForkLeavingPath) { return new ForkContext( parentContext.idGenerator, parentContext, - (forkLeavingPath ? 2 : 1) * parentContext.count + (shouldForkLeavingPath ? 2 : 1) * parentContext.count ); } } diff --git a/lib/linter/config-comment-parser.js b/lib/linter/config-comment-parser.js index 9aab3c444585..cde261204f5e 100644 --- a/lib/linter/config-comment-parser.js +++ b/lib/linter/config-comment-parser.js @@ -139,7 +139,7 @@ module.exports = class ConfigCommentParser { const items = {}; string.split(",").forEach(name => { - const trimmedName = name.trim(); + const trimmedName = name.trim().replace(/^(?['"]?)(?.*)\k$/us, "$"); if (trimmedName) { items[trimmedName] = true; diff --git a/lib/options.js b/lib/options.js index 2bc4018afb5d..ae9a5d5552a2 100644 --- a/lib/options.js +++ b/lib/options.js @@ -55,6 +55,7 @@ const optionator = require("optionator"); * @property {string} [stdinFilename] Specify filename to process STDIN as * @property {boolean} quiet Report errors only * @property {boolean} [version] Output the version number + * @property {boolean} warnIgnored Show warnings when the file list includes ignored files * @property {string[]} _ Positional filenames or patterns */ @@ -139,6 +140,17 @@ module.exports = function(usingFlatConfig) { }; } + let warnIgnoredFlag; + + if (usingFlatConfig) { + warnIgnoredFlag = { + option: "warn-ignored", + type: "Boolean", + default: "true", + description: "Suppress warnings when the file list includes ignored files" + }; + } + return optionator({ prepend: "eslint [options] file.js [file.js] [dir]", defaults: { @@ -349,6 +361,7 @@ module.exports = function(usingFlatConfig) { default: "false", description: "Exit with exit code 2 in case of fatal error" }, + warnIgnoredFlag, { option: "debug", type: "Boolean", diff --git a/lib/rules/logical-assignment-operators.js b/lib/rules/logical-assignment-operators.js index 27ca585e9958..c084c04c8eda 100644 --- a/lib/rules/logical-assignment-operators.js +++ b/lib/rules/logical-assignment-operators.js @@ -150,6 +150,31 @@ function isInsideWithBlock(node) { return node.parent.type === "WithStatement" && node.parent.body === node ? true : isInsideWithBlock(node.parent); } +/** + * Gets the leftmost operand of a consecutive logical expression. + * @param {SourceCode} sourceCode The ESLint source code object + * @param {LogicalExpression} node LogicalExpression + * @returns {Expression} Leftmost operand + */ +function getLeftmostOperand(sourceCode, node) { + let left = node.left; + + while (left.type === "LogicalExpression" && left.operator === node.operator) { + + if (astUtils.isParenthesised(sourceCode, left)) { + + /* + * It should have associativity, + * but ignore it if use parentheses to make the evaluation order clear. + */ + return left; + } + left = left.left; + } + return left; + +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -318,7 +343,10 @@ module.exports = { // foo = foo || bar "AssignmentExpression[operator='='][right.type='LogicalExpression']"(assignment) { - if (!astUtils.isSameReference(assignment.left, assignment.right.left)) { + const leftOperand = getLeftmostOperand(sourceCode, assignment.right); + + if (!astUtils.isSameReference(assignment.left, leftOperand) + ) { return; } @@ -342,10 +370,10 @@ module.exports = { yield ruleFixer.insertTextBefore(assignmentOperatorToken, assignment.right.operator); // -> foo ||= bar - const logicalOperatorToken = getOperatorToken(assignment.right); + const logicalOperatorToken = getOperatorToken(leftOperand.parent); const firstRightOperandToken = sourceCode.getTokenAfter(logicalOperatorToken); - yield ruleFixer.removeRange([assignment.right.range[0], firstRightOperandToken.range[0]]); + yield ruleFixer.removeRange([leftOperand.parent.range[0], firstRightOperandToken.range[0]]); } }; diff --git a/package.json b/package.json index 989e3528a479..ece3957419ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "8.50.0", + "version": "8.51.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { @@ -63,7 +63,7 @@ "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.50.0", + "@eslint/js": "8.51.0", "@humanwhocodes/config-array": "^0.11.11", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", diff --git a/packages/js/package.json b/packages/js/package.json index fc5fb50fcb9a..7e43db7f2e9a 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/js", - "version": "8.50.0", + "version": "8.51.0", "description": "ESLint JavaScript language implementation", "main": "./src/index.js", "scripts": {}, diff --git a/tests/bin/eslint.js b/tests/bin/eslint.js index b09496202b03..dca8955d0386 100644 --- a/tests/bin/eslint.js +++ b/tests/bin/eslint.js @@ -387,6 +387,36 @@ describe("bin/eslint.js", () => { return Promise.all([exitCodeAssertion, outputAssertion]); }); + // https://github.com/eslint/eslint/issues/17560 + describe("does not print duplicate errors in the event of a crash", () => { + + it("when there is an invalid config read from a config file", () => { + const config = path.join(__dirname, "../fixtures/bin/eslint.config-invalid.js"); + const child = runESLint(["--config", config, "conf", "tools"]); + const exitCodeAssertion = assertExitCode(child, 2); + const outputAssertion = getOutput(child).then(output => { + + // The error text should appear exactly once in stderr + assert.strictEqual(output.stderr.match(/A config object is using the "globals" key/gu).length, 1); + }); + + return Promise.all([exitCodeAssertion, outputAssertion]); + }); + + it("when there is an error in the next tick", () => { + const config = path.join(__dirname, "../fixtures/bin/eslint.config-tick-throws.js"); + const child = runESLint(["--config", config, "Makefile.js"]); + const exitCodeAssertion = assertExitCode(child, 2); + const outputAssertion = getOutput(child).then(output => { + + // The error text should appear exactly once in stderr + assert.strictEqual(output.stderr.match(/test_error_stack/gu).length, 1); + }); + + return Promise.all([exitCodeAssertion, outputAssertion]); + }); + }); + it("prints the error message pointing to line of code", () => { const invalidConfig = path.join(__dirname, "../fixtures/bin/eslint.config.js"); const child = runESLint(["--no-ignore", "-c", invalidConfig]); diff --git a/tests/fixtures/bin/eslint.config-invalid.js b/tests/fixtures/bin/eslint.config-invalid.js new file mode 100644 index 000000000000..6f68e1784199 --- /dev/null +++ b/tests/fixtures/bin/eslint.config-invalid.js @@ -0,0 +1,3 @@ +module.exports = [{ + globals: {} +}]; diff --git a/tests/fixtures/bin/eslint.config-tick-throws.js b/tests/fixtures/bin/eslint.config-tick-throws.js new file mode 100644 index 000000000000..c72f86a840f7 --- /dev/null +++ b/tests/fixtures/bin/eslint.config-tick-throws.js @@ -0,0 +1,24 @@ +function throwError() { + const error = new Error(); + error.stack = "test_error_stack"; + throw error; +} + +module.exports = [{ + plugins: { + foo: { + rules: { + bar: { + create() { + process.nextTick(throwError); + process.nextTick(throwError); + return {}; + } + } + } + } + }, + rules: { + "foo/bar": "error" + } +}]; diff --git a/tests/fixtures/code-path-analysis/assignment--nested-and-3.js b/tests/fixtures/code-path-analysis/assignment--nested-and-3.js index 8bb52f022395..09ca58fade56 100644 --- a/tests/fixtures/code-path-analysis/assignment--nested-and-3.js +++ b/tests/fixtures/code-path-analysis/assignment--nested-and-3.js @@ -1,7 +1,8 @@ /*expected initial->s1_1->s1_2->s1_3->s1_4; -s1_1->s1_4; -s1_2->s1_4->final; +s1_1->s1_3; +s1_2->s1_4; +s1_1->s1_4->final; */ (a &&= b) ?? c; @@ -15,7 +16,8 @@ digraph { s1_3[label="Identifier (c)"]; s1_4[label="LogicalExpression:exit\nExpressionStatement:exit\nProgram:exit"]; initial->s1_1->s1_2->s1_3->s1_4; - s1_1->s1_4; - s1_2->s1_4->final; + s1_1->s1_3; + s1_2->s1_4; + s1_1->s1_4->final; } */ diff --git a/tests/fixtures/code-path-analysis/logical--and-qq.js b/tests/fixtures/code-path-analysis/logical--and-qq.js new file mode 100644 index 000000000000..5ce3853a240f --- /dev/null +++ b/tests/fixtures/code-path-analysis/logical--and-qq.js @@ -0,0 +1,22 @@ +/*expected +initial->s1_1->s1_2->s1_3->s1_4; +s1_1->s1_3; +s1_2->s1_4; +s1_1->s1_4->final; +*/ +(a && b) ?? c; + +/*DOT +digraph { + node[shape=box,style="rounded,filled",fillcolor=white]; + initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; + final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25]; + s1_1[label="Program:enter\nExpressionStatement:enter\nLogicalExpression:enter\nLogicalExpression:enter\nIdentifier (a)"]; + s1_2[label="Identifier (b)\nLogicalExpression:exit"]; + s1_3[label="Identifier (c)"]; + s1_4[label="LogicalExpression:exit\nExpressionStatement:exit\nProgram:exit"]; + initial->s1_1->s1_2->s1_3->s1_4; + s1_1->s1_3; + s1_2->s1_4; + s1_1->s1_4->final; +}*/ diff --git a/tests/fixtures/code-path-analysis/logical--if-mix-and-qq-1.js b/tests/fixtures/code-path-analysis/logical--if-mix-and-qq-1.js index 4863ac81db3b..427cc22ec5a5 100644 --- a/tests/fixtures/code-path-analysis/logical--if-mix-and-qq-1.js +++ b/tests/fixtures/code-path-analysis/logical--if-mix-and-qq-1.js @@ -1,8 +1,9 @@ /*expected initial->s1_1->s1_2->s1_3->s1_4->s1_6; -s1_1->s1_5->s1_6; +s1_1->s1_3; s1_2->s1_4; -s1_3->s1_5; +s1_3->s1_5->s1_6; +s1_1->s1_5; s1_2->s1_5; s1_6->final; */ @@ -24,9 +25,10 @@ digraph { s1_6[label="IfStatement:exit\nProgram:exit"]; s1_5[label="BlockStatement\nExpressionStatement\nCallExpression\nIdentifier (bar)\nIdentifier:exit (bar)\nCallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"]; initial->s1_1->s1_2->s1_3->s1_4->s1_6; - s1_1->s1_5->s1_6; + s1_1->s1_3; s1_2->s1_4; - s1_3->s1_5; + s1_3->s1_5->s1_6; + s1_1->s1_5; s1_2->s1_5; s1_6->final; } diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 15556e5cfd1e..ad0b8ad23272 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -801,6 +801,32 @@ describe("cli", () => { assert.isFalse(log.info.called); assert.strictEqual(exit, 0); }); + + it(`should suppress the warning if --no-warn-ignored is passed with configType:${configType}`, async () => { + const options = useFlatConfig + ? `--config ${getFixturePath("eslint.config_with_ignores.js")}` + : `--ignore-path ${getFixturePath(".eslintignore")}`; + const filePath = getFixturePath("passing.js"); + const exit = await cli.execute(`${options} --no-warn-ignored ${filePath}`, null, useFlatConfig); + + assert.isFalse(log.info.called); + + // When eslintrc is used, we get an exit code of 2 because the --no-warn-ignored option is unrecognized. + assert.strictEqual(exit, useFlatConfig ? 0 : 2); + }); + + it(`should suppress the warning if --no-warn-ignored is passed and an ignored file is passed via stdin with configType:${configType}`, async () => { + const options = useFlatConfig + ? `--config ${getFixturePath("eslint.config_with_ignores.js")}` + : `--ignore-path ${getFixturePath(".eslintignore")}`; + const filePath = getFixturePath("passing.js"); + const exit = await cli.execute(`${options} --no-warn-ignored --stdin --stdin-filename ${filePath}`, "foo", useFlatConfig); + + assert.isFalse(log.info.called); + + // When eslintrc is used, we get an exit code of 2 because the --no-warn-ignored option is unrecognized. + assert.strictEqual(exit, useFlatConfig ? 0 : 2); + }); }); describe("when given a pattern to ignore", () => { diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index 358882374831..728b3e93785c 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -1726,6 +1726,17 @@ describe("FlatConfigArray", () => { ], "Key \"rules\": Key \"foo\": Expected severity of \"off\", 0, \"warn\", 1, \"error\", or 2."); }); + it("should error when a string rule severity is not in lowercase", async () => { + + await assertInvalidConfig([ + { + rules: { + foo: "Error" + } + } + ], "Key \"rules\": Key \"foo\": Expected severity of \"off\", 0, \"warn\", 1, \"error\", or 2."); + }); + it("should error when an invalid rule severity is set in an array", async () => { await assertInvalidConfig([ diff --git a/tests/lib/eslint/flat-eslint.js b/tests/lib/eslint/flat-eslint.js index f67e62e5c63c..d20dce0c3195 100644 --- a/tests/lib/eslint/flat-eslint.js +++ b/tests/lib/eslint/flat-eslint.js @@ -211,7 +211,8 @@ describe("FlatESLint", () => { overrideConfig: "", overrideConfigFile: "", plugins: "", - reportUnusedDisableDirectives: "" + reportUnusedDisableDirectives: "", + warnIgnored: "" }), new RegExp(escapeStringRegExp([ "Invalid Options:", @@ -229,7 +230,8 @@ describe("FlatESLint", () => { "- 'overrideConfig' must be an object or null.", "- 'overrideConfigFile' must be a non-empty string, null, or true.", "- 'plugins' must be an object or null.", - "- 'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null." + "- 'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null.", + "- 'warnIgnored' must be a boolean." ].join("\n")), "u") ); }); @@ -369,7 +371,31 @@ describe("FlatESLint", () => { assert.strictEqual(results.length, 1); assert.strictEqual(results[0].filePath, getFixturePath("passing.js")); assert.strictEqual(results[0].messages[0].severity, 1); - assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."); + assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."); + assert.strictEqual(results[0].messages[0].output, void 0); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + assert.strictEqual(results[0].fatalErrorCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + assert.strictEqual(results[0].suppressedMessages.length, 0); + }); + + it("should return a warning when given a filename by --stdin-filename in excluded files list if constructor warnIgnored is false, but lintText warnIgnored is true", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath(".."), + overrideConfigFile: "fixtures/eslint.config_with_ignores.js", + warnIgnored: false + }); + + const options = { filePath: "fixtures/passing.js", warnIgnored: true }; + const results = await eslint.lintText("var bar = foo;", options); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, getFixturePath("passing.js")); + assert.strictEqual(results[0].messages[0].severity, 1); + assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."); assert.strictEqual(results[0].messages[0].output, void 0); assert.strictEqual(results[0].errorCount, 0); assert.strictEqual(results[0].warningCount, 1); @@ -397,18 +423,31 @@ describe("FlatESLint", () => { assert.strictEqual(results.length, 0); }); - it("should suppress excluded file warnings by default", async () => { + it("should not return a warning when given a filename by --stdin-filename in excluded files list if constructor warnIgnored is false", async () => { eslint = new FlatESLint({ cwd: getFixturePath(".."), - overrideConfigFile: "fixtures/eslint.config_with_ignores.js" + overrideConfigFile: "fixtures/eslint.config_with_ignores.js", + warnIgnored: false }); const options = { filePath: "fixtures/passing.js" }; const results = await eslint.lintText("var bar = foo;", options); - // should not report anything because there are no errors + // should not report anything because the warning is suppressed assert.strictEqual(results.length, 0); }); + it("should show excluded file warnings by default", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath(".."), + overrideConfigFile: "fixtures/eslint.config_with_ignores.js" + }); + const options = { filePath: "fixtures/passing.js" }; + const results = await eslint.lintText("var bar = foo;", options); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."); + }); + it("should return a message when given a filename by --stdin-filename in excluded files list and ignore is off", async () => { eslint = new FlatESLint({ cwd: getFixturePath(".."), @@ -685,7 +724,7 @@ describe("FlatESLint", () => { ignore: false }); const results = await eslint.lintText("var bar = foo;", { filePath: "node_modules/passing.js", warnIgnored: true }); - const expectedMsg = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to override."; + const expectedMsg = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."; assert.strictEqual(results.length, 1); assert.strictEqual(results[0].filePath, getFixturePath("node_modules/passing.js")); @@ -1311,7 +1350,7 @@ describe("FlatESLint", () => { cwd: getFixturePath("cli-engine") }); const results = await eslint.lintFiles(["node_modules/foo.js"]); - const expectedMsg = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to override."; + const expectedMsg = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."; assert.strictEqual(results.length, 1); assert.strictEqual(results[0].errorCount, 0); @@ -1329,7 +1368,7 @@ describe("FlatESLint", () => { cwd: getFixturePath("cli-engine") }); const results = await eslint.lintFiles(["nested_node_modules/subdir/node_modules/text.js"]); - const expectedMsg = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to override."; + const expectedMsg = "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."; assert.strictEqual(results.length, 1); assert.strictEqual(results[0].errorCount, 0); @@ -1348,7 +1387,7 @@ describe("FlatESLint", () => { ignorePatterns: ["*.js"] }); const results = await eslint.lintFiles(["node_modules_cleaner.js"]); - const expectedMsg = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."; + const expectedMsg = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."; assert.strictEqual(results.length, 1); assert.strictEqual(results[0].errorCount, 0); @@ -1361,6 +1400,16 @@ describe("FlatESLint", () => { assert.strictEqual(results[0].suppressedMessages.length, 0); }); + it("should suppress the warning when a file in the node_modules folder passed explicitly and warnIgnored is false", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath("cli-engine"), + warnIgnored: false + }); + const results = await eslint.lintFiles(["node_modules/foo.js"]); + + assert.strictEqual(results.length, 0); + }); + it("should report on globs with explicit inclusion of dotfiles", async () => { eslint = new FlatESLint({ cwd: getFixturePath("cli-engine"), @@ -1483,7 +1532,7 @@ describe("FlatESLint", () => { assert.strictEqual(results.length, 1); assert.strictEqual(results[0].filePath, filePath); assert.strictEqual(results[0].messages[0].severity, 1); - assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."); + assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."); assert.strictEqual(results[0].errorCount, 0); assert.strictEqual(results[0].warningCount, 1); assert.strictEqual(results[0].fatalErrorCount, 0); @@ -1492,6 +1541,18 @@ describe("FlatESLint", () => { assert.strictEqual(results[0].suppressedMessages.length, 0); }); + it("should suppress the warning when an explicitly given file is ignored and warnIgnored is false", async () => { + eslint = new FlatESLint({ + overrideConfigFile: "eslint.config_with_ignores.js", + cwd: getFixturePath(), + warnIgnored: false + }); + const filePath = getFixturePath("passing.js"); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 0); + }); + it("should return a warning about matching ignore patterns when an explicitly given dotfile is ignored", async () => { eslint = new FlatESLint({ overrideConfigFile: "eslint.config_with_ignores.js", @@ -1503,7 +1564,7 @@ describe("FlatESLint", () => { assert.strictEqual(results.length, 1); assert.strictEqual(results[0].filePath, filePath); assert.strictEqual(results[0].messages[0].severity, 1); - assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."); + assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."); assert.strictEqual(results[0].errorCount, 0); assert.strictEqual(results[0].warningCount, 1); assert.strictEqual(results[0].fatalErrorCount, 0); @@ -5400,7 +5461,7 @@ describe("FlatESLint", () => { { ruleId: null, fatal: false, - message: "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to override.", + message: "File ignored by default because it is located under the node_modules directory. Use ignore pattern \"!**/node_modules/\" to disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning.", severity: 1, nodeType: null } diff --git a/tests/lib/linter/config-comment-parser.js b/tests/lib/linter/config-comment-parser.js index e9f595d77504..39b06b13cf6e 100644 --- a/tests/lib/linter/config-comment-parser.js +++ b/tests/lib/linter/config-comment-parser.js @@ -224,6 +224,28 @@ describe("ConfigCommentParser", () => { b: true }); }); + + it("should parse list config with quoted items", () => { + const code = "'a', \"b\", 'c\", \"d'"; + const result = commentParser.parseListConfig(code); + + assert.deepStrictEqual(result, { + a: true, + b: true, + "\"d'": true, // This result is correct because used mismatched quotes. + "'c\"": true // This result is correct because used mismatched quotes. + }); + }); + it("should parse list config with spaced items", () => { + const code = " a b , 'c d' , \"e f\" "; + const result = commentParser.parseListConfig(code); + + assert.deepStrictEqual(result, { + "a b": true, + "c d": true, + "e f": true + }); + }); }); }); diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 5b3d06826126..c8fd4134caa6 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -1467,6 +1467,32 @@ describe("Linter", () => { linter.verify(code, config); assert(spy && spy.calledOnce); }); + + it("variables should be available in global scope with quoted items", () => { + const code = `/*${ESLINT_ENV} 'node'*/ function f() {} /*${ESLINT_ENV} "browser", "mocha"*/`; + const config = { rules: { checker: "error" } }; + let spy; + + linter.defineRule("checker", { + create(context) { + spy = sinon.spy(() => { + const scope = context.getScope(), + exports = getVariable(scope, "exports"), + window = getVariable(scope, "window"), + it = getVariable(scope, "it"); + + assert.strictEqual(exports.writeable, true); + assert.strictEqual(window.writeable, false); + assert.strictEqual(it.writeable, false); + }); + + return { Program: spy }; + } + }); + + linter.verify(code, config); + assert(spy && spy.calledOnce); + }); }); describe("when evaluating code containing /*eslint-env */ block with sloppy whitespace", () => { @@ -1906,6 +1932,22 @@ describe("Linter", () => { assert.strictEqual(suppressedMessages.length, 0); }); + it("should enable rule configured using a string severity that contains uppercase letters", () => { + const code = "/*eslint no-alert: \"Error\"*/ alert('test');"; + const config = { rules: {} }; + + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, "Unexpected alert."); + assert.include(messages[0].nodeType, "CallExpression"); + + assert.strictEqual(suppressedMessages.length, 0); + }); + it("rules should not change initial config", () => { const config = { rules: { strict: 2 } }; const codeA = "/*eslint strict: 0*/ function bar() { return 2; }"; @@ -2604,6 +2646,33 @@ describe("Linter", () => { assert.strictEqual(suppressedMessages.length, 2); assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); }); + + it("should report a violation with quoted rule names in eslint-disable-line", () => { + const code = [ + "alert('test'); // eslint-disable-line 'no-alert'", + "console.log('test');", // here + "alert('test'); // eslint-disable-line \"no-alert\"" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + assert.strictEqual(messages[0].line, 2); + + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].line, 1); + assert.strictEqual(suppressedMessages[1].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].line, 3); + }); }); describe("eslint-disable-next-line", () => { @@ -2957,6 +3026,31 @@ describe("Linter", () => { assert.strictEqual(suppressedMessages.length, 0); }); + + it("should ignore violation of specified rule on next line with quoted rule names", () => { + const code = [ + "// eslint-disable-next-line 'no-alert'", + "alert('test');", + "// eslint-disable-next-line \"no-alert\"", + "alert('test');", + "console.log('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].ruleId, "no-alert"); + }); }); }); @@ -3233,6 +3327,61 @@ describe("Linter", () => { assert.strictEqual(suppressedMessages[2].ruleId, "no-console"); assert.strictEqual(suppressedMessages[2].line, 6); }); + + it("should report a violation with quoted rule names in eslint-disable", () => { + const code = [ + "/*eslint-disable 'no-alert' */", + "alert('test');", + "console.log('test');", // here + "/*eslint-enable */", + "/*eslint-disable \"no-console\" */", + "alert('test');", // here + "console.log('test');" + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; + + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-console"); + assert.strictEqual(messages[1].ruleId, "no-alert"); + + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); + }); + + it("should report a violation with quoted rule names in eslint-enable", () => { + const code = [ + "/*eslint-disable no-alert, no-console */", + "alert('test');", + "console.log('test');", + "/*eslint-enable 'no-alert'*/", + "alert('test');", // here + "console.log('test');", + "/*eslint-enable \"no-console\"*/", + "console.log('test');" // here + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; + + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].line, 5); + assert.strictEqual(messages[1].ruleId, "no-console"); + assert.strictEqual(messages[1].line, 8); + + assert.strictEqual(suppressedMessages.length, 3); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].line, 2); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[1].line, 3); + assert.strictEqual(suppressedMessages[2].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[2].line, 6); + }); }); describe("when evaluating code with comments to enable and disable multiple comma separated rules", () => { @@ -4813,6 +4962,20 @@ var a = "test2"; output ); }); + + // Test for quoted rule names + for (const testcaseForLiteral of [ + { code: code.replace(/((?:un)?used[\w-]*)/gu, '"$1"'), output: output.replace(/((?:un)?used[\w-]*)/gu, '"$1"') }, + { code: code.replace(/((?:un)?used[\w-]*)/gu, "'$1'"), output: output.replace(/((?:un)?used[\w-]*)/gu, "'$1'") } + ]) { + // eslint-disable-next-line no-loop-func -- `linter` is getting updated in beforeEach() + it(testcaseForLiteral.code, () => { + assert.strictEqual( + linter.verifyAndFix(testcaseForLiteral.code, config).output, + testcaseForLiteral.output + ); + }); + } } }); }); @@ -12206,6 +12369,29 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(suppressedMessages.length, 0); }); + it("should report a violation when a rule is configured using a string severity that contains uppercase letters", () => { + const messages = linter.verify("/*eslint no-alert: \"Error\"*/ alert('test');", {}); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual( + messages, + [ + { + severity: 2, + ruleId: "no-alert", + message: "Inline configuration for rule \"no-alert\" is invalid:\n\tExpected severity of \"off\", 0, \"warn\", 1, \"error\", or 2. You passed \"Error\".\n", + line: 1, + column: 1, + endLine: 1, + endColumn: 29, + nodeType: null + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + it("should report a violation when the config violates a rule's schema", () => { const messages = linter.verify("/* eslint no-alert: [error, {nonExistentPropertyName: true}]*/", {}); const suppressedMessages = linter.getSuppressedMessages(); @@ -13060,6 +13246,60 @@ var a = "test2"; assert.strictEqual(suppressedMessages[1].line, 3); }); + it("should report a violation with quoted rule names in eslint-disable", () => { + const code = [ + "/*eslint-disable 'no-alert' */", + "alert('test');", + "console.log('test');", // here + "/*eslint-enable */", + "/*eslint-disable \"no-console\" */", + "alert('test');", // here + "console.log('test');" + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; + + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-console"); + assert.strictEqual(messages[1].ruleId, "no-alert"); + + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); + }); + + it("should report a violation with quoted rule names in eslint-enable", () => { + const code = [ + "/*eslint-disable no-alert, no-console */", + "alert('test');", + "console.log('test');", + "/*eslint-enable 'no-alert'*/", + "alert('test');", // here + "console.log('test');", + "/*eslint-enable \"no-console\"*/", + "console.log('test');" // here + ].join("\n"); + const config = { rules: { "no-alert": 1, "no-console": 1 } }; + + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].ruleId, "no-alert"); + assert.strictEqual(messages[0].line, 5); + assert.strictEqual(messages[1].ruleId, "no-console"); + assert.strictEqual(messages[1].line, 8); + + assert.strictEqual(suppressedMessages.length, 3); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].line, 2); + assert.strictEqual(suppressedMessages[1].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[1].line, 3); + assert.strictEqual(suppressedMessages[2].ruleId, "no-console"); + assert.strictEqual(suppressedMessages[2].line, 6); + }); }); describe("/*eslint-disable-line*/", () => { @@ -13293,6 +13533,32 @@ var a = "test2"; assert.strictEqual(suppressedMessages.length, 5); }); + it("should report a violation with quoted rule names in eslint-disable-line", () => { + const code = [ + "alert('test'); // eslint-disable-line 'no-alert'", + "console.log('test');", // here + "alert('test'); // eslint-disable-line \"no-alert\"" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + assert.strictEqual(messages[0].line, 2); + + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[0].line, 1); + assert.strictEqual(suppressedMessages[1].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].line, 3); + }); }); describe("/*eslint-disable-next-line*/", () => { @@ -13689,6 +13955,31 @@ var a = "test2"; assert.strictEqual(suppressedMessages.length, 0); }); + + it("should ignore violation of specified rule on next line with quoted rule names", () => { + const code = [ + "// eslint-disable-next-line 'no-alert'", + "alert('test');", + "// eslint-disable-next-line \"no-alert\"", + "alert('test');", + "console.log('test');" + ].join("\n"); + const config = { + rules: { + "no-alert": 1, + "no-console": 1 + } + }; + const messages = linter.verify(code, config, filename); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + assert.strictEqual(suppressedMessages[1].ruleId, "no-alert"); + }); }); describe("descriptions in directive comments", () => { @@ -15366,6 +15657,20 @@ var a = "test2"; output ); }); + + // Test for quoted rule names + for (const testcaseForLiteral of [ + { code: code.replace(/(test\/[\w-]+)/gu, '"$1"'), output: output.replace(/(test\/[\w-]+)/gu, '"$1"') }, + { code: code.replace(/(test\/[\w-]+)/gu, "'$1'"), output: output.replace(/(test\/[\w-]+)/gu, "'$1'") } + ]) { + // eslint-disable-next-line no-loop-func -- `linter` is getting updated in beforeEach() + it(testcaseForLiteral.code, () => { + assert.strictEqual( + linter.verifyAndFix(testcaseForLiteral.code, config).output, + testcaseForLiteral.output + ); + }); + } } }); }); @@ -15627,7 +15932,7 @@ var a = "test2"; const code = BROKEN_TEST_CODE; it("should report a violation with a useful parse error prefix", () => { - const messages = linter.verify(code); + const messages = linter.verify(code, {}); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); @@ -15648,7 +15953,7 @@ var a = "test2"; " x++;", "}" ]; - const messages = linter.verify(inValidCode.join("\n")); + const messages = linter.verify(inValidCode.join("\n"), {}); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); @@ -16584,7 +16889,7 @@ var a = "test2"; }); it("should not crash when invalid parentheses syntax is encountered", () => { - linter.verify("left = (aSize.width/2) - ()"); + linter.verify("left = (aSize.width/2) - ()", {}); }); it("should not crash when let is used inside of switch case", () => { diff --git a/tests/lib/options.js b/tests/lib/options.js index d8f795b78a22..b663e8623e36 100644 --- a/tests/lib/options.js +++ b/tests/lib/options.js @@ -415,4 +415,18 @@ describe("options", () => { }); }); + describe("--no-warn-ignored", () => { + it("should return false when --no-warn-ignored is passed", () => { + const currentOptions = flatOptions.parse("--no-warn-ignored"); + + assert.isFalse(currentOptions.warnIgnored); + }); + + it("should return true when --warn-ignored is passed", () => { + const currentOptions = flatOptions.parse("--warn-ignored"); + + assert.isTrue(currentOptions.warnIgnored); + }); + }); + }); diff --git a/tests/lib/rules/logical-assignment-operators.js b/tests/lib/rules/logical-assignment-operators.js index 36756815a90f..471416322d2b 100644 --- a/tests/lib/rules/logical-assignment-operators.js +++ b/tests/lib/rules/logical-assignment-operators.js @@ -354,6 +354,28 @@ ruleTester.run("logical-assignment-operators", rule, { }, { code: "a.b = a.b || c", options: ["never"] + }, + + // 3 or more operands + { + code: "a = a && b || c", + options: ["always"] + }, + { + code: "a = a && b && c || d", + options: ["always"] + }, + { + code: "a = (a || b) || c", // Allow if parentheses are used. + options: ["always"] + }, + { + code: "a = (a && b) && c", // Allow if parentheses are used. + options: ["always"] + }, + { + code: "a = (a ?? b) ?? c", // Allow if parentheses are used. + options: ["always"] } ], invalid: [ @@ -1511,6 +1533,151 @@ ruleTester.run("logical-assignment-operators", rule, { output: "(a.b.c ||= d) as number" }] }] + }, + + // 3 or more operands + { + code: "a = a || b || c", + output: "a ||= b || c", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = a && b && c", + output: "a &&= b && c", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "&&=" }, + suggestions: [] + }] + }, + { + code: "a = a ?? b ?? c", + output: "a ??= b ?? c", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "??=" }, + suggestions: [] + }] + }, + { + code: "a = a || b && c", + output: "a ||= b && c", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = a || b || c || d", + output: "a ||= b || c || d", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = a && b && c && d", + output: "a &&= b && c && d", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "&&=" }, + suggestions: [] + }] + }, + { + code: "a = a ?? b ?? c ?? d", + output: "a ??= b ?? c ?? d", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "??=" }, + suggestions: [] + }] + }, + { + code: "a = a || b || c && d", + output: "a ||= b || c && d", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = a || b && c || d", + output: "a ||= b && c || d", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = (a) || b || c", + output: "a ||= b || c", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = a || (b || c) || d", + output: "a ||= (b || c) || d", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = (a || b || c)", + output: "a ||= (b || c)", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] + }, + { + code: "a = ((a) || (b || c) || d)", + output: "a ||= ((b || c) || d)", + options: ["always"], + errors: [{ + messageId: "assignment", + type: "AssignmentExpression", + data: { operator: "||=" }, + suggestions: [] + }] } ] });