From 239a7e27209a6b861d634b3ef245ebbb805793a3 Mon Sep 17 00:00:00 2001 From: gyeongwoo park <138835573+gwBear1@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:02:43 +0900 Subject: [PATCH 1/8] docs: Clarify the description of `sort-imports` options (#18198) * fix sort-imports doc * fix sort import's test cases * remove one useless test case * fix typo * applied review * better to clear description Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> * Better description for ignoreCase * write default option at the top of the option. * Apply suggestions from code review Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> --------- Co-authored-by: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> --- docs/src/rules/sort-imports.md | 102 +++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 23 deletions(-) diff --git a/docs/src/rules/sort-imports.md b/docs/src/rules/sort-imports.md index 762e4acfa43..fa284593207 100644 --- a/docs/src/rules/sort-imports.md +++ b/docs/src/rules/sort-imports.md @@ -192,40 +192,64 @@ import {b, a, c} from 'foo.js' ### `ignoreCase` -When `true` the rule ignores the case-sensitivity of the imports local name. +When `false` (default), uppercase letters of the alphabet must always precede lowercase letters. -Examples of **incorrect** code for this rule with the `{ "ignoreCase": true }` option: +When `true`, the rule ignores the case-sensitivity of the imports local name. + +Examples of **incorrect** code for this rule with the default `{ "ignoreCase": false }` option: ::: incorrect ```js -/*eslint sort-imports: ["error", { "ignoreCase": true }]*/ - -import B from 'foo.js'; +/*eslint sort-imports: ["error", { "ignoreCase": false }]*/ import a from 'bar.js'; +import B from 'foo.js'; +import c from 'baz.js'; ``` ::: -Examples of **correct** code for this rule with the `{ "ignoreCase": true }` option: +Examples of **correct** code for this rule with the default `{ "ignoreCase": false }` option: ::: correct ```js -/*eslint sort-imports: ["error", { "ignoreCase": true }]*/ - -import a from 'foo.js'; +/*eslint sort-imports: ["error", { "ignoreCase": false }]*/ import B from 'bar.js'; +import a from 'foo.js'; +import c from 'baz.js'; +``` + +::: + +Examples of **correct** code for this rule with `{ "ignoreCase": true }` option: + +::: correct + +```js +/*eslint sort-imports: ["error", { "ignoreCase": true }]*/ +import a from 'bar.js'; +import B from 'foo.js'; import c from 'baz.js'; ``` ::: -Default is `false`. +Examples of **incorrect** code for this rule with the `{ "ignoreCase": true }` option: + +::: incorrect + +```js +/*eslint sort-imports: ["error", { "ignoreCase": true }]*/ +import B from 'foo.js'; +import a from 'bar.js'; +``` + +::: ### `ignoreDeclarationSort` -Ignores the sorting of import declaration statements. +When `true`, the rule ignores the sorting of import declaration statements. Default is `false`. Examples of **incorrect** code for this rule with the default `{ "ignoreDeclarationSort": false }` option: @@ -239,18 +263,20 @@ import a from 'bar.js' ::: -Examples of **correct** code for this rule with the `{ "ignoreDeclarationSort": true }` option: +Examples of **correct** code for this rule with the default `{ "ignoreDeclarationSort": false }` option: ::: correct ```js -/*eslint sort-imports: ["error", { "ignoreDeclarationSort": true }]*/ -import a from 'foo.js' -import b from 'bar.js' +/*eslint sort-imports: ["error", { "ignoreDeclarationSort": false }]*/ +import a from 'bar.js'; +import b from 'foo.js'; ``` ::: +Examples of **correct** code for this rule with the `{ "ignoreDeclarationSort": true }` option: + ::: correct ```js @@ -261,11 +287,20 @@ import a from 'bar.js' ::: -Default is `false`. +Examples of **incorrect** code for this rule with the `{ "ignoreDeclarationSort": true }` option: + +::: incorrect + +```js +/*eslint sort-imports: ["error", { "ignoreDeclarationSort": true }]*/ +import {b, a, c} from 'foo.js'; +``` + +::: ### `ignoreMemberSort` -Ignores the member sorting within a `multiple` member import declaration. +When `true`, the rule ignores the member sorting within a `multiple` member import declaration. Default is `false`. Examples of **incorrect** code for this rule with the default `{ "ignoreMemberSort": false }` option: @@ -278,6 +313,17 @@ import {b, a, c} from 'foo.js' ::: +Examples of **correct** code for this rule with the default `{ "ignoreMemberSort": false }` option: + +::: correct + +```js +/*eslint sort-imports: ["error", { "ignoreMemberSort": false }]*/ +import {a, b, c} from 'foo.js'; +``` + +::: + Examples of **correct** code for this rule with the `{ "ignoreMemberSort": true }` option: ::: correct @@ -289,10 +335,24 @@ import {b, a, c} from 'foo.js' ::: -Default is `false`. +Examples of **incorrect** code for this rule with the `{ "ignoreMemberSort": true }` option: + +::: incorrect + +```js +/*eslint sort-imports: ["error", { "ignoreMemberSort": true }]*/ +import b from 'foo.js'; +import a from 'bar.js'; +``` + +::: ### `memberSyntaxSortOrder` +This option takes an array with four predefined elements, the order of elements specifies the order of import styles. + +Default order is `["none", "all", "multiple", "single"]`. + There are four different styles and the default member syntax sort order is: * `none` - import module without exported bindings. @@ -341,11 +401,9 @@ import {a, b} from 'foo.js'; ::: -Default is `["none", "all", "multiple", "single"]`. - ### `allowSeparatedGroups` -When `true` the rule checks the sorting of import declaration statements only for those that appear on consecutive lines. +When `true`, the rule checks the sorting of import declaration statements only for those that appear on consecutive lines. Default is `false`. In other words, a blank line or a comment line or line with any other statement after an import declaration statement will reset the sorting of import declaration statements. @@ -404,8 +462,6 @@ import a from 'baz.js'; ::: -Default is `false`. - ## When Not To Use It This rule is a formatting preference and not following it won't negatively affect the quality of your code. If alphabetizing imports isn't a part of your coding standards, then you can leave this rule disabled. From d363c51b177e085b011c7fde1c5a5a09b3db9cdb Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 22 Mar 2024 20:21:57 +0000 Subject: [PATCH 2/8] chore: package.json update for @eslint/js release --- packages/js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/package.json b/packages/js/package.json index 12ef8235578..8fd90b5961c 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/js", - "version": "9.0.0-beta.2", + "version": "9.0.0-rc.0", "description": "ESLint JavaScript language implementation", "main": "./src/index.js", "scripts": {}, From 297416d2b41f5880554d052328aa36cd79ceb051 Mon Sep 17 00:00:00 2001 From: Francesco Trotta Date: Fri, 22 Mar 2024 21:42:40 +0100 Subject: [PATCH 3/8] chore: package.json update for eslint-9.0.0-rc.0 (#18223) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9be1a67532d..788c44cead8 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^3.0.2", - "@eslint/js": "9.0.0-beta.2", + "@eslint/js": "9.0.0-rc.0", "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -76,7 +76,7 @@ "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.0", + "eslint-scope": "^8.0.1", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.1", "esquery": "^1.4.2", From 26010c209d2657cd401bf2550ba4f276cb318f7d Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 22 Mar 2024 20:54:12 +0000 Subject: [PATCH 4/8] Build: changelog update for 9.0.0-rc.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2169b2af1bb..3a9484074cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +v9.0.0-rc.0 - March 22, 2024 + +* [`297416d`](https://github.com/eslint/eslint/commit/297416d2b41f5880554d052328aa36cd79ceb051) chore: package.json update for eslint-9.0.0-rc.0 (#18223) (Francesco Trotta) +* [`d363c51`](https://github.com/eslint/eslint/commit/d363c51b177e085b011c7fde1c5a5a09b3db9cdb) chore: package.json update for @eslint/js release (Jenkins) +* [`239a7e2`](https://github.com/eslint/eslint/commit/239a7e27209a6b861d634b3ef245ebbb805793a3) docs: Clarify the description of `sort-imports` options (#18198) (gyeongwoo park) +* [`09bd7fe`](https://github.com/eslint/eslint/commit/09bd7fe09ad255a263286e90accafbe2bf04ccfc) feat!: move AST traversal into SourceCode (#18167) (Nicholas C. Zakas) +* [`b91f9dc`](https://github.com/eslint/eslint/commit/b91f9dc072f17f5ea79803deb86cf002d031b4cf) build: fix TypeError in prism-eslint-hooks.js (#18209) (Francesco Trotta) +* [`4769c86`](https://github.com/eslint/eslint/commit/4769c86cc16e0b54294c0a394a1ec7ed88fc334f) docs: fix incorrect example in `no-lone-blocks` (#18215) (Tanuj Kanti) +* [`1b841bb`](https://github.com/eslint/eslint/commit/1b841bb04ac642c5ee84d1e44be3e53317579526) chore: fix some comments (#18213) (avoidaway) +* [`b8fb572`](https://github.com/eslint/eslint/commit/b8fb57256103b908712302ccd508f464eff1c9dc) feat: add `reportUnusedFallthroughComment` option to no-fallthrough rule (#18188) (Kirk Waiblinger) +* [`ae8103d`](https://github.com/eslint/eslint/commit/ae8103de69c12c6e71644a1de9589644e6767d15) fix: load plugins in the CLI in flat config mode (#18185) (Francesco Trotta) +* [`5251327`](https://github.com/eslint/eslint/commit/5251327711a2d7083e3c629cb8e48d9d1e809add) docs: Update README (GitHub Actions Bot) +* [`29c3595`](https://github.com/eslint/eslint/commit/29c359599c2ddd168084a2c8cbca626c51d0dc13) chore: remove repetitive words (#18193) (cuithon) +* [`1dc8618`](https://github.com/eslint/eslint/commit/1dc861897e8b47280e878d609c13c9e41892f427) docs: Update README (GitHub Actions Bot) +* [`acc2e06`](https://github.com/eslint/eslint/commit/acc2e06edd55eaab58530d891c0a572c1f0ec453) chore: Introduce Knip (#18005) (Lars Kappert) + v9.0.0-beta.2 - March 8, 2024 * [`7509276`](https://github.com/eslint/eslint/commit/75092764db117252067558bd3fbbf0c66ac081b7) chore: upgrade @eslint/js@9.0.0-beta.2 (#18180) (Milos Djermanovic) From b185eb97ec60319cc39023e8615959dd598919ae Mon Sep 17 00:00:00 2001 From: Jenkins Date: Fri, 22 Mar 2024 20:54:13 +0000 Subject: [PATCH 5/8] 9.0.0-rc.0 --- docs/package.json | 2 +- docs/src/use/formatters/html-formatter-example.html | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package.json b/docs/package.json index d976a151996..3c90ccfa41a 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,7 +1,7 @@ { "name": "docs-eslint", "private": true, - "version": "9.0.0-beta.2", + "version": "9.0.0-rc.0", "description": "", "main": "index.js", "keywords": [], diff --git a/docs/src/use/formatters/html-formatter-example.html b/docs/src/use/formatters/html-formatter-example.html index 520b2bfc78c..d9a8be4e1a3 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

- 8 problems (4 errors, 4 warnings) - Generated on Fri Mar 08 2024 21:18:52 GMT+0000 (Coordinated Universal Time) + 8 problems (4 errors, 4 warnings) - Generated on Fri Mar 22 2024 20:54:14 GMT+0000 (Coordinated Universal Time)
diff --git a/package.json b/package.json index 788c44cead8..51b5f966a66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "9.0.0-beta.2", + "version": "9.0.0-rc.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { From d85c436353d566d261798c51dadb8ed50def1a7d Mon Sep 17 00:00:00 2001 From: Tanuj Kanti <86398394+Tanujkanti4441@users.noreply.github.com> Date: Sat, 23 Mar 2024 20:08:15 +0530 Subject: [PATCH 6/8] feat: use-isnan report NaN in `indexOf` and `lastIndexOf` with fromIndex (#18225) * fix: report NaN with fromIndex * revert known limitation title change * add tests with third argument --- docs/src/rules/use-isnan.md | 4 ++ lib/rules/use-isnan.js | 4 +- tests/lib/rules/use-isnan.js | 88 ++++++++++++++++++++++++++++++++++-- 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/docs/src/rules/use-isnan.md b/docs/src/rules/use-isnan.md index f3de5919d18..6097afb7d1f 100644 --- a/docs/src/rules/use-isnan.md +++ b/docs/src/rules/use-isnan.md @@ -226,6 +226,10 @@ var firstIndex = myArray.indexOf(NaN); var lastIndex = myArray.lastIndexOf(NaN); var indexWithSequenceExpression = myArray.indexOf((doStuff(), NaN)); + +var firstIndexFromSecondElement = myArray.indexOf(NaN, 1); + +var lastIndexFromSecondElement = myArray.lastIndexOf(NaN, 1); ``` ::: diff --git a/lib/rules/use-isnan.js b/lib/rules/use-isnan.js index 06b2284ecd5..5c0b65feefb 100644 --- a/lib/rules/use-isnan.js +++ b/lib/rules/use-isnan.js @@ -182,7 +182,7 @@ module.exports = { if ( (methodName === "indexOf" || methodName === "lastIndexOf") && - node.arguments.length === 1 && + node.arguments.length <= 2 && isNaNIdentifier(node.arguments[0]) ) { @@ -190,7 +190,7 @@ module.exports = { * To retain side effects, it's essential to address `NaN` beforehand, which * is not possible with fixes like `arr.findIndex(Number.isNaN)`. */ - const isSuggestable = node.arguments[0].type !== "SequenceExpression"; + const isSuggestable = node.arguments[0].type !== "SequenceExpression" && !node.arguments[1]; const suggestedFixes = []; if (isSuggestable) { diff --git a/tests/lib/rules/use-isnan.js b/tests/lib/rules/use-isnan.js index 6cb61821952..3a12ffadb35 100644 --- a/tests/lib/rules/use-isnan.js +++ b/tests/lib/rules/use-isnan.js @@ -263,7 +263,7 @@ ruleTester.run("use-isnan", rule, { options: [{ enforceForIndexOf: true }] }, { - code: "foo.lastIndexOf(NaN, b)", + code: "foo.lastIndexOf(NaN, b, c)", options: [{ enforceForIndexOf: true }] }, { @@ -271,7 +271,7 @@ ruleTester.run("use-isnan", rule, { options: [{ enforceForIndexOf: true }] }, { - code: "foo.lastIndexOf(NaN, NaN)", + code: "foo.lastIndexOf(NaN, NaN, b)", options: [{ enforceForIndexOf: true }] }, { @@ -340,11 +340,11 @@ ruleTester.run("use-isnan", rule, { options: [{ enforceForIndexOf: true }] }, { - code: "foo.lastIndexOf(Number.NaN, b)", + code: "foo.lastIndexOf(Number.NaN, b, c)", options: [{ enforceForIndexOf: true }] }, { - code: "foo.lastIndexOf(Number.NaN, NaN)", + code: "foo.lastIndexOf(Number.NaN, NaN, b)", options: [{ enforceForIndexOf: true }] }, { @@ -1289,6 +1289,86 @@ ruleTester.run("use-isnan", rule, { data: { methodName: "lastIndexOf" }, suggestions: [] }] + }, + { + code: "foo.indexOf(NaN, 1)", + options: [{ enforceForIndexOf: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "indexOf" }, + suggestions: [] + }] + }, + { + code: "foo.lastIndexOf(NaN, 1)", + options: [{ enforceForIndexOf: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "lastIndexOf" }, + suggestions: [] + }] + }, + { + code: "foo.indexOf(NaN, b)", + options: [{ enforceForIndexOf: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "indexOf" }, + suggestions: [] + }] + }, + { + code: "foo.lastIndexOf(NaN, b)", + options: [{ enforceForIndexOf: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "lastIndexOf" }, + suggestions: [] + }] + }, + { + code: "foo.indexOf(Number.NaN, b)", + options: [{ enforceForIndexOf: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "indexOf" }, + suggestions: [] + }] + }, + { + code: "foo.lastIndexOf(Number.NaN, b)", + options: [{ enforceForIndexOf: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "lastIndexOf" }, + suggestions: [] + }] + }, + { + code: "foo.lastIndexOf(NaN, NaN)", + options: [{ enforceForIndexOf: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "lastIndexOf" }, + suggestions: [] + }] + }, + { + code: "foo.indexOf((1, NaN), 1)", + options: [{ enforceForIndexOf: true }], + languageOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "indexOfNaN", + data: { methodName: "indexOf" }, + suggestions: [] + }] } ] }); From de408743b5c3fc25ebd7ef5fb11ab49ab4d06c36 Mon Sep 17 00:00:00 2001 From: Mara Kiefer <8320933+mnkiefer@users.noreply.github.com> Date: Mon, 25 Mar 2024 19:14:19 +0100 Subject: [PATCH 7/8] feat: Rule Performance Statistics for flat ESLint (#17850) * Add stats option * Remove resetTimes, stats per file * Add suggestions from review * Update types & docs * Update times description * Add Stats Data page * Fix file verification issues * Use named exports * Update docs/src/extend/stats.md Co-authored-by: Milos Djermanovic * Update docs/src/extend/stats.md Co-authored-by: Milos Djermanovic * Update docs/src/extend/stats.md Co-authored-by: Milos Djermanovic * Update docs/src/extend/stats.md Co-authored-by: Milos Djermanovic * Update docs/src/extend/stats.md Co-authored-by: Milos Djermanovic * Update docs/src/extend/stats.md Co-authored-by: Milos Djermanovic * Update docs/src/extend/stats.md Co-authored-by: Milos Djermanovic * Add suggestions and new data * Update lib/shared/stats.js Co-authored-by: Milos Djermanovic * Update lib/shared/stats.js Co-authored-by: Milos Djermanovic * Update docs/src/extend/stats.md Co-authored-by: Milos Djermanovic * Update lib/shared/types.js Co-authored-by: Milos Djermanovic * Update docs/src/integrate/nodejs-api.md Co-authored-by: Milos Djermanovic * Update docs/src/extend/stats.md Co-authored-by: Milos Djermanovic * Update docs/src/extend/stats.md Co-authored-by: Milos Djermanovic * Update docs/src/extend/stats.md Co-authored-by: Milos Djermanovic * Update docs/src/extend/stats.md Co-authored-by: Milos Djermanovic * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic * Update lib/linter/linter.js Co-authored-by: Milos Djermanovic * Update tests/lib/eslint/eslint.js Co-authored-by: Milos Djermanovic * Update tests/fixtures/stats-example/file-to-fix.js Co-authored-by: Milos Djermanovic * Update docs/src/integrate/nodejs-api.md Co-authored-by: Milos Djermanovic * Update docs/src/use/command-line-interface.md Co-authored-by: Milos Djermanovic * Fix missing bracket * Update docs/src/use/command-line-interface.md Co-authored-by: Milos Djermanovic * Update stats tests * Update tests/lib/eslint/eslint.js Co-authored-by: Milos Djermanovic * Update docs/src/integrate/nodejs-api.md Co-authored-by: Milos Djermanovic * Fix missing properties * Remove trailing spaces * Update nodejs-api.md * Update docs/src/extend/stats.md Co-authored-by: Nicholas C. Zakas * Update docs/src/extend/stats.md Co-authored-by: Nicholas C. Zakas * Update docs/src/extend/stats.md Co-authored-by: Nicholas C. Zakas * Update docs/src/extend/stats.md Co-authored-by: Nicholas C. Zakas * Update stats.md * Add more docs --------- Co-authored-by: Milos Djermanovic Co-authored-by: Nicholas C. Zakas --- docs/src/extend/custom-formatters.md | 1 + docs/src/extend/custom-rules.md | 2 + docs/src/extend/stats.md | 139 +++++++++++++++++ docs/src/integrate/nodejs-api.md | 12 ++ docs/src/use/command-line-interface.md | 15 ++ lib/cli.js | 2 + lib/eslint/eslint-helpers.js | 5 + lib/eslint/eslint.js | 17 +- lib/linter/linter.js | 163 ++++++++++++++++++-- lib/linter/timing.js | 24 ++- lib/options.js | 15 +- lib/shared/stats.js | 30 ++++ lib/shared/types.js | 34 ++++ tests/fixtures/stats-example/file-to-fix.js | 5 + tests/lib/eslint/eslint.js | 68 ++++++++ tests/lib/options.js | 8 + 16 files changed, 521 insertions(+), 19 deletions(-) create mode 100644 docs/src/extend/stats.md create mode 100644 lib/shared/stats.js create mode 100644 tests/fixtures/stats-example/file-to-fix.js diff --git a/docs/src/extend/custom-formatters.md b/docs/src/extend/custom-formatters.md index b95db0f8bd6..4513b42e000 100644 --- a/docs/src/extend/custom-formatters.md +++ b/docs/src/extend/custom-formatters.md @@ -98,6 +98,7 @@ Each object in the `results` array is a `result` object. Each `result` object co * **messages**: An array of [`message`](#the-message-object) objects. See below for more info about messages. * **errorCount**: The number of errors for the given file. * **warningCount**: The number of warnings for the given file. +* **stats**: The optional [`stats`](./stats#-stats-type) object that only exists when the `stats` option is used. * **source**: The source code for the given file. This property is omitted if this file has no errors/warnings or if the `output` property is present. * **output**: The source code for the given file with as many fixes applied as possible. This property is omitted if no fix is available. diff --git a/docs/src/extend/custom-rules.md b/docs/src/extend/custom-rules.md index 9b99121d22e..84d2ca46a6f 100644 --- a/docs/src/extend/custom-rules.md +++ b/docs/src/extend/custom-rules.md @@ -938,3 +938,5 @@ quotes | 18.066 | 100.0% ``` To see a longer list of results (more than 10), set the environment variable to another value such as `TIMING=50` or `TIMING=all`. + +For more granular timing information (per file per rule), use the [`stats`](./stats) option instead. diff --git a/docs/src/extend/stats.md b/docs/src/extend/stats.md new file mode 100644 index 00000000000..3e54598ab6f --- /dev/null +++ b/docs/src/extend/stats.md @@ -0,0 +1,139 @@ +--- +title: Stats Data +eleventyNavigation: + key: stats data + parent: extend eslint + title: Stats Data + order: 6 +--- + +While an analysis of the overall rule performance for an ESLint run can be carried out by setting the [TIMING](./custom-rules#profile-rule-performance) environment variable, it can sometimes be useful to acquire more *granular* timing data (lint time per file per rule) or collect other measures of interest. In particular, when developing new [custom plugins](./plugins) and evaluating/benchmarking new languages or rule sets. For these use cases, you can optionally collect runtime statistics from ESLint. + +## Enable stats collection + +To enable collection of statistics, you can either: + +1. Use the `--stats` CLI option. This will pass the stats data into the formatter used to output results from ESLint. (Note: not all formatters output stats data.) +1. Set `stats: true` as an option on the `ESLint` constructor. + +Enabling stats data adds a new `stats` key to each [LintResult](../integrate/nodejs-api#-lintresult-type) object containing data such as parse times, fix times, lint times per rule. + +As such, it is not available via stdout but made easily ingestible via a formatter using the CLI or via the Node.js API to cater to your specific needs. + +## ◆ Stats type + +The `Stats` value is the timing information of each lint run. The `stats` property of the [LintResult](../integrate/nodejs-api#-lintresult-type) type contains it. It has the following properties: + +* `fixPasses` (`number`)
+ The number of times ESLint has applied at least one fix after linting. +* `times` (`{ passes: TimePass[] }`)
+ The times spent on (parsing, fixing, linting) a file, where the linting refers to the timing information for each rule. + * `TimePass` (`{ parse: ParseTime, rules?: Record, fix: FixTime, total: number }`)
+ An object containing the times spent on (parsing, fixing, linting) + * `ParseTime` (`{ total: number }`)
+ The total time that is spent when parsing a file. + * `RuleTime` (`{ total: number }`) + The total time that is spent on a rule. + * `FixTime` (`{ total: number }`) + The total time that is spent on applying fixes to the code. + +### CLI usage + +Let's consider the following example: + +```js [file-to-fix.js] +/*eslint no-regex-spaces: "error", wrap-regex: "error"*/ + +function a() { + return / foo/.test("bar"); +} +``` + +Run ESLint with `--stats` and output to JSON via the built-in [`json` formatter](../use/formatters/): + +```bash +npx eslint file-to-fix.js --fix --stats -f json +``` + +This yields the following `stats` entry as part of the formatted lint results object: + +```json +{ + "times": { + "passes": [ + { + "parse": { + "total": 3.975959 + }, + "rules": { + "no-regex-spaces": { + "total": 0.160792 + }, + "wrap-regex": { + "total": 0.422626 + } + }, + "fix": { + "total": 0.080208 + }, + "total": 12.765959 + }, + { + "parse": { + "total": 0.623542 + }, + "rules": { + "no-regex-spaces": { + "total": 0.043084 + }, + "wrap-regex": { + "total": 0.007959 + } + }, + "fix": { + "total": 0 + }, + "total": 1.148875 + } + ] + }, + "fixPasses": 1 +} +``` + +Note, that for the simple example above, the sum of all rule times should be directly comparable to the first column of the TIMING output. Running the same command with `TIMING=all`, you can verify this: + +```bash +$ TIMING=all npx eslint file-to-fix.js --fix --stats -f json +... +Rule | Time (ms) | Relative +:---------------|----------:|--------: +wrap-regex | 0.431 | 67.9% +no-regex-spaces | 0.204 | 32.1% +``` + +### API Usage + +You can achieve the same thing using the Node.js API by passing`stats: true` as an option to the `ESLint` constructor. For example: + +```js +const { ESLint } = require("eslint"); + +(async function main() { + // 1. Create an instance. + const eslint = new ESLint({ stats: true, fix: true }); + + // 2. Lint files. + const results = await eslint.lintFiles(["file-to-fix.js"]); + + // 3. Format the results. + const formatter = await eslint.loadFormatter("json"); + const resultText = formatter.format(results); + + // 4. Output it. + console.log(resultText); +})().catch((error) => { + process.exitCode = 1; + console.error(error); +}); +``` diff --git a/docs/src/integrate/nodejs-api.md b/docs/src/integrate/nodejs-api.md index fdc06daf12f..352d0a28f0a 100644 --- a/docs/src/integrate/nodejs-api.md +++ b/docs/src/integrate/nodejs-api.md @@ -150,6 +150,8 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob Default is `null`. The plugin implementations that ESLint uses for the `plugins` setting of your configuration. This is a map-like object. Those keys are plugin IDs and each value is implementation. * `options.ruleFilter` (`({ruleId: string, severity: number}) => boolean`)
Default is `() => true`. A predicate function that filters rules to be run. This function is called with an object containing `ruleId` and `severity`, and returns `true` if the rule should be run. +* `options.stats` (`boolean`)
+ Default is `false`. When set to `true`, additional statistics are added to the lint results (see [Stats type](../extend/stats#-stats-type)). ##### Autofix @@ -367,6 +369,8 @@ The `LintResult` value is the information of the linting result of each file. Th The modified source code text. This property is undefined if any fixable messages didn't exist. * `source` (`string | undefined`)
The original source code text. This property is undefined if any messages didn't exist or the `output` property exists. +* `stats` (`Stats | undefined`)
+ The [Stats](../extend/stats#-stats-type) object. This contains the lint performance statistics collected with the `stats` option. * `usedDeprecatedRules` (`{ ruleId: string; replacedBy: string[] }[]`)
The information about the deprecated rules that were used to check this file. @@ -715,6 +719,14 @@ const Linter = require("eslint").Linter; Linter.version; // => '9.0.0' ``` +### Linter#getTimes() + +This method is used to get the times spent on (parsing, fixing, linting) a file. See `times` property of the [Stats](../extend/stats#-stats-type) object. + +### Linter#getFixPassCount() + +This method is used to get the number of autofix passes made. See `fixPasses` property of the [Stats](../extend/stats#-stats-type) object. + --- ## RuleTester diff --git a/docs/src/use/command-line-interface.md b/docs/src/use/command-line-interface.md index f1ceddfd8f3..d4ec452611a 100644 --- a/docs/src/use/command-line-interface.md +++ b/docs/src/use/command-line-interface.md @@ -126,6 +126,7 @@ Miscellaneous: -h, --help Show help -v, --version Output the version number --print-config path::String Print the configuration for the given file + --stats Add statistics to the lint report - default: false ``` ### Basic Configuration @@ -811,6 +812,20 @@ This option outputs the configuration to be used for the file passed. When prese npx eslint --print-config file.js ``` +#### `--stats` + +This option adds a series of detailed performance statistics (see [Stats type](../extend/stats#-stats-type)) such as the *parse*-, *fix*- and *lint*-times (time per rule) to [`result`](../extend/custom-formatters#the-result-object) objects that are passed to the formatter (see [Stats CLI usage](../extend/stats#cli-usage)). + +* **Argument Type**: No argument. + +This option is intended for use with custom formatters that display statistics. It can also be used with the built-in `json` formatter. + +##### `--stats` example + +```shell +npx eslint --stats --format json file.js +``` + ## Exit Codes When linting files, ESLint exits with one of the following exit codes: diff --git a/lib/cli.js b/lib/cli.js index 20a007cbc7b..24d72d6e21a 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -131,6 +131,7 @@ async function translateOptions({ resolvePluginsRelativeTo, rule, rulesdir, + stats, warnIgnored, passOnNoPatterns, maxWarnings @@ -222,6 +223,7 @@ async function translateOptions({ if (configType === "flat") { options.ignorePatterns = ignorePattern; + options.stats = stats; options.warnIgnored = warnIgnored; /* diff --git a/lib/eslint/eslint-helpers.js b/lib/eslint/eslint-helpers.js index 0206177831c..44a5a253cd6 100644 --- a/lib/eslint/eslint-helpers.js +++ b/lib/eslint/eslint-helpers.js @@ -685,6 +685,7 @@ function processOptions({ overrideConfig = null, overrideConfigFile = null, plugins = {}, + stats = false, warnIgnored = true, passOnNoPatterns = false, ruleFilter = () => true, @@ -791,6 +792,9 @@ function processOptions({ if (Array.isArray(plugins)) { errors.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead."); } + if (typeof stats !== "boolean") { + errors.push("'stats' must be a boolean."); + } if (typeof warnIgnored !== "boolean") { errors.push("'warnIgnored' must be a boolean."); } @@ -818,6 +822,7 @@ function processOptions({ globInputPaths, ignore, ignorePatterns, + stats, passOnNoPatterns, warnIgnored, ruleFilter diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 42b8ddd2410..b4c38503a6e 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -84,6 +84,7 @@ const LintResultCache = require("../cli-engine/lint-result-cache"); * doesn't do any config file lookup when `true`; considered to be a config filename * when a string. * @property {Record} [plugins] An array of plugin implementations. + * @property {boolean} [stats] True enables added statistics on lint results. * @property {boolean} warnIgnored Show warnings when the file list includes ignored files * @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause * the linting operation to short circuit and not report any failures. @@ -465,6 +466,7 @@ async function calculateConfigArray(eslint, { * @param {boolean} config.fix If `true` then it does fix. * @param {boolean} config.allowInlineConfig If `true` then it uses directive comments. * @param {Function} config.ruleFilter A predicate function to filter which rules should be run. + * @param {boolean} config.stats If `true`, then if reports extra statistics with the lint results. * @param {Linter} config.linter The linter instance to verify. * @returns {LintResult} The result of linting. * @private @@ -477,6 +479,7 @@ function verifyText({ fix, allowInlineConfig, ruleFilter, + stats, linter }) { const filePath = providedFilePath || ""; @@ -497,6 +500,7 @@ function verifyText({ filename: filePathToVerify, fix, ruleFilter, + stats, /** * Check if the linter should adopt a given code block or not. @@ -528,6 +532,13 @@ function verifyText({ result.source = text; } + if (stats) { + result.stats = { + times: linter.getTimes(), + fixPasses: linter.getFixPassCount() + }; + } + return result; } @@ -808,6 +819,7 @@ class ESLint { fix, fixTypes, ruleFilter, + stats, globInputPaths, errorOnUnmatchedPattern, warnIgnored @@ -922,6 +934,7 @@ class ESLint { fix: fixer, allowInlineConfig, ruleFilter, + stats, linter }); @@ -1010,7 +1023,8 @@ class ESLint { cwd, fix, warnIgnored: constructorWarnIgnored, - ruleFilter + ruleFilter, + stats } = eslintOptions; const results = []; const startTime = Date.now(); @@ -1034,6 +1048,7 @@ class ESLint { fix, allowInlineConfig, ruleFilter, + stats, linter })); } diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 8ae8ad367a3..22cf17b54ba 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -41,6 +41,7 @@ const ruleReplacements = require("../../conf/replacements.json"); const { getRuleFromConfig } = require("../config/flat-config-helpers"); const { FlatConfigArray } = require("../config/flat-config-array"); +const { startTime, endTime } = require("../shared/stats"); const { RuleValidator } = require("../config/rule-validator"); const { assertIsRuleSeverity } = require("../config/flat-config-schema"); const { normalizeSeverityToString } = require("../shared/severity"); @@ -68,6 +69,7 @@ const STEP_KIND_CALL = 2; /** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */ /** @typedef {import("../shared/types").Processor} Processor */ /** @typedef {import("../shared/types").Rule} Rule */ +/** @typedef {import("../shared/types").Times} Times */ /* eslint-disable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */ /** @@ -92,6 +94,7 @@ const STEP_KIND_CALL = 2; * @property {SourceCode|null} lastSourceCode The `SourceCode` instance that the last `verify()` call used. * @property {SuppressedLintMessage[]} lastSuppressedMessages The `SuppressedLintMessage[]` instance that the last `verify()` call produced. * @property {Map} parserMap The loaded parsers. + * @property {Times} times The times spent on applying a rule to a file (see `stats` option). * @property {Rules} ruleMap The loaded rules. */ @@ -736,6 +739,7 @@ function normalizeVerifyOptions(providedOptions, config) { : null, reportUnusedDisableDirectives, disableFixes: Boolean(providedOptions.disableFixes), + stats: providedOptions.stats, ruleFilter }; } @@ -825,6 +829,36 @@ function stripUnicodeBOM(text) { return text; } +/** + * Store time measurements in map + * @param {number} time Time measurement + * @param {Object} timeOpts Options relating which time was measured + * @param {WeakMap} slots Linter internal slots map + * @returns {void} + */ +function storeTime(time, timeOpts, slots) { + const { type, key } = timeOpts; + + if (!slots.times) { + slots.times = { passes: [{}] }; + } + + const passIndex = slots.fixPasses; + + if (passIndex > slots.times.passes.length - 1) { + slots.times.passes.push({}); + } + + if (key) { + slots.times.passes[passIndex][type] ??= {}; + slots.times.passes[passIndex][type][key] ??= { total: 0 }; + slots.times.passes[passIndex][type][key].total += time; + } else { + slots.times.passes[passIndex][type] ??= { total: 0 }; + slots.times.passes[passIndex][type].total += time; + } +} + /** * Get the options for a rule (not including severity), if any * @param {Array|number} ruleConfig rule configuration @@ -986,10 +1020,13 @@ function createRuleListeners(rule, ruleContext) { * @param {string | undefined} cwd cwd of the cli * @param {string} physicalFilename The full path of the file on disk without any code block information * @param {Function} ruleFilter A predicate function to filter which rules should be executed. + * @param {boolean} stats If true, stats are collected appended to the result + * @param {WeakMap} slots InternalSlotsMap of linter * @returns {LintMessage[]} An array of reported problems * @throws {Error} If traversal into a node fails. */ -function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename, ruleFilter) { +function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageOptions, settings, filename, disableFixes, cwd, physicalFilename, ruleFilter, + stats, slots) { const emitter = createEmitter(); // must happen first to assign all node.parent properties @@ -1088,7 +1125,14 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO ) ); - const ruleListeners = timing.enabled ? timing.time(ruleId, createRuleListeners)(rule, ruleContext) : createRuleListeners(rule, ruleContext); + const ruleListenersReturn = (timing.enabled || stats) + ? timing.time(ruleId, createRuleListeners, stats)(rule, ruleContext) : createRuleListeners(rule, ruleContext); + + const ruleListeners = stats ? ruleListenersReturn.result : ruleListenersReturn; + + if (stats) { + storeTime(ruleListenersReturn.tdiff, { type: "rules", key: ruleId }, slots); + } /** * Include `ruleId` in error logs @@ -1098,7 +1142,15 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO function addRuleErrorHandler(ruleListener) { return function ruleErrorHandler(...listenerArgs) { try { - return ruleListener(...listenerArgs); + const ruleListenerReturn = ruleListener(...listenerArgs); + + const ruleListenerResult = stats ? ruleListenerReturn.result : ruleListenerReturn; + + if (stats) { + storeTime(ruleListenerReturn.tdiff, { type: "rules", key: ruleId }, slots); + } + + return ruleListenerResult; } catch (e) { e.ruleId = ruleId; throw e; @@ -1112,9 +1164,8 @@ function runRules(sourceCode, configuredRules, ruleMapper, parserName, languageO // add all the selectors from the rule as listeners Object.keys(ruleListeners).forEach(selector => { - const ruleListener = timing.enabled - ? timing.time(ruleId, ruleListeners[selector]) - : ruleListeners[selector]; + const ruleListener = (timing.enabled || stats) + ? timing.time(ruleId, ruleListeners[selector], stats) : ruleListeners[selector]; emitter.on( selector, @@ -1236,7 +1287,6 @@ function assertEslintrcConfig(linter) { } } - //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -1342,12 +1392,25 @@ class Linter { }); if (!slots.lastSourceCode) { + let t; + + if (options.stats) { + t = startTime(); + } + const parseResult = parse( text, languageOptions, options.filename ); + if (options.stats) { + const time = endTime(t); + const timeOpts = { type: "parse" }; + + storeTime(time, timeOpts, slots); + } + if (!parseResult.success) { return [parseResult.error]; } @@ -1398,7 +1461,9 @@ class Linter { options.disableFixes, slots.cwd, providedOptions.physicalFilename, - null + null, + options.stats, + slots ); } catch (err) { err.message += `\nOccurred while linting ${options.filename}`; @@ -1626,12 +1691,24 @@ class Linter { const settings = config.settings || {}; if (!slots.lastSourceCode) { + let t; + + if (options.stats) { + t = startTime(); + } + const parseResult = parse( text, languageOptions, options.filename ); + if (options.stats) { + const time = endTime(t); + + storeTime(time, { type: "parse" }, slots); + } + if (!parseResult.success) { return [parseResult.error]; } @@ -1841,7 +1918,9 @@ class Linter { options.disableFixes, slots.cwd, providedOptions.physicalFilename, - options.ruleFilter + options.ruleFilter, + options.stats, + slots ); } catch (err) { err.message += `\nOccurred while linting ${options.filename}`; @@ -2081,6 +2160,22 @@ class Linter { return internalSlotsMap.get(this).lastSourceCode; } + /** + * Gets the times spent on (parsing, fixing, linting) a file. + * @returns {LintTimes} The times. + */ + getTimes() { + return internalSlotsMap.get(this).times ?? { passes: [] }; + } + + /** + * Gets the number of autofix passes that were made in the last run. + * @returns {number} The number of autofix passes. + */ + getFixPassCount() { + return internalSlotsMap.get(this).fixPasses ?? 0; + } + /** * Gets the list of SuppressedLintMessage produced in the last running. * @returns {SuppressedLintMessage[]} The list of SuppressedLintMessage @@ -2157,6 +2252,7 @@ class Linter { currentText = text; const debugTextDescription = options && options.filename || `${text.slice(0, 10)}...`; const shouldFix = options && typeof options.fix !== "undefined" ? options.fix : true; + const stats = options?.stats; /** * This loop continues until one of the following is true: @@ -2167,15 +2263,46 @@ class Linter { * That means anytime a fix is successfully applied, there will be another pass. * Essentially, guaranteeing a minimum of two passes. */ + const slots = internalSlotsMap.get(this); + + // Remove lint times from the last run. + if (stats) { + delete slots.times; + slots.fixPasses = 0; + } + do { passNumber++; + let tTotal; + + if (stats) { + tTotal = startTime(); + } debug(`Linting code for ${debugTextDescription} (pass ${passNumber})`); messages = this.verify(currentText, config, options); debug(`Generating fixed text for ${debugTextDescription} (pass ${passNumber})`); + let t; + + if (stats) { + t = startTime(); + } + fixedResult = SourceCodeFixer.applyFixes(currentText, messages, shouldFix); + if (stats) { + + if (fixedResult.fixed) { + const time = endTime(t); + + storeTime(time, { type: "fix" }, slots); + slots.fixPasses++; + } else { + storeTime(0, { type: "fix" }, slots); + } + } + /* * stop if there are any syntax errors. * 'fixedResult.output' is a empty string. @@ -2190,6 +2317,13 @@ class Linter { // update to use the fixed output instead of the original text currentText = fixedResult.output; + if (stats) { + tTotal = endTime(tTotal); + const passIndex = slots.times.passes.length - 1; + + slots.times.passes[passIndex].total = tTotal; + } + } while ( fixedResult.fixed && passNumber < MAX_AUTOFIX_PASSES @@ -2200,7 +2334,18 @@ class Linter { * the most up-to-date information. */ if (fixedResult.fixed) { + let tTotal; + + if (stats) { + tTotal = startTime(); + } + fixedResult.messages = this.verify(currentText, config, options); + + if (stats) { + storeTime(0, { type: "fix" }, slots); + slots.times.passes.at(-1).total = endTime(tTotal); + } } // ensure the last result properly reflects if fixes were done diff --git a/lib/linter/timing.js b/lib/linter/timing.js index 1076ff25887..232c5f4f2fb 100644 --- a/lib/linter/timing.js +++ b/lib/linter/timing.js @@ -5,6 +5,8 @@ "use strict"; +const { startTime, endTime } = require("../shared/stats"); + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ @@ -128,21 +130,27 @@ module.exports = (function() { * Time the run * @param {any} key key from the data object * @param {Function} fn function to be called + * @param {boolean} stats if 'stats' is true, return the result and the time difference * @returns {Function} function to be executed * @private */ - function time(key, fn) { - if (typeof data[key] === "undefined") { - data[key] = 0; - } + function time(key, fn, stats) { return function(...args) { - let t = process.hrtime(); + + const t = startTime(); const result = fn(...args); + const tdiff = endTime(t); + + if (enabled) { + if (typeof data[key] === "undefined") { + data[key] = 0; + } + + data[key] += tdiff; + } - t = process.hrtime(t); - data[key] += t[0] * 1e3 + t[1] / 1e6; - return result; + return stats ? { result, tdiff } : result; }; } diff --git a/lib/options.js b/lib/options.js index 2f5380148a7..135e3f2d760 100644 --- a/lib/options.js +++ b/lib/options.js @@ -60,6 +60,7 @@ const optionator = require("optionator"); * @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause * the linting operation to short circuit and not report any failures. * @property {string[]} _ Positional filenames or patterns + * @property {boolean} [stats] Report additional statistics */ //------------------------------------------------------------------------------ @@ -143,6 +144,17 @@ module.exports = function(usingFlatConfig) { }; } + let statsFlag; + + if (usingFlatConfig) { + statsFlag = { + option: "stats", + type: "Boolean", + default: "false", + description: "Add statistics to the lint report" + }; + } + let warnIgnoredFlag; if (usingFlatConfig) { @@ -400,7 +412,8 @@ module.exports = function(usingFlatConfig) { option: "print-config", type: "path::String", description: "Print the configuration for the given file" - } + }, + statsFlag ].filter(value => !!value) }); }; diff --git a/lib/shared/stats.js b/lib/shared/stats.js new file mode 100644 index 00000000000..c5d4d1885d4 --- /dev/null +++ b/lib/shared/stats.js @@ -0,0 +1,30 @@ +/** + * @fileoverview Provides helper functions to start/stop the time measurements + * that are provided by the ESLint 'stats' option. + * @author Mara Kiefer + */ +"use strict"; + +/** + * Start time measurement + * @returns {[number, number]} t variable for tracking time + */ +function startTime() { + return process.hrtime(); +} + +/** + * End time measurement + * @param {[number, number]} t Variable for tracking time + * @returns {number} The measured time in milliseconds + */ +function endTime(t) { + const time = process.hrtime(t); + + return time[0] * 1e3 + time[1] / 1e6; +} + +module.exports = { + startTime, + endTime +}; diff --git a/lib/shared/types.js b/lib/shared/types.js index 15666d1c23f..f4186dd96ad 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -189,11 +189,45 @@ module.exports = {}; * @property {number} warningCount Number of warnings for the result. * @property {number} fixableErrorCount Number of fixable errors for the result. * @property {number} fixableWarningCount Number of fixable warnings for the result. + * @property {Stats} [stats] The performance statistics collected with the `stats` flag. * @property {string} [source] The source code of the file that was linted. * @property {string} [output] The source code of the file that was linted, with as many fixes applied as possible. * @property {DeprecatedRuleInfo[]} usedDeprecatedRules The list of used deprecated rules. */ +/** + * Performance statistics + * @typedef {Object} Stats + * @property {number} fixPasses The number of times ESLint has applied at least one fix after linting. + * @property {Times} times The times spent on (parsing, fixing, linting) a file. + */ + +/** + * Performance Times for each ESLint pass + * @typedef {Object} Times + * @property {TimePass[]} passes Time passes + */ + +/** + * @typedef {Object} TimePass + * @property {ParseTime} parse The parse object containing all parse time information. + * @property {Record} [rules] The rules object containing all lint time information for each rule. + * @property {FixTime} fix The parse object containing all fix time information. + * @property {number} total The total time that is spent on (parsing, fixing, linting) a file. + */ +/** + * @typedef {Object} ParseTime + * @property {number} total The total time that is spent when parsing a file. + */ +/** + * @typedef {Object} RuleTime + * @property {number} total The total time that is spent on a rule. + */ +/** + * @typedef {Object} FixTime + * @property {number} total The total time that is spent on applying fixes to the code. + */ + /** * Information provided when the maximum warning threshold is exceeded. * @typedef {Object} MaxWarningsExceeded diff --git a/tests/fixtures/stats-example/file-to-fix.js b/tests/fixtures/stats-example/file-to-fix.js new file mode 100644 index 00000000000..104fca51a11 --- /dev/null +++ b/tests/fixtures/stats-example/file-to-fix.js @@ -0,0 +1,5 @@ +/*eslint wrap-regex: "error"*/ + +function a() { + return / foo/.test("bar"); +} diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 39eea5787b6..9eb1913c7d1 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -5060,6 +5060,74 @@ describe("ESLint", () => { }); + describe("Use stats option", () => { + + /** + * Check if the given number is a number. + * @param {number} n The number to check. + * @returns {boolean} `true` if the number is a number, `false` otherwise. + */ + function isNumber(n) { + return typeof n === "number" && !Number.isNaN(n); + } + + it("should report stats", async () => { + const engine = new ESLint({ + overrideConfigFile: true, + overrideConfig: { + rules: { + "no-regex-spaces": "error" + } + }, + cwd: getFixturePath("stats-example"), + stats: true + }); + const results = await engine.lintFiles(["file-to-fix.js"]); + + assert.strictEqual(results[0].stats.fixPasses, 0); + assert.strictEqual(results[0].stats.times.passes.length, 1); + assert.strictEqual(isNumber(results[0].stats.times.passes[0].parse.total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[0].rules["no-regex-spaces"].total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[0].rules["wrap-regex"].total), true); + assert.strictEqual(results[0].stats.times.passes[0].fix.total, 0); + assert.strictEqual(isNumber(results[0].stats.times.passes[0].total), true); + }); + + it("should report stats with fix", async () => { + const engine = new ESLint({ + overrideConfigFile: true, + overrideConfig: { + rules: { + "no-regex-spaces": "error" + } + }, + cwd: getFixturePath("stats-example"), + fix: true, + stats: true + }); + const results = await engine.lintFiles(["file-to-fix.js"]); + + assert.strictEqual(results[0].stats.fixPasses, 2); + assert.strictEqual(results[0].stats.times.passes.length, 3); + assert.strictEqual(isNumber(results[0].stats.times.passes[0].parse.total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[1].parse.total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[2].parse.total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[0].rules["no-regex-spaces"].total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[0].rules["wrap-regex"].total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[1].rules["no-regex-spaces"].total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[1].rules["wrap-regex"].total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[2].rules["no-regex-spaces"].total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[2].rules["wrap-regex"].total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[0].fix.total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[1].fix.total), true); + assert.strictEqual(results[0].stats.times.passes[2].fix.total, 0); + assert.strictEqual(isNumber(results[0].stats.times.passes[0].total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[1].total), true); + assert.strictEqual(isNumber(results[0].stats.times.passes[2].total), true); + }); + + }); + describe("getRulesMetaForResults()", () => { it("should throw an error when this instance did not lint any files", async () => { diff --git a/tests/lib/options.js b/tests/lib/options.js index 781f8250634..a75381f7241 100644 --- a/tests/lib/options.js +++ b/tests/lib/options.js @@ -437,4 +437,12 @@ describe("options", () => { }); }); + describe("--stats", () => { + it("should return true --stats is passed", () => { + const currentOptions = flatOptions.parse("--stats"); + + assert.isTrue(currentOptions.stats); + }); + }); + }); From dadc5bf843a7181b9724a261c7ac0486091207aa Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Mon, 25 Mar 2024 19:18:30 +0100 Subject: [PATCH 8/8] fix: `constructor-super` false positives with loops (#18226) --- lib/rules/constructor-super.js | 155 +++++++++++---------------- tests/lib/rules/constructor-super.js | 98 ++++++++++++++++- 2 files changed, 159 insertions(+), 94 deletions(-) diff --git a/lib/rules/constructor-super.js b/lib/rules/constructor-super.js index 7ded20f6075..6f46278f781 100644 --- a/lib/rules/constructor-super.js +++ b/lib/rules/constructor-super.js @@ -9,22 +9,6 @@ // Helpers //------------------------------------------------------------------------------ -/** - * Checks all segments in a set and returns true if any are reachable. - * @param {Set} segments The segments to check. - * @returns {boolean} True if any segment is reachable; false otherwise. - */ -function isAnySegmentReachable(segments) { - - for (const segment of segments) { - if (segment.reachable) { - return true; - } - } - - return false; -} - /** * Checks whether or not a given node is a constructor. * @param {ASTNode} node A node to check. This node type is one of @@ -165,8 +149,7 @@ module.exports = { missingAll: "Expected to call 'super()'.", duplicate: "Unexpected duplicate 'super()'.", - badSuper: "Unexpected 'super()' because 'super' is not a constructor.", - unexpected: "Unexpected 'super()'." + badSuper: "Unexpected 'super()' because 'super' is not a constructor." } }, @@ -186,7 +169,7 @@ module.exports = { /** * @type {Record} */ - let segInfoMap = Object.create(null); + const segInfoMap = Object.create(null); /** * Gets the flag which shows `super()` is called in some paths. @@ -194,7 +177,7 @@ module.exports = { * @returns {boolean} The flag which shows `super()` is called in some paths */ function isCalledInSomePath(segment) { - return segment.reachable && segInfoMap[segment.id]?.calledInSomePaths; + return segment.reachable && segInfoMap[segment.id].calledInSomePaths; } /** @@ -212,17 +195,6 @@ module.exports = { * @returns {boolean} The flag which shows `super()` is called in all paths. */ function isCalledInEveryPath(segment) { - - /* - * If specific segment is the looped segment of the current segment, - * skip the segment. - * If not skipped, this never becomes true after a loop. - */ - if (segment.nextSegments.length === 1 && - segment.nextSegments[0]?.isLoopedPrevSegment(segment)) { - return true; - } - return segment.reachable && segInfoMap[segment.id].calledInEveryPaths; } @@ -279,9 +251,9 @@ module.exports = { } // Reports if `super()` lacked. - const seenSegments = codePath.returnedSegments.filter(hasSegmentBeenSeen); - const calledInEveryPaths = seenSegments.every(isCalledInEveryPath); - const calledInSomePaths = seenSegments.some(isCalledInSomePath); + const returnedSegments = codePath.returnedSegments; + const calledInEveryPaths = returnedSegments.every(isCalledInEveryPath); + const calledInSomePaths = returnedSegments.some(isCalledInSomePath); if (!calledInEveryPaths) { context.report({ @@ -296,28 +268,38 @@ module.exports = { /** * Initialize information of a given code path segment. * @param {CodePathSegment} segment A code path segment to initialize. + * @param {CodePathSegment} node Node that starts the segment. * @returns {void} */ - onCodePathSegmentStart(segment) { + onCodePathSegmentStart(segment, node) { funcInfo.currentSegments.add(segment); - if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { + if (!(funcInfo.isConstructor && funcInfo.hasExtends)) { return; } // Initialize info. const info = segInfoMap[segment.id] = new SegmentInfo(); - // When there are previous segments, aggregates these. - const prevSegments = segment.prevSegments; - - if (prevSegments.length > 0) { - const seenPrevSegments = prevSegments.filter(hasSegmentBeenSeen); + const seenPrevSegments = segment.prevSegments.filter(hasSegmentBeenSeen); + // When there are previous segments, aggregates these. + if (seenPrevSegments.length > 0) { info.calledInSomePaths = seenPrevSegments.some(isCalledInSomePath); info.calledInEveryPaths = seenPrevSegments.every(isCalledInEveryPath); } + + /* + * ForStatement > *.update segments are a special case as they are created in advance, + * without seen previous segments. Since they logically don't affect `calledInEveryPaths` + * calculations, and they can never be a lone previous segment of another one, we'll set + * their `calledInEveryPaths` to `true` to effectively ignore them in those calculations. + * . + */ + if (node.parent && node.parent.type === "ForStatement" && node.parent.update === node) { + info.calledInEveryPaths = true; + } }, onUnreachableCodePathSegmentStart(segment) { @@ -343,25 +325,30 @@ module.exports = { * @returns {void} */ onCodePathSegmentLoop(fromSegment, toSegment) { - if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { + if (!(funcInfo.isConstructor && funcInfo.hasExtends)) { return; } - // Update information inside of the loop. - const isRealLoop = toSegment.prevSegments.length >= 2; - funcInfo.codePath.traverseSegments( { first: toSegment, last: fromSegment }, - segment => { - const info = segInfoMap[segment.id] ?? new SegmentInfo(); + (segment, controller) => { + const info = segInfoMap[segment.id]; + + // skip segments after the loop + if (!info) { + controller.skip(); + return; + } + const seenPrevSegments = segment.prevSegments.filter(hasSegmentBeenSeen); + const calledInSomePreviousPaths = seenPrevSegments.some(isCalledInSomePath); + const calledInEveryPreviousPaths = seenPrevSegments.every(isCalledInEveryPath); - // Updates flags. - info.calledInSomePaths = seenPrevSegments.some(isCalledInSomePath); - info.calledInEveryPaths = seenPrevSegments.every(isCalledInEveryPath); + info.calledInSomePaths ||= calledInSomePreviousPaths; + info.calledInEveryPaths ||= calledInEveryPreviousPaths; // If flags become true anew, reports the valid nodes. - if (info.calledInSomePaths || isRealLoop) { + if (calledInSomePreviousPaths) { const nodes = info.validNodes; info.validNodes = []; @@ -375,9 +362,6 @@ module.exports = { }); } } - - // save just in case we created a new SegmentInfo object - segInfoMap[segment.id] = info; } ); }, @@ -388,7 +372,7 @@ module.exports = { * @returns {void} */ "CallExpression:exit"(node) { - if (!(funcInfo && funcInfo.isConstructor)) { + if (!(funcInfo.isConstructor && funcInfo.hasExtends)) { return; } @@ -398,41 +382,34 @@ module.exports = { } // Reports if needed. - if (funcInfo.hasExtends) { - const segments = funcInfo.currentSegments; - let duplicate = false; - let info = null; + const segments = funcInfo.currentSegments; + let duplicate = false; + let info = null; - for (const segment of segments) { + for (const segment of segments) { - if (segment.reachable) { - info = segInfoMap[segment.id]; + if (segment.reachable) { + info = segInfoMap[segment.id]; - duplicate = duplicate || info.calledInSomePaths; - info.calledInSomePaths = info.calledInEveryPaths = true; - } + duplicate = duplicate || info.calledInSomePaths; + info.calledInSomePaths = info.calledInEveryPaths = true; } + } - if (info) { - if (duplicate) { - context.report({ - messageId: "duplicate", - node - }); - } else if (!funcInfo.superIsConstructor) { - context.report({ - messageId: "badSuper", - node - }); - } else { - info.validNodes.push(node); - } + if (info) { + if (duplicate) { + context.report({ + messageId: "duplicate", + node + }); + } else if (!funcInfo.superIsConstructor) { + context.report({ + messageId: "badSuper", + node + }); + } else { + info.validNodes.push(node); } - } else if (isAnySegmentReachable(funcInfo.currentSegments)) { - context.report({ - messageId: "unexpected", - node - }); } }, @@ -442,7 +419,7 @@ module.exports = { * @returns {void} */ ReturnStatement(node) { - if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { + if (!(funcInfo.isConstructor && funcInfo.hasExtends)) { return; } @@ -462,14 +439,6 @@ module.exports = { info.calledInSomePaths = info.calledInEveryPaths = true; } } - }, - - /** - * Resets state. - * @returns {void} - */ - "Program:exit"() { - segInfoMap = Object.create(null); } }; } diff --git a/tests/lib/rules/constructor-super.js b/tests/lib/rules/constructor-super.js index 70fd14a8859..9268a13fb7d 100644 --- a/tests/lib/rules/constructor-super.js +++ b/tests/lib/rules/constructor-super.js @@ -72,6 +72,7 @@ ruleTester.run("constructor-super", rule, { // https://github.com/eslint/eslint/issues/5261 "class A extends B { constructor(a) { super(); for (const b of a) { this.a(); } } }", + "class A extends B { constructor(a) { super(); for (b in a) ( foo(b) ); } }", // https://github.com/eslint/eslint/issues/5319 "class Foo extends Object { constructor(method) { super(); this.method = method || function() {}; } }", @@ -85,6 +86,42 @@ ruleTester.run("constructor-super", rule, { " }", "}" ].join("\n"), + [ + "class A extends Object {", + " constructor() {", + " super();", + " for (; i < 0; i++);", + " }", + "}" + ].join("\n"), + [ + "class A extends Object {", + " constructor() {", + " super();", + " for (let i = 0;; i++) {", + " if (foo) break;", + " }", + " }", + "}" + ].join("\n"), + [ + "class A extends Object {", + " constructor() {", + " super();", + " for (let i = 0; i < 0;);", + " }", + "}" + ].join("\n"), + [ + "class A extends Object {", + " constructor() {", + " super();", + " for (let i = 0;;) {", + " if (foo) break;", + " }", + " }", + "}" + ].join("\n"), // https://github.com/eslint/eslint/issues/8848 ` @@ -103,7 +140,21 @@ ruleTester.run("constructor-super", rule, { `, // Optional chaining - "class A extends obj?.prop { constructor() { super(); } }" + "class A extends obj?.prop { constructor() { super(); } }", + + ` + class A extends Base { + constructor(list) { + for (const a of list) { + if (a.foo) { + super(a); + return; + } + } + super(); + } + } + ` ], invalid: [ @@ -168,6 +219,10 @@ ruleTester.run("constructor-super", rule, { code: "class A extends B { constructor() { for (var a of b) super.foo(); } }", errors: [{ messageId: "missingAll", type: "MethodDefinition" }] }, + { + code: "class A extends B { constructor() { for (var i = 1; i < 10; i++) super.foo(); } }", + errors: [{ messageId: "missingAll", type: "MethodDefinition" }] + }, // nested execution scope. { @@ -285,6 +340,47 @@ ruleTester.run("constructor-super", rule, { }`, errors: [{ messageId: "missingAll", type: "MethodDefinition" }] + }, + { + code: `class C extends D { + + constructor() { + for (let i = 1;;i++) { + if (bar) { + break; + } + } + } + + }`, + errors: [{ messageId: "missingAll", type: "MethodDefinition" }] + }, + { + code: `class C extends D { + + constructor() { + do { + super(); + } while (foo); + } + + }`, + errors: [{ messageId: "duplicate", type: "CallExpression" }] + }, + { + code: `class C extends D { + + constructor() { + while (foo) { + if (bar) { + super(); + break; + } + } + } + + }`, + errors: [{ messageId: "missingSome", type: "MethodDefinition" }] } ] });