diff --git a/.eslintrc.js b/.eslintrc.js index 4ceed4d3693..7757f21bd2b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,8 +6,7 @@ * Internally, ESLint is using the eslint.config.js file to lint itself. * This file is needed too, because: * - * 1. There are tests that expect .eslintrc.js to be present to actually run. - * 2. ESLint VS Code extension expects eslintrc config files to be + * 1. ESLint VS Code extension expects eslintrc config files to be * present to work correctly. * * Once we no longer need to support both eslintrc and flat config, we will @@ -63,21 +62,11 @@ module.exports = { "internal-rules" ], extends: [ - "eslint" + "eslint/eslintrc" ], parserOptions: { ecmaVersion: 2021 }, - - /* - * it fixes eslint-plugin-jsdoc's reports: "Invalid JSDoc tag name "template" jsdoc/check-tag-names" - * refs: https://github.com/gajus/eslint-plugin-jsdoc#check-tag-names - */ - settings: { - jsdoc: { - mode: "typescript" - } - }, rules: { "internal-rules/multiline-comment-style": "error" }, @@ -96,9 +85,6 @@ module.exports = { "plugin:eslint-plugin/rules-recommended" ], rules: { - "eslint-plugin/no-missing-message-ids": "error", - "eslint-plugin/no-unused-message-ids": "error", - "eslint-plugin/prefer-message-ids": "error", "eslint-plugin/prefer-placeholders": "error", "eslint-plugin/prefer-replace-text": "error", "eslint-plugin/report-message-format": ["error", "[^a-z].*\\.$"], @@ -119,7 +105,6 @@ module.exports = { "plugin:eslint-plugin/tests-recommended" ], rules: { - "eslint-plugin/prefer-output-null": "error", "eslint-plugin/test-case-property-ordering": "error", "eslint-plugin/test-case-shorthand-strings": "error" } diff --git a/.github/workflows/add-to-triage.yml b/.github/workflows/add-to-triage.yml deleted file mode 100644 index 83297dcab33..00000000000 --- a/.github/workflows/add-to-triage.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Add to Triage - -on: - issues: - types: - - opened - -jobs: - add-to-project: - name: Add issue to project - runs-on: ubuntu-latest - steps: - - uses: actions/add-to-project@v0.5.0 - with: - project-url: https://github.com/orgs/eslint/projects/3 - github-token: ${{ secrets.PROJECT_BOT_TOKEN }} - labeled: "triage:no" - label-operator: NOT diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27dae4c1262..c2ee74329c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,8 +13,8 @@ jobs: name: Verify Files runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 'lts/*' - name: Install Packages @@ -45,7 +45,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node: [20.x, 19.x, 18.x, 17.x, 16.x, 14.x, 12.x, "12.22.0"] + node: [21.x, 20.x, 19.x, 18.x, 17.x, 16.x, 14.x, 12.x, "12.22.0"] include: - os: windows-latest node: "lts/*" @@ -53,8 +53,8 @@ jobs: node: "lts/*" runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - name: Install Packages @@ -68,13 +68,19 @@ jobs: name: Browser Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '16' - name: Install Packages run: npm install - name: Test - run: node Makefile karma + run: node Makefile wdio - name: Fuzz Test run: node Makefile fuzz + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: logs + path: | + wdio-logs/*.log diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e2995e66d40..637f06e2e51 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index e6399920b51..5e3fb97c48d 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -9,12 +9,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: token: ${{ secrets.WORKFLOW_PUSH_BOT_TOKEN }} - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 - name: Install npm packages run: npm install diff --git a/.gitignore b/.gitignore index 075a4d740c7..148181e0776 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ test.js coverage/ build/ +logs +wdio-logs npm-debug.log yarn-error.log .pnpm-debug.log diff --git a/CHANGELOG.md b/CHANGELOG.md index a103b11c6e5..cb64e16b2c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,262 @@ +v8.53.0 - November 3, 2023 + +* [`ba4d4d5`](https://github.com/eslint/eslint/commit/ba4d4d567a82554250dd8c7933322824e6a73944) chore: remove metascraper (#17707) (Milos Djermanovic) +* [`0d07338`](https://github.com/eslint/eslint/commit/0d0733882944b4849d71a40723c251213698cef9) chore: Update dependencies (#17706) (Milos Djermanovic) +* [`93256a3`](https://github.com/eslint/eslint/commit/93256a32e312f3f4e5c532762df71bdc06bded20) chore: package.json update for @eslint/js release (ESLint Jenkins) +* [`ab8c60d`](https://github.com/eslint/eslint/commit/ab8c60d4f859cec787b5a12f7271b40e666235f5) docs: change position of return to top button (#17688) (Tanuj Kanti) +* [`528e1c0`](https://github.com/eslint/eslint/commit/528e1c00dc2aa8636e5b706c4270dc655cfa17e3) feat: Deprecate formatting rules (#17696) (Nicholas C. Zakas) +* [`485ec7d`](https://github.com/eslint/eslint/commit/485ec7d08ed2040c292f52bf9b9152f6c8ef4809) test: fix ESLint tests for caching (#17699) (Milos Djermanovic) +* [`c0b11dd`](https://github.com/eslint/eslint/commit/c0b11ddb9f8aacc64c3933b9f278939aa7bea481) feat: Add suggestions for no-prototype-builtins (#17677) (Yonathan Randolph) +* [`4fc44c0`](https://github.com/eslint/eslint/commit/4fc44c0b8c5dca466bffdfe01dfd80794d7762b7) docs: update twitter icon to new X icon (#17687) (Tanuj Kanti) +* [`1ad6257`](https://github.com/eslint/eslint/commit/1ad6257744d63281235fcc33288394b1d69b34ce) fix: ensure that exit code for fatal errors is not overwritten (#17683) (Milos Djermanovic) +* [`4164b2c`](https://github.com/eslint/eslint/commit/4164b2ceec89726b18ea0b0e34fab05735d55a09) docs: Update README (GitHub Actions Bot) +* [`8651895`](https://github.com/eslint/eslint/commit/8651895ca7ae15e13d74c8be67d9eebd63a7ce1f) docs: Fix tabs in rule examples (#17653) (Francesco Trotta) +* [`3aec1c5`](https://github.com/eslint/eslint/commit/3aec1c55ba2c6d2833e1c0afe0a58f0cc6bbc0a4) docs: explained rule fixers and suggestions (#17657) (Josh Goldberg ✨) +* [`db06a7f`](https://github.com/eslint/eslint/commit/db06a7ff7992a74368f03d1f21beb00df0407021) ci: bump actions/setup-node from 3 to 4 (#17676) (dependabot[bot]) +* [`b329ea7`](https://github.com/eslint/eslint/commit/b329ea748dff45f11c7e218208244dc24fcb5c8f) fix: add `;` after JSX nodes in `no-object-constructor` autofix (#17672) (Francesco Trotta) +* [`994596b`](https://github.com/eslint/eslint/commit/994596b07f5ff20a615a4be1ea03e5fd59cdb84b) ci: run tests in Node.js 21 (#17673) (Francesco Trotta) + +v8.52.0 - October 20, 2023 + +* [`6d1f0c2`](https://github.com/eslint/eslint/commit/6d1f0c2da0309c06c21149b8d71a8f439a70d7e8) chore: upgrade @eslint/js@8.52.0 (#17671) (Milos Djermanovic) +* [`d63d4fe`](https://github.com/eslint/eslint/commit/d63d4fe0942e6747ab60e758aa36076f43041a30) chore: package.json update for @eslint/js release (ESLint Jenkins) +* [`476d58a`](https://github.com/eslint/eslint/commit/476d58a584d5d2db003c4c22ffee90e63566164d) docs: Add note about invalid CLI flags when using flat config. (#17664) (Nicholas C. Zakas) +* [`5de9637`](https://github.com/eslint/eslint/commit/5de9637fc925729a83d5a5e9e868a41792a184e3) fix: Ensure shared references in rule configs are separated (#17666) (Nicholas C. Zakas) +* [`f30cefe`](https://github.com/eslint/eslint/commit/f30cefee6bda2789ede18e1664b84c2638ea1bb5) test: fix FlatESLint tests for caching (#17658) (Milos Djermanovic) +* [`ef650cb`](https://github.com/eslint/eslint/commit/ef650cb612510bcfa1379c1f0af56dd563b3a705) test: update tests for no-promise-executor-return (#17661) (Milos Djermanovic) +* [`70648ee`](https://github.com/eslint/eslint/commit/70648ee49c07f7b533d09f6bf8a5291e5a5a8601) feat: report-unused-disable-directive to report unused eslint-enable (#17611) (Yosuke Ota) +* [`dcfe573`](https://github.com/eslint/eslint/commit/dcfe5739c374c9d7ed21f14027870ec0fd453661) fix: add preceding semicolon in suggestions of `no-object-constructor` (#17649) (Francesco Trotta) +* [`660ed3a`](https://github.com/eslint/eslint/commit/660ed3afd128ad529234a855345629982caf1bc7) docs: Plugin flat config migration guide (#17640) (Nicholas C. Zakas) +* [`a58aa20`](https://github.com/eslint/eslint/commit/a58aa200fccedae7e2e9b6129246f2cedab14f8d) docs: fix examples for several rules (#17645) (Milos Djermanovic) +* [`179929b`](https://github.com/eslint/eslint/commit/179929bd46892f18f2aef0c159d5cc361cb69987) docs: Remove trailing newline from the code of Playground links (#17641) (Francesco Trotta) +* [`f8e5c30`](https://github.com/eslint/eslint/commit/f8e5c30636450d4a8baf51f0e227685e6d77ac64) docs: Update README (GitHub Actions Bot) +* [`b7ef2f3`](https://github.com/eslint/eslint/commit/b7ef2f34fe12b68a366e1b4bf5f64d7332c6e72e) docs: Enable pretty code formatter output (#17635) (Nicholas C. Zakas) +* [`0bcb9a8`](https://github.com/eslint/eslint/commit/0bcb9a8db608a3d0bd2645f99e0707b9a9bbaaf0) docs: Fix syntax errors in rule examples (#17633) (Francesco Trotta) +* [`61b9083`](https://github.com/eslint/eslint/commit/61b90839633ef300ac7707a651f65f532e65f42d) docs: Make no-continue example code work (#17643) (Zhongyuan Zhou) +* [`9fafe45`](https://github.com/eslint/eslint/commit/9fafe450c31ed9b6bdd9dcd6c115255943b8c1c2) docs: upgrade to 11ty 2.0 (#17632) (Percy Ma) +* [`ff8e4bf`](https://github.com/eslint/eslint/commit/ff8e4bf327b5c92b0623b0fc5f8f101954f785db) docs: Update README (GitHub Actions Bot) +* [`fab249a`](https://github.com/eslint/eslint/commit/fab249ae6addac2ee18cd81cee80916010bb469e) docs: Update README (GitHub Actions Bot) +* [`392305b`](https://github.com/eslint/eslint/commit/392305bf4797e3ebc696dfca48bd874741fca845) docs: Update `no-irregular-whitespace` and fix examples (#17626) (Francesco Trotta) +* [`6b8acfb`](https://github.com/eslint/eslint/commit/6b8acfb770589f3941df41c3910d3b8ffc3e1e45) docs: Add real whitespace to `no-trailing-spaces` examples (#17630) (Francesco Trotta) +* [`1000187`](https://github.com/eslint/eslint/commit/1000187e00949332babcee4d37d46c96a6a554a8) docs: Fix examples in `unicode-bom` (#17631) (Francesco Trotta) +* [`000290c`](https://github.com/eslint/eslint/commit/000290c4c923cc1473e21b4bdbdc0c42765ef7dd) docs: Update README (GitHub Actions Bot) + +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) +* [`38ada6d`](https://github.com/eslint/eslint/commit/38ada6df8f4a0313b7d0739b28f0af6b4897b8ce) chore: package.json update for @eslint/js release (ESLint Jenkins) +* [`27d5a9e`](https://github.com/eslint/eslint/commit/27d5a9e57ad347982a68fcd0e75eafee42d344f0) feat: add suggestions to array-callback-return (#17590) (Tanuj Kanti) +* [`f9082ff`](https://github.com/eslint/eslint/commit/f9082ff3f3956a0a5a7d7659de63640a21c4de0f) feat: flat-rule-tester make sure default config always matches (#17585) (fnx) +* [`83914ad`](https://github.com/eslint/eslint/commit/83914adbfd5fce7d11b33d095ba6d6a39be0dbbc) feat: Implement SourceCode#applyInlineConfig() (#17351) (Nicholas C. Zakas) +* [`cc4d26b`](https://github.com/eslint/eslint/commit/cc4d26b5a59d510f2c878e973fd245e8eff27c2a) fix: Ensure deprecated context.parserServices warns (#17593) (Nicholas C. Zakas) +* [`1ea4cfb`](https://github.com/eslint/eslint/commit/1ea4cfb585dcb52ac3cb1522a32f25cfe507121b) fix: Ensure all RuleTester tests all deprecated context methods (#17587) (Nicholas C. Zakas) +* [`1800537`](https://github.com/eslint/eslint/commit/180053759c6cf05a326c710353b4717fbf289ee0) docs: Fix and standardize JSX code examples (#17591) (Francesco Trotta) +* [`22a5582`](https://github.com/eslint/eslint/commit/22a558228ff98f478fa308c9ecde361acc4caf20) feat: add rule `no-object-constructor`, deprecate `no-new-object` (#17576) (Francesco Trotta) +* [`48a44a7`](https://github.com/eslint/eslint/commit/48a44a73ac456739bdee348bbaf1840d2b1e4830) docs: Add correct/incorrect tags to `prefer-arrow-callback` (#17589) (Francesco Trotta) +* [`aa1b657`](https://github.com/eslint/eslint/commit/aa1b657a9febcd03e9298c03ae2888762795e322) fix: wrong suggestion and message in `no-misleading-character-class` (#17571) (Yosuke Ota) +* [`20893d4`](https://github.com/eslint/eslint/commit/20893d48b9012f2b61bbbfeac8bee70d68d90e5e) docs: fix incorrect tag's place (#17575) (Tanuj Kanti) +* [`85a3d9e`](https://github.com/eslint/eslint/commit/85a3d9e967b19cb4a0189746499d81ef2f93e14e) feat: allowVoid option in array-callback-return (#17564) (Tanuj Kanti) +* [`bd7a71f`](https://github.com/eslint/eslint/commit/bd7a71fd6b7efb0445393304e2d48c5c06d46a45) docs: Update README (GitHub Actions Bot) + +v8.49.0 - September 8, 2023 + +* [`b7621c3`](https://github.com/eslint/eslint/commit/b7621c3b16cf7d5539f05336a827e1b32d95e6ac) chore: remove browser test from `npm test` (#17550) (Milos Djermanovic) +* [`cac45d0`](https://github.com/eslint/eslint/commit/cac45d04b890b0700dd8908927300608adad05fe) chore: upgrade @eslint/js@8.49.0 (#17549) (Milos Djermanovic) +* [`cd39508`](https://github.com/eslint/eslint/commit/cd395082bffcb4b68efa09226d7c682cef56179e) chore: package.json update for @eslint/js release (ESLint Jenkins) +* [`ecfb54f`](https://github.com/eslint/eslint/commit/ecfb54ff4cdd18f28b4f9b78f0a78fb4cf80f1b8) docs: Update README (GitHub Actions Bot) +* [`da09f4e`](https://github.com/eslint/eslint/commit/da09f4e641141f585ef611c6e9d63d4331054706) feat: Implement onUnreachableCodePathStart/End (#17511) (Nicholas C. Zakas) +* [`de86b3b`](https://github.com/eslint/eslint/commit/de86b3b2e58edd5826200c23255d8325abe375e1) docs: update `no-promise-executor-return` examples (#17529) (Nitin Kumar) +* [`203a971`](https://github.com/eslint/eslint/commit/203a971c0abc3a95ae02ff74104a01e569707060) ci: bump actions/checkout from 3 to 4 (#17530) (dependabot[bot]) +* [`32b2327`](https://github.com/eslint/eslint/commit/32b2327aafdd3b911fabab69ed75c9ff97658c60) feat: Emit deprecation warnings in RuleTester (#17527) (Nicholas C. Zakas) +* [`acb7df3`](https://github.com/eslint/eslint/commit/acb7df35b9a7485f26bc6b3e1f9083d1c585dce9) feat: add new `enforce` option to `lines-between-class-members` (#17462) (Nitin Kumar) +* [`032c4b1`](https://github.com/eslint/eslint/commit/032c4b1476a7b8cfd917a66772d2221950ea87eb) docs: add typescript template (#17500) (James) +* [`cd7da5c`](https://github.com/eslint/eslint/commit/cd7da5cc3154f86f7ca45fb58929d27a7af359ed) docs: Update README (GitHub Actions Bot) +* [`a40fa50`](https://github.com/eslint/eslint/commit/a40fa509922b36bb986eb1be9394591f84f62d9e) chore: use eslint-plugin-jsdoc's flat config (#17516) (Milos Djermanovic) +* [`926a286`](https://github.com/eslint/eslint/commit/926a28684282aeec37680bbc52a66973b8055f54) test: replace Karma with Webdriver.IO (#17126) (Christian Bromann) +* [`f591d2c`](https://github.com/eslint/eslint/commit/f591d2c88bf15af72e3a207b34fa872b4b90464b) chore: Upgrade config-array (#17512) (Nicholas C. Zakas) + +v8.48.0 - August 25, 2023 + +* [`8dd3cec`](https://github.com/eslint/eslint/commit/8dd3cec90c97ed97d243a83b87ad4ea9e6b4781a) chore: upgrade @eslint/js@8.48.0 (#17501) (Milos Djermanovic) +* [`6d0496e`](https://github.com/eslint/eslint/commit/6d0496e9476fb2210fba0a3d541df8c052ecf73a) chore: package.json update for @eslint/js release (ESLint Jenkins) +* [`7a51d77`](https://github.com/eslint/eslint/commit/7a51d77c0a066e461ff288568fdfee0e9539a2b5) docs: no-param-reassign mention strict mode (#17494) (Stephen Hardy) +* [`9cd7ac2`](https://github.com/eslint/eslint/commit/9cd7ac2fdb6b1d71a9fb1b8297a478cafacbdafd) docs: add `fetch` script to package.json conventions (#17459) (Nitin Kumar) +* [`7234f6a`](https://github.com/eslint/eslint/commit/7234f6a706a209aa2d79259110328752e9ae3928) fix: update RuleTester JSDoc and deprecations (#17496) (Jonas Berlin) +* [`1fbb3b0`](https://github.com/eslint/eslint/commit/1fbb3b0b477c814c0d179564fe495f4c50a451e9) feat: correct update direction in `for-direction` (#17483) (Francesco Trotta) +* [`9d4216d`](https://github.com/eslint/eslint/commit/9d4216d638d39844decffac33ee3d5a47413c80a) chore: Refactor and document CodePathSegment (#17474) (Nicholas C. Zakas) +* [`cab21e6`](https://github.com/eslint/eslint/commit/cab21e64a8f79779c641178f825945958667c6e4) docs: advice for inline disabling of rules (#17458) (Ashish Yadav) +* [`056499d`](https://github.com/eslint/eslint/commit/056499de31a139dbc965d18652b0b520e11b408d) docs: fix example of flat config from plugin (#17482) (Francesco Trotta) +* [`d73fbf2`](https://github.com/eslint/eslint/commit/d73fbf2228631d6c468cd24710e2579fe6cb70fd) feat: rule tester do not create empty valid or invalid test suites (#17475) (fnx) +* [`ee2f718`](https://github.com/eslint/eslint/commit/ee2f718188d32e9888b1932fe6b9bd2a62c529a4) feat: Allow `void` in rule `no-promise-executor-return` (#17282) (nopeless) +* [`9e9edf9`](https://github.com/eslint/eslint/commit/9e9edf93ecfa0658e8b79e71bc98530ade150081) docs: update documentation URL in error message (#17465) (Nitin Kumar) + +v8.47.0 - August 11, 2023 + +* [`bf69aa6`](https://github.com/eslint/eslint/commit/bf69aa6408f5403a88d8c9b71b0e58232b1ea833) chore: Update dependencies (#17456) (Nicholas C. Zakas) +* [`0e45760`](https://github.com/eslint/eslint/commit/0e4576012ab938b880e6f27641bff55fb4313d20) chore: package.json update for @eslint/js release (ESLint Jenkins) +* [`631648e`](https://github.com/eslint/eslint/commit/631648ee0b51a8951ce576ccd4430e09c9c8bcae) fix: do not report on shadowed constructors in `no-new-wrappers` (#17447) (Francesco Trotta) +* [`757bfe1`](https://github.com/eslint/eslint/commit/757bfe1c35b5ddab7042d388f8d21e834875fff5) chore: Remove add-to-triage (#17450) (Nicholas C. Zakas) +* [`b066640`](https://github.com/eslint/eslint/commit/b066640b7040ec30f740dcc803511244fe19473b) chore: standardize npm script names (#17431) (Nitin Kumar) +* [`a766a48`](https://github.com/eslint/eslint/commit/a766a48030d4359db76523d5b413d6332130e485) docs: document lack of config file names (#17442) (James) +* [`a1635d6`](https://github.com/eslint/eslint/commit/a1635d6198a8baf6571b3351e098e5ac960be887) docs: Update README (GitHub Actions Bot) +* [`6b2410f`](https://github.com/eslint/eslint/commit/6b2410f911dd2e3d915c879041c6e257d41a2f4e) chore: Update add-to-triage.yml (#17444) (Nicholas C. Zakas) +* [`47a0859`](https://github.com/eslint/eslint/commit/47a08597966651975126dd6726939cd34f13b80e) docs: update `require-unicode-regexp.md` as following up #17402 (#17441) (SUZUKI Sosuke) +* [`53d7508`](https://github.com/eslint/eslint/commit/53d750800b1c0c1f8c29393c488bb3167bb1d2a5) feat: update regex for methods with `thisArg` (#17439) (Francesco Trotta) +* [`fcdc85d`](https://github.com/eslint/eslint/commit/fcdc85d3a6bc14970c3349cc8d6f3a47eca172a3) docs: Update README (GitHub Actions Bot) +* [`2a92b6c`](https://github.com/eslint/eslint/commit/2a92b6cc9520a27255520369206556e9841a3af8) docs: update with "Specifying Parser Options" (#17435) (Cheol-Won) +* [`d743ed3`](https://github.com/eslint/eslint/commit/d743ed3c06c62a639da0389ad27907b324ea1715) docs: add metadata for parser/processor (#17438) (Huáng Jùnliàng) +* [`224376c`](https://github.com/eslint/eslint/commit/224376cd99a08394291a9584ad9c1ea1283673c6) docs: Update README (GitHub Actions Bot) +* [`a41a8e4`](https://github.com/eslint/eslint/commit/a41a8e4a7da14726d6fce71a023f12101fd52fdb) docs: update script names in README (#17432) (Nitin Kumar) + +v8.46.0 - July 28, 2023 + +* [`d1eb7e4`](https://github.com/eslint/eslint/commit/d1eb7e46e954c64af8d7d13d087b3a18f43e6d72) chore: Update ecosystem dependencies (#17427) (Nicholas C. Zakas) +* [`fab9e97`](https://github.com/eslint/eslint/commit/fab9e97ef9dff40e98a5b3b97bdd3b0ff5439d46) chore: package.json update for eslint-config-eslint release (ESLint Jenkins) +* [`6246711`](https://github.com/eslint/eslint/commit/6246711e0650d03afe044c36acde048ed2d39ee3) chore: package.json update for @eslint/js release (ESLint Jenkins) +* [`8a93438`](https://github.com/eslint/eslint/commit/8a9343871f7dade19d910ca8e2a4177bfca28b64) feat: `require-unicode-regexp` support `v` flag (#17402) (SUZUKI Sosuke) +* [`4d474e3`](https://github.com/eslint/eslint/commit/4d474e351ba6ce0242f18e55c27cb3ae17b84f63) docs: update with TypeScript info (#17423) (James) +* [`091f44e`](https://github.com/eslint/eslint/commit/091f44e4c72007edb2ac6d4db4eafa5501e41e94) docs: File extension named processor deprecation (#17362) (Matt Wilkinson) +* [`1a2f966`](https://github.com/eslint/eslint/commit/1a2f966fabe35103141d2f936180d2f1a72154db) feat: `no-useless-escape` support `v` flag (#17420) (Yosuke Ota) +* [`0aa0bc3`](https://github.com/eslint/eslint/commit/0aa0bc365a5425440c8e86c96104d0053a51b602) chore: Add PRs to triage project (#17421) (Nicholas C. Zakas) +* [`ee68d1d`](https://github.com/eslint/eslint/commit/ee68d1d9630892d99ae0d8dabe2f9f8d3b1338be) feat: `no-empty-character-class` support `v` flag (#17419) (Milos Djermanovic) +* [`853d32b`](https://github.com/eslint/eslint/commit/853d32baa8934c08b59a738470b72522e1505f6f) feat: deprecate no-return-await (#17417) (Carlos Lopez) +* [`d4f02e4`](https://github.com/eslint/eslint/commit/d4f02e4bf1b9ae4e1fc8f2bc4e4851ae3c36a127) feat: `no-control-regex` support `v` flag (#17405) (Yosuke Ota) +* [`9254a6c`](https://github.com/eslint/eslint/commit/9254a6cea845dfaf2f3f52f718cb9b071853aa09) docs: Update README (GitHub Actions Bot) +* [`2a35f3e`](https://github.com/eslint/eslint/commit/2a35f3e6ed27deafbebba48b6aec570d3abf9974) feat: `prefer-named-capture-group` support `v` flag (#17409) (Yosuke Ota) +* [`8ca8b50`](https://github.com/eslint/eslint/commit/8ca8b50b0425b3bad34a9505bc3095168e2f59d8) feat: Better error message for flat config plugins (#17399) (Nicholas C. Zakas) +* [`6d6dc51`](https://github.com/eslint/eslint/commit/6d6dc5141f535728029eef8735854a421bc08eba) docs: fix overlapping of `open in playground` button (#17403) (Tanuj Kanti) +* [`509f753`](https://github.com/eslint/eslint/commit/509f75395035822280245772e2a95732a0dde0e1) feat: `no-misleading-character-class` support `v` flag (#17406) (Yosuke Ota) +* [`3caf514`](https://github.com/eslint/eslint/commit/3caf51487decdf93a4b17765a2af2a51c337e974) feat: `no-regex-spaces` support `v` flag (#17407) (Yosuke Ota) +* [`b7fad2b`](https://github.com/eslint/eslint/commit/b7fad2b52f23667628cf209663795a721c88d0ba) feat: `prefer-regex-literals` support `v` flag (#17410) (Yosuke Ota) +* [`a6a3ad4`](https://github.com/eslint/eslint/commit/a6a3ad4ae438ea7fc3a1d97cd2555f6534b565f1) feat: `no-useless-backreference` support `v` flag (#17408) (Yosuke Ota) +* [`94954a7`](https://github.com/eslint/eslint/commit/94954a715448d5794f2892bf212fe986b43228ed) feat: `no-invalid-regexp` support `v` flag (#17404) (Yosuke Ota) +* [`7fc3a2c`](https://github.com/eslint/eslint/commit/7fc3a2ce68979a2c2a6fc779e647b3004ab6f4ac) docs: Add private class features info to no-underscore-dangle (#17386) (Matt Wilkinson) +* [`da73e58`](https://github.com/eslint/eslint/commit/da73e583e1703a420551d8fa8f7c70b56dc88dd5) docs: Migrating `eslint-env` configuration comments (#17390) (Francesco Trotta) +* [`10e9cfa`](https://github.com/eslint/eslint/commit/10e9cfa01ac043961f2c476198848f0ca5e8bbb0) Merge pull request from GHSA-qwh7-v8hg-w8rh (leo-centurion) +* [`1af6eac`](https://github.com/eslint/eslint/commit/1af6eac5727080c809e37c07dc729b44ef24483c) feat: adds option for allowing empty object patterns as parameter (#17365) (Tanuj Kanti) +* [`9803c7c`](https://github.com/eslint/eslint/commit/9803c7c04078f0672d8a480fd39cf3bbef8017e6) fix: FlatESLint#getRulesMetaForResults shouldn't throw on unknown rules (#17393) (Milos Djermanovic) +* [`80dffed`](https://github.com/eslint/eslint/commit/80dffed4c81dcc71fb72bc187aff2f87d141a6ed) docs: fix Ignoring Files section in config migration guide (#17392) (Milos Djermanovic) +* [`8a9abb7`](https://github.com/eslint/eslint/commit/8a9abb7cf424bd49d45c09345dc45ae95f29cc9d) docs: Update README (GitHub Actions Bot) +* [`cf03104`](https://github.com/eslint/eslint/commit/cf03104b278fea59ef46e09f667110f5eaaf95e3) feat: Improve config error messages (#17385) (Nicholas C. Zakas) +* [`42faa17`](https://github.com/eslint/eslint/commit/42faa17b1c93f801b14bea2840d1d528e25c7211) fix: Update no-loop-func to not overlap with no-undef (#17358) (Matt Wilkinson) +* [`7e9be4b`](https://github.com/eslint/eslint/commit/7e9be4bd7331d0e8e8e0af0b075a2f6d28d1bea3) docs: Update README (GitHub Actions Bot) +* [`0b0bbe0`](https://github.com/eslint/eslint/commit/0b0bbe07d4fb0870f3916e975b8ec6978f838077) docs: Update README (GitHub Actions Bot) + +v8.45.0 - July 14, 2023 + +* [`68f63d7`](https://github.com/eslint/eslint/commit/68f63d76ce785fab4f42b76f1599026eea379bf7) chore: package.json update for @eslint/js release (ESLint Jenkins) +* [`89f3225`](https://github.com/eslint/eslint/commit/89f3225108c66425e4132f76db6c1ab13aac98d7) docs: add playground links to correct and incorrect code blocks (#17306) (Josh Goldberg ✨) +* [`f8892b5`](https://github.com/eslint/eslint/commit/f8892b52920b8967f9e7bec23c75b74e03977d6b) docs: Expand rule option schema docs (#17198) (Matt Wilkinson) +* [`8bcbf11`](https://github.com/eslint/eslint/commit/8bcbf11b6050418262ffa8e0ca37f365ae92e7ce) docs: Config Migration Guide (#17230) (Ben Perlmutter) +* [`bb30908`](https://github.com/eslint/eslint/commit/bb3090897166dbfd2931a43a70e2a5c1f3fa0a07) docs: Update README (GitHub Actions Bot) +* [`b79b6fb`](https://github.com/eslint/eslint/commit/b79b6fb64473969b426d086b484d2e29594a5e9a) fix: Fix suggestion message in `no-useless-escape` (#17339) (Francesco Trotta) +* [`84d243b`](https://github.com/eslint/eslint/commit/84d243b245b01b667f0752b592e8bda02a9aa2b1) docs: Update README (GitHub Actions Bot) +* [`5ca9b4d`](https://github.com/eslint/eslint/commit/5ca9b4d29f747e9cf5c9055e85c93b3b605d57fc) chore: update eslint-config-eslint exports (#17336) (Milos Djermanovic) +* [`b762632`](https://github.com/eslint/eslint/commit/b762632298f20c4f81e7d01ab850c3f5e3874637) docs: Update README (GitHub Actions Bot) +* [`7bf2e86`](https://github.com/eslint/eslint/commit/7bf2e86022c9e95db4ca1472fddfa2ea4edd1870) chore: remove unused dependencies (#17352) (Percy Ma) +* [`c6f8cd0`](https://github.com/eslint/eslint/commit/c6f8cd0d62e4a3c314c6860ff367490bbd05325a) chore: Remove `defaultIgnores` from FlatESLint private members (#17349) (Francesco Trotta) +* [`cdd063c`](https://github.com/eslint/eslint/commit/cdd063c388bbfe1781d7a864a832f03a2c1cc277) feat: Expose LegacyESLint in unsupported API (#17341) (Nicholas C. Zakas) +* [`c667055`](https://github.com/eslint/eslint/commit/c667055fb9da8ebac3a99f6e5a8b5565cc86af8e) fix: provide unique `fix` and `fix.range` objects in lint messages (#17332) (Milos Djermanovic) +* [`138c096`](https://github.com/eslint/eslint/commit/138c096bc9468b553dbafc0e573c6522a17a7922) docs: add more prefer-destructuring examples with array destructuring (#17330) (Milos Djermanovic) +* [`0052374`](https://github.com/eslint/eslint/commit/0052374035672efe9129343fc00ee51a4c288ff3) chore: move jsdoc settings to eslint-config-eslint (#17338) (唯然) +* [`d34abe5`](https://github.com/eslint/eslint/commit/d34abe59eb23932dcbc79757d7932d08ee8b20e5) feat: fix indent rule for else-if (#17318) (Milos Djermanovic) +* [`1fc50a8`](https://github.com/eslint/eslint/commit/1fc50a89753346f4f4c786ffd20ac4cf185bb036) docs: `max-len` rule `code` and `tabWidth` as positional arguments (#17331) (Jesús Leganés-Combarro) + +v8.44.0 - June 30, 2023 + +* [`49e46ed`](https://github.com/eslint/eslint/commit/49e46edf3c8dc71d691a97fc33b63ed80ae0db0c) chore: upgrade @eslint/js@8.44.0 (#17329) (Milos Djermanovic) +* [`a1cb642`](https://github.com/eslint/eslint/commit/a1cb6421f9d185901cd99e5f696e912226ef6632) chore: package.json update for @eslint/js release (ESLint Jenkins) +* [`1766771`](https://github.com/eslint/eslint/commit/176677180a4a1209fc192771521c9192e1f67578) feat: add `es2023` and `es2024` environments (#17328) (Milos Djermanovic) +* [`4c50400`](https://github.com/eslint/eslint/commit/4c5040022639ae804c15b366afc6e64982bd8ae3) feat: add `ecmaVersion: 2024`, regexp `v` flag parsing (#17324) (Milos Djermanovic) +* [`4d411e4`](https://github.com/eslint/eslint/commit/4d411e4c7063274d6d346f1b7ee46f7575d0bbd2) feat: add ternaryOperandBinaryExpressions option to no-extra-parens rule (#17270) (Percy Ma) +* [`840a264`](https://github.com/eslint/eslint/commit/840a26462bbf6c27c52c01b85ee2018062157951) test: More test cases for no-case-declarations (#17315) (Elian Cordoba) +* [`e6e74f9`](https://github.com/eslint/eslint/commit/e6e74f9eef0448129dd4775628aba554a2d8c8c9) chore: package.json update for eslint-config-eslint release (ESLint Jenkins) +* [`eb3d794`](https://github.com/eslint/eslint/commit/eb3d7946e1e9f70254008744dba2397aaa730114) chore: upgrade semver@7.5.3 (#17323) (Ziyad El Abid) +* [`a36bcb6`](https://github.com/eslint/eslint/commit/a36bcb67f26be42c794797d0cc9948b9cfd4ff71) fix: no-unused-vars false positive with logical assignment operators (#17320) (Gweesin Chan) +* [`c8b1f4d`](https://github.com/eslint/eslint/commit/c8b1f4d61a256727755d561bf53f889b6cd712e0) feat: Move `parserServices` to `SourceCode` (#17311) (Milos Djermanovic) +* [`cf88439`](https://github.com/eslint/eslint/commit/cf884390ad8071d88eae05df9321100f1770363d) chore: upgrade optionator@0.9.3 (#17319) (Milos Djermanovic) +* [`7620b89`](https://github.com/eslint/eslint/commit/7620b891e81c234f30f9dbcceb64a05fd0dde65e) fix: Remove `no-unused-labels` autofix before potential directives (#17314) (Francesco Trotta) +* [`ef6e24e`](https://github.com/eslint/eslint/commit/ef6e24e42670f321d996948623846d9caaedac99) feat: treat unknown nodes as having the lowest precedence (#17302) (Brad Zacher) +* [`9718a97`](https://github.com/eslint/eslint/commit/9718a9781d69d2c40b68c631aed97700b32c0082) refactor: remove unnecessary code in `flat-eslint.js` (#17308) (Milos Djermanovic) +* [`1866e1d`](https://github.com/eslint/eslint/commit/1866e1df6175e4ba0ae4a0d88dc3c956bb310035) feat: allow flat config files to export a Promise (#17301) (Milos Djermanovic) +* [`f82e56e`](https://github.com/eslint/eslint/commit/f82e56e9acfb9562ece76441472d5657d7d5e296) perf: various performance improvements (#17135) (moonlightaria) +* [`da81e66`](https://github.com/eslint/eslint/commit/da81e66e22b4f3d3fe292cf70c388753304deaad) chore: update eslint-plugin-jsdoc to 46.2.5 (#17245) (唯然) +* [`526e911`](https://github.com/eslint/eslint/commit/526e91106e6fe101578e9478a9d7f4844d4f72ac) docs: resubmit pr 17115 doc changes (#17291) (唯然) +* [`b991640`](https://github.com/eslint/eslint/commit/b991640176d5dce4750f7cc71c56cd6f284c882f) chore: switch eslint-config-eslint to the flat format (#17247) (唯然) +* [`391ed38`](https://github.com/eslint/eslint/commit/391ed38b09bd1a3abe85db65b8fcda980ab3d6f4) fix: Remove `no-extra-semi` autofix before potential directives (#17297) (Francesco Trotta) +* [`e1314bf`](https://github.com/eslint/eslint/commit/e1314bf85a52bb0d05b1c9ca3b4c1732bae22172) docs: Integration section and tutorial (#17132) (Ben Perlmutter) +* [`19a8c5d`](https://github.com/eslint/eslint/commit/19a8c5d84596a9f7f2aa428c1696ba86daf854e6) docs: Update README (GitHub Actions Bot) + +v8.43.0 - June 16, 2023 + +* [`78350f6`](https://github.com/eslint/eslint/commit/78350f63045c82b7990bb7bfe5080c5ad5e1c3f5) chore: upgrade @eslint/js@8.43.0 (#17295) (Milos Djermanovic) +* [`8b855ea`](https://github.com/eslint/eslint/commit/8b855ea058992d5446d1d6dc6394ee683c3200a0) docs: resubmit pr17061 doc changes (#17292) (唯然) +* [`62bf759`](https://github.com/eslint/eslint/commit/62bf759124811b013ad7906c2536deb8b39c31a8) chore: package.json update for @eslint/js release (ESLint Jenkins) +* [`14581ff`](https://github.com/eslint/eslint/commit/14581ff15aaee5a55c46bbf4983818ddc8dd7cb1) feat: directive prologue detection and autofix condition in `quotes` (#17284) (Francesco Trotta) +* [`372722e`](https://github.com/eslint/eslint/commit/372722eac32ca9e3f31cf0d0bc10317c6f153369) docs: resubmit pr17012 doc changes (#17293) (唯然) +* [`67e7af3`](https://github.com/eslint/eslint/commit/67e7af3fdbdb4648b747dfd669be4decfe24086a) docs: resubmit custom-rules doc changes (#17294) (唯然) +* [`5338b56`](https://github.com/eslint/eslint/commit/5338b56fda7f47d16bdb23514f1e95b24de7b92f) fix: normalize `cwd` passed to `ESLint`/`FlatESLint` constructor (#17277) (Milos Djermanovic) +* [`9e3d77c`](https://github.com/eslint/eslint/commit/9e3d77cba65d0e38e07996e57961fb04f30d9303) docs: Resubmit Fix formatting in Custom Rules docs (#17281) (Milos Djermanovic) +* [`503647a`](https://github.com/eslint/eslint/commit/503647a0b94ca8c776d7e7e8c54c8b1d32904467) docs: Resubmit markVariableAsUsed docs (#17280) (Nicholas C. Zakas) +* [`54383e6`](https://github.com/eslint/eslint/commit/54383e69b092ef537d59a1f7799a85b1412f4e59) fix: Remove `no-extra-parens` autofix for potential directives (#17022) (Francesco Trotta) +* [`e0cf0d8`](https://github.com/eslint/eslint/commit/e0cf0d86d985ed2b2f901dd9aab5ccd2fff062ad) docs: Custom rule & plugin tutorial (#17024) (Ben Perlmutter) +* [`8e51ea9`](https://github.com/eslint/eslint/commit/8e51ea943c2fcd05bd8917cfa89e36b91209c7cd) docs: resubmit `no-new` rule documentation (#17264) (Nitin Kumar) +* [`1b217f8`](https://github.com/eslint/eslint/commit/1b217f8de15961fd3c80389621080132f517a0fb) docs: resubmit `Custom Processors` documentation (#17265) (Nitin Kumar) +* [`428fc76`](https://github.com/eslint/eslint/commit/428fc76806dea1ac82484d628261a5385f928e6a) docs: resubmit `Create Plugins` documentation (#17268) (Nitin Kumar) +* [`bdca88c`](https://github.com/eslint/eslint/commit/bdca88cf4f8b7888cb72197bfe9c1d90b490a0dd) docs: resubmit `Configuration Files` documentation (#17267) (Nitin Kumar) +* [`f5c01f2`](https://github.com/eslint/eslint/commit/f5c01f281ad288b1a0ebddbf579230ae11587c6c) docs: resubmit `Manage Issues` documentation (#17266) (Nitin Kumar) +* [`b199295`](https://github.com/eslint/eslint/commit/b1992954591a3f4d8417013f52739b5fef4e0cd7) docs: Resubmit custom rules update docs (#17273) (Ben Perlmutter) +* [`e50fac3`](https://github.com/eslint/eslint/commit/e50fac3f8f998f729e3080e256066db3a7827c67) feat: add declaration loc to message in block-scoped-var (#17252) (Milos Djermanovic) +* [`0e9980c`](https://github.com/eslint/eslint/commit/0e9980c3a8a1e554fdb377305c0ebe9e94a354c9) docs: add new `omitLastInOneLineClassBody` option to the `semi` rule (#17263) (Nitin Kumar) +* [`cb2560f`](https://github.com/eslint/eslint/commit/cb2560f7a393e74b761faa9adad938fb1deb947d) docs: Resubmit getScope/getDeclaredVariables docs (#17262) (Nicholas C. Zakas) +* [`85d2b30`](https://github.com/eslint/eslint/commit/85d2b30bc318c1355e52ebb21c56cca32f0ab198) docs: explain how to include predefined globals (#17261) (Marcus Wyatt) +* [`de4d3c1`](https://github.com/eslint/eslint/commit/de4d3c14c30a88795b9075d59827d3fe63a42c5e) docs: update flat config default ignore patterns (#17258) (Milos Djermanovic) +* [`3912f3a`](https://github.com/eslint/eslint/commit/3912f3a225c12bfb5ce9b7ba26c2b5301e6275bd) docs: Improve `ignores` documentation (#17239) (Francesco Trotta) +* [`35e11d3`](https://github.com/eslint/eslint/commit/35e11d3248e00b711fd652836edc900f22af0ebd) docs: fix typos and missing info (#17257) (Ed Lucas) +* [`e0a2448`](https://github.com/eslint/eslint/commit/e0a2448e0c0ef354e69998858846630a3fce8ebe) chore: docs package.license ISC => MIT (#17254) (唯然) +* [`0bc257c`](https://github.com/eslint/eslint/commit/0bc257c290b12fcda85cb61b40d55fc2be0f938c) docs: Clarify `no-div-regex` rule docs (#17051) (#17255) (Francesco Trotta) +* [`1b7faf0`](https://github.com/eslint/eslint/commit/1b7faf0702b1af86b6a0ddafc37cf45d60f5d4d8) feat: add `skipJSXText` option to `no-irregular-whitespace` rule (#17182) (Azat S) +* [`788d836`](https://github.com/eslint/eslint/commit/788d83629a3790a7db6f52dcf0b4bddf51c6d063) docs: add references to MIT License (#17248) (Milos Djermanovic) +* [`58aab6b`](https://github.com/eslint/eslint/commit/58aab6b6c09996875418aefeeb0fd76c50caef7a) docs: Update README (GitHub Actions Bot) +* [`6a0196c`](https://github.com/eslint/eslint/commit/6a0196c51310630a0ff96a1e8d7f257c2c7adda9) chore: use eslint-plugin-eslint-plugin flat configs (#17204) (Milos Djermanovic) +* [`030a827`](https://github.com/eslint/eslint/commit/030a82737f51563f9a7b4985fc91b6d8eab54fce) Revert "feat: docs license (#17010)" (#17231) (唯然) +* [`3ef5814`](https://github.com/eslint/eslint/commit/3ef58140550cf8ff34af35fc4d9a1f9a124fe0e6) docs: Revert all changes after the license change (#17227) (Milos Djermanovic) +* [`03fc4aa`](https://github.com/eslint/eslint/commit/03fc4aa847bd0445e7b3ea81bcc9523b1847facc) docs: Update README (GitHub Actions Bot) + +v8.42.0 - June 2, 2023 + +* [`6ca5b7c`](https://github.com/eslint/eslint/commit/6ca5b7ca3bac9e10c6cfee4cdc78446e94eb7607) chore: upgrade @eslint/js@8.42.0 (#17236) (Milos Djermanovic) +* [`67fc5e7`](https://github.com/eslint/eslint/commit/67fc5e730e4dfc372dea11e15d3f5165bc812491) chore: package.json update for @eslint/js release (ESLint Jenkins) +* [`0892412`](https://github.com/eslint/eslint/commit/0892412556b2ba6c3d1b85152dafe47a3f4cba72) refactor: remove `Identifier` listener in no-irregular-whitespace (#17235) (Milos Djermanovic) +* [`a589636`](https://github.com/eslint/eslint/commit/a5896360c3faa1e7d1fe81a9907a434b8b8f6b60) fix: Config with `ignores` and without `files` should not always apply (#17181) (Milos Djermanovic) +* [`01d7142`](https://github.com/eslint/eslint/commit/01d7142642c87241135699571e8010f5e8fcda4f) docs: Update README (GitHub Actions Bot) +* [`f67d298`](https://github.com/eslint/eslint/commit/f67d2984c3c3f26497842a04d5166707587c1fca) test: Add `FlatESLint` tests with missing config files (#17164) (Milos Djermanovic) +* [`e5182b7`](https://github.com/eslint/eslint/commit/e5182b723ff82bb3b55c50c06d64626055414b31) docs: Update README (GitHub Actions Bot) +* [`c4fad17`](https://github.com/eslint/eslint/commit/c4fad173c7149dbcd25695c19c68663102b9ec6b) fix: Correct ignore message for "node_modules" subfolders (#17217) (Francesco Trotta) +* [`5b68d51`](https://github.com/eslint/eslint/commit/5b68d51e3e6bd003d6cf74d3434f7165691b4f4d) chore: Fix `fixedsize` attribute in code path analysis DOT debug output (#17202) (Milos Djermanovic) +* [`b8448ff`](https://github.com/eslint/eslint/commit/b8448ff1ae1adf26a81dea07f340caa5b5c2f257) feat: correct no-useless-return behaviour in try statements (#16996) (Nitin Kumar) +* [`37432f2`](https://github.com/eslint/eslint/commit/37432f27dc15817d66cf42377792197dc2aeb8b2) chore: update descriptions in key-spacing tests (#17195) (Milos Djermanovic) + v8.41.0 - May 19, 2023 * [`f43216a`](https://github.com/eslint/eslint/commit/f43216a8c77ab6cf1d0823978e8c728786b4cba7) chore: upgrade @eslint/js@8.41.0 (#17200) (Milos Djermanovic) diff --git a/Makefile.js b/Makefile.js index 717cc785946..24accfb6103 100644 --- a/Makefile.js +++ b/Makefile.js @@ -214,7 +214,7 @@ function generateRuleIndexPage() { }; if (rule.meta.deprecated) { - ruleTypesData.deprecated.rules.push({ + ruleTypesData.deprecated.push({ name: basename, replacedBy: rule.meta.replacedBy || [] }); @@ -226,22 +226,18 @@ function generateRuleIndexPage() { fixable: !!rule.meta.fixable, hasSuggestions: !!rule.meta.hasSuggestions }, - ruleType = ruleTypesData.types.find(c => c.name === rule.meta.type); + ruleType = ruleTypesData.types[rule.meta.type]; - if (!ruleType.rules) { - ruleType.rules = []; - } - - ruleType.rules.push(output); + ruleType.push(output); } }); - // `.rules` will be `undefined` if all rules in category are deprecated. - ruleTypesData.types = ruleTypesData.types.filter(ruleType => !!ruleType.rules); + ruleTypesData.types = Object.fromEntries( + Object.entries(ruleTypesData.types).filter(([, value]) => value && value.length > 0) + ); JSON.stringify(ruleTypesData, null, 4).to(docsSiteOutputFile); JSON.stringify(meta, null, 4).to(docsSiteMetaOutputFile); - } /** @@ -628,12 +624,10 @@ target.mocha = () => { } }; -target.karma = () => { +target.wdio = () => { echo("Running unit tests on browsers"); - target.webpack("production"); - - const lastReturn = exec(`${getBinFile("karma")} start karma.conf.js`); + const lastReturn = exec(`${getBinFile("wdio")} run wdio.conf.js`); if (lastReturn.code !== 0) { exit(1); @@ -643,7 +637,9 @@ target.karma = () => { target.test = function() { target.checkRuleFiles(); target.mocha(); - target.karma(); + + // target.wdio(); // Temporarily disabled due to problems on Jenkins + target.fuzz({ amount: 150, fuzzBrokenAutofixes: false }); target.checkLicenses(); }; @@ -830,7 +826,7 @@ target.checkRuleFiles = function() { // check deprecated if (ruleDef.meta.deprecated && !hasDeprecatedInfo()) { - console.error(`Missing deprecated information in ${basename} rule code or README.md. Please write @deprecated tag in code or 「This rule was deprecated in ESLint ...」 in README.md.`); + console.error(`Missing deprecated information in ${basename} rule code or README.md. Please write @deprecated tag in code and「This rule was deprecated in ESLint ...」 in README.md.`); errors++; } diff --git a/README.md b/README.md index 50602564541..5379c319491 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [confi ### What ECMAScript versions does ESLint support? -ESLint has full support for ECMAScript 3, 5 (default), 2015, 2016, 2017, 2018, 2019, 2020, 2021 and 2022. You can set your desired ECMAScript syntax (and other settings, like global variables or your target environments) through [configuration](https://eslint.org/docs/latest/use/configure). +ESLint has full support for ECMAScript 3, 5 (default), 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, and 2023. You can set your desired ECMAScript syntax (and other settings, like global variables or your target environments) through [configuration](https://eslint.org/docs/latest/use/configure). ### What about experimental features? @@ -249,6 +249,16 @@ Bryan Mishkin
Francesco Trotta + + +
+Yosuke Ota +
+ + +
+Tanuj Kanti +
### Website Team @@ -283,8 +293,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 American Express

Bronze Sponsors

-

Ilmaiset Pitkävetovihjeet PayDay Say ThemeIsle Nx (by Nrwl) Anagram Solver Icons8: free icons, photos, illustrations, and music Discord Transloadit Ignition HeroCoders QuickBooks Tool hub

+

Liftoff American Express

Bronze Sponsors

+

ThemeIsle Anagram Solver Icons8 Discord Transloadit Ignition HeroCoders

## Technology Sponsors diff --git a/bin/eslint.js b/bin/eslint.js index 7094ac77bc4..eeb4647e70b 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -92,6 +92,19 @@ 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(); + +/** + * Tracks whether an unexpected error was caught + * @type {boolean} + */ +let hadFatalError = false; + /** * Catch and report unexpected error. * @param {any} error The thrown error object. @@ -99,16 +112,20 @@ function getErrorMessage(error) { */ function onFatalError(error) { process.exitCode = 2; + hadFatalError = true; 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); + } } //------------------------------------------------------------------------------ @@ -132,9 +149,25 @@ ${message}`); } // Otherwise, call the CLI. - process.exitCode = await require("../lib/cli").execute( + const exitCode = await require("../lib/cli").execute( process.argv, process.argv.includes("--stdin") ? await readStdin() : null, true ); + + /* + * If an uncaught exception or unhandled rejection was detected in the meantime, + * keep the fatal exit code 2 that is already assigned to `process.exitCode`. + * Without this condition, exit code 2 (unsuccessful execution) could be overwritten with + * 1 (successful execution, lint problems found) or even 0 (successful execution, no lint problems found). + * This ensures that unexpected errors that seemingly don't affect the success + * of the execution will still cause a non-zero exit code, as it's a common + * practice and the default behavior of Node.js to exit with non-zero + * in case of an uncaught exception or unhandled rejection. + * + * Otherwise, assign the exit code returned from CLI. + */ + if (!hadFatalError) { + process.exitCode = exitCode; + } }()).catch(onFatalError); diff --git a/conf/globals.js b/conf/globals.js index 6018b7af15c..58710e05bc6 100644 --- a/conf/globals.js +++ b/conf/globals.js @@ -128,6 +128,10 @@ const es2023 = { ...es2022 }; +const es2024 = { + ...es2023 +}; + //----------------------------------------------------------------------------- // Exports @@ -145,5 +149,6 @@ module.exports = { es2020, es2021, es2022, - es2023 + es2023, + es2024 }; diff --git a/conf/rule-type-list.json b/conf/rule-type-list.json index d5823acc898..6ca730f34f0 100644 --- a/conf/rule-type-list.json +++ b/conf/rule-type-list.json @@ -1,36 +1,28 @@ { - "types": [ - { "name": "problem", "displayName": "Possible Problems", "description": "These rules relate to possible logic errors in code:" }, - { "name": "suggestion", "displayName": "Suggestions", "description": "These rules suggest alternate ways of doing things:" }, - { "name": "layout", "displayName": "Layout & Formatting", "description": "These rules care about how the code looks rather than how it executes:" } - ], - "deprecated": { - "name": "Deprecated", - "description": "These rules have been deprecated in accordance with the deprecation policy, and replaced by newer rules:", - "rules": [] + "types": { + "problem": [], + "suggestion": [], + "layout": [] }, - "removed": { - "name": "Removed", - "description": "These rules from older versions of ESLint (before the deprecation policy existed) have been replaced by newer rules:", - "rules": [ - { "removed": "generator-star", "replacedBy": ["generator-star-spacing"] }, - { "removed": "global-strict", "replacedBy": ["strict"] }, - { "removed": "no-arrow-condition", "replacedBy": ["no-confusing-arrow", "no-constant-condition"] }, - { "removed": "no-comma-dangle", "replacedBy": ["comma-dangle"] }, - { "removed": "no-empty-class", "replacedBy": ["no-empty-character-class"] }, - { "removed": "no-empty-label", "replacedBy": ["no-labels"] }, - { "removed": "no-extra-strict", "replacedBy": ["strict"] }, - { "removed": "no-reserved-keys", "replacedBy": ["quote-props"] }, - { "removed": "no-space-before-semi", "replacedBy": ["semi-spacing"] }, - { "removed": "no-wrap-func", "replacedBy": ["no-extra-parens"] }, - { "removed": "space-after-function-name", "replacedBy": ["space-before-function-paren"] }, - { "removed": "space-after-keywords", "replacedBy": ["keyword-spacing"] }, - { "removed": "space-before-function-parentheses", "replacedBy": ["space-before-function-paren"] }, - { "removed": "space-before-keywords", "replacedBy": ["keyword-spacing"] }, - { "removed": "space-in-brackets", "replacedBy": ["object-curly-spacing", "array-bracket-spacing"] }, - { "removed": "space-return-throw-case", "replacedBy": ["keyword-spacing"] }, - { "removed": "space-unary-word-ops", "replacedBy": ["space-unary-ops"] }, - { "removed": "spaced-line-comment", "replacedBy": ["spaced-comment"] } - ] - } + "deprecated": [], + "removed": [ + { "removed": "generator-star", "replacedBy": ["generator-star-spacing"] }, + { "removed": "global-strict", "replacedBy": ["strict"] }, + { "removed": "no-arrow-condition", "replacedBy": ["no-confusing-arrow", "no-constant-condition"] }, + { "removed": "no-comma-dangle", "replacedBy": ["comma-dangle"] }, + { "removed": "no-empty-class", "replacedBy": ["no-empty-character-class"] }, + { "removed": "no-empty-label", "replacedBy": ["no-labels"] }, + { "removed": "no-extra-strict", "replacedBy": ["strict"] }, + { "removed": "no-reserved-keys", "replacedBy": ["quote-props"] }, + { "removed": "no-space-before-semi", "replacedBy": ["semi-spacing"] }, + { "removed": "no-wrap-func", "replacedBy": ["no-extra-parens"] }, + { "removed": "space-after-function-name", "replacedBy": ["space-before-function-paren"] }, + { "removed": "space-after-keywords", "replacedBy": ["keyword-spacing"] }, + { "removed": "space-before-function-parentheses", "replacedBy": ["space-before-function-paren"] }, + { "removed": "space-before-keywords", "replacedBy": ["keyword-spacing"] }, + { "removed": "space-in-brackets", "replacedBy": ["object-curly-spacing", "array-bracket-spacing"] }, + { "removed": "space-return-throw-case", "replacedBy": ["keyword-spacing"] }, + { "removed": "space-unary-word-ops", "replacedBy": ["space-unary-ops"] }, + { "removed": "spaced-line-comment", "replacedBy": ["spaced-comment"] } + ] } diff --git a/docs/.eleventy.js b/docs/.eleventy.js index 1bd1de1af2f..75a372d3d5e 100644 --- a/docs/.eleventy.js +++ b/docs/.eleventy.js @@ -54,6 +54,7 @@ module.exports = function(eleventyConfig) { eleventyConfig.addGlobalData("GIT_BRANCH", process.env.BRANCH); eleventyConfig.addGlobalData("HEAD", process.env.BRANCH === "main"); eleventyConfig.addGlobalData("NOINDEX", process.env.BRANCH !== "latest"); + eleventyConfig.addGlobalData("PATH_PREFIX", pathPrefix); eleventyConfig.addDataExtension("yml", contents => yaml.load(contents)); //------------------------------------------------------------------------------ @@ -179,14 +180,68 @@ module.exports = function(eleventyConfig) { `.trim(); } + /** + * Encodes text in the base 64 format used in playground URL params. + * @param {string} text Text to be encoded to base 64. + * @see https://github.com/eslint/eslint.org/blob/1b2f2aabeac2955a076d61788da8a0008bca6fb6/src/playground/utils/unicode.js + * @returns {string} The base 64 encoded equivalent of the text. + */ + function encodeToBase64(text) { + /* global btoa -- It does exist, and is what the playground uses. */ + return btoa(unescape(encodeURIComponent(text))); + } + + /** + * Creates markdownItContainer settings for a playground-linked codeblock. + * @param {string} name Plugin name and class name to add to the code block. + * @returns {[string, object]} Plugin name and options for markdown-it. + */ + function withPlaygroundRender(name) { + return [ + name, + { + render(tokens, index) { + if (tokens[index].nesting !== 1) { + return ""; + } + + // See https://github.com/eslint/eslint.org/blob/ac38ab41f99b89a8798d374f74e2cce01171be8b/src/playground/App.js#L44 + const parserOptionsJSON = tokens[index].info?.split("correct ")[1]?.trim(); + const parserOptions = { sourceType: "module", ...(parserOptionsJSON && JSON.parse(parserOptionsJSON)) }; + + // Remove trailing newline and presentational `⏎` characters (https://github.com/eslint/eslint/issues/17627): + const content = tokens[index + 1].content + .replace(/\n$/u, "") + .replace(/⏎(?=\n)/gu, ""); + const state = encodeToBase64( + JSON.stringify({ + options: { parserOptions }, + text: content + }) + ); + const prefix = process.env.CONTEXT && process.env.CONTEXT !== "deploy-preview" + ? "" + : "https://eslint.org"; + + return ` +
+ + Open in Playground + + `.trim(); + } + } + ]; + } + const markdownIt = require("markdown-it"); const md = markdownIt({ html: true, linkify: true, typographer: true, highlight: (str, lang) => highlighter(md, str, lang) }) .use(markdownItAnchor, { slugify: s => slug(s) }) .use(markdownItContainer, "img-container", {}) - .use(markdownItContainer, "correct", {}) - .use(markdownItContainer, "incorrect", {}) + .use(markdownItContainer, ...withPlaygroundRender("correct")) + .use(markdownItContainer, ...withPlaygroundRender("incorrect")) .use(markdownItContainer, "warning", { render(tokens, idx) { return generateAlertMarkup("warning", tokens, idx); @@ -437,25 +492,6 @@ module.exports = function(eleventyConfig) { // Settings //------------------------------------------------------------------------------ - /* - * When we run `eleventy --serve`, Eleventy 1.x uses browser-sync to serve the content. - * By default, browser-sync (more precisely, underlying serve-static) will not serve - * `foo/bar.html` when we request `foo/bar`. Thus, we need to rewrite URLs to append `.html` - * so that pretty links without `.html` can work in a local development environment. - * - * There's no need to rewrite URLs that end with `/`, because that already works well - * (server will return the content of `index.html` in the directory). - * URLs with a file extension, like main.css, main.js, sitemap.xml, etc. should not be rewritten - */ - eleventyConfig.setBrowserSyncConfig({ - middleware(req, res, next) { - if (!/(?:\.[a-zA-Z][^/]*|\/)$/u.test(req.url)) { - req.url += ".html"; - } - return next(); - } - }); - /* * Generate the sitemap only in certain contexts to prevent unwanted discovery of sitemaps that * contain URLs we'd prefer not to appear in search results (URLs in sitemaps are considered important). @@ -475,14 +511,12 @@ module.exports = function(eleventyConfig) { eleventyConfig.ignores.add("src/static/sitemap.njk"); // ... then don't generate the sitemap. } - return { passthroughFileCopy: true, pathPrefix, markdownTemplateEngine: "njk", - dataTemplateEngine: "njk", htmlTemplateEngine: "njk", dir: { diff --git a/docs/README.md b/docs/README.md index dbf3c02e8f4..026430cc0d2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,21 +25,21 @@ Once the script finishes building the documentation site, you can visit it at To update the links data file, run this from the root folder (not the `docs` folder): ```shell -npm run docs:update-links +npm run build:docs:update-links ``` To lint JS files, run this from the root folder (not the `docs` folder): ```shell -npm run lint:docsjs +npm run lint:docs:js ``` To autofix JS files, run this from the root folder (not the `docs` folder): ```shell -npm run fix:docsjs +npm run lint:fix:docs:js ``` ## License -© OpenJS Foundation and ESLint contributors, [www.openjsf.org](https://www.openjsf.org/). Content licensed under [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-nc-sa/4.0/). +© OpenJS Foundation and ESLint contributors, [www.openjsf.org](https://www.openjsf.org/). Content licensed under [MIT License](https://github.com/eslint/eslint/blob/main/LICENSE). diff --git a/docs/_examples/custom-rule-tutorial-code/.gitignore b/docs/_examples/custom-rule-tutorial-code/.gitignore new file mode 100644 index 00000000000..b512c09d476 --- /dev/null +++ b/docs/_examples/custom-rule-tutorial-code/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/docs/_examples/custom-rule-tutorial-code/enforce-foo-bar.js b/docs/_examples/custom-rule-tutorial-code/enforce-foo-bar.js new file mode 100644 index 00000000000..5f3e677f638 --- /dev/null +++ b/docs/_examples/custom-rule-tutorial-code/enforce-foo-bar.js @@ -0,0 +1,57 @@ +/** + * @fileoverview Rule to enforce that `const foo` is assigned "bar". + * @author Ben Perlmutter + */ + +"use strict"; + +// The enforce-foo-bar rule definition +module.exports = { + meta: { + type: "problem", + docs: { + description: "Enforce that a variable named `foo` can only be assigned a value of 'bar'." + }, + fixable: "code", + schema: [] + }, + create(context) { + return { + + // Performs action in the function on every variable declarator + VariableDeclarator(node) { + + // Check if a `const` variable declaration + if (node.parent.kind === "const") { + + // Check if variable name is `foo` + if (node.id.type === "Identifier" && node.id.name === "foo") { + + // Check if value of variable is "bar" + if (node.init && node.init.type === "Literal" && node.init.value !== "bar") { + + /* + * Report error to ESLint. Error message uses + * a message placeholder to include the incorrect value + * in the error message. + * Also includes a `fix(fixer)` function that replaces + * any values assigned to `const foo` with "bar". + */ + context.report({ + node, + message: 'Value other than "bar" assigned to `const foo`. Unexpected value: {{ notBar }}.', + data: { + notBar: node.init.value + }, + fix(fixer) { + return fixer.replaceText(node.init, '"bar"'); + } + }); + } + } + } + } + }; + } +}; + diff --git a/docs/_examples/custom-rule-tutorial-code/enforce-foo-bar.test.js b/docs/_examples/custom-rule-tutorial-code/enforce-foo-bar.test.js new file mode 100644 index 00000000000..d5f9c40334d --- /dev/null +++ b/docs/_examples/custom-rule-tutorial-code/enforce-foo-bar.test.js @@ -0,0 +1,34 @@ +/** + * @fileoverview Tests for enforce-foo-bar.js rule. + * @author Ben Perlmutter +*/ +"use strict"; + +const {RuleTester} = require("eslint"); +const fooBarRule = require("./enforce-foo-bar"); + +const ruleTester = new RuleTester({ + // Must use at least ecmaVersion 2015 because + // that's when `const` variable were introduced. + parserOptions: { ecmaVersion: 2015 } +}); + +// Throws error if the tests in ruleTester.run() do not pass +ruleTester.run( + "enforce-foo-bar", // rule name + fooBarRule, // rule code + { // checks + // 'valid' checks cases that should pass + valid: [{ + code: "const foo = 'bar';", + }], + // 'invalid' checks cases that should not pass + invalid: [{ + code: "const foo = 'baz';", + output: 'const foo = "bar";', + errors: 1, + }], + } +); + +console.log("All tests passed!"); \ No newline at end of file diff --git a/docs/_examples/custom-rule-tutorial-code/eslint-plugin-example.js b/docs/_examples/custom-rule-tutorial-code/eslint-plugin-example.js new file mode 100644 index 00000000000..1a32ca4db0a --- /dev/null +++ b/docs/_examples/custom-rule-tutorial-code/eslint-plugin-example.js @@ -0,0 +1,9 @@ +/** + * @fileoverview Example an ESLint plugin with a custom rule. + * @author Ben Perlmutter +*/ +"use strict"; + +const fooBarRule = require("./enforce-foo-bar"); +const plugin = { rules: { "enforce-foo-bar": fooBarRule } }; +module.exports = plugin; diff --git a/docs/_examples/custom-rule-tutorial-code/eslint.config.js b/docs/_examples/custom-rule-tutorial-code/eslint.config.js new file mode 100644 index 00000000000..cf08f1ee57c --- /dev/null +++ b/docs/_examples/custom-rule-tutorial-code/eslint.config.js @@ -0,0 +1,23 @@ +/** + * @fileoverview Example ESLint config file that uses the custom rule from this tutorial. + * @author Ben Perlmutter +*/ +"use strict"; + +// Import the ESLint plugin +const eslintPluginExample = require("./eslint-plugin-example"); + +module.exports = [ + { + files: ["**/*.js"], + languageOptions: { + sourceType: "commonjs", + ecmaVersion: "latest", + }, + // Using the eslint-plugin-example plugin defined locally + plugins: {"example": eslintPluginExample}, + rules: { + "example/enforce-foo-bar": "error", + }, + } +] diff --git a/docs/_examples/custom-rule-tutorial-code/example.js b/docs/_examples/custom-rule-tutorial-code/example.js new file mode 100644 index 00000000000..0d6da91d49e --- /dev/null +++ b/docs/_examples/custom-rule-tutorial-code/example.js @@ -0,0 +1,22 @@ +/** + * @fileoverview Example of a file that will fail the custom rule in this tutorial. + * @author Ben Perlmutter +*/ +"use strict"; + +/* eslint-disable no-unused-vars -- Disable other rule causing problem for this file */ + +// To see the error in the terminal, run the following command: +// npx eslint example.js + +// To fix the error, run the following command: +// npx eslint example.js --fix + +function correctFooBar() { + const foo = "bar"; +} + +function incorrectFoo(){ + const foo = "baz"; // Problem! +} + diff --git a/docs/_examples/custom-rule-tutorial-code/package.json b/docs/_examples/custom-rule-tutorial-code/package.json new file mode 100644 index 00000000000..0578c79496c --- /dev/null +++ b/docs/_examples/custom-rule-tutorial-code/package.json @@ -0,0 +1,22 @@ +{ + "name": "eslint-plugin-example", + "version": "1.0.0", + "description": "ESLint plugin for enforce-foo-bar rule.", + "main": "eslint-plugin-example.js", + "keywords": [ + "eslint", + "eslintplugin", + "eslint-plugin" + ], + "peerDependencies": { + "eslint": ">=8.0.0" + }, + "scripts": { + "test": "node enforce-foo-bar.test.js" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "eslint": "^8.36.0" + } +} \ No newline at end of file diff --git a/docs/_examples/integration-tutorial-code/.gitignore b/docs/_examples/integration-tutorial-code/.gitignore new file mode 100644 index 00000000000..28f1ba7565f --- /dev/null +++ b/docs/_examples/integration-tutorial-code/.gitignore @@ -0,0 +1,2 @@ +node_modules +.DS_Store \ No newline at end of file diff --git a/docs/_examples/integration-tutorial-code/example-eslint-integration.js b/docs/_examples/integration-tutorial-code/example-eslint-integration.js new file mode 100644 index 00000000000..f36b4e46e76 --- /dev/null +++ b/docs/_examples/integration-tutorial-code/example-eslint-integration.js @@ -0,0 +1,62 @@ +/** + * @fileoverview An example of how to integrate ESLint into your own tool + * @author Ben Perlmutter + */ + +const { ESLint } = require("eslint"); + +// Create an instance of ESLint with the configuration passed to the function +function createESLintInstance(overrideConfig){ + return new ESLint({ useEslintrc: false, overrideConfig: overrideConfig, fix: true }); +} + +// Lint the specified files and return the error results +async function lintAndFix(eslint, filePaths) { + const results = await eslint.lintFiles(filePaths); + + // Apply automatic fixes and output fixed code + await ESLint.outputFixes(results); + + return results; +} + +// Log results to console if there are any problems +function outputLintingResults(results) { + // Identify the number of problems found + const problems = results.reduce((acc, result) => acc + result.errorCount + result.warningCount, 0); + + if (problems > 0) { + console.log("Linting errors found!"); + console.log(results); + } else { + console.log("No linting errors found."); + } + return results; +} + +// Put previous functions all together +async function lintFiles(filePaths) { + + // The ESLint configuration. Alternatively, you could load the configuration + // from a .eslintrc file or just use the default config. + const overrideConfig = { + env: { + es6: true, + node: true, + }, + parserOptions: { + ecmaVersion: 2018, + }, + rules: { + "no-console": "error", + "no-unused-vars": "warn", + }, + }; + + const eslint = createESLintInstance(overrideConfig); + const results = await lintAndFix(eslint, filePaths); + return outputLintingResults(results); +} + +// Export integration +module.exports = { lintFiles } \ No newline at end of file diff --git a/docs/_examples/integration-tutorial-code/example-eslint-integration.test.js b/docs/_examples/integration-tutorial-code/example-eslint-integration.test.js new file mode 100644 index 00000000000..5db9aead60a --- /dev/null +++ b/docs/_examples/integration-tutorial-code/example-eslint-integration.test.js @@ -0,0 +1,27 @@ +/** + * @fileoverview Test ESLint integration example code + * @author Ben Perlmutter + */ + +const { lintFiles } = require("./example-eslint-integration"); + +async function testExampleEslintIntegration(){ + const filePaths = ["sample-data/test-file.js"]; + const lintResults = await lintFiles(filePaths); + + // Test cases + if(lintResults[0].messages.length !== 6){ + throw new Error("Expected 6 linting problems, got " + lintResults[0].messages.length); + } + const messageRuleIds = new Set() + lintResults[0].messages.forEach(msg => messageRuleIds.add(msg.ruleId)); + if(messageRuleIds.size !== 2){ + throw new Error("Expected 2 linting rule, got " + messageRuleIds.size); + } + if(!messageRuleIds.has("no-console")){ + throw new Error("Expected linting rule 'no-console', got " + messageRuleIds); + } + console.log("All tests passed!"); +} + +testExampleEslintIntegration() \ No newline at end of file diff --git a/docs/_examples/integration-tutorial-code/package.json b/docs/_examples/integration-tutorial-code/package.json new file mode 100644 index 00000000000..df00d7382f1 --- /dev/null +++ b/docs/_examples/integration-tutorial-code/package.json @@ -0,0 +1,15 @@ +{ + "name": "_integration-tutorial-code", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "node example-eslint-integration.test.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "eslint": "^8.39.0" + } +} diff --git a/docs/_examples/integration-tutorial-code/sample-data/test-file.js b/docs/_examples/integration-tutorial-code/sample-data/test-file.js new file mode 100644 index 00000000000..425375f8f8a --- /dev/null +++ b/docs/_examples/integration-tutorial-code/sample-data/test-file.js @@ -0,0 +1,29 @@ +/** + * @fileoverview Example data to lint using ESLint. This file contains a variety of errors. + * @author Ben Perlmutter + */ + +// Unused variable 'y' (no-unused-vars from configured rules) +const y = 20; + +function add(a, b) { + // Unexpected console statement (no-console from configured rules) + console.log('Adding two numbers'); + return a + b; +} + +// 'result' is assigned a value but never used (no-unused-vars from configured rules) +const result = add(x, 5); + +if (x > 5) { + // Unexpected console statement (no-console from configured rules) + console.log('x is greater than 5'); +} else { + // Unexpected console statement (no-console from configured rules) + console.log('x is not greater than 5'); +} + +// 'subtract' is defined but never used (no-unused-vars from configured rules) +function subtract(a, b) { + return a - b; +} diff --git a/docs/package.json b/docs/package.json index 7be17b97436..0619d4d5b8f 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,33 +1,33 @@ { "name": "docs-eslint", "private": true, - "version": "8.41.0", + "version": "8.53.0", "description": "", "main": "index.js", "keywords": [], "author": "", - "license": "ISC", + "license": "MIT", "files": [], "scripts": { - "images": "imagemin '_site/assets/images' --out-dir='_site/assets/images'", - "watch:postcss": "postcss src/assets/css -d src/assets/css --watch --poll", - "watch:sass": "sass --watch --poll src/assets/scss:src/assets/css --no-source-map", - "watch:eleventy": "eleventy --serve --port=2023", + "build": "npm-run-all build:sass build:postcss build:website build:minify-images", "build:postcss": "postcss src/assets/css -d src/assets/css", + "build:postcss:watch": "postcss src/assets/css -d src/assets/css --watch --poll", "build:sass": "sass src/assets/scss:src/assets/css --no-source-map", - "build:eleventy": "npx @11ty/eleventy", - "start": "npm-run-all build:sass build:postcss --parallel watch:*", - "build": "npm-run-all build:sass build:postcss build:eleventy images", - "lint:scss": "stylelint \"**/*.{scss,html}\"", + "build:sass:watch": "sass --watch --poll src/assets/scss:src/assets/css --no-source-map", + "build:website": "npx @11ty/eleventy", + "build:website:watch": "eleventy --serve --port=2023", "lint:links": "cross-env NODE_OPTIONS=--max-old-space-size=4096 node tools/validate-links.js", - "lint:fix:scss": "npm run lint:scss -- --fix" + "lint:scss": "stylelint \"**/*.{scss,html}\"", + "lint:fix:scss": "npm run lint:scss -- --fix", + "build:minify-images": "imagemin '_site/assets/images' --out-dir='_site/assets/images'", + "start": "npm-run-all build:sass build:postcss --parallel *:*:watch" }, "devDependencies": { - "@11ty/eleventy": "^1.0.1", - "@11ty/eleventy-img": "^1.0.0", - "@11ty/eleventy-navigation": "^0.3.2", + "@11ty/eleventy": "^2.0.1", + "@11ty/eleventy-img": "^3.1.1", + "@11ty/eleventy-navigation": "^0.3.5", "@11ty/eleventy-plugin-rss": "^1.1.1", - "@11ty/eleventy-plugin-syntaxhighlight": "^3.1.2", + "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0", "@munter/tap-render": "^0.2.0", "@types/markdown-it": "^12.2.3", "algoliasearch": "^4.12.1", diff --git a/docs/src/_data/further_reading_links.json b/docs/src/_data/further_reading_links.json index 120dd37032f..a33ce6c0b3b 100644 --- a/docs/src/_data/further_reading_links.json +++ b/docs/src/_data/further_reading_links.json @@ -719,5 +719,33 @@ "logo": "https://tc39.es/ecma262/img/favicon.ico", "title": "ECMAScript® 2023 Language Specification", "description": null + }, + "https://v8.dev/blog/fast-async": { + "domain": "v8.dev", + "url": "https://v8.dev/blog/fast-async", + "logo": "https://v8.dev/favicon.ico", + "title": "Faster async functions and promises · V8", + "description": "Faster and easier-to-debug async functions and promises are coming to V8 v7.2 / Chrome 72." + }, + "https://github.com/tc39/proposal-regexp-v-flag": { + "domain": "github.com", + "url": "https://github.com/tc39/proposal-regexp-v-flag", + "logo": "https://github.com/fluidicon.png", + "title": "GitHub - tc39/proposal-regexp-v-flag: UTS18 set notation in regular expressions", + "description": "UTS18 set notation in regular expressions. Contribute to tc39/proposal-regexp-v-flag development by creating an account on GitHub." + }, + "https://v8.dev/features/regexp-v-flag": { + "domain": "v8.dev", + "url": "https://v8.dev/features/regexp-v-flag", + "logo": "https://v8.dev/favicon.ico", + "title": "RegExp v flag with set notation and properties of strings · V8", + "description": "The new RegExp `v` flag enables `unicodeSets` mode, unlocking support for extended character classes, including Unicode properties of strings, set notation, and improved case-insensitive matching." + }, + "https://codepoints.net/U+1680": { + "domain": "codepoints.net", + "url": "https://codepoints.net/U+1680", + "logo": "https://codepoints.net/favicon.ico", + "title": "U+1680 OGHAM SPACE MARK:   – Unicode – Codepoints", + "description": " , codepoint U+1680 OGHAM SPACE MARK in Unicode, is located in the block “Ogham”. It belongs to the Ogham script and is a Space Separator." } } \ No newline at end of file diff --git a/docs/src/_data/rule_versions.json b/docs/src/_data/rule_versions.json index 3e0b0ad761d..14e6d858ac0 100644 --- a/docs/src/_data/rule_versions.json +++ b/docs/src/_data/rule_versions.json @@ -307,7 +307,8 @@ "yoda": "0.7.1", "logical-assignment-operators": "8.24.0", "no-empty-static-block": "8.27.0", - "no-new-native-nonconstructor": "8.27.0" + "no-new-native-nonconstructor": "8.27.0", + "no-object-constructor": "8.50.0" }, "removed": { "generator-star": "1.0.0-rc-1", diff --git a/docs/src/_data/rules.json b/docs/src/_data/rules.json index 45caaa9f031..33915152ad6 100644 --- a/docs/src/_data/rules.json +++ b/docs/src/_data/rules.json @@ -1,2126 +1,1905 @@ { - "types": [ - { - "name": "problem", - "displayName": "Possible Problems", - "description": "These rules relate to possible logic errors in code:", - "rules": [ - { - "name": "array-callback-return", - "description": "Enforce `return` statements in callbacks of array methods", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "constructor-super", - "description": "Require `super()` calls in constructors", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "for-direction", - "description": "Enforce \"for\" loop update clause moving the counter in the right direction", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "getter-return", - "description": "Enforce `return` statements in getters", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-async-promise-executor", - "description": "Disallow using an async function as a Promise executor", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-await-in-loop", - "description": "Disallow `await` inside of loops", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-class-assign", - "description": "Disallow reassigning class members", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-compare-neg-zero", - "description": "Disallow comparing against -0", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-cond-assign", - "description": "Disallow assignment operators in conditional expressions", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-const-assign", - "description": "Disallow reassigning `const` variables", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-constant-binary-expression", - "description": "Disallow expressions where the operation doesn't affect the value", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-constant-condition", - "description": "Disallow constant expressions in conditions", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-constructor-return", - "description": "Disallow returning value from constructor", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-control-regex", - "description": "Disallow control characters in regular expressions", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-debugger", - "description": "Disallow the use of `debugger`", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-dupe-args", - "description": "Disallow duplicate arguments in `function` definitions", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-dupe-class-members", - "description": "Disallow duplicate class members", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-dupe-else-if", - "description": "Disallow duplicate conditions in if-else-if chains", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-dupe-keys", - "description": "Disallow duplicate keys in object literals", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-duplicate-case", - "description": "Disallow duplicate case labels", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-duplicate-imports", - "description": "Disallow duplicate module imports", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-empty-character-class", - "description": "Disallow empty character classes in regular expressions", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-empty-pattern", - "description": "Disallow empty destructuring patterns", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-ex-assign", - "description": "Disallow reassigning exceptions in `catch` clauses", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-fallthrough", - "description": "Disallow fallthrough of `case` statements", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-func-assign", - "description": "Disallow reassigning `function` declarations", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-import-assign", - "description": "Disallow assigning to imported bindings", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-inner-declarations", - "description": "Disallow variable or `function` declarations in nested blocks", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-invalid-regexp", - "description": "Disallow invalid regular expression strings in `RegExp` constructors", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-irregular-whitespace", - "description": "Disallow irregular whitespace", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-loss-of-precision", - "description": "Disallow literal numbers that lose precision", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-misleading-character-class", - "description": "Disallow characters which are made with multiple code points in character class syntax", - "recommended": true, - "fixable": false, - "hasSuggestions": true - }, - { - "name": "no-new-native-nonconstructor", - "description": "Disallow `new` operators with global non-constructor functions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-new-symbol", - "description": "Disallow `new` operators with the `Symbol` object", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-obj-calls", - "description": "Disallow calling global object properties as functions", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-promise-executor-return", - "description": "Disallow returning values from Promise executor functions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-prototype-builtins", - "description": "Disallow calling some `Object.prototype` methods directly on objects", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-self-assign", - "description": "Disallow assignments where both sides are exactly the same", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-self-compare", - "description": "Disallow comparisons where both sides are exactly the same", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-setter-return", - "description": "Disallow returning values from setters", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-sparse-arrays", - "description": "Disallow sparse arrays", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-template-curly-in-string", - "description": "Disallow template literal placeholder syntax in regular strings", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-this-before-super", - "description": "Disallow `this`/`super` before calling `super()` in constructors", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-undef", - "description": "Disallow the use of undeclared variables unless mentioned in `/*global */` comments", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-unexpected-multiline", - "description": "Disallow confusing multiline expressions", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-unmodified-loop-condition", - "description": "Disallow unmodified loop conditions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-unreachable", - "description": "Disallow unreachable code after `return`, `throw`, `continue`, and `break` statements", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-unreachable-loop", - "description": "Disallow loops with a body that allows only one iteration", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-unsafe-finally", - "description": "Disallow control flow statements in `finally` blocks", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-unsafe-negation", - "description": "Disallow negating the left operand of relational operators", - "recommended": true, - "fixable": false, - "hasSuggestions": true - }, - { - "name": "no-unsafe-optional-chaining", - "description": "Disallow use of optional chaining in contexts where the `undefined` value is not allowed", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-unused-private-class-members", - "description": "Disallow unused private class members", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-unused-vars", - "description": "Disallow unused variables", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-use-before-define", - "description": "Disallow the use of variables before they are defined", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-useless-backreference", - "description": "Disallow useless backreferences in regular expressions", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "require-atomic-updates", - "description": "Disallow assignments that can lead to race conditions due to usage of `await` or `yield`", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "use-isnan", - "description": "Require calls to `isNaN()` when checking for `NaN`", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "valid-typeof", - "description": "Enforce comparing `typeof` expressions against valid strings", - "recommended": true, - "fixable": false, - "hasSuggestions": true - } - ] - }, - { - "name": "suggestion", - "displayName": "Suggestions", - "description": "These rules suggest alternate ways of doing things:", - "rules": [ - { - "name": "accessor-pairs", - "description": "Enforce getter and setter pairs in objects and classes", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "arrow-body-style", - "description": "Require braces around arrow function bodies", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "block-scoped-var", - "description": "Enforce the use of variables within the scope they are defined", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "camelcase", - "description": "Enforce camelcase naming convention", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "capitalized-comments", - "description": "Enforce or disallow capitalization of the first letter of a comment", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "class-methods-use-this", - "description": "Enforce that class methods utilize `this`", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "complexity", - "description": "Enforce a maximum cyclomatic complexity allowed in a program", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "consistent-return", - "description": "Require `return` statements to either always or never specify values", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "consistent-this", - "description": "Enforce consistent naming when capturing the current execution context", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "curly", - "description": "Enforce consistent brace style for all control statements", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "default-case", - "description": "Require `default` cases in `switch` statements", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "default-case-last", - "description": "Enforce default clauses in switch statements to be last", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "default-param-last", - "description": "Enforce default parameters to be last", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "dot-notation", - "description": "Enforce dot notation whenever possible", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "eqeqeq", - "description": "Require the use of `===` and `!==`", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "func-name-matching", - "description": "Require function names to match the name of the variable or property to which they are assigned", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "func-names", - "description": "Require or disallow named `function` expressions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "func-style", - "description": "Enforce the consistent use of either `function` declarations or expressions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "grouped-accessor-pairs", - "description": "Require grouped accessor pairs in object literals and classes", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "guard-for-in", - "description": "Require `for-in` loops to include an `if` statement", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "id-denylist", - "description": "Disallow specified identifiers", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "id-length", - "description": "Enforce minimum and maximum identifier lengths", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "id-match", - "description": "Require identifiers to match a specified regular expression", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "init-declarations", - "description": "Require or disallow initialization in variable declarations", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "logical-assignment-operators", - "description": "Require or disallow logical assignment operator shorthand", - "recommended": false, - "fixable": true, - "hasSuggestions": true - }, - { - "name": "max-classes-per-file", - "description": "Enforce a maximum number of classes per file", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "max-depth", - "description": "Enforce a maximum depth that blocks can be nested", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "max-lines", - "description": "Enforce a maximum number of lines per file", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "max-lines-per-function", - "description": "Enforce a maximum number of lines of code in a function", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "max-nested-callbacks", - "description": "Enforce a maximum depth that callbacks can be nested", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "max-params", - "description": "Enforce a maximum number of parameters in function definitions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "max-statements", - "description": "Enforce a maximum number of statements allowed in function blocks", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "multiline-comment-style", - "description": "Enforce a particular style for multiline comments", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "new-cap", - "description": "Require constructor names to begin with a capital letter", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-alert", - "description": "Disallow the use of `alert`, `confirm`, and `prompt`", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-array-constructor", - "description": "Disallow `Array` constructors", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-bitwise", - "description": "Disallow bitwise operators", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-caller", - "description": "Disallow the use of `arguments.caller` or `arguments.callee`", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-case-declarations", - "description": "Disallow lexical declarations in case clauses", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-confusing-arrow", - "description": "Disallow arrow functions where they could be confused with comparisons", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-console", - "description": "Disallow the use of `console`", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-continue", - "description": "Disallow `continue` statements", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-delete-var", - "description": "Disallow deleting variables", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-div-regex", - "description": "Disallow equal signs explicitly at the beginning of regular expressions", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-else-return", - "description": "Disallow `else` blocks after `return` statements in `if` statements", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-empty", - "description": "Disallow empty block statements", - "recommended": true, - "fixable": false, - "hasSuggestions": true - }, - { - "name": "no-empty-function", - "description": "Disallow empty functions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-empty-static-block", - "description": "Disallow empty static blocks", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-eq-null", - "description": "Disallow `null` comparisons without type-checking operators", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-eval", - "description": "Disallow the use of `eval()`", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-extend-native", - "description": "Disallow extending native types", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-extra-bind", - "description": "Disallow unnecessary calls to `.bind()`", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-extra-boolean-cast", - "description": "Disallow unnecessary boolean casts", - "recommended": true, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-extra-label", - "description": "Disallow unnecessary labels", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-extra-semi", - "description": "Disallow unnecessary semicolons", - "recommended": true, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-floating-decimal", - "description": "Disallow leading or trailing decimal points in numeric literals", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-global-assign", - "description": "Disallow assignments to native objects or read-only global variables", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-implicit-coercion", - "description": "Disallow shorthand type conversions", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-implicit-globals", - "description": "Disallow declarations in the global scope", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-implied-eval", - "description": "Disallow the use of `eval()`-like methods", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-inline-comments", - "description": "Disallow inline comments after code", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-invalid-this", - "description": "Disallow use of `this` in contexts where the value of `this` is `undefined`", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-iterator", - "description": "Disallow the use of the `__iterator__` property", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-label-var", - "description": "Disallow labels that share a name with a variable", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-labels", - "description": "Disallow labeled statements", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-lone-blocks", - "description": "Disallow unnecessary nested blocks", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-lonely-if", - "description": "Disallow `if` statements as the only statement in `else` blocks", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-loop-func", - "description": "Disallow function declarations that contain unsafe references inside loop statements", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-magic-numbers", - "description": "Disallow magic numbers", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-mixed-operators", - "description": "Disallow mixed binary operators", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-multi-assign", - "description": "Disallow use of chained assignment expressions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-multi-str", - "description": "Disallow multiline strings", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-negated-condition", - "description": "Disallow negated conditions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-nested-ternary", - "description": "Disallow nested ternary expressions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-new", - "description": "Disallow `new` operators outside of assignments or comparisons", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-new-func", - "description": "Disallow `new` operators with the `Function` object", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-new-object", - "description": "Disallow `Object` constructors", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-new-wrappers", - "description": "Disallow `new` operators with the `String`, `Number`, and `Boolean` objects", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-nonoctal-decimal-escape", - "description": "Disallow `\\8` and `\\9` escape sequences in string literals", - "recommended": true, - "fixable": false, - "hasSuggestions": true - }, - { - "name": "no-octal", - "description": "Disallow octal literals", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-octal-escape", - "description": "Disallow octal escape sequences in string literals", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-param-reassign", - "description": "Disallow reassigning `function` parameters", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-plusplus", - "description": "Disallow the unary operators `++` and `--`", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-proto", - "description": "Disallow the use of the `__proto__` property", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-redeclare", - "description": "Disallow variable redeclaration", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-regex-spaces", - "description": "Disallow multiple spaces in regular expressions", - "recommended": true, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-restricted-exports", - "description": "Disallow specified names in exports", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-restricted-globals", - "description": "Disallow specified global variables", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-restricted-imports", - "description": "Disallow specified modules when loaded by `import`", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-restricted-properties", - "description": "Disallow certain properties on certain objects", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-restricted-syntax", - "description": "Disallow specified syntax", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-return-assign", - "description": "Disallow assignment operators in `return` statements", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-return-await", - "description": "Disallow unnecessary `return await`", - "recommended": false, - "fixable": false, - "hasSuggestions": true - }, - { - "name": "no-script-url", - "description": "Disallow `javascript:` urls", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-sequences", - "description": "Disallow comma operators", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-shadow", - "description": "Disallow variable declarations from shadowing variables declared in the outer scope", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-shadow-restricted-names", - "description": "Disallow identifiers from shadowing restricted names", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-ternary", - "description": "Disallow ternary operators", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-throw-literal", - "description": "Disallow throwing literals as exceptions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-undef-init", - "description": "Disallow initializing variables to `undefined`", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-undefined", - "description": "Disallow the use of `undefined` as an identifier", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-underscore-dangle", - "description": "Disallow dangling underscores in identifiers", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-unneeded-ternary", - "description": "Disallow ternary operators when simpler alternatives exist", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-unused-expressions", - "description": "Disallow unused expressions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-unused-labels", - "description": "Disallow unused labels", - "recommended": true, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-useless-call", - "description": "Disallow unnecessary calls to `.call()` and `.apply()`", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-useless-catch", - "description": "Disallow unnecessary `catch` clauses", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-useless-computed-key", - "description": "Disallow unnecessary computed property keys in objects and classes", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-useless-concat", - "description": "Disallow unnecessary concatenation of literals or template literals", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-useless-constructor", - "description": "Disallow unnecessary constructors", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-useless-escape", - "description": "Disallow unnecessary escape characters", - "recommended": true, - "fixable": false, - "hasSuggestions": true - }, - { - "name": "no-useless-rename", - "description": "Disallow renaming import, export, and destructured assignments to the same name", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-useless-return", - "description": "Disallow redundant return statements", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-var", - "description": "Require `let` or `const` instead of `var`", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-void", - "description": "Disallow `void` operators", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-warning-comments", - "description": "Disallow specified warning terms in comments", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-with", - "description": "Disallow `with` statements", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "object-shorthand", - "description": "Require or disallow method and property shorthand syntax for object literals", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "one-var", - "description": "Enforce variables to be declared either together or separately in functions", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "one-var-declaration-per-line", - "description": "Require or disallow newlines around variable declarations", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "operator-assignment", - "description": "Require or disallow assignment operator shorthand where possible", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "prefer-arrow-callback", - "description": "Require using arrow functions for callbacks", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "prefer-const", - "description": "Require `const` declarations for variables that are never reassigned after declared", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "prefer-destructuring", - "description": "Require destructuring from arrays and/or objects", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "prefer-exponentiation-operator", - "description": "Disallow the use of `Math.pow` in favor of the `**` operator", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "prefer-named-capture-group", - "description": "Enforce using named capture group in regular expression", - "recommended": false, - "fixable": false, - "hasSuggestions": true - }, - { - "name": "prefer-numeric-literals", - "description": "Disallow `parseInt()` and `Number.parseInt()` in favor of binary, octal, and hexadecimal literals", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "prefer-object-has-own", - "description": "Disallow use of `Object.prototype.hasOwnProperty.call()` and prefer use of `Object.hasOwn()`", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "prefer-object-spread", - "description": "Disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "prefer-promise-reject-errors", - "description": "Require using Error objects as Promise rejection reasons", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "prefer-regex-literals", - "description": "Disallow use of the `RegExp` constructor in favor of regular expression literals", - "recommended": false, - "fixable": false, - "hasSuggestions": true - }, - { - "name": "prefer-rest-params", - "description": "Require rest parameters instead of `arguments`", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "prefer-spread", - "description": "Require spread operators instead of `.apply()`", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "prefer-template", - "description": "Require template literals instead of string concatenation", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "quote-props", - "description": "Require quotes around object literal property names", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "radix", - "description": "Enforce the consistent use of the radix argument when using `parseInt()`", - "recommended": false, - "fixable": false, - "hasSuggestions": true - }, - { - "name": "require-await", - "description": "Disallow async functions which have no `await` expression", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "require-unicode-regexp", - "description": "Enforce the use of `u` flag on RegExp", - "recommended": false, - "fixable": false, - "hasSuggestions": true - }, - { - "name": "require-yield", - "description": "Require generator functions to contain `yield`", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "sort-imports", - "description": "Enforce sorted import declarations within modules", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "sort-keys", - "description": "Require object keys to be sorted", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "sort-vars", - "description": "Require variables within the same declaration block to be sorted", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "spaced-comment", - "description": "Enforce consistent spacing after the `//` or `/*` in a comment", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "strict", - "description": "Require or disallow strict mode directives", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "symbol-description", - "description": "Require symbol descriptions", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "vars-on-top", - "description": "Require `var` declarations be placed at the top of their containing scope", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "yoda", - "description": "Require or disallow \"Yoda\" conditions", - "recommended": false, - "fixable": true, - "hasSuggestions": false - } - ] - }, - { - "name": "layout", - "displayName": "Layout & Formatting", - "description": "These rules care about how the code looks rather than how it executes:", - "rules": [ - { - "name": "array-bracket-newline", - "description": "Enforce linebreaks after opening and before closing array brackets", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "array-bracket-spacing", - "description": "Enforce consistent spacing inside array brackets", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "array-element-newline", - "description": "Enforce line breaks after each array element", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "arrow-parens", - "description": "Require parentheses around arrow function arguments", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "arrow-spacing", - "description": "Enforce consistent spacing before and after the arrow in arrow functions", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "block-spacing", - "description": "Disallow or enforce spaces inside of blocks after opening block and before closing block", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "brace-style", - "description": "Enforce consistent brace style for blocks", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "comma-dangle", - "description": "Require or disallow trailing commas", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "comma-spacing", - "description": "Enforce consistent spacing before and after commas", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "comma-style", - "description": "Enforce consistent comma style", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "computed-property-spacing", - "description": "Enforce consistent spacing inside computed property brackets", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "dot-location", - "description": "Enforce consistent newlines before and after dots", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "eol-last", - "description": "Require or disallow newline at the end of files", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "func-call-spacing", - "description": "Require or disallow spacing between function identifiers and their invocations", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "function-call-argument-newline", - "description": "Enforce line breaks between arguments of a function call", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "function-paren-newline", - "description": "Enforce consistent line breaks inside function parentheses", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "generator-star-spacing", - "description": "Enforce consistent spacing around `*` operators in generator functions", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "implicit-arrow-linebreak", - "description": "Enforce the location of arrow function bodies", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "indent", - "description": "Enforce consistent indentation", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "jsx-quotes", - "description": "Enforce the consistent use of either double or single quotes in JSX attributes", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "key-spacing", - "description": "Enforce consistent spacing between keys and values in object literal properties", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "keyword-spacing", - "description": "Enforce consistent spacing before and after keywords", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "line-comment-position", - "description": "Enforce position of line comments", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "linebreak-style", - "description": "Enforce consistent linebreak style", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "lines-around-comment", - "description": "Require empty lines around comments", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "lines-between-class-members", - "description": "Require or disallow an empty line between class members", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "max-len", - "description": "Enforce a maximum line length", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "max-statements-per-line", - "description": "Enforce a maximum number of statements allowed per line", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "multiline-ternary", - "description": "Enforce newlines between operands of ternary expressions", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "new-parens", - "description": "Enforce or disallow parentheses when invoking a constructor with no arguments", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "newline-per-chained-call", - "description": "Require a newline after each call in a method chain", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-extra-parens", - "description": "Disallow unnecessary parentheses", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-mixed-spaces-and-tabs", - "description": "Disallow mixed spaces and tabs for indentation", - "recommended": true, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-multi-spaces", - "description": "Disallow multiple spaces", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-multiple-empty-lines", - "description": "Disallow multiple empty lines", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-tabs", - "description": "Disallow all tabs", - "recommended": false, - "fixable": false, - "hasSuggestions": false - }, - { - "name": "no-trailing-spaces", - "description": "Disallow trailing whitespace at the end of lines", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "no-whitespace-before-property", - "description": "Disallow whitespace before properties", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "nonblock-statement-body-position", - "description": "Enforce the location of single-line statements", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "object-curly-newline", - "description": "Enforce consistent line breaks after opening and before closing braces", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "object-curly-spacing", - "description": "Enforce consistent spacing inside braces", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "object-property-newline", - "description": "Enforce placing object properties on separate lines", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "operator-linebreak", - "description": "Enforce consistent linebreak style for operators", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "padded-blocks", - "description": "Require or disallow padding within blocks", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "padding-line-between-statements", - "description": "Require or disallow padding lines between statements", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "quotes", - "description": "Enforce the consistent use of either backticks, double, or single quotes", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "rest-spread-spacing", - "description": "Enforce spacing between rest and spread operators and their expressions", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "semi", - "description": "Require or disallow semicolons instead of ASI", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "semi-spacing", - "description": "Enforce consistent spacing before and after semicolons", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "semi-style", - "description": "Enforce location of semicolons", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "space-before-blocks", - "description": "Enforce consistent spacing before blocks", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "space-before-function-paren", - "description": "Enforce consistent spacing before `function` definition opening parenthesis", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "space-in-parens", - "description": "Enforce consistent spacing inside parentheses", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "space-infix-ops", - "description": "Require spacing around infix operators", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "space-unary-ops", - "description": "Enforce consistent spacing before or after unary operators", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "switch-colon-spacing", - "description": "Enforce spacing around colons of switch statements", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "template-curly-spacing", - "description": "Require or disallow spacing around embedded expressions of template strings", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "template-tag-spacing", - "description": "Require or disallow spacing between template tags and their literals", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "unicode-bom", - "description": "Require or disallow Unicode byte order mark (BOM)", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "wrap-iife", - "description": "Require parentheses around immediate `function` invocations", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "wrap-regex", - "description": "Require parenthesis around regex literals", - "recommended": false, - "fixable": true, - "hasSuggestions": false - }, - { - "name": "yield-star-spacing", - "description": "Require or disallow spacing around the `*` in `yield*` expressions", - "recommended": false, - "fixable": true, - "hasSuggestions": false - } - ] - } - ], - "deprecated": { - "name": "Deprecated", - "description": "These rules have been deprecated in accordance with the deprecation policy, and replaced by newer rules:", - "rules": [ + "types": { + "problem": [ + { + "name": "array-callback-return", + "description": "Enforce `return` statements in callbacks of array methods", + "recommended": false, + "fixable": false, + "hasSuggestions": true + }, + { + "name": "constructor-super", + "description": "Require `super()` calls in constructors", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "for-direction", + "description": "Enforce \"for\" loop update clause moving the counter in the right direction", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "getter-return", + "description": "Enforce `return` statements in getters", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-async-promise-executor", + "description": "Disallow using an async function as a Promise executor", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-await-in-loop", + "description": "Disallow `await` inside of loops", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-class-assign", + "description": "Disallow reassigning class members", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-compare-neg-zero", + "description": "Disallow comparing against -0", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-cond-assign", + "description": "Disallow assignment operators in conditional expressions", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-const-assign", + "description": "Disallow reassigning `const` variables", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-constant-binary-expression", + "description": "Disallow expressions where the operation doesn't affect the value", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-constant-condition", + "description": "Disallow constant expressions in conditions", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-constructor-return", + "description": "Disallow returning value from constructor", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-control-regex", + "description": "Disallow control characters in regular expressions", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-debugger", + "description": "Disallow the use of `debugger`", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-dupe-args", + "description": "Disallow duplicate arguments in `function` definitions", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-dupe-class-members", + "description": "Disallow duplicate class members", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-dupe-else-if", + "description": "Disallow duplicate conditions in if-else-if chains", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-dupe-keys", + "description": "Disallow duplicate keys in object literals", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-duplicate-case", + "description": "Disallow duplicate case labels", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-duplicate-imports", + "description": "Disallow duplicate module imports", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-empty-character-class", + "description": "Disallow empty character classes in regular expressions", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-empty-pattern", + "description": "Disallow empty destructuring patterns", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-ex-assign", + "description": "Disallow reassigning exceptions in `catch` clauses", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-fallthrough", + "description": "Disallow fallthrough of `case` statements", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-func-assign", + "description": "Disallow reassigning `function` declarations", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-import-assign", + "description": "Disallow assigning to imported bindings", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-inner-declarations", + "description": "Disallow variable or `function` declarations in nested blocks", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-invalid-regexp", + "description": "Disallow invalid regular expression strings in `RegExp` constructors", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-irregular-whitespace", + "description": "Disallow irregular whitespace", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-loss-of-precision", + "description": "Disallow literal numbers that lose precision", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-misleading-character-class", + "description": "Disallow characters which are made with multiple code points in character class syntax", + "recommended": true, + "fixable": false, + "hasSuggestions": true + }, + { + "name": "no-new-native-nonconstructor", + "description": "Disallow `new` operators with global non-constructor functions", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-new-symbol", + "description": "Disallow `new` operators with the `Symbol` object", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-obj-calls", + "description": "Disallow calling global object properties as functions", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-promise-executor-return", + "description": "Disallow returning values from Promise executor functions", + "recommended": false, + "fixable": false, + "hasSuggestions": true + }, + { + "name": "no-prototype-builtins", + "description": "Disallow calling some `Object.prototype` methods directly on objects", + "recommended": true, + "fixable": false, + "hasSuggestions": true + }, + { + "name": "no-self-assign", + "description": "Disallow assignments where both sides are exactly the same", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-self-compare", + "description": "Disallow comparisons where both sides are exactly the same", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-setter-return", + "description": "Disallow returning values from setters", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-sparse-arrays", + "description": "Disallow sparse arrays", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-template-curly-in-string", + "description": "Disallow template literal placeholder syntax in regular strings", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-this-before-super", + "description": "Disallow `this`/`super` before calling `super()` in constructors", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-undef", + "description": "Disallow the use of undeclared variables unless mentioned in `/*global */` comments", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-unexpected-multiline", + "description": "Disallow confusing multiline expressions", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-unmodified-loop-condition", + "description": "Disallow unmodified loop conditions", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-unreachable", + "description": "Disallow unreachable code after `return`, `throw`, `continue`, and `break` statements", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-unreachable-loop", + "description": "Disallow loops with a body that allows only one iteration", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-unsafe-finally", + "description": "Disallow control flow statements in `finally` blocks", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-unsafe-negation", + "description": "Disallow negating the left operand of relational operators", + "recommended": true, + "fixable": false, + "hasSuggestions": true + }, + { + "name": "no-unsafe-optional-chaining", + "description": "Disallow use of optional chaining in contexts where the `undefined` value is not allowed", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-unused-private-class-members", + "description": "Disallow unused private class members", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-unused-vars", + "description": "Disallow unused variables", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-use-before-define", + "description": "Disallow the use of variables before they are defined", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-useless-backreference", + "description": "Disallow useless backreferences in regular expressions", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "require-atomic-updates", + "description": "Disallow assignments that can lead to race conditions due to usage of `await` or `yield`", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "use-isnan", + "description": "Require calls to `isNaN()` when checking for `NaN`", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "valid-typeof", + "description": "Enforce comparing `typeof` expressions against valid strings", + "recommended": true, + "fixable": false, + "hasSuggestions": true + } + ], + "suggestion": [ + { + "name": "accessor-pairs", + "description": "Enforce getter and setter pairs in objects and classes", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "arrow-body-style", + "description": "Require braces around arrow function bodies", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "block-scoped-var", + "description": "Enforce the use of variables within the scope they are defined", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "camelcase", + "description": "Enforce camelcase naming convention", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "capitalized-comments", + "description": "Enforce or disallow capitalization of the first letter of a comment", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "class-methods-use-this", + "description": "Enforce that class methods utilize `this`", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "complexity", + "description": "Enforce a maximum cyclomatic complexity allowed in a program", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "consistent-return", + "description": "Require `return` statements to either always or never specify values", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "consistent-this", + "description": "Enforce consistent naming when capturing the current execution context", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "curly", + "description": "Enforce consistent brace style for all control statements", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "default-case", + "description": "Require `default` cases in `switch` statements", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "default-case-last", + "description": "Enforce default clauses in switch statements to be last", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "default-param-last", + "description": "Enforce default parameters to be last", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "dot-notation", + "description": "Enforce dot notation whenever possible", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "eqeqeq", + "description": "Require the use of `===` and `!==`", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "func-name-matching", + "description": "Require function names to match the name of the variable or property to which they are assigned", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "func-names", + "description": "Require or disallow named `function` expressions", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "func-style", + "description": "Enforce the consistent use of either `function` declarations or expressions", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "grouped-accessor-pairs", + "description": "Require grouped accessor pairs in object literals and classes", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "guard-for-in", + "description": "Require `for-in` loops to include an `if` statement", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "id-denylist", + "description": "Disallow specified identifiers", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "id-length", + "description": "Enforce minimum and maximum identifier lengths", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "id-match", + "description": "Require identifiers to match a specified regular expression", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "init-declarations", + "description": "Require or disallow initialization in variable declarations", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "logical-assignment-operators", + "description": "Require or disallow logical assignment operator shorthand", + "recommended": false, + "fixable": true, + "hasSuggestions": true + }, + { + "name": "max-classes-per-file", + "description": "Enforce a maximum number of classes per file", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "max-depth", + "description": "Enforce a maximum depth that blocks can be nested", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "max-lines", + "description": "Enforce a maximum number of lines per file", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "max-lines-per-function", + "description": "Enforce a maximum number of lines of code in a function", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "max-nested-callbacks", + "description": "Enforce a maximum depth that callbacks can be nested", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "max-params", + "description": "Enforce a maximum number of parameters in function definitions", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "max-statements", + "description": "Enforce a maximum number of statements allowed in function blocks", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "multiline-comment-style", + "description": "Enforce a particular style for multiline comments", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "new-cap", + "description": "Require constructor names to begin with a capital letter", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-alert", + "description": "Disallow the use of `alert`, `confirm`, and `prompt`", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-array-constructor", + "description": "Disallow `Array` constructors", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-bitwise", + "description": "Disallow bitwise operators", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-caller", + "description": "Disallow the use of `arguments.caller` or `arguments.callee`", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-case-declarations", + "description": "Disallow lexical declarations in case clauses", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-console", + "description": "Disallow the use of `console`", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-continue", + "description": "Disallow `continue` statements", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-delete-var", + "description": "Disallow deleting variables", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-div-regex", + "description": "Disallow equal signs explicitly at the beginning of regular expressions", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "no-else-return", + "description": "Disallow `else` blocks after `return` statements in `if` statements", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "no-empty", + "description": "Disallow empty block statements", + "recommended": true, + "fixable": false, + "hasSuggestions": true + }, + { + "name": "no-empty-function", + "description": "Disallow empty functions", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-empty-static-block", + "description": "Disallow empty static blocks", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-eq-null", + "description": "Disallow `null` comparisons without type-checking operators", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-eval", + "description": "Disallow the use of `eval()`", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-extend-native", + "description": "Disallow extending native types", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-extra-bind", + "description": "Disallow unnecessary calls to `.bind()`", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "no-extra-boolean-cast", + "description": "Disallow unnecessary boolean casts", + "recommended": true, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "no-extra-label", + "description": "Disallow unnecessary labels", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "no-global-assign", + "description": "Disallow assignments to native objects or read-only global variables", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-implicit-coercion", + "description": "Disallow shorthand type conversions", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "no-implicit-globals", + "description": "Disallow declarations in the global scope", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-implied-eval", + "description": "Disallow the use of `eval()`-like methods", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-inline-comments", + "description": "Disallow inline comments after code", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-invalid-this", + "description": "Disallow use of `this` in contexts where the value of `this` is `undefined`", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-iterator", + "description": "Disallow the use of the `__iterator__` property", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-label-var", + "description": "Disallow labels that share a name with a variable", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-labels", + "description": "Disallow labeled statements", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-lone-blocks", + "description": "Disallow unnecessary nested blocks", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-lonely-if", + "description": "Disallow `if` statements as the only statement in `else` blocks", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "no-loop-func", + "description": "Disallow function declarations that contain unsafe references inside loop statements", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-magic-numbers", + "description": "Disallow magic numbers", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-multi-assign", + "description": "Disallow use of chained assignment expressions", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-multi-str", + "description": "Disallow multiline strings", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-negated-condition", + "description": "Disallow negated conditions", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-nested-ternary", + "description": "Disallow nested ternary expressions", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-new", + "description": "Disallow `new` operators outside of assignments or comparisons", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-new-func", + "description": "Disallow `new` operators with the `Function` object", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-new-wrappers", + "description": "Disallow `new` operators with the `String`, `Number`, and `Boolean` objects", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-nonoctal-decimal-escape", + "description": "Disallow `\\8` and `\\9` escape sequences in string literals", + "recommended": true, + "fixable": false, + "hasSuggestions": true + }, + { + "name": "no-object-constructor", + "description": "Disallow calls to the `Object` constructor without an argument", + "recommended": false, + "fixable": false, + "hasSuggestions": true + }, + { + "name": "no-octal", + "description": "Disallow octal literals", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-octal-escape", + "description": "Disallow octal escape sequences in string literals", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-param-reassign", + "description": "Disallow reassigning `function` parameters", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-plusplus", + "description": "Disallow the unary operators `++` and `--`", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-proto", + "description": "Disallow the use of the `__proto__` property", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-redeclare", + "description": "Disallow variable redeclaration", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-regex-spaces", + "description": "Disallow multiple spaces in regular expressions", + "recommended": true, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "no-restricted-exports", + "description": "Disallow specified names in exports", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-restricted-globals", + "description": "Disallow specified global variables", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-restricted-imports", + "description": "Disallow specified modules when loaded by `import`", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-restricted-properties", + "description": "Disallow certain properties on certain objects", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-restricted-syntax", + "description": "Disallow specified syntax", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-return-assign", + "description": "Disallow assignment operators in `return` statements", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-script-url", + "description": "Disallow `javascript:` urls", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-sequences", + "description": "Disallow comma operators", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-shadow", + "description": "Disallow variable declarations from shadowing variables declared in the outer scope", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-shadow-restricted-names", + "description": "Disallow identifiers from shadowing restricted names", + "recommended": true, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-ternary", + "description": "Disallow ternary operators", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-throw-literal", + "description": "Disallow throwing literals as exceptions", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-undef-init", + "description": "Disallow initializing variables to `undefined`", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "no-undefined", + "description": "Disallow the use of `undefined` as an identifier", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-underscore-dangle", + "description": "Disallow dangling underscores in identifiers", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, + { + "name": "no-unneeded-ternary", + "description": "Disallow ternary operators when simpler alternatives exist", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, + { + "name": "no-unused-expressions", + "description": "Disallow unused expressions", + "recommended": false, + "fixable": false, + "hasSuggestions": false + }, { - "name": "callback-return", - "replacedBy": [] + "name": "no-unused-labels", + "description": "Disallow unused labels", + "recommended": true, + "fixable": true, + "hasSuggestions": false }, { - "name": "global-require", - "replacedBy": [] + "name": "no-useless-call", + "description": "Disallow unnecessary calls to `.call()` and `.apply()`", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "name": "handle-callback-err", - "replacedBy": [] + "name": "no-useless-catch", + "description": "Disallow unnecessary `catch` clauses", + "recommended": true, + "fixable": false, + "hasSuggestions": false }, { - "name": "id-blacklist", - "replacedBy": [ - "id-denylist" - ] + "name": "no-useless-computed-key", + "description": "Disallow unnecessary computed property keys in objects and classes", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "name": "indent-legacy", - "replacedBy": [ - "indent" - ] + "name": "no-useless-concat", + "description": "Disallow unnecessary concatenation of literals or template literals", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "name": "lines-around-directive", - "replacedBy": [ - "padding-line-between-statements" - ] + "name": "no-useless-constructor", + "description": "Disallow unnecessary constructors", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "name": "newline-after-var", - "replacedBy": [ - "padding-line-between-statements" - ] + "name": "no-useless-escape", + "description": "Disallow unnecessary escape characters", + "recommended": true, + "fixable": false, + "hasSuggestions": true }, { - "name": "newline-before-return", - "replacedBy": [ - "padding-line-between-statements" - ] + "name": "no-useless-rename", + "description": "Disallow renaming import, export, and destructured assignments to the same name", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "name": "no-buffer-constructor", - "replacedBy": [] + "name": "no-useless-return", + "description": "Disallow redundant return statements", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "name": "no-catch-shadow", - "replacedBy": [ - "no-shadow" - ] + "name": "no-var", + "description": "Require `let` or `const` instead of `var`", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "name": "no-mixed-requires", - "replacedBy": [] + "name": "no-void", + "description": "Disallow `void` operators", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "name": "no-native-reassign", - "replacedBy": [ - "no-global-assign" - ] + "name": "no-warning-comments", + "description": "Disallow specified warning terms in comments", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "name": "no-negated-in-lhs", - "replacedBy": [ - "no-unsafe-negation" - ] + "name": "no-with", + "description": "Disallow `with` statements", + "recommended": true, + "fixable": false, + "hasSuggestions": false }, { - "name": "no-new-require", - "replacedBy": [] + "name": "object-shorthand", + "description": "Require or disallow method and property shorthand syntax for object literals", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "name": "no-path-concat", - "replacedBy": [] + "name": "one-var", + "description": "Enforce variables to be declared either together or separately in functions", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "name": "no-process-env", - "replacedBy": [] + "name": "operator-assignment", + "description": "Require or disallow assignment operator shorthand where possible", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "name": "no-process-exit", - "replacedBy": [] + "name": "prefer-arrow-callback", + "description": "Require using arrow functions for callbacks", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "name": "no-restricted-modules", - "replacedBy": [] + "name": "prefer-const", + "description": "Require `const` declarations for variables that are never reassigned after declared", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "name": "no-spaced-func", - "replacedBy": [ - "func-call-spacing" - ] + "name": "prefer-destructuring", + "description": "Require destructuring from arrays and/or objects", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "name": "no-sync", - "replacedBy": [] + "name": "prefer-exponentiation-operator", + "description": "Disallow the use of `Math.pow` in favor of the `**` operator", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "name": "prefer-reflect", - "replacedBy": [] + "name": "prefer-named-capture-group", + "description": "Enforce using named capture group in regular expression", + "recommended": false, + "fixable": false, + "hasSuggestions": true }, { - "name": "require-jsdoc", - "replacedBy": [] + "name": "prefer-numeric-literals", + "description": "Disallow `parseInt()` and `Number.parseInt()` in favor of binary, octal, and hexadecimal literals", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "name": "valid-jsdoc", - "replacedBy": [] - } - ] - }, - "removed": { - "name": "Removed", - "description": "These rules from older versions of ESLint (before the deprecation policy existed) have been replaced by newer rules:", - "rules": [ + "name": "prefer-object-has-own", + "description": "Disallow use of `Object.prototype.hasOwnProperty.call()` and prefer use of `Object.hasOwn()`", + "recommended": false, + "fixable": true, + "hasSuggestions": false + }, { - "removed": "generator-star", - "replacedBy": [ - "generator-star-spacing" - ] + "name": "prefer-object-spread", + "description": "Disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "removed": "global-strict", - "replacedBy": [ - "strict" - ] + "name": "prefer-promise-reject-errors", + "description": "Require using Error objects as Promise rejection reasons", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "removed": "no-arrow-condition", - "replacedBy": [ - "no-confusing-arrow", - "no-constant-condition" - ] + "name": "prefer-regex-literals", + "description": "Disallow use of the `RegExp` constructor in favor of regular expression literals", + "recommended": false, + "fixable": false, + "hasSuggestions": true }, { - "removed": "no-comma-dangle", - "replacedBy": [ - "comma-dangle" - ] + "name": "prefer-rest-params", + "description": "Require rest parameters instead of `arguments`", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "removed": "no-empty-class", - "replacedBy": [ - "no-empty-character-class" - ] + "name": "prefer-spread", + "description": "Require spread operators instead of `.apply()`", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "removed": "no-empty-label", - "replacedBy": [ - "no-labels" - ] + "name": "prefer-template", + "description": "Require template literals instead of string concatenation", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "removed": "no-extra-strict", - "replacedBy": [ - "strict" - ] + "name": "radix", + "description": "Enforce the consistent use of the radix argument when using `parseInt()`", + "recommended": false, + "fixable": false, + "hasSuggestions": true }, { - "removed": "no-reserved-keys", - "replacedBy": [ - "quote-props" - ] + "name": "require-await", + "description": "Disallow async functions which have no `await` expression", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "removed": "no-space-before-semi", - "replacedBy": [ - "semi-spacing" - ] + "name": "require-unicode-regexp", + "description": "Enforce the use of `u` or `v` flag on RegExp", + "recommended": false, + "fixable": false, + "hasSuggestions": true }, { - "removed": "no-wrap-func", - "replacedBy": [ - "no-extra-parens" - ] + "name": "require-yield", + "description": "Require generator functions to contain `yield`", + "recommended": true, + "fixable": false, + "hasSuggestions": false }, { - "removed": "space-after-function-name", - "replacedBy": [ - "space-before-function-paren" - ] + "name": "sort-imports", + "description": "Enforce sorted import declarations within modules", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "removed": "space-after-keywords", - "replacedBy": [ - "keyword-spacing" - ] + "name": "sort-keys", + "description": "Require object keys to be sorted", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "removed": "space-before-function-parentheses", - "replacedBy": [ - "space-before-function-paren" - ] + "name": "sort-vars", + "description": "Require variables within the same declaration block to be sorted", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "removed": "space-before-keywords", - "replacedBy": [ - "keyword-spacing" - ] + "name": "strict", + "description": "Require or disallow strict mode directives", + "recommended": false, + "fixable": true, + "hasSuggestions": false }, { - "removed": "space-in-brackets", - "replacedBy": [ - "object-curly-spacing", - "array-bracket-spacing" - ] + "name": "symbol-description", + "description": "Require symbol descriptions", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "removed": "space-return-throw-case", - "replacedBy": [ - "keyword-spacing" - ] + "name": "vars-on-top", + "description": "Require `var` declarations be placed at the top of their containing scope", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "removed": "space-unary-word-ops", - "replacedBy": [ - "space-unary-ops" - ] + "name": "yoda", + "description": "Require or disallow \"Yoda\" conditions", + "recommended": false, + "fixable": true, + "hasSuggestions": false + } + ], + "layout": [ + { + "name": "line-comment-position", + "description": "Enforce position of line comments", + "recommended": false, + "fixable": false, + "hasSuggestions": false }, { - "removed": "spaced-line-comment", - "replacedBy": [ - "spaced-comment" - ] + "name": "unicode-bom", + "description": "Require or disallow Unicode byte order mark (BOM)", + "recommended": false, + "fixable": true, + "hasSuggestions": false } ] - } + }, + "deprecated": [ + { + "name": "array-bracket-newline", + "replacedBy": [] + }, + { + "name": "array-bracket-spacing", + "replacedBy": [] + }, + { + "name": "array-element-newline", + "replacedBy": [] + }, + { + "name": "arrow-parens", + "replacedBy": [] + }, + { + "name": "arrow-spacing", + "replacedBy": [] + }, + { + "name": "block-spacing", + "replacedBy": [] + }, + { + "name": "brace-style", + "replacedBy": [] + }, + { + "name": "callback-return", + "replacedBy": [] + }, + { + "name": "comma-dangle", + "replacedBy": [] + }, + { + "name": "comma-spacing", + "replacedBy": [] + }, + { + "name": "comma-style", + "replacedBy": [] + }, + { + "name": "computed-property-spacing", + "replacedBy": [] + }, + { + "name": "dot-location", + "replacedBy": [] + }, + { + "name": "eol-last", + "replacedBy": [] + }, + { + "name": "func-call-spacing", + "replacedBy": [] + }, + { + "name": "function-call-argument-newline", + "replacedBy": [] + }, + { + "name": "function-paren-newline", + "replacedBy": [] + }, + { + "name": "generator-star-spacing", + "replacedBy": [] + }, + { + "name": "global-require", + "replacedBy": [] + }, + { + "name": "handle-callback-err", + "replacedBy": [] + }, + { + "name": "id-blacklist", + "replacedBy": [ + "id-denylist" + ] + }, + { + "name": "implicit-arrow-linebreak", + "replacedBy": [] + }, + { + "name": "indent", + "replacedBy": [] + }, + { + "name": "indent-legacy", + "replacedBy": [ + "indent" + ] + }, + { + "name": "jsx-quotes", + "replacedBy": [] + }, + { + "name": "key-spacing", + "replacedBy": [] + }, + { + "name": "keyword-spacing", + "replacedBy": [] + }, + { + "name": "linebreak-style", + "replacedBy": [] + }, + { + "name": "lines-around-comment", + "replacedBy": [] + }, + { + "name": "lines-around-directive", + "replacedBy": [ + "padding-line-between-statements" + ] + }, + { + "name": "lines-between-class-members", + "replacedBy": [] + }, + { + "name": "max-len", + "replacedBy": [] + }, + { + "name": "max-statements-per-line", + "replacedBy": [] + }, + { + "name": "multiline-ternary", + "replacedBy": [] + }, + { + "name": "new-parens", + "replacedBy": [] + }, + { + "name": "newline-after-var", + "replacedBy": [ + "padding-line-between-statements" + ] + }, + { + "name": "newline-before-return", + "replacedBy": [ + "padding-line-between-statements" + ] + }, + { + "name": "newline-per-chained-call", + "replacedBy": [] + }, + { + "name": "no-buffer-constructor", + "replacedBy": [] + }, + { + "name": "no-catch-shadow", + "replacedBy": [ + "no-shadow" + ] + }, + { + "name": "no-confusing-arrow", + "replacedBy": [] + }, + { + "name": "no-extra-parens", + "replacedBy": [] + }, + { + "name": "no-extra-semi", + "replacedBy": [] + }, + { + "name": "no-floating-decimal", + "replacedBy": [] + }, + { + "name": "no-mixed-operators", + "replacedBy": [] + }, + { + "name": "no-mixed-requires", + "replacedBy": [] + }, + { + "name": "no-mixed-spaces-and-tabs", + "replacedBy": [] + }, + { + "name": "no-multi-spaces", + "replacedBy": [] + }, + { + "name": "no-multiple-empty-lines", + "replacedBy": [] + }, + { + "name": "no-native-reassign", + "replacedBy": [ + "no-global-assign" + ] + }, + { + "name": "no-negated-in-lhs", + "replacedBy": [ + "no-unsafe-negation" + ] + }, + { + "name": "no-new-object", + "replacedBy": [ + "no-object-constructor" + ] + }, + { + "name": "no-new-require", + "replacedBy": [] + }, + { + "name": "no-path-concat", + "replacedBy": [] + }, + { + "name": "no-process-env", + "replacedBy": [] + }, + { + "name": "no-process-exit", + "replacedBy": [] + }, + { + "name": "no-restricted-modules", + "replacedBy": [] + }, + { + "name": "no-return-await", + "replacedBy": [] + }, + { + "name": "no-spaced-func", + "replacedBy": [ + "func-call-spacing" + ] + }, + { + "name": "no-sync", + "replacedBy": [] + }, + { + "name": "no-tabs", + "replacedBy": [] + }, + { + "name": "no-trailing-spaces", + "replacedBy": [] + }, + { + "name": "no-whitespace-before-property", + "replacedBy": [] + }, + { + "name": "nonblock-statement-body-position", + "replacedBy": [] + }, + { + "name": "object-curly-newline", + "replacedBy": [] + }, + { + "name": "object-curly-spacing", + "replacedBy": [] + }, + { + "name": "object-property-newline", + "replacedBy": [] + }, + { + "name": "one-var-declaration-per-line", + "replacedBy": [] + }, + { + "name": "operator-linebreak", + "replacedBy": [] + }, + { + "name": "padded-blocks", + "replacedBy": [] + }, + { + "name": "padding-line-between-statements", + "replacedBy": [] + }, + { + "name": "prefer-reflect", + "replacedBy": [] + }, + { + "name": "quote-props", + "replacedBy": [] + }, + { + "name": "quotes", + "replacedBy": [] + }, + { + "name": "require-jsdoc", + "replacedBy": [] + }, + { + "name": "rest-spread-spacing", + "replacedBy": [] + }, + { + "name": "semi", + "replacedBy": [] + }, + { + "name": "semi-spacing", + "replacedBy": [] + }, + { + "name": "semi-style", + "replacedBy": [] + }, + { + "name": "space-before-blocks", + "replacedBy": [] + }, + { + "name": "space-before-function-paren", + "replacedBy": [] + }, + { + "name": "space-in-parens", + "replacedBy": [] + }, + { + "name": "space-infix-ops", + "replacedBy": [] + }, + { + "name": "space-unary-ops", + "replacedBy": [] + }, + { + "name": "spaced-comment", + "replacedBy": [] + }, + { + "name": "switch-colon-spacing", + "replacedBy": [] + }, + { + "name": "template-curly-spacing", + "replacedBy": [] + }, + { + "name": "template-tag-spacing", + "replacedBy": [] + }, + { + "name": "valid-jsdoc", + "replacedBy": [] + }, + { + "name": "wrap-iife", + "replacedBy": [] + }, + { + "name": "wrap-regex", + "replacedBy": [] + }, + { + "name": "yield-star-spacing", + "replacedBy": [] + } + ], + "removed": [ + { + "removed": "generator-star", + "replacedBy": [ + "generator-star-spacing" + ] + }, + { + "removed": "global-strict", + "replacedBy": [ + "strict" + ] + }, + { + "removed": "no-arrow-condition", + "replacedBy": [ + "no-confusing-arrow", + "no-constant-condition" + ] + }, + { + "removed": "no-comma-dangle", + "replacedBy": [ + "comma-dangle" + ] + }, + { + "removed": "no-empty-class", + "replacedBy": [ + "no-empty-character-class" + ] + }, + { + "removed": "no-empty-label", + "replacedBy": [ + "no-labels" + ] + }, + { + "removed": "no-extra-strict", + "replacedBy": [ + "strict" + ] + }, + { + "removed": "no-reserved-keys", + "replacedBy": [ + "quote-props" + ] + }, + { + "removed": "no-space-before-semi", + "replacedBy": [ + "semi-spacing" + ] + }, + { + "removed": "no-wrap-func", + "replacedBy": [ + "no-extra-parens" + ] + }, + { + "removed": "space-after-function-name", + "replacedBy": [ + "space-before-function-paren" + ] + }, + { + "removed": "space-after-keywords", + "replacedBy": [ + "keyword-spacing" + ] + }, + { + "removed": "space-before-function-parentheses", + "replacedBy": [ + "space-before-function-paren" + ] + }, + { + "removed": "space-before-keywords", + "replacedBy": [ + "keyword-spacing" + ] + }, + { + "removed": "space-in-brackets", + "replacedBy": [ + "object-curly-spacing", + "array-bracket-spacing" + ] + }, + { + "removed": "space-return-throw-case", + "replacedBy": [ + "keyword-spacing" + ] + }, + { + "removed": "space-unary-word-ops", + "replacedBy": [ + "space-unary-ops" + ] + }, + { + "removed": "spaced-line-comment", + "replacedBy": [ + "spaced-comment" + ] + } + ] } \ No newline at end of file diff --git a/docs/src/_data/rules_categories.js b/docs/src/_data/rules_categories.js new file mode 100644 index 00000000000..46856958f22 --- /dev/null +++ b/docs/src/_data/rules_categories.js @@ -0,0 +1,26 @@ +module.exports = eleventy => { + const PATH_PREFIX = eleventy.PATH_PREFIX; + + return { + problem: { + displayName: "Possible Problems", + description: "These rules relate to possible logic errors in code:" + }, + suggestion: { + displayName: "Suggestions", + description: "These rules suggest alternate ways of doing things:" + }, + layout: { + displayName: "Layout & Formatting", + description: "These rules care about how the code looks rather than how it executes:" + }, + deprecated: { + displayName: "Deprecated", + description: `These rules have been deprecated in accordance with the deprecation policy, and replaced by newer rules:` + }, + removed: { + displayName: "Removed", + description: `These rules from older versions of ESLint (before the deprecation policy existed) have been replaced by newer rules:` + } + } +}; diff --git a/docs/src/_data/rules_meta.json b/docs/src/_data/rules_meta.json index 6d9fb3d5a22..a00b86c7066 100644 --- a/docs/src/_data/rules_meta.json +++ b/docs/src/_data/rules_meta.json @@ -8,6 +8,8 @@ } }, "array-bracket-newline": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce linebreaks after opening and before closing array brackets", @@ -17,6 +19,8 @@ "fixable": "whitespace" }, "array-bracket-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing inside array brackets", @@ -31,9 +35,12 @@ "description": "Enforce `return` statements in callbacks of array methods", "recommended": false, "url": "https://eslint.org/docs/latest/rules/array-callback-return" - } + }, + "hasSuggestions": true }, "array-element-newline": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce line breaks after each array element", @@ -52,6 +59,8 @@ "fixable": "code" }, "arrow-parens": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require parentheses around arrow function arguments", @@ -61,6 +70,8 @@ "fixable": "code" }, "arrow-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing before and after the arrow in arrow functions", @@ -78,6 +89,8 @@ } }, "block-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Disallow or enforce spaces inside of blocks after opening block and before closing block", @@ -87,6 +100,8 @@ "fixable": "whitespace" }, "brace-style": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent brace style for blocks", @@ -131,6 +146,8 @@ } }, "comma-dangle": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require or disallow trailing commas", @@ -140,6 +157,8 @@ "fixable": "code" }, "comma-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing before and after commas", @@ -149,6 +168,8 @@ "fixable": "whitespace" }, "comma-style": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent comma style", @@ -166,6 +187,8 @@ } }, "computed-property-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing inside computed property brackets", @@ -232,6 +255,8 @@ } }, "dot-location": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent newlines before and after dots", @@ -250,6 +275,8 @@ "fixable": "code" }, "eol-last": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require or disallow newline at the end of files", @@ -277,6 +304,8 @@ "fixable": null }, "func-call-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require or disallow spacing between function identifiers and their invocations", @@ -310,6 +339,8 @@ } }, "function-call-argument-newline": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce line breaks between arguments of a function call", @@ -319,6 +350,8 @@ "fixable": "whitespace" }, "function-paren-newline": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent line breaks inside function parentheses", @@ -328,6 +361,8 @@ "fixable": "whitespace" }, "generator-star-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing around `*` operators in generator functions", @@ -418,6 +453,8 @@ } }, "implicit-arrow-linebreak": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce the location of arrow function bodies", @@ -427,6 +464,8 @@ "fixable": "whitespace" }, "indent": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent indentation", @@ -457,6 +496,8 @@ } }, "jsx-quotes": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce the consistent use of either double or single quotes in JSX attributes", @@ -466,6 +507,8 @@ "fixable": "whitespace" }, "key-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing between keys and values in object literal properties", @@ -475,6 +518,8 @@ "fixable": "whitespace" }, "keyword-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing before and after keywords", @@ -492,6 +537,8 @@ } }, "linebreak-style": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent linebreak style", @@ -501,6 +548,8 @@ "fixable": "whitespace" }, "lines-around-comment": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require empty lines around comments", @@ -523,6 +572,8 @@ ] }, "lines-between-class-members": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require or disallow an empty line between class members", @@ -558,6 +609,8 @@ } }, "max-len": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce a maximum line length", @@ -606,6 +659,8 @@ } }, "max-statements-per-line": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce a maximum number of statements allowed per line", @@ -623,6 +678,8 @@ "fixable": "whitespace" }, "multiline-ternary": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce newlines between operands of ternary expressions", @@ -640,6 +697,8 @@ } }, "new-parens": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce or disallow parentheses when invoking a constructor with no arguments", @@ -675,6 +734,8 @@ ] }, "newline-per-chained-call": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require a newline after each call in a method chain", @@ -788,6 +849,8 @@ } }, "no-confusing-arrow": { + "deprecated": true, + "replacedBy": [], "type": "suggestion", "docs": { "description": "Disallow arrow functions where they could be confused with comparisons", @@ -1037,6 +1100,8 @@ "fixable": "code" }, "no-extra-parens": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Disallow unnecessary parentheses", @@ -1046,6 +1111,8 @@ "fixable": "code" }, "no-extra-semi": { + "deprecated": true, + "replacedBy": [], "type": "suggestion", "docs": { "description": "Disallow unnecessary semicolons", @@ -1063,6 +1130,8 @@ } }, "no-floating-decimal": { + "deprecated": true, + "replacedBy": [], "type": "suggestion", "docs": { "description": "Disallow leading or trailing decimal points in numeric literals", @@ -1235,6 +1304,8 @@ "hasSuggestions": true }, "no-mixed-operators": { + "deprecated": true, + "replacedBy": [], "type": "suggestion", "docs": { "description": "Disallow mixed binary operators", @@ -1253,6 +1324,8 @@ } }, "no-mixed-spaces-and-tabs": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Disallow mixed spaces and tabs for indentation", @@ -1269,6 +1342,8 @@ } }, "no-multi-spaces": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Disallow multiple spaces", @@ -1286,6 +1361,8 @@ } }, "no-multiple-empty-lines": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Disallow multiple empty lines", @@ -1364,7 +1441,11 @@ "description": "Disallow `Object` constructors", "recommended": false, "url": "https://eslint.org/docs/latest/rules/no-new-object" - } + }, + "deprecated": true, + "replacedBy": [ + "no-object-constructor" + ] }, "no-new-require": { "deprecated": true, @@ -1409,6 +1490,15 @@ "url": "https://eslint.org/docs/latest/rules/no-obj-calls" } }, + "no-object-constructor": { + "type": "suggestion", + "docs": { + "description": "Disallow calls to the `Object` constructor without an argument", + "recommended": false, + "url": "https://eslint.org/docs/latest/rules/no-object-constructor" + }, + "hasSuggestions": true + }, "no-octal": { "type": "suggestion", "docs": { @@ -1477,7 +1567,8 @@ "description": "Disallow returning values from Promise executor functions", "recommended": false, "url": "https://eslint.org/docs/latest/rules/no-promise-executor-return" - } + }, + "hasSuggestions": true }, "no-proto": { "type": "suggestion", @@ -1493,7 +1584,8 @@ "description": "Disallow calling some `Object.prototype` methods directly on objects", "recommended": true, "url": "https://eslint.org/docs/latest/rules/no-prototype-builtins" - } + }, + "hasSuggestions": true }, "no-redeclare": { "type": "suggestion", @@ -1578,7 +1670,9 @@ "recommended": false, "url": "https://eslint.org/docs/latest/rules/no-return-await" }, - "fixable": null + "fixable": null, + "deprecated": true, + "replacedBy": [] }, "no-script-url": { "type": "suggestion", @@ -1668,6 +1762,8 @@ } }, "no-tabs": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Disallow all tabs", @@ -1708,6 +1804,8 @@ } }, "no-trailing-spaces": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Disallow trailing whitespace at the end of lines", @@ -1960,6 +2058,8 @@ } }, "no-whitespace-before-property": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Disallow whitespace before properties", @@ -1977,6 +2077,8 @@ } }, "nonblock-statement-body-position": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce the location of single-line statements", @@ -1986,6 +2088,8 @@ "fixable": "whitespace" }, "object-curly-newline": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent line breaks after opening and before closing braces", @@ -1995,6 +2099,8 @@ "fixable": "whitespace" }, "object-curly-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing inside braces", @@ -2004,6 +2110,8 @@ "fixable": "whitespace" }, "object-property-newline": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce placing object properties on separate lines", @@ -2031,6 +2139,8 @@ "fixable": "code" }, "one-var-declaration-per-line": { + "deprecated": true, + "replacedBy": [], "type": "suggestion", "docs": { "description": "Require or disallow newlines around variable declarations", @@ -2049,6 +2159,8 @@ "fixable": "code" }, "operator-linebreak": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent linebreak style for operators", @@ -2058,6 +2170,8 @@ "fixable": "code" }, "padded-blocks": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require or disallow padding within blocks", @@ -2067,6 +2181,8 @@ "fixable": "whitespace" }, "padding-line-between-statements": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require or disallow padding lines between statements", @@ -2202,6 +2318,8 @@ "fixable": "code" }, "quote-props": { + "deprecated": true, + "replacedBy": [], "type": "suggestion", "docs": { "description": "Require quotes around object literal property names", @@ -2211,6 +2329,8 @@ "fixable": "code" }, "quotes": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce the consistent use of either backticks, double, or single quotes", @@ -2258,7 +2378,7 @@ "require-unicode-regexp": { "type": "suggestion", "docs": { - "description": "Enforce the use of `u` flag on RegExp", + "description": "Enforce the use of `u` or `v` flag on RegExp", "recommended": false, "url": "https://eslint.org/docs/latest/rules/require-unicode-regexp" }, @@ -2273,6 +2393,8 @@ } }, "rest-spread-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce spacing between rest and spread operators and their expressions", @@ -2282,6 +2404,8 @@ "fixable": "whitespace" }, "semi": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require or disallow semicolons instead of ASI", @@ -2291,6 +2415,8 @@ "fixable": "code" }, "semi-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing before and after semicolons", @@ -2300,6 +2426,8 @@ "fixable": "whitespace" }, "semi-style": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce location of semicolons", @@ -2335,6 +2463,8 @@ "fixable": "code" }, "space-before-blocks": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing before blocks", @@ -2344,6 +2474,8 @@ "fixable": "whitespace" }, "space-before-function-paren": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing before `function` definition opening parenthesis", @@ -2353,6 +2485,8 @@ "fixable": "whitespace" }, "space-in-parens": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing inside parentheses", @@ -2362,6 +2496,8 @@ "fixable": "whitespace" }, "space-infix-ops": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require spacing around infix operators", @@ -2371,6 +2507,8 @@ "fixable": "whitespace" }, "space-unary-ops": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce consistent spacing before or after unary operators", @@ -2380,6 +2518,8 @@ "fixable": "whitespace" }, "spaced-comment": { + "deprecated": true, + "replacedBy": [], "type": "suggestion", "docs": { "description": "Enforce consistent spacing after the `//` or `/*` in a comment", @@ -2398,6 +2538,8 @@ "fixable": "code" }, "switch-colon-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Enforce spacing around colons of switch statements", @@ -2416,6 +2558,8 @@ "fixable": null }, "template-curly-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require or disallow spacing around embedded expressions of template strings", @@ -2425,6 +2569,8 @@ "fixable": "whitespace" }, "template-tag-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require or disallow spacing between template tags and their literals", @@ -2479,6 +2625,8 @@ } }, "wrap-iife": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require parentheses around immediate `function` invocations", @@ -2488,6 +2636,8 @@ "fixable": "code" }, "wrap-regex": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require parenthesis around regex literals", @@ -2497,6 +2647,8 @@ "fixable": "code" }, "yield-star-spacing": { + "deprecated": true, + "replacedBy": [], "type": "layout", "docs": { "description": "Require or disallow spacing around the `*` in `yield*` expressions", diff --git a/docs/src/_data/sites/en.yml b/docs/src/_data/sites/en.yml index ccd87ad37bd..532630be810 100644 --- a/docs/src/_data/sites/en.yml +++ b/docs/src/_data/sites/en.yml @@ -87,7 +87,7 @@ footer: language: Language latest: Latest copyright: > - © OpenJS Foundation and ESLint contributors, www.openjsf.org. Content licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. + © OpenJS Foundation and ESLint contributors, www.openjsf.org. Content licensed under MIT License. links: open_jsf: The OpenJS Foundation terms: Terms of Use diff --git a/docs/src/_data/sites/zh-hans.yml b/docs/src/_data/sites/zh-hans.yml index efa9474b89d..421401535d0 100644 --- a/docs/src/_data/sites/zh-hans.yml +++ b/docs/src/_data/sites/zh-hans.yml @@ -47,7 +47,7 @@ shared: #------------------------------------------------------------------------------ # Navigation #------------------------------------------------------------------------------ - + navigation: - text: 团队 link: team @@ -85,7 +85,7 @@ footer: language: 语言 latest: 最新 copyright: > - © OpenJS Foundation and ESLint contributors, www.openjsf.org. Content licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. + © OpenJS Foundation and ESLint contributors, www.openjsf.org. Content licensed under MIT License. links: open_jsf: OpenJS 基金会 terms: 使用条款 diff --git a/docs/src/_includes/components/rule-categories.macro.html b/docs/src/_includes/components/rule-categories.macro.html index 193f6def64e..f38d371049e 100644 --- a/docs/src/_includes/components/rule-categories.macro.html +++ b/docs/src/_includes/components/rule-categories.macro.html @@ -21,7 +21,7 @@
💡 hasSuggestions

- Some problems reported by this rule are manually fixable by editor suggestions + Some problems reported by this rule are manually fixable by editor suggestions

{%- endif -%} diff --git a/docs/src/_includes/components/social-icons.html b/docs/src/_includes/components/social-icons.html index bb025af5b13..6f2b887e949 100644 --- a/docs/src/_includes/components/social-icons.html +++ b/docs/src/_includes/components/social-icons.html @@ -3,11 +3,8 @@
@@ -102,6 +118,7 @@

{{ title }}

{% include "partials/docs-footer.html" %} + diff --git a/docs/src/assets/js/main.js b/docs/src/assets/js/main.js index bf3268266f4..80168a136c9 100644 --- a/docs/src/assets/js/main.js +++ b/docs/src/assets/js/main.js @@ -192,24 +192,6 @@ } })(); -// add "Open in Playground" button to code blocks -// (function() { -// let blocks = document.querySelectorAll('pre[class*="language-"]'); -// if (blocks) { -// blocks.forEach(function(block) { -// let button = document.createElement("a"); -// button.classList.add('c-btn--playground'); -// button.classList.add('c-btn'); -// button.classList.add('c-btn--secondary'); -// button.setAttribute("href", "#"); -// button.innerText = "Open in Playground"; -// block.appendChild(button); -// }); -// } -// })(); - - - // add utilities var util = { keyCodes: { diff --git a/docs/src/assets/scss/docs.scss b/docs/src/assets/scss/docs.scss index ee40123891d..a59742475da 100644 --- a/docs/src/assets/scss/docs.scss +++ b/docs/src/assets/scss/docs.scss @@ -112,6 +112,10 @@ div.incorrect { offset-inline-end: -22px; offset-block-start: -22px; } + + pre.line-numbers-mode { + padding-bottom: 4.5rem; + } } div.correct { @@ -142,13 +146,12 @@ pre[class*="language-"] { .c-btn.c-btn--playground { position: absolute; font-size: var(--step--1); - bottom: 0.5rem; - right: 0.5rem; - offset-block-end: 0.5rem; - offset-inline-end: 0.5rem; + bottom: 1rem; + right: 1rem; + z-index: 1; - @media all and (max-width: 768px) { - display: none; + @media all and (min-width: 768px) { + bottom: 1.5rem; } } @@ -163,8 +166,9 @@ pre[class*="language-"] { height: 50px; display: none; position: fixed; - right: 50px; + right: 19.8vw; bottom: 35px; + z-index: 1; font-size: 1.5rem; border-radius: 50%; color: var(--body-background-color); @@ -173,7 +177,35 @@ pre[class*="language-"] { align-items: center; background-color: var(--link-color); - @media (max-width: 800px) { + @media (max-width: 1299px) { + right: 18.99vw; + } + + @media (max-width: 1100px) { + right: 19.4vw; + } + + @media (max-width: 1060px) { + right: 19.9vw; + } + + @media (max-width: 1024px) { + right: 22vw; + } + + @media (max-width: 860px) { + right: 22.2vw; + } + + @media (max-width: 850px) { + right: 22.6vw; + } + + @media (max-width: 820px) { + right: 23.4vw; + } + + @media (max-width: 799px) { right: 35px; } diff --git a/docs/src/contribute/core-rules.md b/docs/src/contribute/core-rules.md index 5610c000e84..93b7647f52b 100644 --- a/docs/src/contribute/core-rules.md +++ b/docs/src/contribute/core-rules.md @@ -4,7 +4,7 @@ eleventyNavigation: key: contribute core rule parent: contribute to eslint title: Contribute to Core Rules - order: 10 + order: 11 --- The ESLint core rules are the rules included in the ESLint package. diff --git a/docs/src/contribute/governance.md b/docs/src/contribute/governance.md index 563840a01dc..3f761fb2a51 100644 --- a/docs/src/contribute/governance.md +++ b/docs/src/contribute/governance.md @@ -4,7 +4,7 @@ eleventyNavigation: key: governance parent: contribute to eslint title: Governance - order: 11 + order: 12 --- diff --git a/docs/src/contribute/index.md b/docs/src/contribute/index.md index 3a20390db3d..3b0a6d8f5c5 100644 --- a/docs/src/contribute/index.md +++ b/docs/src/contribute/index.md @@ -3,7 +3,7 @@ title: Contribute to ESLint eleventyNavigation: key: contribute to eslint title: Contribute to ESLint - order: 3 + order: 4 --- One of the great things about open source projects is that anyone can contribute in any number of meaningful ways. ESLint couldn't exist without the help of the many contributors it's had since the project began, and we want you to feel like you can contribute and make a difference as well. diff --git a/docs/src/contribute/package-json-conventions.md b/docs/src/contribute/package-json-conventions.md index 99afe8b2222..a030dc3f3dc 100644 --- a/docs/src/contribute/package-json-conventions.md +++ b/docs/src/contribute/package-json-conventions.md @@ -1,6 +1,11 @@ --- title: Package.json Conventions edit_link: https://github.com/eslint/eslint/edit/main/docs/src/contribute/package-json-conventions.md +eleventyNavigation: + key: package.json conventions + parent: contribute to eslint + title: Package.json Conventions + order: 8 --- The following applies to the "scripts" section of `package.json` files. @@ -14,7 +19,7 @@ Here is a summary of the proposal in ABNF. ```abnf name = life-cycle / main target? option* ":watch"? life-cycle = "prepare" / "preinstall" / "install" / "postinstall" / "prepublish" / "preprepare" / "prepare" / "postprepare" / "prepack" / "postpack" / "prepublishOnly" -main = "build" / "lint" ":fix"? / "release" / "start" / "test" +main = "build" / "lint" ":fix"? / "release" / "start" / "test" / "fetch" target = ":" word ("-" word)* / extension ("+" extension)* option = ":" word ("-" word)* word = ALPHA + @@ -35,6 +40,12 @@ Scripts that generate a set of files from source code and / or data MUST have na If a package contains any `build:*` scripts, there MAY be a script named `build`. If so, SHOULD produce the same output as running each of the `build` scripts individually. It MUST produce a subset of the output from running those scripts. +### Fetch + +Scripts that generate a set of files from external data or resources MUST have names that begin with `fetch`. + +If a package contains any `fetch:*` scripts, there MAY be a script named `fetch`. If so, it SHOULD produce the same output as running each of the `fetch` scripts individually. It MUST produce a subset of the output from running those scripts. + ### Release Scripts that have public side effects (publishing the web site, committing to Git, etc.) MUST begin with `release`. diff --git a/docs/src/contribute/pull-requests.md b/docs/src/contribute/pull-requests.md index 8854ee2fbb2..44f2378d8dc 100644 --- a/docs/src/contribute/pull-requests.md +++ b/docs/src/contribute/pull-requests.md @@ -4,7 +4,7 @@ eleventyNavigation: key: submit pull request parent: contribute to eslint title: Submit a Pull Request - order: 9 + order: 10 --- If you want to contribute to an ESLint repo, please use a GitHub pull request. This is the fastest way for us to evaluate your code and to merge it into the code base. Please don't file an issue with snippets of code. Doing so means that we need to manually merge the changes in and update any appropriate tests. That decreases the likelihood that your code is going to get included in a timely manner. Please use pull requests. diff --git a/docs/src/contribute/report-security-vulnerability.md b/docs/src/contribute/report-security-vulnerability.md index f68319fd34e..69f31a9a73b 100644 --- a/docs/src/contribute/report-security-vulnerability.md +++ b/docs/src/contribute/report-security-vulnerability.md @@ -4,7 +4,7 @@ eleventyNavigation: key: report security vulnerability parent: contribute to eslint title: Report a Security Vulnerability - order: 11 + order: 13 --- To report a security vulnerability in ESLint, please use our [create an advisory form](https://github.com/eslint/eslint/security/advisories/new) on GitHub. diff --git a/docs/src/contribute/work-on-issue.md b/docs/src/contribute/work-on-issue.md index 6205a37fd7e..d258aa4eb15 100644 --- a/docs/src/contribute/work-on-issue.md +++ b/docs/src/contribute/work-on-issue.md @@ -4,17 +4,17 @@ eleventyNavigation: key: work on issues parent: contribute to eslint title: Work on Issues - order: 8 + order: 9 --- Our public [issues tracker](https://github.com/eslint/eslint/issues) lists all of the things we plan on doing as well as suggestions from the community. Before starting to work on an issue, be sure you read through the rest of this page. ## Issue Labels -We use labels to indicate the status of issues. The most complete documentation on the labels is found in the [Maintain ESLint documentation](../maintain/manage-issues#when-an-issue-is-opened), but most contributors should find the information on this page sufficient. The most important questions that labels can help you, as a contributor, answer are: +We use labels to indicate the status of issues. The most complete documentation on the labels is found in the [Maintain ESLint documentation](../maintain/manage-issues#when-an-issue-or-pull-request-is-opened), but most contributors should find the information on this page sufficient. The most important questions that labels can help you, as a contributor, answer are: 1. Is this issue available for me to work on? If you have little or no experience contributing to ESLint, the [`good first issue`](https://github.com/eslint/eslint/labels/good%20first%20issue) label marks appropriate issues. Otherwise, the [`help wanted`](https://github.com/eslint/eslint/labels/help%20wanted) label is an invitation to work on the issue. If you have more experience, you can try working on other issues labeled [`accepted`](https://github.com/eslint/eslint/labels/accepted). Conversely, issues not yet ready to work on are labeled `triage`, `evaluating`, and/or `needs bikeshedding`, and issues that cannot currently be worked on because of something else, such as a bug in a dependency, are labeled `blocked`. -1. What is this issue about? Labels describing the nature of issues include `bug`, `enhancement`, `feature`, `question`, `rule`, `documentation`, `core`, `build`, `cli`, `infrastructure`, `breaking`, and `chore`. These are documented in [Maintain ESLint](../maintain/manage-issues#types-of-issues). +1. What is this issue about? Labels describing the nature of issues include `bug`, `enhancement`, `feature`, `question`, `rule`, `documentation`, `core`, `build`, `cli`, `infrastructure`, `breaking`, and `chore`. These are documented in [Maintain ESLint](../maintain/manage-issues#types-of-issues-and-pull-requests). 1. What is the priority of this issue? Because we have a lot of issues, we prioritize certain issues above others. The following is the list of priorities, from highest to lowest: 1. **Bugs** - problems with the project are actively affecting users. We want to get these resolved as quickly as possible. diff --git a/docs/src/extend/code-path-analysis.md b/docs/src/extend/code-path-analysis.md index 7344f8647ad..87911957490 100644 --- a/docs/src/extend/code-path-analysis.md +++ b/docs/src/extend/code-path-analysis.md @@ -37,7 +37,7 @@ This has references of both the initial segment and the final segments of a code * `finalSegments` (`CodePathSegment[]`) - The final segments which includes both returned and thrown. * `returnedSegments` (`CodePathSegment[]`) - The final segments which includes only returned. * `thrownSegments` (`CodePathSegment[]`) - The final segments which includes only thrown. -* `currentSegments` (`CodePathSegment[]`) - Segments of the current position. +* `currentSegments` (`CodePathSegment[]`) - **Deprecated.** Segments of the current traversal position. * `upper` (`CodePath|null`) - The code path of the upper function/global scope. * `childCodePaths` (`CodePath[]`) - Code paths of functions this code path contains. @@ -56,77 +56,110 @@ Difference from doubly linked list is what there are forking and merging (the ne ## Events -There are five events related to code paths, and you can define event handlers in rules. +There are seven events related to code paths, and you can define event handlers by adding them alongside node visitors in the object exported from the `create()` method of your rule. ```js -module.exports = function(context) { - return { - /** - * This is called at the start of analyzing a code path. - * In this time, the code path object has only the initial segment. - * - * @param {CodePath} codePath - The new code path. - * @param {ASTNode} node - The current node. - * @returns {void} - */ - "onCodePathStart": function(codePath, node) { - // do something with codePath - }, - - /** - * This is called at the end of analyzing a code path. - * In this time, the code path object is complete. - * - * @param {CodePath} codePath - The completed code path. - * @param {ASTNode} node - The current node. - * @returns {void} - */ - "onCodePathEnd": function(codePath, node) { - // do something with codePath - }, - - /** - * This is called when a code path segment was created. - * It meant the code path is forked or merged. - * In this time, the segment has the previous segments and has been - * judged reachable or not. - * - * @param {CodePathSegment} segment - The new code path segment. - * @param {ASTNode} node - The current node. - * @returns {void} - */ - "onCodePathSegmentStart": function(segment, node) { - // do something with segment - }, - - /** - * This is called when a code path segment was left. - * In this time, the segment does not have the next segments yet. - * - * @param {CodePathSegment} segment - The left code path segment. - * @param {ASTNode} node - The current node. - * @returns {void} - */ - "onCodePathSegmentEnd": function(segment, node) { - // do something with segment - }, - - /** - * This is called when a code path segment was looped. - * Usually segments have each previous segments when created, - * but when looped, a segment is added as a new previous segment into a - * existing segment. - * - * @param {CodePathSegment} fromSegment - A code path segment of source. - * @param {CodePathSegment} toSegment - A code path segment of destination. - * @param {ASTNode} node - The current node. - * @returns {void} - */ - "onCodePathSegmentLoop": function(fromSegment, toSegment, node) { - // do something with segment - } - }; -}; +module.exports = { + meta: { + // ... + }, + create(context) { + + return { + /** + * This is called at the start of analyzing a code path. + * In this time, the code path object has only the initial segment. + * + * @param {CodePath} codePath - The new code path. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathStart(codePath, node) { + // do something with codePath + }, + + /** + * This is called at the end of analyzing a code path. + * In this time, the code path object is complete. + * + * @param {CodePath} codePath - The completed code path. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathEnd(codePath, node) { + // do something with codePath + }, + + /** + * This is called when a reachable code path segment was created. + * It meant the code path is forked or merged. + * In this time, the segment has the previous segments and has been + * judged reachable or not. + * + * @param {CodePathSegment} segment - The new code path segment. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathSegmentStart(segment, node) { + // do something with segment + }, + + /** + * This is called when a reachable code path segment was left. + * In this time, the segment does not have the next segments yet. + * + * @param {CodePathSegment} segment - The left code path segment. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathSegmentEnd(segment, node) { + // do something with segment + }, + + /** + * This is called when an unreachable code path segment was created. + * It meant the code path is forked or merged. + * In this time, the segment has the previous segments and has been + * judged reachable or not. + * + * @param {CodePathSegment} segment - The new code path segment. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onUnreachableCodePathSegmentStart(segment, node) { + // do something with segment + }, + + /** + * This is called when an unreachable code path segment was left. + * In this time, the segment does not have the next segments yet. + * + * @param {CodePathSegment} segment - The left code path segment. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onUnreachableCodePathSegmentEnd(segment, node) { + // do something with segment + }, + + /** + * This is called when a code path segment was looped. + * Usually segments have each previous segments when created, + * but when looped, a segment is added as a new previous segment into a + * existing segment. + * + * @param {CodePathSegment} fromSegment - A code path segment of source. + * @param {CodePathSegment} toSegment - A code path segment of destination. + * @param {ASTNode} node - The current node. + * @returns {void} + */ + onCodePathSegmentLoop(fromSegment, toSegment, node) { + // do something with segment + } + }; + + } +} ``` ### About `onCodePathSegmentLoop` @@ -212,35 +245,134 @@ bar(); ## Usage Examples -### To check whether or not this is reachable +### Track current segment position + +To track the current code path segment position, you can define a rule like this: ```js -function isReachable(segment) { - return segment.reachable; -} +module.exports = { + meta: { + // ... + }, + create(context) { + + // tracks the code path we are currently in + let currentCodePath; + + // tracks the segments we've traversed in the current code path + let currentSegments; + + // tracks all current segments for all open paths + const allCurrentSegments = []; + + return { + + onCodePathStart(codePath) { + currentCodePath = codePath; + allCurrentSegments.push(currentSegments); + currentSegments = new Set(); + }, + + onCodePathEnd(codePath) { + currentCodePath = codePath.upper; + currentSegments = allCurrentSegments.pop(); + }, + + onCodePathSegmentStart(segment) { + currentSegments.add(segment); + }, + + onCodePathSegmentEnd(segment) { + currentSegments.delete(segment); + }, -module.exports = function(context) { - var codePathStack = []; - - return { - // Stores CodePath objects. - "onCodePathStart": function(codePath) { - codePathStack.push(codePath); - }, - "onCodePathEnd": function(codePath) { - codePathStack.pop(); - }, - - // Checks reachable or not. - "ExpressionStatement": function(node) { - var codePath = codePathStack[codePathStack.length - 1]; - - // Checks the current code path segments. - if (!codePath.currentSegments.some(isReachable)) { - context.report({message: "Unreachable!", node: node}); + onUnreachableCodePathSegmentStart(segment) { + currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + currentSegments.delete(segment); } + }; + + } +}; +``` + +In this example, the `currentCodePath` variable is used to access the code path that is currently being traversed and the `currentSegments` variable tracks the segments in that code path that have been traversed to that point. Note that `currentSegments` both starts and ends as an empty set, constantly being updated as the traversal progresses. + +Tracking the current segment position is helpful for analyzing the code path that led to a particular node, as in the next example. + +### Find an unreachable node + +To find an unreachable node, track the current segment position and then use a node visitor to check if any of the segments are reachable. For example, the following looks for any `ExpressionStatement` that is unreachable. + +```js +function areAnySegmentsReachable(segments) { + for (const segment of segments) { + if (segment.reachable) { + return true; } - }; + } + + return false; +} + +module.exports = { + meta: { + // ... + }, + create(context) { + + // tracks the code path we are currently in + let currentCodePath; + + // tracks the segments we've traversed in the current code path + let currentSegments; + + // tracks all current segments for all open paths + const allCurrentSegments = []; + + return { + + onCodePathStart(codePath) { + currentCodePath = codePath; + allCurrentSegments.push(currentSegments); + currentSegments = new Set(); + }, + + onCodePathEnd(codePath) { + currentCodePath = codePath.upper; + currentSegments = allCurrentSegments.pop(); + }, + + onCodePathSegmentStart(segment) { + currentSegments.add(segment); + }, + + onCodePathSegmentEnd(segment) { + currentSegments.delete(segment); + }, + + onUnreachableCodePathSegmentStart(segment) { + currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + currentSegments.delete(segment); + }, + + ExpressionStatement(node) { + + // check all the code path segments that led to this node + if (!areAnySegmentsReachable(currentSegments)) { + context.report({ message: "Unreachable!", node }); + } + } + + }; + + } }; ``` @@ -249,9 +381,9 @@ See Also: [no-fallthrough](https://github.com/eslint/eslint/blob/HEAD/lib/rules/no-fallthrough.js), [consistent-return](https://github.com/eslint/eslint/blob/HEAD/lib/rules/consistent-return.js) -### To check state of a code path +### Check if a function is called in every path -This example is checking whether or not the parameter `cb` is called in every path. +This example checks whether or not the parameter `cb` is called in every path. Instances of `CodePath` and `CodePathSegment` are shared to every rule. So a rule must not modify those instances. Please use a map of information instead. @@ -271,75 +403,101 @@ function isCbCalled(info) { return info.cbCalled; } -module.exports = function(context) { - var funcInfoStack = []; - var segmentInfoMap = Object.create(null); - - return { - // Checks `cb`. - "onCodePathStart": function(codePath, node) { - funcInfoStack.push({ - codePath: codePath, - hasCb: hasCb(node, context) - }); - }, - "onCodePathEnd": function(codePath, node) { - funcInfoStack.pop(); - - // Checks `cb` was called in every paths. - var cbCalled = codePath.finalSegments.every(function(segment) { - var info = segmentInfoMap[segment.id]; - return info.cbCalled; - }); - - if (!cbCalled) { - context.report({ - message: "`cb` should be called in every path.", - node: node +module.exports = { + meta: { + // ... + }, + create(context) { + + let funcInfo; + const funcInfoStack = []; + const segmentInfoMap = Object.create(null); + + return { + // Checks `cb`. + onCodePathStart(codePath, node) { + funcInfoStack.push(funcInfo); + + funcInfo = { + codePath: codePath, + hasCb: hasCb(node, context), + currentSegments: new Set() + }; + }, + + onCodePathEnd(codePath, node) { + funcInfo = funcInfoStack.pop(); + + // Checks `cb` was called in every paths. + const cbCalled = codePath.finalSegments.every(function(segment) { + const info = segmentInfoMap[segment.id]; + return info.cbCalled; }); - } - }, - - // Manages state of code paths. - "onCodePathSegmentStart": function(segment) { - var funcInfo = funcInfoStack[funcInfoStack.length - 1]; - // Ignores if `cb` doesn't exist. - if (!funcInfo.hasCb) { - return; + if (!cbCalled) { + context.report({ + message: "`cb` should be called in every path.", + node: node + }); + } + }, + + // Manages state of code paths and tracks traversed segments + onCodePathSegmentStart(segment) { + + funcInfo.currentSegments.add(segment); + + // Ignores if `cb` doesn't exist. + if (!funcInfo.hasCb) { + return; + } + + // Initialize state of this path. + const info = segmentInfoMap[segment.id] = { + cbCalled: false + }; + + // If there are the previous paths, merges state. + // Checks `cb` was called in every previous path. + if (segment.prevSegments.length > 0) { + info.cbCalled = segment.prevSegments.every(isCbCalled); + } + }, + + // Tracks unreachable segment traversal + onUnreachableCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + }, + + // Tracks reachable segment traversal + onCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + // Tracks unreachable segment traversal + onUnreachableCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + // Checks reachable or not. + CallExpression(node) { + + // Ignores if `cb` doesn't exist. + if (!funcInfo.hasCb) { + return; + } + + // Sets marks that `cb` was called. + const callee = node.callee; + if (callee.type === "Identifier" && callee.name === "cb") { + funcInfo.currentSegments.forEach(segment => { + const info = segmentInfoMap[segment.id]; + info.cbCalled = true; + }); + } } - - // Initialize state of this path. - var info = segmentInfoMap[segment.id] = { - cbCalled: false - }; - - // If there are the previous paths, merges state. - // Checks `cb` was called in every previous path. - if (segment.prevSegments.length > 0) { - info.cbCalled = segment.prevSegments.every(isCbCalled); - } - }, - - // Checks reachable or not. - "CallExpression": function(node) { - var funcInfo = funcInfoStack[funcInfoStack.length - 1]; - - // Ignores if `cb` doesn't exist. - if (!funcInfo.hasCb) { - return; - } - - // Sets marks that `cb` was called. - var callee = node.callee; - if (callee.type === "Identifier" && callee.name === "cb") { - funcInfo.codePath.currentSegments.forEach(function(segment) { - var info = segmentInfoMap[segment.id]; - info.cbCalled = true; - }); - } - } - }; + }; + } }; ``` diff --git a/docs/src/extend/custom-parsers.md b/docs/src/extend/custom-parsers.md index 388f54b726b..55ed726f046 100644 --- a/docs/src/extend/custom-parsers.md +++ b/docs/src/extend/custom-parsers.md @@ -12,6 +12,8 @@ ESLint custom parsers let you extend ESLint to support linting new non-standard ## Creating a Custom Parser +### Methods in Custom Parsers + A custom parser is a JavaScript object with either a `parse` or `parseForESLint` method. The `parse` method only returns the AST, whereas `parseForESLint` also returns additional values that let the parser customize the behavior of ESLint even more. Both methods should take in the source code as the first argument, and an optional configuration object as the second argument, which is provided as [`parserOptions`](../use/configure/language-options#specifying-parser-options) in a configuration file. @@ -33,21 +35,37 @@ function parse(code, options) { module.exports = { parse }; ``` -## `parse` Return Object +### `parse` Return Object The `parse` method should simply return the [AST](#ast-specification) object. -## `parseForESLint` Return Object +### `parseForESLint` Return Object The `parseForESLint` method should return an object that contains the required property `ast` and optional properties `services`, `scopeManager`, and `visitorKeys`. * `ast` should contain the [AST](#ast-specification) object. -* `services` can contain any parser-dependent services (such as type checkers for nodes). The value of the `services` property is available to rules as `context.parserServices`. Default is an empty object. +* `services` can contain any parser-dependent services (such as type checkers for nodes). The value of the `services` property is available to rules as `context.sourceCode.parserServices`. Default is an empty object. * `scopeManager` can be a [ScopeManager](./scope-manager-interface) object. Custom parsers can use customized scope analysis for experimental/enhancement syntaxes. The default is the `ScopeManager` object which is created by [eslint-scope](https://github.com/eslint/eslint-scope). * Support for `scopeManager` was added in ESLint v4.14.0. ESLint versions that support `scopeManager` will provide an `eslintScopeManager: true` property in `parserOptions`, which can be used for feature detection. * `visitorKeys` can be an object to customize AST traversal. The keys of the object are the type of AST nodes. Each value is an array of the property names which should be traversed. The default is [KEYS of `eslint-visitor-keys`](https://github.com/eslint/eslint-visitor-keys#evkkeys). * Support for `visitorKeys` was added in ESLint v4.14.0. ESLint versions that support `visitorKeys` will provide an `eslintVisitorKeys: true` property in `parserOptions`, which can be used for feature detection. +### Meta Data in Custom Parsers + +For easier debugging and more effective caching of custom parsers, it's recommended to provide a name and version in a `meta` object at the root of your custom parsers, like this: + +```js +// preferred location of name and version +module.exports = { + meta: { + name: "eslint-parser-custom", + version: "1.2.3" + } +}; +``` + +The `meta.name` property should match the npm package name for your custom parser and the `meta.version` property should match the npm package version for your custom parser. The easiest way to accomplish this is by reading this information from your `package.json`. + ## AST Specification The AST that custom parsers should create is based on [ESTree](https://github.com/estree/estree). The AST requires some additional properties about detail information of the source code. @@ -120,7 +138,7 @@ To learn more about using ESLint parsers in your project, refer to [Configure a For a complex example of a custom parser, refer to the [`@typescript-eslint/parser`](https://github.com/typescript-eslint/typescript-eslint/tree/main/packages/parser) source code. -A simple custom parser that provides a `context.parserServices.foo()` method to rules. +A simple custom parser that provides a `context.sourceCode.parserServices.foo()` method to rules. ```javascript // awesome-custom-parser.js diff --git a/docs/src/extend/custom-processors.md b/docs/src/extend/custom-processors.md index d112d724688..d5a5261d976 100644 --- a/docs/src/extend/custom-processors.md +++ b/docs/src/extend/custom-processors.md @@ -4,7 +4,7 @@ eleventyNavigation: key: custom processors parent: create plugins title: Custom Processors - order: 2 + order: 3 --- @@ -18,6 +18,10 @@ In order to create a custom processor, the object exported from your module has module.exports = { processors: { "processor-name": { + meta: { + name: "eslint-processor-name", + version: "1.2.3" + }, // takes text of the file and filename preprocess: function(text, filename) { // here, you can strip out any non-JS content @@ -121,6 +125,8 @@ By default, ESLint does not perform autofixes when a custom processor is used, e You can have both rules and custom processors in a single plugin. You can also have multiple processors in one plugin. To support multiple extensions, add each one to the `processors` element and point them to the same object. +**The `meta` object** helps ESLint cache the processor and provide more friendly debug message. The `meta.name` property should match the processor name and the `meta.version` property should match the npm package version for your processors. The easiest way to accomplish this is by reading this information from your `package.json`. + ## Specifying Processor in Config Files To use a processor, add its ID to a `processor` section in the config file. Processor ID is a concatenated string of plugin name and processor name with a slash as a separator. This can also be added to a `overrides` section of the config, to specify which processors should handle which files. @@ -139,6 +145,10 @@ See [Specify a Processor](../use/configure/plugins#specify-a-processor) in the P ## File Extension-named Processor +::: warning +This feature is deprecated and only works in eslintrc-style configuration files. Flat config files do not automatically apply processors; you need to explicitly set the `processor` property. +::: + If a custom processor name starts with `.`, ESLint handles the processor as a **file extension-named processor**. ESLint applies the processor to files with that filename extension automatically. Users don't need to specify the file extension-named processors in their config files. For example: diff --git a/docs/src/extend/custom-rule-tutorial.md b/docs/src/extend/custom-rule-tutorial.md new file mode 100644 index 00000000000..8426961acae --- /dev/null +++ b/docs/src/extend/custom-rule-tutorial.md @@ -0,0 +1,478 @@ +--- +title: Custom Rule Tutorial +eleventyNavigation: + key: custom rule tutorial + parent: create plugins + title: Custom Rule Tutorial + order: 1 +--- +This tutorial covers how to create a custom rule for ESLint and distribute it with a plugin. + +You can create custom rules to validate if your code meets a certain expectation, and determine what to do if it does not meet that expectation. Plugins package custom rules and other configuration, allowing you to easily share and reuse them in different projects. + +To learn more about custom rules and plugins refer to the following documentation: + +* [Custom Rules](custom-rules) +* [Plugins](plugins) + +## Why Create a Custom Rule? + +Create a custom rule if the ESLint [built-in rules](../rules/) and community-published custom rules do not meet your needs. You might create a custom rule to enforce a best practice for your company or project, prevent a particular bug from recurring, or ensure compliance with a style guide. + +Before creating a custom rule that isn't specific to your company or project, it's worth searching the web to see if someone has published a plugin with a custom rule that solves your use case. It's quite possible the rule may already exist. + +## Prerequisites + +Before you begin, make sure you have the following installed in your development environment: + +* [Node.js](https://nodejs.org/en/download/) +* [npm](https://www.npmjs.com/) + +This tutorial also assumes that you have a basic understanding of ESLint and ESLint rules. + +## The Custom Rule + +The custom rule in this tutorial requires that all `const` variables named `foo` are assigned the string literal `"bar"`. The rule is defined in the file `enforce-foo-bar.js`. The rule also suggests replacing any other value assigned to `const foo` with `"bar"`. + +For example, say you had the following `foo.js` file: + +```javascript +// foo.js + +const foo = "baz123"; +``` + +Running ESLint with the rule would flag `"baz123"` as an incorrect value for variable `foo`. If ESLint is running in autofix mode, then ESLint would fix the file to contain the following: + +```javascript +// foo.js + +const foo = "bar"; +``` + +## Step 1: Set up Your Project + +First, create a new project for your custom rule. Create a new directory, initiate a new npm project in it, and create a new file for the custom rule: + +```shell +mkdir eslint-custom-rule-example # create directory +cd eslint-custom-rule-example # enter the directory +npm init -y # init new npm project +touch enforce-foo-bar.js # create file enforce-foo-bar.js +``` + +## Step 2: Stub Out the Rule File + +In the `enforce-foo-bar.js` file, add some scaffolding for the `enforce-foo-bar` custom rule. Also, add a `meta` object with some basic information about the rule. + +```javascript +// enforce-foo-bar.js + +module.exports = { + meta: { + // TODO: add metadata + }, + create(context) { + return { + // TODO: add callback function(s) + }; + } +}; +``` + +## Step 3: Add Rule Metadata + +Before writing the rule, add some metadata to the rule object. ESLint uses this information when running the rule. + +Start by exporting an object with a `meta` property containing the rule's metadata, such as the rule type, documentation, and fixability. In this case, the rule type is "problem," the description is "Enforce that a variable named `foo` can only be assigned a value of 'bar'.", and the rule is fixable by modifying the code. + +```javascript +// enforce-foo-bar.js + +module.exports = { + meta: { + type: "problem", + docs: { + description: "Enforce that a variable named `foo` can only be assigned a value of 'bar'.", + }, + fixable: "code", + schema: [] + }, + create(context) { + return { + // TODO: add callback function(s) + }; + } +}; +``` + +To learn more about rule metadata, refer to [Rule Structure](custom-rules#rule-structure). + +## Step 4: Add Rule Visitor Methods + +Define the rule's `create` function, which accepts a `context` object and returns an object with a property for each syntax node type you want to handle. In this case, you want to handle `VariableDeclarator` nodes. +You can choose any [ESTree node type](https://github.com/estree/estree) or [selector](selectors). + +Inside the `VariableDeclarator` visitor method, check if the node represents a `const` variable declaration, if its name is `foo`, and if it's not assigned to the string `"bar"`. You do this by evaluating the `node` passed to the `VariableDeclaration` method. + +If the `const foo` declaration is assigned a value of `"bar"`, then the rule does nothing. If `const foo` **is not** assigned a value of `"bar"`, then `context.report()` reports an error to ESLint. The error report includes information about the error and how to fix it. + +```javascript +// enforce-foo-bar.js +{% raw %} +module.exports = { + meta: { + type: "problem", + docs: { + description: "Enforce that a variable named `foo` can only be assigned a value of 'bar'." + }, + fixable: "code", + schema: [] + }, + create(context) { + return { + + // Performs action in the function on every variable declarator + VariableDeclarator(node) { + + // Check if a `const` variable declaration + if (node.parent.kind === "const") { + + // Check if variable name is `foo` + if (node.id.type === "Identifier" && node.id.name === "foo") { + + // Check if value of variable is "bar" + if (node.init && node.init.type === "Literal" && node.init.value !== "bar") { + + /* + * Report error to ESLint. Error message uses + * a message placeholder to include the incorrect value + * in the error message. + * Also includes a `fix(fixer)` function that replaces + * any values assigned to `const foo` with "bar". + */ + context.report({ + node, + message: 'Value other than "bar" assigned to `const foo`. Unexpected value: {{ notBar }}.', + data: { + notBar: node.init.value + }, + fix(fixer) { + return fixer.replaceText(node.init, '"bar"'); + } + }); + } + } + } + } + }; + } +}; +{% endraw %} +``` + +## Step 5: Set up Testing + +With the rule written, you can test it to make sure it's working as expected. + +ESLint provides the built-in [`RuleTester`](../integrate/nodejs-api#ruletester) class to test rules. You do not need to use third-party testing libraries to test ESLint rules, but `RuleTester` works seamlessly with tools like Mocha and Jest. + +Next, create the file for the tests, `enforce-foo-bar.test.js`: + +```shell +touch enforce-foo-bar.test.js +``` + +You will use the `eslint` package in the test file. Install it as a development dependency: + +```shell +npm install eslint --save-dev +``` + +And add a test script to your `package.json` file to run the tests: + +```javascript +// package.json +{ + // ...other configuration + "scripts": { + "test": "node enforce-foo-bar.test.js" + }, + // ...other configuration +} +``` + +## Step 6: Write the Test + +To write the test using `RuleTester`, import the class and your custom rule into the `enforce-foo-bar.test.js` file. + +The `RuleTester#run()` method tests the rule against valid and invalid test cases. If the rule fails to pass any of the test scenarios, this method throws an error. +`RuleTester` requires that at least one valid and one invalid test scenario be present. + +```javascript +// enforce-foo-bar.test.js +const {RuleTester} = require("eslint"); +const fooBarRule = require("./enforce-foo-bar"); + +const ruleTester = new RuleTester({ + // Must use at least ecmaVersion 2015 because + // that's when `const` variables were introduced. + parserOptions: { ecmaVersion: 2015 } +}); + +// Throws error if the tests in ruleTester.run() do not pass +ruleTester.run( + "enforce-foo-bar", // rule name + fooBarRule, // rule code + { // checks + // 'valid' checks cases that should pass + valid: [{ + code: "const foo = 'bar';", + }], + // 'invalid' checks cases that should not pass + invalid: [{ + code: "const foo = 'baz';", + output: 'const foo = "bar";', + errors: 1, + }], + } +); + +console.log("All tests passed!"); +``` + +Run the test with the following command: + +```shell +npm test +``` + +If the test passes, you should see the following in your console: + +```shell +All tests passed! +``` + +## Step 7: Bundle the Custom Rule in a Plugin + +Now that you've written the custom rule and validated that it works, you can include it in a plugin. Using a plugin, you can share the rule in an npm package to use in other projects. + +Create the file for the plugin: + +```shell +touch eslint-plugin-example.js +``` + +And now write the plugin code. Plugins are just exported JavaScript objects. To include a rule in a plugin, include it in the plugin's `rules` object, which contains key-value pairs of rule names and their source code. + +To learn more about creating plugins, refer to [Create Plugins](plugins). + +```javascript +// eslint-plugin-example.js + +const fooBarRule = require("./enforce-foo-bar"); +const plugin = { rules: { "enforce-foo-bar": fooBarRule } }; +module.exports = plugin; +``` + +## Step 8: Use the Plugin Locally + +You can use a locally defined plugin to execute the custom rule in your project. To use a local plugin, specify the path to the plugin in the `plugins` property of your ESLint configuration file. + +You might want to use a locally defined plugin in one of the following scenarios: + +* You want to test the plugin before publishing it to npm. +* You want to use a plugin, but do not want to publish it to npm. + +Before you can add the plugin to the project, create an ESLint configuration for your project using a [flat configuration file](../use/configure/configuration-files-new), `eslint.config.js`: + +```shell +touch eslint.config.js +``` + +Then, add the following code to `eslint.config.js`: + +```javascript +// eslint.config.js +"use strict"; + +// Import the ESLint plugin locally +const eslintPluginExample = require("./eslint-plugin-example"); + +module.exports = [ + { + files: ["**/*.js"], + languageOptions: { + sourceType: "commonjs", + ecmaVersion: "latest", + }, + // Using the eslint-plugin-example plugin defined locally + plugins: {"example": eslintPluginExample}, + rules: { + "example/enforce-foo-bar": "error", + }, + } +] +``` + +Before you can test the rule, you must create a file to test the rule on. + +Create a file `example.js`: + +```shell +touch example.js +``` + +Add the following code to `example.js`: + +```javascript +// example.js + +function correctFooBar() { + const foo = "bar"; +} + +function incorrectFoo(){ + const foo = "baz"; // Problem! +} +``` + +Now you're ready to test the custom rule with the locally defined plugin. + +Run ESLint on `example.js`: + +```shell +npx eslint example.js +``` + +This produces the following output in the terminal: + +```text +//eslint-custom-rule-example/example.js + 8:11 error Value other than "bar" assigned to `const foo`. Unexpected value: baz example/enforce-foo-bar + +✖ 1 problem (1 error, 0 warnings) + 1 error and 0 warnings potentially fixable with the `--fix` option. +``` + +## Step 9: Publish the Plugin + +To publish a plugin containing a rule to npm, you need to configure the `package.json`. Add the following in the corresponding fields: + +1. `"name"`: A unique name for the package. No other package on npm can have the same name. +1. `"main"`: The relative path to the plugin file. Following this example, the path is `"eslint-plugin-example.js"`. +1. `"description"`: A description of the package that's viewable on npm. +1. `"peerDependencies"`: Add `"eslint": ">=8.0.0"` as a peer dependency. Any version greater than or equal to that is necessary to use the plugin. Declaring `eslint` as a peer dependency requires that users add the package to the project separately from the plugin. +1. `"keywords"`: Include the standard keywords `["eslint", "eslintplugin", "eslint-plugin"]` to make the package easy to find. You can add any other keywords that might be relevant to your plugin as well. + +A complete annotated example of what a plugin's `package.json` file should look like: + +```javascript +// package.json +{ + // Name npm package. + // Add your own package name. eslint-plugin-example is taken! + "name": "eslint-plugin-example", + "version": "1.0.0", + "description": "ESLint plugin for enforce-foo-bar rule.", + "main": "eslint-plugin-example.js", // plugin entry point + "scripts": { + "test": "node enforce-foo-bar.test.js" + }, + // Add eslint>=8.0.0 as a peer dependency. + "peerDependencies": { + "eslint": ">=8.0.0" + }, + // Add these standard keywords to make plugin easy to find! + "keywords": [ + "eslint", + "eslintplugin", + "eslint-plugin" + ], + "author": "", + "license": "ISC", + "devDependencies": { + "eslint": "^8.36.0" + } +} +``` + +To publish the package, run `npm publish` and follow the CLI prompts. + +You should see the package live on npm! + +## Step 10: Use the Published Custom Rule + +Next, you can use the published plugin. + +Run the following command in your project to download the package: + +```shell +npm install --save-dev eslint-plugin-example # Add your package name here +``` + +Update the `eslint.config.js` to use the packaged version of the plugin: + +```javascript +// eslint.config.js +"use strict"; + +// Import the plugin downloaded from npm +const eslintPluginExample = require("eslint-plugin-example"); + +// ... rest of configuration +``` + +Now you're ready to test the custom rule. + +Run ESLint on the `example.js` file you created in step 8, now with the downloaded plugin: + +```shell +npx eslint example.js +``` + +This produces the following output in the terminal: + +```text +//eslint-custom-rule-example/example.js + 8:11 error Value other than "bar" assigned to `const foo`. Unexpected value: baz example/enforce-foo-bar + +✖ 1 problem (1 error, 0 warnings) + 1 error and 0 warnings potentially fixable with the `--fix` option. +``` + +As you can see in the above message, you can actually fix the issue with the `--fix` flag, correcting the variable assignment to be `"bar"`. + +Run ESLint again with the `--fix` flag: + +```shell +npx eslint example.js --fix +``` + +There is no error output in the terminal when you run this, but you can see the fix applied in `example.js`. You should see the following: + +```javascript +// example.js + +// ... rest of file + +function incorrectFoo(){ + const foo = "bar"; // Fixed! +} +``` + +## Summary + +In this tutorial, you've made a custom rule that requires all `const` variables named `foo` to be assigned the string `"bar"` and suggests replacing any other value assigned to `const foo` with `"bar"`. You've also added the rule to a plugin, and published the plugin on npm. + +Through doing this, you've learned the following practices which you can apply to create other custom rules and plugins: + +1. Creating a custom ESLint rule +1. Testing the custom rule +1. Bundling the rule in a plugin +1. Publishing the plugin +1. Using the rule from the plugin + +## View the Tutorial Code + +You can view the annotated source code for the tutorial [here](https://github.com/eslint/eslint/tree/main/docs/_examples/custom-rule-tutorial-code). diff --git a/docs/src/extend/custom-rules.md b/docs/src/extend/custom-rules.md index 35b04fdbf8e..35df20541b6 100644 --- a/docs/src/extend/custom-rules.md +++ b/docs/src/extend/custom-rules.md @@ -4,7 +4,7 @@ eleventyNavigation: key: custom rules parent: create plugins title: Custom Rules - order: 1 + order: 2 --- @@ -131,7 +131,7 @@ The `context` object has the following properties: * `sourceCode`: (`object`) A `SourceCode` object that you can use to work with the source that was passed to ESLint (see [Accessing the Source Code](#accessing-the-source-code)). * `settings`: (`object`) The [shared settings](../use/configure/configuration-files#adding-shared-settings) from the configuration. * `parserPath`: (`string`) The name of the `parser` from the configuration. -* `parserServices`: (`object`) Contains parser-provided services for rules. The default parser does not provide any services. However, if a rule is intended to be used with a custom parser, it could use `parserServices` to access anything provided by that parser. (For example, a TypeScript parser could provide the ability to get the computed type of a given node.) +* `parserServices`: (**Deprecated:** Use `SourceCode#parserServices` instead.) Contains parser-provided services for rules. The default parser does not provide any services. However, if a rule is intended to be used with a custom parser, it could use `parserServices` to access anything provided by that parser. (For example, a TypeScript parser could provide the ability to get the computed type of a given node.) * `parserOptions`: The parser options configured for this run (more details [here](../use/configure/language-options#specifying-parser-options)). Additionally, the `context` object has the following methods: @@ -575,6 +575,7 @@ There are also some properties you can access: * `ast`: (`object`) `Program` node of the AST for the code being linted. * `scopeManager`: [ScopeManager](./scope-manager-interface#scopemanager-interface) object of the code. * `visitorKeys`: (`object`) Visitor keys to traverse this AST. +* `parserServices`: (`object`) Contains parser-provided services for rules. The default parser does not provide any services. However, if a rule is intended to be used with a custom parser, it could use `parserServices` to access anything provided by that parser. (For example, a TypeScript parser could provide the ability to get the computed type of a given node.) * `lines`: (`array`) Array of lines, split according to the specification's definition of line breaks. You should use a `SourceCode` object whenever you need to get more information about the code being linted. @@ -612,39 +613,168 @@ You can also access comments through many of `sourceCode`'s methods using the `i ### Options Schemas -Rules may export a `schema` property, which is a [JSON Schema](https://json-schema.org/) format description of a rule's options which will be used by ESLint to validate configuration options and prevent invalid or unexpected inputs before they are passed to the rule in `context.options`. +Rules may specify a `schema` property, which is a [JSON Schema](https://json-schema.org/) format description of a rule's options which will be used by ESLint to validate configuration options and prevent invalid or unexpected inputs before they are passed to the rule in `context.options`. + +Note: Prior to ESLint v9.0.0, rules without a schema are passed their options directly from the config without any validation. In ESLint v9.0.0 and later, rules without schemas will throw errors when options are passed. See the [Require schemas and object-style rules](https://github.com/eslint/rfcs/blob/main/designs/2021-schema-object-rules/README.md) RFC for further details. + +When validating a rule's config, there are five steps: -There are two formats for a rule's exported `schema`: +1. If the rule config is not an array, then the value is wrapped into an array (e.g. `"off"` becomes `["off"]`); if the rule config is an array then it is used directly. +2. ESLint validates the first element of the rule config array as a severity (`"off"`, `"warn"`, `"error"`, `0`, `1`, `2`) +3. If the severity is `off` or `0`, then the rule is disabled and validation stops, ignoring any other elements of the rule config array. +4. If the rule is enabled, then any elements of the array after the severity are copied into the `context.options` array (e.g. a config of `["warn", "never", { someOption: 5 }]` results in `context.options = ["never", { someOption: 5 }]`) +5. The rule's schema validation is run on the `context.options` array. -1. A full JSON Schema object describing all possible options the rule accepts. -2. An array of JSON Schema objects for each optional positional argument. +Note: this means that the rule schema cannot validate the severity. The rule schema only validates the array elements _after_ the severity in a rule config. There is no way for a rule to know what severity it is configured at. -In both cases, these should exclude the [severity](../use/configure/rules#rule-severities), as ESLint automatically validates this first. +There are two formats for a rule's `schema`: + +* An array of JSON Schema objects + * Each element will be checked against the same position in the `context.options` array. + * If the `context.options` array has fewer elements than there are schemas, then the unmatched schemas are ignored + * If the `context.options` array has more elements than there are schemas, then the validation fails + * There are two important consequences to using this format: + * It is _always valid_ for a user to provide no options to your rule (beyond severity) + * If you specify an empty array, then it is _always an error_ for a user to provide any options to your rule (beyond severity) +* A full JSON Schema object that will validate the `context.options` array + * The schema should assume an array of options to validate even if your rule only accepts one option. + * The schema can be arbitrarily complex, so you can validate completely different sets of potential options via `oneOf`, `anyOf` etc. + * The supported version of JSON Schemas is [Draft-04](http://json-schema.org/draft-04/schema), so some newer features such as `if` or `$data` are unavailable. + * At present, it is explicitly planned to not update schema support beyond this level due to ecosystem compatibility concerns. See [this comment](https://github.com/eslint/eslint/issues/13888#issuecomment-872591875) for further context. For example, the `yoda` rule accepts a primary mode argument of `"always"` or `"never"`, as well as an extra options object with an optional property `exceptRange`: ```js +// Valid configuration: +// "yoda": "warn" +// "yoda": ["error"] +// "yoda": ["error", "always"] // "yoda": ["error", "never", { "exceptRange": true }] +// Invalid configuration: +// "yoda": ["warn", "never", { "exceptRange": true }, 5] +// "yoda": ["error", { "exceptRange": true }, "never"] module.exports = { meta: { schema: [ { - "enum": ["always", "never"] + enum: ["always", "never"] }, { - "type": "object", - "properties": { - "exceptRange": { - "type": "boolean" - } + type: "object", + properties: { + exceptRange: { type: "boolean" } }, - "additionalProperties": false + additionalProperties: false } ] - }, + } }; ``` +And here is the equivalent object-based schema: + +```js +// Valid configuration: +// "yoda": "warn" +// "yoda": ["error"] +// "yoda": ["error", "always"] +// "yoda": ["error", "never", { "exceptRange": true }] +// Invalid configuration: +// "yoda": ["warn", "never", { "exceptRange": true }, 5] +// "yoda": ["error", { "exceptRange": true }, "never"] +module.exports = { + meta: { + schema: { + type: "array", + minItems: 0, + maxItems: 2, + items: [ + { + enum: ["always", "never"] + }, + { + type: "object", + properties: { + exceptRange: { type: "boolean" } + }, + additionalProperties: false + } + ] + } + } +}; +``` + +Object schemas can be more precise and restrictive in what is permitted. For example, the below schema always requires the first option to be specified (a number between 0 and 10), but the second option is optional, and can either be an object with some options explicitly set, or `"off"` or `"strict"`. + +```js +// Valid configuration: +// "someRule": ["error", 6] +// "someRule": ["error", 5, "strict"] +// "someRule": ["warn", 10, { someNonOptionalProperty: true }] +// Invalid configuration: +// "someRule": "warn" +// "someRule": ["error"] +// "someRule": ["warn", 15] +// "someRule": ["warn", 7, { }] +// "someRule": ["error", 3, "on"] +// "someRule": ["warn", 7, { someOtherProperty: 5 }] +// "someRule": ["warn", 7, { someNonOptionalProperty: false, someOtherProperty: 5 }] +module.exports = { + meta: { + schema: { + type: "array", + minItems: 1, // Can't specify only severity! + maxItems: 2, + items: [ + { + type: "number", + minimum: 0, + maximum: 10 + }, + { + anyOf: [ + { + type: "object", + properties: { + someNonOptionalProperty: { type: "boolean" } + }, + required: ["someNonOptionalProperty"], + additionalProperties: false + }, + { + enum: ["off", "strict"] + } + ] + } + ] + } + } +} +``` + +Remember, rule options are always an array, so be careful not to specify a schema for a non-array type at the top level. If your schema does not specify an array at the top-level, users can _never_ enable your rule, as their configuration will always be invalid when the rule is enabled. + +Here's an example schema that will always fail validation: + +```js +// Possibly trying to validate ["error", { someOptionalProperty: true }] +// but when the rule is enabled, config will always fail validation because the options are an array which doesn't match "object" +module.exports = { + meta: { + schema: { + type: "object", + properties: { + someOptionalProperty: { + type: "boolean" + } + }, + additionalProperties: false + } + } +} +``` + **Note:** If your rule schema uses JSON schema [`$ref`](https://json-schema.org/understanding-json-schema/structuring.html#ref) properties, you must use the full JSON Schema object rather than the array of positional property schemas. This is because ESLint transforms the array shorthand into a single schema without updating references that makes them incorrect (they are ignored). To learn more about JSON Schema, we recommend looking at some examples on the [JSON Schema website](https://json-schema.org/learn/), or reading the free [Understanding JSON Schema](https://json-schema.org/understanding-json-schema/) ebook. diff --git a/docs/src/extend/index.md b/docs/src/extend/index.md index 9f623a6119d..815a42b457b 100644 --- a/docs/src/extend/index.md +++ b/docs/src/extend/index.md @@ -25,6 +25,10 @@ This page summarizes the various ways that you can extend ESLint and how these e You've developed custom rules for ESLint and you want to share them with the community. You can publish an ESLint plugin on npm. +## [Custom Rule Tutorial](custom-rule-tutorial) + +A tutorial that walks you through creating a custom rule for ESLint. + ## [Custom Rules](custom-rules) This section explains how to create custom rules to use with ESLint. @@ -44,7 +48,3 @@ This section explains how you can use a custom processor to have ESLint process ## [Share Configurations](shareable-configs) This section explains how you can bundle and share ESLint configuration in a JavaScript package. - -## [Node.js API Reference](../integrate/nodejs-api) - -If you're interested in writing a tool that uses ESLint, then you can use the Node.js API to get programmatic access to functionality. diff --git a/docs/src/extend/plugin-migration-flat-config.md b/docs/src/extend/plugin-migration-flat-config.md new file mode 100644 index 00000000000..6c277596fc4 --- /dev/null +++ b/docs/src/extend/plugin-migration-flat-config.md @@ -0,0 +1,293 @@ +--- +title: Plugin Migration to Flat Config +eleventyNavigation: + key: plugin flat config + parent: create plugins + title: Migration to Flat Config + order: 4 + +--- + +Beginning in ESLint v9.0.0, the default configuration system will be the new flat config system. In order for your plugins to work with flat config files, you'll need to make some changes to your existing plugins. + +## Recommended Plugin Structure + +To make it easier to work with your plugin in the flat config system, it's recommended that you switch your existing plugin entrypoint to look like this: + +```js +const plugin = { + meta: {}, + configs: {}, + rules: {}, + processors: {} +}; + +// for ESM +export default plugin; + +// OR for CommonJS +module.exports = plugin; +``` + +This structure allows the most flexibility when making other changes discussed on this page. + +## Adding Plugin Meta Information + +With the old eslintrc configuration system, ESLint could pull information about the plugin from the package name, but with flat config, ESLint no longer has access to the name of the plugin package. To replace that missing information, you should add a `meta` key that contains at least a `name` key, and ideally, a `version` key, such as: + +```js +const plugin = { + meta: { + name: "eslint-plugin-example", + version: "1.0.0" + }, + configs: {}, + rules: {}, + processors: {} +}; + +// for ESM +export default plugin; + +// OR for CommonJS +module.exports = plugin; +``` + +If your plugin is published as an npm package, the `name` and `version` should be the same as in your `package.json` file; otherwise, you can assign any value you'd like. + +Without this meta information, your plugin will not be usable with the `--cache` and `--print-config` command line options. + +## Migrating Rules for Flat Config + +No changes are necessary for the `rules` key in your plugin. Everything works the same as with the old eslintrc configuration system. + +## Migrating Processors for Flat Config + +No changes are necessary for the `processors` key in your plugin as long as you aren't using file extension-named processors. If you have any [file extension-named processors](custom-processors#file-extension-named-processor), you must update the name to a valid identifier (numbers and letters). File extension-named processors were automatically applied in the old configuration system but are not automatically applied when using flat config. Here is an example of a file extension-named processor: + +```js +const plugin = { + configs: {}, + rules: {}, + processors: { + + // no longer supported + ".md": { + preprocess() {}, + postprocess() {} + } + } +}; + +// for ESM +export default plugin; + +// OR for CommonJS +module.exports = plugin; +``` + +The name `".md"` is no longer valid for a processor, so it must be replaced with a valid identifier such as `markdown`: + +```js +const plugin = { + configs: {}, + rules: {}, + processors: { + + // works in both old and new config systems + "markdown": { + preprocess() {}, + postprocess() {} + } + } +}; + +// for ESM +export default plugin; + +// OR for CommonJS +module.exports = plugin; +``` + +In order to use this renamed processor, you'll also need to manually specify it inside of a config, such as: + +```js +import example from "eslint-plugin-example"; + +export default [ + { + plugins: { + example + }, + processor: "example/markdown" + } +]; +``` + +You should update your plugin's documentation to advise your users if you have renamed a file extension-named processor. + +## Migrating Configs for Flat Config + +If your plugin is exporting configs that refer back to your plugin, then you'll need to update your configs to flat config format. As part of the migration, you'll need to reference your plugin directly in the `plugins` key. For example, here is an exported config in the old configuration system format for a plugin named `eslint-plugin-example`: + +```js +// plugin name: eslint-plugin-example +module.exports = { + configs: { + + // the config referenced by example/recommended + recommended: { + plugins: ["example"], + rules: { + "example/rule1": "error", + "example/rule2": "error" + } + } + }, + rules: { + rule1: {}, + rule2: {}; + } +}; +``` + +To migrate to flat config format, you'll need to move the configs to after the definition of the `plugin` variable in the recommended plugin structure, like this: + +```js +const plugin = { + configs: {}, + rules: {}, + processors: {} +}; + +// assign configs here so we can reference `plugin` +Object.assign(plugin.configs, { + recommended: { + plugins: { + example: plugin + }, + rules: { + "example/rule1": "error", + "example/rule2": "error" + } + } +}) + +// for ESM +export default plugin; + +// OR for CommonJS +module.exports = plugin; +``` + +Your users can then use this exported config like this: + +```js +import example from "eslint-plugin-example"; + +export default [ + + // use recommended config + example.configs.recommended, + + // and provide your own overrides + { + rules: { + "example/rule1": "warn" + } + } +]; +``` + +You should update our documentation so your plugin users know how to reference the exported configs. + +## Migrating Environments for Flat Config + +Environments are no longer supported in flat config, and so we recommend transitioning your environments into exported configs. For example, suppose you export a `mocha` environment like this: + +```js +// plugin name: eslint-plugin-example +module.exports = { + environments: { + mocha: { + globals: { + it: true, + xit: true, + describe: true, + xdescribe: true + } + } + }, + rules: { + rule1: {}, + rule2: {}; + } +}; +``` + +To migrate this environment into a config, you need to add a new key in the `plugin.configs` object that has a flat config object containing the same information, like this: + +```js +const plugin = { + configs: {}, + rules: {}, + processors: {} +}; + +// assign configs here so we can reference `plugin` +Object.assign(plugin.configs, { + mocha: { + languageOptions: { + globals: { + it: "writeable", + xit: "writeable", + describe: "writeable", + xdescribe: "writeable" + } + } + } +}) + +// for ESM +export default plugin; + +// OR for CommonJS +module.exports = plugin; +``` + +Your users can then use this exported config like this: + +```js +import example from "eslint-plugin-example"; + +export default [ + + // use the mocha globals + example.configs.mocha, + + // and provide your own overrides + { + languageOptions: { + globals: { + it: "readonly" + } + } + } +]; +``` + +You should update your documentation so your plugin users know how to reference the exported configs. + +## Backwards Compatibility + +If your plugin needs to work with both the old and new configuration systems, then you'll need to: + +1. **Export a CommonJS entrypoint.** The old configuration system cannot load plugins that are published only in ESM format. If your source code is in ESM, then you'll need to use a bundler that can generate a CommonJS version and use the [`exports`](https://nodejs.org/api/packages.html#package-entry-points) key in your `package.json` file to ensure the CommonJS version can be found by Node.js. +1. **Keep the `environments` key.** If your plugin exports custom environments, you should keep those as they are and also export the equivalent flat configs as described above. The `environments` key is ignored when ESLint is running in flat config mode. +1. **Export both eslintrc and flat configs.** The `configs` key is only validated when a config is used, so you can provide both formats of configs in the `configs` key. We recommend that you append older format configs with `-legacy` to make it clear that these configs will not be supported in the future. For example, if your primary config is called `recommended` and is in flat config format, then you can also have a config named `recommended-legacy` that is the eslintrc config format. + +## Further Reading + +* [Overview of the flat config file format blog post](https://eslint.org/blog/2022/08/new-config-system-part-2/) +* [API usage of new configuration system blog post](https://eslint.org/blog/2022/08/new-config-system-part-3/) +* [Background to new configuration system blog post](https://eslint.org/blog/2022/08/new-config-system-part-1/) diff --git a/docs/src/extend/plugins.md b/docs/src/extend/plugins.md index 70bc3ee7c74..5374a0f5094 100644 --- a/docs/src/extend/plugins.md +++ b/docs/src/extend/plugins.md @@ -18,34 +18,6 @@ Each plugin is an npm module with a name in the format of `eslint-plugin- acc + result.errorCount + result.warningCount, 0); + + if (problems > 0) { + console.log("Linting errors found!"); + console.log(results); + } else { + console.log("No linting errors found."); + } + return results; +} +``` + +## Step 5: Put It All Together + +Put the above functions together in a new function called `lintFiles`. This function will be the main entry point for your integration: + +```javascript +// example-eslint-integration.js + +// Put previous functions all together +async function lintFiles(filePaths) { + + // The ESLint configuration. Alternatively, you could load the configuration + // from a .eslintrc file or just use the default config. + const overrideConfig = { + env: { + es6: true, + node: true, + }, + parserOptions: { + ecmaVersion: 2018, + }, + rules: { + "no-console": "error", + "no-unused-vars": "warn", + }, + }; + + const eslint = createESLintInstance(overrideConfig); + const results = await lintAndFix(eslint, filePaths); + return outputLintingResults(results); +} + +// Export integration +module.exports = { lintFiles } +``` + +Here's the complete code example for `example-eslint-integration.js`: + +```javascript +const { ESLint } = require("eslint"); + +// Create an instance of ESLint with the configuration passed to the function +function createESLintInstance(overrideConfig){ + return new ESLint({ useEslintrc: false, overrideConfig: overrideConfig, fix: true }); +} + +// Lint the specified files and return the results +async function lintAndFix(eslint, filePaths) { + const results = await eslint.lintFiles(filePaths); + + // Apply automatic fixes and output fixed code + await ESLint.outputFixes(results); + + return results; +} + +// Log results to console if there are any problems +function outputLintingResults(results) { + // Identify the number of problems found + const problems = results.reduce((acc, result) => acc + result.errorCount + result.warningCount, 0); + + if (problems > 0) { + console.log("Linting errors found!"); + console.log(results); + } else { + console.log("No linting errors found."); + } + return results; +} + +// Put previous functions all together +async function lintFiles(filePaths) { + + // The ESLint configuration. Alternatively, you could load the configuration + // from a .eslintrc file or just use the default config. + const overrideConfig = { + env: { + es6: true, + node: true, + }, + parserOptions: { + ecmaVersion: 2018, + }, + rules: { + "no-console": "error", + "no-unused-vars": "warn", + }, + }; + + const eslint = createESLintInstance(overrideConfig); + const results = await lintAndFix(eslint, filePaths); + return outputLintingResults(results); +} + +// Export integration +module.exports = { lintFiles } +``` + +## Conclusion + +In this tutorial, we have covered the essentials of using the `ESLint` class to lint files and retrieve results in your projects. This knowledge can be applied to create custom integrations, such as code editor plugins, to provide real-time feedback on code quality. + +## View the Tutorial Code + +You can view the annotated source code for the tutorial [here](https://github.com/eslint/eslint/tree/main/docs/_examples/integration-tutorial-code). diff --git a/docs/src/integrate/nodejs-api.md b/docs/src/integrate/nodejs-api.md index 853abdc57b8..744f98295f4 100644 --- a/docs/src/integrate/nodejs-api.md +++ b/docs/src/integrate/nodejs-api.md @@ -2,9 +2,9 @@ title: Node.js API Reference eleventyNavigation: key: node.js api - parent: extend eslint + parent: integrate eslint title: Node.js API Reference - order: 6 + order: 2 --- While ESLint is designed to be run on the command line, it's possible to use ESLint programmatically through the Node.js API. The purpose of the Node.js API is to allow plugin and tool authors to use the ESLint functionality directly, without going through the command line interface. @@ -152,7 +152,7 @@ The `ESLint` constructor takes an `options` object. If you omit the `options` ob * `options.plugins` (`Record | null`)
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.reportUnusedDisableDirectives` (`"error" | "warn" | "off" | null`)
- Default is `null`. The severity to report unused eslint-disable directives. If this option is a severity, it overrides the `reportUnusedDisableDirectives` setting in your configurations. + Default is `null`. The severity to report unused eslint-disable and eslint-enable directives. If this option is a severity, it overrides the `reportUnusedDisableDirectives` setting in your configurations. * `options.resolvePluginsRelativeTo` (`string` | `null`)
Default is `null`. The path to a directory where plugins should be resolved from. If `null` is present, ESLint loads plugins from the location of the configuration file that contains the plugin setting. If a path is present, ESLint loads all plugins from there. * `options.rulePaths` (`string[]`)
@@ -537,7 +537,7 @@ The most important method on `Linter` is `verify()`, which initiates linting of * `filterCodeBlock` - (optional) A function that decides which code blocks the linter should adopt. The function receives two arguments. The first argument is the virtual filename of a code block. The second argument is the text of the code block. If the function returned `true` then the linter adopts the code block. If the function was omitted, the linter adopts only `*.js` code blocks. If you provided a `filterCodeBlock` function, it overrides this default behavior, so the linter doesn't adopt `*.js` code blocks automatically. * `disableFixes` - (optional) when set to `true`, the linter doesn't make either the `fix` or `suggestions` property of the lint result. * `allowInlineConfig` - (optional) set to `false` to disable inline comments from changing ESLint rules. - * `reportUnusedDisableDirectives` - (optional) when set to `true`, adds reported errors for unused `eslint-disable` directives when no problems would be reported in the disabled area anyway. + * `reportUnusedDisableDirectives` - (optional) when set to `true`, adds reported errors for unused `eslint-disable` and `eslint-enable` directives when no problems would be reported in the disabled area anyway. If the third argument is a string, it is interpreted as the `filename`. diff --git a/docs/src/maintain/index.md b/docs/src/maintain/index.md index 29c3ab89f30..1b083836f3f 100644 --- a/docs/src/maintain/index.md +++ b/docs/src/maintain/index.md @@ -3,7 +3,7 @@ title: Maintain ESLint eleventyNavigation: key: maintain eslint title: Maintain ESLint - order: 4 + order: 5 --- diff --git a/docs/src/maintain/manage-issues.md b/docs/src/maintain/manage-issues.md index 803451b4c63..9e243c12e72 100644 --- a/docs/src/maintain/manage-issues.md +++ b/docs/src/maintain/manage-issues.md @@ -1,14 +1,14 @@ --- -title: Manage Issues +title: Manage Issues and Pull Requests eleventyNavigation: key: manage issues parent: maintain eslint - title: Manage Issues + title: Manage Issues and Pull Requests order: 2 --- -New issues are filed frequently, and how we respond to those issues directly affects the success of the project. Being part of the project team means helping to triage and address issues as they come in so the project can continue to run smoothly. +New issues and pull requests are filed frequently, and how we respond to those issues directly affects the success of the project. Being part of the project team means helping to triage and address issues as they come in so the project can continue to run smoothly. ## Things to Keep in Mind @@ -17,44 +17,45 @@ New issues are filed frequently, and how we respond to those issues directly aff 1. **Not all requests are equal.** It's unlikely we'll be able to accommodate every request, so don't be afraid to say that something doesn't fit into the scope of the project or isn't practical. It's better to give such feedback if that's the case. 1. **Close when appropriate.** Don't be afraid to close issues that you don't think will be done, or when it's become clear from the conversation that there's no further work to do. Issues can always be reopened if they are closed incorrectly, so feel free to close issues when appropriate. Just be sure to leave a comment explaining why the issue is being closed (if not closed by a commit). -## Types of Issues +## Types of Issues and Pull Requests -There are four primary issue categories: +There are five primary categories: 1. **Bug**: Something isn't working the way it's expected to work. 1. **Enhancement**: A change to something that already exists. For instance, adding a new option to an existing rule or fixing a bug in a rule where fixing it will result in the rule reporting more problems (in this case, use both "Bug" and "Enhancement"). 1. **Feature**: Adding something that doesn't already exist. For example, adding a new rule, new formatter, or new command line flag. +1. **Documentation**: Adding, updating, or removing project documentation. 1. **Question**: An inquiry about how something works that won't result in a code change. We prefer if people use GitHub Discussions or Discord for questions, but sometimes they'll open an issue. -The first goal when evaluating an issue is to determine which category the issue falls into. +The first goal when evaluating an issue or pull request is to determine which category the issue falls into. ## Triaging Process -All of ESLint's issues, across all GitHub repositories, are managed on our [Triage Project](https://github.com/orgs/eslint/projects/3). Please use the Triage project instead of the issues list when reviewing issues to determine what to work on. The Triage project has several columns: +All of ESLint's issues and pull requests, across all GitHub repositories, are managed on our [Triage Project](https://github.com/orgs/eslint/projects/3). Please use the Triage project instead of the issues list when reviewing issues to determine what to work on. The Triage project has several columns: -* **Needs Triage**: Issues that have not yet been reviewed by anyone -* **Triaging**: Issues that someone has reviewed but has not been able to fully triage yet -* **Ready for Dev Team**: Issues that have been triaged and have all the information necessary for the dev team to take a look -* **Evaluating**: The dev team is evaluating these issues to determine whether to move forward or not +* **Needs Triage**: Issues and pull requests that have not yet been reviewed by anyone +* **Triaging**: Issues and pull requests that someone has reviewed but has not been able to fully triage yet +* **Ready for Dev Team**: Issues and pull requests that have been triaged and have all the information necessary for the dev team to take a look +* **Evaluating**: The dev team is evaluating these issues and pull requests to determine whether to move forward or not * **Feedback Needed**: A team member is requesting more input from the rest of the team before proceeding * **Waiting for RFC**: The next step in the process is for an RFC to be written * **RFC Opened**: An RFC is opened to address these issues * **Blocked**: The issue can't move forward due to some dependency * **Ready to Implement**: These issues have all the details necessary to start implementation -* **Implementing**: There is an open pull request for each of these issues +* **Implementing**: There is an open pull request for each of these issues or this is a pull request that has been approved * **Completed**: The issue has been closed (either via pull request merge or by the team manually closing the issue) We make every attempt to automate movement between as many columns as we can, but sometimes moving issues needs to be done manually. -### When an Issue is Opened +### When an Issue or Pull Request is Opened -When an issue is opened, it is automatically added to the "Needs Triage" column in the Triage project. These issues need to be evaluated to determine next steps. Anyone on the support team or dev team can follow these steps to properly triage issues. +When an issue or pull request is opened, it is automatically added to the "Needs Triage" column in the Triage project. These issues and pull requests need to be evaluated to determine the next steps. Anyone on the support team or dev team can follow these steps to properly triage issues. -**Note:** If an issue is in the "Triaging" column, that means someone is already triaging it, and you should let them finish. There's no need to comment on issues in the "Triaging" column unless someone asks for help. +**Note:** If an issue or pull request is in the "Triaging" column, that means someone is already triaging it, and you should let them finish. There's no need to comment on issues or pull requests in the "Triaging" column unless someone asks for help. -The steps for triaging an issue are: +The steps for triaging an issue or pull request are: -1. Move the issue from "Needs Triage" to "Triaging" in the Triage project. +1. Move the issue or pull request from "Needs Triage" to "Triaging" in the Triage project. 1. Check: Has all the information in the issue template been provided? * **No:** If information is missing from the issue template, or you can't tell what is being requested, please ask the author to provide the missing information: * Add the "needs info" label to the issue so we know that this issue is stalled due to lack of information. @@ -62,8 +63,8 @@ The steps for triaging an issue are: * If the issue author hasn't provided the necessary information after 7 days, please close the issue. The bot will add a comment stating that the issue was closed because there was information missing. * **Yes:** * If the issue is actually a question (rather than something the dev team needs to change), please [convert it to a discussion](https://docs.github.com/en/free-pro-team@latest/discussions/managing-discussions-for-your-community/moderating-discussions#converting-an-issue-to-a-discussion). You can continue the conversation as a discussion. - * If the issue is reporting a bug, try to reproduce the issue following the instructions in the issue. If you can reproduce the bug, please add the "repro:yes" label. (The bot will automatically remove the "repro:needed" label.) If you can't reproduce the bug, ask the author for more information about their environment or to clarify reproduction steps. - * If the issue is reporting something that works as intended, please add the "works as intended" label and close the issue. + * If the issue is reporting a bug, or if a pull request is fixing a bug, try to reproduce the issue following the instructions in the issue. If you can reproduce the bug, please add the "repro:yes" label. (The bot will automatically remove the "repro:needed" label.) If you can't reproduce the bug, ask the author for more information about their environment or to clarify reproduction steps. + * If the issue or pull request is reporting something that works as intended, please add the "works as intended" label and close the issue. * Please add labels describing the part of ESLint affected: * **3rd party plugin**: Related to third-party functionality (plugins, parsers, rules, etc.) * **build**: Related to commands run during a build (testing, linting, release scripts, etc.) @@ -72,17 +73,18 @@ The steps for triaging an issue are: * **documentation**: Related to content on eslint.org * **infrastructure**: Related to resources needed for builds or deployment (VMs, CI tools, bots, etc.) * **rule**: Related to core rules - * Please assign an initial priority based on the importance of the issue. If you're not sure, use your best judgment. We can always change the priority later. + * Please assign an initial priority based on the importance of the issue or pull request. If you're not sure, use your best judgment. We can always change the priority later. * **P1**: Urgent and important, we need to address this immediately. * **P2**: Important but not urgent. Should be handled by a TSC member or reviewer. * **P3**: Nice to have but not important. Can be handled by any team member. * **P4**: A good idea that we'd like to have but may take a while for the team to get to it. * **P5**: A good idea that the core team can't commit to. Will likely need to be done by an outside contributor. - * Please assign an initial impact assessement (make your best guess): + * Please assign an initial impact assessment (make your best guess): * **Low**: Doesn't affect many users. * **Medium**: Affects most users or has a noticeable effect on user experience. * **High**: Affects a lot of users, is a breaking change, or otherwise will be very noticeable to users. - * If you can't properly triage the issue, move the issue back to the "Needs Triage" column in the Triage project so someone else can triage it. + * If you can't properly triage the issue or pull request, move the issue back to the "Needs Triage" column in the Triage project so someone else can triage it. + * If a pull request references an already accepted issue, move it to the "Implementing" column in the Triage project. * If you have triaged the issue, move the issue to the "Ready for Dev Team" column in the Triage project. ## Evaluation Process @@ -102,7 +104,7 @@ When an issue has been moved to the "Ready for Dev Team" column, any dev team me **Note:** "Good first issue" issues are intended to help new contributors feel welcome and empowered to make a contribution to ESLint. To ensure that new contributors are given a chance to work on these issues, issues labeled "good first issue" must be open for 30 days *from the day the issue was labeled* before a team member is permitted to work on them. -## Accepting Issues +## Accepting Issues and Pull Requests Issues may be labeled as "accepted" when the issue is: diff --git a/docs/src/pages/index.md b/docs/src/pages/index.md index c8f5f62dcaa..f1a112a0010 100644 --- a/docs/src/pages/index.md +++ b/docs/src/pages/index.md @@ -12,7 +12,11 @@ as well as guides for migrating from earlier versions of ESLint. ## [Extend ESLint](extend/) -Intended for people who wish to extend ESLint. Contains information about creating custom rules, configurations, plugins, and formatters; and information about our Node.js API. +Intended for people who wish to extend ESLint. Contains information about creating custom rules, configurations, plugins, and formatters. + +## [Integrate ESLint](integrate/) + +Intended for people who wish to create integrations with ESLint. Contains information about creating integrations and using the Node.js API. ## [Contribute to ESLint](contribute/) diff --git a/docs/src/pages/rules.md b/docs/src/pages/rules.md index 996860e2c86..9d801cf5126 100644 --- a/docs/src/pages/rules.md +++ b/docs/src/pages/rules.md @@ -20,14 +20,17 @@ Rules in ESLint are grouped by type to help you understand their purpose. Each r hasSuggestions: true }) }} -{%- for type in rules.types -%} +{%- for type, content in rules.types -%} -

{{ type.displayName }}

+

{{ rules_categories[type].displayName }}

-{{ type.description | safe }} +{{ rules_categories[type].description | safe }} - {%- for the_rule in type.rules -%} - {%- if type.displayName == 'deprecated' -%}{%- set deprecated_value = true -%}{%- endif -%} + {%- for the_rule in content -%} + + {%- if rules_categories[type].displayName == 'deprecated' -%} + {%- set deprecated_value = true -%} + {%- endif -%} {%- set name_value = the_rule.name -%} {%- set description_value = the_rule.description -%} @@ -50,11 +53,11 @@ Rules in ESLint are grouped by type to help you understand their purpose. Each r {%- if rules.deprecated -%} -

{{ rules.deprecated.name }}

+

{{ rules_categories.deprecated.displayName }}

-{{ rules.deprecated.description | safe }} +{{ rules_categories.deprecated.description | safe }} -{%- for the_rule in rules.deprecated.rules -%} +{%- for the_rule in rules.deprecated -%} {%- set name_value = the_rule.name -%} {%- set isReplacedBy = the_rule.replacedBy -%} @@ -68,11 +71,11 @@ Rules in ESLint are grouped by type to help you understand their purpose. Each r {%- if rules.removed -%} -

{{ rules.removed.name }}

+

{{ rules_categories.removed.displayName }}

-{{ rules.removed.description | safe }} +{{ rules_categories.removed.description | safe }} -{%- for the_rule in rules.removed.rules -%} +{%- for the_rule in rules.removed -%} {%- set name_value = the_rule.removed -%} {%- set isReplacedBy = the_rule.replacedBy -%} diff --git a/docs/src/rules/array-bracket-newline.md b/docs/src/rules/array-bracket-newline.md index 4ad7e27b560..2ccb13dddf9 100644 --- a/docs/src/rules/array-bracket-newline.md +++ b/docs/src/rules/array-bracket-newline.md @@ -5,7 +5,7 @@ related_rules: - array-bracket-spacing --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). A number of style guides require or disallow line breaks inside of array brackets. diff --git a/docs/src/rules/array-bracket-spacing.md b/docs/src/rules/array-bracket-spacing.md index 5b6dd8a7cac..0fdc8310747 100644 --- a/docs/src/rules/array-bracket-spacing.md +++ b/docs/src/rules/array-bracket-spacing.md @@ -7,7 +7,7 @@ related_rules: - computed-property-spacing --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). A number of style guides require or disallow spaces between array brackets and other tokens. This rule applies to both array literals and destructuring assignments (ECMAScript 6). diff --git a/docs/src/rules/array-callback-return.md b/docs/src/rules/array-callback-return.md index 965d18537af..625cb14ced3 100644 --- a/docs/src/rules/array-callback-return.md +++ b/docs/src/rules/array-callback-return.md @@ -92,10 +92,13 @@ var bar = foo.map(node => node.getAttribute("id")); ## Options -This rule accepts a configuration object with two options: +This rule accepts a configuration object with three options: * `"allowImplicit": false` (default) When set to `true`, allows callbacks of methods that require a return value to implicitly return `undefined` with a `return` statement containing no expression. * `"checkForEach": false` (default) When set to `true`, rule will also report `forEach` callbacks that return a value. +* `"allowVoid": false` (default) When set to `true`, allows `void` in `forEach` callbacks, so rule will not report the return value with a `void` operator. + +**Note:** `{ "allowVoid": true }` works only if `checkForEach` option is set to `true`. ### allowImplicit @@ -122,7 +125,7 @@ Examples of **incorrect** code for the `{ "checkForEach": true }` option: /*eslint array-callback-return: ["error", { checkForEach: true }]*/ myArray.forEach(function(item) { - return handleItem(item) + return handleItem(item); }); myArray.forEach(function(item) { @@ -132,11 +135,24 @@ myArray.forEach(function(item) { handleItem(item); }); +myArray.forEach(function(item) { + if (item < 0) { + return void x; + } + handleItem(item); +}); + myArray.forEach(item => handleItem(item)); +myArray.forEach(item => void handleItem(item)); + myArray.forEach(item => { return handleItem(item); }); + +myArray.forEach(item => { + return void handleItem(item); +}); ``` ::: @@ -171,6 +187,31 @@ myArray.forEach(item => { ::: +### allowVoid + +Examples of **correct** code for the `{ "allowVoid": true }` option: + +:::correct + +```js +/*eslint array-callback-return: ["error", { checkForEach: true, allowVoid: true }]*/ + +myArray.forEach(item => void handleItem(item)); + +myArray.forEach(item => { + return void handleItem(item); +}); + +myArray.forEach(item => { + if (item < 0) { + return void x; + } + handleItem(item); +}); +``` + +::: + ## Known Limitations This rule checks callback functions of methods with the given names, *even if* the object which has the method is *not* an array. diff --git a/docs/src/rules/array-element-newline.md b/docs/src/rules/array-element-newline.md index 2b34bd53014..0a8bc39b9bc 100644 --- a/docs/src/rules/array-element-newline.md +++ b/docs/src/rules/array-element-newline.md @@ -12,7 +12,7 @@ related_rules: - brace-style --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). A number of style guides require or disallow line breaks between array elements. diff --git a/docs/src/rules/arrow-body-style.md b/docs/src/rules/arrow-body-style.md index 1975af13eed..1dc6fcc36e8 100644 --- a/docs/src/rules/arrow-body-style.md +++ b/docs/src/rules/arrow-body-style.md @@ -32,6 +32,7 @@ Examples of **incorrect** code for this rule with the `"always"` option: ```js /*eslint arrow-body-style: ["error", "always"]*/ /*eslint-env es6*/ + let foo = () => 0; ``` @@ -42,10 +43,13 @@ Examples of **correct** code for this rule with the `"always"` option: :::correct ```js +/*eslint arrow-body-style: ["error", "always"]*/ +/*eslint-env es6*/ + let foo = () => { return 0; }; -let foo = (retv, name) => { +let bar = (retv, name) => { retv[name] = true; return retv; }; @@ -66,7 +70,7 @@ Examples of **incorrect** code for this rule with the default `"as-needed"` opti let foo = () => { return 0; }; -let foo = () => { +let bar = () => { return { bar: { foo: 1, @@ -86,24 +90,24 @@ Examples of **correct** code for this rule with the default `"as-needed"` option /*eslint arrow-body-style: ["error", "as-needed"]*/ /*eslint-env es6*/ -let foo = () => 0; -let foo = (retv, name) => { +let foo1 = () => 0; +let foo2 = (retv, name) => { retv[name] = true; return retv; }; -let foo = () => ({ +let foo3 = () => ({ bar: { foo: 1, bar: 2, } }); -let foo = () => { bar(); }; -let foo = () => {}; -let foo = () => { /* do nothing */ }; -let foo = () => { +let foo4 = () => { bar(); }; +let foo5 = () => {}; +let foo6 = () => { /* do nothing */ }; +let foo7 = () => { // do nothing. }; -let foo = () => ({ bar: 0 }); +let foo8 = () => ({ bar: 0 }); ``` ::: @@ -120,7 +124,7 @@ Examples of **incorrect** code for this rule with the `{ "requireReturnForObject /*eslint arrow-body-style: ["error", "as-needed", { "requireReturnForObjectLiteral": true }]*/ /*eslint-env es6*/ let foo = () => ({}); -let foo = () => ({ bar: 0 }); +let bar = () => ({ bar: 0 }); ``` ::: @@ -134,7 +138,7 @@ Examples of **correct** code for this rule with the `{ "requireReturnForObjectLi /*eslint-env es6*/ let foo = () => {}; -let foo = () => { return { bar: 0 }; }; +let bar = () => { return { bar: 0 }; }; ``` ::: @@ -152,7 +156,7 @@ Examples of **incorrect** code for this rule with the `"never"` option: let foo = () => { return 0; }; -let foo = (retv, name) => { +let bar = (retv, name) => { retv[name] = true; return retv; }; @@ -169,7 +173,7 @@ Examples of **correct** code for this rule with the `"never"` option: /*eslint-env es6*/ let foo = () => 0; -let foo = () => ({ foo: 0 }); +let bar = () => ({ foo: 0 }); ``` ::: diff --git a/docs/src/rules/arrow-parens.md b/docs/src/rules/arrow-parens.md index 8b956c35664..4bfa819f87b 100644 --- a/docs/src/rules/arrow-parens.md +++ b/docs/src/rules/arrow-parens.md @@ -5,7 +5,7 @@ further_reading: - https://github.com/airbnb/javascript#arrows--one-arg-parens --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Arrow functions can omit parentheses when they have exactly one parameter. In all other cases the parameter(s) must be wrapped in parentheses. This rule enforces the consistent use of parentheses in arrow functions. diff --git a/docs/src/rules/arrow-spacing.md b/docs/src/rules/arrow-spacing.md index fa5a42410a4..3975e9f82cd 100644 --- a/docs/src/rules/arrow-spacing.md +++ b/docs/src/rules/arrow-spacing.md @@ -3,7 +3,7 @@ title: arrow-spacing rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). This rule normalize style of spacing before/after an arrow function's arrow(`=>`). diff --git a/docs/src/rules/block-spacing.md b/docs/src/rules/block-spacing.md index 415fc7db0bd..831916b55e7 100644 --- a/docs/src/rules/block-spacing.md +++ b/docs/src/rules/block-spacing.md @@ -6,7 +6,7 @@ related_rules: - brace-style --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). ## Rule Details diff --git a/docs/src/rules/brace-style.md b/docs/src/rules/brace-style.md index 456ccf2c2e1..8fb982328a6 100644 --- a/docs/src/rules/brace-style.md +++ b/docs/src/rules/brace-style.md @@ -8,7 +8,7 @@ further_reading: - https://en.wikipedia.org/wiki/Indent_style --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Brace style is closely related to [indent style](https://en.wikipedia.org/wiki/Indent_style) in programming and describes the placement of braces relative to their control statement and body. There are probably a dozen, if not more, brace styles in the world. diff --git a/docs/src/rules/camelcase.md b/docs/src/rules/camelcase.md index 46757fd53b0..536fba9439b 100644 --- a/docs/src/rules/camelcase.md +++ b/docs/src/rules/camelcase.md @@ -49,11 +49,11 @@ function foo({ no_camelcased }) { // ... }; -function foo({ isCamelcased: no_camelcased }) { +function bar({ isCamelcased: no_camelcased }) { // ... } -function foo({ no_camelcased = 'default value' }) { +function baz({ no_camelcased = 'default value' }) { // ... }; @@ -63,7 +63,7 @@ var obj = { var { category_id = 1 } = query; -var { foo: no_camelcased } = bar; +var { foo: snake_cased } = bar; var { foo: bar_baz = 1 } = quz; ``` @@ -83,8 +83,8 @@ var myFavoriteColor = "#112C85"; var _myFavoriteColor = "#112C85"; var myFavoriteColor_ = "#112C85"; var MY_FAVORITE_COLOR = "#112C85"; -var foo = bar.baz_boom; -var foo = { qux: bar.baz_boom }; +var foo1 = bar.baz_boom; +var foo2 = { qux: bar.baz_boom }; obj.do_something(); do_something(); @@ -96,11 +96,11 @@ function foo({ isCamelCased }) { // ... }; -function foo({ isCamelCased: isAlsoCamelCased }) { +function bar({ isCamelCased: isAlsoCamelCased }) { // ... } -function foo({ isCamelCased = 'default value' }) { +function baz({ isCamelCased = 'default value' }) { // ... }; @@ -143,9 +143,9 @@ Examples of **incorrect** code for this rule with the default `{ "ignoreDestruct var { category_id } = query; -var { category_id = 1 } = query; +var { category_name = 1 } = query; -var { category_id: category_id } = query; +var { category_id: category_title } = query; var { category_id: category_alias } = query; @@ -328,7 +328,7 @@ function UNSAFE_componentWillMount() { // ... } -function UNSAFE_componentWillMount() { +function UNSAFE_componentWillReceiveProps() { // ... } ``` diff --git a/docs/src/rules/class-methods-use-this.md b/docs/src/rules/class-methods-use-this.md index 2269fac8eff..5a12f9af3c9 100644 --- a/docs/src/rules/class-methods-use-this.md +++ b/docs/src/rules/class-methods-use-this.md @@ -86,13 +86,13 @@ class A { } } -class A { +class B { constructor() { // OK. constructor is exempt } } -class A { +class C { static foo() { // OK. static methods aren't expected to use this. } diff --git a/docs/src/rules/comma-dangle.md b/docs/src/rules/comma-dangle.md index 1370874ed50..17685a08f3d 100644 --- a/docs/src/rules/comma-dangle.md +++ b/docs/src/rules/comma-dangle.md @@ -3,7 +3,7 @@ title: comma-dangle rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Trailing commas in object literals are valid according to the ECMAScript 5 (and ECMAScript 3!) spec. However, IE8 (when not in IE8 document mode) and below will throw an error when it encounters trailing commas in JavaScript. diff --git a/docs/src/rules/comma-spacing.md b/docs/src/rules/comma-spacing.md index b6937856321..e4d430997be 100644 --- a/docs/src/rules/comma-spacing.md +++ b/docs/src/rules/comma-spacing.md @@ -16,7 +16,7 @@ further_reading: - https://dojotoolkit.org/reference-guide/1.9/developer/styleguide.html --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Spacing around commas improves readability of a list of items. Although most of the style guidelines for languages prescribe adding a space after a comma and not before it, it is subjective to the preferences of a project. @@ -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 99d14b27f4f..5859710ee89 100644 --- a/docs/src/rules/comma-style.md +++ b/docs/src/rules/comma-style.md @@ -7,7 +7,7 @@ further_reading: - https://gist.github.com/isaacs/357981 --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). The Comma Style rule enforces styles for comma-separated lists. There are two comma styles primarily used in JavaScript: @@ -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/computed-property-spacing.md b/docs/src/rules/computed-property-spacing.md index 065270d6caf..424e97cce63 100644 --- a/docs/src/rules/computed-property-spacing.md +++ b/docs/src/rules/computed-property-spacing.md @@ -7,7 +7,7 @@ related_rules: - space-in-parens --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). While formatting preferences are very personal, a number of style guides require or disallow spaces between computed properties in the following situations: diff --git a/docs/src/rules/consistent-return.md b/docs/src/rules/consistent-return.md index 2d5f4e3f2ca..797c9b08ca0 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/constructor-super.md b/docs/src/rules/constructor-super.md index 93019eb2a79..c6f008f13d6 100644 --- a/docs/src/rules/constructor-super.md +++ b/docs/src/rules/constructor-super.md @@ -1,6 +1,7 @@ --- title: constructor-super rule_type: problem +handled_by_typescript: true --- Constructors of derived classes must call `super()`. @@ -13,6 +14,16 @@ This rule checks whether or not there is a valid `super()` call. This rule is aimed to flag invalid/missing `super()` calls. +This is a syntax error because there is no `extends` clause in the class: + +```js +class A { + constructor() { + super(); + } +} +``` + Examples of **incorrect** code for this rule: :::incorrect @@ -21,24 +32,18 @@ Examples of **incorrect** code for this rule: /*eslint constructor-super: "error"*/ /*eslint-env es6*/ -class A { - constructor() { - super(); // This is a SyntaxError. - } -} - class A extends B { constructor() { } // Would throw a ReferenceError. } // Classes which inherits from a non constructor are always problems. -class A extends null { +class C extends null { constructor() { super(); // Would throw a TypeError. } } -class A extends null { +class D extends null { constructor() { } // Would throw a ReferenceError. } ``` @@ -57,7 +62,7 @@ class A { constructor() { } } -class A extends B { +class B extends C { constructor() { super(); } diff --git a/docs/src/rules/default-param-last.md b/docs/src/rules/default-param-last.md index 7f64f7c4464..63672a9103e 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/dot-location.md b/docs/src/rules/dot-location.md index 47ee575be97..8abb47708f4 100644 --- a/docs/src/rules/dot-location.md +++ b/docs/src/rules/dot-location.md @@ -6,7 +6,7 @@ related_rules: - dot-notation --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). JavaScript allows you to place newlines before or after a dot in a member expression. diff --git a/docs/src/rules/eol-last.md b/docs/src/rules/eol-last.md index 4c9d837c5c3..c4c0dc991f0 100644 --- a/docs/src/rules/eol-last.md +++ b/docs/src/rules/eol-last.md @@ -3,7 +3,7 @@ title: eol-last rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Trailing newlines in non-empty files are a common UNIX idiom. Benefits of trailing newlines include the ability to concatenate or append to files as well @@ -42,7 +42,8 @@ Examples of **correct** code for this rule: function doSomething() { var foo = 2; -}\n +} + ``` ::: diff --git a/docs/src/rules/for-direction.md b/docs/src/rules/for-direction.md index 832c87cde3d..45759eb48f9 100644 --- a/docs/src/rules/for-direction.md +++ b/docs/src/rules/for-direction.md @@ -3,11 +3,11 @@ title: for-direction rule_type: problem --- - +A `for` loop with a stop condition that can never be reached, such as one with a counter that moves in the wrong direction, will run infinitely. While there are occasions when an infinite loop is intended, the convention is to construct such loops as `while` loops. More typically, an infinite `for` loop is a bug. ## Rule Details -A `for` loop with a stop condition that can never be reached, such as one with a counter that moves in the wrong direction, will run infinitely. While there are occasions when an infinite loop is intended, the convention is to construct such loops as `while` loops. More typically, an infinite for loop is a bug. +This rule forbids `for` loops where the counter variable changes in such a way that the stop condition will never be met. For example, if the counter variable is increasing (i.e. `i++`) and the stop condition tests that the counter is greater than zero (`i >= 0`) then the loop will never exit. Examples of **incorrect** code for this rule: @@ -23,6 +23,10 @@ for (var i = 10; i >= 0; i++) { for (var i = 0; i > 10; i++) { } + +const n = -2; +for (let i = 0; i < 10; i += n) { +} ``` ::: @@ -35,6 +39,12 @@ Examples of **correct** code for this rule: /*eslint for-direction: "error"*/ for (var i = 0; i < 10; i++) { } + +for (let i = 10; i >= 0; i += this.step) { // direction unknown +} + +for (let i = MIN; i <= MAX; i -= 0) { // not increasing or decreasing +} ``` ::: diff --git a/docs/src/rules/func-call-spacing.md b/docs/src/rules/func-call-spacing.md index 082c86d8140..2821630fdbf 100644 --- a/docs/src/rules/func-call-spacing.md +++ b/docs/src/rules/func-call-spacing.md @@ -5,7 +5,7 @@ related_rules: - no-spaced-func --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). When calling a function, developers may insert optional whitespace between the function's name and the parentheses that invoke it. The following pairs of function calls are equivalent: diff --git a/docs/src/rules/function-call-argument-newline.md b/docs/src/rules/function-call-argument-newline.md index be44b2ed7e9..73adf30c59f 100644 --- a/docs/src/rules/function-call-argument-newline.md +++ b/docs/src/rules/function-call-argument-newline.md @@ -8,7 +8,7 @@ related_rules: - array-element-newline --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). A number of style guides require or disallow line breaks between arguments of a function call. diff --git a/docs/src/rules/function-paren-newline.md b/docs/src/rules/function-paren-newline.md index 4ba5a922c8f..116737118e9 100644 --- a/docs/src/rules/function-paren-newline.md +++ b/docs/src/rules/function-paren-newline.md @@ -3,7 +3,7 @@ title: function-paren-newline rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Many style guides require or disallow newlines inside of function parentheses. @@ -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,17 +333,21 @@ function foo( baz ) {} -function foo(bar, baz, qux) {} +function foobar(bar, baz, qux) {} -var foo = function( +var barbaz = function( bar, baz ) {}; -var foo = (bar, - baz) => {}; +var barbaz = ( + bar, + baz +) => {}; -foo(bar, - baz); +foo( + bar, + baz +); ``` ::: @@ -357,13 +361,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/generator-star-spacing.md b/docs/src/rules/generator-star-spacing.md index 3ef58d86da6..a8b3c6c9b17 100644 --- a/docs/src/rules/generator-star-spacing.md +++ b/docs/src/rules/generator-star-spacing.md @@ -5,7 +5,7 @@ further_reading: - https://leanpub.com/understandinges6/read/#leanpub-auto-generators --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Generators are a new type of function in ECMAScript 6 that can return multiple values over time. These special functions are indicated by placing an `*` after the `function` keyword. diff --git a/docs/src/rules/getter-return.md b/docs/src/rules/getter-return.md index 0c8937d14cc..9d316303d02 100644 --- a/docs/src/rules/getter-return.md +++ b/docs/src/rules/getter-return.md @@ -1,6 +1,7 @@ --- title: getter-return rule_type: problem +handled_by_typescript: true further_reading: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get - https://leanpub.com/understandinges6/read/#leanpub-auto-accessor-properties diff --git a/docs/src/rules/global-require.md b/docs/src/rules/global-require.md index e960468b380..8460acb89ad 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-denylist.md b/docs/src/rules/id-denylist.md index 850f5923e83..82859ec8b43 100644 --- a/docs/src/rules/id-denylist.md +++ b/docs/src/rules/id-denylist.md @@ -46,7 +46,7 @@ Examples of **incorrect** code for this rule with sample `"data", "callback"` re ```js /*eslint id-denylist: ["error", "data", "callback"] */ -var data = {...}; +var data = { ...values }; function callback() { // ... @@ -57,23 +57,23 @@ element.callback = function() { }; var itemSet = { - data: [...] + data: [...values] }; class Foo { data = []; } -class Foo { +class Bar { #data = []; } -class Foo { - callback( {); +class Baz { + callback() {} } -class Foo { - #callback( {); +class Qux { + #callback() {} } ``` @@ -86,7 +86,7 @@ Examples of **correct** code for this rule with sample `"data", "callback"` rest ```js /*eslint id-denylist: ["error", "data", "callback"] */ -var encodingOptions = {...}; +var encodingOptions = {...values}; function processFileResult() { // ... @@ -97,7 +97,7 @@ element.successHandler = function() { }; var itemSet = { - entities: [...] + entities: [...values] }; callback(); // all function calls are ignored @@ -110,16 +110,16 @@ class Foo { items = []; } -class Foo { +class Bar { #items = []; } -class Foo { - method( {); +class Baz { + method() {} } -class Foo { - #method( {); +class Qux { + #method() {} } ``` diff --git a/docs/src/rules/id-length.md b/docs/src/rules/id-length.md index adf5e897817..ea5d2fa9d1a 100644 --- a/docs/src/rules/id-length.md +++ b/docs/src/rules/id-length.md @@ -41,16 +41,16 @@ try { } var myObj = { a: 1 }; (a) => { a * a }; -class x { } +class y { } class Foo { x() {} } -class Foo { #x() {} } -class Foo { x = 1 } -class Foo { #x = 1 } -function foo(...x) { } -function foo([x]) { } +class Bar { #x() {} } +class Baz { x = 1 } +class Qux { #x = 1 } +function bar(...x) { } +function baz([x]) { } var [x] = arr; var { prop: [x]} = {}; -function foo({x}) { } +function qux({x}) { } var { x } = {}; var { prop: a} = {}; ({ prop: obj.x } = {}); @@ -78,19 +78,19 @@ try { } var myObj = { apple: 1 }; (num) => { num * num }; -function foo(num = 0) { } +function bar(num = 0) { } class MyClass { } class Foo { method() {} } -class Foo { #method() {} } -class Foo { field = 1 } -class Foo { #field = 1 } -function foo(...args) { } -function foo([longName]) { } +class Bar { #method() {} } +class Baz { field = 1 } +class Qux { #field = 1 } +function baz(...args) { } +function qux([longName]) { } var { prop } = {}; var { prop: [longName] } = {}; var [longName] = arr; -function foo({ prop }) { } -function foo({ a: prop }) { } +function foobar({ prop }) { } +function foobaz({ a: prop }) { } var { prop } = {}; var { a: prop } = {}; ({ prop: obj.longName } = {}); @@ -129,9 +129,9 @@ try { } var myObj = { a: 1 }; (val) => { val * val }; -class x { } +class y { } class Foo { x() {} } -function foo(...x) { } +function bar(...x) { } var { x } = {}; var { prop: a} = {}; var [x] = arr; @@ -151,7 +151,7 @@ Examples of **correct** code for this rule with the `{ "min": 4 }` option: var value = 5; function func() { return 42; } -obj.element = document.body; +object.element = document.body; var foobar = function (event) { /* do stuff */ }; try { dangerousStuff(); @@ -160,15 +160,15 @@ 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] } = {}; var { a: longName } = {}; -({ prop: obj.name } = {}); +({ prop: object.name } = {}); var data = { "x": 1 }; // excused because of quotes data["y"] = 3; // excused because of calculated property access ``` @@ -242,16 +242,16 @@ var myObj = { a: 1 }; ### exceptions -Examples of additional **correct** code for this rule with the `{ "exceptions": ["x"] }` option: +Examples of additional **correct** code for this rule with the `{ "exceptions": ["x", "y", "z", "ζ"] }` option: ::: correct ```js -/*eslint id-length: ["error", { "exceptions": ["x"] }]*/ +/*eslint id-length: ["error", { "exceptions": ["x", "y", "z", "ζ"] }]*/ /*eslint-env es6*/ var x = 5; -function x() { return 42; } +function y() { return 42; } obj.x = document.body; var foo = function (x) { /* do stuff */ }; try { @@ -261,8 +261,8 @@ try { } (x) => { return x * x; }; var [x] = arr; -const { x } = foo; -const { a: x } = foo; +const { z } = foo; +const { a: ζ } = foo; ``` ::: diff --git a/docs/src/rules/id-match.md b/docs/src/rules/id-match.md index 08d5ff3ba9c..59954fc2a48 100644 --- a/docs/src/rules/id-match.md +++ b/docs/src/rules/id-match.md @@ -42,17 +42,13 @@ function do_something() { // ... } -obj.do_something = function() { - // ... -}; - class My_Class {} class myClass { do_something() {} } -class myClass { +class anotherClass { #do_something() {} } ``` @@ -76,11 +72,11 @@ var obj = { class myClass {} -class myClass { +class anotherClass { doSomething() {} } -class myClass { +class oneMoreClass { #doSomething() {} } ``` @@ -110,6 +106,10 @@ Examples of **incorrect** code for this rule with the `"^[a-z]+([A-Z][a-z]+)*$", var obj = { my_pref: 1 }; + +obj.do_something = function() { + // ... +}; ``` ::: @@ -121,13 +121,13 @@ Examples of **incorrect** code for this rule with the `"^[a-z]+([A-Z][a-z]+)*$", ::: incorrect ```js -/*eslint id-match: ["error", "^[a-z]+([A-Z][a-z]+)*$", { "properties": true }]*/ +/*eslint id-match: ["error", "^[a-z]+([A-Z][a-z]+)*$", { "classFields": true }]*/ class myClass { my_pref = 1; } -class myClass { +class anotherClass { #my_pref = 1; } ``` @@ -143,7 +143,7 @@ Examples of **correct** code for this rule with the `"^[a-z]+([A-Z][a-z]+)*$", { ```js /*eslint id-match: [2, "^[a-z]+([A-Z][a-z]+)*$", { "onlyDeclarations": true }]*/ -do_something(__dirname); +foo = __dirname; ``` ::: diff --git a/docs/src/rules/implicit-arrow-linebreak.md b/docs/src/rules/implicit-arrow-linebreak.md index 09aaba18e03..25f5bc97d76 100644 --- a/docs/src/rules/implicit-arrow-linebreak.md +++ b/docs/src/rules/implicit-arrow-linebreak.md @@ -5,7 +5,7 @@ related_rules: - brace-style --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). An arrow function body can contain an implicit return as an expression instead of a block body. It can be useful to enforce a consistent location for the implicitly returned expression. diff --git a/docs/src/rules/indent-legacy.md b/docs/src/rules/indent-legacy.md index 97372ebc8ff..a8097b22932 100644 --- a/docs/src/rules/indent-legacy.md +++ b/docs/src/rules/indent-legacy.md @@ -39,7 +39,7 @@ For example, for 2-space indentation: ```json { - "indent": ["error", 2] + "indent-legacy": ["error", 2] } ``` @@ -47,7 +47,7 @@ Or for tabbed indentation: ```json { - "indent": ["error", "tab"] + "indent-legacy": ["error", "tab"] } ``` @@ -56,7 +56,7 @@ Examples of **incorrect** code for this rule with the default options: ::: incorrect ```js -/*eslint indent: "error"*/ +/*eslint indent-legacy: "error"*/ if (a) { b=c; @@ -73,7 +73,7 @@ Examples of **correct** code for this rule with the default options: ::: correct ```js -/*eslint indent: "error"*/ +/*eslint indent-legacy: "error"*/ if (a) { b=c; @@ -126,7 +126,7 @@ Examples of **incorrect** code for this rule with the `"tab"` option: ::: incorrect ```js -/*eslint indent: ["error", "tab"]*/ +/*eslint indent-legacy: ["error", "tab"]*/ if (a) { b=c; @@ -140,20 +140,23 @@ function foo(d) { Examples of **correct** code for this rule with the `"tab"` option: + + ::: correct ```js -/*eslint indent: ["error", "tab"]*/ +/*eslint indent-legacy: ["error", "tab"]*/ if (a) { -/*tab*/b=c; -/*tab*/function foo(d) { -/*tab*//*tab*/e=f; -/*tab*/} + b=c; + function foo(d) { + e=f; + } } ``` ::: + ### SwitchCase @@ -162,7 +165,7 @@ Examples of **incorrect** code for this rule with the `2, { "SwitchCase": 1 }` o ::: incorrect ```js -/*eslint indent: ["error", 2, { "SwitchCase": 1 }]*/ +/*eslint indent-legacy: ["error", 2, { "SwitchCase": 1 }]*/ switch(a){ case "a": @@ -179,7 +182,7 @@ Examples of **correct** code for this rule with the `2, { "SwitchCase": 1 }` opt ::: correct ```js -/*eslint indent: ["error", 2, { "SwitchCase": 1 }]*/ +/*eslint indent-legacy: ["error", 2, { "SwitchCase": 1 }]*/ switch(a){ case "a": @@ -198,18 +201,18 @@ Examples of **incorrect** code for this rule with the `2, { "VariableDeclarator" ::: incorrect ```js -/*eslint indent: ["error", 2, { "VariableDeclarator": 1 }]*/ +/*eslint indent-legacy: ["error", 2, { "VariableDeclarator": 1 }]*/ /*eslint-env es6*/ var a, b, c; -let a, - b, - c; -const a = 1, - b = 2, - c = 3; +let d, + e, + f; +const g = 1, + h = 2, + i = 3; ``` ::: @@ -219,18 +222,18 @@ Examples of **correct** code for this rule with the `2, { "VariableDeclarator": ::: correct ```js -/*eslint indent: ["error", 2, { "VariableDeclarator": 1 }]*/ +/*eslint indent-legacy: ["error", 2, { "VariableDeclarator": 1 }]*/ /*eslint-env es6*/ var a, b, c; -let a, - b, - c; -const a = 1, - b = 2, - c = 3; +let d, + e, + f; +const g = 1, + h = 2, + i = 3; ``` ::: @@ -240,18 +243,18 @@ Examples of **correct** code for this rule with the `2, { "VariableDeclarator": ::: correct ```js -/*eslint indent: ["error", 2, { "VariableDeclarator": 2 }]*/ +/*eslint indent-legacy: ["error", 2, { "VariableDeclarator": 2 }]*/ /*eslint-env es6*/ var a, b, c; -let a, - b, - c; -const a = 1, - b = 2, - c = 3; +let d, + e, + f; +const g = 1, + h = 2, + i = 3; ``` ::: @@ -261,18 +264,18 @@ Examples of **correct** code for this rule with the `2, { "VariableDeclarator": ::: correct ```js -/*eslint indent: ["error", 2, { "VariableDeclarator": { "var": 2, "let": 2, "const": 3 } }]*/ +/*eslint indent-legacy: ["error", 2, { "VariableDeclarator": { "var": 2, "let": 2, "const": 3 } }]*/ /*eslint-env es6*/ var a, b, c; -let a, - b, - c; -const a = 1, - b = 2, - c = 3; +let e, + f, + g; +const h = 1, + i = 2, + j = 3; ``` ::: @@ -284,7 +287,7 @@ Examples of **incorrect** code for this rule with the options `2, { "outerIIFEBo ::: incorrect ```js -/*eslint indent: ["error", 2, { "outerIIFEBody": 0 }]*/ +/*eslint indent-legacy: ["error", 2, { "outerIIFEBody": 0 }]*/ (function() { @@ -306,7 +309,7 @@ Examples of **correct** code for this rule with the options `2, {"outerIIFEBody" ::: correct ```js -/*eslint indent: ["error", 2, { "outerIIFEBody": 0 }]*/ +/*eslint indent-legacy: ["error", 2, { "outerIIFEBody": 0 }]*/ (function() { @@ -330,7 +333,7 @@ Examples of **incorrect** code for this rule with the `2, { "MemberExpression": ::: incorrect ```js -/*eslint indent: ["error", 2, { "MemberExpression": 1 }]*/ +/*eslint indent-legacy: ["error", 2, { "MemberExpression": 1 }]*/ foo .bar @@ -344,7 +347,7 @@ Examples of **correct** code for this rule with the `2, { "MemberExpression": 1 ::: correct ```js -/*eslint indent: ["error", 2, { "MemberExpression": 1 }]*/ +/*eslint indent-legacy: ["error", 2, { "MemberExpression": 1 }]*/ foo .bar @@ -364,7 +367,7 @@ Examples of **incorrect** code for this rule with the `2, { "FunctionDeclaration ::: incorrect ```js -/*eslint indent: ["error", 2, { "FunctionDeclaration": {"body": 1, "parameters": 2} }]*/ +/*eslint indent-legacy: ["error", 2, { "FunctionDeclaration": {"body": 1, "parameters": 2} }]*/ function foo(bar, baz, @@ -380,7 +383,7 @@ Examples of **correct** code for this rule with the `2, { "FunctionDeclaration": ::: correct ```js -/*eslint indent: ["error", 2, { "FunctionDeclaration": {"body": 1, "parameters": 2} }]*/ +/*eslint indent-legacy: ["error", 2, { "FunctionDeclaration": {"body": 1, "parameters": 2} }]*/ function foo(bar, baz, @@ -396,7 +399,7 @@ Examples of **incorrect** code for this rule with the `2, { "FunctionDeclaration ::: incorrect ```js -/*eslint indent: ["error", 2, {"FunctionDeclaration": {"parameters": "first"}}]*/ +/*eslint indent-legacy: ["error", 2, {"FunctionDeclaration": {"parameters": "first"}}]*/ function foo(bar, baz, qux, boop) { @@ -411,7 +414,7 @@ Examples of **correct** code for this rule with the `2, { "FunctionDeclaration": ::: correct ```js -/*eslint indent: ["error", 2, {"FunctionDeclaration": {"parameters": "first"}}]*/ +/*eslint indent-legacy: ["error", 2, {"FunctionDeclaration": {"parameters": "first"}}]*/ function foo(bar, baz, qux, boop) { @@ -428,7 +431,7 @@ Examples of **incorrect** code for this rule with the `2, { "FunctionExpression" ::: incorrect ```js -/*eslint indent: ["error", 2, { "FunctionExpression": {"body": 1, "parameters": 2} }]*/ +/*eslint indent-legacy: ["error", 2, { "FunctionExpression": {"body": 1, "parameters": 2} }]*/ var foo = function(bar, baz, @@ -444,7 +447,7 @@ Examples of **correct** code for this rule with the `2, { "FunctionExpression": ::: correct ```js -/*eslint indent: ["error", 2, { "FunctionExpression": {"body": 1, "parameters": 2} }]*/ +/*eslint indent-legacy: ["error", 2, { "FunctionExpression": {"body": 1, "parameters": 2} }]*/ var foo = function(bar, baz, @@ -460,7 +463,7 @@ Examples of **incorrect** code for this rule with the `2, { "FunctionExpression" ::: incorrect ```js -/*eslint indent: ["error", 2, {"FunctionExpression": {"parameters": "first"}}]*/ +/*eslint indent-legacy: ["error", 2, {"FunctionExpression": {"parameters": "first"}}]*/ var foo = function(bar, baz, qux, boop) { @@ -475,7 +478,7 @@ Examples of **correct** code for this rule with the `2, { "FunctionExpression": ::: correct ```js -/*eslint indent: ["error", 2, {"FunctionExpression": {"parameters": "first"}}]*/ +/*eslint indent-legacy: ["error", 2, {"FunctionExpression": {"parameters": "first"}}]*/ var foo = function(bar, baz, qux, boop) { @@ -492,7 +495,7 @@ Examples of **incorrect** code for this rule with the `2, { "CallExpression": {" ::: incorrect ```js -/*eslint indent: ["error", 2, { "CallExpression": {"arguments": 1} }]*/ +/*eslint indent-legacy: ["error", 2, { "CallExpression": {"arguments": 1} }]*/ foo(bar, baz, @@ -507,7 +510,7 @@ Examples of **correct** code for this rule with the `2, { "CallExpression": {"ar ::: correct ```js -/*eslint indent: ["error", 2, { "CallExpression": {"arguments": 1} }]*/ +/*eslint indent-legacy: ["error", 2, { "CallExpression": {"arguments": 1} }]*/ foo(bar, baz, @@ -522,7 +525,7 @@ Examples of **incorrect** code for this rule with the `2, { "CallExpression": {" ::: incorrect ```js -/*eslint indent: ["error", 2, {"CallExpression": {"arguments": "first"}}]*/ +/*eslint indent-legacy: ["error", 2, {"CallExpression": {"arguments": "first"}}]*/ foo(bar, baz, baz, boop, beep); @@ -535,7 +538,7 @@ Examples of **correct** code for this rule with the `2, { "CallExpression": {"ar ::: correct ```js -/*eslint indent: ["error", 2, {"CallExpression": {"arguments": "first"}}]*/ +/*eslint indent-legacy: ["error", 2, {"CallExpression": {"arguments": "first"}}]*/ foo(bar, baz, baz, boop, beep); @@ -550,7 +553,7 @@ Examples of **incorrect** code for this rule with the `2, { "ArrayExpression": 1 ::: incorrect ```js -/*eslint indent: ["error", 2, { "ArrayExpression": 1 }]*/ +/*eslint indent-legacy: ["error", 2, { "ArrayExpression": 1 }]*/ var foo = [ bar, @@ -566,7 +569,7 @@ Examples of **correct** code for this rule with the `2, { "ArrayExpression": 1 } ::: correct ```js -/*eslint indent: ["error", 2, { "ArrayExpression": 1 }]*/ +/*eslint indent-legacy: ["error", 2, { "ArrayExpression": 1 }]*/ var foo = [ bar, @@ -582,7 +585,7 @@ Examples of **incorrect** code for this rule with the `2, { "ArrayExpression": " ::: incorrect ```js -/*eslint indent: ["error", 2, {"ArrayExpression": "first"}]*/ +/*eslint indent-legacy: ["error", 2, {"ArrayExpression": "first"}]*/ var foo = [bar, baz, @@ -597,7 +600,7 @@ Examples of **correct** code for this rule with the `2, { "ArrayExpression": "fi ::: correct ```js -/*eslint indent: ["error", 2, {"ArrayExpression": "first"}]*/ +/*eslint indent-legacy: ["error", 2, {"ArrayExpression": "first"}]*/ var foo = [bar, baz, @@ -614,7 +617,7 @@ Examples of **incorrect** code for this rule with the `2, { "ObjectExpression": ::: incorrect ```js -/*eslint indent: ["error", 2, { "ObjectExpression": 1 }]*/ +/*eslint indent-legacy: ["error", 2, { "ObjectExpression": 1 }]*/ var foo = { bar: 1, @@ -630,7 +633,7 @@ Examples of **correct** code for this rule with the `2, { "ObjectExpression": 1 ::: correct ```js -/*eslint indent: ["error", 2, { "ObjectExpression": 1 }]*/ +/*eslint indent-legacy: ["error", 2, { "ObjectExpression": 1 }]*/ var foo = { bar: 1, @@ -646,7 +649,7 @@ Examples of **incorrect** code for this rule with the `2, { "ObjectExpression": ::: incorrect ```js -/*eslint indent: ["error", 2, {"ObjectExpression": "first"}]*/ +/*eslint indent-legacy: ["error", 2, {"ObjectExpression": "first"}]*/ var foo = { bar: 1, baz: 2 }; @@ -659,7 +662,7 @@ Examples of **correct** code for this rule with the `2, { "ObjectExpression": "f ::: correct ```js -/*eslint indent: ["error", 2, {"ObjectExpression": "first"}]*/ +/*eslint indent-legacy: ["error", 2, {"ObjectExpression": "first"}]*/ var foo = { bar: 1, baz: 2 }; diff --git a/docs/src/rules/indent.md b/docs/src/rules/indent.md index 7f81c0fd57b..6cd25b73aec 100644 --- a/docs/src/rules/indent.md +++ b/docs/src/rules/indent.md @@ -3,7 +3,7 @@ title: indent rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). There are several common guidelines which require specific indentation of nested blocks and statements, like: @@ -141,20 +141,23 @@ function foo(d) { Examples of **correct** code for this rule with the `"tab"` option: + + ::: correct ```js /*eslint indent: ["error", "tab"]*/ if (a) { -/*tab*/b=c; -/*tab*/function foo(d) { -/*tab*//*tab*/e=f; -/*tab*/} + b=c; + function foo(d) { + e=f; + } } ``` ::: + ### ignoredNodes @@ -192,7 +195,7 @@ Examples of **correct** code for this rule with the `4, { "ignoredNodes": ["Call foo(); bar(); -}) +})(); ``` ::: @@ -248,12 +251,12 @@ Examples of **incorrect** code for this rule with the `2, { "VariableDeclarator" var a, b, c; -let a, - b, - c; -const a = 1, - b = 2, - c = 3; +let d, + e, + f; +const g = 1, + h = 2, + i = 3; ``` ::: @@ -269,12 +272,12 @@ Examples of **correct** code for this rule with the `2, { "VariableDeclarator": var a, b, c; -let a, - b, - c; -const a = 1, - b = 2, - c = 3; +let d, + e, + f; +const g = 1, + h = 2, + i = 3; ``` ::: @@ -290,12 +293,12 @@ Examples of **correct** code for this rule with the `2, { "VariableDeclarator": var a, b, c; -let a, - b, - c; -const a = 1, - b = 2, - c = 3; +let d, + e, + f; +const g = 1, + h = 2, + i = 3; ``` ::: @@ -311,12 +314,12 @@ Examples of **incorrect** code for this rule with the `2, { "VariableDeclarator" var a, b, c; -let a, - b, - c; -const a = 1, - b = 2, - c = 3; +let d, + e, + f; +const g = 1, + h = 2, + i = 3; ``` ::: @@ -332,12 +335,12 @@ Examples of **correct** code for this rule with the `2, { "VariableDeclarator": var a, b, c; -let a, - b, - c; -const a = 1, - b = 2, - c = 3; +let d, + e, + f; +const g = 1, + h = 2, + i = 3; ``` ::: @@ -353,12 +356,12 @@ Examples of **correct** code for this rule with the `2, { "VariableDeclarator": var a, b, c; -let a, - b, - c; -const a = 1, - b = 2, - c = 3; +let d, + e, + f; +const g = 1, + h = 2, + i = 3; ``` ::: @@ -403,7 +406,7 @@ function foo(x) { })(); if (y) { - console.log('foo'); + console.log('foo'); } ``` @@ -858,6 +861,14 @@ import { foo, bar, baz, } from 'qux'; +``` + +::: + +::: correct + +```js +/*eslint indent: ["error", 4, { "ImportDeclaration": 1 }]*/ import { foo, diff --git a/docs/src/rules/jsx-quotes.md b/docs/src/rules/jsx-quotes.md index 4f0d78ba78a..b26450a9bc4 100644 --- a/docs/src/rules/jsx-quotes.md +++ b/docs/src/rules/jsx-quotes.md @@ -5,21 +5,21 @@ related_rules: - quotes --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). JSX attribute values can contain string literals, which are delimited with single or double quotes. -```xml - - +```jsx +; +; ``` Unlike string literals in JavaScript, string literals within JSX attributes can’t contain escaped quotes. If you want to have e.g. a double quote within a JSX attribute value, you have to use single quotes as string delimiter. -```xml - - +```jsx +; +; ``` ## Rule Details @@ -37,25 +37,25 @@ This rule has a string option: Examples of **incorrect** code for this rule with the default `"prefer-double"` option: -:::incorrect +:::incorrect { "ecmaFeatures": { "jsx": true } } -```xml +```jsx /*eslint jsx-quotes: ["error", "prefer-double"]*/ - +; ``` ::: Examples of **correct** code for this rule with the default `"prefer-double"` option: -:::correct +:::correct { "ecmaFeatures": { "jsx": true } } -```xml +```jsx /*eslint jsx-quotes: ["error", "prefer-double"]*/ - - +; +; ``` ::: @@ -64,25 +64,25 @@ Examples of **correct** code for this rule with the default `"prefer-double"` op Examples of **incorrect** code for this rule with the `"prefer-single"` option: -:::incorrect +:::incorrect { "ecmaFeatures": { "jsx": true } } -```xml +```jsx /*eslint jsx-quotes: ["error", "prefer-single"]*/ - +; ``` ::: Examples of **correct** code for this rule with the `"prefer-single"` option: -:::correct +:::correct { "ecmaFeatures": { "jsx": true } } -```xml +```jsx /*eslint jsx-quotes: ["error", "prefer-single"]*/ - - +; +; ``` ::: diff --git a/docs/src/rules/key-spacing.md b/docs/src/rules/key-spacing.md index d6e500053cf..234b0de1b21 100644 --- a/docs/src/rules/key-spacing.md +++ b/docs/src/rules/key-spacing.md @@ -3,7 +3,7 @@ title: key-spacing rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). This rule enforces spacing around the colon in object literal properties. It can verify each property individually, or it can ensure horizontal alignment of adjacent properties in an object literal. diff --git a/docs/src/rules/keyword-spacing.md b/docs/src/rules/keyword-spacing.md index a77e3ddc420..7510378dea0 100644 --- a/docs/src/rules/keyword-spacing.md +++ b/docs/src/rules/keyword-spacing.md @@ -3,7 +3,7 @@ title: keyword-spacing rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Keywords are syntax elements of JavaScript, such as `try` and `if`. These keywords have special meaning to the language and so often appear in a different color in code editors. @@ -58,9 +58,9 @@ if (foo) { Examples of **correct** code for this rule with the default `{ "before": true }` option: -::: correct +::: correct { "ecmaFeatures": { "jsx": true } } -```js +```jsx /*eslint keyword-spacing: ["error", { "before": true }]*/ /*eslint-env es6*/ @@ -77,30 +77,30 @@ let a = [this]; let b = [function() {}]; // Avoid conflict with `arrow-spacing` -let a = ()=> this.foo; +let c = ()=> this.foo; // Avoid conflict with `block-spacing` {function foo() {}} // Avoid conflict with `comma-spacing` -let a = [100,this.foo, this.bar]; +let d = [100,this.foo, this.bar]; // Avoid conflict with `computed-property-spacing` obj[this.foo] = 0; // Avoid conflict with `generator-star-spacing` -function *foo() {} +function *bar() {} // Avoid conflict with `key-spacing` -let obj = { +let obj1 = { foo:function() {} }; // Avoid conflict with `object-curly-spacing` -let obj = {foo: this}; +let obj2 = {foo: this}; // Avoid conflict with `semi-spacing` -let a = this;function foo() {} +let e = this;function foo() {} // Avoid conflict with `space-in-parens` (function () {})(); @@ -110,7 +110,7 @@ if ("foo"in {foo: 0}) {} if (10+this.foo<= this.bar) {} // Avoid conflict with `jsx-curly-spacing` -let a = +let f = ``` ::: @@ -173,9 +173,9 @@ if(foo) { Examples of **correct** code for this rule with the default `{ "after": true }` option: -::: correct +::: correct { "ecmaFeatures": { "jsx": true } } -```js +```jsx /*eslint keyword-spacing: ["error", { "after": true }]*/ if (foo) { @@ -190,10 +190,10 @@ if (foo) { let a = [this]; // Avoid conflict with `arrow-spacing` -let a = ()=> this.foo; +let b = ()=> this.foo; // Avoid conflict with `comma-spacing` -let a = [100, this.foo, this.bar]; +let c = [100, this.foo, this.bar]; // Avoid conflict with `computed-property-spacing` obj[this.foo] = 0; @@ -202,42 +202,42 @@ obj[this.foo] = 0; function* foo() {} // Avoid conflict with `key-spacing` -let obj = { +let obj1 = { foo:function() {} }; // Avoid conflict with `func-call-spacing` -class A { +class A extends B { constructor() { super(); } } // Avoid conflict with `object-curly-spacing` -let obj = {foo: this}; +let obj2 = {foo: this}; // Avoid conflict with `semi-spacing` -let a = this;function foo() {} +let d = this;function bar() {} // Avoid conflict with `space-before-function-paren` -function() {} +(function() {})(); // Avoid conflict with `space-infix-ops` if ("foo"in{foo: 0}) {} if (10+this.foo<= this.bar) {} // Avoid conflict with `space-unary-ops` -function* foo(a) { +function* baz(a) { return yield+a; } // Avoid conflict with `yield-star-spacing` -function* foo(a) { +function* qux(a) { return yield* a; } // Avoid conflict with `jsx-curly-spacing` -let a = +let e = ``` ::: diff --git a/docs/src/rules/linebreak-style.md b/docs/src/rules/linebreak-style.md index 71158115f34..c4a764c9deb 100644 --- a/docs/src/rules/linebreak-style.md +++ b/docs/src/rules/linebreak-style.md @@ -3,7 +3,7 @@ title: linebreak-style rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). When developing with a lot of people all having different editors, VCS applications and operating systems it may occur that different line endings are written by either of the mentioned (might especially happen when using the windows and mac versions of SourceTree together). diff --git a/docs/src/rules/lines-around-comment.md b/docs/src/rules/lines-around-comment.md index 924ba66cf1a..56ea775ef44 100644 --- a/docs/src/rules/lines-around-comment.md +++ b/docs/src/rules/lines-around-comment.md @@ -6,7 +6,7 @@ related_rules: - spaced-comment --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Many style guides require empty lines before or after comments. The primary goal of these rules is to make the comments easier to read and improve readability of the code. @@ -663,7 +663,7 @@ Examples of **correct** code for the `ignorePattern` option: /*eslint lines-around-comment: ["error"]*/ foo(); -/* eslint mentioned in this comment */, +/* eslint mentioned in this comment */ bar(); /*eslint lines-around-comment: ["error", { "ignorePattern": "pragma" }] */ diff --git a/docs/src/rules/lines-around-directive.md b/docs/src/rules/lines-around-directive.md index fb71775c7f1..7c18b517446 100644 --- a/docs/src/rules/lines-around-directive.md +++ b/docs/src/rules/lines-around-directive.md @@ -60,19 +60,13 @@ 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"] */ -/* Top of file */ -"use strict"; -var foo; - -/* Top of file */ // comment "use strict"; -"use asm"; var foo; function foo() { @@ -90,23 +84,29 @@ function foo() { ::: -Examples of **correct** code for this rule with the `"always"` option: - -::: correct +::: incorrect { "sourceType": "script" } ```js /* eslint lines-around-directive: ["error", "always"] */ -/* Top of file */ +// comment "use strict"; - +"use asm"; var foo; +``` + +::: + +Examples of **correct** code for this rule with the `"always"` option: + +::: correct { "sourceType": "script" } + +```js +/* eslint lines-around-directive: ["error", "always"] */ -/* Top of file */ // comment "use strict"; -"use asm"; var foo; @@ -128,26 +128,33 @@ function foo() { ::: -### never - -Examples of **incorrect** code for this rule with the `"never"` option: - -::: incorrect +::: correct { "sourceType": "script" } ```js -/* eslint lines-around-directive: ["error", "never"] */ +/* eslint lines-around-directive: ["error", "always"] */ -/* Top of file */ +// comment "use strict"; +"use asm"; var foo; +``` + +::: + +### never + +Examples of **incorrect** code for this rule with the `"never"` option: + +::: incorrect { "sourceType": "script" } + +```js +/* eslint lines-around-directive: ["error", "never"] */ -/* Top of file */ // comment "use strict"; -"use asm"; var foo; @@ -169,21 +176,30 @@ function foo() { ::: -Examples of **correct** code for this rule with the `"never"` option: - -::: correct +::: incorrect { "sourceType": "script" } ```js /* eslint lines-around-directive: ["error", "never"] */ -/* Top of file */ +// comment + "use strict"; +"use asm"; + var foo; +``` + +::: + +Examples of **correct** code for this rule with the `"never"` option: + +::: correct { "sourceType": "script" } + +```js +/* eslint lines-around-directive: ["error", "never"] */ -/* Top of file */ // comment "use strict"; -"use asm"; var foo; function foo() { @@ -201,25 +217,31 @@ function foo() { ::: +::: correct { "sourceType": "script" } + +```js +/* eslint lines-around-directive: ["error", "never"] */ + +// comment +"use strict"; +"use asm"; +var foo; +``` + +::: + ### before & after 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" }] */ -/* Top of file */ - -"use strict"; -var foo; - -/* Top of file */ // comment "use strict"; -"use asm"; var foo; function foo() { @@ -238,22 +260,29 @@ function foo() { ::: -Examples of **correct** code for this rule with the `{ "before": "never", "after": "always" }` option: - -::: correct +::: incorrect { "sourceType": "script" } ```js /* eslint lines-around-directive: ["error", { "before": "never", "after": "always" }] */ -/* Top of file */ -"use strict"; +// comment +"use strict"; +"use asm"; var foo; +``` + +::: + +Examples of **correct** code for this rule with the `{ "before": "never", "after": "always" }` option: + +::: correct { "sourceType": "script" } + +```js +/* eslint lines-around-directive: ["error", { "before": "never", "after": "always" }] */ -/* Top of file */ // comment "use strict"; -"use asm"; var foo; @@ -274,22 +303,29 @@ function foo() { ::: -Examples of **incorrect** code for this rule with the `{ "before": "always", "after": "never" }` option: - -::: incorrect +::: correct { "sourceType": "script" } ```js -/* eslint lines-around-directive: ["error", { "before": "always", "after": "never" }] */ +/* eslint lines-around-directive: ["error", { "before": "never", "after": "always" }] */ -/* Top of file */ +// comment "use strict"; +"use asm"; var foo; +``` + +::: + +Examples of **incorrect** code for this rule with the `{ "before": "always", "after": "never" }` option: + +::: incorrect { "sourceType": "script" } + +```js +/* eslint lines-around-directive: ["error", { "before": "always", "after": "never" }] */ -/* Top of file */ // comment "use strict"; -"use asm"; var foo; @@ -310,22 +346,30 @@ function foo() { ::: -Examples of **correct** code for this rule with the `{ "before": "always", "after": "never" }` option: - -::: correct +::: incorrect { "sourceType": "script" } ```js /* eslint lines-around-directive: ["error", { "before": "always", "after": "never" }] */ -/* Top of file */ +// comment "use strict"; +"use asm"; + var foo; +``` + +::: + +Examples of **correct** code for this rule with the `{ "before": "always", "after": "never" }` option: + +::: correct { "sourceType": "script" } + +```js +/* eslint lines-around-directive: ["error", { "before": "always", "after": "never" }] */ -/* Top of file */ // comment "use strict"; -"use asm"; var foo; function foo() { @@ -344,6 +388,20 @@ function foo() { ::: +::: correct { "sourceType": "script" } + +```js +/* eslint lines-around-directive: ["error", { "before": "always", "after": "never" }] */ + +// comment + +"use strict"; +"use asm"; +var foo; +``` + +::: + ## When Not To Use It You can safely disable this rule if you do not have any strict conventions about whether or not directive prologues should have blank newlines before or after them. diff --git a/docs/src/rules/lines-between-class-members.md b/docs/src/rules/lines-between-class-members.md index 8daf2a2f78d..9c9d7e0cbad 100644 --- a/docs/src/rules/lines-between-class-members.md +++ b/docs/src/rules/lines-between-class-members.md @@ -6,7 +6,7 @@ related_rules: - padding-line-between-statements --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). This rule improves readability by enforcing lines between class members. It will not check empty lines before the first member and after the last member, since that is already taken care of by padded-blocks. @@ -69,14 +69,19 @@ class MyClass { ### Options -This rule has a string option and an object option. +This rule has two options, first option can be string or object, second option is object. -String option: +First option can be string `"always"` or `"never"` or an object with a property named `enforce`: * `"always"`(default) require an empty line after class members * `"never"` disallows an empty line after class members +* `Object`: An object with a property named `enforce`. The enforce property should be an array of objects, each specifying the configuration for enforcing empty lines between specific pairs of class members. + * **enforce**: You can supply any number of configurations. If a member pair matches multiple configurations, the last matched configuration will be used. If a member pair does not match any configurations, it will be ignored. Each object should have the following properties: + * **blankLine**: Can be set to either `"always"` or `"never"`, indicating whether a blank line should be required or disallowed between the specified members. + * **prev**: Specifies the type of the preceding class member. It can be `"method"` for class methods, `"field"` for class fields, or `"*"` for any class member. + * **next**: Specifies the type of the following class member. It follows the same options as `prev`. -Object option: +Second option is an object with a property named `exceptAfterSingleLine`: * `"exceptAfterSingleLine": false`(default) **do not** skip checking empty lines after single-line class members * `"exceptAfterSingleLine": true` skip checking empty lines after single-line class members @@ -92,9 +97,15 @@ class Foo{ bar(){} baz(){} } +``` + +::: + +::: incorrect +```js /* eslint lines-between-class-members: ["error", "never"]*/ -class Foo{ +class Bar{ x; bar(){} @@ -118,9 +129,15 @@ class Foo{ baz(){} } +``` +::: + +::: correct + +```js /* eslint lines-between-class-members: ["error", "never"]*/ -class Foo{ +class Bar{ x; bar(){} baz(){} @@ -129,6 +146,146 @@ class Foo{ ::: +Examples of **incorrect** code for this rule with the array of configurations option: + +::: incorrect + +```js +// disallows blank lines between methods +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + }, +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} +} +``` + +::: + +::: incorrect + +```js +// requires blank lines around fields, disallows blank lines between methods +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "always", prev: "*", next: "field" }, + { blankLine: "always", prev: "field", next: "*" }, + { blankLine: "never", prev: "method", next: "method" } + ] + }, +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} +} +``` + +::: + +Examples of **correct** code for this rule with the array of configurations option: + +::: correct + +```js +// disallows blank lines between methods +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + }, +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} +} +``` + +::: + +::: correct + +```js +// requires blank lines around fields, disallows blank lines between methods +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "always", prev: "*", next: "field" }, + { blankLine: "always", prev: "field", next: "*" }, + { blankLine: "never", prev: "method", next: "method" } + ] + }, +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} +} +``` + +::: + Examples of **correct** code for this rule with the object option: ::: correct @@ -148,6 +305,40 @@ class Foo{ ::: +::: correct + +```js +/*eslint lines-between-class-members: [ + "error", + { + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + }, + { exceptAfterSingleLine: true } +]*/ + +class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + + method2() {} +} +``` + +::: + ## When Not To Use It If you don't want to enforce empty lines between class members, you can disable this rule. diff --git a/docs/src/rules/logical-assignment-operators.md b/docs/src/rules/logical-assignment-operators.md index 3f9350031f3..1b59cfa70bb 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 ``` ::: @@ -96,10 +104,10 @@ a = a ?? b This option checks for additional patterns with if statements which could be expressed with the logical assignment operator. -::: incorrect - Examples of **incorrect** code for this rule with the `["always", { enforceForIfStatements: true }]` option: +::: incorrect + ```js /*eslint logical-assignment-operators: ["error", "always", { enforceForIfStatements: true }]*/ diff --git a/docs/src/rules/max-len.md b/docs/src/rules/max-len.md index be883a8ca0e..7f7a0d006c0 100644 --- a/docs/src/rules/max-len.md +++ b/docs/src/rules/max-len.md @@ -9,6 +9,7 @@ related_rules: - max-statements --- +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Very long lines of code in any language can be difficult to read. In order to aid in readability and maintainability many coders have developed a convention to limit lines of code to X number of characters (traditionally 80 characters). @@ -22,7 +23,7 @@ This rule enforces a maximum line length to increase code readability and mainta ## Options -This rule has a number or object option: +This rule can have up to two numbers as positional arguments (for `code` and `tabWidth` options), followed by an object option (provided positional arguments have priority): * `"code"` (default `80`) enforces a maximum line length * `"tabWidth"` (default `4`) specifies the character width for tab characters @@ -69,30 +70,36 @@ var foo = { Examples of **incorrect** code for this rule with the default `{ "tabWidth": 4 }` option: + + ::: incorrect ```js /*eslint max-len: ["error", { "code": 80, "tabWidth": 4 }]*/ -\t \t var foo = { "bar": "This is a bar.", "baz": { "qux": "This is a qux" } }; + var foo = { "bar": "This is a bar.", "baz": { "qux": "This is a qux" } }; ``` ::: + Examples of **correct** code for this rule with the default `{ "tabWidth": 4 }` option: + + ::: correct ```js /*eslint max-len: ["error", { "code": 80, "tabWidth": 4 }]*/ -\t \t var foo = { -\t \t \t \t "bar": "This is a bar.", -\t \t \t \t "baz": { "qux": "This is a qux" } -\t \t }; + var foo = { + "bar": "This is a bar.", + "baz": { "qux": "This is a qux" } + }; ``` ::: + ### comments @@ -203,7 +210,8 @@ Examples of **correct** code for this rule with the `ignorePattern` option: ::: correct ```js -/*eslint max-len: ["error", { "ignorePattern": "^\\s*var\\s.+=\\s*require\\s*\\(" }]*/ +/*eslint max-len: +["error", { "ignorePattern": "^\\s*var\\s.+=\\s*require\\s*\\(" }]*/ var dep = require('really/really/really/really/really/really/really/really/long/module'); ``` diff --git a/docs/src/rules/max-params.md b/docs/src/rules/max-params.md index 681723fb811..b0a55e9009f 100644 --- a/docs/src/rules/max-params.md +++ b/docs/src/rules/max-params.md @@ -42,11 +42,11 @@ Examples of **incorrect** code for this rule with the default `{ "max": 3 }` opt /*eslint max-params: ["error", 3]*/ /*eslint-env es6*/ -function foo (bar, baz, qux, qxx) { +function foo1 (bar, baz, qux, qxx) { doSomething(); } -let foo = (bar, baz, qux, qxx) => { +let foo2 = (bar, baz, qux, qxx) => { doSomething(); }; ``` @@ -61,11 +61,11 @@ Examples of **correct** code for this rule with the default `{ "max": 3 }` optio /*eslint max-params: ["error", 3]*/ /*eslint-env es6*/ -function foo (bar, baz, qux) { +function foo1 (bar, baz, qux) { doSomething(); } -let foo = (bar, baz, qux) => { +let foo2 = (bar, baz, qux) => { doSomething(); }; ``` diff --git a/docs/src/rules/max-statements-per-line.md b/docs/src/rules/max-statements-per-line.md index 238ade787e8..1a579add6fb 100644 --- a/docs/src/rules/max-statements-per-line.md +++ b/docs/src/rules/max-statements-per-line.md @@ -11,6 +11,7 @@ related_rules: - max-statements --- +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). A line of code containing too many statements can be difficult to read. Code is generally read from the top down, especially when scanning, so limiting the number of statements allowed on a single line can be very beneficial for readability and maintainability. @@ -40,7 +41,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 +59,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 +77,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; baz = 2; }; (function foo() { bar = 1; baz = 2; })(); ``` @@ -94,7 +95,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/max-statements.md b/docs/src/rules/max-statements.md index d41f7f2de4d..5bdc7e8c2f4 100644 --- a/docs/src/rules/max-statements.md +++ b/docs/src/rules/max-statements.md @@ -63,7 +63,7 @@ function foo() { var foo11 = 11; // Too many. } -let foo = () => { +let bar = () => { var foo1 = 1; var foo2 = 2; var foo3 = 3; @@ -99,17 +99,18 @@ function foo() { var foo7 = 7; var foo8 = 8; var foo9 = 9; - var foo10 = 10; - return function () { + return function () { // 10 // The number of statements in the inner function does not count toward the // statement maximum. + var bar; + var baz; return 42; }; } -let foo = () => { +let bar = () => { var foo1 = 1; var foo2 = 2; var foo3 = 3; @@ -119,12 +120,13 @@ let foo = () => { var foo7 = 7; var foo8 = 8; var foo9 = 9; - var foo10 = 10; - return function () { + return function () { // 10 // The number of statements in the inner function does not count toward the // statement maximum. + var bar; + var baz; return 42; }; } diff --git a/docs/src/rules/multiline-ternary.md b/docs/src/rules/multiline-ternary.md index 7e4688d637a..12ed8e176b5 100644 --- a/docs/src/rules/multiline-ternary.md +++ b/docs/src/rules/multiline-ternary.md @@ -5,7 +5,7 @@ related_rules: - operator-linebreak --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). JavaScript allows operands of ternary expressions to be separated by newlines, which can improve the readability of your program. @@ -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/new-parens.md b/docs/src/rules/new-parens.md index 08292e8a978..0f652c2eb34 100644 --- a/docs/src/rules/new-parens.md +++ b/docs/src/rules/new-parens.md @@ -3,7 +3,7 @@ title: new-parens rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). JavaScript allows the omission of parentheses when invoking a function via the `new` keyword and the constructor has no arguments. However, some coders believe that omitting the parentheses is inconsistent with the rest of the language and thus makes code less clear. diff --git a/docs/src/rules/newline-after-var.md b/docs/src/rules/newline-after-var.md index db11e25471c..9d6dc216ece 100644 --- a/docs/src/rules/newline-after-var.md +++ b/docs/src/rules/newline-after-var.md @@ -52,9 +52,9 @@ var greet = "hello,", name = "world"; console.log(greet, name); -let greet = "hello,", - name = "world"; -console.log(greet, name); +let hello = "hello,", + world = "world"; +console.log(hello, world); var greet = "hello,"; const NAME = "world"; @@ -81,10 +81,10 @@ var greet = "hello,", console.log(greet, name); -let greet = "hello,", - name = "world"; +let hello = "hello,", + world = "world"; -console.log(greet, name); +console.log(hello, world); var greet = "hello,"; const NAME = "world"; @@ -115,10 +115,10 @@ var greet = "hello,", console.log(greet, name); -let greet = "hello,", - name = "world"; +let hello = "hello,", + world = "world"; -console.log(greet, name); +console.log(hello, world); var greet = "hello,"; const NAME = "world"; @@ -146,9 +146,9 @@ var greet = "hello,", name = "world"; console.log(greet, name); -let greet = "hello,", - name = "world"; -console.log(greet, name); +let hello = "hello,", + world = "world"; +console.log(hello, world); var greet = "hello,"; const NAME = "world"; diff --git a/docs/src/rules/newline-before-return.md b/docs/src/rules/newline-before-return.md index 41fb7726c7b..1980f5bd346 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/newline-per-chained-call.md b/docs/src/rules/newline-per-chained-call.md index e2482cdbc20..34eddb31338 100644 --- a/docs/src/rules/newline-per-chained-call.md +++ b/docs/src/rules/newline-per-chained-call.md @@ -3,7 +3,7 @@ title: newline-per-chained-call rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Chained method calls on a single line without line breaks are harder to read, so some developers place a newline character after each method call in the chain to make it more readable and easy to maintain. diff --git a/docs/src/rules/no-array-constructor.md b/docs/src/rules/no-array-constructor.md index 7a22df0e68a..aa7c079baa2 100644 --- a/docs/src/rules/no-array-constructor.md +++ b/docs/src/rules/no-array-constructor.md @@ -2,8 +2,8 @@ title: no-array-constructor rule_type: suggestion related_rules: -- no-new-object - no-new-wrappers +- no-object-constructor --- diff --git a/docs/src/rules/no-catch-shadow.md b/docs/src/rules/no-catch-shadow.md index 23328e42fef..c875f9fe4a9 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 b72c2d09c67..7cb02d7c32f 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-confusing-arrow.md b/docs/src/rules/no-confusing-arrow.md index 546e30321a8..b51140aa75f 100644 --- a/docs/src/rules/no-confusing-arrow.md +++ b/docs/src/rules/no-confusing-arrow.md @@ -6,7 +6,7 @@ related_rules: - arrow-parens --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Arrow functions (`=>`) are similar in syntax to some comparison operators (`>`, `<`, `<=`, and `>=`). This rule warns against using the arrow function syntax in places where it could be confused with a comparison operator. diff --git a/docs/src/rules/no-const-assign.md b/docs/src/rules/no-const-assign.md index f9f0ed172bb..ca62132a757 100644 --- a/docs/src/rules/no-const-assign.md +++ b/docs/src/rules/no-const-assign.md @@ -1,6 +1,7 @@ --- title: no-const-assign rule_type: problem +handled_by_typescript: true --- diff --git a/docs/src/rules/no-continue.md b/docs/src/rules/no-continue.md index af5cd8f98f1..fd0df533dec 100644 --- a/docs/src/rules/no-continue.md +++ b/docs/src/rules/no-continue.md @@ -15,7 +15,7 @@ for(i = 0; i < 10; i++) { continue; } - a += i; + sum += i; } ``` @@ -38,7 +38,7 @@ for(i = 0; i < 10; i++) { continue; } - a += i; + sum += i; } ``` @@ -57,7 +57,7 @@ labeledLoop: for(i = 0; i < 10; i++) { continue labeledLoop; } - a += i; + sum += i; } ``` @@ -75,7 +75,7 @@ var sum = 0, for(i = 0; i < 10; i++) { if(i < 5) { - a += i; + sum += i; } } ``` diff --git a/docs/src/rules/no-delete-var.md b/docs/src/rules/no-delete-var.md index 65a5aa1614d..f66aec5879f 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 79f791c4666..748d52c2e21 100644 --- a/docs/src/rules/no-dupe-args.md +++ b/docs/src/rules/no-dupe-args.md @@ -1,6 +1,7 @@ --- title: no-dupe-args rule_type: problem +handled_by_typescript: true --- @@ -15,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"*/ @@ -33,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-dupe-class-members.md b/docs/src/rules/no-dupe-class-members.md index aaea4ce324e..6565c9f15ee 100644 --- a/docs/src/rules/no-dupe-class-members.md +++ b/docs/src/rules/no-dupe-class-members.md @@ -1,6 +1,7 @@ --- title: no-dupe-class-members rule_type: problem +handled_by_typescript: true --- @@ -33,27 +34,27 @@ Examples of **incorrect** code for this rule: ```js /*eslint no-dupe-class-members: "error"*/ -class Foo { +class A { bar() { } bar() { } } -class Foo { +class B { bar() { } get bar() { } } -class Foo { +class C { bar; bar; } -class Foo { +class D { bar; bar() { } } -class Foo { +class E { static bar() { } static bar() { } } @@ -68,27 +69,27 @@ Examples of **correct** code for this rule: ```js /*eslint no-dupe-class-members: "error"*/ -class Foo { +class A { bar() { } qux() { } } -class Foo { +class B { get bar() { } set bar(value) { } } -class Foo { +class C { bar; qux; } -class Foo { +class D { bar; qux() { } } -class Foo { +class E { static bar() { } bar() { } } @@ -101,5 +102,3 @@ class Foo { This rule should not be used in ES3/5 environments. In ES2015 (ES6) or later, if you don't want to be notified about duplicate names in class members, you can safely disable this rule. - -It's also safe to disable this rule when using TypeScript because TypeScript's compiler already checks for duplicate function implementations. diff --git a/docs/src/rules/no-dupe-keys.md b/docs/src/rules/no-dupe-keys.md index 75fc9491fb6..1527bf8f1ec 100644 --- a/docs/src/rules/no-dupe-keys.md +++ b/docs/src/rules/no-dupe-keys.md @@ -1,6 +1,7 @@ --- title: no-dupe-keys rule_type: problem +handled_by_typescript: true --- diff --git a/docs/src/rules/no-else-return.md b/docs/src/rules/no-else-return.md index 917c4455390..79b4fe5fb01 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-character-class.md b/docs/src/rules/no-empty-character-class.md index 984f1e18330..2ab0679b394 100644 --- a/docs/src/rules/no-empty-character-class.md +++ b/docs/src/rules/no-empty-character-class.md @@ -24,6 +24,23 @@ Examples of **incorrect** code for this rule: /^abc[]/.test("abcdefg"); // false "abcdefg".match(/^abc[]/); // null + +/^abc[[]]/v.test("abcdefg"); // false +"abcdefg".match(/^abc[[]]/v); // null + +/^abc[[]--[x]]/v.test("abcdefg"); // false +"abcdefg".match(/^abc[[]--[x]]/v); // null + +/^abc[[d]&&[]]/v.test("abcdefg"); // false +"abcdefg".match(/^abc[[d]&&[]]/v); // null + +const regex = /^abc[d[]]/v; +regex.test("abcdefg"); // true, the nested `[]` has no effect +"abcdefg".match(regex); // ["abcd"] +regex.test("abcefg"); // false, the nested `[]` has no effect +"abcefg".match(regex); // null +regex.test("abc"); // false, the nested `[]` has no effect +"abc".match(regex); // null ``` ::: @@ -40,6 +57,9 @@ Examples of **correct** code for this rule: /^abc[a-z]/.test("abcdefg"); // true "abcdefg".match(/^abc[a-z]/); // ["abcd"] + +/^abc[^]/.test("abcdefg"); // true +"abcdefg".match(/^abc[^]/); // ["abcd"] ``` ::: diff --git a/docs/src/rules/no-empty-function.md b/docs/src/rules/no-empty-function.md index a9be4bb05d5..68aa2ad887b 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 d510f592d55..92000add185 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,51 @@ Examples of **correct** code for this rule: var {a = {}} = foo; var {a = []} = foo; function foo({a = {}}) {} -function foo({a = []}) {} +function bar({a = []}) {} +``` + +::: + +## Options + +This rule has an object option for exceptions: + +### allowObjectPatternsAsParameters + +Set to `false` by default. Setting this option to `true` allows empty object patterns as function parameters. + +**Note:** This rule doesn't allow empty array patterns as function parameters. + +Examples of **incorrect** code for this rule with the `{"allowObjectPatternsAsParameters": true}` option: + +::: incorrect + +```js +/*eslint no-empty-pattern: ["error", { "allowObjectPatternsAsParameters": true }]*/ + +function foo({a: {}}) {} +var bar = function({a: {}}) {}; +var bar = ({a: {}}) => {}; +var bar = ({} = bar) => {}; +var bar = ({} = { bar: 1 }) => {}; + +function baz([]) {} +``` + +::: + +Examples of **correct** code for this rule with the `{"allowObjectPatternsAsParameters": true}` option: + +::: correct + +```js +/*eslint no-empty-pattern: ["error", { "allowObjectPatternsAsParameters": true }]*/ + +function foo({}) {} +var bar = function({}) {}; +var bar = ({}) => {}; + +function baz({} = {}) {} ``` ::: diff --git a/docs/src/rules/no-empty-static-block.md b/docs/src/rules/no-empty-static-block.md index 283a4e21b77..1825e5c7832 100644 --- a/docs/src/rules/no-empty-static-block.md +++ b/docs/src/rules/no-empty-static-block.md @@ -42,7 +42,7 @@ class Foo { } } -class Foo { +class Bar { static { // comment } diff --git a/docs/src/rules/no-extra-boolean-cast.md b/docs/src/rules/no-extra-boolean-cast.md index e45509890f5..2408d617174 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 9b1b8df0725..ab5655b2b0a 100644 --- a/docs/src/rules/no-extra-parens.md +++ b/docs/src/rules/no-extra-parens.md @@ -9,7 +9,7 @@ further_reading: - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). This rule restricts the use of parentheses to only where they are necessary. @@ -21,6 +21,25 @@ This rule always ignores extra parentheses around the following: * immediately-invoked function expressions (also known as IIFEs) such as `var x = (function () {})();` and `var x = (function () {}());` to avoid conflicts with the [wrap-iife](wrap-iife) rule * arrow function arguments to avoid conflicts with the [arrow-parens](arrow-parens) rule +Problems reported by this rule can be fixed automatically, except when removing the parentheses would create a new directive, because that could change the semantics of the code. +For example, the following script prints `object` to the console, but if the parentheses around `"use strict"` were removed, it would print `undefined` instead. + +```js + + +("use strict"); + +function test() { + console.log(typeof this); +} + +test(); +``` + +In this case, the rule will not try to remove the parentheses around `"use strict"` but will still report them as a problem. + ## Options This rule has a string option: @@ -33,6 +52,7 @@ This rule has an object option for exceptions to the `"all"` option: * `"conditionalAssign": false` allows extra parentheses around assignments in conditional test expressions * `"returnAssign": false` allows extra parentheses around assignments in `return` statements * `"nestedBinaryExpressions": false` allows extra parentheses in nested binary expressions +* `"ternaryOperandBinaryExpressions": false` allows extra parentheses around binary expressions that are operands of ternary `?:` * `"ignoreJSX": "none|all|multi-line|single-line"` allows extra parentheses around no/all/multi-line/single-line JSX components. Defaults to `none`. * `"enforceForArrowConditionals": false` allows extra parentheses around ternary expressions which are the body of an arrow function * `"enforceForSequenceExpressions": false` allows extra parentheses around sequence expressions @@ -63,8 +83,6 @@ typeof (a); (Object.prototype.toString.call()); -(function(){} ? a() : b()); - class A { [(x)] = 1; } @@ -139,11 +157,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); } @@ -170,16 +188,36 @@ x = (a * b) / c; ::: -### ignoreJSX +### ternaryOperandBinaryExpressions -Examples of **correct** code for this rule with the `all` and `{ "ignoreJSX": "all" }` options: +Examples of **correct** code for this rule with the `"all"` and `{ "ternaryOperandBinaryExpressions": false }` options: ::: correct ```js +/* eslint no-extra-parens: ["error", "all", { "ternaryOperandBinaryExpressions": false }] */ + +(a && b) ? foo : bar; + +(a - b > a) ? foo : bar; + +foo ? (bar || baz) : qux; + +foo ? bar : (baz || qux); +``` + +::: + +### ignoreJSX + +Examples of **correct** code for this rule with the `all` and `{ "ignoreJSX": "all" }` options: + +::: correct { "ecmaFeatures": { "jsx": true } } + +```jsx /* eslint no-extra-parens: ["error", "all", { ignoreJSX: "all" }] */ -const Component = (
) -const Component = ( +const ThisComponent = (
) +const ThatComponent = (
@@ -190,28 +228,28 @@ const Component = ( Examples of **incorrect** code for this rule with the `all` and `{ "ignoreJSX": "multi-line" }` options: -::: incorrect +::: incorrect { "ecmaFeatures": { "jsx": true } } -```js +```jsx /* eslint no-extra-parens: ["error", "all", { ignoreJSX: "multi-line" }] */ -const Component = (
) -const Component = (

) +const ThisComponent = (
) +const ThatComponent = (

) ``` ::: Examples of **correct** code for this rule with the `all` and `{ "ignoreJSX": "multi-line" }` options: -::: correct +::: correct { "ecmaFeatures": { "jsx": true } } -```js +```jsx /* eslint no-extra-parens: ["error", "all", { ignoreJSX: "multi-line" }] */ -const Component = ( +const ThisComponent = (

) -const Component = ( +const ThatComponent = (
@@ -222,16 +260,16 @@ const Component = ( Examples of **incorrect** code for this rule with the `all` and `{ "ignoreJSX": "single-line" }` options: -::: incorrect +::: incorrect { "ecmaFeatures": { "jsx": true } } -```js +```jsx /* eslint no-extra-parens: ["error", "all", { ignoreJSX: "single-line" }] */ -const Component = ( +const ThisComponent = (

) -const Component = ( +const ThatComponent = (
@@ -242,12 +280,12 @@ const Component = ( Examples of **correct** code for this rule with the `all` and `{ "ignoreJSX": "single-line" }` options: -::: correct +::: correct { "ecmaFeatures": { "jsx": true } } -```js +```jsx /* eslint no-extra-parens: ["error", "all", { ignoreJSX: "single-line" }] */ -const Component = (
) -const Component = (

) +const ThisComponent = (
) +const ThatComponent = (

) ``` ::: diff --git a/docs/src/rules/no-extra-semi.md b/docs/src/rules/no-extra-semi.md index 9755c5167fd..1e9ee0a611f 100644 --- a/docs/src/rules/no-extra-semi.md +++ b/docs/src/rules/no-extra-semi.md @@ -6,9 +6,7 @@ related_rules: - semi-spacing --- - - - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Typing mistakes and misunderstandings about where semicolons are required can lead to semicolons that are unnecessary. While not technically an error, extra semicolons can cause confusion when reading code. @@ -16,6 +14,8 @@ Typing mistakes and misunderstandings about where semicolons are required can le This rule disallows unnecessary semicolons. +Problems reported by this rule can be fixed automatically, except when removing a semicolon would cause a following statement to become a directive such as `"use strict"`. + Examples of **incorrect** code for this rule: ::: incorrect diff --git a/docs/src/rules/no-floating-decimal.md b/docs/src/rules/no-floating-decimal.md index 30a43317d55..8ba7a799150 100644 --- a/docs/src/rules/no-floating-decimal.md +++ b/docs/src/rules/no-floating-decimal.md @@ -3,7 +3,7 @@ title: no-floating-decimal rule_type: suggestion --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Float values in JavaScript contain a decimal point, and there is no requirement that the decimal point be preceded or followed by a number. For example, the following are all valid JavaScript numbers: diff --git a/docs/src/rules/no-func-assign.md b/docs/src/rules/no-func-assign.md index ffbcb46c6b8..6346e2b8064 100644 --- a/docs/src/rules/no-func-assign.md +++ b/docs/src/rules/no-func-assign.md @@ -1,6 +1,7 @@ --- title: no-func-assign rule_type: problem +handled_by_typescript: true --- @@ -26,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() { @@ -60,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-implicit-globals.md b/docs/src/rules/no-implicit-globals.md index 1e5c5fad723..6a479a0a765 100644 --- a/docs/src/rules/no-implicit-globals.md +++ b/docs/src/rules/no-implicit-globals.md @@ -44,7 +44,7 @@ This rule disallows `var` and `function` declarations at the top-level script sc Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-implicit-globals: "error"*/ @@ -58,7 +58,7 @@ function bar() {} Examples of **correct** code for this rule: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-implicit-globals: "error"*/ @@ -79,7 +79,7 @@ window.bar = function() {}; Examples of **correct** code for this rule with `"parserOptions": { "sourceType": "module" }` in the ESLint configuration: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-implicit-globals: "error"*/ @@ -100,7 +100,7 @@ This does not apply to ES modules since the module code is implicitly in `strict Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-implicit-globals: "error"*/ @@ -127,7 +127,7 @@ or in a `/*global */` comment. Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-implicit-globals: "error"*/ @@ -155,7 +155,7 @@ If the variable is intended to be local to the script, wrap the code with a bloc Examples of **correct** code for this rule with `"lexicalBindings"` option set to `false` (default): -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-implicit-globals: ["error", {"lexicalBindings": false}]*/ @@ -171,7 +171,7 @@ class Bar {} Examples of **incorrect** code for this rule with `"lexicalBindings"` option set to `true`: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-implicit-globals: ["error", {"lexicalBindings": true}]*/ @@ -187,7 +187,7 @@ class Bar {} Examples of **correct** code for this rule with `"lexicalBindings"` option set to `true`: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-implicit-globals: ["error", {"lexicalBindings": true}]*/ @@ -221,7 +221,7 @@ Even the `typeof` check is not safe from TDZ reference exceptions. Examples of **incorrect** code for this rule with `"lexicalBindings"` option set to `true`: -::: incorrect +::: incorrect { "sourceType": "script" } ```js /*eslint no-implicit-globals: ["error", {"lexicalBindings": true}]*/ @@ -239,7 +239,7 @@ const MyGlobalFunction = (function() { Examples of **correct** code for this rule with `"lexicalBindings"` option set to `true`: -::: correct +::: correct { "sourceType": "script" } ```js /*eslint no-implicit-globals: ["error", {"lexicalBindings": true}]*/ @@ -261,7 +261,7 @@ You can use `/* exported variableName */` block comments in the same way as in [ Examples of **correct** code for `/* exported variableName */` operation: -::: correct +::: correct { "sourceType": "script" } ```js /* exported global_var */ diff --git a/docs/src/rules/no-import-assign.md b/docs/src/rules/no-import-assign.md index ca4b912de81..b0a7432bb80 100644 --- a/docs/src/rules/no-import-assign.md +++ b/docs/src/rules/no-import-assign.md @@ -1,6 +1,8 @@ --- title: no-import-assign rule_type: problem +handled_by_typescript: true +extra_typescript_info: Note that the compiler will not catch the `Object.assign()` case. Thus, if you use `Object.assign()` in your codebase, this rule will still provide some value. --- diff --git a/docs/src/rules/no-inline-comments.md b/docs/src/rules/no-inline-comments.md index 687464e91c7..d6a32db4e47 100644 --- a/docs/src/rules/no-inline-comments.md +++ b/docs/src/rules/no-inline-comments.md @@ -54,9 +54,9 @@ Comments inside the curly braces in JSX are allowed to be on the same line as th Examples of **incorrect** code for this rule: -::: incorrect +::: incorrect { "ecmaFeatures": { "jsx": true } } -```js +```jsx /*eslint no-inline-comments: "error"*/ var foo =
{ /* On the same line with other code */ }

Some heading

; @@ -74,9 +74,9 @@ var bar = ( Examples of **correct** code for this rule: -::: correct +::: correct { "ecmaFeatures": { "jsx": true } } -```js +```jsx /*eslint no-inline-comments: "error"*/ var foo = ( diff --git a/docs/src/rules/no-inner-declarations.md b/docs/src/rules/no-inner-declarations.md index 8828fabe7d2..a81a45a61fa 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 f3aa6ed763e..454fdbd7185 100644 --- a/docs/src/rules/no-invalid-this.md +++ b/docs/src/rules/no-invalid-this.md @@ -1,6 +1,8 @@ --- title: no-invalid-this rule_type: suggestion +handled_by_typescript: true +extra_typescript_info: Note that, technically, TypeScript will only catch this if you have the `strict` or `noImplicitThis` flags enabled. These are enabled in most TypeScript projects, since they are considered to be best practice. --- @@ -47,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"*/ @@ -95,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"*/ @@ -112,7 +114,7 @@ function Foo() { baz(() => this); } -class Foo { +class Bar { constructor() { // OK, this is in a constructor. this.a = 0; @@ -180,7 +182,7 @@ Foo.prototype.foo = function foo() { this.a = 0; }; -class Foo { +class Baz { // OK, this is in a class field initializer. a = this.b; @@ -241,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 }]*/ @@ -269,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 258733b0827..4118defe631 100644 --- a/docs/src/rules/no-irregular-whitespace.md +++ b/docs/src/rules/no-irregular-whitespace.md @@ -4,6 +4,7 @@ rule_type: problem further_reading: - https://es5.github.io/#x7.2 - https://web.archive.org/web/20200414142829/http://timelessrepo.com/json-isnt-a-javascript-subset +- https://codepoints.net/U+1680 --- @@ -16,11 +17,17 @@ A simple fix for this problem could be to rewrite the offending line from scratc Known issues these spaces cause: +* Ogham Space Mark + * Is a valid token separator, but is rendered as a visible glyph in most typefaces, which may be misleading in source code. +* Mongolian Vowel Separator + * Is no longer considered a space separator since Unicode 6.3. It will result in a syntax error in current parsers when used in place of a regular token separator. +* Line Separator and Paragraph Separator + * These have always been valid whitespace characters and line terminators, but were considered illegal in string literals prior to ECMAScript 2019. * Zero Width Space - * Is NOT considered a separator for tokens and is often parsed as an `Unexpected token ILLEGAL` - * Is NOT shown in modern browsers making code repository software expected to resolve the visualization -* Line Separator - * Is NOT a valid character within JSON which would cause parse errors + * Is NOT considered a separator for tokens and is often parsed as an `Unexpected token ILLEGAL`. + * Is NOT shown in modern browsers making code repository software expected to resolve the visualization. + +In JSON, none of the characters listed as irregular whitespace by this rule may appear outside of a string. ## Rule Details @@ -63,6 +70,7 @@ This rule has an object option for exceptions: * `"skipComments": true` allows any whitespace characters in comments * `"skipRegExps": true` allows any whitespace characters in regular expression literals * `"skipTemplates": true` allows any whitespace characters in template literals +* `"skipJSXText": true` allows any whitespace characters in JSX text ### skipStrings @@ -73,31 +81,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 } @@ -105,12 +113,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`; } ``` @@ -124,15 +132,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'; } ``` @@ -192,6 +200,23 @@ function thing() { ::: +### skipJSXText + +Examples of additional **correct** code for this rule with the `{ "skipJSXText": true }` option: + +::: correct { "ecmaFeatures": { "jsx": true } } + +```jsx +/*eslint no-irregular-whitespace: ["error", { "skipJSXText": true }]*/ +/*eslint-env es6*/ + +function Thing() { + return
text in JSX
; // before `JSX` +} +``` + +::: + ## When Not To Use It If you decide that you wish to use whitespace other than tabs and spaces outside of strings in your application. diff --git a/docs/src/rules/no-loss-of-precision.md b/docs/src/rules/no-loss-of-precision.md index 1bec49266dc..6674b15da6b 100644 --- a/docs/src/rules/no-loss-of-precision.md +++ b/docs/src/rules/no-loss-of-precision.md @@ -18,12 +18,12 @@ Examples of **incorrect** code for this rule: ```js /*eslint no-loss-of-precision: "error"*/ -const x = 9007199254740993 -const x = 5123000000000000000000000000001 -const x = 1230000000000000000000000.0 -const x = .1230000000000000000000000 -const x = 0X20000000000001 -const x = 0X2_000000000_0001; +const a = 9007199254740993 +const b = 5123000000000000000000000000001 +const c = 1230000000000000000000000.0 +const d = .1230000000000000000000000 +const e = 0X20000000000001 +const f = 0X2_000000000_0001; ``` ::: @@ -35,13 +35,13 @@ Examples of **correct** code for this rule: ```js /*eslint no-loss-of-precision: "error"*/ -const x = 12345 -const x = 123.456 -const x = 123e34 -const x = 12300000000000000000000000 -const x = 0x1FFFFFFFFFFFFF -const x = 9007199254740991 -const x = 9007_1992547409_91 +const a = 12345 +const b = 123.456 +const c = 123e34 +const d = 12300000000000000000000000 +const e = 0x1FFFFFFFFFFFFF +const f = 9007199254740991 +const g = 9007_1992547409_91 ``` ::: diff --git a/docs/src/rules/no-misleading-character-class.md b/docs/src/rules/no-misleading-character-class.md index a0d9e6d0693..792760f2641 100644 --- a/docs/src/rules/no-misleading-character-class.md +++ b/docs/src/rules/no-misleading-character-class.md @@ -17,36 +17,36 @@ This rule reports the regular expressions which include multiple code point char The combining characters are characters which belong to one of `Mc`, `Me`, and `Mn` [Unicode general categories](http://www.unicode.org/L2/L1999/UnicodeData.html#General%20Category). ```js -/^[Á]$/u.test("Á") //→ false -/^[❇️]$/u.test("❇️") //→ false +/^[Á]$/u.test("Á"); //→ false +/^[❇️]$/u.test("❇️"); //→ false ``` **A character with Emoji modifiers:** ```js -/^[👶🏻]$/u.test("👶🏻") //→ false -/^[👶🏽]$/u.test("👶🏽") //→ false +/^[👶🏻]$/u.test("👶🏻"); //→ false +/^[👶🏽]$/u.test("👶🏽"); //→ false ``` **A pair of regional indicator symbols:** ```js -/^[🇯🇵]$/u.test("🇯🇵") //→ false +/^[🇯🇵]$/u.test("🇯🇵"); //→ false ``` **Characters that ZWJ joins:** ```js -/^[👨‍👩‍👦]$/u.test("👨‍👩‍👦") //→ false +/^[👨‍👩‍👦]$/u.test("👨‍👩‍👦"); //→ false ``` **A surrogate pair without Unicode flag:** ```js -/^[👍]$/.test("👍") //→ false +/^[👍]$/.test("👍"); //→ false // Surrogate pair is OK if with u flag. -/^[👍]$/u.test("👍") //→ true +/^[👍]$/u.test("👍"); //→ true ``` ## Rule Details @@ -60,12 +60,12 @@ Examples of **incorrect** code for this rule: ```js /*eslint no-misleading-character-class: error */ -/^[Á]$/u -/^[❇️]$/u -/^[👶🏻]$/u -/^[🇯🇵]$/u -/^[👨‍👩‍👦]$/u -/^[👍]$/ +/^[Á]$/u; +/^[❇️]$/u; +/^[👶🏻]$/u; +/^[🇯🇵]$/u; +/^[👨‍👩‍👦]$/u; +/^[👍]$/; ``` ::: @@ -77,8 +77,9 @@ Examples of **correct** code for this rule: ```js /*eslint no-misleading-character-class: error */ -/^[abc]$/ -/^[👍]$/u +/^[abc]$/; +/^[👍]$/u; +/^[\q{👶🏻}]$/v; ``` ::: diff --git a/docs/src/rules/no-mixed-operators.md b/docs/src/rules/no-mixed-operators.md index 07ab40f7fe5..d8bb908540b 100644 --- a/docs/src/rules/no-mixed-operators.md +++ b/docs/src/rules/no-mixed-operators.md @@ -5,6 +5,7 @@ related_rules: - no-extra-parens --- +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Enclosing complex expressions by parentheses clarifies the developer's intention, which makes the code more readable. This rule warns when different operators are used consecutively without parentheses in an expression. diff --git a/docs/src/rules/no-mixed-spaces-and-tabs.md b/docs/src/rules/no-mixed-spaces-and-tabs.md index 29f82e2191f..b13223cf585 100644 --- a/docs/src/rules/no-mixed-spaces-and-tabs.md +++ b/docs/src/rules/no-mixed-spaces-and-tabs.md @@ -5,7 +5,7 @@ further_reading: - https://www.emacswiki.org/emacs/SmartTabs --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Most code conventions require either tabs or spaces be used for indentation. As such, it's usually an error if a single line of code is indented with both tabs and spaces. @@ -15,42 +15,42 @@ This rule disallows mixed spaces and tabs for indentation. Examples of **incorrect** code for this rule: + + ::: incorrect ```js /*eslint no-mixed-spaces-and-tabs: "error"*/ function add(x, y) { -// --->..return x + y; - - return x + y; + return x + y; } function main() { -// --->var x = 5, -// --->....y = 7; - - var x = 5, - y = 7; + var x = 5, + y = 7; } ``` ::: + Examples of **correct** code for this rule: + + ::: correct ```js /*eslint no-mixed-spaces-and-tabs: "error"*/ function add(x, y) { -// --->return x + y; - return x + y; + return x + y; } ``` ::: + ## Options @@ -62,18 +62,18 @@ This rule has a string option. Examples of **correct** code for this rule with the `"smart-tabs"` option: + + ::: correct ```js /*eslint no-mixed-spaces-and-tabs: ["error", "smart-tabs"]*/ function main() { -// --->var x = 5, -// --->....y = 7; - - var x = 5, - y = 7; + var x = 5, + y = 7; } ``` ::: + diff --git a/docs/src/rules/no-multi-assign.md b/docs/src/rules/no-multi-assign.md index 7cc4581d152..f4f4c2fce5a 100644 --- a/docs/src/rules/no-multi-assign.md +++ b/docs/src/rules/no-multi-assign.md @@ -31,9 +31,9 @@ var a = b = c = 5; const foo = bar = "baz"; -let a = - b = - c; +let d = + e = + f; class Foo { a = b = 10; @@ -58,8 +58,8 @@ var c = 5; const foo = "baz"; const bar = "baz"; -let a = c; -let b = c; +let d = c; +let e = c; class Foo { a = 10; diff --git a/docs/src/rules/no-multi-spaces.md b/docs/src/rules/no-multi-spaces.md index b23a97964fe..ca7e873ab0b 100644 --- a/docs/src/rules/no-multi-spaces.md +++ b/docs/src/rules/no-multi-spaces.md @@ -11,7 +11,7 @@ related_rules: - space-return-throw-case --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Multiple spaces in a row that are not used for indentation are typically mistakes. For example: diff --git a/docs/src/rules/no-multiple-empty-lines.md b/docs/src/rules/no-multiple-empty-lines.md index 5457c54c1f0..f6ccb3050e5 100644 --- a/docs/src/rules/no-multiple-empty-lines.md +++ b/docs/src/rules/no-multiple-empty-lines.md @@ -3,7 +3,7 @@ title: no-multiple-empty-lines rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Some developers prefer to have multiple blank lines removed, while others feel that it helps improve readability. Whitespace is useful for separating logical sections of code, but excess whitespace takes up more of the screen. @@ -59,13 +59,13 @@ Examples of **incorrect** code for this rule with the `{ max: 2, maxEOF: 0 }` op ::: incorrect ```js -/*eslint no-multiple-empty-lines: ["error", { "max": 2, "maxEOF": 0 }]*/ - -var foo = 5; - - -var bar = 3; - +/*eslint no-multiple-empty-lines: ["error", { "max": 2, "maxEOF": 0 }]*/⏎ +⏎ +var foo = 5;⏎ +⏎ +⏎ +var bar = 3;⏎ +⏎ ``` @@ -75,36 +75,19 @@ Examples of **correct** code for this rule with the `{ max: 2, maxEOF: 0 }` opti ::: correct -```js -/*eslint no-multiple-empty-lines: ["error", { "max": 2, "maxEOF": 0 }]*/ - -var foo = 5; - - -var bar = 3; -``` - -::: - -**Note**: Although this ensures zero empty lines at the EOF, most editors will still show one empty line at the end if the file ends with a line break, as illustrated below. There is no empty line at the end of a file after the last `\n`, although editors may show an additional line. A true additional line would be represented by `\n\n`. - -**Incorrect**: - -::: incorrect - ```js /*eslint no-multiple-empty-lines: ["error", { "max": 2, "maxEOF": 0 }]*/⏎ ⏎ var foo = 5;⏎ ⏎ ⏎ -var bar = 3;⏎ -⏎ - +var bar = 3; ``` ::: +**Note**: Although this ensures zero empty lines at the EOF, most editors will still show one empty line at the end if the file ends with a line break, as illustrated below. There is no empty line at the end of a file after the last `\n`, although editors may show an additional line. A true additional line would be represented by `\n\n`. + **Correct**: ::: correct @@ -128,6 +111,8 @@ Examples of **incorrect** code for this rule with the `{ max: 2, maxBOF: 1 }` op ::: incorrect ```js + + /*eslint no-multiple-empty-lines: ["error", { "max": 2, "maxBOF": 1 }]*/ @@ -149,6 +134,20 @@ Examples of **correct** code for this rule with the `{ max: 2, maxBOF: 1 }` opti var foo = 5; +var bar = 3; +``` + +::: + +::: correct + +```js + +/*eslint no-multiple-empty-lines: ["error", { "max": 2, "maxBOF": 1}]*/ + +var foo = 5; + + var bar = 3; ``` diff --git a/docs/src/rules/no-new-object.md b/docs/src/rules/no-new-object.md index 41b02f87406..bdd57741198 100644 --- a/docs/src/rules/no-new-object.md +++ b/docs/src/rules/no-new-object.md @@ -6,6 +6,7 @@ related_rules: - no-new-wrappers --- +This rule was **deprecated** in ESLint v8.50.0 and replaced by the [no-object-constructor](no-object-constructor) rule. The new rule flags more situations where object literal syntax can be used, and it does not report a problem when the `Object` constructor is invoked with an argument. The `Object` constructor is used to create new generic objects in JavaScript, such as: @@ -25,7 +26,7 @@ While there are no performance differences between the two approaches, the byte ## Rule Details -This rule disallows `Object` constructors. +This rule disallows calling the `Object` constructor with `new`. Examples of **incorrect** code for this rule: @@ -37,6 +38,8 @@ Examples of **incorrect** code for this rule: var myObject = new Object(); new Object(); + +var foo = new Object("foo"); ``` ::: @@ -54,10 +57,12 @@ var myObject = {}; var Object = function Object() {}; new Object(); + +var foo = Object("foo"); ``` ::: ## When Not To Use It -If you wish to allow the use of the `Object` constructor, you can safely turn this rule off. +If you wish to allow the use of the `Object` constructor with `new`, you can safely turn this rule off. diff --git a/docs/src/rules/no-new-symbol.md b/docs/src/rules/no-new-symbol.md index 44c34a4eef0..d557811f30c 100644 --- a/docs/src/rules/no-new-symbol.md +++ b/docs/src/rules/no-new-symbol.md @@ -1,6 +1,7 @@ --- title: no-new-symbol rule_type: problem +handled_by_typescript: true further_reading: - https://www.ecma-international.org/ecma-262/6.0/#sec-symbol-objects --- diff --git a/docs/src/rules/no-new-wrappers.md b/docs/src/rules/no-new-wrappers.md index da0a860ca15..fe11e3af7ee 100644 --- a/docs/src/rules/no-new-wrappers.md +++ b/docs/src/rules/no-new-wrappers.md @@ -3,7 +3,7 @@ title: no-new-wrappers rule_type: suggestion related_rules: - no-array-constructor -- no-new-object +- no-object-constructor further_reading: - https://www.inkling.com/read/javascript-definitive-guide-david-flanagan-6th/chapter-3/wrapper-objects --- diff --git a/docs/src/rules/no-nonoctal-decimal-escape.md b/docs/src/rules/no-nonoctal-decimal-escape.md index 6ca59fdf79a..43d0282635a 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-obj-calls.md b/docs/src/rules/no-obj-calls.md index 8d72e4ce277..2fde92e5cbf 100644 --- a/docs/src/rules/no-obj-calls.md +++ b/docs/src/rules/no-obj-calls.md @@ -1,6 +1,7 @@ --- title: no-obj-calls rule_type: problem +handled_by_typescript: true further_reading: - https://es5.github.io/#x15.8 --- diff --git a/docs/src/rules/no-object-constructor.md b/docs/src/rules/no-object-constructor.md new file mode 100644 index 00000000000..e2f7015cdab --- /dev/null +++ b/docs/src/rules/no-object-constructor.md @@ -0,0 +1,50 @@ +--- +title: no-object-constructor +rule_type: suggestion +related_rules: +- no-array-constructor +- no-new-wrappers +--- + +Use of the `Object` constructor to construct a new empty object is generally discouraged in favor of object literal notation because of conciseness and because the `Object` global may be redefined. +The exception is when the `Object` constructor is used to intentionally wrap a specified value which is passed as an argument. + +## Rule Details + +This rule disallows calling the `Object` constructor without an argument. + +Examples of **incorrect** code for this rule: + +:::incorrect + +```js +/*eslint no-object-constructor: "error"*/ + +Object(); + +new Object(); +``` + +::: + +Examples of **correct** code for this rule: + +:::correct + +```js +/*eslint no-object-constructor: "error"*/ + +Object("foo"); + +const obj = { a: 1, b: 2 }; + +const isObject = value => value === Object(value); + +const createObject = Object => new Object(); +``` + +::: + +## When Not To Use It + +If you wish to allow the use of the `Object` constructor, you can safely turn this rule off. diff --git a/docs/src/rules/no-octal-escape.md b/docs/src/rules/no-octal-escape.md index ae8fecdce32..755bc56bc84 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 6a8fa7709a4..c40d9a977b2 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 2bddf7b9921..a3cc558cf45 100644 --- a/docs/src/rules/no-param-reassign.md +++ b/docs/src/rules/no-param-reassign.md @@ -6,7 +6,7 @@ further_reading: --- -Assignment to variables declared as function parameters can be misleading and lead to confusing behavior, as modifying function parameters will also mutate the `arguments` object. Often, assignment to function parameters is unintended and indicative of a mistake or programmer error. +Assignment to variables declared as function parameters can be misleading and lead to confusing behavior, as modifying function parameters will also mutate the `arguments` object when not in strict mode (see [When Not To Use It](#when-not-to-use-it) below). Often, assignment to function parameters is unintended and indicative of a mistake or programmer error. This rule can be also configured to fail when function parameters are modified. Side effects on parameters can cause counter-intuitive execution flow and make errors difficult to track down. @@ -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) {} } ``` @@ -183,3 +183,5 @@ function foo(barBaz) { ## When Not To Use It If you want to allow assignment to function parameters, then you can safely disable this rule. + +Strict mode code doesn't sync indices of the arguments object with each parameter binding. Therefore, this rule is not necessary to protect against arguments object mutation in ESM modules or other strict mode functions. diff --git a/docs/src/rules/no-plusplus.md b/docs/src/rules/no-plusplus.md index ba787f1968f..4f564b51cb9 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-promise-executor-return.md b/docs/src/rules/no-promise-executor-return.md index 28891624822..f163d9ebf0b 100644 --- a/docs/src/rules/no-promise-executor-return.md +++ b/docs/src/rules/no-promise-executor-return.md @@ -38,6 +38,7 @@ Examples of **incorrect** code for this rule: ```js /*eslint no-promise-executor-return: "error"*/ +/*eslint-env es6*/ new Promise((resolve, reject) => { if (someCondition) { @@ -63,6 +64,8 @@ new Promise((resolve, reject) => getSomething((err, data) => { new Promise(() => { return 1; }); + +new Promise(r => r(1)); ``` ::: @@ -73,7 +76,9 @@ Examples of **correct** code for this rule: ```js /*eslint no-promise-executor-return: "error"*/ +/*eslint-env es6*/ +// Turn return inline into two lines new Promise((resolve, reject) => { if (someCondition) { resolve(defaultResult); @@ -88,6 +93,7 @@ new Promise((resolve, reject) => { }); }); +// Add curly braces new Promise((resolve, reject) => { getSomething((err, data) => { if (err) { @@ -98,7 +104,51 @@ new Promise((resolve, reject) => { }); }); +new Promise(r => { r(1) }); +// or just use Promise.resolve Promise.resolve(1); ``` ::: + +## Options + +This rule takes one option, an object, with the following properties: + +* `allowVoid`: If set to `true` (`false` by default), this rule will allow returning void values. + +### allowVoid + +Examples of **correct** code for this rule with the `{ "allowVoid": true }` option: + +::: correct + +```js +/*eslint no-promise-executor-return: ["error", { allowVoid: true }]*/ +/*eslint-env es6*/ + +new Promise((resolve, reject) => { + if (someCondition) { + return void resolve(defaultResult); + } + getSomething((err, result) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); +}); + +new Promise((resolve, reject) => void getSomething((err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } +})); + +new Promise(r => void r(1)); +``` + +::: diff --git a/docs/src/rules/no-redeclare.md b/docs/src/rules/no-redeclare.md index e66f0570fc6..009ba889fcc 100644 --- a/docs/src/rules/no-redeclare.md +++ b/docs/src/rules/no-redeclare.md @@ -1,6 +1,8 @@ --- title: no-redeclare rule_type: suggestion +handled_by_typescript: true +extra_typescript_info: Note that while TypeScript will catch `let` redeclares and `const` redeclares, it will not catch `var` redeclares. Thus, if you use the legacy `var` keyword in your TypeScript codebase, this rule will still provide some value. related_rules: - no-shadow --- diff --git a/docs/src/rules/no-restricted-exports.md b/docs/src/rules/no-restricted-exports.md index 2f83643f9cb..44672021a10 100644 --- a/docs/src/rules/no-restricted-exports.md +++ b/docs/src/rules/no-restricted-exports.md @@ -144,7 +144,25 @@ Examples of **incorrect** code for the `"restrictDefaultExports": { "direct": tr /*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "direct": true } }]*/ export default foo; +``` + +::: + +::: incorrect + +```js +/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "direct": true } }]*/ + export default 42; +``` + +::: + +::: incorrect + +```js +/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "direct": true } }]*/ + export default function foo() {} ``` @@ -176,6 +194,15 @@ Examples of **incorrect** code for the `"restrictDefaultExports": { "defaultFrom /*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "defaultFrom": true } }]*/ export { default } from 'foo'; +``` + +::: + +::: incorrect + +```js +/*eslint no-restricted-exports: ["error", { "restrictDefaultExports": { "defaultFrom": true } }]*/ + export { default as default } from 'foo'; ``` diff --git a/docs/src/rules/no-restricted-imports.md b/docs/src/rules/no-restricted-imports.md index ece0ceb5490..72d97baf3e7 100644 --- a/docs/src/rules/no-restricted-imports.md +++ b/docs/src/rules/no-restricted-imports.md @@ -130,7 +130,7 @@ To restrict the use of all Node.js core imports (via 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 8f42100cfff..2ee5a8b3659 100644 --- a/docs/src/rules/no-return-await.md +++ b/docs/src/rules/no-return-await.md @@ -2,10 +2,12 @@ title: no-return-await rule_type: suggestion further_reading: +- https://v8.dev/blog/fast-async - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function - https://jakearchibald.com/2017/await-vs-return-vs-return-await/ --- +This rule was **deprecated** in ESLint v8.46.0 with no replacement. The original intent of this rule no longer applies due to the fact JavaScript now handles native `Promises` differently. It can now be slower to remove `await` rather than keeping it. More technical information can be found in [this V8 blog entry](https://v8.dev/blog/fast-async). Using `return await` inside an `async function` keeps the current function in the call stack until the Promise that is being awaited has resolved, at the cost of an extra microtask before resolving the outer Promise. `return await` can also be used in a try/catch statement to catch errors from another function that returns a Promise. @@ -36,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 cb2b99c50e1..cbacb46ce26 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"*/ @@ -87,9 +87,9 @@ Examples of **incorrect** code for arrow functions: /*eslint no-sequences: "error"*/ const foo = (val) => (console.log('bar'), val); -const foo = () => ((bar = 123), 10); +const baz = () => ((bar = 123), 10); -const foo = () => { return (bar = 123), 10 } +const qux = () => { return (bar = 123), 10 } ``` ::: @@ -102,9 +102,9 @@ Examples of **correct** code for arrow functions: /*eslint no-sequences: "error"*/ const foo = (val) => ((console.log('bar'), val)); -const foo = () => (((bar = 123), 10)); +const baz = () => (((bar = 123), 10)); -const foo = () => { return ((bar = 123), 10) } +const qux = () => { return ((bar = 123), 10) } ``` ::: @@ -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-setter-return.md b/docs/src/rules/no-setter-return.md index ceb4558c15a..50353c754da 100644 --- a/docs/src/rules/no-setter-return.md +++ b/docs/src/rules/no-setter-return.md @@ -1,6 +1,7 @@ --- title: no-setter-return rule_type: problem +handled_by_typescript: true related_rules: - getter-return further_reading: diff --git a/docs/src/rules/no-shadow-restricted-names.md b/docs/src/rules/no-shadow-restricted-names.md index 308772edaf3..7a5e1b0403d 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 e4a081f0a30..9b9a27ee7d3 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-tabs.md b/docs/src/rules/no-tabs.md index 316db6203f3..fe999fa3c78 100644 --- a/docs/src/rules/no-tabs.md +++ b/docs/src/rules/no-tabs.md @@ -3,6 +3,7 @@ title: no-tabs rule_type: layout --- +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Some style guides don't allow the use of tab characters at all, including within comments. @@ -12,26 +13,33 @@ This rule looks for tabs anywhere inside a file: code, comments or anything else Examples of **incorrect** code for this rule: + + ::: incorrect ```js -var a \t= 2; +/* eslint no-tabs: "error" */ + +var a = 2; /** -* \t\t it's a test function +* it's a test function */ function test(){} -var x = 1; // \t test +var x = 1; // test ``` ::: + Examples of **correct** code for this rule: ::: correct ```js +/* eslint no-tabs: "error" */ + var a = 2; /** @@ -54,19 +62,22 @@ This rule has an optional object option with the following properties: Examples of **correct** code for this rule with the `allowIndentationTabs: true` option: + + ::: correct ```js /* eslint no-tabs: ["error", { allowIndentationTabs: true }] */ function test() { -\tdoSomething(); + doSomething(); } -\t// comment with leading indentation tab + // comment with leading indentation tab ``` ::: + ## When Not To Use It diff --git a/docs/src/rules/no-this-before-super.md b/docs/src/rules/no-this-before-super.md index f1425b5ed35..a06b5735553 100644 --- a/docs/src/rules/no-this-before-super.md +++ b/docs/src/rules/no-this-before-super.md @@ -1,6 +1,7 @@ --- title: no-this-before-super rule_type: problem +handled_by_typescript: true --- @@ -23,28 +24,28 @@ Examples of **incorrect** code for this rule: /*eslint no-this-before-super: "error"*/ /*eslint-env es6*/ -class A extends B { +class A1 extends B { constructor() { this.a = 0; super(); } } -class A extends B { +class A2 extends B { constructor() { this.foo(); super(); } } -class A extends B { +class A3 extends B { constructor() { super.foo(); super(); } } -class A extends B { +class A4 extends B { constructor() { super(this.foo()); } @@ -61,20 +62,20 @@ Examples of **correct** code for this rule: /*eslint no-this-before-super: "error"*/ /*eslint-env es6*/ -class A { +class A1 { constructor() { this.a = 0; // OK, this class doesn't have an `extends` clause. } } -class A extends B { +class A2 extends B { constructor() { super(); this.a = 0; // OK, this is after `super()`. } } -class A extends B { +class A3 extends B { foo() { this.a = 0; // OK. this is not in a constructor. } diff --git a/docs/src/rules/no-throw-literal.md b/docs/src/rules/no-throw-literal.md index 621bb11a09c..1ab939ff3a0 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-trailing-spaces.md b/docs/src/rules/no-trailing-spaces.md index fba4f7df9a2..71c50e41f44 100644 --- a/docs/src/rules/no-trailing-spaces.md +++ b/docs/src/rules/no-trailing-spaces.md @@ -3,7 +3,7 @@ title: no-trailing-spaces rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Sometimes in the course of editing files, you can end up with extra whitespace at the end of lines. These whitespace differences can be picked up by source control systems and flagged as diffs, causing frustration for developers. While this extra whitespace causes no functional issues, many code conventions require that trailing spaces be removed before check-in. @@ -18,9 +18,9 @@ Examples of **incorrect** code for this rule: ```js /*eslint no-trailing-spaces: "error"*/ -var foo = 0;//••••• -var baz = 5;//•• -//••••• +var foo = 0;/* trailing whitespace */ +var baz = 5;/* trailing whitespace */ +/* trailing whitespace */ ``` ::: @@ -58,7 +58,8 @@ Examples of **correct** code for this rule with the `{ "skipBlankLines": true }` var foo = 0; var baz = 5; -//••••• +// ↓ a line with whitespace only ↓ + ``` ::: @@ -72,12 +73,12 @@ Examples of **correct** code for this rule with the `{ "ignoreComments": true }` ```js /*eslint no-trailing-spaces: ["error", { "ignoreComments": true }]*/ -//foo• -//••••• +// ↓ these comments have trailing whitespace → +// /** - *•baz - *•• - *•bar + * baz + * + * bar */ ``` diff --git a/docs/src/rules/no-undef.md b/docs/src/rules/no-undef.md index 0bc7a28279f..72ea516f966 100644 --- a/docs/src/rules/no-undef.md +++ b/docs/src/rules/no-undef.md @@ -1,6 +1,7 @@ --- title: no-undef rule_type: problem +handled_by_typescript: true related_rules: - no-global-assign - no-redeclare diff --git a/docs/src/rules/no-undefined.md b/docs/src/rules/no-undefined.md index 9b0be204b2f..10096533a64 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-underscore-dangle.md b/docs/src/rules/no-underscore-dangle.md index d35aefeb72d..19157dfb55f 100644 --- a/docs/src/rules/no-underscore-dangle.md +++ b/docs/src/rules/no-underscore-dangle.md @@ -3,16 +3,17 @@ title: no-underscore-dangle rule_type: suggestion --- - As far as naming conventions for identifiers go, dangling underscores may be the most polarizing in JavaScript. Dangling underscores are underscores at either the beginning or end of an identifier, such as: ```js var _foo; ``` -There is actually a long history of using dangling underscores to indicate "private" members of objects in JavaScript (though JavaScript doesn't have truly private members, this convention served as a warning). This began with SpiderMonkey adding nonstandard methods such as `__defineGetter__()`. The intent with the underscores was to make it obvious that this method was special in some way. Since that time, using a single underscore prefix has become popular as a way to indicate "private" members of objects. +There is a long history of marking "private" members with dangling underscores in JavaScript, beginning with SpiderMonkey adding nonstandard methods such as `__defineGetter__()`. Since that time, using a single underscore prefix has become the most popular convention for indicating a member is not part of the public interface of an object. + +It is recommended to use the formal [private class features](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields) introduced in ECMAScript 2022 for encapsulating private data and methods rather than relying on naming conventions. -Whether or not you choose to allow dangling underscores in identifiers is purely a convention and has no effect on performance, readability, or complexity. It's purely a preference. +Allowing dangling underscores in identifiers is purely a convention and has no effect on performance, readability, or complexity. They do not have the same encapsulation benefits as private class features, even with this rule enabled. ## Rule Details @@ -44,8 +45,8 @@ var obj = _.contains(items, item); obj.__proto__ = {}; var file = __filename; function foo(_bar) {}; -const foo = { onClick(_bar) {} }; -const foo = (_bar) => {}; +const bar = { onClick(_bar) {} }; +const baz = (_bar) => {}; ``` ::: @@ -103,8 +104,12 @@ Examples of **correct** code for this rule with the `{ "allowAfterSuper": true } ```js /*eslint no-underscore-dangle: ["error", { "allowAfterSuper": true }]*/ -var a = super.foo_; -super._bar(); +class Foo extends Bar { + doSomething() { + var a = super.foo_; + super._bar(); + } +} ``` ::: @@ -137,16 +142,16 @@ class Foo { _bar() {} } -class Foo { +class Bar { bar_() {} } -const o = { +const o1 = { _bar() {} }; -const o = { - bar_() = {} +const o2 = { + bar_() {} }; ``` @@ -165,19 +170,19 @@ class Foo { _bar; } -class Foo { +class Bar { _bar = () => {}; } -class Foo { +class Baz { bar_; } -class Foo { +class Qux { #_bar; } -class Foo { +class FooBar { #bar_; } ``` @@ -194,7 +199,7 @@ Examples of **incorrect** code for this rule with the `{ "allowInArrayDestructur /*eslint no-underscore-dangle: ["error", { "allowInArrayDestructuring": false }]*/ const [_foo, _bar] = list; -const [foo_, ..._bar] = list; +const [foo_, ..._qux] = list; const [foo, [bar, _baz]] = list; ``` @@ -210,7 +215,7 @@ Examples of **incorrect** code for this rule with the `{ "allowInObjectDestructu /*eslint no-underscore-dangle: ["error", { "allowInObjectDestructuring": false }]*/ const { foo, bar: _bar } = collection; -const { foo, bar, _baz } = collection; +const { qux, xyz, _baz } = collection; ``` ::: @@ -223,7 +228,7 @@ Examples of **correct** code for this rule with the `{ "allowInObjectDestructuri /*eslint no-underscore-dangle: ["error", { "allowInObjectDestructuring": false }]*/ const { foo, bar, _baz: { a, b } } = collection; -const { foo, bar, _baz: baz } = collection; +const { qux, xyz, _baz: baz } = collection; ``` ::: @@ -237,17 +242,17 @@ Examples of **incorrect** code for this rule with the `{ "allowFunctionParams": ```js /*eslint no-underscore-dangle: ["error", { "allowFunctionParams": false }]*/ -function foo (_bar) {} -function foo (_bar = 0) {} -function foo (..._bar) {} +function foo1 (_bar) {} +function foo2 (_bar = 0) {} +function foo3 (..._bar) {} -const foo = function onClick (_bar) {} -const foo = function onClick (_bar = 0) {} -const foo = function onClick (..._bar) {} +const foo4 = function onClick (_bar) {} +const foo5 = function onClick (_bar = 0) {} +const foo6 = function onClick (..._bar) {} -const foo = (_bar) => {}; -const foo = (_bar = 0) => {}; -const foo = (..._bar) => {}; +const foo7 = (_bar) => {}; +const foo8 = (_bar = 0) => {}; +const foo9 = (..._bar) => {}; ``` ::: diff --git a/docs/src/rules/no-unexpected-multiline.md b/docs/src/rules/no-unexpected-multiline.md index 158a38642a4..b8f3582fb39 100644 --- a/docs/src/rules/no-unexpected-multiline.md +++ b/docs/src/rules/no-unexpected-multiline.md @@ -40,11 +40,11 @@ var hello = 'world' let x = function() {} `hello` -let x = function() {} -x +let y = function() {} +y `hello` -let x = foo +let z = foo /regex/g.test(bar) ``` diff --git a/docs/src/rules/no-unreachable.md b/docs/src/rules/no-unreachable.md index 4f762084b41..15f77f8173e 100644 --- a/docs/src/rules/no-unreachable.md +++ b/docs/src/rules/no-unreachable.md @@ -1,6 +1,7 @@ --- title: no-unreachable rule_type: problem +handled_by_typescript: true --- diff --git a/docs/src/rules/no-unsafe-negation.md b/docs/src/rules/no-unsafe-negation.md index 522e4ab4d1a..f3b49522316 100644 --- a/docs/src/rules/no-unsafe-negation.md +++ b/docs/src/rules/no-unsafe-negation.md @@ -1,6 +1,7 @@ --- title: no-unsafe-negation rule_type: problem +handled_by_typescript: true --- diff --git a/docs/src/rules/no-unsafe-optional-chaining.md b/docs/src/rules/no-unsafe-optional-chaining.md index e4cc6f83166..57ad6de8780 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-unused-expressions.md b/docs/src/rules/no-unused-expressions.md index c67a2a61706..614ea5b11b7 100644 --- a/docs/src/rules/no-unused-expressions.md +++ b/docs/src/rules/no-unused-expressions.md @@ -251,7 +251,7 @@ JSX is most-commonly used in the React ecosystem, where it is compiled to `React Examples of **incorrect** code for the `{ "enforceForJSX": true }` option: -::: incorrect +::: incorrect { "ecmaFeatures": { "jsx": true } } ```jsx /*eslint no-unused-expressions: ["error", { "enforceForJSX": true }]*/ @@ -265,7 +265,7 @@ Examples of **incorrect** code for the `{ "enforceForJSX": true }` option: Examples of **correct** code for the `{ "enforceForJSX": true }` option: -::: correct +::: correct { "ecmaFeatures": { "jsx": true } } ```jsx /*eslint no-unused-expressions: ["error", { "enforceForJSX": true }]*/ diff --git a/docs/src/rules/no-unused-labels.md b/docs/src/rules/no-unused-labels.md index 971c5672203..fcbe2e179ab 100644 --- a/docs/src/rules/no-unused-labels.md +++ b/docs/src/rules/no-unused-labels.md @@ -30,6 +30,8 @@ Such labels take up space in the code and can lead to confusion by readers. This rule is aimed at eliminating unused labels. +Problems reported by this rule can be fixed automatically, except when there are any comments between the label and the following statement, or when removing a label would cause the following statement to become a directive such as `"use strict"`. + Examples of **incorrect** code for this rule: ::: incorrect diff --git a/docs/src/rules/no-unused-private-class-members.md b/docs/src/rules/no-unused-private-class-members.md index c92a8827be1..ec6951480b5 100644 --- a/docs/src/rules/no-unused-private-class-members.md +++ b/docs/src/rules/no-unused-private-class-members.md @@ -20,29 +20,29 @@ Examples of **incorrect** code for this rule: ```js /*eslint no-unused-private-class-members: "error"*/ -class Foo { +class A { #unusedMember = 5; } -class Foo { +class B { #usedOnlyInWrite = 5; method() { this.#usedOnlyInWrite = 42; } } -class Foo { +class C { #usedOnlyToUpdateItself = 5; method() { this.#usedOnlyToUpdateItself++; } } -class Foo { +class D { #unusedMethod() {} } -class Foo { +class E { get #unusedAccessor() {} set #unusedAccessor(value) {} } @@ -57,14 +57,14 @@ Examples of **correct** code for this rule: ```js /*eslint no-unused-private-class-members: "error"*/ -class Foo { +class A { #usedMember = 42; method() { return this.#usedMember; } } -class Foo { +class B { #usedMethod() { return 42; } @@ -73,7 +73,7 @@ class Foo { } } -class Foo { +class C { get #usedAccessor() {} set #usedAccessor(value) {} diff --git a/docs/src/rules/no-useless-constructor.md b/docs/src/rules/no-useless-constructor.md index cfdae434d72..bf2c4c32984 100644 --- a/docs/src/rules/no-useless-constructor.md +++ b/docs/src/rules/no-useless-constructor.md @@ -56,19 +56,19 @@ Examples of **correct** code for this rule: class A { } -class A { +class B { constructor () { doSomething(); } } -class B extends A { +class C extends A { constructor() { super('foo'); } } -class B extends A { +class D extends A { constructor() { super(); doSomething(); diff --git a/docs/src/rules/no-useless-escape.md b/docs/src/rules/no-useless-escape.md index cd28fda75a8..ff173c229c2 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-rename.md b/docs/src/rules/no-useless-rename.md index d65541c2ad0..6e79e3e7dc0 100644 --- a/docs/src/rules/no-useless-rename.md +++ b/docs/src/rules/no-useless-rename.md @@ -60,14 +60,14 @@ Examples of **incorrect** code for this rule by default: ```js /*eslint no-useless-rename: "error"*/ -import { foo as foo } from "bar"; -import { "foo" as foo } from "bar"; -export { foo as foo }; -export { foo as "foo" }; -export { foo as foo } from "bar"; -export { "foo" as "foo" } from "bar"; -let { foo: foo } = bar; -let { 'foo': foo } = bar; +import { foo1 as foo1 } from "bar"; +import { "foo2" as foo2 } from "bar"; +export { foo1 as foo1 }; +export { foo2 as "foo2" }; +export { foo3 as foo3 } from "bar"; +export { "foo4" as "foo4" } from "bar"; +let { foo3: foo3 } = bar; +let { 'foo4': foo4 } = bar; function foo({ bar: bar }) {} ({ foo: foo }) => {} ``` @@ -81,23 +81,23 @@ Examples of **correct** code for this rule by default: ```js /*eslint no-useless-rename: "error"*/ -import * as foo from "foo"; -import { foo } from "bar"; -import { foo as bar } from "baz"; -import { "foo" as bar } from "baz"; +import * as foo1 from "foo"; +import { foo2 } from "bar"; +import { foo as bar1 } from "baz"; +import { "foo" as bar2 } from "baz"; export { foo }; -export { foo as bar }; -export { foo as "bar" }; -export { foo as bar } from "foo"; -export { "foo" as "bar" } from "foo"; +export { foo as bar1 }; +export { foo as "bar2" }; +export { foo as bar3 } from "foo"; +export { "foo" as "bar4" } from "foo"; let { foo } = bar; let { foo: bar } = baz; -let { [foo]: foo } = bar; +let { [qux]: qux } = bar; -function foo({ bar }) {} -function foo({ bar: baz }) {} +function foo3({ bar }) {} +function foo4({ bar: baz }) {} ({ foo }) => {} ({ foo: bar }) => {} @@ -124,8 +124,9 @@ Examples of **correct** code for this rule with `{ ignoreExport: true }`: ```js /*eslint no-useless-rename: ["error", { ignoreExport: true }]*/ +const foo = 1; export { foo as foo }; -export { foo as foo } from "bar"; +export { bar as bar } from "bar"; ``` ::: @@ -138,7 +139,7 @@ Examples of **correct** code for this rule with `{ ignoreDestructuring: true }`: /*eslint no-useless-rename: ["error", { ignoreDestructuring: true }]*/ let { foo: foo } = bar; -function foo({ bar: bar }) {} +function baz({ bar: bar }) {} ({ foo: foo }) => {} ``` diff --git a/docs/src/rules/no-useless-return.md b/docs/src/rules/no-useless-return.md index 961f0aef5ca..de7045347c2 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-whitespace-before-property.md b/docs/src/rules/no-whitespace-before-property.md index db79ca2a897..a00864d491c 100644 --- a/docs/src/rules/no-whitespace-before-property.md +++ b/docs/src/rules/no-whitespace-before-property.md @@ -3,7 +3,7 @@ title: no-whitespace-before-property rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). JavaScript allows whitespace between objects and their properties. However, inconsistent spacing can make code harder to read and can lead to errors. diff --git a/docs/src/rules/no-with.md b/docs/src/rules/no-with.md index 4fc3f75841d..c4e880f812d 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/nonblock-statement-body-position.md b/docs/src/rules/nonblock-statement-body-position.md index 78d332026e7..1a2aae9122f 100644 --- a/docs/src/rules/nonblock-statement-body-position.md +++ b/docs/src/rules/nonblock-statement-body-position.md @@ -5,7 +5,7 @@ further_reading: - https://jscs-dev.github.io/rule/requireNewlineBeforeSingleStatementsInIf --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). When writing `if`, `else`, `while`, `do-while`, and `for` statements, the body can be a single statement instead of a block. It can be useful to enforce a consistent location for these single statements. diff --git a/docs/src/rules/object-curly-newline.md b/docs/src/rules/object-curly-newline.md index a9b8875a592..d33dd6433e8 100644 --- a/docs/src/rules/object-curly-newline.md +++ b/docs/src/rules/object-curly-newline.md @@ -8,7 +8,7 @@ related_rules: - object-property-newline --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). A number of style guides require or disallow line breaks inside of object braces and other tokens. @@ -554,9 +554,9 @@ Examples of **incorrect** code for this rule with the `{ "ImportDeclaration": "a /*eslint-env es6*/ import {foo, bar} from 'foo-bar'; -import {foo as f, bar} from 'foo-bar'; -import {foo, - bar} from 'foo-bar'; +import {foo as f, baz} from 'foo-bar'; +import {qux, + foobar} from 'foo-bar'; export { foo, @@ -564,7 +564,7 @@ export { }; export { foo as f, - bar + baz } from 'foo-bar'; ``` @@ -583,15 +583,15 @@ import { bar } from 'foo-bar'; import { - foo, bar + baz, qux } from 'foo-bar'; import { foo as f, - bar + foobar } from 'foo-bar'; export { foo, bar } from 'foo-bar'; -export { foo as f, bar } from 'foo-bar'; +export { foo as f, baz } from 'foo-bar'; ``` ::: diff --git a/docs/src/rules/object-curly-spacing.md b/docs/src/rules/object-curly-spacing.md index 4027c9ab755..0484c436a4b 100644 --- a/docs/src/rules/object-curly-spacing.md +++ b/docs/src/rules/object-curly-spacing.md @@ -8,7 +8,7 @@ related_rules: - space-in-parens --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). While formatting preferences are very personal, a number of style guides require or disallow spaces between curly braces in the following situations: diff --git a/docs/src/rules/object-property-newline.md b/docs/src/rules/object-property-newline.md index 50214bdb7c0..e2bb161ea58 100644 --- a/docs/src/rules/object-property-newline.md +++ b/docs/src/rules/object-property-newline.md @@ -8,7 +8,7 @@ related_rules: - object-curly-spacing --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). This rule permits you to restrict the locations of property specifications in object literals. You may prohibit any part of any property specification from appearing on the same line as any part of any other property specification. You may make this prohibition absolute, or, by invoking an object option, you may allow an exception, permitting an object literal to have all parts of all of its property specifications on a single line. diff --git a/docs/src/rules/one-var-declaration-per-line.md b/docs/src/rules/one-var-declaration-per-line.md index e9b159fc992..742f52352fb 100644 --- a/docs/src/rules/one-var-declaration-per-line.md +++ b/docs/src/rules/one-var-declaration-per-line.md @@ -5,7 +5,7 @@ related_rules: - one-var --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Some developers declare multiple var statements on the same line: @@ -46,8 +46,8 @@ Examples of **incorrect** code for this rule with the default `"initializations" var a, b, c = 0; -let a, - b = 0, c; +let d, + e = 0, f; ``` ::: @@ -62,11 +62,11 @@ Examples of **correct** code for this rule with the default `"initializations"` var a, b; -let a, - b; +let c, + d; -let a, - b = 0; +let e, + f = 0; ``` ::: @@ -83,9 +83,9 @@ Examples of **incorrect** code for this rule with the `"always"` option: var a, b; -let a, b = 0; +let c, d = 0; -const a = 0, b = 0; +const e = 0, f = 0; ``` ::: @@ -101,8 +101,8 @@ Examples of **correct** code for this rule with the `"always"` option: var a, b; -let a, - b = 0; +let c, + d = 0; ``` ::: diff --git a/docs/src/rules/one-var.md b/docs/src/rules/one-var.md index d9a50ed8671..cb3bc09b955 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) { @@ -192,14 +192,14 @@ Examples of **incorrect** code for this rule with the `"never"` option: ```js /*eslint one-var: ["error", "never"]*/ -function foo() { +function foo1() { var bar, baz; - const bar = true, - baz = false; + const qux = true, + foobar = false; } -function foo() { +function foo2() { var bar, qux; @@ -208,7 +208,7 @@ function foo() { } } -function foo(){ +function foo3(){ let bar = true, baz = false; } @@ -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; @@ -416,12 +416,16 @@ Examples of **correct** code for this rule with the `{ var: "never" }` option: /*eslint-env es6*/ function foo() { - var bar, - baz; - const bar = 1; // `const` and `let` declarations are ignored if they are not specified - const baz = 2; + var bar; + var baz; + + // `const` and `let` declarations are ignored if they are not specified + const foobar = 1; + const foobaz = 2; + const barfoo = 1, bazfoo = 2; let qux; let norf; + let fooqux, foonorf; } ``` @@ -472,7 +476,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 +485,7 @@ function foo() { e; } -function foo() { +function foo2() { const a = 1, b = 2; const c = 3; @@ -501,7 +505,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 +515,7 @@ function foo() { let f; } -function foo() { +function foo2() { const a = 1, b = 2; diff --git a/docs/src/rules/operator-linebreak.md b/docs/src/rules/operator-linebreak.md index f3cab6e4f03..0724797f3ee 100644 --- a/docs/src/rules/operator-linebreak.md +++ b/docs/src/rules/operator-linebreak.md @@ -5,7 +5,7 @@ related_rules: - comma-style --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). When a statement is too long to fit on a single line, line breaks are generally inserted next to the operators separating expressions. The first style coming to mind would be to place the operator at the end of the line, following the English punctuation rules. diff --git a/docs/src/rules/padded-blocks.md b/docs/src/rules/padded-blocks.md index fd57f2a2905..7d1a32042af 100644 --- a/docs/src/rules/padded-blocks.md +++ b/docs/src/rules/padded-blocks.md @@ -6,7 +6,7 @@ related_rules: - padding-line-between-statements --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Some style guides require block statements to start and end with blank lines. The goal is to improve readability by visually separating the block content and the surrounding code. diff --git a/docs/src/rules/padding-line-between-statements.md b/docs/src/rules/padding-line-between-statements.md index 1b16881c3ee..af57ed48990 100644 --- a/docs/src/rules/padding-line-between-statements.md +++ b/docs/src/rules/padding-line-between-statements.md @@ -3,7 +3,7 @@ title: padding-line-between-statements rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). This rule requires or disallows blank lines between the given 2 kinds of statements. Properly blank lines help developers to understand the code. @@ -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-arrow-callback.md b/docs/src/rules/prefer-arrow-callback.md index 0d724938447..d3e9000561c 100644 --- a/docs/src/rules/prefer-arrow-callback.md +++ b/docs/src/rules/prefer-arrow-callback.md @@ -23,6 +23,8 @@ This rule locates function expressions used as callbacks or function arguments. The following examples **will** be flagged: +::: incorrect + ```js /* eslint prefer-arrow-callback: "error" */ @@ -33,10 +35,14 @@ foo(function() { return this.a; }.bind(this)); // ERROR // prefer: foo(() => this.a) ``` +::: + Instances where an arrow function would not produce identical results will be ignored. The following examples **will not** be flagged: +::: correct + ```js /* eslint prefer-arrow-callback: "error" */ /* eslint-env es6 */ @@ -57,6 +63,8 @@ foo(function() { return this.a; }); // OK foo(function bar(n) { return n && n + bar(n - 1); }); // OK ``` +::: + ## Options Access further control over this rule's behavior via an options object. @@ -71,12 +79,16 @@ Changing this value to `true` will reverse this option's behavior by allowing us `{ "allowNamedFunctions": true }` **will not** flag the following example: +::: correct + ```js /* eslint prefer-arrow-callback: [ "error", { "allowNamedFunctions": true } ] */ foo(function bar() {}); ``` +::: + ### allowUnboundThis By default `{ "allowUnboundThis": true }`, this `boolean` option allows function expressions containing `this` to be used as callbacks, as long as the function in question has not been explicitly bound. @@ -85,6 +97,8 @@ When set to `false` this option prohibits the use of function expressions as cal `{ "allowUnboundThis": false }` **will** flag the following examples: +::: incorrect + ```js /* eslint prefer-arrow-callback: [ "error", { "allowUnboundThis": false } ] */ /* eslint-env es6 */ @@ -96,6 +110,8 @@ foo(function() { (() => this); }); someArray.map(function(item) { return this.doSomething(item); }, someObject); ``` +::: + ## When Not To Use It * In environments that have not yet adopted ES6 language features (ES3/5). diff --git a/docs/src/rules/prefer-const.md b/docs/src/rules/prefer-const.md index 19801eb952a..41b144fe466 100644 --- a/docs/src/rules/prefer-const.md +++ b/docs/src/rules/prefer-const.md @@ -27,9 +27,9 @@ Examples of **incorrect** code for this rule: let a = 3; console.log(a); -let a; -a = 0; -console.log(a); +let b; +b = 0; +console.log(b); class C { static { @@ -63,35 +63,35 @@ Examples of **correct** code for this rule: const a = 0; // it's never initialized. -let a; -console.log(a); +let b; +console.log(b); // it's reassigned after initialized. -let a; -a = 0; -a = 1; -console.log(a); +let c; +c = 0; +c = 1; +console.log(c); // it's initialized in a different block from the declaration. -let a; +let d; if (true) { - a = 0; + d = 0; } -console.log(a); +console.log(d); // it's initialized in a different scope. -let a; +let e; class C { #x; static { - a = obj => obj.#x; + e = obj => obj.#x; } } // it's initialized at a place that we cannot write a variable declaration. -let a; -if (true) a = 0; -console.log(a); +let f; +if (true) f = 0; +console.log(f); // `i` gets a new binding each iteration for (const i in [1, 2, 3]) { @@ -112,14 +112,14 @@ for (let i = 0, end = 10; i < end; ++i) { let predicate; [object.type, predicate] = foo(); -// `a` is only assigned once but cannot be separately declared as `const` -let a; -const b = {}; -({ a, c: b.c } = func()); +// `g` is only assigned once but cannot be separately declared as `const` +let g; +const h = {}; +({ g, c: h.c } = func()); // suggest to use `no-var` rule. -var b = 3; -console.log(b); +var i = 3; +console.log(i); ``` ::: @@ -170,9 +170,9 @@ const {a: a0, b} = obj; const a = a0 + 1; // all variables are reassigned. -let {a, b} = obj; -a = a + 1; -b = b + 1; +let {c, d} = obj; +c = c + 1; +d = d + 1; ``` ::: diff --git a/docs/src/rules/prefer-destructuring.md b/docs/src/rules/prefer-destructuring.md index c98abdc0c90..34b30c8de32 100644 --- a/docs/src/rules/prefer-destructuring.md +++ b/docs/src/rules/prefer-destructuring.md @@ -36,8 +36,11 @@ Examples of **incorrect** code for this rule: ::: incorrect ```javascript +/* eslint prefer-destructuring: "error" */ + // With `array` enabled var foo = array[0]; +bar.baz = array[0]; // With `object` enabled var foo = object.foo; @@ -51,17 +54,21 @@ Examples of **correct** code for this rule: ::: correct ```javascript +/* eslint prefer-destructuring: "error" */ + // With `array` enabled var [ foo ] = array; var foo = array[someIndex]; +[bar.baz] = array; + // With `object` enabled var { foo } = object; var foo = object.bar; -let foo; -({ foo } = object); +let bar; +({ bar } = object); ``` ::: @@ -71,6 +78,7 @@ Examples of **incorrect** code when `enforceForRenamedProperties` is enabled: ::: incorrect ```javascript +/* eslint "prefer-destructuring": ["error", { "object": true }, { "enforceForRenamedProperties": true }] */ var foo = object.bar; ``` @@ -81,6 +89,7 @@ Examples of **correct** code when `enforceForRenamedProperties` is enabled: ::: correct ```javascript +/* eslint "prefer-destructuring": ["error", { "object": true }, { "enforceForRenamedProperties": true }] */ var { bar: foo } = object; ``` @@ -91,6 +100,7 @@ Examples of additional **correct** code when `enforceForRenamedProperties` is en ::: correct ```javascript +/* eslint "prefer-destructuring": ["error", { "object": true }, { "enforceForRenamedProperties": true }] */ class C { #x; foo() { diff --git a/docs/src/rules/prefer-reflect.md b/docs/src/rules/prefer-reflect.md index e2a632eb6c6..d6e3d87330a 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 cf234260290..4406b5468dc 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/quote-props.md b/docs/src/rules/quote-props.md index b994b82507e..2e204dd0cb6 100644 --- a/docs/src/rules/quote-props.md +++ b/docs/src/rules/quote-props.md @@ -6,7 +6,7 @@ further_reading: - https://mathiasbynens.be/notes/javascript-properties --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Object literal property names can be defined in two ways: using literals or using strings. For example, these two objects are equivalent: diff --git a/docs/src/rules/quotes.md b/docs/src/rules/quotes.md index 38d21b428f9..ea935d610a1 100644 --- a/docs/src/rules/quotes.md +++ b/docs/src/rules/quotes.md @@ -3,7 +3,7 @@ title: quotes rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). JavaScript allows you to define strings in one of three ways: double quotes, single quotes, and backticks (as of ECMAScript 6). For example: @@ -15,7 +15,7 @@ var single = 'single'; var backtick = `backtick`; // ES6 only ``` -Each of these lines creates a string and, in some cases, can be used interchangeably. The choice of how to define strings in a codebase is a stylistic one outside of template literals (which allow embedded of expressions to be interpreted). +Each of these lines creates a string and, in some cases, can be used interchangeably. The choice of how to define strings in a codebase is a stylistic one outside of template literals (which allow embedded expressions to be interpreted). Many codebases require strings to be defined in a consistent manner. @@ -23,6 +23,8 @@ Many codebases require strings to be defined in a consistent manner. This rule enforces the consistent use of either backticks, double, or single quotes. +This rule is aware of directive prologues such as `"use strict"` and will not flag or autofix them if doing so will change how the directive prologue is interpreted. + ## Options This rule has two options, a string option and an object option. @@ -125,7 +127,9 @@ Examples of **correct** code for this rule with the `"backtick"` option: /*eslint quotes: ["error", "backtick"]*/ /*eslint-env es6*/ +"use strict"; // directives must use single or double quotes var backtick = `backtick`; +var obj = { 'prop-name': `value` }; // backticks not allowed for property names ``` ::: diff --git a/docs/src/rules/require-await.md b/docs/src/rules/require-await.md index a17eb972dca..e0371dfb6ad 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 20516fb0188..82ea4b43222 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-unicode-regexp.md b/docs/src/rules/require-unicode-regexp.md index f3277066a5b..768f34a71d3 100644 --- a/docs/src/rules/require-unicode-regexp.md +++ b/docs/src/rules/require-unicode-regexp.md @@ -1,6 +1,9 @@ --- title: require-unicode-regexp rule_type: suggestion +further_reading: +- https://github.com/tc39/proposal-regexp-v-flag +- https://v8.dev/features/regexp-v-flag --- @@ -21,11 +24,39 @@ RegExp `u` flag has two effects: The `u` flag disables the recovering logic Annex B defined. As a result, you can find errors early. This is similar to [the strict mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode). -Therefore, the `u` flag lets us work better with regular expressions. +The RegExp `v` flag, introduced in ECMAScript 2024, is a superset of the `u` flag, and offers two more features: + +1. **Unicode properties of strings** + + With the Unicode property escape, you can use properties of strings. + + ```js + const re = /^\p{RGI_Emoji}$/v; + + // Match an emoji that consists of just 1 code point: + re.test('⚽'); // '\u26BD' + // → true ✅ + + // Match an emoji that consists of multiple code points: + re.test('👨🏾‍⚕️'); // '\u{1F468}\u{1F3FE}\u200D\u2695\uFE0F' + // → true ✅ + ``` + +2. **Set notation** + + It allows for set operations between character classes. + + ```js + const re = /[\p{White_Space}&&\p{ASCII}]/v; + re.test('\n'); // → true + re.test('\u2028'); // → false + ``` + +Therefore, the `u` and `v` flags let us work better with regular expressions. ## Rule Details -This rule aims to enforce the use of `u` flag on regular expressions. +This rule aims to enforce the use of `u` or `v` flag on regular expressions. Examples of **incorrect** code for this rule: @@ -54,8 +85,13 @@ const b = /bbb/giu const c = new RegExp("ccc", "u") const d = new RegExp("ddd", "giu") +const e = /aaa/v +const f = /bbb/giv +const g = new RegExp("ccc", "v") +const h = new RegExp("ddd", "giv") + // This rule ignores RegExp calls if the flags could not be evaluated to a static value. -function f(flags) { +function i(flags) { return new RegExp("eee", flags) } ``` @@ -64,4 +100,4 @@ function f(flags) { ## When Not To Use It -If you don't want to notify regular expressions with no `u` flag, then it's safe to disable this rule. +If you don't want to warn on regular expressions without either a `u` or a `v` flag, then it's safe to disable this rule. diff --git a/docs/src/rules/require-yield.md b/docs/src/rules/require-yield.md index e4f975faa3e..eec9d0cfc94 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/rest-spread-spacing.md b/docs/src/rules/rest-spread-spacing.md index b27b5378d23..8a52856cad9 100644 --- a/docs/src/rules/rest-spread-spacing.md +++ b/docs/src/rules/rest-spread-spacing.md @@ -5,7 +5,7 @@ further_reading: - https://github.com/tc39/proposal-object-rest-spread --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). ES2015 introduced the rest and spread operators, which expand an iterable structure into its individual parts. Some examples of their usage are as follows: @@ -85,8 +85,8 @@ Examples of **incorrect** code for this rule with `"never"`: ```js /*eslint rest-spread-spacing: ["error", "never"]*/ -fn(... args) -[... arr, 4, 5, 6] +fn(... args); +[... arr, 4, 5, 6]; let [a, b, ... arr] = [1, 2, 3, 4, 5]; function fn(... args) { console.log(args); } let { x, y, ... z } = { x: 1, y: 2, a: 3, b: 4 }; @@ -102,8 +102,8 @@ Examples of **correct** code for this rule with `"never"`: ```js /*eslint rest-spread-spacing: ["error", "never"]*/ -fn(...args) -[...arr, 4, 5, 6] +fn(...args); +[...arr, 4, 5, 6]; let [a, b, ...arr] = [1, 2, 3, 4, 5]; function fn(...args) { console.log(args); } let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; @@ -127,8 +127,8 @@ Examples of **incorrect** code for this rule with `"always"`: ```js /*eslint rest-spread-spacing:["error", "always"]*/ -fn(...args) -[...arr, 4, 5, 6] +fn(...args); +[...arr, 4, 5, 6]; let [a, b, ...arr] = [1, 2, 3, 4, 5]; function fn(...args) { console.log(args); } let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; @@ -144,8 +144,8 @@ Examples of **correct** code for this rule with `"always"`: ```js /*eslint rest-spread-spacing: ["error", "always"]*/ -fn(... args) -[... arr, 4, 5, 6] +fn(... args); +[... arr, 4, 5, 6]; let [a, b, ... arr] = [1, 2, 3, 4, 5]; function fn(... args) { console.log(args); } let { x, y, ... z } = { x: 1, y: 2, a: 3, b: 4 }; diff --git a/docs/src/rules/semi-spacing.md b/docs/src/rules/semi-spacing.md index 4ed43ac570e..5a27040f9b5 100644 --- a/docs/src/rules/semi-spacing.md +++ b/docs/src/rules/semi-spacing.md @@ -9,7 +9,7 @@ related_rules: - space-in-parens --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). JavaScript allows you to place unnecessary spaces before or after a semicolon. diff --git a/docs/src/rules/semi-style.md b/docs/src/rules/semi-style.md index 66db3af0f75..2979c188564 100644 --- a/docs/src/rules/semi-style.md +++ b/docs/src/rules/semi-style.md @@ -7,7 +7,7 @@ related_rules: - semi-spacing --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Generally, semicolons are at the end of lines. However, in semicolon-less style, semicolons are at the beginning of lines. This rule enforces that semicolons are at the configured location. diff --git a/docs/src/rules/semi.md b/docs/src/rules/semi.md index 6fd05501cd1..e118b4518f8 100644 --- a/docs/src/rules/semi.md +++ b/docs/src/rules/semi.md @@ -10,7 +10,7 @@ further_reading: - https://web.archive.org/web/20200420230322/http://inimino.org/~inimino/blog/javascript_semicolons --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). JavaScript doesn't require semicolons at the end of each statement. In many cases, the JavaScript engine can determine that a semicolon should be in a certain spot and will automatically add it. This feature is known as **automatic semicolon insertion (ASI)** and is considered one of the more controversial features of JavaScript. For example, the following lines are both valid: diff --git a/docs/src/rules/sort-imports.md b/docs/src/rules/sort-imports.md index d48ead6f66e..762e4acfa43 100644 --- a/docs/src/rules/sort-imports.md +++ b/docs/src/rules/sort-imports.md @@ -82,19 +82,37 @@ import {alpha, beta} from 'alpha.js'; import {delta, gamma} from 'delta.js'; import a from 'baz.js'; import {b} from 'qux.js'; +``` + +::: +::: correct + +```js /*eslint sort-imports: "error"*/ import a from 'foo.js'; import b from 'bar.js'; import c from 'baz.js'; +``` +::: + +::: correct + +```js /*eslint sort-imports: "error"*/ import 'foo.js' import * as bar from 'bar.js'; import {a, b} from 'baz.js'; import c from 'qux.js'; import {d} from 'quux.js'; +``` +::: + +::: correct + +```js /*eslint sort-imports: "error"*/ import {a, b, c} from 'foo.js' ``` @@ -109,27 +127,63 @@ Examples of **incorrect** code for this rule when using default options: /*eslint sort-imports: "error"*/ import b from 'foo.js'; import a from 'bar.js'; +``` + +::: +::: incorrect + +```js /*eslint sort-imports: "error"*/ import a from 'foo.js'; import A from 'bar.js'; +``` + +::: +::: incorrect + +```js /*eslint sort-imports: "error"*/ -import {b, c} from 'foo.js'; +import {c, d} from 'foo.js'; import {a, b} from 'bar.js'; +``` + +::: + +::: incorrect +```js /*eslint sort-imports: "error"*/ import a from 'foo.js'; import {b, c} from 'bar.js'; +``` + +::: + +::: incorrect +```js /*eslint sort-imports: "error"*/ import {a} from 'foo.js'; import {b, c} from 'bar.js'; +``` + +::: +::: incorrect + +```js /*eslint sort-imports: "error"*/ import a from 'foo.js'; import * as b from 'bar.js'; +``` + +::: +::: incorrect + +```js /*eslint sort-imports: "error"*/ import {b, a, c} from 'foo.js' ``` diff --git a/docs/src/rules/sort-keys.md b/docs/src/rules/sort-keys.md index 0fd00aeff7f..14cdc12f8f3 100644 --- a/docs/src/rules/sort-keys.md +++ b/docs/src/rules/sort-keys.md @@ -21,20 +21,20 @@ Examples of **incorrect** code for this rule: /*eslint sort-keys: "error"*/ /*eslint-env es6*/ -let obj = {a: 1, c: 3, b: 2}; -let obj = {a: 1, "c": 3, b: 2}; +let obj1 = {a: 1, c: 3, b: 2}; +let obj2 = {a: 1, "c": 3, b: 2}; // Case-sensitive by default. -let obj = {a: 1, b: 2, C: 3}; +let obj3 = {a: 1, b: 2, C: 3}; // Non-natural order by default. -let obj = {1: a, 2: c, 10: b}; +let obj4 = {1: a, 2: c, 10: b}; // This rule checks computed properties which have a simple name as well. // Simple names are names which are expressed by an Identifier node or a Literal node. const S = Symbol("s") -let obj = {a: 1, ["c"]: 3, b: 2}; -let obj = {a: 1, [S]: 3, b: 2}; +let obj5 = {a: 1, ["c"]: 3, b: 2}; +let obj6 = {a: 1, [S]: 3, b: 2}; ``` ::: @@ -47,27 +47,27 @@ Examples of **correct** code for this rule: /*eslint sort-keys: "error"*/ /*eslint-env es6*/ -let obj = {a: 1, b: 2, c: 3}; -let obj = {a: 1, "b": 2, c: 3}; +let obj1 = {a: 1, b: 2, c: 3}; +let obj2 = {a: 1, "b": 2, c: 3}; // Case-sensitive by default. -let obj = {C: 3, a: 1, b: 2}; +let obj3 = {C: 3, a: 1, b: 2}; // Non-natural order by default. -let obj = {1: a, 10: b, 2: c}; +let obj4 = {1: a, 10: b, 2: c}; // This rule checks computed properties which have a simple name as well. -let obj = {a: 1, ["b"]: 2, c: 3}; -let obj = {a: 1, [b]: 2, c: 3}; +let obj5 = {a: 1, ["b"]: 2, c: 3}; +let obj6 = {a: 1, [b]: 2, c: 3}; // This rule ignores computed properties which have a non-simple name. -let obj = {a: 1, [c + d]: 3, b: 2}; -let obj = {a: 1, ["c" + "d"]: 3, b: 2}; -let obj = {a: 1, [`${c}`]: 3, b: 2}; -let obj = {a: 1, [tag`c`]: 3, b: 2}; +let obj7 = {a: 1, [c + d]: 3, b: 2}; +let obj8 = {a: 1, ["c" + "d"]: 3, b: 2}; +let obj9 = {a: 1, [`${c}`]: 3, b: 2}; +let obj10 = {a: 1, [tag`c`]: 3, b: 2}; // This rule does not report unsorted properties that are separated by a spread property. -let obj = {b: 1, ...c, a: 2}; +let obj11 = {b: 1, ...c, a: 2}; ``` ::: @@ -118,14 +118,14 @@ Examples of **incorrect** code for the `"desc"` option: /*eslint sort-keys: ["error", "desc"]*/ /*eslint-env es6*/ -let obj = {b: 2, c: 3, a: 1}; -let obj = {"b": 2, c: 3, a: 1}; +let obj1 = {b: 2, c: 3, a: 1}; +let obj2 = {"b": 2, c: 3, a: 1}; // Case-sensitive by default. -let obj = {C: 1, b: 3, a: 2}; +let obj3 = {C: 1, b: 3, a: 2}; // Non-natural order by default. -let obj = {10: b, 2: c, 1: a}; +let obj4 = {10: b, 2: c, 1: a}; ``` ::: @@ -138,14 +138,14 @@ Examples of **correct** code for the `"desc"` option: /*eslint sort-keys: ["error", "desc"]*/ /*eslint-env es6*/ -let obj = {c: 3, b: 2, a: 1}; -let obj = {c: 3, "b": 2, a: 1}; +let obj1 = {c: 3, b: 2, a: 1}; +let obj2 = {c: 3, "b": 2, a: 1}; // Case-sensitive by default. -let obj = {b: 3, a: 2, C: 1}; +let obj3 = {b: 3, a: 2, C: 1}; // Non-natural order by default. -let obj = {2: c, 10: b, 1: a}; +let obj4 = {2: c, 10: b, 1: a}; ``` ::: @@ -160,8 +160,8 @@ Examples of **incorrect** code for the `{caseSensitive: false}` option: /*eslint sort-keys: ["error", "asc", {caseSensitive: false}]*/ /*eslint-env es6*/ -let obj = {a: 1, c: 3, C: 4, b: 2}; -let obj = {a: 1, C: 3, c: 4, b: 2}; +let obj1 = {a: 1, c: 3, C: 4, b: 2}; +let obj2 = {a: 1, C: 3, c: 4, b: 2}; ``` ::: @@ -174,8 +174,8 @@ Examples of **correct** code for the `{caseSensitive: false}` option: /*eslint sort-keys: ["error", "asc", {caseSensitive: false}]*/ /*eslint-env es6*/ -let obj = {a: 1, b: 2, c: 3, C: 4}; -let obj = {a: 1, b: 2, C: 3, c: 4}; +let obj1 = {a: 1, b: 2, c: 3, C: 4}; +let obj2 = {a: 1, b: 2, C: 3, c: 4}; ``` ::: @@ -219,7 +219,7 @@ Examples of **incorrect** code for the `{minKeys: 4}` option: /*eslint-env es6*/ // 4 keys -let obj = { +let obj1 = { b: 2, a: 1, // not sorted correctly (should be 1st key) c: 3, @@ -227,7 +227,7 @@ let obj = { }; // 5 keys -let obj = { +let obj2 = { 2: 'a', 1: 'b', // not sorted correctly (should be 1st key) 3: 'c', @@ -247,14 +247,14 @@ Examples of **correct** code for the `{minKeys: 4}` option: /*eslint-env es6*/ // 3 keys -let obj = { +let obj1 = { b: 2, a: 1, c: 3, }; // 2 keys -let obj = { +let obj2 = { 2: 'b', 1: 'a', }; @@ -318,7 +318,7 @@ Examples of **correct** code for the `{allowLineSeparatedGroups: true}` option: /*eslint sort-keys: ["error", "asc", {allowLineSeparatedGroups: true}]*/ /*eslint-env es6*/ -let obj = { +let obj1 = { e: 1, f: 2, g: 3, @@ -328,7 +328,7 @@ let obj = { c: 6 } -let obj = { +let obj2 = { b: 1, // comment @@ -336,7 +336,7 @@ let obj = { c: 5, } -let obj = { +let obj3 = { c: 1, d: 2, @@ -346,7 +346,7 @@ let obj = { e: 3, } -let obj = { +let obj4 = { c: 1, d: 2, // comment @@ -358,14 +358,14 @@ let obj = { e: 4 } -let obj = { +let obj5 = { b, [foo + bar]: 1, a } -let obj = { +let obj6 = { b: 1 // comment before comma @@ -373,7 +373,7 @@ let obj = { a: 2 }; -var obj = { +var obj7 = { b: 1, a: 2, diff --git a/docs/src/rules/space-before-blocks.md b/docs/src/rules/space-before-blocks.md index a7bcad2c70a..42f02f99ebf 100644 --- a/docs/src/rules/space-before-blocks.md +++ b/docs/src/rules/space-before-blocks.md @@ -9,7 +9,7 @@ related_rules: - brace-style --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Consistency is an important part of any style guide. While it is a personal preference where to put the opening brace of blocks, diff --git a/docs/src/rules/space-before-function-paren.md b/docs/src/rules/space-before-function-paren.md index 1948d903cf0..8b439dac5f1 100644 --- a/docs/src/rules/space-before-function-paren.md +++ b/docs/src/rules/space-before-function-paren.md @@ -5,7 +5,7 @@ related_rules: - keyword-spacing --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). When formatting a function, whitespace is allowed between the function name or `function` keyword and the opening paren. Named functions also require a space between the `function` keyword and the function name, but anonymous functions require no whitespace. For example: @@ -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 a5c0eceb01f..0b6b9b60d61 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/space-before-keywords.md b/docs/src/rules/space-before-keywords.md index 9c2f7358109..5eaa76db768 100644 --- a/docs/src/rules/space-before-keywords.md +++ b/docs/src/rules/space-before-keywords.md @@ -55,7 +55,7 @@ if (foo) { const foo = 'bar';let baz = 'qux'; -var foo =function bar () {} +var qux =function bar () {} function bar() { if (foo) {return; } @@ -66,7 +66,7 @@ function bar() { Examples of **correct** code for this rule with the default `"always"` option: -::: correct +::: correct { "ecmaFeatures": { "jsx": true } } ```js /*eslint space-before-keywords: ["error", "always"]*/ @@ -76,7 +76,7 @@ if (foo) { // ... } else {} -(function() {})() +(function() {})(); diff --git a/docs/src/rules/space-in-parens.md b/docs/src/rules/space-in-parens.md index 1d6ca52bdf1..9f92cdc524a 100644 --- a/docs/src/rules/space-in-parens.md +++ b/docs/src/rules/space-in-parens.md @@ -7,7 +7,7 @@ related_rules: - computed-property-spacing --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Some style guides require or disallow spaces inside of parentheses: diff --git a/docs/src/rules/space-infix-ops.md b/docs/src/rules/space-infix-ops.md index 984e3b37f41..8a8c0c3dff7 100644 --- a/docs/src/rules/space-infix-ops.md +++ b/docs/src/rules/space-infix-ops.md @@ -3,7 +3,7 @@ title: space-infix-ops rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). While formatting preferences are very personal, a number of style guides require spaces around operators, such as: @@ -57,7 +57,7 @@ a?b:c const a={b:1}; -var {a=0}=bar; +var {b=0}=bar; function foo(a=0) { } ``` @@ -80,7 +80,7 @@ a ? b : c const a = {b:1}; -var {a = 0} = bar; +var {b = 0} = bar; function foo(a = 0) { } ``` diff --git a/docs/src/rules/space-unary-ops.md b/docs/src/rules/space-unary-ops.md index a33735b5565..4e7f79bfce9 100644 --- a/docs/src/rules/space-unary-ops.md +++ b/docs/src/rules/space-unary-ops.md @@ -3,7 +3,7 @@ title: space-unary-ops rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Some style guides require or disallow spaces before or after unary operators. This is mainly a stylistic issue, however, some JavaScript expressions can be written without spacing which makes it harder to read and maintain. diff --git a/docs/src/rules/spaced-comment.md b/docs/src/rules/spaced-comment.md index f15e16c72e8..3094c3cebed 100644 --- a/docs/src/rules/spaced-comment.md +++ b/docs/src/rules/spaced-comment.md @@ -5,7 +5,7 @@ related_rules: - spaced-line-comment --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Some style guides require or disallow a whitespace immediately after the initial `//` or `/*` of a comment. Whitespace after the `//` or `/*` makes it easier to read text in comments. diff --git a/docs/src/rules/strict.md b/docs/src/rules/strict.md index c559b00710d..731ddba2c20 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/switch-colon-spacing.md b/docs/src/rules/switch-colon-spacing.md index 63df48f23e1..7672c6275b7 100644 --- a/docs/src/rules/switch-colon-spacing.md +++ b/docs/src/rules/switch-colon-spacing.md @@ -3,7 +3,7 @@ title: switch-colon-spacing rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). Spacing around colons improves readability of `case`/`default` clauses. diff --git a/docs/src/rules/template-curly-spacing.md b/docs/src/rules/template-curly-spacing.md index b96a30c870d..7a095cc38d8 100644 --- a/docs/src/rules/template-curly-spacing.md +++ b/docs/src/rules/template-curly-spacing.md @@ -3,7 +3,7 @@ title: template-curly-spacing rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). We can embed expressions in template strings with using a pair of `${` and `}`. diff --git a/docs/src/rules/template-tag-spacing.md b/docs/src/rules/template-tag-spacing.md index da0cfa8884b..2adf4c76205 100644 --- a/docs/src/rules/template-tag-spacing.md +++ b/docs/src/rules/template-tag-spacing.md @@ -6,7 +6,7 @@ further_reading: - https://exploringjs.com/es6/ch_template-literals.html#_examples-of-using-tagged-template-literals --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). With ES6, it's possible to create functions called [tagged template literals](#further-reading) where the function parameters consist of a template literal's strings and expressions. diff --git a/docs/src/rules/unicode-bom.md b/docs/src/rules/unicode-bom.md index 1bae51ce64e..6fcde6c3304 100644 --- a/docs/src/rules/unicode-bom.md +++ b/docs/src/rules/unicode-bom.md @@ -31,9 +31,10 @@ Example of **correct** code for this rule with the `"always"` option: ::: correct ```js +// U+FEFF at the beginning + /*eslint unicode-bom: ["error", "always"]*/ -U+FEFF var abc; ``` @@ -70,9 +71,10 @@ Example of **incorrect** code for this rule with the `"never"` option: ::: incorrect ```js +// U+FEFF at the beginning + /*eslint unicode-bom: ["error", "never"]*/ -U+FEFF var abc; ``` diff --git a/docs/src/rules/vars-on-top.md b/docs/src/rules/vars-on-top.md index 20d1440aee4..5fdb2f16048 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/rules/wrap-iife.md b/docs/src/rules/wrap-iife.md index af8ec85c785..dd0048a8bb7 100644 --- a/docs/src/rules/wrap-iife.md +++ b/docs/src/rules/wrap-iife.md @@ -3,7 +3,7 @@ title: wrap-iife rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). You can immediately invoke function expressions, but not function declarations. A common technique to create an immediately-invoked function expression (IIFE) is to wrap a function declaration in parentheses. The opening parentheses causes the contained function to be parsed as an expression, rather than a declaration. diff --git a/docs/src/rules/wrap-regex.md b/docs/src/rules/wrap-regex.md index d4cff7df988..0a5daf0e1a0 100644 --- a/docs/src/rules/wrap-regex.md +++ b/docs/src/rules/wrap-regex.md @@ -3,7 +3,7 @@ title: wrap-regex rule_type: layout --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). When a regular expression is used in certain situations, it can end up looking like a division operator. For example: diff --git a/docs/src/rules/yield-star-spacing.md b/docs/src/rules/yield-star-spacing.md index c5e47380926..aeeb0c73b2d 100644 --- a/docs/src/rules/yield-star-spacing.md +++ b/docs/src/rules/yield-star-spacing.md @@ -5,7 +5,7 @@ further_reading: - https://leanpub.com/understandinges6/read/#leanpub-auto-generators --- - +This rule was **deprecated** in ESLint v8.53.0. Please use the corresponding rule in [`@stylistic/eslint-plugin-js`](https://eslint.style/packages/js). ## Rule Details diff --git a/docs/src/use/command-line-interface.md b/docs/src/use/command-line-interface.md index 59f1d2a392f..9253e9c23f3 100644 --- a/docs/src/use/command-line-interface.md +++ b/docs/src/use/command-line-interface.md @@ -97,7 +97,7 @@ Output: Inline configuration comments: --no-inline-config Prevent comments from changing config or rules - --report-unused-disable-directives Adds reported errors for unused eslint-disable directives + --report-unused-disable-directives Adds reported errors for unused eslint-disable and eslint-enable directives --report-unused-disable-directives-severity String Choose what severity level that eslint-disable directives should be reported as Caching: @@ -111,6 +111,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 @@ -121,7 +122,7 @@ Miscellaneous: #### `--no-eslintrc` -Disables use of configuration from `.eslintrc.*` and `package.json` files. +**eslintrc Mode Only.** Disables use of configuration from `.eslintrc.*` and `package.json` files. For flat config mode, use `--no-config-lookup` instead. * **Argument Type**: No argument. @@ -150,7 +151,7 @@ If `.eslintrc.*` and/or `package.json` files are also used for configuration (i. #### `--env` -This option enables specific environments. +**eslintrc Mode Only.** This option enables specific environments. * **Argument Type**: String. One of the available environments. * **Multiple Arguments**: Yes @@ -166,7 +167,7 @@ npx eslint --env browser --env node file.js #### `--ext` -This option allows you to specify which file extensions ESLint uses when searching for target files in the directories you specify. +**eslintrc Mode Only.** This option allows you to specify which file extensions ESLint uses when searching for target files in the directories you specify. * **Argument Type**: String. File extension. * **Multiple Arguments**: Yes @@ -232,7 +233,7 @@ echo '3 ** 4' | npx eslint --stdin --parser-options ecmaVersion:7 # succeeds, ya #### `--resolve-plugins-relative-to` -Changes the directory where plugins are resolved from. +**eslintrc Mode Only.** Changes the directory where plugins are resolved from. * **Argument Type**: String. Path to directory. * **Multiple Arguments**: No @@ -374,7 +375,7 @@ npx eslint --fix --fix-type suggestion,layout . #### `--ignore-path` -This option allows you to specify the file to use as your `.eslintignore`. +**eslintrc Mode Only.** This option allows you to specify the file to use as your `.eslintignore`. * **Argument Type**: String. Path to file. * **Multiple Arguments**: No @@ -577,7 +578,7 @@ This option causes ESLint to report disable directive comments like `// eslint-d * **Argument Type**: No argument. -This can be useful to prevent future errors from unexpectedly being suppressed, by cleaning up old `eslint-disable` comments which are no longer applicable. +This can be useful to prevent future errors from unexpectedly being suppressed, by cleaning up old `eslint-disable` and `eslint-enable` comments which are no longer applicable. ::: warning When using this option, it is possible that new errors start being reported whenever ESLint or custom rules are upgraded. @@ -714,6 +715,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/configuration-files-new.md b/docs/src/use/configure/configuration-files-new.md index 7693048c28b..96907fcf05f 100644 --- a/docs/src/use/configure/configuration-files-new.md +++ b/docs/src/use/configure/configuration-files-new.md @@ -26,11 +26,42 @@ export default [ "prefer-const": "error" } } -] +]; ``` In this example, the configuration array contains just one configuration object. The configuration object enables two rules: `semi` and `prefer-const`. These rules are applied to all of the files ESLint processes using this config file. +If your project does not specify `"type":"module"` in its `package.json` file, then `eslint.config.js` must be in CommonJS format, such as: + +```js +module.exports = [ + { + rules: { + semi: "error", + "prefer-const": "error" + } + } +]; +``` + +The configuration file can also export a promise that resolves to the configuration array. This can be useful for using ESM dependencies in CommonJS configuration files, as in this example: + +```js +module.exports = (async () => { + + const someDependency = await import("some-esm-dependency"); + + return [ + // ... use `someDependency` here + ]; + +})(); +``` + +::: warning +ESLint only automatically looks for a config file named `eslint.config.js` and does not look for `eslint.config.cjs` or `eslint.config.mjs`. If you'd like to specify a different config filename than the default, use the `--config` command line option. +::: + ## Configuration Objects Each configuration object contains all of the information ESLint needs to execute on a set of files. Each configuration object is made up of these properties: @@ -45,7 +76,7 @@ Each configuration object contains all of the information ESLint needs to execut * `parserOptions` - An object specifying additional options that are passed directly to the `parse()` or `parseForESLint()` method on the parser. The available options are parser-dependent. * `linterOptions` - An object containing settings related to the linting process. * `noInlineConfig` - A Boolean value indicating if inline configuration is allowed. - * `reportUnusedDisableDirectives` - A severity string indicating if and how unused disable directives should be tracked and reported. For legacy compatibility, `true` is equivalent to `"warn"` and `false` is equivalent to `"off"`. (default: `"off"`) + * `reportUnusedDisableDirectives` - A severity string indicating if and how unused disable and enable directives should be tracked and reported. For legacy compatibility, `true` is equivalent to `"warn"` and `false` is equivalent to `"off"`. (default: `"off"`) * `processor` - Either an object containing `preprocess()` and `postprocess()` methods or a string indicating the name of a processor inside of a plugin (i.e., `"pluginName/processorName"`). * `plugins` - An object containing a name-value mapping of plugin names to plugin objects. When `files` is specified, these plugins are only available to the matching files. * `rules` - An object containing the configured rules. When `files` or `ignores` are specified, these rule configurations are only available to the matching files. @@ -116,7 +147,9 @@ export default [ Here, the configuration object excludes files ending with `.config.js` except for `eslint.config.js`. That file still has `semi` applied. -If `ignores` is used without `files` and any other setting, then the configuration object applies to all files except the ones specified in `ignores`, for example: +Non-global `ignores` patterns can only match file names. A pattern like `"dir-to-exclude/"` will not ignore anything. To ignore everything in a particular directory, a pattern like `"dir-to-exclude/**"` should be used instead. + +If `ignores` is used without `files` and there are other keys (such as `rules`), then the configuration object applies to all files except the ones specified in `ignores`, for example: ```js export default [ @@ -159,6 +192,9 @@ export default [ ]; ``` +Note that only global `ignores` patterns can match directories. +`ignores` patterns that are specific to a configuration will only match file names. + #### Cascading configuration objects When more than one configuration object matches a given filename, the configuration objects are merged with later objects overriding previous objects when there is a conflict. For example: @@ -208,7 +244,7 @@ export default [ #### Reporting unused disable directives -Disable directives such as `/*eslint-disable*/` and `/*eslint-disable-next-line*/` are used to disable ESLint rules around certain portions of code. As code changes, it's possible for these directives to no longer be needed because the code has changed in such a way that the rule is no longer triggered. You can enable reporting of these unused disable directives by setting the `reportUnusedDisableDirectives` option to a severity string, as in this example: +Disable and enable directives such as `/*eslint-disable*/` and `/*eslint-disable-next-line*/` are used to disable ESLint rules around certain portions of code. As code changes, it's possible for these directives to no longer be needed because the code has changed in such a way that the rule is no longer triggered. You can enable reporting of these unused disable directives by setting the `reportUnusedDisableDirectives` option to a severity string, as in this example: ```js export default [ @@ -350,7 +386,6 @@ Apart from the ECMAScript standard built-in globals, which are automatically ena ```js import globals from "globals"; - export default [ { languageOptions: { @@ -442,7 +477,7 @@ import jsdoc from "eslint-plugin-jsdoc"; export default [ // configuration included in plugin - jsdoc.configs.recommended, + jsdoc.configs["flat/recommended"], // other configuration objects... { files: ["**/*.js"], @@ -638,7 +673,7 @@ When ESLint is run on the command line, it first checks the current working dire You can prevent this search for `eslint.config.js` by setting the `ESLINT_USE_FLAT_CONFIG` environment variable to `true` and using the `-c` or `--config` option on the command line to specify an alternate configuration file, such as: ```shell -ESLINT_USE_FLAT_CONFIG=true npx eslint -c some-other-file.js **/*.js +ESLINT_USE_FLAT_CONFIG=true npx eslint --config some-other-file.js **/*.js ``` In this case, ESLint does not search for `eslint.config.js` and instead uses `some-other-file.js`. diff --git a/docs/src/use/configure/ignore.md b/docs/src/use/configure/ignore.md index ffc23428ee0..16f1bfbcdc9 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/configure/language-options.md b/docs/src/use/configure/language-options.md index 4f8fa148bce..4bcd63e3c98 100644 --- a/docs/src/use/configure/language-options.md +++ b/docs/src/use/configure/language-options.md @@ -26,6 +26,8 @@ An environment provides predefined global variables. The available environments * `es2020` - adds all ECMAScript 2020 globals and automatically sets the `ecmaVersion` parser option to 11. * `es2021` - adds all ECMAScript 2021 globals and automatically sets the `ecmaVersion` parser option to 12. * `es2022` - adds all ECMAScript 2022 globals and automatically sets the `ecmaVersion` parser option to 13. +* `es2023` - adds all ECMAScript 2023 globals and automatically sets the `ecmaVersion` parser option to 14. +* `es2024` - adds all ECMAScript 2024 globals and automatically sets the `ecmaVersion` parser option to 15. * `worker` - web workers global variables. * `amd` - defines `require()` and `define()` as global variables as per the [amd](https://github.com/amdjs/amdjs-api/blob/master/AMD.md) spec. * `mocha` - adds all of the Mocha testing global variables. @@ -189,11 +191,11 @@ ESLint allows you to specify the JavaScript language options you want to support Please note that supporting JSX syntax is not the same as supporting React. React applies specific semantics to JSX syntax that ESLint doesn't recognize. We recommend using [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) if you are using React. -By the same token, supporting ES6 syntax is not the same as supporting new ES6 globals (e.g., new types such as `Set`). For ES6 syntax, use `{ "parserOptions": { "ecmaVersion": 6 } }`; for new ES6 global variables, use `{ "env": { "es6": true } }`. Setting `{ "env": { "es6": true } }` enables ES6 syntax automatically, but `{ "parserOptions": { "ecmaVersion": 6 } }` does not enable ES6 globals automatically. +By the same token, supporting ES6 syntax is not the same as supporting new ES6 globals (e.g., new types such as `Set`). For ES6 syntax, use `{ "parserOptions": { "ecmaVersion": 6 } }`; for new ES6 global variables, use `{ "env": { "es6": true } }`. Setting `{ "env": { "es6": true } }` enables ES6 syntax automatically, but `{ "parserOptions": { "ecmaVersion": 6 } }` does not enable ES6 globals automatically. In summary, to support only ES6 syntax, use `{ "parserOptions": { "ecmaVersion": 6 } }`, and to support both ES6 syntax and new ES6 global variables, such as `Set` and others, use `{ "env": { "es6": true } }`. Parser options are set in your `.eslintrc.*` file with the `parserOptions` property. The available options are: -* `ecmaVersion` - set to 3, 5 (default), 6, 7, 8, 9, 10, 11, 12, 13, or 14 to specify the version of ECMAScript syntax you want to use. You can also set it to 2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), 2020 (same as 11), 2021 (same as 12), 2022 (same as 13), or 2023 (same as 14) to use the year-based naming. You can also set `"latest"` to use the most recently supported version. +* `ecmaVersion` - set to 3, 5 (default), 6, 7, 8, 9, 10, 11, 12, 13, 14, or 15 to specify the version of ECMAScript syntax you want to use. You can also set it to 2015 (same as 6), 2016 (same as 7), 2017 (same as 8), 2018 (same as 9), 2019 (same as 10), 2020 (same as 11), 2021 (same as 12), 2022 (same as 13), 2023 (same as 14), or 2024 (same as 15) to use the year-based naming. You can also set `"latest"` to use the most recently supported version. * `sourceType` - set to `"script"` (default) or `"module"` if your code is in ECMAScript modules. * `allowReserved` - allow the use of reserved words as identifiers (if `ecmaVersion` is 3). * `ecmaFeatures` - an object indicating which additional language features you'd like to use: diff --git a/docs/src/use/configure/migration-guide.md b/docs/src/use/configure/migration-guide.md new file mode 100644 index 00000000000..d89a8347dfc --- /dev/null +++ b/docs/src/use/configure/migration-guide.md @@ -0,0 +1,614 @@ +--- +title: Configuration Migration Guide +eleventyNavigation: + key: migration guide + parent: configure + title: Configuration Migration Guide + order: 8 +--- + +This guide provides an overview of how you can migrate your ESLint configuration file from the eslintrc format (typically configured in `.eslintrc.js` or `.eslintrc.json` files) to the new flat config format (typically configured in an `eslint.config.js` file). + +To learn more about the flat config format, refer to [this blog post](https://eslint.org/blog/2022/08/new-config-system-part-2/). + +For reference information on these configuration formats, refer to the following documentation: + +* [eslintrc configuration files](configuration-files) +* [flat configuration files](configuration-files-new) + +## Start Using Flat Config Files + +Starting with ESLint v9.0.0, the flat config file format will be the default configuration file format. Once ESLint v9.0.0 is released, you can start using the flat config file format without any additional configuration. + +To use flat config with ESLint v8, place a `eslint.config.js` file in the root of your project **or** set the `ESLINT_USE_FLAT_CONFIG` environment variable to `true`. + +## Things That Haven’t Changed between Configuration File Formats + +While the configuration file format has changed from eslintrc to flat config, the following has stayed the same: + +* Syntax for configuring rules +* Syntax for configuring processors +* The CLI, except for the flag changes noted in [CLI Flag Changes](#cli-flag-changes). +* Global variables are configured the same way, but on a different property (see [Configuring Language Options](#configuring-language-options)). + +## Key Differences between Configuration Formats + +A few of the most notable differences between the eslintrc and flat config formats are the following: + +### Importing Plugins and Custom Parsers + +Eslintrc files use string-based import system inside the `plugins` property to load plugins and inside the `extends` property to load external configurations. + +Flat config files represent plugins and parsers as JavaScript objects. This means you can use CommonJS `require()` or ES module `import` statements to load plugins and custom parsers from external files. + +For example, this eslintrc config file loads `eslint-plugin-jsdoc` and configures rules from that plugin: + +```javascript +// .eslintrc.js + +module.exports = { + // ...other config + plugins: ["jsdoc"], + rules: { + "jsdoc/require-description": "error", + "jsdoc/check-values": "error" + } + // ...other config +}; +``` + +In flat config, you would do the same thing like this: + +```javascript +// eslint.config.js + +import jsdoc from "eslint-plugin-jsdoc"; + +export default [ + { + files: ["**/*.js"], + plugins: { + jsdoc: jsdoc + }, + rules: { + "jsdoc/require-description": "error", + "jsdoc/check-values": "error" + } + } +]; +``` + +### Custom Parsers + +In eslintrc files, importing a custom parser is similar to importing a plugin: you use a string to specify the name of the parser. + +In flat config files, import a custom parser as a module, then assign it to the `languageOptions.parser` property of a configuration object. + +For example, this eslintrc config file uses the `@babel/eslint-parser` parser: + +```javascript +// .eslintrc.js + +module.exports = { + // ...other config + parser: "@babel/eslint-parser", + // ...other config +}; +``` + +In flat config, you would do the same thing like this: + +```javascript +// eslint.config.js + +import babelParser from "@babel/eslint-parser"; + +export default [ + { + // ...other config + languageOptions: { + parser: babelParser + } + // ...other config + } +]; +``` + +### Processors + +In eslintrc files, processors had to be defined in a plugin, and then referenced by name in the configuration. Processors beginning with a dot indicated a [file extension-named processor](../../extend/custom-processors#file-extension-named-processor) which ESLint would automatically configure for that file extension. + +In flat config files, processors can still be referenced from plugins by their name, but they can now also be inserted directly into the configuration. Processors will _never_ be automatically configured, and must be explicitly set in the configuration. + +As an example with a custom plugin with processors: + +```javascript +// node_modules/eslint-plugin-someplugin/index.js +module.exports = { + processors: { + ".md": { + preprocess() {}, + postprocess() {} + }, + "someProcessor": { + preprocess() {}, + postprocess() {} + } + } +}; +``` + +In eslintrc, you would configure as follows: + +```javascript +// .eslintrc.js +module.exports = { + plugins: ["someplugin"], + processor: "someplugin/someProcessor" +}; +``` + +ESLint would also automatically add the equivalent of the following: + +```javascript +{ + overrides: [{ + files: ["**/*.md"], + processor: "someplugin/.md" + }] +} +``` + +In flat config, the following are all valid ways to express the same: + +```javascript +// eslint.config.js +import somePlugin from "eslint-plugin-someplugin"; + +export default [ + { + plugins: { somePlugin }, + processor: "somePlugin/someProcessor" + }, + { + plugins: { somePlugin }, + // We can embed the processor object in the config directly + processor: somePlugin.processors.someProcessor + }, + { + // We don't need the plugin to be present in the config to use the processor directly + processor: somePlugin.processors.someProcessor + } +]; +``` + +Note that because the `.md` processor is _not_ automatically added by flat config, you also need to specify an extra configuration element: + +```javascript +{ + files: ["**/*.md"], + processor: somePlugin.processors[".md"] +} +``` + +### Glob-Based Configs + +By default, eslintrc files lint all files (except those covered by `.eslintignore`) in the directory in which they’re placed and its child directories. If you want to have different configurations for different file glob patterns, you can specify them in the `overrides` property. + +By default, flat config files support different glob pattern-based configs in exported array. You can include the glob pattern in a config object's `files` property. If you don't specify a `files` property, the config defaults to the glob pattern `"**/*.{js,mjs,cjs}"`. Basically, all configuration in the flat config file is like the eslintrc `overrides` property. + +#### eslintrc Examples + +For example, this eslintrc file applies to all files in the directory where it is placed and its child directories: + +```javascript +// .eslintrc.js + +module.exports = { + // ...other config + rules: { + semi: ["warn", "always"] + } +}; +``` + +This eslintrc file supports multiple configs with overrides: + +```javascript +// .eslintrc.js + +module.exports = { + // ...other config + overrides: [ + { + files: ["src/**/*"], + rules: { + semi: ["warn", "always"] + } + }, + { + files:["test/**/*"], + rules: { + "no-console": "off" + } + } + ] +}; +``` + +For flat config, here is a configuration with the default glob pattern: + +```javascript +// eslint.config.js + +import js from "@eslint/js"; + +export default [ + js.configs.recommended, // Recommended config applied to all files + // Override the recommended config + { + rules: { + indent: ["error", 2], + "no-unused-vars": "warn" + } + // ...other configuration + } +]; +``` + +A flag config example configuration supporting multiple configs for different glob patterns: + +```javascript +// eslint.config.js + +import js from "@eslint/js"; + +export default [ + js.configs.recommended, // Recommended config applied to all files + // File-pattern specific overrides + { + files: ["src/**/*", "test/**/*"], + rules: { + semi: ["warn", "always"] + } + }, + { + files:["test/**/*"], + rules: { + "no-console": "off" + } + } + // ...other configurations +]; +``` + +### Configuring Language Options + +In eslintrc files, you configure various language options across the `env`, `globals` and `parserOptions` properties. Groups of global variables for specific runtimes (e.g. `document` and `window` for browser JavaScript; `process` and `require` for Node.js) are configured with the `env` property. + +In flat config files, the `globals`, and `parserOptions` are consolidated under the `languageOptions` key; the `env` property doesn't exist. Groups of global variables for specific runtimes are imported from the [globals](https://www.npmjs.com/package/globals) npm package and included in the `globals` property. You can use the spread operator (`...`) to import multiple globals at once. + +For example, here's an eslintrc file with language options: + +```javascript +// .eslintrc.js + +module.exports = { + env: { + browser: true, + }, + globals: { + myCustomGlobal: "readonly", + }, + parserOptions: { + ecmaVersion: 2022, + sourceType: "module" + } + // ...other config +} +``` + +Here's the same configuration in flat config: + +```javascript +// eslint.config.js + +import globals from "globals"; + +export default [ + { + languageOptions: { + globals: { + ...globals.browser, + myCustomGlobal: "readonly" + }, + parserOptions: { + ecmaVersion: 2022, + sourceType: "module" + } + } + // ...other config + } +]; +``` + +### `eslint-env` Configuration Comments + +In the eslintrc config system it was possible to use `eslint-env` configuration comments to define globals for a file. +These comments are no longer recognized when linting with flat config: in a future version of ESLint, `eslint-env` comments will be reported as errors. +For this reason, when migrating from eslintrc to flat config, `eslint-env` configuration comments should be removed from all files. +They can be either replaced with equivalent but more verbose `global` configuration comments, or dropped in favor of `globals` definitions in the config file. + +For example, when using eslintrc, a file to be linted could look like this: + +```javascript +// tests/my-file.js + +/* eslint-env mocha */ + +describe("unit tests", () => { + it("should pass", () => { + // ... + }); +}); +``` + +In the above example, `describe` and `it` would be recognized as global identifiers because of the `/* eslint-env mocha */` comment. + +The same effect can be achieved with flat config with a `global` configuration comment, e.g.: + +```javascript +// tests/my-file.js + +/* global describe, it -- Globals defined by Mocha */ + +describe("unit tests", () => { + it("should pass", () => { + // ... + }); +}); +``` + +Another option is to remove the comment from the file being linted and define the globals in the configuration, for example: + +```javascript +// eslint.config.js + +import globals from "globals"; + +export default [ + // ...other config + { + files: [ + "tests/**" + ], + languageOptions: { + globals: { + ...globals.mocha + } + } + } +]; +``` + +### Predefined and Shareable Configs + +In eslintrc files, use the `extends` property to use predefined and shareable configs. ESLint comes with two predefined configs that you can access as strings: + +* `"eslint:recommended"`: the rules recommended by ESLint +* `"eslint:all"`: all rules shipped with ESLint + +You can also use the `extends` property to extend a shareable config. Shareable configs can either be paths to local config files or npm package names. + +In flat config files, predefined configs are imported from separate modules into flat config files. The `recommended` and `all` rules configs are located in the [`@eslint/js`](https://www.npmjs.com/package/@eslint/js) package. You must import this package to use these configs: + +```shell +npm install @eslint/js --save-dev +``` + +You can add each of these configs to the exported array or expose specific rules from them. You must import the modules for local config files and npm package configs with flat config. + +For example, here's an eslintrc file using the built-in `eslint:recommended` config: + +```javascript +// .eslintrc.js + +module.exports = { + // ...other config + extends: "eslint:recommended", + rules: { + semi: ["warn", "always"] + }, + // ...other config +} +``` + +This eslintrc file uses built-in config, local custom config, and shareable config from an npm package: + +```javascript +// .eslintrc.js + +module.exports = { + // ...other config + extends: ["eslint:recommended", "./custom-config.js", "eslint-config-my-config"], + rules: { + semi: ["warn", "always"] + }, + // ...other config +} +``` + +To use the same configs in flat config, you would do the following: + +```javascript +// eslint.config.js + +import js from "@eslint/js"; +import customConfig from "./custom-config.js"; +import myConfig from "eslint-config-my-config"; + +export default [ + js.configs.recommended, + customConfig, + myConfig, + { + rules: { + semi: ["warn", "always"] + }, + // ...other config + } +]; +``` + +Note that because you are just importing JavaScript modules, you can mutate the config objects before ESLint uses them. For example, you might want to have a certain config object only apply to your test files: + +```javascript +// eslint.config.js + +import js from "@eslint/js"; +import customTestConfig from "./custom-test-config.js"; + +export default [ + js.configs.recommended, + { + ...customTestConfig, + files: ["**/*.test.js"], + }, +]; +``` + +#### Using eslintrc Configs in Flat Config + +You may find that there's a shareable config you rely on that hasn't yet been updated to flat config format. In that case, you can use the `FlatCompat` utility to translate the eslintrc format into flat config format. First, install the `@eslint/eslintrc` package: + +```shell +npm install @eslint/eslintrc --save-dev +``` + +Then, import `FlatCompat` and create a new instance to convert an existing eslintrc config. For example, if the npm package `eslint-config-my-config` is in eslintrc format, you can write this: + +```js +import { FlatCompat } from "@eslint/eslintrc"; +import path from "path"; +import { fileURLToPath } from "url"; + +// mimic CommonJS variables -- not needed if using CommonJS +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname +}); + +export default [ + + // mimic ESLintRC-style extends + ...compat.extends("eslint-config-my-config"), +]; +``` + +This example uses the `FlatCompat#extends()` method to insert the `eslint-config-my-config` into the flat config array. + +For more information about the `FlatCompat` class, please see the [package README](https://github.com/eslint/eslintrc#usage). + +### Ignoring Files + +With eslintrc, you can make ESLint ignore files by creating a separate `.eslintignore` file in the root of your project. The `.eslintignore` file uses the same glob pattern syntax as `.gitignore` files. Alternatively, you can use an `ignorePatterns` property in your eslintrc file. + +To ignore files with flat config, you can use the `ignores` property in a config object. The `ignores` property accepts an array of glob patterns. Flat config does not support loading ignore patterns from `.eslintignore` files, so you'll need to migrate those patterns directly into flat config. + +For example, here's a `.eslintignore` example you can use with an eslintrc config: + +```shell +# .eslintignore +temp.js +config/* +# ...other ignored files +``` + +`ignorePatterns` example: + +```javascript +// .eslintrc.js +module.exports = { + // ...other config + ignorePatterns: ["temp.js", "config/*"], +}; +``` + +Here are the same files ignore patterns in flat config: + +```javascript +export default [ + // ...other config + { + ignores: ["**/temp.js", "config/*"] + } +]; +``` + +Also, with flat config, dotfiles (e.g. `.dotfile.js`) are no longer ignored by default. If you want to ignore dotfiles, add files ignore pattern `"**/.*"`. + +### Linter Options + +ESlintrc files let you configure the linter itself with the `noInlineConfig` and `reportUnusedDisableDirectives` properties. + +The flat config system introduces a new top-level property `linterOptions` that you can use to configure the linter. In the `linterOptions` object, you can include `noInlineConfig` and `reportUnusedDisableDirectives`. + +For example, here's an eslintrc file with linter options enabled: + +```javascript +// .eslintrc.js + +module.exports = { + // ...other config + noInlineConfig: true, + reportUnusedDisableDirectives: true +} +``` + +Here's the same options in flat config: + +```javascript +// eslint.config.js + +export default [ + { + // ...other config + linterOptions: { + noInlineConfig: true, + reportUnusedDisableDirectives: true + } + } +]; +``` + +### CLI Flag Changes + +The following CLI flags are no longer supported with the flat config file format: + +* `--rulesdir` +* `--ext` +* `--resolve-plugins-relative-to` + +The flag `--no-eslintrc` has been replaced with `--no-config-lookup`. + +### Additional Changes + +The following changes have been made from the eslintrc to the flat config file format: + +* The `root` option no longer exists. (Flat config files act as if `root: true` is set.) +* The `files` option cannot be a single string anymore, it must be an array. +* The `sourceType` option now supports the new value `"commonjs"` (`.eslintrc` supports it too, but it was never documented). + +## TypeScript Types for Flat Config Files + +You can see the TypeScript types for the flat config file format in the DefinitelyTyped project. The interface for the objects in the config’s array is called the `FlatConfig`. + +You can view the type definitions in the [DefinitelyTyped repository on Github](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/eslint/index.d.ts). + +## Further Reading + +* [Overview of the flat config file format blog post](https://eslint.org/blog/2022/08/new-config-system-part-2/) +* [API usage of new configuration system blog post](https://eslint.org/blog/2022/08/new-config-system-part-3/) +* [Background to new configuration system blog post](https://eslint.org/blog/2022/08/new-config-system-part-1/) diff --git a/docs/src/use/configure/rules.md b/docs/src/use/configure/rules.md index 2be606fb2af..dffe4232052 100644 --- a/docs/src/use/configure/rules.md +++ b/docs/src/use/configure/rules.md @@ -144,6 +144,17 @@ You can also use this format with configuration comments, such as: ### Using configuration comments +* **Use with Caution.** Disabling ESLint rules inline should be restricted and used only in situations with a clear and + valid reason for doing so. Disabling rules inline should not be the default solution to resolve linting errors. +* **Document the Reason.** Provide a comment explaining the reason for disabling a particular rule after the `--` section of the comment. This + documentation should clarify why the rule is being disabled and why it is necessary in that specific situation. +* **Temporary Solutions.** If a disable comment is added as a temporary measure to address a pressing issue, create a follow-up task to address the underlying problem adequately. This ensures that the + disable comment is revisited and resolved at a later stage. +* **Code Reviews and Pair Programming.** Encourage team members to review each other's code regularly. Code reviews can help + identify the reasons behind disable comments and ensure that they are used appropriately. +* **Configurations.** Whenever possible, prefer using ESLint configuration files over disable comments. Configuration + files allow for consistent and project-wide rule handling. + To disable rule warnings in a part of a file, use block comments in the following format: ```js diff --git a/docs/src/use/core-concepts.md b/docs/src/use/core-concepts.md index b9dfbbbfa1c..b60ad3b0b0e 100644 --- a/docs/src/use/core-concepts.md +++ b/docs/src/use/core-concepts.md @@ -23,6 +23,23 @@ ESLint contains hundreds of built-in rules that you can use. You can also create For more information, refer to [Rules](../rules/). +### Rule Fixes + +Rules may optionally provide fixes for violations that they find. Fixes safely correct the violation without changing application logic. + +Fixes may be applied automatically with the [`--fix` command line option](https://eslint.org/docs/latest/use/command-line-interface#--fix) and via editor extensions. + +Rules that may provide fixes are marked with 🔧 in [Rules](../rules/). + +### Rule Suggestions + +Rules may optionally provide suggestions in addition to or instead of providing fixes. Suggestions differ from fixes in two ways: + +1. Suggestions may change application logic and so cannot be automatically applied. +1. Suggestions cannot be applied through the ESLint CLI and are only available through editor integrations. + +Rules that may provide suggestions are marked with 💡 in [Rules](../rules/). + ## Configuration Files An ESLint configuration file is a place where you put the configuration for ESLint in your project. You can include built-in rules, how you want them enforced, plugins with custom rules, shareable configurations, which files you want rules to apply to, and more. diff --git a/docs/src/use/formatters/html-formatter-example.html b/docs/src/use/formatters/html-formatter-example.html index 1bc28e19389..03dc7dcefef 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 May 19 2023 16:52:13 GMT-0400 (Eastern Daylight Time) + 9 problems (5 errors, 4 warnings) - Generated on Fri Nov 03 2023 19:23:39 GMT-0400 (Eastern Daylight Time)
diff --git a/docs/src/use/formatters/index.md b/docs/src/use/formatters/index.md index ec6ce537ed7..6d853e16785 100644 --- a/docs/src/use/formatters/index.md +++ b/docs/src/use/formatters/index.md @@ -71,7 +71,7 @@ Outputs results to the [Checkstyle](https://checkstyle.sourceforge.io/) format. Example output: -```text +```xml ``` @@ -109,7 +109,7 @@ Outputs results to format compatible with the [JSLint Jenkins plugin](https://pl Example output: -```text +```xml ``` @@ -119,10 +119,601 @@ Outputs JSON-serialized results. The `json-with-metadata` provides the same lint Alternatively, you can use the [ESLint Node.js API](../../integrate/nodejs-api) to programmatically use ESLint. -Example output: +Example output (formatted for easier reading): -```text -{"results":[{"filePath":"/var/lib/jenkins/workspace/Releases/eslint Release/eslint/fullOfProblems.js","messages":[{"ruleId":"no-unused-vars","severity":2,"message":"'addOne' is defined but never used.","line":1,"column":10,"nodeType":"Identifier","messageId":"unusedVar","endLine":1,"endColumn":16},{"ruleId":"use-isnan","severity":2,"message":"Use the isNaN function to compare with NaN.","line":2,"column":9,"nodeType":"BinaryExpression","messageId":"comparisonWithNaN","endLine":2,"endColumn":17},{"ruleId":"space-unary-ops","severity":2,"message":"Unexpected space before unary operator '++'.","line":3,"column":16,"nodeType":"UpdateExpression","messageId":"unexpectedBefore","endLine":3,"endColumn":20,"fix":{"range":[57,58],"text":""}},{"ruleId":"semi","severity":1,"message":"Missing semicolon.","line":3,"column":20,"nodeType":"ReturnStatement","messageId":"missingSemi","endLine":4,"endColumn":1,"fix":{"range":[60,60],"text":";"}},{"ruleId":"no-else-return","severity":1,"message":"Unnecessary 'else' after 'return'.","line":4,"column":12,"nodeType":"BlockStatement","messageId":"unexpected","endLine":6,"endColumn":6,"fix":{"range":[0,94],"text":"function addOne(i) {\n if (i != NaN) {\n return i ++\n } \n return\n \n}"}},{"ruleId":"indent","severity":1,"message":"Expected indentation of 8 spaces but found 6.","line":5,"column":1,"nodeType":"Keyword","messageId":"wrongIndentation","endLine":5,"endColumn":7,"fix":{"range":[74,80],"text":" "}},{"ruleId":"consistent-return","severity":2,"message":"Function 'addOne' expected a return value.","line":5,"column":7,"nodeType":"ReturnStatement","messageId":"missingReturnValue","endLine":5,"endColumn":13},{"ruleId":"semi","severity":1,"message":"Missing semicolon.","line":5,"column":13,"nodeType":"ReturnStatement","messageId":"missingSemi","endLine":6,"endColumn":1,"fix":{"range":[86,86],"text":";"}},{"ruleId":"no-extra-semi","severity":2,"message":"Unnecessary semicolon.","line":7,"column":2,"nodeType":"EmptyStatement","messageId":"unexpected","endLine":7,"endColumn":3,"fix":{"range":[93,95],"text":"}"}}],"suppressedMessages":[],"errorCount":5,"fatalErrorCount":0,"warningCount":4,"fixableErrorCount":2,"fixableWarningCount":4,"source":"function addOne(i) {\n if (i != NaN) {\n return i ++\n } else {\n return\n }\n};"}],"metadata":{"rulesMeta":{"no-else-return":{"type":"suggestion","docs":{"description":"Disallow `else` blocks after `return` statements in `if` statements","recommended":false,"url":"https://eslint.org/docs/latest/rules/no-else-return"},"schema":[{"type":"object","properties":{"allowElseIf":{"type":"boolean","default":true}},"additionalProperties":false}],"fixable":"code","messages":{"unexpected":"Unnecessary 'else' after 'return'."}},"indent":{"type":"layout","docs":{"description":"Enforce consistent indentation","recommended":false,"url":"https://eslint.org/docs/latest/rules/indent"},"fixable":"whitespace","schema":[{"oneOf":[{"enum":["tab"]},{"type":"integer","minimum":0}]},{"type":"object","properties":{"SwitchCase":{"type":"integer","minimum":0,"default":0},"VariableDeclarator":{"oneOf":[{"oneOf":[{"type":"integer","minimum":0},{"enum":["first","off"]}]},{"type":"object","properties":{"var":{"oneOf":[{"type":"integer","minimum":0},{"enum":["first","off"]}]},"let":{"oneOf":[{"type":"integer","minimum":0},{"enum":["first","off"]}]},"const":{"oneOf":[{"type":"integer","minimum":0},{"enum":["first","off"]}]}},"additionalProperties":false}]},"outerIIFEBody":{"oneOf":[{"type":"integer","minimum":0},{"enum":["off"]}]},"MemberExpression":{"oneOf":[{"type":"integer","minimum":0},{"enum":["off"]}]},"FunctionDeclaration":{"type":"object","properties":{"parameters":{"oneOf":[{"type":"integer","minimum":0},{"enum":["first","off"]}]},"body":{"type":"integer","minimum":0}},"additionalProperties":false},"FunctionExpression":{"type":"object","properties":{"parameters":{"oneOf":[{"type":"integer","minimum":0},{"enum":["first","off"]}]},"body":{"type":"integer","minimum":0}},"additionalProperties":false},"StaticBlock":{"type":"object","properties":{"body":{"type":"integer","minimum":0}},"additionalProperties":false},"CallExpression":{"type":"object","properties":{"arguments":{"oneOf":[{"type":"integer","minimum":0},{"enum":["first","off"]}]}},"additionalProperties":false},"ArrayExpression":{"oneOf":[{"type":"integer","minimum":0},{"enum":["first","off"]}]},"ObjectExpression":{"oneOf":[{"type":"integer","minimum":0},{"enum":["first","off"]}]},"ImportDeclaration":{"oneOf":[{"type":"integer","minimum":0},{"enum":["first","off"]}]},"flatTernaryExpressions":{"type":"boolean","default":false},"offsetTernaryExpressions":{"type":"boolean","default":false},"ignoredNodes":{"type":"array","items":{"type":"string","not":{"pattern":":exit$"}}},"ignoreComments":{"type":"boolean","default":false}},"additionalProperties":false}],"messages":{"wrongIndentation":"Expected indentation of {{expected}} but found {{actual}}."}},"space-unary-ops":{"type":"layout","docs":{"description":"Enforce consistent spacing before or after unary operators","recommended":false,"url":"https://eslint.org/docs/latest/rules/space-unary-ops"},"fixable":"whitespace","schema":[{"type":"object","properties":{"words":{"type":"boolean","default":true},"nonwords":{"type":"boolean","default":false},"overrides":{"type":"object","additionalProperties":{"type":"boolean"}}},"additionalProperties":false}],"messages":{"unexpectedBefore":"Unexpected space before unary operator '{{operator}}'.","unexpectedAfter":"Unexpected space after unary operator '{{operator}}'.","unexpectedAfterWord":"Unexpected space after unary word operator '{{word}}'.","wordOperator":"Unary word operator '{{word}}' must be followed by whitespace.","operator":"Unary operator '{{operator}}' must be followed by whitespace.","beforeUnaryExpressions":"Space is required before unary expressions '{{token}}'."}},"semi":{"type":"layout","docs":{"description":"Require or disallow semicolons instead of ASI","recommended":false,"url":"https://eslint.org/docs/latest/rules/semi"},"fixable":"code","schema":{"anyOf":[{"type":"array","items":[{"enum":["never"]},{"type":"object","properties":{"beforeStatementContinuationChars":{"enum":["always","any","never"]}},"additionalProperties":false}],"minItems":0,"maxItems":2},{"type":"array","items":[{"enum":["always"]},{"type":"object","properties":{"omitLastInOneLineBlock":{"type":"boolean"},"omitLastInOneLineClassBody":{"type":"boolean"}},"additionalProperties":false}],"minItems":0,"maxItems":2}]},"messages":{"missingSemi":"Missing semicolon.","extraSemi":"Extra semicolon."}},"consistent-return":{"type":"suggestion","docs":{"description":"Require `return` statements to either always or never specify values","recommended":false,"url":"https://eslint.org/docs/latest/rules/consistent-return"},"schema":[{"type":"object","properties":{"treatUndefinedAsUnspecified":{"type":"boolean","default":false}},"additionalProperties":false}],"messages":{"missingReturn":"Expected to return a value at the end of {{name}}.","missingReturnValue":"{{name}} expected a return value.","unexpectedReturnValue":"{{name}} expected no return value."}}}}} +```json +{ + "results": [ + { + "filePath": "/var/lib/jenkins/workspace/Releases/eslint Release/eslint/fullOfProblems.js", + "messages": [ + { + "ruleId": "no-unused-vars", + "severity": 2, + "message": "'addOne' is defined but never used.", + "line": 1, + "column": 10, + "nodeType": "Identifier", + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 16 + }, + { + "ruleId": "use-isnan", + "severity": 2, + "message": "Use the isNaN function to compare with NaN.", + "line": 2, + "column": 9, + "nodeType": "BinaryExpression", + "messageId": "comparisonWithNaN", + "endLine": 2, + "endColumn": 17 + }, + { + "ruleId": "space-unary-ops", + "severity": 2, + "message": "Unexpected space before unary operator '++'.", + "line": 3, + "column": 16, + "nodeType": "UpdateExpression", + "messageId": "unexpectedBefore", + "endLine": 3, + "endColumn": 20, + "fix": { + "range": [ + 57, + 58 + ], + "text": "" + } + }, + { + "ruleId": "semi", + "severity": 1, + "message": "Missing semicolon.", + "line": 3, + "column": 20, + "nodeType": "ReturnStatement", + "messageId": "missingSemi", + "endLine": 4, + "endColumn": 1, + "fix": { + "range": [ + 60, + 60 + ], + "text": ";" + } + }, + { + "ruleId": "no-else-return", + "severity": 1, + "message": "Unnecessary 'else' after 'return'.", + "line": 4, + "column": 12, + "nodeType": "BlockStatement", + "messageId": "unexpected", + "endLine": 6, + "endColumn": 6, + "fix": { + "range": [ + 0, + 94 + ], + "text": "function addOne(i) {\n if (i != NaN) {\n return i ++\n } \n return\n \n}" + } + }, + { + "ruleId": "indent", + "severity": 1, + "message": "Expected indentation of 8 spaces but found 6.", + "line": 5, + "column": 1, + "nodeType": "Keyword", + "messageId": "wrongIndentation", + "endLine": 5, + "endColumn": 7, + "fix": { + "range": [ + 74, + 80 + ], + "text": " " + } + }, + { + "ruleId": "consistent-return", + "severity": 2, + "message": "Function 'addOne' expected a return value.", + "line": 5, + "column": 7, + "nodeType": "ReturnStatement", + "messageId": "missingReturnValue", + "endLine": 5, + "endColumn": 13 + }, + { + "ruleId": "semi", + "severity": 1, + "message": "Missing semicolon.", + "line": 5, + "column": 13, + "nodeType": "ReturnStatement", + "messageId": "missingSemi", + "endLine": 6, + "endColumn": 1, + "fix": { + "range": [ + 86, + 86 + ], + "text": ";" + } + }, + { + "ruleId": "no-extra-semi", + "severity": 2, + "message": "Unnecessary semicolon.", + "line": 7, + "column": 2, + "nodeType": "EmptyStatement", + "messageId": "unexpected", + "endLine": 7, + "endColumn": 3, + "fix": { + "range": [ + 93, + 95 + ], + "text": "}" + } + } + ], + "suppressedMessages": [], + "errorCount": 5, + "fatalErrorCount": 0, + "warningCount": 4, + "fixableErrorCount": 2, + "fixableWarningCount": 4, + "source": "function addOne(i) {\n if (i != NaN) {\n return i ++\n } else {\n return\n }\n};" + } + ], + "metadata": { + "rulesMeta": { + "no-else-return": { + "type": "suggestion", + "docs": { + "description": "Disallow `else` blocks after `return` statements in `if` statements", + "recommended": false, + "url": "https://eslint.org/docs/latest/rules/no-else-return" + }, + "schema": [ + { + "type": "object", + "properties": { + "allowElseIf": { + "type": "boolean", + "default": true + } + }, + "additionalProperties": false + } + ], + "fixable": "code", + "messages": { + "unexpected": "Unnecessary 'else' after 'return'." + } + }, + "indent": { + "deprecated": true, + "replacedBy": [], + "type": "layout", + "docs": { + "description": "Enforce consistent indentation", + "recommended": false, + "url": "https://eslint.org/docs/latest/rules/indent" + }, + "fixable": "whitespace", + "schema": [ + { + "oneOf": [ + { + "enum": [ + "tab" + ] + }, + { + "type": "integer", + "minimum": 0 + } + ] + }, + { + "type": "object", + "properties": { + "SwitchCase": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "VariableDeclarator": { + "oneOf": [ + { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "enum": [ + "first", + "off" + ] + } + ] + }, + { + "type": "object", + "properties": { + "var": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "enum": [ + "first", + "off" + ] + } + ] + }, + "let": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "enum": [ + "first", + "off" + ] + } + ] + }, + "const": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "enum": [ + "first", + "off" + ] + } + ] + } + }, + "additionalProperties": false + } + ] + }, + "outerIIFEBody": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "enum": [ + "off" + ] + } + ] + }, + "MemberExpression": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "enum": [ + "off" + ] + } + ] + }, + "FunctionDeclaration": { + "type": "object", + "properties": { + "parameters": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "enum": [ + "first", + "off" + ] + } + ] + }, + "body": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": false + }, + "FunctionExpression": { + "type": "object", + "properties": { + "parameters": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "enum": [ + "first", + "off" + ] + } + ] + }, + "body": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": false + }, + "StaticBlock": { + "type": "object", + "properties": { + "body": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": false + }, + "CallExpression": { + "type": "object", + "properties": { + "arguments": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "enum": [ + "first", + "off" + ] + } + ] + } + }, + "additionalProperties": false + }, + "ArrayExpression": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "enum": [ + "first", + "off" + ] + } + ] + }, + "ObjectExpression": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "enum": [ + "first", + "off" + ] + } + ] + }, + "ImportDeclaration": { + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "enum": [ + "first", + "off" + ] + } + ] + }, + "flatTernaryExpressions": { + "type": "boolean", + "default": false + }, + "offsetTernaryExpressions": { + "type": "boolean", + "default": false + }, + "ignoredNodes": { + "type": "array", + "items": { + "type": "string", + "not": { + "pattern": ":exit$" + } + } + }, + "ignoreComments": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + ], + "messages": { + "wrongIndentation": "Expected indentation of {{expected}} but found {{actual}}." + } + }, + "space-unary-ops": { + "deprecated": true, + "replacedBy": [], + "type": "layout", + "docs": { + "description": "Enforce consistent spacing before or after unary operators", + "recommended": false, + "url": "https://eslint.org/docs/latest/rules/space-unary-ops" + }, + "fixable": "whitespace", + "schema": [ + { + "type": "object", + "properties": { + "words": { + "type": "boolean", + "default": true + }, + "nonwords": { + "type": "boolean", + "default": false + }, + "overrides": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + }, + "additionalProperties": false + } + ], + "messages": { + "unexpectedBefore": "Unexpected space before unary operator '{{operator}}'.", + "unexpectedAfter": "Unexpected space after unary operator '{{operator}}'.", + "unexpectedAfterWord": "Unexpected space after unary word operator '{{word}}'.", + "wordOperator": "Unary word operator '{{word}}' must be followed by whitespace.", + "operator": "Unary operator '{{operator}}' must be followed by whitespace.", + "beforeUnaryExpressions": "Space is required before unary expressions '{{token}}'." + } + }, + "semi": { + "deprecated": true, + "replacedBy": [], + "type": "layout", + "docs": { + "description": "Require or disallow semicolons instead of ASI", + "recommended": false, + "url": "https://eslint.org/docs/latest/rules/semi" + }, + "fixable": "code", + "schema": { + "anyOf": [ + { + "type": "array", + "items": [ + { + "enum": [ + "never" + ] + }, + { + "type": "object", + "properties": { + "beforeStatementContinuationChars": { + "enum": [ + "always", + "any", + "never" + ] + } + }, + "additionalProperties": false + } + ], + "minItems": 0, + "maxItems": 2 + }, + { + "type": "array", + "items": [ + { + "enum": [ + "always" + ] + }, + { + "type": "object", + "properties": { + "omitLastInOneLineBlock": { + "type": "boolean" + }, + "omitLastInOneLineClassBody": { + "type": "boolean" + } + }, + "additionalProperties": false + } + ], + "minItems": 0, + "maxItems": 2 + } + ] + }, + "messages": { + "missingSemi": "Missing semicolon.", + "extraSemi": "Extra semicolon." + } + }, + "consistent-return": { + "type": "suggestion", + "docs": { + "description": "Require `return` statements to either always or never specify values", + "recommended": false, + "url": "https://eslint.org/docs/latest/rules/consistent-return" + }, + "schema": [ + { + "type": "object", + "properties": { + "treatUndefinedAsUnspecified": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + ], + "messages": { + "missingReturn": "Expected to return a value at the end of {{name}}.", + "missingReturnValue": "{{name}} expected a return value.", + "unexpectedReturnValue": "{{name}} expected no return value." + } + } + } + } +} ``` ### json @@ -131,10 +722,164 @@ Outputs JSON-serialized results. The `json` formatter is useful when you want to Alternatively, you can use the [ESLint Node.js API](../../integrate/nodejs-api) to programmatically use ESLint. -Example output: +Example output (formatted for easier reading): -```text -[{"filePath":"/var/lib/jenkins/workspace/Releases/eslint Release/eslint/fullOfProblems.js","messages":[{"ruleId":"no-unused-vars","severity":2,"message":"'addOne' is defined but never used.","line":1,"column":10,"nodeType":"Identifier","messageId":"unusedVar","endLine":1,"endColumn":16},{"ruleId":"use-isnan","severity":2,"message":"Use the isNaN function to compare with NaN.","line":2,"column":9,"nodeType":"BinaryExpression","messageId":"comparisonWithNaN","endLine":2,"endColumn":17},{"ruleId":"space-unary-ops","severity":2,"message":"Unexpected space before unary operator '++'.","line":3,"column":16,"nodeType":"UpdateExpression","messageId":"unexpectedBefore","endLine":3,"endColumn":20,"fix":{"range":[57,58],"text":""}},{"ruleId":"semi","severity":1,"message":"Missing semicolon.","line":3,"column":20,"nodeType":"ReturnStatement","messageId":"missingSemi","endLine":4,"endColumn":1,"fix":{"range":[60,60],"text":";"}},{"ruleId":"no-else-return","severity":1,"message":"Unnecessary 'else' after 'return'.","line":4,"column":12,"nodeType":"BlockStatement","messageId":"unexpected","endLine":6,"endColumn":6,"fix":{"range":[0,94],"text":"function addOne(i) {\n if (i != NaN) {\n return i ++\n } \n return\n \n}"}},{"ruleId":"indent","severity":1,"message":"Expected indentation of 8 spaces but found 6.","line":5,"column":1,"nodeType":"Keyword","messageId":"wrongIndentation","endLine":5,"endColumn":7,"fix":{"range":[74,80],"text":" "}},{"ruleId":"consistent-return","severity":2,"message":"Function 'addOne' expected a return value.","line":5,"column":7,"nodeType":"ReturnStatement","messageId":"missingReturnValue","endLine":5,"endColumn":13},{"ruleId":"semi","severity":1,"message":"Missing semicolon.","line":5,"column":13,"nodeType":"ReturnStatement","messageId":"missingSemi","endLine":6,"endColumn":1,"fix":{"range":[86,86],"text":";"}},{"ruleId":"no-extra-semi","severity":2,"message":"Unnecessary semicolon.","line":7,"column":2,"nodeType":"EmptyStatement","messageId":"unexpected","endLine":7,"endColumn":3,"fix":{"range":[93,95],"text":"}"}}],"suppressedMessages":[],"errorCount":5,"fatalErrorCount":0,"warningCount":4,"fixableErrorCount":2,"fixableWarningCount":4,"source":"function addOne(i) {\n if (i != NaN) {\n return i ++\n } else {\n return\n }\n};"}] +```json +[ + { + "filePath": "/var/lib/jenkins/workspace/Releases/eslint Release/eslint/fullOfProblems.js", + "messages": [ + { + "ruleId": "no-unused-vars", + "severity": 2, + "message": "'addOne' is defined but never used.", + "line": 1, + "column": 10, + "nodeType": "Identifier", + "messageId": "unusedVar", + "endLine": 1, + "endColumn": 16 + }, + { + "ruleId": "use-isnan", + "severity": 2, + "message": "Use the isNaN function to compare with NaN.", + "line": 2, + "column": 9, + "nodeType": "BinaryExpression", + "messageId": "comparisonWithNaN", + "endLine": 2, + "endColumn": 17 + }, + { + "ruleId": "space-unary-ops", + "severity": 2, + "message": "Unexpected space before unary operator '++'.", + "line": 3, + "column": 16, + "nodeType": "UpdateExpression", + "messageId": "unexpectedBefore", + "endLine": 3, + "endColumn": 20, + "fix": { + "range": [ + 57, + 58 + ], + "text": "" + } + }, + { + "ruleId": "semi", + "severity": 1, + "message": "Missing semicolon.", + "line": 3, + "column": 20, + "nodeType": "ReturnStatement", + "messageId": "missingSemi", + "endLine": 4, + "endColumn": 1, + "fix": { + "range": [ + 60, + 60 + ], + "text": ";" + } + }, + { + "ruleId": "no-else-return", + "severity": 1, + "message": "Unnecessary 'else' after 'return'.", + "line": 4, + "column": 12, + "nodeType": "BlockStatement", + "messageId": "unexpected", + "endLine": 6, + "endColumn": 6, + "fix": { + "range": [ + 0, + 94 + ], + "text": "function addOne(i) {\n if (i != NaN) {\n return i ++\n } \n return\n \n}" + } + }, + { + "ruleId": "indent", + "severity": 1, + "message": "Expected indentation of 8 spaces but found 6.", + "line": 5, + "column": 1, + "nodeType": "Keyword", + "messageId": "wrongIndentation", + "endLine": 5, + "endColumn": 7, + "fix": { + "range": [ + 74, + 80 + ], + "text": " " + } + }, + { + "ruleId": "consistent-return", + "severity": 2, + "message": "Function 'addOne' expected a return value.", + "line": 5, + "column": 7, + "nodeType": "ReturnStatement", + "messageId": "missingReturnValue", + "endLine": 5, + "endColumn": 13 + }, + { + "ruleId": "semi", + "severity": 1, + "message": "Missing semicolon.", + "line": 5, + "column": 13, + "nodeType": "ReturnStatement", + "messageId": "missingSemi", + "endLine": 6, + "endColumn": 1, + "fix": { + "range": [ + 86, + 86 + ], + "text": ";" + } + }, + { + "ruleId": "no-extra-semi", + "severity": 2, + "message": "Unnecessary semicolon.", + "line": 7, + "column": 2, + "nodeType": "EmptyStatement", + "messageId": "unexpected", + "endLine": 7, + "endColumn": 3, + "fix": { + "range": [ + 93, + 95 + ], + "text": "}" + } + } + ], + "suppressedMessages": [], + "errorCount": 5, + "fatalErrorCount": 0, + "warningCount": 4, + "fixableErrorCount": 2, + "fixableWarningCount": 4, + "source": "function addOne(i) {\n if (i != NaN) {\n return i ++\n } else {\n return\n }\n};" + } +] ``` ### junit @@ -143,7 +888,7 @@ Outputs results to format compatible with the [JUnit Jenkins plugin](https://plu Example output: -```text +```xml diff --git a/eslint.config.js b/eslint.config.js index 5c488a57a15..40ebe8c08f7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -27,20 +27,16 @@ const path = require("path"); const internalPlugin = require("eslint-plugin-internal-rules"); -const eslintPlugin = require("eslint-plugin-eslint-plugin"); -const { FlatCompat } = require("@eslint/eslintrc"); -const js = require("./packages/js"); +const eslintPluginRulesRecommendedConfig = require("eslint-plugin-eslint-plugin/configs/rules-recommended"); +const eslintPluginTestsRecommendedConfig = require("eslint-plugin-eslint-plugin/configs/tests-recommended"); const globals = require("globals"); +const merge = require("lodash.merge"); +const eslintConfigESLintCJS = require("eslint-config-eslint/cjs"); //----------------------------------------------------------------------------- // Helpers //----------------------------------------------------------------------------- -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended -}); - const INTERNAL_FILES = { CLI_ENGINE_PATTERN: "lib/cli-engine/**/*", LINTER_PATTERN: "lib/linter/**/*", @@ -79,7 +75,7 @@ function createInternalFilesPatterns(pattern = null) { } module.exports = [ - ...compat.extends("eslint"), + ...eslintConfigESLintCJS, { ignores: [ "build/**", @@ -99,22 +95,11 @@ module.exports = [ }, { plugins: { - "internal-rules": internalPlugin, - "eslint-plugin": eslintPlugin + "internal-rules": internalPlugin }, languageOptions: { ecmaVersion: "latest" }, - - /* - * it fixes eslint-plugin-jsdoc's reports: "Invalid JSDoc tag name "template" jsdoc/check-tag-names" - * refs: https://github.com/gajus/eslint-plugin-jsdoc#check-tag-names - */ - settings: { - jsdoc: { - mode: "typescript" - } - }, rules: { "internal-rules/multiline-comment-style": "error" } @@ -129,33 +114,31 @@ module.exports = [ { files: ["lib/rules/*", "tools/internal-rules/*"], ignores: ["**/index.js"], - rules: { - ...eslintPlugin.configs["rules-recommended"].rules, - "eslint-plugin/no-missing-message-ids": "error", - "eslint-plugin/no-unused-message-ids": "error", - "eslint-plugin/prefer-message-ids": "error", - "eslint-plugin/prefer-placeholders": "error", - "eslint-plugin/prefer-replace-text": "error", - "eslint-plugin/report-message-format": ["error", "[^a-z].*\\.$"], - "eslint-plugin/require-meta-docs-description": ["error", { pattern: "^(Enforce|Require|Disallow) .+[^. ]$" }], - "internal-rules/no-invalid-meta": "error" - } + ...merge({}, eslintPluginRulesRecommendedConfig, { + rules: { + "eslint-plugin/prefer-placeholders": "error", + "eslint-plugin/prefer-replace-text": "error", + "eslint-plugin/report-message-format": ["error", "[^a-z].*\\.$"], + "eslint-plugin/require-meta-docs-description": ["error", { pattern: "^(Enforce|Require|Disallow) .+[^. ]$" }], + "internal-rules/no-invalid-meta": "error" + } + }) }, { files: ["lib/rules/*"], - ignores: ["index.js"], + ignores: ["**/index.js"], rules: { "eslint-plugin/require-meta-docs-url": ["error", { pattern: "https://eslint.org/docs/latest/rules/{{name}}" }] } }, { files: ["tests/lib/rules/*", "tests/tools/internal-rules/*"], - rules: { - ...eslintPlugin.configs["tests-recommended"].rules, - "eslint-plugin/prefer-output-null": "error", - "eslint-plugin/test-case-property-ordering": "error", - "eslint-plugin/test-case-shorthand-strings": "error" - } + ...merge({}, eslintPluginTestsRecommendedConfig, { + rules: { + "eslint-plugin/test-case-property-ordering": "error", + "eslint-plugin/test-case-shorthand-strings": "error" + } + }) }, { files: ["tests/**/*.js"], @@ -243,7 +226,6 @@ module.exports = [ files: [INTERNAL_FILES.RULE_TESTER_PATTERN], rules: { "n/no-restricted-require": ["error", [ - ...createInternalFilesPatterns(INTERNAL_FILES.RULE_TESTER_PATTERN), resolveAbsolutePath("lib/cli-engine/index.js") ]] } diff --git a/karma.conf.js b/karma.conf.js deleted file mode 100644 index 606d13f88f6..00000000000 --- a/karma.conf.js +++ /dev/null @@ -1,125 +0,0 @@ -"use strict"; -const os = require("os"); -const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); - -if (os.platform === "linux" && os.arch() === "arm64") { - - // For arm64 architecture, install chromium-browser using "apt-get install chromium-browser" - process.env.CHROME_BIN = "/usr/bin/chromium-browser"; -} else { - process.env.CHROME_BIN = require("puppeteer").executablePath(); -} - -module.exports = function(config) { - config.set({ - - // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: "", - - // next three sections allow console.log to work - client: { - captureConsole: true - }, - - browserConsoleLogOptions: { - terminal: true, - level: "log" - }, - - /* - * frameworks to use - * available frameworks: https://npmjs.org/browse/keyword/karma-adapter - */ - frameworks: ["mocha", "webpack"], - - - // list of files / patterns to load in the browser - files: [ - "tests/lib/linter/linter.js" - ], - - - // list of files to exclude - exclude: [ - ], - - - /* - * preprocess matching files before serving them to the browser - * available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor - */ - preprocessors: { - "tests/lib/linter/linter.js": ["webpack"] - }, - webpack: { - mode: "none", - plugins: [ - new NodePolyfillPlugin() - ], - resolve: { - alias: { - "../../../lib/linter$": "../../../build/eslint.js" - } - }, - stats: "errors-only" - }, - webpackMiddleware: { - logLevel: "error" - }, - - - /* - * test results reporter to use - * possible values: "dots", "progress" - * available reporters: https://npmjs.org/browse/keyword/karma-reporter - */ - reporters: ["mocha"], - - mochaReporter: { - output: "minimal" - }, - - // web server port - port: 9876, - - - // enable / disable colors in the output (reporters and logs) - colors: true, - - - /* - * level of logging - * possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - */ - logLevel: config.LOG_INFO, - - - // enable / disable watching file and executing tests whenever any file changes - autoWatch: false, - - - /* - * start these browsers - * available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - */ - browsers: ["HeadlessChrome"], - customLaunchers: { - HeadlessChrome: { - base: "ChromeHeadless", - flags: ["--no-sandbox"] - } - }, - - /* - * Continuous Integration mode - * if true, Karma captures browsers, runs the tests and exits - */ - singleRun: true, - - /* - * Concurrency level - * how many browser should be started simultaneous - */ - concurrency: Infinity - }); -}; diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 2814bd81541..aafb3deb8be 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -158,7 +158,17 @@ function validateFixTypes(fixTypes) { * @private */ function calculateStatsPerFile(messages) { - return messages.reduce((stat, message) => { + const stat = { + errorCount: 0, + fatalErrorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0 + }; + + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + if (message.fatal || message.severity === 2) { stat.errorCount++; if (message.fatal) { @@ -173,14 +183,8 @@ function calculateStatsPerFile(messages) { stat.fixableWarningCount++; } } - return stat; - }, { - errorCount: 0, - fatalErrorCount: 0, - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0 - }); + } + return stat; } /** @@ -190,20 +194,25 @@ function calculateStatsPerFile(messages) { * @private */ function calculateStatsPerRun(results) { - return results.reduce((stat, result) => { - stat.errorCount += result.errorCount; - stat.fatalErrorCount += result.fatalErrorCount; - stat.warningCount += result.warningCount; - stat.fixableErrorCount += result.fixableErrorCount; - stat.fixableWarningCount += result.fixableWarningCount; - return stat; - }, { + const stat = { errorCount: 0, fatalErrorCount: 0, warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0 - }); + }; + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + + stat.errorCount += result.errorCount; + stat.fatalErrorCount += result.fatalErrorCount; + stat.warningCount += result.warningCount; + stat.fixableErrorCount += result.fixableErrorCount; + stat.fixableWarningCount += result.fixableWarningCount; + } + + return stat; } /** diff --git a/lib/cli.js b/lib/cli.js index 01287da6417..a0f387c50de 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -92,7 +92,8 @@ async function translateOptions({ reportUnusedDisableDirectivesSeverity, resolvePluginsRelativeTo, rule, - rulesdir + rulesdir, + warnIgnored }, configType) { let overrideConfig, overrideConfigFile; @@ -183,6 +184,7 @@ async function translateOptions({ if (configType === "flat") { options.ignorePatterns = ignorePattern; + options.warnIgnored = warnIgnored; } else { options.resolvePluginsRelativeTo = resolvePluginsRelativeTo; options.rulePaths = rulesdir; @@ -317,7 +319,14 @@ const cli = { options = CLIOptions.parse(args); } catch (error) { debug("Error parsing CLI options:", error.message); - log.error(error.message); + + let errorMessage = error.message; + + if (usingFlatConfig) { + errorMessage += "\nYou're using eslint.config.js, some command line flags are no longer available. Please see https://eslint.org/docs/latest/use/command-line-interface for details."; + } + + log.error(errorMessage); return 2; } @@ -391,7 +400,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 5700668a37b..cab5e131a4d 100644 --- a/lib/config/flat-config-schema.js +++ b/lib/config/flat-config-schema.js @@ -5,6 +5,16 @@ "use strict"; +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +/* + * Note: This can be removed in ESLint v9 because structuredClone is available globally + * starting in Node.js v17. + */ +const structuredClone = require("@ungap/structured-clone").default; + //----------------------------------------------------------------------------- // Type Definitions //----------------------------------------------------------------------------- @@ -119,7 +129,7 @@ function normalizeRuleOptions(ruleOptions) { : [ruleOptions]; finalOptions[0] = ruleSeverities.get(finalOptions[0]); - return finalOptions; + return structuredClone(finalOptions); } //----------------------------------------------------------------------------- @@ -179,9 +189,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); @@ -212,6 +220,38 @@ function assertIsObject(value) { } } +/** + * The error type when there's an eslintrc-style options in a flat config. + */ +class IncompatibleKeyError extends Error { + + /** + * @param {string} key The invalid key. + */ + constructor(key) { + super("This appears to be in eslintrc format rather than flat config format."); + this.messageTemplate = "eslintrc-incompat"; + this.messageData = { key }; + } +} + +/** + * The error type when there's an eslintrc-style plugins array found. + */ +class IncompatiblePluginsError extends Error { + + /** + * Creates a new instance. + * @param {Array} plugins The plugins array. + */ + constructor(plugins) { + super("This appears to be in eslintrc format (array of strings) rather than flat config format (object)."); + this.messageTemplate = "eslintrc-plugins"; + this.messageData = { plugins }; + } +} + + //----------------------------------------------------------------------------- // Low-Level Schemas //----------------------------------------------------------------------------- @@ -313,6 +353,11 @@ const pluginsSchema = { throw new TypeError("Expected an object."); } + // make sure it's not an array, which would mean eslintrc-style is used + if (Array.isArray(value)) { + throw new IncompatiblePluginsError(value); + } + // second check the keys to make sure they are objects for (const key of Object.keys(value)) { @@ -353,48 +398,57 @@ const rulesSchema = { ...second }; - for (const ruleId of Object.keys(result)) { - // avoid hairy edge case - if (ruleId === "__proto__") { - - /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */ - delete result.__proto__; - continue; - } - - result[ruleId] = normalizeRuleOptions(result[ruleId]); - - /* - * If either rule config is missing, then the correct - * config is already present and we just need to normalize - * the severity. - */ - if (!(ruleId in first) || !(ruleId in second)) { - continue; - } - - const firstRuleOptions = normalizeRuleOptions(first[ruleId]); - const secondRuleOptions = normalizeRuleOptions(second[ruleId]); + for (const ruleId of Object.keys(result)) { - /* - * If the second rule config only has a severity (length of 1), - * then use that severity and keep the rest of the options from - * the first rule config. - */ - if (secondRuleOptions.length === 1) { - result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)]; - continue; + try { + + // avoid hairy edge case + if (ruleId === "__proto__") { + + /* eslint-disable-next-line no-proto -- Though deprecated, may still be present */ + delete result.__proto__; + continue; + } + + result[ruleId] = normalizeRuleOptions(result[ruleId]); + + /* + * If either rule config is missing, then the correct + * config is already present and we just need to normalize + * the severity. + */ + if (!(ruleId in first) || !(ruleId in second)) { + continue; + } + + const firstRuleOptions = normalizeRuleOptions(first[ruleId]); + const secondRuleOptions = normalizeRuleOptions(second[ruleId]); + + /* + * If the second rule config only has a severity (length of 1), + * then use that severity and keep the rest of the options from + * the first rule config. + */ + if (secondRuleOptions.length === 1) { + result[ruleId] = [secondRuleOptions[0], ...firstRuleOptions.slice(1)]; + continue; + } + + /* + * In any other situation, then the second rule config takes + * precedence. That means the value at `result[ruleId]` is + * already correct and no further work is necessary. + */ + } catch (ex) { + throw new Error(`Key "${ruleId}": ${ex.message}`, { cause: ex }); } - /* - * In any other situation, then the second rule config takes - * precedence. That means the value at `result[ruleId]` is - * already correct and no further work is necessary. - */ } return result; + + }, validate(value) { @@ -448,11 +502,44 @@ const sourceTypeSchema = { } }; +/** + * Creates a schema that always throws an error. Useful for warning + * about eslintrc-style keys. + * @param {string} key The eslintrc key to create a schema for. + * @returns {ObjectPropertySchema} The schema. + */ +function createEslintrcErrorSchema(key) { + return { + merge: "replace", + validate() { + throw new IncompatibleKeyError(key); + } + }; +} + +const eslintrcKeys = [ + "env", + "extends", + "globals", + "ignorePatterns", + "noInlineConfig", + "overrides", + "parser", + "parserOptions", + "reportUnusedDisableDirectives", + "root" +]; + //----------------------------------------------------------------------------- // Full schema //----------------------------------------------------------------------------- -exports.flatConfigSchema = { +const flatConfigSchema = { + + // eslintrc-style keys that should always error + ...Object.fromEntries(eslintrcKeys.map(key => [key, createEslintrcErrorSchema(key)])), + + // flat config keys settings: deepObjectAssignSchema, linterOptions: { schema: { @@ -473,3 +560,13 @@ exports.flatConfigSchema = { plugins: pluginsSchema, rules: rulesSchema }; + +//----------------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------------- + +module.exports = { + flatConfigSchema, + assertIsRuleSeverity, + assertIsRuleOptions +}; diff --git a/lib/config/rule-validator.js b/lib/config/rule-validator.js index 0b5858fb30f..eee5b40bd07 100644 --- a/lib/config/rule-validator.js +++ b/lib/config/rule-validator.js @@ -9,7 +9,8 @@ // Requirements //----------------------------------------------------------------------------- -const ajv = require("../shared/ajv")(); +const ajvImport = require("../shared/ajv"); +const ajv = ajvImport(); const { parseRuleId, getRuleFromConfig, diff --git a/lib/eslint/eslint-helpers.js b/lib/eslint/eslint-helpers.js index c81c82dc944..2ad1619ff0c 100644 --- a/lib/eslint/eslint-helpers.js +++ b/lib/eslint/eslint-helpers.js @@ -591,12 +591,12 @@ function isErrorMessage(message) { */ function createIgnoreResult(filePath, baseDir) { let message; - const isInNodeModules = baseDir && path.relative(baseDir, filePath).startsWith("node_modules"); + 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. TODO: should anything change based on this comment? + 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); } @@ -795,14 +799,15 @@ function processOptions({ // when overrideConfigFile is true that means don't do config file lookup configFile: overrideConfigFile === true ? false : overrideConfigFile, overrideConfig, - cwd, + cwd: path.normalize(cwd), errorOnUnmatchedPattern, fix, fixTypes, globInputPaths, ignore, ignorePatterns, - reportUnusedDisableDirectives + reportUnusedDisableDirectives, + warnIgnored }; } diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 056e40f1461..3717b9fe447 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -289,7 +289,7 @@ function processOptions({ cacheLocation, cacheStrategy, configFile: overrideConfigFile, - cwd, + cwd: path.normalize(cwd), errorOnUnmatchedPattern, extensions, fix, diff --git a/lib/eslint/flat-eslint.js b/lib/eslint/flat-eslint.js index dc8bf0cea50..8f86b9a5962 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 */ //------------------------------------------------------------------------------ @@ -103,7 +104,17 @@ const importedConfigFileModificationTime = new Map(); * @private */ function calculateStatsPerFile(messages) { - return messages.reduce((stat, message) => { + const stat = { + errorCount: 0, + fatalErrorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0 + }; + + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + if (message.fatal || message.severity === 2) { stat.errorCount++; if (message.fatal) { @@ -118,37 +129,8 @@ function calculateStatsPerFile(messages) { stat.fixableWarningCount++; } } - return stat; - }, { - errorCount: 0, - fatalErrorCount: 0, - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0 - }); -} - -/** - * It will calculate the error and warning count for collection of results from all files - * @param {LintResult[]} results Collection of messages from all the files - * @returns {Object} Contains the stats - * @private - */ -function calculateStatsPerRun(results) { - return results.reduce((stat, result) => { - stat.errorCount += result.errorCount; - stat.fatalErrorCount += result.fatalErrorCount; - stat.warningCount += result.warningCount; - stat.fixableErrorCount += result.fixableErrorCount; - stat.fixableWarningCount += result.fixableWarningCount; - return stat; - }, { - errorCount: 0, - fatalErrorCount: 0, - warningCount: 0, - fixableErrorCount: 0, - fixableWarningCount: 0 - }); + } + return stat; } /** @@ -550,43 +532,6 @@ function shouldMessageBeFixed(message, config, fixTypes) { return Boolean(rule && rule.meta && fixTypes.has(rule.meta.type)); } -/** - * Collect used deprecated rules. - * @param {Array} configs The configs to evaluate. - * @returns {IterableIterator} Used deprecated rules. - */ -function *iterateRuleDeprecationWarnings(configs) { - const processedRuleIds = new Set(); - - for (const config of configs) { - for (const [ruleId, ruleConfig] of Object.entries(config.rules)) { - - // Skip if it was processed. - if (processedRuleIds.has(ruleId)) { - continue; - } - processedRuleIds.add(ruleId); - - // Skip if it's not used. - if (!getRuleSeverity(ruleConfig)) { - continue; - } - const rule = getRuleFromConfig(ruleId, config); - - // Skip if it's not deprecated. - if (!(rule && rule.meta && rule.meta.deprecated)) { - continue; - } - - // This rule was used and deprecated. - yield { - ruleId, - replacedBy: rule.meta.replacedBy || [] - }; - } - } -} - /** * Creates an error to be thrown when an array of results passed to `getRulesMetaForResults` was not created by the current engine. * @returns {TypeError} An error object. @@ -632,7 +577,6 @@ class FlatESLint { cacheFilePath, lintResultCache, defaultConfigs, - defaultIgnores: () => false, configs: null }); @@ -771,12 +715,10 @@ class FlatESLint { } const rule = getRuleFromConfig(ruleId, config); - // ensure the rule exists - if (!rule) { - throw new TypeError(`Could not find the rule "${ruleId}".`); + // ignore unknown rules + if (rule) { + resultRules.set(ruleId, rule); } - - resultRules.set(ruleId, rule); } } @@ -808,10 +750,10 @@ class FlatESLint { fixTypes, reportUnusedDisableDirectives, globInputPaths, - errorOnUnmatchedPattern + errorOnUnmatchedPattern, + warnIgnored } = eslintOptions; const startTime = Date.now(); - const usedConfigs = []; const fixTypesSet = fixTypes ? new Set(fixTypes) : null; // Delete cache file; should this be done here? @@ -855,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); @@ -869,15 +815,6 @@ class FlatESLint { return void 0; } - /* - * Store used configs for: - * - this method uses to collect used deprecated rules. - * - `--fix-type` option uses to get the loaded rule's meta data. - */ - if (!usedConfigs.includes(config)) { - usedConfigs.push(config); - } - // Skip if there is cached result. if (lintResultCache) { const cachedResult = @@ -946,22 +883,10 @@ class FlatESLint { lintResultCache.reconcile(); } - let usedDeprecatedRules; const finalResults = results.filter(result => !!result); return processLintReport(this, { - results: finalResults, - ...calculateStatsPerRun(finalResults), - - // Initialize it lazily because CLI and `ESLint` API don't use it. - get usedDeprecatedRules() { - if (!usedDeprecatedRules) { - usedDeprecatedRules = Array.from( - iterateRuleDeprecationWarnings(usedConfigs) - ); - } - return usedDeprecatedRules; - } + results: finalResults }); } @@ -989,7 +914,7 @@ class FlatESLint { const { filePath, - warnIgnored = false, + warnIgnored, ...unknownOptions } = options || {}; @@ -1003,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"); } @@ -1018,23 +943,22 @@ class FlatESLint { allowInlineConfig, cwd, fix, - reportUnusedDisableDirectives + reportUnusedDisableDirectives, + warnIgnored: constructorWarnIgnored } = eslintOptions; const results = []; const startTime = Date.now(); const resolvedFilename = path.resolve(cwd, filePath || "__placeholder__.js"); - let config; // 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 { - // TODO: Needed? - config = configs.getConfig(resolvedFilename); - // Do lint. results.push(verifyText({ text: code, @@ -1049,21 +973,9 @@ class FlatESLint { } debug(`Linting complete in: ${Date.now() - startTime}ms`); - let usedDeprecatedRules; return processLintReport(this, { - results, - ...calculateStatsPerRun(results), - - // Initialize it lazily because CLI and `ESLint` API don't use it. - get usedDeprecatedRules() { - if (!usedDeprecatedRules) { - usedDeprecatedRules = Array.from( - iterateRuleDeprecationWarnings(config) - ); - } - return usedDeprecatedRules; - } + results }); } diff --git a/lib/linter/apply-disable-directives.js b/lib/linter/apply-disable-directives.js index 13ced990ff4..c5e3c9ddc1c 100644 --- a/lib/linter/apply-disable-directives.js +++ b/lib/linter/apply-disable-directives.js @@ -30,7 +30,7 @@ function compareLocations(itemA, itemB) { /** * Groups a set of directives into sub-arrays by their parent comment. - * @param {Directive[]} directives Unused directives to be removed. + * @param {Iterable} directives Unused directives to be removed. * @returns {Directive[][]} Directives grouped by their parent comment. */ function groupByParentComment(directives) { @@ -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; @@ -177,10 +177,10 @@ function createCommentRemoval(directives, commentToken) { /** * Parses details from directives to create output Problems. - * @param {Directive[]} allDirectives Unused directives to be removed. + * @param {Iterable} allDirectives Unused directives to be removed. * @returns {{ description, fix, unprocessedDirective }[]} Details for later creation of output Problems. */ -function processUnusedDisableDirectives(allDirectives) { +function processUnusedDirectives(allDirectives) { const directiveGroups = groupByParentComment(allDirectives); return directiveGroups.flatMap( @@ -199,6 +199,95 @@ function processUnusedDisableDirectives(allDirectives) { ); } +/** + * Collect eslint-enable comments that are removing suppressions by eslint-disable comments. + * @param {Directive[]} directives The directives to check. + * @returns {Set} The used eslint-enable comments + */ +function collectUsedEnableDirectives(directives) { + + /** + * A Map of `eslint-enable` keyed by ruleIds that may be marked as used. + * If `eslint-enable` does not have a ruleId, the key will be `null`. + * @type {Map} + */ + const enabledRules = new Map(); + + /** + * A Set of `eslint-enable` marked as used. + * It is also the return value of `collectUsedEnableDirectives` function. + * @type {Set} + */ + const usedEnableDirectives = new Set(); + + /* + * Checks the directives backwards to see if the encountered `eslint-enable` is used by the previous `eslint-disable`, + * and if so, stores the `eslint-enable` in `usedEnableDirectives`. + */ + for (let index = directives.length - 1; index >= 0; index--) { + const directive = directives[index]; + + if (directive.type === "disable") { + if (enabledRules.size === 0) { + continue; + } + if (directive.ruleId === null) { + + // If encounter `eslint-disable` without ruleId, + // mark all `eslint-enable` currently held in enabledRules as used. + // e.g. + // /* eslint-disable */ <- current directive + // /* eslint-enable rule-id1 */ <- used + // /* eslint-enable rule-id2 */ <- used + // /* eslint-enable */ <- used + for (const enableDirective of enabledRules.values()) { + usedEnableDirectives.add(enableDirective); + } + enabledRules.clear(); + } else { + const enableDirective = enabledRules.get(directive.ruleId); + + if (enableDirective) { + + // If encounter `eslint-disable` with ruleId, and there is an `eslint-enable` with the same ruleId in enabledRules, + // mark `eslint-enable` with ruleId as used. + // e.g. + // /* eslint-disable rule-id */ <- current directive + // /* eslint-enable rule-id */ <- used + usedEnableDirectives.add(enableDirective); + } else { + const enabledDirectiveWithoutRuleId = enabledRules.get(null); + + if (enabledDirectiveWithoutRuleId) { + + // If encounter `eslint-disable` with ruleId, and there is no `eslint-enable` with the same ruleId in enabledRules, + // mark `eslint-enable` without ruleId as used. + // e.g. + // /* eslint-disable rule-id */ <- current directive + // /* eslint-enable */ <- used + usedEnableDirectives.add(enabledDirectiveWithoutRuleId); + } + } + } + } else if (directive.type === "enable") { + if (directive.ruleId === null) { + + // If encounter `eslint-enable` without ruleId, the `eslint-enable` that follows it are unused. + // So clear enabledRules. + // e.g. + // /* eslint-enable */ <- current directive + // /* eslint-enable rule-id *// <- unused + // /* eslint-enable */ <- unused + enabledRules.clear(); + enabledRules.set(null, directive); + } else { + enabledRules.set(directive.ruleId, directive); + } + } + } + return usedEnableDirectives; +} + /** * This is the same as the exported function, except that it * doesn't handle disable-line and disable-next-line directives, and it always reports unused @@ -206,7 +295,7 @@ function processUnusedDisableDirectives(allDirectives) { * @param {Object} options options for applying directives. This is the same as the options * for the exported function, except that `reportUnusedDisableDirectives` is not supported * (this function always reports unused disable directives). - * @returns {{problems: LintMessage[], unusedDisableDirectives: LintMessage[]}} An object with a list + * @returns {{problems: LintMessage[], unusedDirectives: LintMessage[]}} An object with a list * of problems (including suppressed ones) and unused eslint-disable directives */ function applyDirectives(options) { @@ -258,17 +347,42 @@ function applyDirectives(options) { const unusedDisableDirectivesToReport = options.directives .filter(directive => directive.type === "disable" && !usedDisableDirectives.has(directive)); - const processed = processUnusedDisableDirectives(unusedDisableDirectivesToReport); - const unusedDisableDirectives = processed + const unusedEnableDirectivesToReport = new Set( + options.directives.filter(directive => directive.unprocessedDirective.type === "enable") + ); + + /* + * If directives has the eslint-enable directive, + * check whether the eslint-enable comment is used. + */ + if (unusedEnableDirectivesToReport.size > 0) { + for (const directive of collectUsedEnableDirectives(options.directives)) { + unusedEnableDirectivesToReport.delete(directive); + } + } + + const processed = processUnusedDirectives(unusedDisableDirectivesToReport) + .concat(processUnusedDirectives(unusedEnableDirectivesToReport)); + + const unusedDirectives = processed .map(({ description, fix, unprocessedDirective }) => { const { parentComment, type, line, column } = unprocessedDirective; + let message; + + if (type === "enable") { + message = description + ? `Unused eslint-enable directive (no matching eslint-disable directives were found for ${description}).` + : "Unused eslint-enable directive (no matching eslint-disable directives were found)."; + } else { + message = description + ? `Unused eslint-disable directive (no problems were reported from ${description}).` + : "Unused eslint-disable directive (no problems were reported)."; + } return { ruleId: null, - message: description - ? `Unused eslint-disable directive (no problems were reported from ${description}).` - : "Unused eslint-disable directive (no problems were reported).", + message, line: type === "disable-next-line" ? parentComment.commentToken.loc.start.line : line, column: type === "disable-next-line" ? parentComment.commentToken.loc.start.column + 1 : column, severity: options.reportUnusedDisableDirectives === "warn" ? 1 : 2, @@ -277,7 +391,7 @@ function applyDirectives(options) { }; }); - return { problems, unusedDisableDirectives }; + return { problems, unusedDirectives }; } /** @@ -344,8 +458,8 @@ module.exports = ({ directives, disableFixes, problems, reportUnusedDisableDirec return reportUnusedDisableDirectives !== "off" ? lineDirectivesResult.problems - .concat(blockDirectivesResult.unusedDisableDirectives) - .concat(lineDirectivesResult.unusedDisableDirectives) + .concat(blockDirectivesResult.unusedDirectives) + .concat(lineDirectivesResult.unusedDirectives) .sort(compareLocations) : lineDirectivesResult.problems; }; diff --git a/lib/linter/code-path-analysis/code-path-analyzer.js b/lib/linter/code-path-analysis/code-path-analyzer.js index 2dcc2734884..b60e55c16de 100644 --- a/lib/linter/code-path-analysis/code-path-analyzer.js +++ b/lib/linter/code-path-analysis/code-path-analyzer.js @@ -192,15 +192,18 @@ function forwardCurrentToHead(analyzer, node) { headSegment = headSegments[i]; if (currentSegment !== headSegment && currentSegment) { - debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`); - if (currentSegment.reachable) { - analyzer.emitter.emit( - "onCodePathSegmentEnd", - currentSegment, - node - ); - } + const eventName = currentSegment.reachable + ? "onCodePathSegmentEnd" + : "onUnreachableCodePathSegmentEnd"; + + debug.dump(`${eventName} ${currentSegment.id}`); + + analyzer.emitter.emit( + eventName, + currentSegment, + node + ); } } @@ -213,16 +216,19 @@ function forwardCurrentToHead(analyzer, node) { headSegment = headSegments[i]; if (currentSegment !== headSegment && headSegment) { - debug.dump(`onCodePathSegmentStart ${headSegment.id}`); + + const eventName = headSegment.reachable + ? "onCodePathSegmentStart" + : "onUnreachableCodePathSegmentStart"; + + debug.dump(`${eventName} ${headSegment.id}`); CodePathSegment.markUsed(headSegment); - if (headSegment.reachable) { - analyzer.emitter.emit( - "onCodePathSegmentStart", - headSegment, - node - ); - } + analyzer.emitter.emit( + eventName, + headSegment, + node + ); } } @@ -241,15 +247,17 @@ function leaveFromCurrentSegment(analyzer, node) { for (let i = 0; i < currentSegments.length; ++i) { const currentSegment = currentSegments[i]; + const eventName = currentSegment.reachable + ? "onCodePathSegmentEnd" + : "onUnreachableCodePathSegmentEnd"; - debug.dump(`onCodePathSegmentEnd ${currentSegment.id}`); - if (currentSegment.reachable) { - analyzer.emitter.emit( - "onCodePathSegmentEnd", - currentSegment, - node - ); - } + debug.dump(`${eventName} ${currentSegment.id}`); + + analyzer.emitter.emit( + eventName, + currentSegment, + node + ); } state.currentSegments = []; diff --git a/lib/linter/code-path-analysis/code-path-segment.js b/lib/linter/code-path-analysis/code-path-segment.js index fd2726a9937..3b8dbb41be6 100644 --- a/lib/linter/code-path-analysis/code-path-segment.js +++ b/lib/linter/code-path-analysis/code-path-segment.js @@ -1,5 +1,5 @@ /** - * @fileoverview A class of the code path segment. + * @fileoverview The CodePathSegment class. * @author Toru Nagashima */ @@ -30,10 +30,22 @@ function isReachable(segment) { /** * A code path segment. + * + * Each segment is arranged in a series of linked lists (implemented by arrays) + * that keep track of the previous and next segments in a code path. In this way, + * you can navigate between all segments in any code path so long as you have a + * reference to any segment in that code path. + * + * When first created, the segment is in a detached state, meaning that it knows the + * segments that came before it but those segments don't know that this new segment + * follows it. Only when `CodePathSegment#markUsed()` is called on a segment does it + * officially become part of the code path by updating the previous segments to know + * that this new segment follows. */ class CodePathSegment { /** + * Creates a new instance. * @param {string} id An identifier. * @param {CodePathSegment[]} allPrevSegments An array of the previous segments. * This array includes unreachable segments. @@ -49,27 +61,25 @@ class CodePathSegment { this.id = id; /** - * An array of the next segments. + * An array of the next reachable segments. * @type {CodePathSegment[]} */ this.nextSegments = []; /** - * An array of the previous segments. + * An array of the previous reachable segments. * @type {CodePathSegment[]} */ this.prevSegments = allPrevSegments.filter(isReachable); /** - * An array of the next segments. - * This array includes unreachable segments. + * An array of all next segments including reachable and unreachable. * @type {CodePathSegment[]} */ this.allNextSegments = []; /** - * An array of the previous segments. - * This array includes unreachable segments. + * An array of all previous segments including reachable and unreachable. * @type {CodePathSegment[]} */ this.allPrevSegments = allPrevSegments; @@ -83,7 +93,11 @@ class CodePathSegment { // Internal data. Object.defineProperty(this, "internal", { value: { + + // determines if the segment has been attached to the code path used: false, + + // array of previous segments coming from the end of a loop loopedPrevSegments: [] } }); @@ -113,9 +127,10 @@ class CodePathSegment { } /** - * Creates a segment that follows given segments. + * Creates a new segment and appends it after the given segments. * @param {string} id An identifier. - * @param {CodePathSegment[]} allPrevSegments An array of the previous segments. + * @param {CodePathSegment[]} allPrevSegments An array of the previous segments + * to append to. * @returns {CodePathSegment} The created segment. */ static newNext(id, allPrevSegments) { @@ -127,7 +142,7 @@ class CodePathSegment { } /** - * Creates an unreachable segment that follows given segments. + * Creates an unreachable segment and appends it after the given segments. * @param {string} id An identifier. * @param {CodePathSegment[]} allPrevSegments An array of the previous segments. * @returns {CodePathSegment} The created segment. @@ -137,7 +152,7 @@ class CodePathSegment { /* * In `if (a) return a; foo();` case, the unreachable segment preceded by - * the return statement is not used but must not be remove. + * the return statement is not used but must not be removed. */ CodePathSegment.markUsed(segment); @@ -157,7 +172,7 @@ class CodePathSegment { } /** - * Makes a given segment being used. + * Marks a given segment as used. * * And this function registers the segment into the previous segments as a next. * @param {CodePathSegment} segment A segment to mark. @@ -172,6 +187,13 @@ class CodePathSegment { let i; if (segment.reachable) { + + /* + * If the segment is reachable, then it's officially part of the + * code path. This loops through all previous segments to update + * their list of next segments. Because the segment is reachable, + * it's added to both `nextSegments` and `allNextSegments`. + */ for (i = 0; i < segment.allPrevSegments.length; ++i) { const prevSegment = segment.allPrevSegments[i]; @@ -179,6 +201,13 @@ class CodePathSegment { prevSegment.nextSegments.push(segment); } } else { + + /* + * If the segment is not reachable, then it's not officially part of the + * code path. This loops through all previous segments to update + * their list of next segments. Because the segment is not reachable, + * it's added only to `allNextSegments`. + */ for (i = 0; i < segment.allPrevSegments.length; ++i) { segment.allPrevSegments[i].allNextSegments.push(segment); } @@ -196,19 +225,20 @@ class CodePathSegment { } /** - * Replaces unused segments with the previous segments of each unused segment. - * @param {CodePathSegment[]} segments An array of segments to replace. - * @returns {CodePathSegment[]} The replaced array. + * Creates a new array based on an array of segments. If any segment in the + * array is unused, then it is replaced by all of its previous segments. + * All used segments are returned as-is without replacement. + * @param {CodePathSegment[]} segments The array of segments to flatten. + * @returns {CodePathSegment[]} The flattened array. */ static flattenUnusedSegments(segments) { - const done = Object.create(null); - const retv = []; + const done = new Set(); for (let i = 0; i < segments.length; ++i) { const segment = segments[i]; // Ignores duplicated. - if (done[segment.id]) { + if (done.has(segment)) { continue; } @@ -217,18 +247,16 @@ class CodePathSegment { for (let j = 0; j < segment.allPrevSegments.length; ++j) { const prevSegment = segment.allPrevSegments[j]; - if (!done[prevSegment.id]) { - done[prevSegment.id] = true; - retv.push(prevSegment); + if (!done.has(prevSegment)) { + done.add(prevSegment); } } } else { - done[segment.id] = true; - retv.push(segment); + done.add(segment); } } - return retv; + return [...done]; } } diff --git a/lib/linter/code-path-analysis/code-path-state.js b/lib/linter/code-path-analysis/code-path-state.js index d187297d32b..2b0dc2bfca0 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 a028ca69481..3bf570d754b 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,8 +126,14 @@ 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 */ get currentSegments() { return this.internal.currentSegments; @@ -125,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; @@ -172,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; } @@ -211,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; } @@ -232,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/debug-helpers.js b/lib/linter/code-path-analysis/debug-helpers.js index e06b6cde5f1..c0e01a8248b 100644 --- a/lib/linter/code-path-analysis/debug-helpers.js +++ b/lib/linter/code-path-analysis/debug-helpers.js @@ -109,7 +109,7 @@ module.exports = { text += "final[label=\"\",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];\n"; } if (codePath.thrownSegments.length > 0) { - text += "thrown[label=\"✘\",shape=circle,width=0.3,height=0.3,fixedsize];\n"; + text += "thrown[label=\"✘\",shape=circle,width=0.3,height=0.3,fixedsize=true];\n"; } const traceMap = Object.create(null); diff --git a/lib/linter/code-path-analysis/fork-context.js b/lib/linter/code-path-analysis/fork-context.js index 04c59b5e417..33140272f53 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 9aab3c44458..cde261204f5 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/linter/linter.js b/lib/linter/linter.js index e5d11e938b0..a99677e3118 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -42,7 +42,8 @@ const ruleReplacements = require("../../conf/replacements.json"); const { getRuleFromConfig } = require("../config/flat-config-helpers"); const { FlatConfigArray } = require("../config/flat-config-array"); - +const { RuleValidator } = require("../config/rule-validator"); +const { assertIsRuleOptions, assertIsRuleSeverity } = require("../config/flat-config-schema"); const debug = require("debug")("eslint:linter"); const MAX_AUTOFIX_PASSES = 10; const DEFAULT_PARSER_NAME = "espree"; @@ -50,7 +51,6 @@ const DEFAULT_ECMA_VERSION = 5; const commentParser = new ConfigCommentParser(); const DEFAULT_ERROR_LOC = { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } }; const parserSymbol = Symbol.for("eslint.RuleTester.parser"); -const globals = require("../../conf/globals"); //------------------------------------------------------------------------------ // Typedefs @@ -145,29 +145,6 @@ function isEspree(parser) { return !!(parser === espree || parser[parserSymbol] === espree); } -/** - * Retrieves globals for the given ecmaVersion. - * @param {number} ecmaVersion The version to retrieve globals for. - * @returns {Object} The globals for the given ecmaVersion. - */ -function getGlobalsForEcmaVersion(ecmaVersion) { - - switch (ecmaVersion) { - case 3: - return globals.es3; - - case 5: - return globals.es5; - - default: - if (ecmaVersion < 2015) { - return globals[`es${ecmaVersion + 2009}`]; - } - - return globals[`es${ecmaVersion}`]; - } -} - /** * Ensures that variables representing built-in properties of the Global Object, * and any globals declared by special block comments, are present in the global @@ -361,13 +338,13 @@ function extractDirectiveComment(value) { * Parses comments in file to extract file-specific config of rules, globals * and environments and merges them with global config; also code blocks * where reporting is disabled or enabled and merges them with reporting config. - * @param {ASTNode} ast The top node of the AST. + * @param {SourceCode} sourceCode The SourceCode object to get comments from. * @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules * @param {string|null} warnInlineConfig If a string then it should warn directive comments as disabled. The string value is the config name what the setting came from. * @returns {{configuredRules: Object, enabledGlobals: {value:string,comment:Token}[], exportedVariables: Object, problems: LintMessage[], disableDirectives: DisableDirective[]}} * A collection of the directive comments that were found, along with any problems that occurred when parsing */ -function getDirectiveComments(ast, ruleMapper, warnInlineConfig) { +function getDirectiveComments(sourceCode, ruleMapper, warnInlineConfig) { const configuredRules = {}; const enabledGlobals = Object.create(null); const exportedVariables = {}; @@ -377,7 +354,7 @@ function getDirectiveComments(ast, ruleMapper, warnInlineConfig) { builtInRules: Rules }); - ast.comments.filter(token => token.type !== "Shebang").forEach(comment => { + sourceCode.getInlineConfigNodes().filter(token => token.type !== "Shebang").forEach(comment => { const { directivePart, justificationPart } = extractDirectiveComment(comment.value); const match = directivesPattern.exec(directivePart); @@ -511,6 +488,69 @@ function getDirectiveComments(ast, ruleMapper, warnInlineConfig) { }; } +/** + * Parses comments in file to extract disable directives. + * @param {SourceCode} sourceCode The SourceCode object to get comments from. + * @param {function(string): {create: Function}} ruleMapper A map from rule IDs to defined rules + * @returns {{problems: LintMessage[], disableDirectives: DisableDirective[]}} + * A collection of the directive comments that were found, along with any problems that occurred when parsing + */ +function getDirectiveCommentsForFlatConfig(sourceCode, ruleMapper) { + const problems = []; + const disableDirectives = []; + + sourceCode.getInlineConfigNodes().filter(token => token.type !== "Shebang").forEach(comment => { + const { directivePart, justificationPart } = extractDirectiveComment(comment.value); + + const match = directivesPattern.exec(directivePart); + + if (!match) { + return; + } + const directiveText = match[1]; + const lineCommentSupported = /^eslint-disable-(next-)?line$/u.test(directiveText); + + if (comment.type === "Line" && !lineCommentSupported) { + return; + } + + if (directiveText === "eslint-disable-line" && comment.loc.start.line !== comment.loc.end.line) { + const message = `${directiveText} comment should not span multiple lines.`; + + problems.push(createLintingProblem({ + ruleId: null, + message, + loc: comment.loc + })); + return; + } + + const directiveValue = directivePart.slice(match.index + directiveText.length); + + switch (directiveText) { + case "eslint-disable": + case "eslint-enable": + case "eslint-disable-next-line": + case "eslint-disable-line": { + const directiveType = directiveText.slice("eslint-".length); + const options = { commentToken: comment, type: directiveType, value: directiveValue, justification: justificationPart, ruleMapper }; + const { directives, directiveProblems } = createDisableDirectives(options); + + disableDirectives.push(...directives); + problems.push(...directiveProblems); + break; + } + + // no default + } + }); + + return { + problems, + disableDirectives + }; +} + /** * Normalize ECMAScript version from the initial config * @param {Parser} parser The parser which uses this options. @@ -908,6 +948,7 @@ const DEPRECATED_SOURCECODE_PASSTHROUGHS = { getTokensBetween: "getTokensBetween" }; + const BASE_TRAVERSAL_CONTEXT = Object.freeze( Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).reduce( (contextInfo, methodName) => @@ -1322,7 +1363,7 @@ class Linter { const sourceCode = slots.lastSourceCode; const commentDirectives = options.allowInlineConfig - ? getDirectiveComments(sourceCode.ast, ruleId => getRule(slots, ruleId), options.warnInlineConfig) + ? getDirectiveComments(sourceCode, ruleId => getRule(slots, ruleId), options.warnInlineConfig) : { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] }; // augment global scope with declared global variables @@ -1333,7 +1374,6 @@ class Linter { ); const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules); - let lintingProblems; try { @@ -1549,19 +1589,6 @@ class Linter { languageOptions.ecmaVersion ); - /* - * add configured globals and language globals - * - * using Object.assign instead of object spread for performance reasons - * https://github.com/eslint/eslint/issues/16302 - */ - const configuredGlobals = Object.assign( - {}, - getGlobalsForEcmaVersion(languageOptions.ecmaVersion), - languageOptions.sourceType === "commonjs" ? globals.commonjs : void 0, - languageOptions.globals - ); - // double check that there is a parser to avoid mysterious error messages if (!languageOptions.parser) { throw new TypeError(`No parser specified for ${options.filename}`); @@ -1617,25 +1644,113 @@ class Linter { } const sourceCode = slots.lastSourceCode; - const commentDirectives = options.allowInlineConfig - ? getDirectiveComments( - sourceCode.ast, - ruleId => getRuleFromConfig(ruleId, config), - options.warnInlineConfig - ) - : { configuredRules: {}, enabledGlobals: {}, exportedVariables: {}, problems: [], disableDirectives: [] }; - // augment global scope with declared global variables - addDeclaredGlobals( - sourceCode.scopeManager.scopes[0], - configuredGlobals, - { exportedVariables: commentDirectives.exportedVariables, enabledGlobals: commentDirectives.enabledGlobals } - ); + /* + * Make adjustments based on the language options. For JavaScript, + * this is primarily about adding variables into the global scope + * to account for ecmaVersion and configured globals. + */ + sourceCode.applyLanguageOptions(languageOptions); - const configuredRules = Object.assign({}, config.rules, commentDirectives.configuredRules); + const mergedInlineConfig = { + rules: {} + }; + const inlineConfigProblems = []; + /* + * Inline config can be either enabled or disabled. If disabled, it's possible + * to detect the inline config and emit a warning (though this is not required). + * So we first check to see if inline config is allowed at all, and if so, we + * need to check if it's a warning or not. + */ + if (options.allowInlineConfig) { + + // if inline config should warn then add the warnings + if (options.warnInlineConfig) { + sourceCode.getInlineConfigNodes().forEach(node => { + inlineConfigProblems.push(createLintingProblem({ + ruleId: null, + message: `'${sourceCode.text.slice(node.range[0], node.range[1])}' has no effect because you have 'noInlineConfig' setting in ${options.warnInlineConfig}.`, + loc: node.loc, + severity: 1 + })); + + }); + } else { + const inlineConfigResult = sourceCode.applyInlineConfig(); + + inlineConfigProblems.push( + ...inlineConfigResult.problems + .map(createLintingProblem) + .map(problem => { + problem.fatal = true; + return problem; + }) + ); + + // next we need to verify information about the specified rules + const ruleValidator = new RuleValidator(); + + for (const { config: inlineConfig, node } of inlineConfigResult.configs) { + + Object.keys(inlineConfig.rules).forEach(ruleId => { + const rule = getRuleFromConfig(ruleId, config); + const ruleValue = inlineConfig.rules[ruleId]; + + if (!rule) { + inlineConfigProblems.push(createLintingProblem({ ruleId, loc: node.loc })); + return; + } + + try { + + const ruleOptions = Array.isArray(ruleValue) ? ruleValue : [ruleValue]; + + assertIsRuleOptions(ruleId, ruleValue); + assertIsRuleSeverity(ruleId, ruleOptions[0]); + + ruleValidator.validate({ + plugins: config.plugins, + rules: { + [ruleId]: ruleOptions + } + }); + mergedInlineConfig.rules[ruleId] = ruleValue; + } catch (err) { + + let baseMessage = err.message.slice( + err.message.startsWith("Key \"rules\":") + ? err.message.indexOf(":", 12) + 1 + : err.message.indexOf(":") + 1 + ).trim(); + + if (err.messageTemplate) { + baseMessage += ` You passed "${ruleValue}".`; + } + + inlineConfigProblems.push(createLintingProblem({ + ruleId, + message: `Inline configuration for rule "${ruleId}" is invalid:\n\t${baseMessage}\n`, + loc: node.loc + })); + } + }); + } + } + } + + const commentDirectives = options.allowInlineConfig && !options.warnInlineConfig + ? getDirectiveCommentsForFlatConfig( + sourceCode, + ruleId => getRuleFromConfig(ruleId, config) + ) + : { problems: [], disableDirectives: [] }; + + const configuredRules = Object.assign({}, config.rules, mergedInlineConfig.rules); let lintingProblems; + sourceCode.finalize(); + try { lintingProblems = runRules( sourceCode, @@ -1676,6 +1791,7 @@ class Linter { disableFixes: options.disableFixes, problems: lintingProblems .concat(commentDirectives.problems) + .concat(inlineConfigProblems) .sort((problemA, problemB) => problemA.line - problemB.line || problemA.column - problemB.column), reportUnusedDisableDirectives: options.reportUnusedDisableDirectives }); diff --git a/lib/linter/report-translator.js b/lib/linter/report-translator.js index 7d2705206cd..41a43eadc3b 100644 --- a/lib/linter/report-translator.js +++ b/lib/linter/report-translator.js @@ -100,6 +100,22 @@ function normalizeReportLoc(descriptor) { return descriptor.node.loc; } +/** + * Clones the given fix object. + * @param {Fix|null} fix The fix to clone. + * @returns {Fix|null} Deep cloned fix object or `null` if `null` or `undefined` was passed in. + */ +function cloneFix(fix) { + if (!fix) { + return null; + } + + return { + range: [fix.range[0], fix.range[1]], + text: fix.text + }; +} + /** * Check that a fix has a valid range. * @param {Fix|null} fix The fix to validate. @@ -137,7 +153,7 @@ function mergeFixes(fixes, sourceCode) { return null; } if (fixes.length === 1) { - return fixes[0]; + return cloneFix(fixes[0]); } fixes.sort(compareFixesByRange); @@ -183,7 +199,7 @@ function normalizeFixes(descriptor, sourceCode) { } assertValidFix(fix); - return fix; + return cloneFix(fix); } /** diff --git a/lib/options.js b/lib/options.js index aaaec286485..c1994df9550 100644 --- a/lib/options.js +++ b/lib/options.js @@ -47,7 +47,7 @@ const optionator = require("optionator"); * @property {Object} [parserOptions] Specify parser options * @property {string[]} [plugin] Specify plugins * @property {string} [printConfig] Print the configuration for the given file - * @property {boolean|string|undefined} reportUnusedDisableDirectives Adds reported errors for unused eslint-disable directives + * @property {boolean | string | undefined} reportUnusedDisableDirectives Adds reported errors for unused eslint-disable and eslint-enable directives * @property {string} [resolvePluginsRelativeTo] A folder where plugins should be resolved from, CWD by default * @property {Object} [rule] Specify rules * @property {string[]} [rulesdir] Load additional rules from this directory. Deprecated: Use rules from plugins @@ -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: { @@ -292,7 +304,7 @@ module.exports = function(usingFlatConfig) { option: "report-unused-disable-directives", type: "Boolean", default: void 0, - description: "Adds reported errors for unused eslint-disable directives" + description: "Adds reported errors for unused eslint-disable and eslint-enable directives" }, { option: "report-unused-disable-directives-severity", @@ -356,6 +368,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/rule-tester/flat-rule-tester.js b/lib/rule-tester/flat-rule-tester.js index 97055d104f4..51cb73b5f80 100644 --- a/lib/rule-tester/flat-rule-tester.js +++ b/lib/rule-tester/flat-rule-tester.js @@ -16,7 +16,9 @@ const equal = require("fast-deep-equal"), Traverser = require("../shared/traverser"), { getRuleOptionsSchema } = require("../config/flat-config-helpers"), - { Linter, SourceCodeFixer, interpolate } = require("../linter"); + { Linter, SourceCodeFixer, interpolate } = require("../linter"), + CodePath = require("../linter/code-path-analysis/code-path"); + const { FlatConfigArray } = require("../config/flat-config-array"); const { defaultConfig } = require("../config/default-config"); @@ -32,8 +34,9 @@ const { ConfigArraySymbol } = require("@humanwhocodes/config-array"); /** @typedef {import("../shared/types").Parser} Parser */ /** @typedef {import("../shared/types").LanguageOptions} LanguageOptions */ +/** @typedef {import("../shared/types").Rule} Rule */ + -/* eslint-disable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */ /** * A test case that is expected to pass lint. * @typedef {Object} ValidTestCase @@ -72,7 +75,6 @@ const { ConfigArraySymbol } = require("@humanwhocodes/config-array"); * @property {number} [endLine] The 1-based line number of the reported end location. * @property {number} [endColumn] The 1-based column number of the reported end location. */ -/* eslint-enable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */ //------------------------------------------------------------------------------ // Private Members @@ -131,6 +133,15 @@ const suggestionObjectParameters = new Set([ ]); const friendlySuggestionObjectParameterList = `[${[...suggestionObjectParameters].map(key => `'${key}'`).join(", ")}]`; +const forbiddenMethods = [ + "applyInlineConfig", + "applyLanguageOptions", + "finalize" +]; + +/** @type {Map} */ +const forbiddenMethodCalls = new Map(forbiddenMethods.map(methodName => ([methodName, new WeakSet()]))); + const hasOwnProperty = Function.call.bind(Object.hasOwnProperty); /** @@ -274,6 +285,49 @@ function getCommentsDeprecation() { ); } +/** + * Emit a deprecation warning if rule uses CodePath#currentSegments. + * @param {string} ruleName Name of the rule. + * @returns {void} + */ +function emitCodePathCurrentSegmentsWarning(ruleName) { + if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) { + emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true; + process.emitWarning( + `"${ruleName}" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples`, + "DeprecationWarning" + ); + } +} + +/** + * Function to replace forbidden `SourceCode` methods. Allows just one call per method. + * @param {string} methodName The name of the method to forbid. + * @param {Function} prototype The prototype with the original method to call. + * @returns {Function} The function that throws the error. + */ +function throwForbiddenMethodError(methodName, prototype) { + + const original = prototype[methodName]; + + return function(...args) { + + const called = forbiddenMethodCalls.get(methodName); + + /* eslint-disable no-invalid-this -- needed to operate as a method. */ + if (!called.has(this)) { + called.add(this); + + return original.apply(this, args); + } + /* eslint-enable no-invalid-this -- not needed past this point */ + + throw new Error( + `\`SourceCode#${methodName}()\` cannot be called inside a rule.` + ); + }; +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -447,7 +501,7 @@ class FlatRuleTester { /** * Adds a new rule test to execute. * @param {string} ruleName The name of the rule to run. - * @param {Function} rule The rule to test. + * @param {Function | Rule} rule The rule to test. * @param {{ * valid: (ValidTestCase | string)[], * invalid: InvalidTestCase[] @@ -481,6 +535,7 @@ class FlatRuleTester { } const baseConfig = [ + { files: ["**"] }, // Make sure the default config matches for all files { plugins: { @@ -662,10 +717,6 @@ class FlatRuleTester { } } - // Verify the code. - const { getComments } = SourceCode.prototype; - let messages; - // check for validation errors try { configs.normalizeSync(); @@ -675,13 +726,34 @@ class FlatRuleTester { throw error; } + // Verify the code. + const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; + const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments"); + let messages; + try { SourceCode.prototype.getComments = getCommentsDeprecation; + Object.defineProperty(CodePath.prototype, "currentSegments", { + get() { + emitCodePathCurrentSegmentsWarning(ruleName); + return originalCurrentSegments.get.call(this); + } + }); + + forbiddenMethods.forEach(methodName => { + SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName, SourceCode.prototype); + }); + messages = linter.verify(code, configs, filename); } finally { SourceCode.prototype.getComments = getComments; + Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); + SourceCode.prototype.applyInlineConfig = applyInlineConfig; + SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; + SourceCode.prototype.finalize = finalize; } + const fatalErrorMessage = messages.find(m => m.fatal); assert(!fatalErrorMessage, `A fatal parsing error occurred: ${fatalErrorMessage && fatalErrorMessage.message}`); @@ -1012,29 +1084,35 @@ class FlatRuleTester { /* * This creates a mocha test suite and pipes all supplied info through * one of the templates above. + * The test suites for valid/invalid are created conditionally as + * test runners (eg. vitest) fail for empty test suites. */ this.constructor.describe(ruleName, () => { - this.constructor.describe("valid", () => { - test.valid.forEach(valid => { - this.constructor[valid.only ? "itOnly" : "it"]( - sanitize(typeof valid === "object" ? valid.name || valid.code : valid), - () => { - testValidTemplate(valid); - } - ); + if (test.valid.length > 0) { + this.constructor.describe("valid", () => { + test.valid.forEach(valid => { + this.constructor[valid.only ? "itOnly" : "it"]( + sanitize(typeof valid === "object" ? valid.name || valid.code : valid), + () => { + testValidTemplate(valid); + } + ); + }); }); - }); + } - this.constructor.describe("invalid", () => { - test.invalid.forEach(invalid => { - this.constructor[invalid.only ? "itOnly" : "it"]( - sanitize(invalid.name || invalid.code), - () => { - testInvalidTemplate(invalid); - } - ); + if (test.invalid.length > 0) { + this.constructor.describe("invalid", () => { + test.invalid.forEach(invalid => { + this.constructor[invalid.only ? "itOnly" : "it"]( + sanitize(invalid.name || invalid.code), + () => { + testInvalidTemplate(invalid); + } + ); + }); }); - }); + } }); } } diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index 8518299d0b0..3bc80ab1837 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -48,7 +48,8 @@ const equal = require("fast-deep-equal"), Traverser = require("../../lib/shared/traverser"), { getRuleOptionsSchema, validate } = require("../shared/config-validator"), - { Linter, SourceCodeFixer, interpolate } = require("../linter"); + { Linter, SourceCodeFixer, interpolate } = require("../linter"), + CodePath = require("../linter/code-path-analysis/code-path"); const ajv = require("../shared/ajv")({ strictDefaults: true }); @@ -62,8 +63,9 @@ const { SourceCode } = require("../source-code"); //------------------------------------------------------------------------------ /** @typedef {import("../shared/types").Parser} Parser */ +/** @typedef {import("../shared/types").Rule} Rule */ + -/* eslint-disable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */ /** * A test case that is expected to pass lint. * @typedef {Object} ValidTestCase @@ -108,7 +110,6 @@ const { SourceCode } = require("../source-code"); * @property {number} [endLine] The 1-based line number of the reported end location. * @property {number} [endColumn] The 1-based column number of the reported end location. */ -/* eslint-enable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/4#issuecomment-778805577 */ //------------------------------------------------------------------------------ // Private Members @@ -162,8 +163,43 @@ const suggestionObjectParameters = new Set([ ]); const friendlySuggestionObjectParameterList = `[${[...suggestionObjectParameters].map(key => `'${key}'`).join(", ")}]`; +const forbiddenMethods = [ + "applyInlineConfig", + "applyLanguageOptions", + "finalize" +]; + const hasOwnProperty = Function.call.bind(Object.hasOwnProperty); +const DEPRECATED_SOURCECODE_PASSTHROUGHS = { + getSource: "getText", + getSourceLines: "getLines", + getAllComments: "getAllComments", + getNodeByRangeIndex: "getNodeByRangeIndex", + + // getComments: "getComments", -- already handled by a separate error + getCommentsBefore: "getCommentsBefore", + getCommentsAfter: "getCommentsAfter", + getCommentsInside: "getCommentsInside", + getJSDocComment: "getJSDocComment", + getFirstToken: "getFirstToken", + getFirstTokens: "getFirstTokens", + getLastToken: "getLastToken", + getLastTokens: "getLastTokens", + getTokenAfter: "getTokenAfter", + getTokenBefore: "getTokenBefore", + getTokenByRangeStart: "getTokenByRangeStart", + getTokens: "getTokens", + getTokensAfter: "getTokensAfter", + getTokensBefore: "getTokensBefore", + getTokensBetween: "getTokensBetween", + + getScope: "getScope", + getAncestors: "getAncestors", + getDeclaredVariables: "getDeclaredVariables", + markVariableAsUsed: "markVariableAsUsed" +}; + /** * Clones a given value deeply. * Note: This ignores `parent` property. @@ -305,6 +341,19 @@ function getCommentsDeprecation() { ); } +/** + * Function to replace forbidden `SourceCode` methods. + * @param {string} methodName The name of the method to forbid. + * @returns {Function} The function that throws the error. + */ +function throwForbiddenMethodError(methodName) { + return () => { + throw new Error( + `\`SourceCode#${methodName}()\` cannot be called inside a rule.` + ); + }; +} + /** * Emit a deprecation warning if function-style format is being used. * @param {string} ruleName Name of the rule. @@ -335,6 +384,53 @@ function emitMissingSchemaWarning(ruleName) { } } +/** + * Emit a deprecation warning if a rule uses a deprecated `context` method. + * @param {string} ruleName Name of the rule. + * @param {string} methodName The name of the method on `context` that was used. + * @returns {void} + */ +function emitDeprecatedContextMethodWarning(ruleName, methodName) { + if (!emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`]) { + emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`] = true; + process.emitWarning( + `"${ruleName}" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]}()\` instead.`, + "DeprecationWarning" + ); + } +} + +/** + * Emit a deprecation warning if rule uses CodePath#currentSegments. + * @param {string} ruleName Name of the rule. + * @returns {void} + */ +function emitCodePathCurrentSegmentsWarning(ruleName) { + if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) { + emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true; + process.emitWarning( + `"${ruleName}" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples`, + "DeprecationWarning" + ); + } +} + +/** + * Emit a deprecation warning if `context.parserServices` is used. + * @param {string} ruleName Name of the rule. + * @returns {void} + */ +function emitParserServicesWarning(ruleName) { + if (!emitParserServicesWarning[`warned-${ruleName}`]) { + emitParserServicesWarning[`warned-${ruleName}`] = true; + process.emitWarning( + `"${ruleName}" rule is using \`context.parserServices\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.parserServices\` instead.`, + "DeprecationWarning" + ); + } +} + + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -509,17 +605,20 @@ class RuleTester { /** * Define a rule for one particular run of tests. * @param {string} name The name of the rule to define. - * @param {Function} rule The rule definition. + * @param {Function | Rule} rule The rule definition. * @returns {void} */ defineRule(name, rule) { + if (typeof rule === "function") { + emitLegacyRuleAPIWarning(name); + } this.rules[name] = rule; } /** * Adds a new rule test to execute. * @param {string} ruleName The name of the rule to run. - * @param {Function} rule The rule to test. + * @param {Function | Rule} rule The rule to test. * @param {{ * valid: (ValidTestCase | string)[], * invalid: InvalidTestCase[] @@ -563,7 +662,38 @@ class RuleTester { freezeDeeply(context.settings); freezeDeeply(context.parserOptions); - return (typeof rule === "function" ? rule : rule.create)(context); + // wrap all deprecated methods + const newContext = Object.create( + context, + Object.fromEntries(Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).map(methodName => [ + methodName, + { + value(...args) { + + // emit deprecation warning + emitDeprecatedContextMethodWarning(ruleName, methodName); + + // call the original method + return context[methodName].call(this, ...args); + }, + enumerable: true + } + ])) + ); + + // emit warning about context.parserServices + const parserServices = context.parserServices; + + Object.defineProperty(newContext, "parserServices", { + get() { + emitParserServicesWarning(ruleName); + return parserServices; + } + }); + + Object.freeze(newContext); + + return (typeof rule === "function" ? rule : rule.create)(newContext); } })); @@ -682,14 +812,30 @@ class RuleTester { validate(config, "rule-tester", id => (id === ruleName ? rule : null)); // Verify the code. - const { getComments } = SourceCode.prototype; + const { getComments, applyLanguageOptions, applyInlineConfig, finalize } = SourceCode.prototype; + const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments"); let messages; try { SourceCode.prototype.getComments = getCommentsDeprecation; + Object.defineProperty(CodePath.prototype, "currentSegments", { + get() { + emitCodePathCurrentSegmentsWarning(ruleName); + return originalCurrentSegments.get.call(this); + } + }); + + forbiddenMethods.forEach(methodName => { + SourceCode.prototype[methodName] = throwForbiddenMethodError(methodName); + }); + messages = linter.verify(code, config, filename); } finally { SourceCode.prototype.getComments = getComments; + Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments); + SourceCode.prototype.applyInlineConfig = applyInlineConfig; + SourceCode.prototype.applyLanguageOptions = applyLanguageOptions; + SourceCode.prototype.finalize = finalize; } const fatalErrorMessage = messages.find(m => m.fatal); @@ -1022,29 +1168,35 @@ class RuleTester { /* * This creates a mocha test suite and pipes all supplied info through * one of the templates above. + * The test suites for valid/invalid are created conditionally as + * test runners (eg. vitest) fail for empty test suites. */ this.constructor.describe(ruleName, () => { - this.constructor.describe("valid", () => { - test.valid.forEach(valid => { - this.constructor[valid.only ? "itOnly" : "it"]( - sanitize(typeof valid === "object" ? valid.name || valid.code : valid), - () => { - testValidTemplate(valid); - } - ); + if (test.valid.length > 0) { + this.constructor.describe("valid", () => { + test.valid.forEach(valid => { + this.constructor[valid.only ? "itOnly" : "it"]( + sanitize(typeof valid === "object" ? valid.name || valid.code : valid), + () => { + testValidTemplate(valid); + } + ); + }); }); - }); + } - this.constructor.describe("invalid", () => { - test.invalid.forEach(invalid => { - this.constructor[invalid.only ? "itOnly" : "it"]( - sanitize(invalid.name || invalid.code), - () => { - testInvalidTemplate(invalid); - } - ); + if (test.invalid.length > 0) { + this.constructor.describe("invalid", () => { + test.invalid.forEach(invalid => { + this.constructor[invalid.only ? "itOnly" : "it"]( + sanitize(invalid.name || invalid.code), + () => { + testInvalidTemplate(invalid); + } + ); + }); }); - }); + } }); } } diff --git a/lib/rules/accessor-pairs.js b/lib/rules/accessor-pairs.js index 03b51e461c0..f97032895df 100644 --- a/lib/rules/accessor-pairs.js +++ b/lib/rules/accessor-pairs.js @@ -223,43 +223,6 @@ module.exports = { } } - /** - * Creates a new `AccessorData` object for the given getter or setter node. - * @param {ASTNode} node A getter or setter node. - * @returns {AccessorData} New `AccessorData` object that contains the given node. - * @private - */ - function createAccessorData(node) { - const name = astUtils.getStaticPropertyName(node); - const key = (name !== null) ? name : sourceCode.getTokens(node.key); - - return { - key, - getters: node.kind === "get" ? [node] : [], - setters: node.kind === "set" ? [node] : [] - }; - } - - /** - * Merges the given `AccessorData` object into the given accessors list. - * @param {AccessorData[]} accessors The list to merge into. - * @param {AccessorData} accessorData The object to merge. - * @returns {AccessorData[]} The same instance with the merged object. - * @private - */ - function mergeAccessorData(accessors, accessorData) { - const equalKeyElement = accessors.find(a => areEqualKeys(a.key, accessorData.key)); - - if (equalKeyElement) { - equalKeyElement.getters.push(...accessorData.getters); - equalKeyElement.setters.push(...accessorData.setters); - } else { - accessors.push(accessorData); - } - - return accessors; - } - /** * Checks accessor pairs in the given list of nodes. * @param {ASTNode[]} nodes The list to check. @@ -267,10 +230,39 @@ module.exports = { * @private */ function checkList(nodes) { - const accessors = nodes - .filter(isAccessorKind) - .map(createAccessorData) - .reduce(mergeAccessorData, []); + const accessors = []; + let found = false; + + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + + if (isAccessorKind(node)) { + + // Creates a new `AccessorData` object for the given getter or setter node. + const name = astUtils.getStaticPropertyName(node); + const key = (name !== null) ? name : sourceCode.getTokens(node.key); + + // Merges the given `AccessorData` object into the given accessors list. + for (let j = 0; j < accessors.length; j++) { + const accessor = accessors[j]; + + if (areEqualKeys(accessor.key, key)) { + accessor.getters.push(...node.kind === "get" ? [node] : []); + accessor.setters.push(...node.kind === "set" ? [node] : []); + found = true; + break; + } + } + if (!found) { + accessors.push({ + key, + getters: node.kind === "get" ? [node] : [], + setters: node.kind === "set" ? [node] : [] + }); + } + found = false; + } + } for (const { getters, setters } of accessors) { if (checkSetWithoutGet && setters.length && !getters.length) { diff --git a/lib/rules/array-bracket-newline.js b/lib/rules/array-bracket-newline.js index c3676bf4dfa..12ef5b612d6 100644 --- a/lib/rules/array-bracket-newline.js +++ b/lib/rules/array-bracket-newline.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to enforce linebreaks after open and before close array brackets * @author Jan Peer Stöcklmair + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -14,6 +15,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/array-bracket-spacing.js b/lib/rules/array-bracket-spacing.js index e3a46d82214..9dd3ffd902c 100644 --- a/lib/rules/array-bracket-spacing.js +++ b/lib/rules/array-bracket-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Disallows or enforces spaces inside of array brackets. * @author Jamund Ferguson + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -13,6 +14,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/array-callback-return.js b/lib/rules/array-callback-return.js index 05cd4ede966..6d8f258fa14 100644 --- a/lib/rules/array-callback-return.js +++ b/lib/rules/array-callback-return.js @@ -18,15 +18,6 @@ const astUtils = require("./utils/ast-utils"); const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u; const TARGET_METHODS = /^(?:every|filter|find(?:Last)?(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort|toSorted)$/u; -/** - * Checks a given code path segment is reachable. - * @param {CodePathSegment} segment A segment to check. - * @returns {boolean} `true` if the segment is reachable. - */ -function isReachable(segment) { - return segment.reachable; -} - /** * Checks a given node is a member access which has the specified name's * property. @@ -38,6 +29,22 @@ function isTargetMethod(node) { return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS); } +/** + * 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; +} + /** * Returns a human-legible description of an array method * @param {string} arrayMethodName A method name to fully qualify @@ -129,6 +136,76 @@ function getArrayMethodName(node) { return null; } +/** + * Checks if the given node is a void expression. + * @param {ASTNode} node The node to check. + * @returns {boolean} - `true` if the node is a void expression + */ +function isExpressionVoid(node) { + return node.type === "UnaryExpression" && node.operator === "void"; +} + +/** + * Fixes the linting error by prepending "void " to the given node + * @param {Object} sourceCode context given by context.sourceCode + * @param {ASTNode} node The node to fix. + * @param {Object} fixer The fixer object provided by ESLint. + * @returns {Array} - An array of fix objects to apply to the node. + */ +function voidPrependFixer(sourceCode, node, fixer) { + + const requiresParens = + + // prepending `void ` will fail if the node has a lower precedence than void + astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression", operator: "void" }) && + + // check if there are parentheses around the node to avoid redundant parentheses + !astUtils.isParenthesised(sourceCode, node); + + // avoid parentheses issues + const returnOrArrowToken = sourceCode.getTokenBefore( + node, + node.parent.type === "ArrowFunctionExpression" + ? astUtils.isArrowToken + + // isReturnToken + : token => token.type === "Keyword" && token.value === "return" + ); + + const firstToken = sourceCode.getTokenAfter(returnOrArrowToken); + + const prependSpace = + + // is return token, as => allows void to be adjacent + returnOrArrowToken.value === "return" && + + // If two tokens (return and "(") are adjacent + returnOrArrowToken.range[1] === firstToken.range[0]; + + return [ + fixer.insertTextBefore(firstToken, `${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`), + fixer.insertTextAfter(node, requiresParens ? ")" : "") + ]; +} + +/** + * Fixes the linting error by `wrapping {}` around the given node's body. + * @param {Object} sourceCode context given by context.sourceCode + * @param {ASTNode} node The node to fix. + * @param {Object} fixer The fixer object provided by ESLint. + * @returns {Array} - An array of fix objects to apply to the node. + */ +function curlyWrapFixer(sourceCode, node, fixer) { + const arrowToken = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken); + const firstToken = sourceCode.getTokenAfter(arrowToken); + const lastToken = sourceCode.getLastToken(node); + + return [ + fixer.insertTextBefore(firstToken, "{"), + fixer.insertTextAfter(lastToken, "}") + ]; +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -144,6 +221,9 @@ module.exports = { url: "https://eslint.org/docs/latest/rules/array-callback-return" }, + // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- false positive + hasSuggestions: true, + schema: [ { type: "object", @@ -155,6 +235,10 @@ module.exports = { checkForEach: { type: "boolean", default: false + }, + allowVoid: { + type: "boolean", + default: false } }, additionalProperties: false @@ -165,13 +249,15 @@ module.exports = { expectedAtEnd: "{{arrayMethodName}}() expects a value to be returned at the end of {{name}}.", expectedInside: "{{arrayMethodName}}() expects a return value from {{name}}.", expectedReturnValue: "{{arrayMethodName}}() expects a return value from {{name}}.", - expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}." + expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}.", + wrapBraces: "Wrap the expression in `{}`.", + prependVoid: "Prepend `void` to the expression." } }, create(context) { - const options = context.options[0] || { allowImplicit: false, checkForEach: false }; + const options = context.options[0] || { allowImplicit: false, checkForEach: false, allowVoid: false }; const sourceCode = context.sourceCode; let funcInfo = { @@ -198,26 +284,56 @@ module.exports = { return; } - let messageId = null; + const messageAndSuggestions = { messageId: "", suggest: [] }; if (funcInfo.arrayMethodName === "forEach") { if (options.checkForEach && node.type === "ArrowFunctionExpression" && node.expression) { - messageId = "expectedNoReturnValue"; + + if (options.allowVoid) { + if (isExpressionVoid(node.body)) { + return; + } + + messageAndSuggestions.messageId = "expectedNoReturnValue"; + messageAndSuggestions.suggest = [ + { + messageId: "wrapBraces", + fix(fixer) { + return curlyWrapFixer(sourceCode, node, fixer); + } + }, + { + messageId: "prependVoid", + fix(fixer) { + return voidPrependFixer(sourceCode, node.body, fixer); + } + } + ]; + } else { + messageAndSuggestions.messageId = "expectedNoReturnValue"; + messageAndSuggestions.suggest = [{ + messageId: "wrapBraces", + fix(fixer) { + return curlyWrapFixer(sourceCode, node, fixer); + } + }]; + } } } else { - if (node.body.type === "BlockStatement" && funcInfo.codePath.currentSegments.some(isReachable)) { - messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside"; + if (node.body.type === "BlockStatement" && isAnySegmentReachable(funcInfo.currentSegments)) { + messageAndSuggestions.messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside"; } } - if (messageId) { + if (messageAndSuggestions.messageId) { const name = astUtils.getFunctionNameWithKind(node); context.report({ node, loc: astUtils.getFunctionHeadLoc(node, sourceCode), - messageId, - data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) } + messageId: messageAndSuggestions.messageId, + data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) }, + suggest: messageAndSuggestions.suggest.length !== 0 ? messageAndSuggestions.suggest : null }); } } @@ -242,7 +358,8 @@ module.exports = { methodName && !node.async && !node.generator, - node + node, + currentSegments: new Set() }; }, @@ -251,6 +368,23 @@ module.exports = { funcInfo = funcInfo.upper; }, + onUnreachableCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + onCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + }, + + onCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + // Checks the return statement is valid. ReturnStatement(node) { @@ -260,30 +394,46 @@ module.exports = { funcInfo.hasReturn = true; - let messageId = null; + const messageAndSuggestions = { messageId: "", suggest: [] }; if (funcInfo.arrayMethodName === "forEach") { // if checkForEach: true, returning a value at any path inside a forEach is not allowed if (options.checkForEach && node.argument) { - messageId = "expectedNoReturnValue"; + + if (options.allowVoid) { + if (isExpressionVoid(node.argument)) { + return; + } + + messageAndSuggestions.messageId = "expectedNoReturnValue"; + messageAndSuggestions.suggest = [{ + messageId: "prependVoid", + fix(fixer) { + return voidPrependFixer(sourceCode, node.argument, fixer); + } + }]; + } else { + messageAndSuggestions.messageId = "expectedNoReturnValue"; + } } } else { // if allowImplicit: false, should also check node.argument if (!options.allowImplicit && !node.argument) { - messageId = "expectedReturnValue"; + messageAndSuggestions.messageId = "expectedReturnValue"; } } - if (messageId) { + if (messageAndSuggestions.messageId) { context.report({ node, - messageId, + messageId: messageAndSuggestions.messageId, data: { name: astUtils.getFunctionNameWithKind(funcInfo.node), arrayMethodName: fullMethodName(funcInfo.arrayMethodName) - } + }, + suggest: messageAndSuggestions.suggest.length !== 0 ? messageAndSuggestions.suggest : null }); } }, diff --git a/lib/rules/array-element-newline.js b/lib/rules/array-element-newline.js index be547ec3617..504fe04a0b8 100644 --- a/lib/rules/array-element-newline.js +++ b/lib/rules/array-element-newline.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to enforce line breaks after each array element * @author Jan Peer Stöcklmair + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -14,6 +15,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { @@ -240,19 +243,25 @@ module.exports = { .some(element => element.loc.start.line !== element.loc.end.line); } - const linebreaksCount = node.elements.map((element, i) => { + let linebreaksCount = 0; + + for (let i = 0; i < node.elements.length; i++) { + const element = node.elements[i]; + const previousElement = elements[i - 1]; if (i === 0 || element === null || previousElement === null) { - return false; + continue; } const commaToken = sourceCode.getFirstTokenBetween(previousElement, element, astUtils.isCommaToken); const lastTokenOfPreviousElement = sourceCode.getTokenBefore(commaToken); const firstTokenOfCurrentElement = sourceCode.getTokenAfter(commaToken); - return !astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement); - }).filter(isBreak => isBreak === true).length; + if (!astUtils.isTokenOnSameLine(lastTokenOfPreviousElement, firstTokenOfCurrentElement)) { + linebreaksCount++; + } + } const needsLinebreaks = ( elements.length >= options.minItems || diff --git a/lib/rules/arrow-parens.js b/lib/rules/arrow-parens.js index 0463323176e..2206d8ce2bf 100644 --- a/lib/rules/arrow-parens.js +++ b/lib/rules/arrow-parens.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to require parens in arrow function arguments. * @author Jxck + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -30,6 +31,8 @@ function hasBlockBody(node) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/arrow-spacing.js b/lib/rules/arrow-spacing.js index fb74d2cb272..2b7d464ffcf 100644 --- a/lib/rules/arrow-spacing.js +++ b/lib/rules/arrow-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to define spacing before/after arrow function's arrow. * @author Jxck + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -17,6 +18,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/block-scoped-var.js b/lib/rules/block-scoped-var.js index 5a951276c59..ec597d5661e 100644 --- a/lib/rules/block-scoped-var.js +++ b/lib/rules/block-scoped-var.js @@ -22,7 +22,7 @@ module.exports = { schema: [], messages: { - outOfScope: "'{{name}}' used outside of binding context." + outOfScope: "'{{name}}' declared on line {{definitionLine}} column {{definitionColumn}} is used outside of binding context." } }, @@ -50,12 +50,22 @@ module.exports = { /** * Reports a given reference. * @param {eslint-scope.Reference} reference A reference to report. + * @param {eslint-scope.Definition} definition A definition for which to report reference. * @returns {void} */ - function report(reference) { + function report(reference, definition) { const identifier = reference.identifier; - - context.report({ node: identifier, messageId: "outOfScope", data: { name: identifier.name } }); + const definitionPosition = definition.name.loc.start; + + context.report({ + node: identifier, + messageId: "outOfScope", + data: { + name: identifier.name, + definitionLine: definitionPosition.line, + definitionColumn: definitionPosition.column + 1 + } + }); } /** @@ -92,7 +102,7 @@ module.exports = { variables[i] .references .filter(isOutsideOfScope) - .forEach(report); + .forEach(ref => report(ref, variables[i].defs.find(def => def.parent === node))); } } diff --git a/lib/rules/block-spacing.js b/lib/rules/block-spacing.js index dd4851c6843..9ca461158d9 100644 --- a/lib/rules/block-spacing.js +++ b/lib/rules/block-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview A rule to disallow or enforce spaces inside of single line blocks. * @author Toru Nagashima + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -14,6 +15,8 @@ const util = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/brace-style.js b/lib/rules/brace-style.js index 59758c90925..0fb4c65e68d 100644 --- a/lib/rules/brace-style.js +++ b/lib/rules/brace-style.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to flag block statements that do not use the one true brace style * @author Ian Christian Myers + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -14,6 +15,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/comma-dangle.js b/lib/rules/comma-dangle.js index e49983b722e..5f4180f12c5 100644 --- a/lib/rules/comma-dangle.js +++ b/lib/rules/comma-dangle.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to forbid or enforce dangling commas. * @author Ian Christian Myers + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -73,6 +74,8 @@ function normalizeOptions(optionValue, ecmaVersion) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/comma-spacing.js b/lib/rules/comma-spacing.js index 96015ef6779..e266de4a9c3 100644 --- a/lib/rules/comma-spacing.js +++ b/lib/rules/comma-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Comma spacing - validates spacing before and after comma * @author Vignesh Anand aka vegetableman. + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -13,6 +14,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/comma-style.js b/lib/rules/comma-style.js index bc69de4698d..0b51219531d 100644 --- a/lib/rules/comma-style.js +++ b/lib/rules/comma-style.js @@ -1,6 +1,7 @@ /** * @fileoverview Comma style - enforces comma styles of two types: last and first * @author Vignesh Anand aka vegetableman + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -14,6 +15,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/computed-property-spacing.js b/lib/rules/computed-property-spacing.js index 1e4e17c6c71..2852877fddf 100644 --- a/lib/rules/computed-property-spacing.js +++ b/lib/rules/computed-property-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Disallows or enforces spaces inside computed properties. * @author Jamund Ferguson + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -13,6 +14,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/consistent-return.js b/lib/rules/consistent-return.js index e2d3f078270..304e924b14a 100644 --- a/lib/rules/consistent-return.js +++ b/lib/rules/consistent-return.js @@ -16,12 +16,19 @@ const { upperCaseFirst } = require("../shared/string-utils"); //------------------------------------------------------------------------------ /** - * Checks whether or not a given code path segment is unreachable. - * @param {CodePathSegment} segment A CodePathSegment to check. - * @returns {boolean} `true` if the segment is unreachable. + * Checks all segments in a set and returns true if all are unreachable. + * @param {Set} segments The segments to check. + * @returns {boolean} True if all segments are unreachable; false otherwise. */ -function isUnreachable(segment) { - return !segment.reachable; +function areAllSegmentsUnreachable(segments) { + + for (const segment of segments) { + if (segment.reachable) { + return false; + } + } + + return true; } /** @@ -88,7 +95,7 @@ module.exports = { * When unreachable, all paths are returned or thrown. */ if (!funcInfo.hasReturnValue || - funcInfo.codePath.currentSegments.every(isUnreachable) || + areAllSegmentsUnreachable(funcInfo.currentSegments) || astUtils.isES5Constructor(node) || isClassConstructor(node) ) { @@ -141,13 +148,31 @@ module.exports = { hasReturn: false, hasReturnValue: false, messageId: "", - node + node, + currentSegments: new Set() }; }, onCodePathEnd() { funcInfo = funcInfo.upper; }, + onUnreachableCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + onCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + }, + + onCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + // Reports a given return statement if it's inconsistent. ReturnStatement(node) { const argument = node.argument; diff --git a/lib/rules/constructor-super.js b/lib/rules/constructor-super.js index 5f405881252..330be80f386 100644 --- a/lib/rules/constructor-super.js +++ b/lib/rules/constructor-super.js @@ -10,12 +10,19 @@ //------------------------------------------------------------------------------ /** - * Checks whether a given code path segment is reachable or not. - * @param {CodePathSegment} segment A code path segment to check. - * @returns {boolean} `true` if the segment is reachable. + * 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 isReachable(segment) { - return segment.reachable; +function isAnySegmentReachable(segments) { + + for (const segment of segments) { + if (segment.reachable) { + return true; + } + } + + return false; } /** @@ -210,7 +217,8 @@ module.exports = { isConstructor: true, hasExtends: Boolean(superClass), superIsConstructor: isPossibleConstructor(superClass), - codePath + codePath, + currentSegments: new Set() }; } else { funcInfo = { @@ -218,7 +226,8 @@ module.exports = { isConstructor: false, hasExtends: false, superIsConstructor: false, - codePath + codePath, + currentSegments: new Set() }; } }, @@ -261,6 +270,9 @@ module.exports = { * @returns {void} */ onCodePathSegmentStart(segment) { + + funcInfo.currentSegments.add(segment); + if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) { return; } @@ -281,6 +293,19 @@ module.exports = { } }, + onUnreachableCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + onCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + /** * Update information of the code path segment when a code path was * looped. @@ -344,12 +369,11 @@ module.exports = { // Reports if needed. if (funcInfo.hasExtends) { - const segments = funcInfo.codePath.currentSegments; + const segments = funcInfo.currentSegments; let duplicate = false; let info = null; - for (let i = 0; i < segments.length; ++i) { - const segment = segments[i]; + for (const segment of segments) { if (segment.reachable) { info = segInfoMap[segment.id]; @@ -374,7 +398,7 @@ module.exports = { info.validNodes.push(node); } } - } else if (funcInfo.codePath.currentSegments.some(isReachable)) { + } else if (isAnySegmentReachable(funcInfo.currentSegments)) { context.report({ messageId: "unexpected", node @@ -398,10 +422,9 @@ module.exports = { } // Returning argument is a substitute of 'super()'. - const segments = funcInfo.codePath.currentSegments; + const segments = funcInfo.currentSegments; - for (let i = 0; i < segments.length; ++i) { - const segment = segments[i]; + for (const segment of segments) { if (segment.reachable) { const info = segInfoMap[segment.id]; diff --git a/lib/rules/dot-location.js b/lib/rules/dot-location.js index dac98b06b9e..0d017c16232 100644 --- a/lib/rules/dot-location.js +++ b/lib/rules/dot-location.js @@ -1,6 +1,7 @@ /** * @fileoverview Validates newlines before and after dots * @author Greg Cochard + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -14,6 +15,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/dot-notation.js b/lib/rules/dot-notation.js index 78f13686774..21cba54e2a5 100644 --- a/lib/rules/dot-notation.js +++ b/lib/rules/dot-notation.js @@ -133,8 +133,7 @@ module.exports = { } if ( node.computed && - node.property.type === "TemplateLiteral" && - node.property.expressions.length === 0 + astUtils.isStaticTemplateLiteral(node.property) ) { checkComputedProperty(node, node.property.quasis[0].value.cooked); } diff --git a/lib/rules/eol-last.js b/lib/rules/eol-last.js index 1036db1a108..03487b039f3 100644 --- a/lib/rules/eol-last.js +++ b/lib/rules/eol-last.js @@ -1,6 +1,7 @@ /** * @fileoverview Require or disallow newline at the end of files * @author Nodeca Team + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -11,6 +12,8 @@ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/for-direction.js b/lib/rules/for-direction.js index 4ed73501581..3f2ad9df645 100644 --- a/lib/rules/for-direction.js +++ b/lib/rules/for-direction.js @@ -5,6 +5,12 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const { getStaticValue } = require("@eslint-community/eslint-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -29,6 +35,7 @@ module.exports = { }, create(context) { + const { sourceCode } = context; /** * report an error. @@ -46,17 +53,17 @@ module.exports = { * check the right side of the assignment * @param {ASTNode} update UpdateExpression to check * @param {int} dir expected direction that could either be turned around or invalidated - * @returns {int} return dir, the negated dir or zero if it's not clear for identifiers + * @returns {int} return dir, the negated dir, or zero if the counter does not change or the direction is not clear */ function getRightDirection(update, dir) { - if (update.right.type === "UnaryExpression") { - if (update.right.operator === "-") { - return -dir; - } - } else if (update.right.type === "Identifier") { - return 0; + const staticValue = getStaticValue(update.right, sourceCode.getScope(update)); + + if (staticValue && ["bigint", "boolean", "number"].includes(typeof staticValue.value)) { + const sign = Math.sign(Number(staticValue.value)) || 0; // convert NaN to 0 + + return dir * sign; } - return dir; + return 0; } /** diff --git a/lib/rules/func-call-spacing.js b/lib/rules/func-call-spacing.js index 3d5e538493e..33f73727b43 100644 --- a/lib/rules/func-call-spacing.js +++ b/lib/rules/func-call-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to control spacing within function calls * @author Matt DuVall + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -18,6 +19,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/function-call-argument-newline.js b/lib/rules/function-call-argument-newline.js index 4462afd0b7c..458399d62cd 100644 --- a/lib/rules/function-call-argument-newline.js +++ b/lib/rules/function-call-argument-newline.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to enforce line breaks between arguments of a function call * @author Alexey Gonchar + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -12,6 +13,8 @@ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/function-paren-newline.js b/lib/rules/function-paren-newline.js index 8a8714ac95d..de315a0204b 100644 --- a/lib/rules/function-paren-newline.js +++ b/lib/rules/function-paren-newline.js @@ -1,6 +1,7 @@ /** * @fileoverview enforce consistent line breaks inside function parentheses * @author Teddy Katz + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -17,6 +18,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/generator-star-spacing.js b/lib/rules/generator-star-spacing.js index 81c0b61059a..c633f979f84 100644 --- a/lib/rules/generator-star-spacing.js +++ b/lib/rules/generator-star-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to check the spacing around the * in generator functions. * @author Jamund Ferguson + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -28,6 +29,8 @@ const OVERRIDE_SCHEMA = { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/getter-return.js b/lib/rules/getter-return.js index 622b6a7541c..79ebf3e0902 100644 --- a/lib/rules/getter-return.js +++ b/lib/rules/getter-return.js @@ -14,15 +14,23 @@ const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ + const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u; /** - * Checks a given code path segment is reachable. - * @param {CodePathSegment} segment A segment to check. - * @returns {boolean} `true` if the segment is reachable. + * 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 isReachable(segment) { - return segment.reachable; +function isAnySegmentReachable(segments) { + + for (const segment of segments) { + if (segment.reachable) { + return true; + } + } + + return false; } //------------------------------------------------------------------------------ @@ -71,7 +79,8 @@ module.exports = { codePath: null, hasReturn: false, shouldCheck: false, - node: null + node: null, + currentSegments: [] }; /** @@ -85,7 +94,7 @@ module.exports = { */ function checkLastSegment(node) { if (funcInfo.shouldCheck && - funcInfo.codePath.currentSegments.some(isReachable) + isAnySegmentReachable(funcInfo.currentSegments) ) { context.report({ node, @@ -144,7 +153,8 @@ module.exports = { codePath, hasReturn: false, shouldCheck: isGetter(node), - node + node, + currentSegments: new Set() }; }, @@ -152,6 +162,21 @@ module.exports = { onCodePathEnd() { funcInfo = funcInfo.upper; }, + onUnreachableCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + onCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + }, + + onCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, // Checks the return statement is valid. ReturnStatement(node) { diff --git a/lib/rules/grouped-accessor-pairs.js b/lib/rules/grouped-accessor-pairs.js index c08e1c4973e..9556f475682 100644 --- a/lib/rules/grouped-accessor-pairs.js +++ b/lib/rules/grouped-accessor-pairs.js @@ -137,43 +137,6 @@ module.exports = { }); } - /** - * Creates a new `AccessorData` object for the given getter or setter node. - * @param {ASTNode} node A getter or setter node. - * @returns {AccessorData} New `AccessorData` object that contains the given node. - * @private - */ - function createAccessorData(node) { - const name = astUtils.getStaticPropertyName(node); - const key = (name !== null) ? name : sourceCode.getTokens(node.key); - - return { - key, - getters: node.kind === "get" ? [node] : [], - setters: node.kind === "set" ? [node] : [] - }; - } - - /** - * Merges the given `AccessorData` object into the given accessors list. - * @param {AccessorData[]} accessors The list to merge into. - * @param {AccessorData} accessorData The object to merge. - * @returns {AccessorData[]} The same instance with the merged object. - * @private - */ - function mergeAccessorData(accessors, accessorData) { - const equalKeyElement = accessors.find(a => areEqualKeys(a.key, accessorData.key)); - - if (equalKeyElement) { - equalKeyElement.getters.push(...accessorData.getters); - equalKeyElement.setters.push(...accessorData.setters); - } else { - accessors.push(accessorData); - } - - return accessors; - } - /** * Checks accessor pairs in the given list of nodes. * @param {ASTNode[]} nodes The list to check. @@ -182,11 +145,39 @@ module.exports = { * @private */ function checkList(nodes, shouldCheck) { - const accessors = nodes - .filter(shouldCheck) - .filter(isAccessorKind) - .map(createAccessorData) - .reduce(mergeAccessorData, []); + const accessors = []; + let found = false; + + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + + if (shouldCheck(node) && isAccessorKind(node)) { + + // Creates a new `AccessorData` object for the given getter or setter node. + const name = astUtils.getStaticPropertyName(node); + const key = (name !== null) ? name : sourceCode.getTokens(node.key); + + // Merges the given `AccessorData` object into the given accessors list. + for (let j = 0; j < accessors.length; j++) { + const accessor = accessors[j]; + + if (areEqualKeys(accessor.key, key)) { + accessor.getters.push(...node.kind === "get" ? [node] : []); + accessor.setters.push(...node.kind === "set" ? [node] : []); + found = true; + break; + } + } + if (!found) { + accessors.push({ + key, + getters: node.kind === "get" ? [node] : [], + setters: node.kind === "set" ? [node] : [] + }); + } + found = false; + } + } for (const { getters, setters } of accessors) { diff --git a/lib/rules/implicit-arrow-linebreak.js b/lib/rules/implicit-arrow-linebreak.js index 30ab1a5f3d0..32f422ce828 100644 --- a/lib/rules/implicit-arrow-linebreak.js +++ b/lib/rules/implicit-arrow-linebreak.js @@ -1,6 +1,7 @@ /** * @fileoverview enforce the location of arrow function bodies * @author Sharmila Jesupaul + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -12,6 +13,8 @@ const { isCommentToken, isNotOpeningParenToken } = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/indent.js b/lib/rules/indent.js index bcc5143d26e..9bcbd640c4d 100644 --- a/lib/rules/indent.js +++ b/lib/rules/indent.js @@ -4,6 +4,7 @@ * @author Teddy Katz * @author Vitaly Puzrin * @author Gyandeep Singh + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -188,15 +189,19 @@ class TokenInfo { */ constructor(sourceCode) { this.sourceCode = sourceCode; - this.firstTokensByLineNumber = sourceCode.tokensAndComments.reduce((map, token) => { - if (!map.has(token.loc.start.line)) { - map.set(token.loc.start.line, token); + this.firstTokensByLineNumber = new Map(); + const tokens = sourceCode.tokensAndComments; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + + if (!this.firstTokensByLineNumber.has(token.loc.start.line)) { + this.firstTokensByLineNumber.set(token.loc.start.line, token); } - if (!map.has(token.loc.end.line) && sourceCode.text.slice(token.range[1] - token.loc.end.column, token.range[1]).trim()) { - map.set(token.loc.end.line, token); + if (!this.firstTokensByLineNumber.has(token.loc.end.line) && sourceCode.text.slice(token.range[1] - token.loc.end.column, token.range[1]).trim()) { + this.firstTokensByLineNumber.set(token.loc.end.line, token); } - return map; - }, new Map()); + } } /** @@ -489,6 +494,8 @@ const ELEMENT_LIST_SCHEMA = { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { @@ -964,19 +971,19 @@ module.exports = { const parenStack = []; const parenPairs = []; - tokens.forEach(nextToken => { + for (let i = 0; i < tokens.length; i++) { + const nextToken = tokens[i]; - // Accumulate a list of parenthesis pairs if (astUtils.isOpeningParenToken(nextToken)) { parenStack.push(nextToken); } else if (astUtils.isClosingParenToken(nextToken)) { - parenPairs.unshift({ left: parenStack.pop(), right: nextToken }); + parenPairs.push({ left: parenStack.pop(), right: nextToken }); } - }); + } - parenPairs.forEach(pair => { - const leftParen = pair.left; - const rightParen = pair.right; + for (let i = parenPairs.length - 1; i >= 0; i--) { + const leftParen = parenPairs[i].left; + const rightParen = parenPairs[i].right; // We only want to handle parens around expressions, so exclude parentheses that are in function parameters and function call arguments. if (!parameterParens.has(leftParen) && !parameterParens.has(rightParen)) { @@ -990,7 +997,7 @@ module.exports = { } offsets.setDesiredOffset(rightParen, leftParen, 0); - }); + } } /** @@ -1246,7 +1253,7 @@ module.exports = { IfStatement(node) { addBlocklessNodeIndent(node.consequent); - if (node.alternate && node.alternate.type !== "IfStatement") { + if (node.alternate) { addBlocklessNodeIndent(node.alternate); } }, @@ -1711,9 +1718,13 @@ module.exports = { } // Invoke the queued offset listeners for the nodes that aren't ignored. - listenerCallQueue - .filter(nodeInfo => !ignoredNodes.has(nodeInfo.node)) - .forEach(nodeInfo => nodeInfo.listener(nodeInfo.node)); + for (let i = 0; i < listenerCallQueue.length; i++) { + const nodeInfo = listenerCallQueue[i]; + + if (!ignoredNodes.has(nodeInfo.node)) { + nodeInfo.listener(nodeInfo.node); + } + } // Update the offsets for ignored nodes to prevent their child tokens from being reported. ignoredNodes.forEach(ignoreNode); @@ -1724,27 +1735,31 @@ module.exports = { * Create a Map from (tokenOrComment) => (precedingToken). * This is necessary because sourceCode.getTokenBefore does not handle a comment as an argument correctly. */ - const precedingTokens = sourceCode.ast.comments.reduce((commentMap, comment) => { + const precedingTokens = new WeakMap(); + + for (let i = 0; i < sourceCode.ast.comments.length; i++) { + const comment = sourceCode.ast.comments[i]; + const tokenOrCommentBefore = sourceCode.getTokenBefore(comment, { includeComments: true }); + const hasToken = precedingTokens.has(tokenOrCommentBefore) ? precedingTokens.get(tokenOrCommentBefore) : tokenOrCommentBefore; - return commentMap.set(comment, commentMap.has(tokenOrCommentBefore) ? commentMap.get(tokenOrCommentBefore) : tokenOrCommentBefore); - }, new WeakMap()); + precedingTokens.set(comment, hasToken); + } - sourceCode.lines.forEach((line, lineIndex) => { - const lineNumber = lineIndex + 1; + for (let i = 1; i < sourceCode.lines.length + 1; i++) { - if (!tokenInfo.firstTokensByLineNumber.has(lineNumber)) { + if (!tokenInfo.firstTokensByLineNumber.has(i)) { // Don't check indentation on blank lines - return; + continue; } - const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(lineNumber); + const firstTokenOfLine = tokenInfo.firstTokensByLineNumber.get(i); - if (firstTokenOfLine.loc.start.line !== lineNumber) { + if (firstTokenOfLine.loc.start.line !== i) { // Don't check the indentation of multi-line tokens (e.g. template literals or block comments) twice. - return; + continue; } if (astUtils.isCommentToken(firstTokenOfLine)) { @@ -1769,18 +1784,18 @@ module.exports = { mayAlignWithBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) || mayAlignWithAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter)) ) { - return; + continue; } } // If the token matches the expected indentation, don't report it. if (validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine))) { - return; + continue; } // Otherwise, report the token/comment. report(firstTokenOfLine, offsets.getDesiredIndent(firstTokenOfLine)); - }); + } } } ); diff --git a/lib/rules/index.js b/lib/rules/index.js index e42639656f7..840abe73b0f 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -175,6 +175,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({ "no-new-wrappers": () => require("./no-new-wrappers"), "no-nonoctal-decimal-escape": () => require("./no-nonoctal-decimal-escape"), "no-obj-calls": () => require("./no-obj-calls"), + "no-object-constructor": () => require("./no-object-constructor"), "no-octal": () => require("./no-octal"), "no-octal-escape": () => require("./no-octal-escape"), "no-param-reassign": () => require("./no-param-reassign"), diff --git a/lib/rules/jsx-quotes.js b/lib/rules/jsx-quotes.js index a41c85170fd..3dcd5fa9d22 100644 --- a/lib/rules/jsx-quotes.js +++ b/lib/rules/jsx-quotes.js @@ -1,6 +1,7 @@ /** * @fileoverview A rule to ensure consistent quotes used in jsx syntax. * @author Mathias Schreck + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -39,6 +40,8 @@ const QUOTE_SETTINGS = { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/key-spacing.js b/lib/rules/key-spacing.js index 0b51eb3fe13..19fc0167ae0 100644 --- a/lib/rules/key-spacing.js +++ b/lib/rules/key-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to specify spacing of object literal keys and values * @author Brandon Mills + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -133,6 +134,8 @@ function initOptions(toOptions, fromOptions) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/keyword-spacing.js b/lib/rules/keyword-spacing.js index 8ed82199810..9d18441e0e5 100644 --- a/lib/rules/keyword-spacing.js +++ b/lib/rules/keyword-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to enforce spacing before and after keywords. * @author Toru Nagashima + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -64,6 +65,8 @@ function isCloseParenOfTemplate(token) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/linebreak-style.js b/lib/rules/linebreak-style.js index d8f36094b2e..e59acca1b5c 100644 --- a/lib/rules/linebreak-style.js +++ b/lib/rules/linebreak-style.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to enforce a single linebreak style. * @author Erik Mueller + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -18,6 +19,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/lines-around-comment.js b/lib/rules/lines-around-comment.js index 10aeba3cbc1..2a6e472f9a0 100644 --- a/lib/rules/lines-around-comment.js +++ b/lib/rules/lines-around-comment.js @@ -1,6 +1,7 @@ /** * @fileoverview Enforces empty lines around comments. * @author Jamund Ferguson + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -52,6 +53,8 @@ function getCommentLineNums(comments) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/lines-between-class-members.js b/lib/rules/lines-between-class-members.js index dee4bab5f54..5f36d468dc0 100644 --- a/lib/rules/lines-between-class-members.js +++ b/lib/rules/lines-between-class-members.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to check empty newline between class members * @author 薛定谔的猫 + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -10,6 +11,21 @@ const astUtils = require("./utils/ast-utils"); +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Types of class members. + * Those have `test` method to check it matches to the given class member. + * @private + */ +const ClassMemberTypes = { + "*": { test: () => true }, + field: { test: node => node.type === "PropertyDefinition" }, + method: { test: node => node.type === "MethodDefinition" } +}; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -17,6 +33,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { @@ -29,7 +47,32 @@ module.exports = { schema: [ { - enum: ["always", "never"] + anyOf: [ + { + type: "object", + properties: { + enforce: { + type: "array", + items: { + type: "object", + properties: { + blankLine: { enum: ["always", "never"] }, + prev: { enum: ["method", "field", "*"] }, + next: { enum: ["method", "field", "*"] } + }, + additionalProperties: false, + required: ["blankLine", "prev", "next"] + }, + minItems: 1 + } + }, + additionalProperties: false, + required: ["enforce"] + }, + { + enum: ["always", "never"] + } + ] }, { type: "object", @@ -55,6 +98,7 @@ module.exports = { options[0] = context.options[0] || "always"; options[1] = context.options[1] || { exceptAfterSingleLine: false }; + const configureList = typeof options[0] === "object" ? options[0].enforce : [{ blankLine: options[0], prev: "*", next: "*" }]; const sourceCode = context.sourceCode; /** @@ -144,6 +188,38 @@ module.exports = { return sourceCode.getTokensBetween(before, after, { includeComments: true }).length !== 0; } + /** + * Checks whether the given node matches the given type. + * @param {ASTNode} node The class member node to check. + * @param {string} type The class member type to check. + * @returns {boolean} `true` if the class member node matched the type. + * @private + */ + function match(node, type) { + return ClassMemberTypes[type].test(node); + } + + /** + * Finds the last matched configuration from the configureList. + * @param {ASTNode} prevNode The previous node to match. + * @param {ASTNode} nextNode The current node to match. + * @returns {string|null} Padding type or `null` if no matches were found. + * @private + */ + function getPaddingType(prevNode, nextNode) { + for (let i = configureList.length - 1; i >= 0; --i) { + const configure = configureList[i]; + const matched = + match(prevNode, configure.prev) && + match(nextNode, configure.next); + + if (matched) { + return configure.blankLine; + } + } + return null; + } + return { ClassBody(node) { const body = node.body; @@ -158,22 +234,34 @@ module.exports = { const isPadded = afterPadding.loc.start.line - beforePadding.loc.end.line > 1; const hasTokenInPadding = hasTokenOrCommentBetween(beforePadding, afterPadding); const curLineLastToken = findLastConsecutiveTokenAfter(curLast, nextFirst, 0); + const paddingType = getPaddingType(body[i], body[i + 1]); + + if (paddingType === "never" && isPadded) { + context.report({ + node: body[i + 1], + messageId: "never", - if ((options[0] === "always" && !skip && !isPadded) || - (options[0] === "never" && isPadded)) { + fix(fixer) { + if (hasTokenInPadding) { + return null; + } + return fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n"); + } + }); + } else if (paddingType === "always" && !skip && !isPadded) { context.report({ node: body[i + 1], - messageId: isPadded ? "never" : "always", + messageId: "always", + fix(fixer) { if (hasTokenInPadding) { return null; } - return isPadded - ? fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n") - : fixer.insertTextAfter(curLineLastToken, "\n"); + return fixer.insertTextAfter(curLineLastToken, "\n"); } }); } + } } }; diff --git a/lib/rules/logical-assignment-operators.js b/lib/rules/logical-assignment-operators.js index d373bec6d09..c084c04c8ed 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 //------------------------------------------------------------------------------ @@ -188,7 +213,6 @@ module.exports = { }] }, fixable: "code", - // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- Does not detect conditional suggestions hasSuggestions: true, messages: { assignment: "Assignment (=) can be replaced with operator assignment ({{operator}}).", @@ -319,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; } @@ -343,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]]); } }; @@ -371,8 +398,11 @@ module.exports = { return; } - const requiresOuterParenthesis = logical.parent.type !== "ExpressionStatement" && - (astUtils.getPrecedence({ type: "AssignmentExpression" }) < astUtils.getPrecedence(logical.parent)); + const parentPrecedence = astUtils.getPrecedence(logical.parent); + const requiresOuterParenthesis = logical.parent.type !== "ExpressionStatement" && ( + parentPrecedence === -1 || + astUtils.getPrecedence({ type: "AssignmentExpression" }) < parentPrecedence + ); if (!astUtils.isParenthesised(sourceCode, logical) && requiresOuterParenthesis) { yield ruleFixer.insertTextBefore(logical, "("); diff --git a/lib/rules/max-len.js b/lib/rules/max-len.js index 59e85214a29..138a0f239fd 100644 --- a/lib/rules/max-len.js +++ b/lib/rules/max-len.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to check for max length on a line. * @author Matt DuVall + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -66,6 +67,8 @@ const OPTIONS_OR_INTEGER_SCHEMA = { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { @@ -252,19 +255,23 @@ module.exports = { return sourceCode.ast.tokens.filter(token => token.type === "RegularExpression"); } - /** - * A reducer to group an AST node by line number, both start and end. - * @param {Object} acc the accumulator - * @param {ASTNode} node the AST node in question - * @returns {Object} the modified accumulator - * @private + * + * reduce an array of AST nodes by line number, both start and end. + * @param {ASTNode[]} arr array of AST nodes + * @returns {Object} accululated AST nodes */ - function groupByLineNumber(acc, node) { - for (let i = node.loc.start.line; i <= node.loc.end.line; ++i) { - ensureArrayAndPush(acc, i, node); + function groupArrayByLineNumber(arr) { + const obj = {}; + + for (let i = 0; i < arr.length; i++) { + const node = arr[i]; + + for (let j = node.loc.start.line; j <= node.loc.end.line; ++j) { + ensureArrayAndPush(obj, j, node); + } } - return acc; + return obj; } /** @@ -312,13 +319,13 @@ module.exports = { let commentsIndex = 0; const strings = getAllStrings(); - const stringsByLine = strings.reduce(groupByLineNumber, {}); + const stringsByLine = groupArrayByLineNumber(strings); const templateLiterals = getAllTemplateLiterals(); - const templateLiteralsByLine = templateLiterals.reduce(groupByLineNumber, {}); + const templateLiteralsByLine = groupArrayByLineNumber(templateLiterals); const regExpLiterals = getAllRegExpLiterals(); - const regExpLiteralsByLine = regExpLiterals.reduce(groupByLineNumber, {}); + const regExpLiteralsByLine = groupArrayByLineNumber(regExpLiterals); lines.forEach((line, i) => { diff --git a/lib/rules/max-statements-per-line.js b/lib/rules/max-statements-per-line.js index b9665048764..4ad73a67f98 100644 --- a/lib/rules/max-statements-per-line.js +++ b/lib/rules/max-statements-per-line.js @@ -1,6 +1,7 @@ /** * @fileoverview Specify the maximum number of statements allowed per line. * @author Kenneth Williams + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -17,6 +18,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/multiline-ternary.js b/lib/rules/multiline-ternary.js index f156fe32bb1..8155dd7a5a6 100644 --- a/lib/rules/multiline-ternary.js +++ b/lib/rules/multiline-ternary.js @@ -1,6 +1,7 @@ /** * @fileoverview Enforce newlines between operands of ternary expressions * @author Kai Cataldo + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -14,6 +15,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/new-parens.js b/lib/rules/new-parens.js index e8667310f29..1c5d21d4a0c 100644 --- a/lib/rules/new-parens.js +++ b/lib/rules/new-parens.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to flag when using constructor without parentheses * @author Ilya Volodin + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -22,6 +23,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/newline-after-var.js b/lib/rules/newline-after-var.js index 5c9b5fbd10f..dc8b24d4738 100644 --- a/lib/rules/newline-after-var.js +++ b/lib/rules/newline-after-var.js @@ -212,7 +212,6 @@ module.exports = { context.report({ node, messageId: "unexpected", - data: { identifier: node.name }, fix(fixer) { const linesBetween = sourceCode.getText().slice(lastToken.range[1], nextToken.range[0]).split(astUtils.LINEBREAK_MATCHER); @@ -231,7 +230,6 @@ module.exports = { context.report({ node, messageId: "expected", - data: { identifier: node.name }, fix(fixer) { if ((noNextLineToken ? getLastCommentLineOfBlock(nextLineNum) : lastToken.loc.end.line) === nextToken.loc.start.line) { return fixer.insertTextBefore(nextToken, "\n\n"); diff --git a/lib/rules/newline-per-chained-call.js b/lib/rules/newline-per-chained-call.js index b2e6cd9e49d..3124ac2d19f 100644 --- a/lib/rules/newline-per-chained-call.js +++ b/lib/rules/newline-per-chained-call.js @@ -2,6 +2,7 @@ * @fileoverview Rule to ensure newline per method call when chaining calls * @author Rajendra Patil * @author Burak Yigit Kaya + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -15,6 +16,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/no-confusing-arrow.js b/lib/rules/no-confusing-arrow.js index de6e2f30c2e..6fef1870eb2 100644 --- a/lib/rules/no-confusing-arrow.js +++ b/lib/rules/no-confusing-arrow.js @@ -2,6 +2,7 @@ * @fileoverview A rule to warn against using arrow functions when they could be * confused with comparisons * @author Jxck + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -28,6 +29,8 @@ function isConditional(node) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "suggestion", docs: { diff --git a/lib/rules/no-control-regex.js b/lib/rules/no-control-regex.js index 086747f3746..dc412fcabd5 100644 --- a/lib/rules/no-control-regex.js +++ b/lib/rules/no-control-regex.js @@ -14,6 +14,16 @@ const collector = new (class { } onPatternEnter() { + + /* + * `RegExpValidator` may parse the pattern twice in one `validatePattern`. + * So `this._controlChars` should be cleared here as well. + * + * For example, the `/(?\x1f)/` regex will parse the pattern twice. + * This is based on the content described in Annex B. + * If the regex contains a `GroupName` and the `u` flag is not used, `ParseText` will be called twice. + * See https://tc39.es/ecma262/2023/multipage/additional-ecmascript-features-for-web-browsers.html#sec-parsepattern-annexb + */ this._controlChars = []; } @@ -32,10 +42,13 @@ const collector = new (class { collectControlChars(regexpStr, flags) { const uFlag = typeof flags === "string" && flags.includes("u"); + const vFlag = typeof flags === "string" && flags.includes("v"); + + this._controlChars = []; + this._source = regexpStr; try { - this._source = regexpStr; - this._validator.validatePattern(regexpStr, void 0, void 0, uFlag); // Call onCharacter hook + this._validator.validatePattern(regexpStr, void 0, void 0, { unicode: uFlag, unicodeSets: vFlag }); // Call onCharacter hook } catch { // Ignore syntax errors in RegExp. diff --git a/lib/rules/no-empty-character-class.js b/lib/rules/no-empty-character-class.js index da29bbe9270..5c8410235bc 100644 --- a/lib/rules/no-empty-character-class.js +++ b/lib/rules/no-empty-character-class.js @@ -5,20 +5,18 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp"); + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ -/* - * plain-English description of the following regexp: - * 0. `^` fix the match at the beginning of the string - * 1. `([^\\[]|\\.|\[([^\\\]]|\\.)+\])*`: regexp contents; 0 or more of the following - * 1.0. `[^\\[]`: any character that's not a `\` or a `[` (anything but escape sequences and character classes) - * 1.1. `\\.`: an escape sequence - * 1.2. `\[([^\\\]]|\\.)+\]`: a character class that isn't empty - * 2. `$`: fix the match at the end of the string - */ -const regex = /^([^\\[]|\\.|\[([^\\\]]|\\.)+\])*$/u; +const parser = new RegExpParser(); +const QUICK_TEST_REGEX = /\[\]/u; //------------------------------------------------------------------------------ // Rule Definition @@ -45,9 +43,32 @@ module.exports = { create(context) { return { "Literal[regex]"(node) { - if (!regex.test(node.regex.pattern)) { - context.report({ node, messageId: "unexpected" }); + const { pattern, flags } = node.regex; + + if (!QUICK_TEST_REGEX.test(pattern)) { + return; } + + let regExpAST; + + try { + regExpAST = parser.parsePattern(pattern, 0, pattern.length, { + unicode: flags.includes("u"), + unicodeSets: flags.includes("v") + }); + } catch { + + // Ignore regular expressions that regexpp cannot parse + return; + } + + visitRegExpAST(regExpAST, { + onCharacterClassEnter(characterClass) { + if (!characterClass.negate && characterClass.elements.length === 0) { + context.report({ node, messageId: "unexpected" }); + } + } + }); } }; diff --git a/lib/rules/no-empty-pattern.js b/lib/rules/no-empty-pattern.js index abb1a7c6ddb..fb75f6d25b3 100644 --- a/lib/rules/no-empty-pattern.js +++ b/lib/rules/no-empty-pattern.js @@ -4,6 +4,8 @@ */ "use strict"; +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -19,7 +21,18 @@ module.exports = { url: "https://eslint.org/docs/latest/rules/no-empty-pattern" }, - schema: [], + schema: [ + { + type: "object", + properties: { + allowObjectPatternsAsParameters: { + type: "boolean", + default: false + } + }, + additionalProperties: false + } + ], messages: { unexpected: "Unexpected empty {{type}} pattern." @@ -27,11 +40,33 @@ module.exports = { }, create(context) { + const options = context.options[0] || {}, + allowObjectPatternsAsParameters = options.allowObjectPatternsAsParameters || false; + return { ObjectPattern(node) { - if (node.properties.length === 0) { - context.report({ node, messageId: "unexpected", data: { type: "object" } }); + + if (node.properties.length > 0) { + return; } + + // Allow {} and {} = {} empty object patterns as parameters when allowObjectPatternsAsParameters is true + if ( + allowObjectPatternsAsParameters && + ( + astUtils.isFunction(node.parent) || + ( + node.parent.type === "AssignmentPattern" && + astUtils.isFunction(node.parent.parent) && + node.parent.right.type === "ObjectExpression" && + node.parent.right.properties.length === 0 + ) + ) + ) { + return; + } + + context.report({ node, messageId: "unexpected", data: { type: "object" } }); }, ArrayPattern(node) { if (node.elements.length === 0) { diff --git a/lib/rules/no-extra-parens.js b/lib/rules/no-extra-parens.js index 0f59d7dd382..75c082baf2e 100644 --- a/lib/rules/no-extra-parens.js +++ b/lib/rules/no-extra-parens.js @@ -1,6 +1,7 @@ /** * @fileoverview Disallow parenthesising higher precedence subexpressions. * @author Michael Ficarra + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -14,6 +15,8 @@ const astUtils = require("./utils/ast-utils.js"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { @@ -46,6 +49,7 @@ module.exports = { type: "object", properties: { conditionalAssign: { type: "boolean" }, + ternaryOperandBinaryExpressions: { type: "boolean" }, nestedBinaryExpressions: { type: "boolean" }, returnAssign: { type: "boolean" }, ignoreJSX: { enum: ["none", "all", "single-line", "multi-line"] }, @@ -76,6 +80,7 @@ module.exports = { const precedence = astUtils.getPrecedence; const ALL_NODES = context.options[0] !== "functions"; const EXCEPT_COND_ASSIGN = ALL_NODES && context.options[1] && context.options[1].conditionalAssign === false; + const EXCEPT_COND_TERNARY = ALL_NODES && context.options[1] && context.options[1].ternaryOperandBinaryExpressions === false; const NESTED_BINARY = ALL_NODES && context.options[1] && context.options[1].nestedBinaryExpressions === false; const EXCEPT_RETURN_ASSIGN = ALL_NODES && context.options[1] && context.options[1].returnAssign === false; const IGNORE_JSX = ALL_NODES && context.options[1] && context.options[1].ignoreJSX; @@ -386,6 +391,30 @@ module.exports = { return node && (node.type === "Identifier" || node.type === "MemberExpression"); } + /** + * Checks if a node is fixable. + * A node is fixable if removing a single pair of surrounding parentheses does not turn it + * into a directive after fixing other nodes. + * Almost all nodes are fixable, except if all of the following conditions are met: + * The node is a string Literal + * It has a single pair of parentheses + * It is the only child of an ExpressionStatement + * @param {ASTNode} node The node to evaluate. + * @returns {boolean} Whether or not the node is fixable. + * @private + */ + function isFixable(node) { + + // if it's not a string literal it can be autofixed + if (node.type !== "Literal" || typeof node.value !== "string") { + return true; + } + if (isParenthesisedTwice(node)) { + return true; + } + return !astUtils.isTopLevelExpressionStatement(node.parent); + } + /** * Report the node * @param {ASTNode} node node to evaluate @@ -429,14 +458,16 @@ module.exports = { node, loc: leftParenToken.loc, messageId: "unexpected", - fix(fixer) { - const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0]); - - return fixer.replaceTextRange([ - leftParenToken.range[0], - rightParenToken.range[1] - ], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource + (requiresTrailingSpace(node) ? " " : "")); - } + fix: isFixable(node) + ? fixer => { + const parenthesizedSource = sourceCode.text.slice(leftParenToken.range[1], rightParenToken.range[0]); + + return fixer.replaceTextRange([ + leftParenToken.range[0], + rightParenToken.range[1] + ], (requiresLeadingSpace(node) ? " " : "") + parenthesizedSource + (requiresTrailingSpace(node) ? " " : "")); + } + : null }); } @@ -860,18 +891,26 @@ module.exports = { if (isReturnAssignException(node)) { return; } + + const availableTypes = new Set(["BinaryExpression", "LogicalExpression"]); + if ( + !(EXCEPT_COND_TERNARY && availableTypes.has(node.test.type)) && !isCondAssignException(node) && hasExcessParensWithPrecedence(node.test, precedence({ type: "LogicalExpression", operator: "||" })) ) { report(node.test); } - if (hasExcessParensWithPrecedence(node.consequent, PRECEDENCE_OF_ASSIGNMENT_EXPR)) { + if ( + !(EXCEPT_COND_TERNARY && availableTypes.has(node.consequent.type)) && + hasExcessParensWithPrecedence(node.consequent, PRECEDENCE_OF_ASSIGNMENT_EXPR)) { report(node.consequent); } - if (hasExcessParensWithPrecedence(node.alternate, PRECEDENCE_OF_ASSIGNMENT_EXPR)) { + if ( + !(EXCEPT_COND_TERNARY && availableTypes.has(node.alternate.type)) && + hasExcessParensWithPrecedence(node.alternate, PRECEDENCE_OF_ASSIGNMENT_EXPR)) { report(node.alternate); } }, diff --git a/lib/rules/no-extra-semi.js b/lib/rules/no-extra-semi.js index ebf145f9cd1..af7eb888845 100644 --- a/lib/rules/no-extra-semi.js +++ b/lib/rules/no-extra-semi.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to flag use of unnecessary semicolons * @author Nicholas C. Zakas + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -19,6 +20,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "suggestion", docs: { @@ -38,6 +41,23 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; + /** + * Checks if a node or token is fixable. + * A node is fixable if it can be removed without turning a subsequent statement into a directive after fixing other nodes. + * @param {Token} nodeOrToken The node or token to check. + * @returns {boolean} Whether or not the node is fixable. + */ + function isFixable(nodeOrToken) { + const nextToken = sourceCode.getTokenAfter(nodeOrToken); + + if (!nextToken || nextToken.type !== "String") { + return true; + } + const stringNode = sourceCode.getNodeByRangeIndex(nextToken.range[0]); + + return !astUtils.isTopLevelExpressionStatement(stringNode.parent); + } + /** * Reports an unnecessary semicolon error. * @param {Node|Token} nodeOrToken A node or a token to be reported. @@ -47,17 +67,18 @@ module.exports = { context.report({ node: nodeOrToken, messageId: "unexpected", - fix(fixer) { - - /* - * Expand the replacement range to include the surrounding - * tokens to avoid conflicting with semi. - * https://github.com/eslint/eslint/issues/7928 - */ - return new FixTracker(fixer, context.sourceCode) - .retainSurroundingTokens(nodeOrToken) - .remove(nodeOrToken); - } + fix: isFixable(nodeOrToken) + ? fixer => + + /* + * Expand the replacement range to include the surrounding + * tokens to avoid conflicting with semi. + * https://github.com/eslint/eslint/issues/7928 + */ + new FixTracker(fixer, context.sourceCode) + .retainSurroundingTokens(nodeOrToken) + .remove(nodeOrToken) + : null }); } diff --git a/lib/rules/no-fallthrough.js b/lib/rules/no-fallthrough.js index bd2ee9bbe2c..91da1212022 100644 --- a/lib/rules/no-fallthrough.js +++ b/lib/rules/no-fallthrough.js @@ -16,6 +16,22 @@ const { directivesPattern } = require("../shared/directives"); const DEFAULT_FALLTHROUGH_COMMENT = /falls?\s?through/iu; +/** + * 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 comment string is really a fallthrough comment and not an ESLint directive. * @param {string} comment The comment string to check. @@ -51,15 +67,6 @@ function hasFallthroughComment(caseWhichFallsThrough, subsequentCase, context, f return Boolean(comment && isFallThroughComment(comment.value, fallthroughCommentPattern)); } -/** - * Checks whether or not a given code path segment is reachable. - * @param {CodePathSegment} segment A CodePathSegment to check. - * @returns {boolean} `true` if the segment is reachable. - */ -function isReachable(segment) { - return segment.reachable; -} - /** * Checks whether a node and a token are separated by blank lines * @param {ASTNode} node The node to check @@ -109,7 +116,8 @@ module.exports = { create(context) { const options = context.options[0] || {}; - let currentCodePath = null; + const codePathSegments = []; + let currentCodePathSegments = new Set(); const sourceCode = context.sourceCode; const allowEmptyCase = options.allowEmptyCase || false; @@ -126,13 +134,33 @@ module.exports = { fallthroughCommentPattern = DEFAULT_FALLTHROUGH_COMMENT; } return { - onCodePathStart(codePath) { - currentCodePath = codePath; + + onCodePathStart() { + codePathSegments.push(currentCodePathSegments); + currentCodePathSegments = new Set(); }, + onCodePathEnd() { - currentCodePath = currentCodePath.upper; + currentCodePathSegments = codePathSegments.pop(); + }, + + onUnreachableCodePathSegmentStart(segment) { + currentCodePathSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + currentCodePathSegments.delete(segment); + }, + + onCodePathSegmentStart(segment) { + currentCodePathSegments.add(segment); }, + onCodePathSegmentEnd(segment) { + currentCodePathSegments.delete(segment); + }, + + SwitchCase(node) { /* @@ -157,7 +185,7 @@ module.exports = { * `break`, `return`, or `throw` are unreachable. * And allows empty cases and the last case. */ - if (currentCodePath.currentSegments.some(isReachable) && + if (isAnySegmentReachable(currentCodePathSegments) && (node.consequent.length > 0 || (!allowEmptyCase && hasBlankLinesBetween(node, nextToken))) && node.parent.cases[node.parent.cases.length - 1] !== node) { fallthroughCase = node; diff --git a/lib/rules/no-floating-decimal.js b/lib/rules/no-floating-decimal.js index c26876440a5..80e4994cd72 100644 --- a/lib/rules/no-floating-decimal.js +++ b/lib/rules/no-floating-decimal.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to flag use of a leading/trailing decimal point in a numeric literal * @author James Allardice + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -18,6 +19,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "suggestion", docs: { diff --git a/lib/rules/no-invalid-regexp.js b/lib/rules/no-invalid-regexp.js index 9a35743d122..3c42a68e8a3 100644 --- a/lib/rules/no-invalid-regexp.js +++ b/lib/rules/no-invalid-regexp.js @@ -10,7 +10,7 @@ const RegExpValidator = require("@eslint-community/regexpp").RegExpValidator; const validator = new RegExpValidator(); -const validFlags = /[dgimsuy]/gu; +const validFlags = /[dgimsuvy]/gu; const undefined1 = void 0; //------------------------------------------------------------------------------ @@ -108,12 +108,14 @@ module.exports = { /** * Check syntax error in a given pattern. * @param {string} pattern The RegExp pattern to validate. - * @param {boolean} uFlag The Unicode flag. + * @param {Object} flags The RegExp flags to validate. + * @param {boolean} [flags.unicode] The Unicode flag. + * @param {boolean} [flags.unicodeSets] The UnicodeSets flag. * @returns {string|null} The syntax error. */ - function validateRegExpPattern(pattern, uFlag) { + function validateRegExpPattern(pattern, flags) { try { - validator.validatePattern(pattern, undefined1, undefined1, uFlag); + validator.validatePattern(pattern, undefined1, undefined1, flags); return null; } catch (err) { return err.message; @@ -131,10 +133,19 @@ module.exports = { } try { validator.validateFlags(flags); - return null; } catch { return `Invalid flags supplied to RegExp constructor '${flags}'`; } + + /* + * `regexpp` checks the combination of `u` and `v` flags when parsing `Pattern` according to `ecma262`, + * but this rule may check only the flag when the pattern is unidentifiable, so check it here. + * https://tc39.es/ecma262/multipage/text-processing.html#sec-parsepattern + */ + if (flags.includes("u") && flags.includes("v")) { + return "Regex 'u' and 'v' flags cannot be used together"; + } + return null; } return { @@ -166,8 +177,12 @@ module.exports = { // If flags are unknown, report the regex only if its pattern is invalid both with and without the "u" flag flags === null - ? validateRegExpPattern(pattern, true) && validateRegExpPattern(pattern, false) - : validateRegExpPattern(pattern, flags.includes("u")) + ? ( + validateRegExpPattern(pattern, { unicode: true, unicodeSets: false }) && + validateRegExpPattern(pattern, { unicode: false, unicodeSets: true }) && + validateRegExpPattern(pattern, { unicode: false, unicodeSets: false }) + ) + : validateRegExpPattern(pattern, { unicode: flags.includes("u"), unicodeSets: flags.includes("v") }) ); if (message) { diff --git a/lib/rules/no-irregular-whitespace.js b/lib/rules/no-irregular-whitespace.js index 67519b5e851..ab7ccac54e4 100644 --- a/lib/rules/no-irregular-whitespace.js +++ b/lib/rules/no-irregular-whitespace.js @@ -55,6 +55,10 @@ module.exports = { skipRegExps: { type: "boolean", default: false + }, + skipJSXText: { + type: "boolean", + default: false } }, additionalProperties: false @@ -77,6 +81,7 @@ module.exports = { const skipStrings = options.skipStrings !== false; const skipRegExps = !!options.skipRegExps; const skipTemplates = !!options.skipTemplates; + const skipJSXText = !!options.skipJSXText; const sourceCode = context.sourceCode; const commentNodes = sourceCode.getAllComments(); @@ -100,12 +105,12 @@ module.exports = { } /** - * Checks identifier or literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors + * Checks literal nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors * @param {ASTNode} node to check for matching errors. * @returns {void} * @private */ - function removeInvalidNodeErrorsInIdentifierOrLiteral(node) { + function removeInvalidNodeErrorsInLiteral(node) { const shouldCheckStrings = skipStrings && (typeof node.value === "string"); const shouldCheckRegExps = skipRegExps && Boolean(node.regex); @@ -144,6 +149,18 @@ module.exports = { } } + /** + * Checks JSX nodes for errors that we are choosing to ignore and calls the relevant methods to remove the errors + * @param {ASTNode} node to check for matching errors. + * @returns {void} + * @private + */ + function removeInvalidNodeErrorsInJSXText(node) { + if (ALL_IRREGULARS.test(node.raw)) { + removeWhitespaceError(node); + } + } + /** * Checks the program source for irregular whitespace * @param {ASTNode} node The program node @@ -237,9 +254,9 @@ module.exports = { checkForIrregularLineTerminators(node); }; - nodes.Identifier = removeInvalidNodeErrorsInIdentifierOrLiteral; - nodes.Literal = removeInvalidNodeErrorsInIdentifierOrLiteral; + nodes.Literal = removeInvalidNodeErrorsInLiteral; nodes.TemplateElement = skipTemplates ? removeInvalidNodeErrorsInTemplateLiteral : noop; + nodes.JSXText = skipJSXText ? removeInvalidNodeErrorsInJSXText : noop; nodes["Program:exit"] = function() { if (skipComments) { diff --git a/lib/rules/no-loop-func.js b/lib/rules/no-loop-func.js index e1d65fdc92c..48312fbf58a 100644 --- a/lib/rules/no-loop-func.js +++ b/lib/rules/no-loop-func.js @@ -186,7 +186,7 @@ module.exports = { } const references = sourceCode.getScope(node).through; - const unsafeRefs = references.filter(r => !isSafe(loopNode, r)).map(r => r.identifier.name); + const unsafeRefs = references.filter(r => r.resolved && !isSafe(loopNode, r)).map(r => r.identifier.name); if (unsafeRefs.length > 0) { context.report({ diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 22ca7f93e3a..b3635e3d509 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -83,7 +83,7 @@ module.exports = { * @returns {string} the numeric string with a decimal point in the proper place */ function addDecimalPointToNumber(stringNumber) { - return `${stringNumber.slice(0, 1)}.${stringNumber.slice(1)}`; + return `${stringNumber[0]}.${stringNumber.slice(1)}`; } /** @@ -92,7 +92,12 @@ module.exports = { * @returns {string} the stripped string */ function removeLeadingZeros(numberAsString) { - return numberAsString.replace(/^0*/u, ""); + for (let i = 0; i < numberAsString.length; i++) { + if (numberAsString[i] !== "0") { + return numberAsString.slice(i); + } + } + return numberAsString; } /** @@ -101,7 +106,12 @@ module.exports = { * @returns {string} the stripped string */ function removeTrailingZeros(numberAsString) { - return numberAsString.replace(/0*$/u, ""); + for (let i = numberAsString.length - 1; i >= 0; i--) { + if (numberAsString[i] !== "0") { + return numberAsString.slice(0, i + 1); + } + } + return numberAsString; } /** @@ -128,7 +138,7 @@ module.exports = { const trimmedFloat = removeLeadingZeros(stringFloat); if (trimmedFloat.startsWith(".")) { - const decimalDigits = trimmedFloat.split(".").pop(); + const decimalDigits = trimmedFloat.slice(1); const significantDigits = removeLeadingZeros(decimalDigits); return { @@ -144,7 +154,6 @@ module.exports = { }; } - /** * Converts a base ten number to proper scientific notation * @param {string} stringNumber the string representation of the base ten number to be converted @@ -160,7 +169,6 @@ module.exports = { : normalizedNumber.magnitude; return `${normalizedCoefficient}e${magnitude}`; - } /** diff --git a/lib/rules/no-misleading-character-class.js b/lib/rules/no-misleading-character-class.js index 47ee84ec857..20591df2cc9 100644 --- a/lib/rules/no-misleading-character-class.js +++ b/lib/rules/no-misleading-character-class.js @@ -13,30 +13,40 @@ const { isValidWithUnicodeFlag } = require("./utils/regular-expressions"); // Helpers //------------------------------------------------------------------------------ +/** + * @typedef {import('@eslint-community/regexpp').AST.Character} Character + * @typedef {import('@eslint-community/regexpp').AST.CharacterClassElement} CharacterClassElement + */ + /** * Iterate character sequences of a given nodes. * * CharacterClassRange syntax can steal a part of character sequence, * so this function reverts CharacterClassRange syntax and restore the sequence. - * @param {regexpp.AST.CharacterClassElement[]} nodes The node list to iterate character sequences. - * @returns {IterableIterator} The list of character sequences. + * @param {CharacterClassElement[]} nodes The node list to iterate character sequences. + * @returns {IterableIterator} The list of character sequences. */ function *iterateCharacterSequence(nodes) { + + /** @type {Character[]} */ let seq = []; for (const node of nodes) { switch (node.type) { case "Character": - seq.push(node.value); + seq.push(node); break; case "CharacterClassRange": - seq.push(node.min.value); + seq.push(node.min); yield seq; - seq = [node.max.value]; + seq = [node.max]; break; case "CharacterSet": + case "CharacterClass": // [[]] nesting character class + case "ClassStringDisjunction": // \q{...} + case "ExpressionCharacterClass": // [A--B] if (seq.length > 0) { yield seq; seq = []; @@ -52,32 +62,74 @@ function *iterateCharacterSequence(nodes) { } } + +/** + * Checks whether the given character node is a Unicode code point escape or not. + * @param {Character} char the character node to check. + * @returns {boolean} `true` if the character node is a Unicode code point escape. + */ +function isUnicodeCodePointEscape(char) { + return /^\\u\{[\da-f]+\}$/iu.test(char.raw); +} + +/** + * Each function returns `true` if it detects that kind of problem. + * @type {Record boolean>} + */ const hasCharacterSequence = { surrogatePairWithoutUFlag(chars) { - return chars.some((c, i) => i !== 0 && isSurrogatePair(chars[i - 1], c)); + return chars.some((c, i) => { + if (i === 0) { + return false; + } + const c1 = chars[i - 1]; + + return ( + isSurrogatePair(c1.value, c.value) && + !isUnicodeCodePointEscape(c1) && + !isUnicodeCodePointEscape(c) + ); + }); + }, + + surrogatePair(chars) { + return chars.some((c, i) => { + if (i === 0) { + return false; + } + const c1 = chars[i - 1]; + + return ( + isSurrogatePair(c1.value, c.value) && + ( + isUnicodeCodePointEscape(c1) || + isUnicodeCodePointEscape(c) + ) + ); + }); }, combiningClass(chars) { return chars.some((c, i) => ( i !== 0 && - isCombiningCharacter(c) && - !isCombiningCharacter(chars[i - 1]) + isCombiningCharacter(c.value) && + !isCombiningCharacter(chars[i - 1].value) )); }, emojiModifier(chars) { return chars.some((c, i) => ( i !== 0 && - isEmojiModifier(c) && - !isEmojiModifier(chars[i - 1]) + isEmojiModifier(c.value) && + !isEmojiModifier(chars[i - 1].value) )); }, regionalIndicatorSymbol(chars) { return chars.some((c, i) => ( i !== 0 && - isRegionalIndicatorSymbol(c) && - isRegionalIndicatorSymbol(chars[i - 1]) + isRegionalIndicatorSymbol(c.value) && + isRegionalIndicatorSymbol(chars[i - 1].value) )); }, @@ -87,9 +139,9 @@ const hasCharacterSequence = { return chars.some((c, i) => ( i !== 0 && i !== lastIndex && - c === 0x200d && - chars[i - 1] !== 0x200d && - chars[i + 1] !== 0x200d + c.value === 0x200d && + chars[i - 1].value !== 0x200d && + chars[i + 1].value !== 0x200d )); } }; @@ -117,6 +169,7 @@ module.exports = { messages: { surrogatePairWithoutUFlag: "Unexpected surrogate pair in character class. Use 'u' flag.", + surrogatePair: "Unexpected surrogate pair in character class.", combiningClass: "Unexpected combined character in character class.", emojiModifier: "Unexpected modified Emoji in character class.", regionalIndicatorSymbol: "Unexpected national flag in character class.", @@ -144,7 +197,10 @@ module.exports = { pattern, 0, pattern.length, - flags.includes("u") + { + unicode: flags.includes("u"), + unicodeSets: flags.includes("v") + } ); } catch { diff --git a/lib/rules/no-mixed-operators.js b/lib/rules/no-mixed-operators.js index 724abe09466..6b6f7364a01 100644 --- a/lib/rules/no-mixed-operators.js +++ b/lib/rules/no-mixed-operators.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to disallow mixed binary operators. * @author Toru Nagashima + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -85,6 +86,8 @@ function getChildNode(node) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "suggestion", docs: { diff --git a/lib/rules/no-mixed-spaces-and-tabs.js b/lib/rules/no-mixed-spaces-and-tabs.js index a18e4f30d0a..7698b5da7fa 100644 --- a/lib/rules/no-mixed-spaces-and-tabs.js +++ b/lib/rules/no-mixed-spaces-and-tabs.js @@ -1,6 +1,7 @@ /** * @fileoverview Disallow mixed spaces and tabs for indentation * @author Jary Niebur + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -11,6 +12,8 @@ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/no-multi-spaces.js b/lib/rules/no-multi-spaces.js index 62074e657ae..bc90ee5b5b0 100644 --- a/lib/rules/no-multi-spaces.js +++ b/lib/rules/no-multi-spaces.js @@ -1,6 +1,7 @@ /** * @fileoverview Disallow use of multiple spaces. * @author Nicholas C. Zakas + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -14,6 +15,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/no-multiple-empty-lines.js b/lib/rules/no-multiple-empty-lines.js index 2c0fbf2c6ab..5d038ff05b2 100644 --- a/lib/rules/no-multiple-empty-lines.js +++ b/lib/rules/no-multiple-empty-lines.js @@ -2,6 +2,7 @@ * @fileoverview Disallows multiple blank lines. * implementation adapted from the no-trailing-spaces rule. * @author Greg Cochard + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -12,6 +13,8 @@ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/no-new-object.js b/lib/rules/no-new-object.js index 08a482be715..06275f47125 100644 --- a/lib/rules/no-new-object.js +++ b/lib/rules/no-new-object.js @@ -1,6 +1,7 @@ /** * @fileoverview A rule to disallow calls to the Object constructor * @author Matt DuVall + * @deprecated in ESLint v8.50.0 */ "use strict"; @@ -26,6 +27,12 @@ module.exports = { url: "https://eslint.org/docs/latest/rules/no-new-object" }, + deprecated: true, + + replacedBy: [ + "no-object-constructor" + ], + schema: [], messages: { diff --git a/lib/rules/no-new-wrappers.js b/lib/rules/no-new-wrappers.js index 9a12e1a3b5d..5050a98a044 100644 --- a/lib/rules/no-new-wrappers.js +++ b/lib/rules/no-new-wrappers.js @@ -5,6 +5,12 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const { getVariableByName } = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -28,18 +34,24 @@ module.exports = { }, create(context) { + const { sourceCode } = context; return { NewExpression(node) { const wrapperObjects = ["String", "Number", "Boolean"]; - - if (wrapperObjects.includes(node.callee.name)) { - context.report({ - node, - messageId: "noConstructor", - data: { fn: node.callee.name } - }); + const { name } = node.callee; + + if (wrapperObjects.includes(name)) { + const variable = getVariableByName(sourceCode.getScope(node), name); + + if (variable && variable.identifiers.length === 0) { + context.report({ + node, + messageId: "noConstructor", + data: { fn: name } + }); + } } } }; diff --git a/lib/rules/no-object-constructor.js b/lib/rules/no-object-constructor.js new file mode 100644 index 00000000000..e3ac2095734 --- /dev/null +++ b/lib/rules/no-object-constructor.js @@ -0,0 +1,216 @@ +/** + * @fileoverview Rule to disallow calls to the `Object` constructor without an argument + * @author Francesco Trotta + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const { getVariableByName, isArrowToken, isClosingBraceToken, isClosingParenToken } = require("./utils/ast-utils"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const BREAK_OR_CONTINUE = new Set(["BreakStatement", "ContinueStatement"]); + +// Declaration types that must contain a string Literal node at the end. +const DECLARATIONS = new Set(["ExportAllDeclaration", "ExportNamedDeclaration", "ImportDeclaration"]); + +const IDENTIFIER_OR_KEYWORD = new Set(["Identifier", "Keyword"]); + +// Keywords that can immediately precede an ExpressionStatement node, mapped to the their node types. +const NODE_TYPES_BY_KEYWORD = { + __proto__: null, + break: "BreakStatement", + continue: "ContinueStatement", + debugger: "DebuggerStatement", + do: "DoWhileStatement", + else: "IfStatement", + return: "ReturnStatement", + yield: "YieldExpression" +}; + +/* + * Before an opening parenthesis, postfix `++` and `--` always trigger ASI; + * the tokens `:`, `;`, `{` and `=>` don't expect a semicolon, as that would count as an empty statement. + */ +const PUNCTUATORS = new Set([":", ";", "{", "=>", "++", "--"]); + +/* + * Statements that can contain an `ExpressionStatement` after a closing parenthesis. + * DoWhileStatement is an exception in that it always triggers ASI after the closing parenthesis. + */ +const STATEMENTS = new Set([ + "DoWhileStatement", + "ForInStatement", + "ForOfStatement", + "ForStatement", + "IfStatement", + "WhileStatement", + "WithStatement" +]); + +/** + * Tests if a node appears at the beginning of an ancestor ExpressionStatement node. + * @param {ASTNode} node The node to check. + * @returns {boolean} Whether the node appears at the beginning of an ancestor ExpressionStatement node. + */ +function isStartOfExpressionStatement(node) { + const start = node.range[0]; + let ancestor = node; + + while ((ancestor = ancestor.parent) && ancestor.range[0] === start) { + if (ancestor.type === "ExpressionStatement") { + return true; + } + } + return false; +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +/** @type {import('../shared/types').Rule} */ +module.exports = { + meta: { + type: "suggestion", + + docs: { + description: "Disallow calls to the `Object` constructor without an argument", + recommended: false, + url: "https://eslint.org/docs/latest/rules/no-object-constructor" + }, + + hasSuggestions: true, + + schema: [], + + messages: { + preferLiteral: "The object literal notation {} is preferable.", + useLiteral: "Replace with '{{replacement}}'.", + useLiteralAfterSemicolon: "Replace with '{{replacement}}', add preceding semicolon." + } + }, + + create(context) { + + const sourceCode = context.sourceCode; + + /** + * Determines whether or not an object literal that replaces a specified node needs to be enclosed in parentheses. + * @param {ASTNode} node The node to be replaced. + * @returns {boolean} Whether or not parentheses around the object literal are required. + */ + function needsParentheses(node) { + if (isStartOfExpressionStatement(node)) { + return true; + } + + const prevToken = sourceCode.getTokenBefore(node); + + if (prevToken && isArrowToken(prevToken)) { + return true; + } + + return false; + } + + /** + * Determines whether a parenthesized object literal that replaces a specified node needs to be preceded by a semicolon. + * @param {ASTNode} node The node to be replaced. This node should be at the start of an `ExpressionStatement` or at the start of the body of an `ArrowFunctionExpression`. + * @returns {boolean} Whether a semicolon is required before the parenthesized object literal. + */ + function needsSemicolon(node) { + const prevToken = sourceCode.getTokenBefore(node); + + if (!prevToken || prevToken.type === "Punctuator" && PUNCTUATORS.has(prevToken.value)) { + return false; + } + + const prevNode = sourceCode.getNodeByRangeIndex(prevToken.range[0]); + + if (isClosingParenToken(prevToken)) { + return !STATEMENTS.has(prevNode.type); + } + + if (isClosingBraceToken(prevToken)) { + return ( + prevNode.type === "BlockStatement" && prevNode.parent.type === "FunctionExpression" || + prevNode.type === "ClassBody" && prevNode.parent.type === "ClassExpression" || + prevNode.type === "ObjectExpression" + ); + } + + if (IDENTIFIER_OR_KEYWORD.has(prevToken.type)) { + if (BREAK_OR_CONTINUE.has(prevNode.parent.type)) { + return false; + } + + const keyword = prevToken.value; + const nodeType = NODE_TYPES_BY_KEYWORD[keyword]; + + return prevNode.type !== nodeType; + } + + if (prevToken.type === "String") { + return !DECLARATIONS.has(prevNode.parent.type); + } + + return true; + } + + /** + * Reports on nodes where the `Object` constructor is called without arguments. + * @param {ASTNode} node The node to evaluate. + * @returns {void} + */ + function check(node) { + if (node.callee.type !== "Identifier" || node.callee.name !== "Object" || node.arguments.length) { + return; + } + + const variable = getVariableByName(sourceCode.getScope(node), "Object"); + + if (variable && variable.identifiers.length === 0) { + let replacement; + let fixText; + let messageId = "useLiteral"; + + if (needsParentheses(node)) { + replacement = "({})"; + if (needsSemicolon(node)) { + fixText = ";({})"; + messageId = "useLiteralAfterSemicolon"; + } else { + fixText = "({})"; + } + } else { + replacement = fixText = "{}"; + } + + context.report({ + node, + messageId: "preferLiteral", + suggest: [ + { + messageId, + data: { replacement }, + fix: fixer => fixer.replaceText(node, fixText) + } + ] + }); + } + } + + return { + CallExpression: check, + NewExpression: check + }; + + } +}; diff --git a/lib/rules/no-promise-executor-return.js b/lib/rules/no-promise-executor-return.js index d46a730e474..e6ed7a22efc 100644 --- a/lib/rules/no-promise-executor-return.js +++ b/lib/rules/no-promise-executor-return.js @@ -10,6 +10,7 @@ //------------------------------------------------------------------------------ const { findVariable } = require("@eslint-community/eslint-utils"); +const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ // Helpers @@ -59,6 +60,78 @@ function isPromiseExecutor(node, scope) { isGlobalReference(parent.callee, getOuterScope(scope)); } +/** + * Checks if the given node is a void expression. + * @param {ASTNode} node The node to check. + * @returns {boolean} - `true` if the node is a void expression + */ +function expressionIsVoid(node) { + return node.type === "UnaryExpression" && node.operator === "void"; +} + +/** + * Fixes the linting error by prepending "void " to the given node + * @param {Object} sourceCode context given by context.sourceCode + * @param {ASTNode} node The node to fix. + * @param {Object} fixer The fixer object provided by ESLint. + * @returns {Array} - An array of fix objects to apply to the node. + */ +function voidPrependFixer(sourceCode, node, fixer) { + + const requiresParens = + + // prepending `void ` will fail if the node has a lower precedence than void + astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression", operator: "void" }) && + + // check if there are parentheses around the node to avoid redundant parentheses + !astUtils.isParenthesised(sourceCode, node); + + // avoid parentheses issues + const returnOrArrowToken = sourceCode.getTokenBefore( + node, + node.parent.type === "ArrowFunctionExpression" + ? astUtils.isArrowToken + + // isReturnToken + : token => token.type === "Keyword" && token.value === "return" + ); + + const firstToken = sourceCode.getTokenAfter(returnOrArrowToken); + + const prependSpace = + + // is return token, as => allows void to be adjacent + returnOrArrowToken.value === "return" && + + // If two tokens (return and "(") are adjacent + returnOrArrowToken.range[1] === firstToken.range[0]; + + return [ + fixer.insertTextBefore(firstToken, `${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`), + fixer.insertTextAfter(node, requiresParens ? ")" : "") + ]; +} + +/** + * Fixes the linting error by `wrapping {}` around the given node's body. + * @param {Object} sourceCode context given by context.sourceCode + * @param {ASTNode} node The node to fix. + * @param {Object} fixer The fixer object provided by ESLint. + * @returns {Array} - An array of fix objects to apply to the node. + */ +function curlyWrapFixer(sourceCode, node, fixer) { + + // https://github.com/eslint/eslint/pull/17282#issuecomment-1592795923 + const arrowToken = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken); + const firstToken = sourceCode.getTokenAfter(arrowToken); + const lastToken = sourceCode.getLastToken(node); + + return [ + fixer.insertTextBefore(firstToken, "{"), + fixer.insertTextAfter(lastToken, "}") + ]; +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -74,10 +147,27 @@ module.exports = { url: "https://eslint.org/docs/latest/rules/no-promise-executor-return" }, - schema: [], + hasSuggestions: true, + + schema: [{ + type: "object", + properties: { + allowVoid: { + type: "boolean", + default: false + } + }, + additionalProperties: false + }], messages: { - returnsValue: "Return values from promise executor functions cannot be read." + returnsValue: "Return values from promise executor functions cannot be read.", + + // arrow and function suggestions + prependVoid: "Prepend `void` to the expression.", + + // only arrow suggestions + wrapBraces: "Wrap the expression in `{}`." } }, @@ -85,26 +175,52 @@ module.exports = { let funcInfo = null; const sourceCode = context.sourceCode; - - /** - * Reports the given node. - * @param {ASTNode} node Node to report. - * @returns {void} - */ - function report(node) { - context.report({ node, messageId: "returnsValue" }); - } + const { + allowVoid = false + } = context.options[0] || {}; return { onCodePathStart(_, node) { funcInfo = { upper: funcInfo, - shouldCheck: functionTypesToCheck.has(node.type) && isPromiseExecutor(node, sourceCode.getScope(node)) + shouldCheck: + functionTypesToCheck.has(node.type) && + isPromiseExecutor(node, sourceCode.getScope(node)) }; - if (funcInfo.shouldCheck && node.type === "ArrowFunctionExpression" && node.expression) { - report(node.body); + if (// Is a Promise executor + funcInfo.shouldCheck && + node.type === "ArrowFunctionExpression" && + node.expression && + + // Except void + !(allowVoid && expressionIsVoid(node.body)) + ) { + const suggest = []; + + // prevent useless refactors + if (allowVoid) { + suggest.push({ + messageId: "prependVoid", + fix(fixer) { + return voidPrependFixer(sourceCode, node.body, fixer); + } + }); + } + + suggest.push({ + messageId: "wrapBraces", + fix(fixer) { + return curlyWrapFixer(sourceCode, node, fixer); + } + }); + + context.report({ + node: node.body, + messageId: "returnsValue", + suggest + }); } }, @@ -113,9 +229,31 @@ module.exports = { }, ReturnStatement(node) { - if (funcInfo.shouldCheck && node.argument) { - report(node); + if (!(funcInfo.shouldCheck && node.argument)) { + return; } + + // node is `return ` + if (!allowVoid) { + context.report({ node, messageId: "returnsValue" }); + return; + } + + if (expressionIsVoid(node.argument)) { + return; + } + + // allowVoid && !expressionIsVoid + context.report({ + node, + messageId: "returnsValue", + suggest: [{ + messageId: "prependVoid", + fix(fixer) { + return voidPrependFixer(sourceCode, node.argument, fixer); + } + }] + }); } }; } diff --git a/lib/rules/no-prototype-builtins.js b/lib/rules/no-prototype-builtins.js index a7a57bc119e..b61e585291a 100644 --- a/lib/rules/no-prototype-builtins.js +++ b/lib/rules/no-prototype-builtins.js @@ -10,6 +10,37 @@ const astUtils = require("./utils/ast-utils"); +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Returns true if the node or any of the objects + * to the left of it in the member/call chain is optional. + * + * e.g. `a?.b`, `a?.b.c`, `a?.()`, `a()?.()` + * @param {ASTNode} node The expression to check + * @returns {boolean} `true` if there is a short-circuiting optional `?.` + * in the same option chain to the left of this call or member expression, + * or the node itself is an optional call or member `?.`. + */ +function isAfterOptional(node) { + let leftNode; + + if (node.type === "MemberExpression") { + leftNode = node.object; + } else if (node.type === "CallExpression") { + leftNode = node.callee; + } else { + return false; + } + if (node.optional) { + return true; + } + return isAfterOptional(leftNode); +} + + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -25,10 +56,13 @@ module.exports = { url: "https://eslint.org/docs/latest/rules/no-prototype-builtins" }, + hasSuggestions: true, + schema: [], messages: { - prototypeBuildIn: "Do not access Object.prototype method '{{prop}}' from target object." + prototypeBuildIn: "Do not access Object.prototype method '{{prop}}' from target object.", + callObjectPrototype: "Call Object.prototype.{{prop}} explicitly." } }, @@ -59,7 +93,61 @@ module.exports = { messageId: "prototypeBuildIn", loc: callee.property.loc, data: { prop: propName }, - node + node, + suggest: [ + { + messageId: "callObjectPrototype", + data: { prop: propName }, + fix(fixer) { + const sourceCode = context.sourceCode; + + /* + * A call after an optional chain (e.g. a?.b.hasOwnProperty(c)) + * must be fixed manually because the call can be short-circuited + */ + if (isAfterOptional(node)) { + return null; + } + + /* + * A call on a ChainExpression (e.g. (a?.hasOwnProperty)(c)) will trigger + * no-unsafe-optional-chaining which should be fixed before this suggestion + */ + if (node.callee.type === "ChainExpression") { + return null; + } + + const objectVariable = astUtils.getVariableByName(sourceCode.getScope(node), "Object"); + + /* + * We can't use Object if the global Object was shadowed, + * or Object does not exist in the global scope for some reason + */ + if (!objectVariable || objectVariable.scope.type !== "global" || objectVariable.defs.length > 0) { + return null; + } + + let objectText = sourceCode.getText(callee.object); + + if (astUtils.getPrecedence(callee.object) <= astUtils.getPrecedence({ type: "SequenceExpression" })) { + objectText = `(${objectText})`; + } + + const openParenToken = sourceCode.getTokenAfter( + node.callee, + astUtils.isOpeningParenToken + ); + const isEmptyParameters = node.arguments.length === 0; + const delim = isEmptyParameters ? "" : ", "; + const fixes = [ + fixer.replaceText(callee, `Object.prototype.${propName}.call`), + fixer.insertTextAfter(openParenToken, objectText + delim) + ]; + + return fixes; + } + } + ] }); } } diff --git a/lib/rules/no-regex-spaces.js b/lib/rules/no-regex-spaces.js index e7fae6d4055..cb250107289 100644 --- a/lib/rules/no-regex-spaces.js +++ b/lib/rules/no-regex-spaces.js @@ -77,7 +77,7 @@ module.exports = { let regExpAST; try { - regExpAST = regExpParser.parsePattern(pattern, 0, pattern.length, flags.includes("u")); + regExpAST = regExpParser.parsePattern(pattern, 0, pattern.length, { unicode: flags.includes("u"), unicodeSets: flags.includes("v") }); } catch { // Ignore regular expressions with syntax errors @@ -155,13 +155,28 @@ module.exports = { const regExpVar = astUtils.getVariableByName(scope, "RegExp"); const shadowed = regExpVar && regExpVar.defs.length > 0; const patternNode = node.arguments[0]; - const flagsNode = node.arguments[1]; if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(patternNode) && !shadowed) { const pattern = patternNode.value; const rawPattern = patternNode.raw.slice(1, -1); const rawPatternStartRange = patternNode.range[0] + 1; - const flags = isString(flagsNode) ? flagsNode.value : ""; + let flags; + + if (node.arguments.length < 2) { + + // It has no flags. + flags = ""; + } else { + const flagsNode = node.arguments[1]; + + if (isString(flagsNode)) { + flags = flagsNode.value; + } else { + + // The flags cannot be determined. + return; + } + } checkRegex( node, diff --git a/lib/rules/no-restricted-modules.js b/lib/rules/no-restricted-modules.js index d79bdbe5748..4a9b0a4c036 100644 --- a/lib/rules/no-restricted-modules.js +++ b/lib/rules/no-restricted-modules.js @@ -5,6 +5,12 @@ */ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -116,15 +122,6 @@ module.exports = { return node && node.type === "Literal" && typeof node.value === "string"; } - /** - * Function to check if a node is a static string template literal. - * @param {ASTNode} node The node to check. - * @returns {boolean} If the node is a string template literal. - */ - function isStaticTemplateLiteral(node) { - return node && node.type === "TemplateLiteral" && node.expressions.length === 0; - } - /** * Function to check if a node is a require call. * @param {ASTNode} node The node to check. @@ -144,7 +141,7 @@ module.exports = { return node.value.trim(); } - if (isStaticTemplateLiteral(node)) { + if (astUtils.isStaticTemplateLiteral(node)) { return node.quasis[0].value.cooked.trim(); } diff --git a/lib/rules/no-return-await.js b/lib/rules/no-return-await.js index b5abf14c69b..77abda0cadf 100644 --- a/lib/rules/no-return-await.js +++ b/lib/rules/no-return-await.js @@ -1,6 +1,7 @@ /** * @fileoverview Disallows unnecessary `return await` * @author Jordan Harband + * @deprecated in ESLint v8.46.0 */ "use strict"; @@ -26,6 +27,10 @@ module.exports = { fixable: null, + deprecated: true, + + replacedBy: [], + schema: [ ], diff --git a/lib/rules/no-tabs.js b/lib/rules/no-tabs.js index b33690c2441..8581e19af37 100644 --- a/lib/rules/no-tabs.js +++ b/lib/rules/no-tabs.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to check for tabs inside a file * @author Gyandeep Singh + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -19,6 +20,8 @@ const anyNonWhitespaceRegex = /\S/u; /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/no-this-before-super.js b/lib/rules/no-this-before-super.js index 139bb6649d1..f96d8ace81d 100644 --- a/lib/rules/no-this-before-super.js +++ b/lib/rules/no-this-before-super.js @@ -90,6 +90,21 @@ module.exports = { return Boolean(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends); } + /** + * Determines if every segment in a set has been called. + * @param {Set} segments The segments to search. + * @returns {boolean} True if every segment has been called; false otherwise. + */ + function isEverySegmentCalled(segments) { + for (const segment of segments) { + if (!isCalled(segment)) { + return false; + } + } + + return true; + } + /** * Checks whether or not this is before `super()` is called. * @returns {boolean} `true` if this is before `super()` is called. @@ -97,7 +112,7 @@ module.exports = { function isBeforeCallOfSuper() { return ( isInConstructorOfDerivedClass() && - !funcInfo.codePath.currentSegments.every(isCalled) + !isEverySegmentCalled(funcInfo.currentSegments) ); } @@ -108,11 +123,9 @@ module.exports = { * @returns {void} */ function setInvalid(node) { - const segments = funcInfo.codePath.currentSegments; - - for (let i = 0; i < segments.length; ++i) { - const segment = segments[i]; + const segments = funcInfo.currentSegments; + for (const segment of segments) { if (segment.reachable) { segInfoMap[segment.id].invalidNodes.push(node); } @@ -124,11 +137,9 @@ module.exports = { * @returns {void} */ function setSuperCalled() { - const segments = funcInfo.codePath.currentSegments; - - for (let i = 0; i < segments.length; ++i) { - const segment = segments[i]; + const segments = funcInfo.currentSegments; + for (const segment of segments) { if (segment.reachable) { segInfoMap[segment.id].superCalled = true; } @@ -156,14 +167,16 @@ module.exports = { classNode.superClass && !astUtils.isNullOrUndefined(classNode.superClass) ), - codePath + codePath, + currentSegments: new Set() }; } else { funcInfo = { upper: funcInfo, isConstructor: false, hasExtends: false, - codePath + codePath, + currentSegments: new Set() }; } }, @@ -211,6 +224,8 @@ module.exports = { * @returns {void} */ onCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + if (!isInConstructorOfDerivedClass()) { return; } @@ -225,6 +240,18 @@ module.exports = { }; }, + onUnreachableCodePathSegmentStart(segment) { + funcInfo.currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + + onCodePathSegmentEnd(segment) { + funcInfo.currentSegments.delete(segment); + }, + /** * Update information of the code path segment when a code path was * looped. diff --git a/lib/rules/no-trailing-spaces.js b/lib/rules/no-trailing-spaces.js index 1674de54207..eede46c8634 100644 --- a/lib/rules/no-trailing-spaces.js +++ b/lib/rules/no-trailing-spaces.js @@ -1,6 +1,7 @@ /** * @fileoverview Disallow trailing spaces at the end of lines. * @author Nodeca Team + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -17,6 +18,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/no-unreachable-loop.js b/lib/rules/no-unreachable-loop.js index 1df764e17d8..577d39ac7c7 100644 --- a/lib/rules/no-unreachable-loop.js +++ b/lib/rules/no-unreachable-loop.js @@ -11,6 +11,22 @@ const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"]; +/** + * 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; +} + /** * Determines whether the given node is the first node in the code path to which a loop statement * 'loops' for the next iteration. @@ -90,29 +106,36 @@ module.exports = { loopsByTargetSegments = new Map(), loopsToReport = new Set(); - let currentCodePath = null; + const codePathSegments = []; + let currentCodePathSegments = new Set(); return { - onCodePathStart(codePath) { - currentCodePath = codePath; + + onCodePathStart() { + codePathSegments.push(currentCodePathSegments); + currentCodePathSegments = new Set(); }, onCodePathEnd() { - currentCodePath = currentCodePath.upper; + currentCodePathSegments = codePathSegments.pop(); }, - [loopSelector](node) { + onUnreachableCodePathSegmentStart(segment) { + currentCodePathSegments.add(segment); + }, - /** - * Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise. - * For unreachable segments, the code path analysis does not raise events required for this implementation. - */ - if (currentCodePath.currentSegments.some(segment => segment.reachable)) { - loopsToReport.add(node); - } + onUnreachableCodePathSegmentEnd(segment) { + currentCodePathSegments.delete(segment); + }, + + onCodePathSegmentEnd(segment) { + currentCodePathSegments.delete(segment); }, onCodePathSegmentStart(segment, node) { + + currentCodePathSegments.add(segment); + if (isLoopingTarget(node)) { const loop = node.parent; @@ -140,6 +163,18 @@ module.exports = { } }, + [loopSelector](node) { + + /** + * Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise. + * For unreachable segments, the code path analysis does not raise events required for this implementation. + */ + if (isAnySegmentReachable(currentCodePathSegments)) { + loopsToReport.add(node); + } + }, + + "Program:exit"() { loopsToReport.forEach( node => context.report({ node, messageId: "invalid" }) diff --git a/lib/rules/no-unreachable.js b/lib/rules/no-unreachable.js index 6216a73a235..0cf750e4251 100644 --- a/lib/rules/no-unreachable.js +++ b/lib/rules/no-unreachable.js @@ -24,12 +24,19 @@ function isInitialized(node) { } /** - * Checks whether or not a given code path segment is unreachable. - * @param {CodePathSegment} segment A CodePathSegment to check. - * @returns {boolean} `true` if the segment is unreachable. + * Checks all segments in a set and returns true if all are unreachable. + * @param {Set} segments The segments to check. + * @returns {boolean} True if all segments are unreachable; false otherwise. */ -function isUnreachable(segment) { - return !segment.reachable; +function areAllSegmentsUnreachable(segments) { + + for (const segment of segments) { + if (segment.reachable) { + return false; + } + } + + return true; } /** @@ -124,7 +131,6 @@ module.exports = { }, create(context) { - let currentCodePath = null; /** @type {ConstructorInfo | null} */ let constructorInfo = null; @@ -132,6 +138,12 @@ module.exports = { /** @type {ConsecutiveRange} */ const range = new ConsecutiveRange(context.sourceCode); + /** @type {Array>} */ + const codePathSegments = []; + + /** @type {Set} */ + let currentCodePathSegments = new Set(); + /** * Reports a given node if it's unreachable. * @param {ASTNode} node A statement node to report. @@ -140,7 +152,7 @@ module.exports = { function reportIfUnreachable(node) { let nextNode = null; - if (node && (node.type === "PropertyDefinition" || currentCodePath.currentSegments.every(isUnreachable))) { + if (node && (node.type === "PropertyDefinition" || areAllSegmentsUnreachable(currentCodePathSegments))) { // Store this statement to distinguish consecutive statements. if (range.isEmpty) { @@ -181,12 +193,29 @@ module.exports = { return { // Manages the current code path. - onCodePathStart(codePath) { - currentCodePath = codePath; + onCodePathStart() { + codePathSegments.push(currentCodePathSegments); + currentCodePathSegments = new Set(); }, onCodePathEnd() { - currentCodePath = currentCodePath.upper; + currentCodePathSegments = codePathSegments.pop(); + }, + + onUnreachableCodePathSegmentStart(segment) { + currentCodePathSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + currentCodePathSegments.delete(segment); + }, + + onCodePathSegmentEnd(segment) { + currentCodePathSegments.delete(segment); + }, + + onCodePathSegmentStart(segment) { + currentCodePathSegments.add(segment); }, // Registers for all statement nodes (excludes FunctionDeclaration). diff --git a/lib/rules/no-unused-expressions.js b/lib/rules/no-unused-expressions.js index 8337c19487e..46bb7baac22 100644 --- a/lib/rules/no-unused-expressions.js +++ b/lib/rules/no-unused-expressions.js @@ -4,6 +4,8 @@ */ "use strict"; +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -112,8 +114,6 @@ module.exports = { * @returns {boolean} whether the given node is considered a directive in its current position */ function isDirective(node) { - const parent = node.parent, - grandparent = parent.parent; /** * https://tc39.es/ecma262/#directive-prologue @@ -121,9 +121,7 @@ module.exports = { * Only `FunctionBody`, `ScriptBody` and `ModuleBody` can have directive prologue. * Class static blocks do not have directive prologue. */ - return (parent.type === "Program" || parent.type === "BlockStatement" && - (/Function/u.test(grandparent.type))) && - directives(parent).includes(node); + return astUtils.isTopLevelExpressionStatement(node) && directives(node.parent).includes(node); } /** diff --git a/lib/rules/no-unused-labels.js b/lib/rules/no-unused-labels.js index 9aa1067c681..be06b324c49 100644 --- a/lib/rules/no-unused-labels.js +++ b/lib/rules/no-unused-labels.js @@ -5,6 +5,12 @@ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -46,6 +52,45 @@ module.exports = { }; } + /** + * Checks if a `LabeledStatement` node is fixable. + * For a node to be fixable, there must be no comments between the label and the body. + * Furthermore, is must be possible to remove the label without turning the body statement into a + * directive after other fixes are applied. + * @param {ASTNode} node The node to evaluate. + * @returns {boolean} Whether or not the node is fixable. + */ + function isFixable(node) { + + /* + * Only perform a fix if there are no comments between the label and the body. This will be the case + * when there is exactly one token/comment (the ":") between the label and the body. + */ + if (sourceCode.getTokenAfter(node.label, { includeComments: true }) !== + sourceCode.getTokenBefore(node.body, { includeComments: true })) { + return false; + } + + // Looking for the node's deepest ancestor which is not a `LabeledStatement`. + let ancestor = node.parent; + + while (ancestor.type === "LabeledStatement") { + ancestor = ancestor.parent; + } + + if (ancestor.type === "Program" || + (ancestor.type === "BlockStatement" && astUtils.isFunction(ancestor.parent))) { + const { body } = node; + + if (body.type === "ExpressionStatement" && + ((body.expression.type === "Literal" && typeof body.expression.value === "string") || + astUtils.isStaticTemplateLiteral(body.expression))) { + return false; // potential directive + } + } + return true; + } + /** * Removes the top of the stack. * At the same time, this reports the label if it's never used. @@ -58,19 +103,7 @@ module.exports = { node: node.label, messageId: "unused", data: node.label, - fix(fixer) { - - /* - * Only perform a fix if there are no comments between the label and the body. This will be the case - * when there is exactly one token/comment (the ":") between the label and the body. - */ - if (sourceCode.getTokenAfter(node.label, { includeComments: true }) === - sourceCode.getTokenBefore(node.body, { includeComments: true })) { - return fixer.removeRange([node.range[0], node.body.range[0]]); - } - - return null; - } + fix: isFixable(node) ? fixer => fixer.removeRange([node.range[0], node.body.range[0]]) : null }); } diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js index be8af43dd41..f29e678d6fd 100644 --- a/lib/rules/no-unused-vars.js +++ b/lib/rules/no-unused-vars.js @@ -466,7 +466,8 @@ module.exports = { ( parent.type === "AssignmentExpression" && parent.left === id && - isUnusedExpression(parent) + isUnusedExpression(parent) && + !astUtils.isLogicalAssignmentOperator(parent.operator) ) || ( parent.type === "UpdateExpression" && diff --git a/lib/rules/no-useless-backreference.js b/lib/rules/no-useless-backreference.js index c99ac411495..7ca43c8b260 100644 --- a/lib/rules/no-useless-backreference.js +++ b/lib/rules/no-useless-backreference.js @@ -95,7 +95,7 @@ module.exports = { let regExpAST; try { - regExpAST = parser.parsePattern(pattern, 0, pattern.length, flags.includes("u")); + regExpAST = parser.parsePattern(pattern, 0, pattern.length, { unicode: flags.includes("u"), unicodeSets: flags.includes("v") }); } catch { // Ignore regular expressions with syntax errors diff --git a/lib/rules/no-useless-escape.js b/lib/rules/no-useless-escape.js index 8304d915f9e..0e0f6f09f2c 100644 --- a/lib/rules/no-useless-escape.js +++ b/lib/rules/no-useless-escape.js @@ -6,7 +6,12 @@ "use strict"; const astUtils = require("./utils/ast-utils"); +const { RegExpParser, visitRegExpAST } = require("@eslint-community/regexpp"); +/** + * @typedef {import('@eslint-community/regexpp').AST.CharacterClass} CharacterClass + * @typedef {import('@eslint-community/regexpp').AST.ExpressionCharacterClass} ExpressionCharacterClass + */ //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -28,55 +33,17 @@ const VALID_STRING_ESCAPES = union(new Set("\\nrvtbfux"), astUtils.LINEBREAKS); const REGEX_GENERAL_ESCAPES = new Set("\\bcdDfnpPrsStvwWxu0123456789]"); const REGEX_NON_CHARCLASS_ESCAPES = union(REGEX_GENERAL_ESCAPES, new Set("^/.$*+?[{}|()Bk")); -/** - * Parses a regular expression into a list of characters with character class info. - * @param {string} regExpText The raw text used to create the regular expression - * @returns {Object[]} A list of characters, each with info on escaping and whether they're in a character class. - * @example - * - * parseRegExp("a\\b[cd-]"); - * - * // returns: - * [ - * { text: "a", index: 0, escaped: false, inCharClass: false, startsCharClass: false, endsCharClass: false }, - * { text: "b", index: 2, escaped: true, inCharClass: false, startsCharClass: false, endsCharClass: false }, - * { text: "c", index: 4, escaped: false, inCharClass: true, startsCharClass: true, endsCharClass: false }, - * { text: "d", index: 5, escaped: false, inCharClass: true, startsCharClass: false, endsCharClass: false }, - * { text: "-", index: 6, escaped: false, inCharClass: true, startsCharClass: false, endsCharClass: false } - * ]; - * +/* + * Set of characters that require escaping in character classes in `unicodeSets` mode. + * ( ) [ ] { } / - \ | are ClassSetSyntaxCharacter */ -function parseRegExp(regExpText) { - const charList = []; +const REGEX_CLASSSET_CHARACTER_ESCAPES = union(REGEX_GENERAL_ESCAPES, new Set("q/[{}|()-")); - regExpText.split("").reduce((state, char, index) => { - if (!state.escapeNextChar) { - if (char === "\\") { - return Object.assign(state, { escapeNextChar: true }); - } - if (char === "[" && !state.inCharClass) { - return Object.assign(state, { inCharClass: true, startingCharClass: true }); - } - if (char === "]" && state.inCharClass) { - if (charList.length && charList[charList.length - 1].inCharClass) { - charList[charList.length - 1].endsCharClass = true; - } - return Object.assign(state, { inCharClass: false, startingCharClass: false }); - } - } - charList.push({ - text: char, - index, - escaped: state.escapeNextChar, - inCharClass: state.inCharClass, - startsCharClass: state.startingCharClass, - endsCharClass: false - }); - return Object.assign(state, { escapeNextChar: false, startingCharClass: false }); - }, { escapeNextChar: false, inCharClass: false, startingCharClass: false }); - - return charList; -} +/* + * A single character set of ClassSetReservedDoublePunctuator. + * && !! ## $$ %% ** ++ ,, .. :: ;; << == >> ?? @@ ^^ `` ~~ are ClassSetReservedDoublePunctuator + */ +const REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR = new Set("!#$%&*+,.:;<=>?@^`~"); /** @type {import('../shared/types').Rule} */ module.exports = { @@ -94,6 +61,7 @@ module.exports = { messages: { unnecessaryEscape: "Unnecessary escape character: \\{{character}}.", removeEscape: "Remove the `\\`. This maintains the current functionality.", + removeEscapeDoNotKeepSemantics: "Remove the `\\` if it was inserted by mistake.", escapeBackslash: "Replace the `\\` with `\\\\` to include the actual backslash character." }, @@ -102,15 +70,17 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; + const parser = new RegExpParser(); /** * Reports a node * @param {ASTNode} node The node to report * @param {number} startOffset The backslash's offset from the start of the node * @param {string} character The uselessly escaped character (not including the backslash) + * @param {boolean} [disableEscapeBackslashSuggest] `true` if escapeBackslash suggestion should be turned off. * @returns {void} */ - function report(node, startOffset, character) { + function report(node, startOffset, character, disableEscapeBackslashSuggest) { const rangeStart = node.range[0] + startOffset; const range = [rangeStart, rangeStart + 1]; const start = sourceCode.getLocFromIndex(rangeStart); @@ -125,17 +95,24 @@ module.exports = { data: { character }, suggest: [ { - messageId: "removeEscape", + + // Removing unnecessary `\` characters in a directive is not guaranteed to maintain functionality. + messageId: astUtils.isDirective(node.parent) + ? "removeEscapeDoNotKeepSemantics" : "removeEscape", fix(fixer) { return fixer.removeRange(range); } }, - { - messageId: "escapeBackslash", - fix(fixer) { - return fixer.insertTextBeforeRange(range, "\\"); - } - } + ...disableEscapeBackslashSuggest + ? [] + : [ + { + messageId: "escapeBackslash", + fix(fixer) { + return fixer.insertTextBeforeRange(range, "\\"); + } + } + ] ] }); } @@ -178,6 +155,133 @@ module.exports = { } } + /** + * Checks if the escape character in given regexp is unnecessary. + * @private + * @param {ASTNode} node node to validate. + * @returns {void} + */ + function validateRegExp(node) { + const { pattern, flags } = node.regex; + let patternNode; + const unicode = flags.includes("u"); + const unicodeSets = flags.includes("v"); + + try { + patternNode = parser.parsePattern(pattern, 0, pattern.length, { unicode, unicodeSets }); + } catch { + + // Ignore regular expressions with syntax errors + return; + } + + /** @type {(CharacterClass | ExpressionCharacterClass)[]} */ + const characterClassStack = []; + + visitRegExpAST(patternNode, { + onCharacterClassEnter: characterClassNode => characterClassStack.unshift(characterClassNode), + onCharacterClassLeave: () => characterClassStack.shift(), + onExpressionCharacterClassEnter: characterClassNode => characterClassStack.unshift(characterClassNode), + onExpressionCharacterClassLeave: () => characterClassStack.shift(), + onCharacterEnter(characterNode) { + if (!characterNode.raw.startsWith("\\")) { + + // It's not an escaped character. + return; + } + + const escapedChar = characterNode.raw.slice(1); + + if (escapedChar !== String.fromCodePoint(characterNode.value)) { + + // It's a valid escape. + return; + } + let allowedEscapes; + + if (characterClassStack.length) { + allowedEscapes = unicodeSets ? REGEX_CLASSSET_CHARACTER_ESCAPES : REGEX_GENERAL_ESCAPES; + } else { + allowedEscapes = REGEX_NON_CHARCLASS_ESCAPES; + } + if (allowedEscapes.has(escapedChar)) { + return; + } + + const reportedIndex = characterNode.start + 1; + let disableEscapeBackslashSuggest = false; + + if (characterClassStack.length) { + const characterClassNode = characterClassStack[0]; + + if (escapedChar === "^") { + + /* + * The '^' character is also a special case; it must always be escaped outside of character classes, but + * it only needs to be escaped in character classes if it's at the beginning of the character class. To + * account for this, consider it to be a valid escape character outside of character classes, and filter + * out '^' characters that appear at the start of a character class. + */ + if (characterClassNode.start + 1 === characterNode.start) { + + return; + } + } + if (!unicodeSets) { + if (escapedChar === "-") { + + /* + * The '-' character is a special case, because it's only valid to escape it if it's in a character + * class, and is not at either edge of the character class. To account for this, don't consider '-' + * characters to be valid in general, and filter out '-' characters that appear in the middle of a + * character class. + */ + if (characterClassNode.start + 1 !== characterNode.start && characterNode.end !== characterClassNode.end - 1) { + + return; + } + } + } else { // unicodeSets mode + if (REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR.has(escapedChar)) { + + // Escaping is valid if it is a ClassSetReservedDoublePunctuator. + if (pattern[characterNode.end] === escapedChar) { + return; + } + if (pattern[characterNode.start - 1] === escapedChar) { + if (escapedChar !== "^") { + return; + } + + // If the previous character is a `negate` caret(`^`), escape to caret is unnecessary. + + if (!characterClassNode.negate) { + return; + } + const negateCaretIndex = characterClassNode.start + 1; + + if (negateCaretIndex < characterNode.start - 1) { + return; + } + } + } + + if (characterNode.parent.type === "ClassIntersection" || characterNode.parent.type === "ClassSubtraction") { + disableEscapeBackslashSuggest = true; + } + } + } + + report( + node, + reportedIndex, + escapedChar, + disableEscapeBackslashSuggest + ); + } + }); + } + /** * Checks if a node has an escape. * @param {ASTNode} node node to check. @@ -216,32 +320,7 @@ module.exports = { validateString(node, match); } } else if (node.regex) { - parseRegExp(node.regex.pattern) - - /* - * The '-' character is a special case, because it's only valid to escape it if it's in a character - * class, and is not at either edge of the character class. To account for this, don't consider '-' - * characters to be valid in general, and filter out '-' characters that appear in the middle of a - * character class. - */ - .filter(charInfo => !(charInfo.text === "-" && charInfo.inCharClass && !charInfo.startsCharClass && !charInfo.endsCharClass)) - - /* - * The '^' character is also a special case; it must always be escaped outside of character classes, but - * it only needs to be escaped in character classes if it's at the beginning of the character class. To - * account for this, consider it to be a valid escape character outside of character classes, and filter - * out '^' characters that appear at the start of a character class. - */ - .filter(charInfo => !(charInfo.text === "^" && charInfo.startsCharClass)) - - // Filter out characters that aren't escaped. - .filter(charInfo => charInfo.escaped) - - // Filter out characters that are valid to escape, based on their position in the regular expression. - .filter(charInfo => !(charInfo.inCharClass ? REGEX_GENERAL_ESCAPES : REGEX_NON_CHARCLASS_ESCAPES).has(charInfo.text)) - - // Report all the remaining characters. - .forEach(charInfo => report(node, charInfo.index, charInfo.text)); + validateRegExp(node); } } diff --git a/lib/rules/no-useless-return.js b/lib/rules/no-useless-return.js index db1ccae9739..81d61051053 100644 --- a/lib/rules/no-useless-return.js +++ b/lib/rules/no-useless-return.js @@ -57,6 +57,22 @@ function isInFinally(node) { return false; } +/** + * 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; +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -82,7 +98,6 @@ module.exports = { create(context) { const segmentInfoMap = new WeakMap(); - const usedUnreachableSegments = new WeakSet(); const sourceCode = context.sourceCode; let scopeInfo = null; @@ -152,24 +167,44 @@ module.exports = { * This behavior would simulate code paths for the case that the return * statement does not exist. * @param {CodePathSegment} segment The segment to get return statements. + * @param {Set} usedUnreachableSegments A set of segments that have already been traversed in this call. * @returns {void} */ - function markReturnStatementsOnSegmentAsUsed(segment) { + function markReturnStatementsOnSegmentAsUsed(segment, usedUnreachableSegments) { if (!segment.reachable) { usedUnreachableSegments.add(segment); segment.allPrevSegments .filter(isReturned) .filter(prevSegment => !usedUnreachableSegments.has(prevSegment)) - .forEach(markReturnStatementsOnSegmentAsUsed); + .forEach(prevSegment => markReturnStatementsOnSegmentAsUsed(prevSegment, usedUnreachableSegments)); return; } const info = segmentInfoMap.get(segment); - for (const node of info.uselessReturns) { + info.uselessReturns = info.uselessReturns.filter(node => { + if (scopeInfo.traversedTryBlockStatements && scopeInfo.traversedTryBlockStatements.length > 0) { + const returnInitialRange = node.range[0]; + const returnFinalRange = node.range[1]; + + const areBlocksInRange = scopeInfo.traversedTryBlockStatements.some(tryBlockStatement => { + const blockInitialRange = tryBlockStatement.range[0]; + const blockFinalRange = tryBlockStatement.range[1]; + + return ( + returnInitialRange >= blockInitialRange && + returnFinalRange <= blockFinalRange + ); + }); + + if (areBlocksInRange) { + return true; + } + } + remove(scopeInfo.uselessReturns, node); - } - info.uselessReturns = []; + return false; + }); } /** @@ -186,9 +221,8 @@ module.exports = { */ function markReturnStatementsOnCurrentSegmentsAsUsed() { scopeInfo - .codePath .currentSegments - .forEach(markReturnStatementsOnSegmentAsUsed); + .forEach(segment => markReturnStatementsOnSegmentAsUsed(segment, new Set())); } //---------------------------------------------------------------------- @@ -202,7 +236,9 @@ module.exports = { scopeInfo = { upper: scopeInfo, uselessReturns: [], - codePath + traversedTryBlockStatements: [], + codePath, + currentSegments: new Set() }; }, @@ -239,6 +275,9 @@ module.exports = { * NOTE: This event is notified for only reachable segments. */ onCodePathSegmentStart(segment) { + + scopeInfo.currentSegments.add(segment); + const info = { uselessReturns: getUselessReturns([], segment.allPrevSegments), returned: false @@ -248,6 +287,18 @@ module.exports = { segmentInfoMap.set(segment, info); }, + onUnreachableCodePathSegmentStart(segment) { + scopeInfo.currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + scopeInfo.currentSegments.delete(segment); + }, + + onCodePathSegmentEnd(segment) { + scopeInfo.currentSegments.delete(segment); + }, + // Adds ReturnStatement node to check whether it's useless or not. ReturnStatement(node) { if (node.argument) { @@ -259,12 +310,12 @@ module.exports = { isInFinally(node) || // Ignore `return` statements in unreachable places (https://github.com/eslint/eslint/issues/11647). - !scopeInfo.codePath.currentSegments.some(s => s.reachable) + !isAnySegmentReachable(scopeInfo.currentSegments) ) { return; } - for (const segment of scopeInfo.codePath.currentSegments) { + for (const segment of scopeInfo.currentSegments) { const info = segmentInfoMap.get(segment); if (info) { @@ -275,6 +326,14 @@ module.exports = { scopeInfo.uselessReturns.push(node); }, + "TryStatement > BlockStatement.block:exit"(node) { + scopeInfo.traversedTryBlockStatements.push(node); + }, + + "TryStatement:exit"() { + scopeInfo.traversedTryBlockStatements.pop(); + }, + /* * Registers for all statement nodes except FunctionDeclaration, BlockStatement, BreakStatement. * Removes return statements of the current segments from the useless return statement list. diff --git a/lib/rules/no-whitespace-before-property.js b/lib/rules/no-whitespace-before-property.js index 1153314afe6..94a166e6ade 100644 --- a/lib/rules/no-whitespace-before-property.js +++ b/lib/rules/no-whitespace-before-property.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to disallow whitespace before properties * @author Kai Cataldo + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -17,6 +18,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/nonblock-statement-body-position.js b/lib/rules/nonblock-statement-body-position.js index 1ea2770ceb2..811b32b0821 100644 --- a/lib/rules/nonblock-statement-body-position.js +++ b/lib/rules/nonblock-statement-body-position.js @@ -1,6 +1,7 @@ /** * @fileoverview enforce the location of single-line statements * @author Teddy Katz + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -13,6 +14,8 @@ const POSITION_SCHEMA = { enum: ["beside", "below", "any"] }; /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/object-curly-newline.js b/lib/rules/object-curly-newline.js index caf1982312a..176694b6a07 100644 --- a/lib/rules/object-curly-newline.js +++ b/lib/rules/object-curly-newline.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to require or disallow line breaks inside braces. * @author Toru Nagashima + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -147,6 +148,8 @@ function areLineBreaksRequired(node, options, first, last) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/object-curly-spacing.js b/lib/rules/object-curly-spacing.js index 41ca428fe24..4463bcd5af1 100644 --- a/lib/rules/object-curly-spacing.js +++ b/lib/rules/object-curly-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Disallows or enforces spaces inside of object literals. * @author Jamund Ferguson + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -13,6 +14,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/object-property-newline.js b/lib/rules/object-property-newline.js index deca9b555b1..6ffa06421f0 100644 --- a/lib/rules/object-property-newline.js +++ b/lib/rules/object-property-newline.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to enforce placing object properties on separate lines. * @author Vitor Balocco + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -12,6 +13,8 @@ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/one-var-declaration-per-line.js b/lib/rules/one-var-declaration-per-line.js index b1e045b0dc5..340eac16932 100644 --- a/lib/rules/one-var-declaration-per-line.js +++ b/lib/rules/one-var-declaration-per-line.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to check multiple var declarations per line * @author Alberto Rodríguez + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -11,6 +12,8 @@ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "suggestion", docs: { diff --git a/lib/rules/operator-linebreak.js b/lib/rules/operator-linebreak.js index 2b609f63576..3065e66be12 100644 --- a/lib/rules/operator-linebreak.js +++ b/lib/rules/operator-linebreak.js @@ -1,6 +1,7 @@ /** * @fileoverview Operator linebreak - enforces operator linebreak style of two types: after and before * @author Benoît Zugmeyer + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -18,6 +19,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/padded-blocks.js b/lib/rules/padded-blocks.js index c6d1372ac75..ec4756ba739 100644 --- a/lib/rules/padded-blocks.js +++ b/lib/rules/padded-blocks.js @@ -1,6 +1,7 @@ /** * @fileoverview A rule to ensure blank lines within blocks. * @author Mathias Schreck + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -18,6 +19,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/padding-line-between-statements.js b/lib/rules/padding-line-between-statements.js index be7e2e40488..084651b6dff 100644 --- a/lib/rules/padding-line-between-statements.js +++ b/lib/rules/padding-line-between-statements.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to require or disallow newlines between statements * @author Toru Nagashima + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -130,49 +131,6 @@ function isBlockLikeStatement(sourceCode, node) { ); } -/** - * Check whether the given node is a directive or not. - * @param {ASTNode} node The node to check. - * @param {SourceCode} sourceCode The source code object to get tokens. - * @returns {boolean} `true` if the node is a directive. - */ -function isDirective(node, sourceCode) { - return ( - node.type === "ExpressionStatement" && - ( - node.parent.type === "Program" || - ( - node.parent.type === "BlockStatement" && - astUtils.isFunction(node.parent.parent) - ) - ) && - node.expression.type === "Literal" && - typeof node.expression.value === "string" && - !astUtils.isParenthesised(sourceCode, node.expression) - ); -} - -/** - * Check whether the given node is a part of directive prologue or not. - * @param {ASTNode} node The node to check. - * @param {SourceCode} sourceCode The source code object to get tokens. - * @returns {boolean} `true` if the node is a part of directive prologue. - */ -function isDirectivePrologue(node, sourceCode) { - if (isDirective(node, sourceCode)) { - for (const sibling of node.parent.body) { - if (sibling === node) { - break; - } - if (!isDirective(sibling, sourceCode)) { - return false; - } - } - return true; - } - return false; -} - /** * Gets the actual last token. * @@ -366,12 +324,10 @@ const StatementTypes = { CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init)) }, directive: { - test: isDirectivePrologue + test: astUtils.isDirective }, expression: { - test: (node, sourceCode) => - node.type === "ExpressionStatement" && - !isDirectivePrologue(node, sourceCode) + test: node => node.type === "ExpressionStatement" && !astUtils.isDirective(node) }, iife: { test: isIIFEStatement @@ -382,10 +338,10 @@ const StatementTypes = { isBlockLikeStatement(sourceCode, node) }, "multiline-expression": { - test: (node, sourceCode) => + test: node => node.loc.start.line !== node.loc.end.line && node.type === "ExpressionStatement" && - !isDirectivePrologue(node, sourceCode) + !astUtils.isDirective(node) }, "multiline-const": newMultilineKeywordTester("const"), @@ -428,6 +384,8 @@ const StatementTypes = { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/prefer-exponentiation-operator.js b/lib/rules/prefer-exponentiation-operator.js index dd4ba2c8e04..6d807f9cfea 100644 --- a/lib/rules/prefer-exponentiation-operator.js +++ b/lib/rules/prefer-exponentiation-operator.js @@ -55,11 +55,12 @@ function doesExponentNeedParens(exponent) { function doesExponentiationExpressionNeedParens(node, sourceCode) { const parent = node.parent.type === "ChainExpression" ? node.parent.parent : node.parent; + const parentPrecedence = astUtils.getPrecedence(parent); const needsParens = ( parent.type === "ClassDeclaration" || ( parent.type.endsWith("Expression") && - astUtils.getPrecedence(parent) >= PRECEDENCE_OF_EXPONENTIATION_EXPR && + (parentPrecedence === -1 || parentPrecedence >= PRECEDENCE_OF_EXPONENTIATION_EXPR) && !(parent.type === "BinaryExpression" && parent.operator === "**" && parent.right === node) && !((parent.type === "CallExpression" || parent.type === "NewExpression") && parent.arguments.includes(node)) && !(parent.type === "MemberExpression" && parent.computed && parent.property === node) && diff --git a/lib/rules/prefer-named-capture-group.js b/lib/rules/prefer-named-capture-group.js index 8fb68de1794..a82ee1f7835 100644 --- a/lib/rules/prefer-named-capture-group.js +++ b/lib/rules/prefer-named-capture-group.js @@ -112,14 +112,17 @@ module.exports = { * @param {string} pattern The regular expression pattern to be checked. * @param {ASTNode} node AST node which contains the regular expression or a call/new expression. * @param {ASTNode} regexNode AST node which contains the regular expression. - * @param {boolean} uFlag Flag indicates whether unicode mode is enabled or not. + * @param {string|null} flags The regular expression flags to be checked. * @returns {void} */ - function checkRegex(pattern, node, regexNode, uFlag) { + function checkRegex(pattern, node, regexNode, flags) { let ast; try { - ast = parser.parsePattern(pattern, 0, pattern.length, uFlag); + ast = parser.parsePattern(pattern, 0, pattern.length, { + unicode: Boolean(flags && flags.includes("u")), + unicodeSets: Boolean(flags && flags.includes("v")) + }); } catch { // ignore regex syntax errors @@ -148,7 +151,7 @@ module.exports = { return { Literal(node) { if (node.regex) { - checkRegex(node.regex.pattern, node, node, node.regex.flags.includes("u")); + checkRegex(node.regex.pattern, node, node, node.regex.flags); } }, Program(node) { @@ -166,7 +169,7 @@ module.exports = { const flags = getStringIfConstant(refNode.arguments[1]); if (regex) { - checkRegex(regex, refNode, refNode.arguments[0], flags && flags.includes("u")); + checkRegex(regex, refNode, refNode.arguments[0], flags); } } } diff --git a/lib/rules/prefer-regex-literals.js b/lib/rules/prefer-regex-literals.js index 39e29506421..ffaaeac3f27 100644 --- a/lib/rules/prefer-regex-literals.js +++ b/lib/rules/prefer-regex-literals.js @@ -37,15 +37,6 @@ function isRegexLiteral(node) { return node.type === "Literal" && Object.prototype.hasOwnProperty.call(node, "regex"); } -/** - * Determines whether the given node is a template literal without expressions. - * @param {ASTNode} node Node to check. - * @returns {boolean} True if the node is a template literal without expressions. - */ -function isStaticTemplateLiteral(node) { - return node.type === "TemplateLiteral" && node.expressions.length === 0; -} - const validPrecedingTokens = new Set([ "(", ";", @@ -178,7 +169,7 @@ module.exports = { return node.type === "TaggedTemplateExpression" && astUtils.isSpecificMemberAccess(node.tag, "String", "raw") && isGlobalReference(astUtils.skipChainExpression(node.tag).object) && - isStaticTemplateLiteral(node.quasi); + astUtils.isStaticTemplateLiteral(node.quasi); } /** @@ -191,7 +182,7 @@ module.exports = { return node.value; } - if (isStaticTemplateLiteral(node)) { + if (astUtils.isStaticTemplateLiteral(node)) { return node.quasis[0].value.cooked; } @@ -209,7 +200,7 @@ module.exports = { */ function isStaticString(node) { return isStringLiteral(node) || - isStaticTemplateLiteral(node) || + astUtils.isStaticTemplateLiteral(node) || isStringRawTaggedStaticTemplateLiteral(node); } @@ -250,7 +241,7 @@ module.exports = { /** * Returns a ecmaVersion compatible for regexpp. * @param {number} ecmaVersion The ecmaVersion to convert. - * @returns {import("regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp. + * @returns {import("@eslint-community/regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp. */ function getRegexppEcmaVersion(ecmaVersion) { if (ecmaVersion <= 5) { @@ -306,7 +297,10 @@ module.exports = { const validator = new RegExpValidator({ ecmaVersion: regexppEcmaVersion }); try { - validator.validatePattern(pattern, 0, pattern.length, flags ? flags.includes("u") : false); + validator.validatePattern(pattern, 0, pattern.length, { + unicode: flags ? flags.includes("u") : false, + unicodeSets: flags ? flags.includes("v") : false + }); if (flags) { validator.validateFlags(flags); } @@ -470,7 +464,10 @@ module.exports = { if (regexContent && !noFix) { let charIncrease = 0; - const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false); + const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, { + unicode: flags ? flags.includes("u") : false, + unicodeSets: flags ? flags.includes("v") : false + }); visitRegExpAST(ast, { onCharacterEnter(characterNode) { diff --git a/lib/rules/quote-props.js b/lib/rules/quote-props.js index 8abab150c4e..fe26eed77de 100644 --- a/lib/rules/quote-props.js +++ b/lib/rules/quote-props.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to flag non-quoted property names in object literals. * @author Mathias Bynens + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -19,6 +20,8 @@ const keywords = require("./utils/keywords"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "suggestion", docs: { diff --git a/lib/rules/quotes.js b/lib/rules/quotes.js index 9efb9809aa3..17d97dd697d 100644 --- a/lib/rules/quotes.js +++ b/lib/rules/quotes.js @@ -1,6 +1,7 @@ /** * @fileoverview A rule to choose between single and double quote marks * @author Matt DuVall , Brandon Payton + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -77,6 +78,8 @@ const AVOID_ESCAPE = "avoid-escape"; /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { @@ -157,7 +160,8 @@ module.exports = { /** * Checks whether or not a given node is a directive. - * The directive is a `ExpressionStatement` which has only a string literal. + * The directive is a `ExpressionStatement` which has only a string literal not surrounded by + * parentheses. * @param {ASTNode} node A node to check. * @returns {boolean} Whether or not the node is a directive. * @private @@ -166,23 +170,23 @@ module.exports = { return ( node.type === "ExpressionStatement" && node.expression.type === "Literal" && - typeof node.expression.value === "string" + typeof node.expression.value === "string" && + !astUtils.isParenthesised(sourceCode, node.expression) ); } /** - * Checks whether or not a given node is a part of directive prologues. - * See also: http://www.ecma-international.org/ecma-262/6.0/#sec-directive-prologues-and-the-use-strict-directive + * Checks whether a specified node is either part of, or immediately follows a (possibly empty) directive prologue. + * @see {@link http://www.ecma-international.org/ecma-262/6.0/#sec-directive-prologues-and-the-use-strict-directive} * @param {ASTNode} node A node to check. - * @returns {boolean} Whether or not the node is a part of directive prologues. + * @returns {boolean} Whether a specified node is either part of, or immediately follows a (possibly empty) directive prologue. * @private */ - function isPartOfDirectivePrologue(node) { - const block = node.parent.parent; - - if (block.type !== "Program" && (block.type !== "BlockStatement" || !astUtils.isFunction(block.parent))) { + function isExpressionInOrJustAfterDirectivePrologue(node) { + if (!astUtils.isTopLevelExpressionStatement(node.parent)) { return false; } + const block = node.parent.parent; // Check the node is at a prologue. for (let i = 0; i < block.body.length; ++i) { @@ -212,7 +216,7 @@ module.exports = { // Directive Prologues. case "ExpressionStatement": - return isPartOfDirectivePrologue(node); + return !astUtils.isParenthesised(sourceCode, node) && isExpressionInOrJustAfterDirectivePrologue(node); // LiteralPropertyName. case "Property": @@ -328,12 +332,11 @@ module.exports = { description: settings.description }, fix(fixer) { - if (isPartOfDirectivePrologue(node)) { + if (astUtils.isTopLevelExpressionStatement(node.parent) && !astUtils.isParenthesised(sourceCode, node)) { /* - * TemplateLiterals in a directive prologue aren't actually directives, but if they're - * in the directive prologue, then fixing them might turn them into directives and change - * the behavior of the code. + * TemplateLiterals aren't actually directives, but fixing them might turn + * them into directives and change the behavior of the code. */ return null; } diff --git a/lib/rules/require-atomic-updates.js b/lib/rules/require-atomic-updates.js index ba369a203e7..7e397ceb1cf 100644 --- a/lib/rules/require-atomic-updates.js +++ b/lib/rules/require-atomic-updates.js @@ -213,7 +213,8 @@ module.exports = { stack = { upper: stack, codePath, - referenceMap: shouldVerify ? createReferenceMap(scope) : null + referenceMap: shouldVerify ? createReferenceMap(scope) : null, + currentSegments: new Set() }; }, onCodePathEnd() { @@ -223,11 +224,25 @@ module.exports = { // Initialize the segment information. onCodePathSegmentStart(segment) { segmentInfo.initialize(segment); + stack.currentSegments.add(segment); }, + onUnreachableCodePathSegmentStart(segment) { + stack.currentSegments.add(segment); + }, + + onUnreachableCodePathSegmentEnd(segment) { + stack.currentSegments.delete(segment); + }, + + onCodePathSegmentEnd(segment) { + stack.currentSegments.delete(segment); + }, + + // Handle references to prepare verification. Identifier(node) { - const { codePath, referenceMap } = stack; + const { referenceMap } = stack; const reference = referenceMap && referenceMap.get(node); // Ignore if this is not a valid variable reference. @@ -240,7 +255,7 @@ module.exports = { // Add a fresh read variable. if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) { - segmentInfo.markAsRead(codePath.currentSegments, variable); + segmentInfo.markAsRead(stack.currentSegments, variable); } /* @@ -267,16 +282,15 @@ module.exports = { * If the reference exists in `outdatedReadVariables` list, report it. */ ":expression:exit"(node) { - const { codePath, referenceMap } = stack; // referenceMap exists if this is in a resumable function scope. - if (!referenceMap) { + if (!stack.referenceMap) { return; } // Mark the read variables on this code path as outdated. if (node.type === "AwaitExpression" || node.type === "YieldExpression") { - segmentInfo.makeOutdated(codePath.currentSegments); + segmentInfo.makeOutdated(stack.currentSegments); } // Verify. @@ -288,7 +302,7 @@ module.exports = { for (const reference of references) { const variable = reference.resolved; - if (segmentInfo.isOutdated(codePath.currentSegments, variable)) { + if (segmentInfo.isOutdated(stack.currentSegments, variable)) { if (node.parent.left === reference.identifier) { context.report({ node: node.parent, diff --git a/lib/rules/require-unicode-regexp.js b/lib/rules/require-unicode-regexp.js index f748753a2d1..dd687f8d796 100644 --- a/lib/rules/require-unicode-regexp.js +++ b/lib/rules/require-unicode-regexp.js @@ -28,7 +28,7 @@ module.exports = { type: "suggestion", docs: { - description: "Enforce the use of `u` flag on RegExp", + description: "Enforce the use of `u` or `v` flag on RegExp", recommended: false, url: "https://eslint.org/docs/latest/rules/require-unicode-regexp" }, @@ -51,7 +51,7 @@ module.exports = { "Literal[regex]"(node) { const flags = node.regex.flags || ""; - if (!flags.includes("u")) { + if (!flags.includes("u") && !flags.includes("v")) { context.report({ messageId: "requireUFlag", node, @@ -85,7 +85,7 @@ module.exports = { const pattern = getStringIfConstant(patternNode, scope); const flags = getStringIfConstant(flagsNode, scope); - if (!flagsNode || (typeof flags === "string" && !flags.includes("u"))) { + if (!flagsNode || (typeof flags === "string" && !flags.includes("u") && !flags.includes("v"))) { context.report({ messageId: "requireUFlag", node: refNode, diff --git a/lib/rules/rest-spread-spacing.js b/lib/rules/rest-spread-spacing.js index 7791238081e..287e56f014a 100644 --- a/lib/rules/rest-spread-spacing.js +++ b/lib/rules/rest-spread-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Enforce spacing between rest and spread operators and their expressions. * @author Kai Cataldo + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -12,6 +13,8 @@ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/semi-spacing.js b/lib/rules/semi-spacing.js index 770f62d41f1..35a49d2c22b 100644 --- a/lib/rules/semi-spacing.js +++ b/lib/rules/semi-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Validates spacing before and after semicolon * @author Mathias Schreck + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -14,6 +15,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/semi-style.js b/lib/rules/semi-style.js index 67ed1e478e7..caf2224df3d 100644 --- a/lib/rules/semi-style.js +++ b/lib/rules/semi-style.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to enforce location of semicolons. * @author Toru Nagashima + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -70,6 +71,8 @@ function isLastChild(node) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/semi.js b/lib/rules/semi.js index 6a473535d49..01586b8492d 100644 --- a/lib/rules/semi.js +++ b/lib/rules/semi.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to flag missing semicolons. * @author Nicholas C. Zakas + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -18,6 +19,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/space-before-blocks.js b/lib/rules/space-before-blocks.js index a580a4f2249..a4a5449e17f 100644 --- a/lib/rules/space-before-blocks.js +++ b/lib/rules/space-before-blocks.js @@ -1,6 +1,7 @@ /** * @fileoverview A rule to ensure whitespace before blocks. * @author Mathias Schreck + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -37,6 +38,8 @@ function isFunctionBody(node) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/space-before-function-paren.js b/lib/rules/space-before-function-paren.js index c5faa8cf4dd..575a1597a74 100644 --- a/lib/rules/space-before-function-paren.js +++ b/lib/rules/space-before-function-paren.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to validate spacing before function paren. * @author Mathias Schreck + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -17,6 +18,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/space-in-parens.js b/lib/rules/space-in-parens.js index c6c06d29a3f..d15a64317f4 100644 --- a/lib/rules/space-in-parens.js +++ b/lib/rules/space-in-parens.js @@ -1,6 +1,7 @@ /** * @fileoverview Disallows or enforces spaces inside of parentheses. * @author Jonathan Rajavuori + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -13,6 +14,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/space-infix-ops.js b/lib/rules/space-infix-ops.js index 81a95f83bf2..40071019480 100644 --- a/lib/rules/space-infix-ops.js +++ b/lib/rules/space-infix-ops.js @@ -1,6 +1,7 @@ /** * @fileoverview Require spaces around infix operators * @author Michael Ficarra + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -13,6 +14,8 @@ const { isEqToken } = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/space-unary-ops.js b/lib/rules/space-unary-ops.js index 381381d6e5d..aed43e7249e 100644 --- a/lib/rules/space-unary-ops.js +++ b/lib/rules/space-unary-ops.js @@ -1,6 +1,7 @@ /** * @fileoverview This rule should require or disallow spaces before or after unary operations. * @author Marcin Kumorek + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -17,6 +18,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/spaced-comment.js b/lib/rules/spaced-comment.js index 2eb7f0e3008..90ac7032d03 100644 --- a/lib/rules/spaced-comment.js +++ b/lib/rules/spaced-comment.js @@ -1,6 +1,7 @@ /** * @fileoverview Source code for spaced-comments rule * @author Gyandeep Singh + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -149,6 +150,8 @@ function createNeverStylePattern(markers) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "suggestion", docs: { diff --git a/lib/rules/switch-colon-spacing.js b/lib/rules/switch-colon-spacing.js index 45e401822a8..3ea63ca0eed 100644 --- a/lib/rules/switch-colon-spacing.js +++ b/lib/rules/switch-colon-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to enforce spacing around colons of switch statements. * @author Toru Nagashima + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -18,6 +19,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/template-curly-spacing.js b/lib/rules/template-curly-spacing.js index cc9bbe306d5..1f8cc34c1d7 100644 --- a/lib/rules/template-curly-spacing.js +++ b/lib/rules/template-curly-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to enforce spacing around embedded expressions of template strings * @author Toru Nagashima + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -18,6 +19,8 @@ const astUtils = require("./utils/ast-utils"); /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/template-tag-spacing.js b/lib/rules/template-tag-spacing.js index 9bfdfc2288d..52e0bcf2073 100644 --- a/lib/rules/template-tag-spacing.js +++ b/lib/rules/template-tag-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to check spacing between template tags and their literals * @author Jonathan Wilsson + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -12,6 +13,8 @@ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/utils/ast-utils.js b/lib/rules/utils/ast-utils.js index f4a18cff783..bebb4d5168b 100644 --- a/lib/rules/utils/ast-utils.js +++ b/lib/rules/utils/ast-utils.js @@ -9,6 +9,7 @@ // Requirements //------------------------------------------------------------------------------ +const { KEYS: eslintVisitorKeys } = require("eslint-visitor-keys"); const esutils = require("esutils"); const espree = require("espree"); const escapeRegExp = require("escape-string-regexp"); @@ -25,8 +26,8 @@ const { const anyFunctionPattern = /^(?:Function(?:Declaration|Expression)|ArrowFunctionExpression)$/u; const anyLoopPattern = /^(?:DoWhile|For|ForIn|ForOf|While)Statement$/u; +const arrayMethodWithThisArgPattern = /^(?:every|filter|find(?:Last)?(?:Index)?|flatMap|forEach|map|some)$/u; const arrayOrTypedArrayPattern = /Array$/u; -const arrayMethodPattern = /^(?:every|filter|find|findIndex|forEach|map|some)$/u; const bindOrCallOrApplyPattern = /^(?:bind|call|apply)$/u; const thisTagPattern = /^[\s*]*@this/mu; @@ -466,12 +467,12 @@ function isArrayFromMethod(node) { } /** - * Checks whether or not a node is a method which has `thisArg`. + * Checks whether or not a node is a method which expects a function as a first argument, and `thisArg` as a second argument. * @param {ASTNode} node A node to check. - * @returns {boolean} Whether or not the node is a method which has `thisArg`. + * @returns {boolean} Whether or not the node is a method which expects a function as a first argument, and `thisArg` as a second argument. */ function isMethodWhichHasThisArg(node) { - return isSpecificMemberAccess(node, null, arrayMethodPattern); + return isSpecificMemberAccess(node, null, arrayMethodWithThisArgPattern); } /** @@ -986,6 +987,34 @@ function isConstant(scope, node, inBooleanPosition) { return false; } +/** + * Checks whether a node is an ExpressionStatement at the top level of a file or function body. + * A top-level ExpressionStatement node is a directive if it contains a single unparenthesized + * string literal and if it occurs either as the first sibling or immediately after another + * directive. + * @param {ASTNode} node The node to check. + * @returns {boolean} Whether or not the node is an ExpressionStatement at the top level of a + * file or function body. + */ +function isTopLevelExpressionStatement(node) { + if (node.type !== "ExpressionStatement") { + return false; + } + const parent = node.parent; + + return parent.type === "Program" || (parent.type === "BlockStatement" && isFunction(parent.parent)); + +} + +/** + * Check whether the given node is a part of a directive prologue or not. + * @param {ASTNode} node The node to check. + * @returns {boolean} `true` if the node is a part of directive prologue. + */ +function isDirective(node) { + return node.type === "ExpressionStatement" && typeof node.directive === "string"; +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -1442,7 +1471,16 @@ module.exports = { return 19; default: - return 20; + if (node.type in eslintVisitorKeys) { + return 20; + } + + /* + * if the node is not a standard node that we know about, then assume it has the lowest precedence + * this will mean that rules will wrap unknown nodes in parentheses where applicable instead of + * unwrapping them and potentially changing the meaning of the code or introducing a syntax error. + */ + return -1; } }, @@ -1501,7 +1539,6 @@ module.exports = { return directives; }, - /** * Determines whether this node is a decimal integer literal. If a node is a decimal integer literal, a dot added * after the node will be parsed as a decimal point, rather than a property-access dot. @@ -2105,6 +2142,15 @@ module.exports = { return OCTAL_OR_NON_OCTAL_DECIMAL_ESCAPE_PATTERN.test(rawString); }, + /** + * Determines whether the given node is a template literal without expressions. + * @param {ASTNode} node Node to check. + * @returns {boolean} True if the node is a template literal without expressions. + */ + isStaticTemplateLiteral(node) { + return node.type === "TemplateLiteral" && node.expressions.length === 0; + }, + isReferenceToGlobalVariable, isLogicalExpression, isCoalesceExpression, @@ -2120,5 +2166,7 @@ module.exports = { isLogicalAssignmentOperator, getSwitchCaseColonToken, getModuleExportName, - isConstant + isConstant, + isTopLevelExpressionStatement, + isDirective }; diff --git a/lib/rules/utils/regular-expressions.js b/lib/rules/utils/regular-expressions.js index 234a1cb8b11..12e544e379d 100644 --- a/lib/rules/utils/regular-expressions.js +++ b/lib/rules/utils/regular-expressions.js @@ -8,7 +8,7 @@ const { RegExpValidator } = require("@eslint-community/regexpp"); -const REGEXPP_LATEST_ECMA_VERSION = 2022; +const REGEXPP_LATEST_ECMA_VERSION = 2024; /** * Checks if the given regular expression pattern would be valid with the `u` flag. @@ -28,7 +28,7 @@ function isValidWithUnicodeFlag(ecmaVersion, pattern) { }); try { - validator.validatePattern(pattern, void 0, void 0, /* uFlag = */ true); + validator.validatePattern(pattern, void 0, void 0, { unicode: /* uFlag = */ true }); } catch { return false; } diff --git a/lib/rules/valid-typeof.js b/lib/rules/valid-typeof.js index 82af130f98a..3818dafeae9 100644 --- a/lib/rules/valid-typeof.js +++ b/lib/rules/valid-typeof.js @@ -4,6 +4,12 @@ */ "use strict"; +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const astUtils = require("./utils/ast-utils"); + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -88,7 +94,7 @@ module.exports = { if (parent.type === "BinaryExpression" && OPERATORS.has(parent.operator)) { const sibling = parent.left === node ? parent.right : parent.left; - if (sibling.type === "Literal" || sibling.type === "TemplateLiteral" && !sibling.expressions.length) { + if (sibling.type === "Literal" || astUtils.isStaticTemplateLiteral(sibling)) { const value = sibling.type === "Literal" ? sibling.value : sibling.quasis[0].value.cooked; if (!VALID_TYPES.has(value)) { diff --git a/lib/rules/wrap-iife.js b/lib/rules/wrap-iife.js index 4c448fa7993..518071067db 100644 --- a/lib/rules/wrap-iife.js +++ b/lib/rules/wrap-iife.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to flag when IIFE is not wrapped in parens * @author Ilya Volodin + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -40,6 +41,8 @@ function isCalleeOfNewExpression(node) { /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/wrap-regex.js b/lib/rules/wrap-regex.js index 8166c252f3c..9e2808d60c7 100644 --- a/lib/rules/wrap-regex.js +++ b/lib/rules/wrap-regex.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to flag when regex literals are not wrapped in parens * @author Matt DuVall + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -12,6 +13,8 @@ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/yield-star-spacing.js b/lib/rules/yield-star-spacing.js index 9f9d918ae6c..9a67b78d25f 100644 --- a/lib/rules/yield-star-spacing.js +++ b/lib/rules/yield-star-spacing.js @@ -1,6 +1,7 @@ /** * @fileoverview Rule to check the spacing around the * in yield* expressions. * @author Bryan Smith + * @deprecated in ESLint v8.53.0 */ "use strict"; @@ -12,6 +13,8 @@ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { + deprecated: true, + replacedBy: [], type: "layout", docs: { diff --git a/lib/rules/yoda.js b/lib/rules/yoda.js index 60a6ad2f2db..af8f525182e 100644 --- a/lib/rules/yoda.js +++ b/lib/rules/yoda.js @@ -58,22 +58,13 @@ function isNegativeNumericLiteral(node) { ); } -/** - * Determines whether a node is a Template Literal which can be determined statically. - * @param {ASTNode} node Node to test - * @returns {boolean} True if the node is a Template Literal without expression. - */ -function isStaticTemplateLiteral(node) { - return node.type === "TemplateLiteral" && node.expressions.length === 0; -} - /** * Determines whether a non-Literal node should be treated as a single Literal node. * @param {ASTNode} node Node to test * @returns {boolean} True if the node should be treated as a single Literal node. */ function looksLikeLiteral(node) { - return isNegativeNumericLiteral(node) || isStaticTemplateLiteral(node); + return isNegativeNumericLiteral(node) || astUtils.isStaticTemplateLiteral(node); } /** @@ -100,7 +91,7 @@ function getNormalizedLiteral(node) { }; } - if (isStaticTemplateLiteral(node)) { + if (astUtils.isStaticTemplateLiteral(node)) { return { type: "Literal", value: node.quasis[0].value.cooked, diff --git a/lib/shared/types.js b/lib/shared/types.js index 1a19e85fba1..3880e870067 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -21,7 +21,7 @@ module.exports = {}; /** * @typedef {Object} ParserOptions * @property {EcmaFeatures} [ecmaFeatures] The optional features. - * @property {3|5|6|7|8|9|10|11|12|13|14|2015|2016|2017|2018|2019|2020|2021|2022|2023} [ecmaVersion] The ECMAScript version (or revision number). + * @property {3|5|6|7|8|9|10|11|12|13|14|15|2015|2016|2017|2018|2019|2020|2021|2022|2023|2024} [ecmaVersion] The ECMAScript version (or revision number). * @property {"script"|"module"} [sourceType] The source code type. * @property {boolean} [allowReserved] Allowing the use of reserved words as identifiers in ES3. */ diff --git a/lib/source-code/source-code.js b/lib/source-code/source-code.js index 07c0d294829..4bbd5ae3a5c 100644 --- a/lib/source-code/source-code.js +++ b/lib/source-code/source-code.js @@ -12,7 +12,15 @@ const { isCommentToken } = require("@eslint-community/eslint-utils"), TokenStore = require("./token-store"), astUtils = require("../shared/ast-utils"), - Traverser = require("../shared/traverser"); + Traverser = require("../shared/traverser"), + globals = require("../../conf/globals"), + { + directivesPattern + } = require("../shared/directives"), + + /* eslint-disable-next-line n/no-restricted-require -- Too messy to figure out right now. */ + ConfigCommentParser = require("../linter/config-comment-parser"), + eslintScope = require("eslint-scope"); //------------------------------------------------------------------------------ // Type Definitions @@ -24,6 +32,8 @@ const // Private //------------------------------------------------------------------------------ +const commentParser = new ConfigCommentParser(); + /** * Validates that the given AST has the required information. * @param {ASTNode} ast The Program node of the AST to check. @@ -49,6 +59,29 @@ function validate(ast) { } } +/** + * Retrieves globals for the given ecmaVersion. + * @param {number} ecmaVersion The version to retrieve globals for. + * @returns {Object} The globals for the given ecmaVersion. + */ +function getGlobalsForEcmaVersion(ecmaVersion) { + + switch (ecmaVersion) { + case 3: + return globals.es3; + + case 5: + return globals.es5; + + default: + if (ecmaVersion < 2015) { + return globals[`es${ecmaVersion + 2009}`]; + } + + return globals[`es${ecmaVersion}`]; + } +} + /** * Check to see if its a ES6 export declaration. * @param {ASTNode} astNode An AST node. @@ -83,6 +116,36 @@ function sortedMerge(tokens, comments) { return result; } +/** + * Normalizes a value for a global in a config + * @param {(boolean|string|null)} configuredValue The value given for a global in configuration or in + * a global directive comment + * @returns {("readable"|"writeable"|"off")} The value normalized as a string + * @throws Error if global value is invalid + */ +function normalizeConfigGlobal(configuredValue) { + switch (configuredValue) { + case "off": + return "off"; + + case true: + case "true": + case "writeable": + case "writable": + return "writable"; + + case null: + case false: + case "false": + case "readable": + case "readonly": + return "readonly"; + + default: + throw new Error(`'${configuredValue}' is not a valid configuration for a global (use 'readonly', 'writable', or 'off')`); + } +} + /** * Determines if two nodes or tokens overlap. * @param {ASTNode|Token} first The first node or token to check. @@ -145,6 +208,116 @@ function isSpaceBetween(sourceCode, first, second, checkInsideOfJSXText) { return false; } +//----------------------------------------------------------------------------- +// Directive Comments +//----------------------------------------------------------------------------- + +/** + * Extract the directive and the justification from a given directive comment and trim them. + * @param {string} value The comment text to extract. + * @returns {{directivePart: string, justificationPart: string}} The extracted directive and justification. + */ +function extractDirectiveComment(value) { + const match = /\s-{2,}\s/u.exec(value); + + if (!match) { + return { directivePart: value.trim(), justificationPart: "" }; + } + + const directive = value.slice(0, match.index).trim(); + const justification = value.slice(match.index + match[0].length).trim(); + + return { directivePart: directive, justificationPart: justification }; +} + +/** + * Ensures that variables representing built-in properties of the Global Object, + * and any globals declared by special block comments, are present in the global + * scope. + * @param {Scope} globalScope The global scope. + * @param {Object|undefined} configGlobals The globals declared in configuration + * @param {Object|undefined} inlineGlobals The globals declared in the source code + * @returns {void} + */ +function addDeclaredGlobals(globalScope, configGlobals = {}, inlineGlobals = {}) { + + // Define configured global variables. + for (const id of new Set([...Object.keys(configGlobals), ...Object.keys(inlineGlobals)])) { + + /* + * `normalizeConfigGlobal` will throw an error if a configured global value is invalid. However, these errors would + * typically be caught when validating a config anyway (validity for inline global comments is checked separately). + */ + const configValue = configGlobals[id] === void 0 ? void 0 : normalizeConfigGlobal(configGlobals[id]); + const commentValue = inlineGlobals[id] && inlineGlobals[id].value; + const value = commentValue || configValue; + const sourceComments = inlineGlobals[id] && inlineGlobals[id].comments; + + if (value === "off") { + continue; + } + + let variable = globalScope.set.get(id); + + if (!variable) { + variable = new eslintScope.Variable(id, globalScope); + + globalScope.variables.push(variable); + globalScope.set.set(id, variable); + } + + variable.eslintImplicitGlobalSetting = configValue; + variable.eslintExplicitGlobal = sourceComments !== void 0; + variable.eslintExplicitGlobalComments = sourceComments; + variable.writeable = (value === "writable"); + } + + /* + * "through" contains all references which definitions cannot be found. + * Since we augment the global scope using configuration, we need to update + * references and remove the ones that were added by configuration. + */ + globalScope.through = globalScope.through.filter(reference => { + const name = reference.identifier.name; + const variable = globalScope.set.get(name); + + if (variable) { + + /* + * Links the variable and the reference. + * And this reference is removed from `Scope#through`. + */ + reference.resolved = variable; + variable.references.push(reference); + + return false; + } + + return true; + }); +} + +/** + * Sets the given variable names as exported so they won't be triggered by + * the `no-unused-vars` rule. + * @param {eslint.Scope} globalScope The global scope to define exports in. + * @param {Record} variables An object whose keys are the variable + * names to export. + * @returns {void} + */ +function markExportedVariables(globalScope, variables) { + + Object.keys(variables).forEach(name => { + const variable = globalScope.set.get(name); + + if (variable) { + variable.eslintUsed = true; + variable.eslintExported = true; + } + }); + +} + //------------------------------------------------------------------------------ // Public Interface //------------------------------------------------------------------------------ @@ -187,7 +360,9 @@ class SourceCode extends TokenStore { * General purpose caching for the class. */ this[caches] = new Map([ - ["scopes", new WeakMap()] + ["scopes", new WeakMap()], + ["vars", new Map()], + ["configNodes", void 0] ]); /** @@ -266,7 +441,7 @@ class SourceCode extends TokenStore { // Cache for comments found using getComments(). this._commentCache = new WeakMap(); - // don't allow modification of this object + // don't allow further modification of this object Object.freeze(this); Object.freeze(this.lines); } @@ -724,6 +899,178 @@ class SourceCode extends TokenStore { } + /** + * Returns an array of all inline configuration nodes found in the + * source code. + * @returns {Array} An array of all inline configuration nodes. + */ + getInlineConfigNodes() { + + // check the cache first + let configNodes = this[caches].get("configNodes"); + + if (configNodes) { + return configNodes; + } + + // calculate fresh config nodes + configNodes = this.ast.comments.filter(comment => { + + // shebang comments are never directives + if (comment.type === "Shebang") { + return false; + } + + const { directivePart } = extractDirectiveComment(comment.value); + + const directiveMatch = directivesPattern.exec(directivePart); + + if (!directiveMatch) { + return false; + } + + // only certain comment types are supported as line comments + return comment.type !== "Line" || !!/^eslint-disable-(next-)?line$/u.test(directiveMatch[1]); + }); + + this[caches].set("configNodes", configNodes); + + return configNodes; + } + + /** + * Applies language options sent in from the core. + * @param {Object} languageOptions The language options for this run. + * @returns {void} + */ + applyLanguageOptions(languageOptions) { + + /* + * Add configured globals and language globals + * + * Using Object.assign instead of object spread for performance reasons + * https://github.com/eslint/eslint/issues/16302 + */ + const configGlobals = Object.assign( + {}, + getGlobalsForEcmaVersion(languageOptions.ecmaVersion), + languageOptions.sourceType === "commonjs" ? globals.commonjs : void 0, + languageOptions.globals + ); + const varsCache = this[caches].get("vars"); + + varsCache.set("configGlobals", configGlobals); + } + + /** + * Applies configuration found inside of the source code. This method is only + * called when ESLint is running with inline configuration allowed. + * @returns {{problems:Array,configs:{config:FlatConfigArray,node:ASTNode}}} Information + * that ESLint needs to further process the inline configuration. + */ + applyInlineConfig() { + + const problems = []; + const configs = []; + const exportedVariables = {}; + const inlineGlobals = Object.create(null); + + this.getInlineConfigNodes().forEach(comment => { + + const { directivePart } = extractDirectiveComment(comment.value); + const match = directivesPattern.exec(directivePart); + const directiveText = match[1]; + const directiveValue = directivePart.slice(match.index + directiveText.length); + + switch (directiveText) { + case "exported": + Object.assign(exportedVariables, commentParser.parseStringConfig(directiveValue, comment)); + break; + + case "globals": + case "global": + for (const [id, { value }] of Object.entries(commentParser.parseStringConfig(directiveValue, comment))) { + let normalizedValue; + + try { + normalizedValue = normalizeConfigGlobal(value); + } catch (err) { + problems.push({ + ruleId: null, + loc: comment.loc, + message: err.message + }); + continue; + } + + if (inlineGlobals[id]) { + inlineGlobals[id].comments.push(comment); + inlineGlobals[id].value = normalizedValue; + } else { + inlineGlobals[id] = { + comments: [comment], + value: normalizedValue + }; + } + } + break; + + case "eslint": { + const parseResult = commentParser.parseJsonConfig(directiveValue, comment.loc); + + if (parseResult.success) { + configs.push({ + config: { + rules: parseResult.config + }, + node: comment + }); + } else { + problems.push(parseResult.error); + } + + break; + } + + // no default + } + }); + + // save all the new variables for later + const varsCache = this[caches].get("vars"); + + varsCache.set("inlineGlobals", inlineGlobals); + varsCache.set("exportedVariables", exportedVariables); + + return { + configs, + problems + }; + } + + /** + * Called by ESLint core to indicate that it has finished providing + * information. We now add in all the missing variables and ensure that + * state-changing methods cannot be called by rules. + * @returns {void} + */ + finalize() { + + // Step 1: ensure that all of the necessary variables are up to date + const varsCache = this[caches].get("vars"); + const globalScope = this.scopeManager.scopes[0]; + const configGlobals = varsCache.get("configGlobals"); + const inlineGlobals = varsCache.get("inlineGlobals"); + const exportedVariables = varsCache.get("exportedVariables"); + + addDeclaredGlobals(globalScope, configGlobals, inlineGlobals); + + if (exportedVariables) { + markExportedVariables(globalScope, exportedVariables); + } + + } + } module.exports = SourceCode; diff --git a/lib/unsupported-api.js b/lib/unsupported-api.js index b688608ca88..8a2e147aabe 100644 --- a/lib/unsupported-api.js +++ b/lib/unsupported-api.js @@ -14,6 +14,7 @@ const { FileEnumerator } = require("./cli-engine/file-enumerator"); const { FlatESLint, shouldUseFlatConfig } = require("./eslint/flat-eslint"); const FlatRuleTester = require("./rule-tester/flat-rule-tester"); +const { ESLint } = require("./eslint/eslint"); //----------------------------------------------------------------------------- // Exports @@ -24,5 +25,6 @@ module.exports = { FlatESLint, shouldUseFlatConfig, FlatRuleTester, - FileEnumerator + FileEnumerator, + LegacyESLint: ESLint }; diff --git a/messages/eslintrc-incompat.js b/messages/eslintrc-incompat.js new file mode 100644 index 00000000000..ee77cb2328e --- /dev/null +++ b/messages/eslintrc-incompat.js @@ -0,0 +1,98 @@ +"use strict"; + +/* eslint consistent-return: 0 -- no default case */ + +const messages = { + + env: ` +A config object is using the "env" key, which is not supported in flat config system. + +Flat config uses "languageOptions.globals" to define global variables for your files. + +Please see the following page for information on how to convert your config object into the correct format: +https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options +`, + + extends: ` +A config object is using the "extends" key, which is not supported in flat config system. + +Instead of "extends", you can include config objects that you'd like to extend from directly in the flat config array. + +Please see the following page for more information: +https://eslint.org/docs/latest/use/configure/migration-guide#predefined-and-shareable-configs +`, + + globals: ` +A config object is using the "globals" key, which is not supported in flat config system. + +Flat config uses "languageOptions.globals" to define global variables for your files. + +Please see the following page for information on how to convert your config object into the correct format: +https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options +`, + + ignorePatterns: ` +A config object is using the "ignorePatterns" key, which is not supported in flat config system. + +Flat config uses "ignores" to specify files to ignore. + +Please see the following page for information on how to convert your config object into the correct format: +https://eslint.org/docs/latest/use/configure/migration-guide#ignoring-files +`, + + noInlineConfig: ` +A config object is using the "noInlineConfig" key, which is not supported in flat config system. + +Flat config uses "linterOptions.noInlineConfig" to specify files to ignore. + +Please see the following page for information on how to convert your config object into the correct format: +https://eslint.org/docs/latest/use/configure/migration-guide#linter-options +`, + + overrides: ` +A config object is using the "overrides" key, which is not supported in flat config system. + +Flat config is an array that acts like the eslintrc "overrides" array. + +Please see the following page for information on how to convert your config object into the correct format: +https://eslint.org/docs/latest/use/configure/migration-guide#glob-based-configs +`, + + parser: ` +A config object is using the "parser" key, which is not supported in flat config system. + +Flat config uses "languageOptions.parser" to override the default parser. + +Please see the following page for information on how to convert your config object into the correct format: +https://eslint.org/docs/latest/use/configure/migration-guide#custom-parsers +`, + + parserOptions: ` +A config object is using the "parserOptions" key, which is not supported in flat config system. + +Flat config uses "languageOptions.parserOptions" to specify parser options. + +Please see the following page for information on how to convert your config object into the correct format: +https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options +`, + + reportUnusedDisableDirectives: ` +A config object is using the "reportUnusedDisableDirectives" key, which is not supported in flat config system. + +Flat config uses "linterOptions.reportUnusedDisableDirectives" to specify files to ignore. + +Please see the following page for information on how to convert your config object into the correct format: +https://eslint.org/docs/latest/use/configure/migration-guide#linter-options +`, + + root: ` +A config object is using the "root" key, which is not supported in flat config system. + +Flat configs always act as if they are the root config file, so this key can be safely removed. +` +}; + +module.exports = function({ key }) { + + return messages[key].trim(); +}; diff --git a/messages/eslintrc-plugins.js b/messages/eslintrc-plugins.js new file mode 100644 index 00000000000..bb708c95b05 --- /dev/null +++ b/messages/eslintrc-plugins.js @@ -0,0 +1,24 @@ +"use strict"; + +module.exports = function({ plugins }) { + + const isArrayOfStrings = typeof plugins[0] === "string"; + + return ` +A config object has a "plugins" key defined as an array${isArrayOfStrings ? " of strings" : ""}. + +Flat config requires "plugins" to be an object in this form: + + { + plugins: { + ${isArrayOfStrings && plugins[0] ? plugins[0] : "namespace"}: pluginObject + } + } + +Please see the following page for information on how to convert your config object into the correct format: +https://eslint.org/docs/latest/use/configure/migration-guide#importing-plugins-and-custom-parsers + +If you're using a shareable config that you cannot rewrite in flat config format, then use the compatibility utility: +https://eslint.org/docs/latest/use/configure/migration-guide#using-eslintrc-configs-in-flat-config +`; +}; diff --git a/package.json b/package.json index ae6e8b35fb8..70c5241d15c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint", - "version": "8.41.0", + "version": "8.53.0", "author": "Nicholas C. Zakas ", "description": "An AST-based pattern checker for JavaScript.", "bin": { @@ -61,21 +61,22 @@ "bugs": "https://github.com/eslint/eslint/issues/", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.41.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -85,7 +86,6 @@ "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", @@ -95,14 +95,18 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "devDependencies": { "@babel/core": "^7.4.3", "@babel/preset-env": "^7.4.3", + "@wdio/browser-runner": "^8.14.6", + "@wdio/cli": "^8.14.6", + "@wdio/concise-reporter": "^8.14.0", + "@wdio/globals": "^8.14.6", + "@wdio/mocha-framework": "^8.14.0", "babel-loader": "^8.0.5", "c8": "^7.12.0", "chai": "^4.0.1", @@ -113,10 +117,10 @@ "eslint": "file:.", "eslint-config-eslint": "file:packages/eslint-config-eslint", "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-eslint-plugin": "^4.4.0", + "eslint-plugin-eslint-plugin": "^5.1.0", "eslint-plugin-internal-rules": "file:tools/internal-rules", - "eslint-plugin-jsdoc": "^38.1.6", - "eslint-plugin-n": "^15.2.4", + "eslint-plugin-jsdoc": "^46.2.5", + "eslint-plugin-n": "^16.0.0", "eslint-plugin-unicorn": "^42.0.0", "eslint-release": "^3.2.0", "eslump": "^3.0.0", @@ -126,11 +130,6 @@ "glob": "^7.1.6", "got": "^11.8.3", "gray-matter": "^4.0.3", - "karma": "^6.1.1", - "karma-chrome-launcher": "^3.1.0", - "karma-mocha": "^2.0.1", - "karma-mocha-reporter": "^2.2.5", - "karma-webpack": "^5.0.0", "lint-staged": "^11.0.0", "load-perf": "^0.2.0", "markdownlint": "^0.25.1", @@ -150,13 +149,14 @@ "pirates": "^4.0.5", "progress": "^2.0.3", "proxyquire": "^2.0.1", - "puppeteer": "^13.7.0", "recast": "^0.20.4", "regenerator-runtime": "^0.13.2", - "semver": "^7.3.5", + "rollup-plugin-node-polyfills": "^0.2.1", + "semver": "^7.5.3", "shelljs": "^0.8.2", "sinon": "^11.0.0", - "temp": "^0.9.0", + "vite-plugin-commonjs": "^0.8.2", + "webdriverio": "^8.14.6", "webpack": "^5.23.0", "webpack-cli": "^4.5.0", "yorkie": "^2.0.0" diff --git a/packages/eslint-config-eslint/README.md b/packages/eslint-config-eslint/README.md index c12625d58d3..645a1938260 100644 --- a/packages/eslint-config-eslint/README.md +++ b/packages/eslint-config-eslint/README.md @@ -22,12 +22,33 @@ npm install eslint-config-eslint --save-dev ## Usage -In your `.eslintrc` file, add: +### ESM (`"type":"module"`) projects -```json -{ - "extends": "eslint" -} +In your `eslint.config.js` file, add: + +```js +import eslintConfigESLint from "eslint-config-eslint"; + +export default [ + ...eslintConfigESLint +]; +``` + +**Note**: This configuration array contains configuration objects with the `files` property. + +* `files: ["**/*.js"]`: ESM-specific configurations. +* `files: ["**/*.cjs"]`: CommonJS-specific configurations. + +### CommonJS projects + +In your `eslint.config.js` file, add: + +```js +const eslintConfigESLintCJS = require("eslint-config-eslint/cjs"); + +module.exports = [ + ...eslintConfigESLintCJS +]; ``` ### Where to ask for help? diff --git a/packages/eslint-config-eslint/base.js b/packages/eslint-config-eslint/base.js new file mode 100644 index 00000000000..083336919e1 --- /dev/null +++ b/packages/eslint-config-eslint/base.js @@ -0,0 +1,386 @@ +"use strict"; + +const js = require("@eslint/js"); +const jsdoc = require("eslint-plugin-jsdoc"); +const eslintComments = require("eslint-plugin-eslint-comments"); +const unicorn = require("eslint-plugin-unicorn"); + +/* + * the plugins' configs are not updated to support the flat config, + * need to manually update the `plugins` property + */ +eslintComments.configs.recommended.plugins = { "eslint-comments": eslintComments }; + +// extends eslint recommended config +const jsConfigs = [js.configs.recommended, { + rules: { + "array-bracket-spacing": "error", + "array-callback-return": "error", + "arrow-body-style": ["error", "as-needed"], + "arrow-parens": ["error", "as-needed"], + "arrow-spacing": "error", + indent: ["error", 4, { SwitchCase: 1 }], + "block-spacing": "error", + "brace-style": ["error", "1tbs"], + camelcase: "error", + "class-methods-use-this": "error", + "comma-dangle": "error", + "comma-spacing": "error", + "comma-style": ["error", "last"], + "computed-property-spacing": "error", + "consistent-return": "error", + curly: ["error", "all"], + "default-case": "error", + "default-case-last": "error", + "default-param-last": "error", + "dot-location": ["error", "property"], + "dot-notation": [ + "error", + { allowKeywords: true } + ], + "eol-last": "error", + eqeqeq: "error", + "func-call-spacing": "error", + "func-style": ["error", "declaration"], + "function-call-argument-newline": ["error", "consistent"], + "function-paren-newline": ["error", "consistent"], + "generator-star-spacing": "error", + "grouped-accessor-pairs": "error", + "guard-for-in": "error", + "key-spacing": ["error", { beforeColon: false, afterColon: true }], + "keyword-spacing": "error", + "lines-around-comment": ["error", + { + beforeBlockComment: true, + afterBlockComment: false, + beforeLineComment: true, + afterLineComment: false + } + ], + "max-len": ["error", 160, + { + ignoreComments: true, + ignoreUrls: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + ignoreRegExpLiterals: true + } + ], + "max-statements-per-line": "error", + "new-cap": "error", + "new-parens": "error", + "no-alert": "error", + "no-array-constructor": "error", + "no-caller": "error", + "no-confusing-arrow": "error", + "no-console": "error", + "no-constant-binary-expression": "error", + "no-constructor-return": "error", + "no-else-return": ["error", { allowElseIf: false } + ], + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-floating-decimal": "error", + "no-implied-eval": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-mixed-spaces-and-tabs": ["error", false], + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-multiple-empty-lines": [ + "error", + { + max: 2, + maxBOF: 0, + maxEOF: 0 + } + ], + "no-nested-ternary": "error", + "no-new": "error", + "no-new-func": "error", + "no-new-object": "error", + "no-new-wrappers": "error", + "no-octal-escape": "error", + "no-param-reassign": "error", + "no-proto": "error", + "no-process-exit": "off", + "no-restricted-properties": ["error", + { + property: "substring", + message: "Use .slice instead of .substring." + }, + { + property: "substr", + message: "Use .slice instead of .substr." + }, + { + object: "assert", + property: "equal", + message: "Use assert.strictEqual instead of assert.equal." + }, + { + object: "assert", + property: "notEqual", + message: "Use assert.notStrictEqual instead of assert.notEqual." + }, + { + object: "assert", + property: "deepEqual", + message: "Use assert.deepStrictEqual instead of assert.deepEqual." + }, + { + object: "assert", + property: "notDeepEqual", + message: "Use assert.notDeepStrictEqual instead of assert.notDeepEqual." + } + ], + "no-return-assign": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "error", + "no-tabs": "error", + "no-throw-literal": "error", + "no-trailing-spaces": "error", + "no-undef": ["error", { typeof: true }], + "no-undef-init": "error", + "no-undefined": "error", + "no-underscore-dangle": ["error", { allowAfterThis: true } + ], + "no-unmodified-loop-condition": "error", + "no-unneeded-ternary": "error", + "no-unreachable-loop": "error", + "no-unused-expressions": "error", + "no-unused-vars": ["error", { + vars: "all", + args: "after-used", + caughtErrors: "all" + } + ], + "no-use-before-define": "error", + "no-useless-call": "error", + "no-useless-computed-key": "error", + "no-useless-concat": "error", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-whitespace-before-property": "error", + "no-var": "error", + "object-curly-newline": ["error", + { + consistent: true, + multiline: true + } + ], + "object-curly-spacing": ["error", "always"], + "object-property-newline": ["error", + { + allowAllPropertiesOnSameLine: true + } + ], + "object-shorthand": ["error", + "always", + { + avoidExplicitReturnArrows: true + } + ], + "one-var-declaration-per-line": "error", + "operator-assignment": "error", + "operator-linebreak": "error", + "padding-line-between-statements": ["error", + { + blankLine: "always", + prev: ["const", "let", "var"], + next: "*" + }, + { + blankLine: "any", + prev: ["const", "let", "var"], + next: ["const", "let", "var"] + } + ], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-exponentiation-operator": "error", + "prefer-numeric-literals": "error", + "prefer-promise-reject-errors": "error", + "prefer-regex-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + quotes: ["error", "double", { avoidEscape: true }], + "quote-props": ["error", "as-needed"], + radix: "error", + "require-unicode-regexp": "error", + "rest-spread-spacing": "error", + semi: "error", + "semi-spacing": ["error", + { + before: false, + after: true + } + ], + "semi-style": "error", + "space-before-blocks": "error", + "space-before-function-paren": ["error", + { + anonymous: "never", + named: "never", + asyncArrow: "always" + } + ], + "space-in-parens": "error", + "space-infix-ops": "error", + "space-unary-ops": ["error", + { + words: true, + nonwords: false + } + ], + "spaced-comment": ["error", + "always", + { + exceptions: ["-"] + } + ], + strict: ["error", "global"], + "switch-colon-spacing": "error", + "symbol-description": "error", + "template-curly-spacing": ["error", "never"], + "template-tag-spacing": "error", + "unicode-bom": "error", + "wrap-iife": "error", + "yield-star-spacing": "error", + yoda: ["error", "never", { exceptRange: true }] + } +}]; + +// extends eslint-plugin-jsdoc's recommended config +const jsdocConfigs = [jsdoc.configs["flat/recommended"], { + settings: { + jsdoc: { + mode: "typescript", + tagNamePreference: { + file: "fileoverview", + augments: "extends", + class: "constructor" + }, + preferredTypes: { + "*": { + message: "Use a more precise type or if necessary use `any` or `ArbitraryCallbackResult`", + replacement: "any" + }, + Any: { + message: "Use a more precise type or if necessary use `any` or `ArbitraryCallbackResult`", + replacement: "any" + }, + function: { + message: "Point to a `@callback` namepath or `Function` if truly arbitrary in form", + replacement: "Function" + }, + Promise: { + message: "Specify the specific Promise type, including, if necessary, the type `any`" + }, + ".<>": { + message: "Prefer type form without dot", + replacement: "<>" + }, + object: { + message: "Use the specific object type or `Object` if truly arbitrary", + replacement: "Object" + }, + array: "Array" + } + } + }, + rules: { + "jsdoc/check-syntax": "error", + "jsdoc/check-values": ["error", { allowedLicenses: true }], + "jsdoc/no-bad-blocks": "error", + "jsdoc/no-defaults": "off", + "jsdoc/require-asterisk-prefix": "error", + "jsdoc/require-description": ["error", { checkConstructors: false }], + "jsdoc/require-hyphen-before-param-description": ["error", "never"], + "jsdoc/require-returns": ["error", + { + forceRequireReturn: true, + forceReturnsWithAsync: true + } + ], + "jsdoc/require-throws": "error", + "jsdoc/tag-lines": ["error", "never", + { + tags: { + example: { lines: "always" }, + fileoverview: { lines: "any" } + }, + startLines: 0 + } + ], + "jsdoc/no-undefined-types": "off", + "jsdoc/require-yields": "off", + "jsdoc/check-access": "error", + "jsdoc/check-alignment": "error", + "jsdoc/check-param-names": "error", + "jsdoc/check-property-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/empty-tags": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/multiline-blocks": "error", + "jsdoc/no-multi-asterisks": "error", + "jsdoc/require-jsdoc": ["error", { require: { ClassDeclaration: true } }], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-name": "error", + "jsdoc/require-param-type": "error", + "jsdoc/require-property": "error", + "jsdoc/require-property-description": "error", + "jsdoc/require-property-name": "error", + "jsdoc/require-property-type": "error", + "jsdoc/require-returns-check": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "error", + "jsdoc/require-yields-check": "error", + "jsdoc/valid-types": "error" + } +}]; + +// extends eslint-plugin-unicorn's config +const unicornConfigs = [{ + plugins: { unicorn }, + rules: { + "unicorn/prefer-array-find": "error", + "unicorn/prefer-array-flat-map": "error", + "unicorn/prefer-array-flat": "error", + "unicorn/prefer-array-index-of": "error", + "unicorn/prefer-array-some": "error", + "unicorn/prefer-includes": "error", + "unicorn/prefer-set-has": "error", + "unicorn/prefer-string-slice": "error", + "unicorn/prefer-string-starts-ends-with": "error", + "unicorn/prefer-string-trim-start-end": "error" + } +}]; + +// extends eslint-plugin-eslint-comments's recommended config +const eslintCommentsConfigs = [eslintComments.configs.recommended, { + rules: { + "eslint-comments/disable-enable-pair": ["error"], + "eslint-comments/no-unused-disable": "error", + "eslint-comments/require-description": "error" + } +}]; + +module.exports = [ + { linterOptions: { reportUnusedDisableDirectives: true } }, + ...jsConfigs, + ...unicornConfigs, + ...jsdocConfigs, + ...eslintCommentsConfigs +]; diff --git a/packages/eslint-config-eslint/cjs.js b/packages/eslint-config-eslint/cjs.js new file mode 100644 index 00000000000..61963a39bef --- /dev/null +++ b/packages/eslint-config-eslint/cjs.js @@ -0,0 +1,9 @@ +"use strict"; + +const baseConfigs = require("./base"); +const { cjsConfigs } = require("./nodejs"); + +module.exports = [ + ...baseConfigs, + ...cjsConfigs +]; diff --git a/packages/eslint-config-eslint/default.yml b/packages/eslint-config-eslint/default.yml deleted file mode 100644 index afaabe4f7ab..00000000000 --- a/packages/eslint-config-eslint/default.yml +++ /dev/null @@ -1,340 +0,0 @@ -reportUnusedDisableDirectives: true -extends: - - "eslint:recommended" - - "plugin:n/recommended" - - "plugin:jsdoc/recommended" - - "plugin:eslint-comments/recommended" -plugins: - - unicorn -settings: - jsdoc: - tagNamePreference: - file: "fileoverview" - augments: "extends" - class: "constructor" - preferredTypes: - "*": - message: "Use a more precise type or if necessary use `any` or `ArbitraryCallbackResult`" - replacement: "any" - Any: - message: "Use a more precise type or if necessary use `any` or `ArbitraryCallbackResult`" - replacement: "any" - # Function: - # message: "Point to a `@callback` namepath or `GenericCallback` if truly arbitrary in form" - # replacement: "GenericCallback" - # function: - # message: "Point to a `@callback` namepath or `GenericCallback` if truly arbitrary in form" - # replacement: "GenericCallback" - function: - message: "Point to a `@callback` namepath or `Function` if truly arbitrary in form" - replacement: "Function" - Promise: - message: "Specify the specific Promise type, including, if necessary, the type `any`" - ".<>": - message: "Prefer type form without dot" - replacement: "<>" - # Object: - # message: "Use the specific object type or `PlainObject` if truly arbitrary" - # replacement: "PlainObject" - # object: - # message: "Use the specific object type or `PlainObject` if truly arbitrary" - # replacement: "PlainObject" - object: - message: "Use the specific object type or `Object` if truly arbitrary" - replacement: "Object" - # Array: - # message: "Use the specific array type or `GenericArray` if it is truly arbitrary." - # replacement: "GenericArray" - # array: - # message: "Use specific array type or `GenericArray` if it is truly arbitrary." - # replacement: "GenericArray" - array: "Array" - -rules: - array-bracket-spacing: "error" - array-callback-return: "error" - arrow-body-style: ["error", "as-needed"] - arrow-parens: ["error", "as-needed"] - arrow-spacing: "error" - indent: ["error", 4, { SwitchCase: 1 }] - block-spacing: "error" - brace-style: ["error", "1tbs"] - camelcase: "error" - class-methods-use-this: "error" - comma-dangle: "error" - comma-spacing: "error" - comma-style: ["error", "last"] - computed-property-spacing: "error" - consistent-return: "error" - curly: ["error", "all"] - default-case: "error" - default-case-last: "error" - default-param-last: "error" - dot-location: ["error", "property"] - dot-notation: ["error", { allowKeywords: true }] - eol-last: "error" - eqeqeq: "error" - - eslint-comments/disable-enable-pair: ["error"] - eslint-comments/no-unused-disable: "error" - eslint-comments/require-description: "error" - - func-call-spacing: "error" - func-style: ["error", "declaration"] - function-call-argument-newline: ["error", "consistent"] - function-paren-newline: ["error", "consistent"] - generator-star-spacing: "error" - grouped-accessor-pairs: "error" - guard-for-in: "error" - - # jsdoc: adopt non-recommended rules or change recommended configuration - # jsdoc/check-examples: "error" - # jsdoc/check-indentation: "error" # Revisit if allowing configurable spaces after tag line breaks - jsdoc/check-line-alignment: ["error", "never"] - jsdoc/check-syntax: "error" - # jsdoc/check-types': ["error', { exemptTagContexts: [ { tag: "typedef", types: ["object", "GenericObject"] } ] } - - jsdoc/check-values: ["error", { allowedLicenses: true }] - jsdoc/newline-after-description: ["error", "never"] - jsdoc/no-bad-blocks: "error" - jsdoc/require-asterisk-prefix: "error" - jsdoc/require-description: ["error", { checkConstructors: false }] - # jsdoc/require-file-overview: "error" - jsdoc/require-hyphen-before-param-description: ["error", "never"] - jsdoc/require-returns: - ["error", { forceRequireReturn: true, forceReturnsWithAsync: true }] - jsdoc/require-throws: "error" - jsdoc/tag-lines: - [ - "error", - "never", - { - tags: - { - example: { lines: "always" }, - fileoverview: { lines: "any" }, - }, - }, - ] - - # jsdoc: disable recommended rules - jsdoc/no-undefined-types: "off" - jsdoc/require-yields: "off" - - # jsdoc: change recommended rules from warnings into errors - jsdoc/check-access: "error" - jsdoc/check-alignment: "error" - jsdoc/check-param-names: "error" - jsdoc/check-property-names: "error" - jsdoc/check-tag-names: "error" - jsdoc/check-types: "error" - jsdoc/empty-tags: "error" - jsdoc/implements-on-classes: "error" - jsdoc/multiline-blocks: "error" - jsdoc/no-multi-asterisks: "error" - jsdoc/require-jsdoc: ["error", { require: { ClassDeclaration: true } }] - jsdoc/require-param: "error" - jsdoc/require-param-description: "error" - jsdoc/require-param-name: "error" - jsdoc/require-param-type: "error" - jsdoc/require-property: "error" - jsdoc/require-property-description: "error" - jsdoc/require-property-name: "error" - jsdoc/require-property-type: "error" - jsdoc/require-returns-check: "error" - jsdoc/require-returns-description: "error" - jsdoc/require-returns-type: "error" - jsdoc/require-yields-check: "error" - jsdoc/valid-types: "error" - - key-spacing: ["error", { beforeColon: false, afterColon: true }] - keyword-spacing: "error" - lines-around-comment: - [ - "error", - { - beforeBlockComment: true, - afterBlockComment: false, - beforeLineComment: true, - afterLineComment: false, - }, - ] - max-len: - [ - "error", - 160, - { - "ignoreComments": true, - "ignoreUrls": true, - "ignoreStrings": true, - "ignoreTemplateLiterals": true, - "ignoreRegExpLiterals": true, - }, - ] - max-statements-per-line: "error" - new-cap: "error" - new-parens: "error" - no-alert: "error" - no-array-constructor: "error" - no-caller: "error" - no-confusing-arrow: "error" - no-console: "error" - no-constant-binary-expression: "error" - no-constructor-return: "error" - no-else-return: ["error", { allowElseIf: false }] - no-eval: "error" - no-extend-native: "error" - no-extra-bind: "error" - no-floating-decimal: "error" - no-implied-eval: "error" - no-invalid-this: "error" - no-iterator: "error" - no-label-var: "error" - no-labels: "error" - no-lone-blocks: "error" - no-loop-func: "error" - no-mixed-spaces-and-tabs: ["error", false] # Modified from recommended - no-multi-spaces: "error" - no-multi-str: "error" - no-multiple-empty-lines: ["error", { max: 2, maxBOF: 0, maxEOF: 0 }] - no-nested-ternary: "error" - no-new: "error" - no-new-func: "error" - no-new-object: "error" - no-new-wrappers: "error" - no-octal-escape: "error" - no-param-reassign: "error" - no-proto: "error" - no-process-exit: "off" - no-restricted-properties: - [ - "error", - { - property: "substring", - message: "Use .slice instead of .substring.", - }, - { property: "substr", message: "Use .slice instead of .substr." }, - { - object: "assert", - property: "equal", - message: "Use assert.strictEqual instead of assert.equal.", - }, - { - object: "assert", - property: "notEqual", - message: "Use assert.notStrictEqual instead of assert.notEqual.", - }, - { - object: "assert", - property: "deepEqual", - message: "Use assert.deepStrictEqual instead of assert.deepEqual.", - }, - { - object: "assert", - property: "notDeepEqual", - message: "Use assert.notDeepStrictEqual instead of assert.notDeepEqual.", - }, - ] - no-return-assign: "error" - no-script-url: "error" - no-self-compare: "error" - no-sequences: "error" - no-shadow: "error" - no-tabs: "error" - no-throw-literal: "error" - no-trailing-spaces: "error" - no-undef: ["error", { typeof: true }] # Modified from recommended - no-undef-init: "error" - no-undefined: "error" - no-underscore-dangle: ["error", { allowAfterThis: true }] - no-unmodified-loop-condition: "error" - no-unneeded-ternary: "error" - no-unreachable-loop: "error" - no-unused-expressions: "error" - no-unused-vars: [ - "error", - { vars: "all", args: "after-used", caughtErrors: "all" }, - ] # Modified from recommended - no-use-before-define: "error" - no-useless-call: "error" - no-useless-computed-key: "error" - no-useless-concat: "error" - no-useless-constructor: "error" - no-useless-rename: "error" - no-useless-return: "error" - no-whitespace-before-property: "error" - no-var: "error" - - n/callback-return: ["error", ["cb", "callback", "next"]] - n/handle-callback-err: ["error", "err"] - n/no-deprecated-api: "error" - n/no-mixed-requires: "error" - n/no-new-require: "error" - n/no-path-concat: "error" - - object-curly-newline: ["error", { "consistent": true, "multiline": true }] - object-curly-spacing: ["error", "always"] - object-property-newline: ["error", { "allowAllPropertiesOnSameLine": true }] - object-shorthand: ["error", "always", { "avoidExplicitReturnArrows": true }] - one-var-declaration-per-line: "error" - operator-assignment: "error" - operator-linebreak: "error" - padding-line-between-statements: - [ - "error", - { blankLine: "always", prev: ["const", "let", "var"], next: "*" }, - { - blankLine: "any", - prev: ["const", "let", "var"], - next: ["const", "let", "var"], - }, - ] - prefer-arrow-callback: "error" - prefer-const: "error" - prefer-exponentiation-operator: "error" - prefer-numeric-literals: "error" - prefer-promise-reject-errors: "error" - prefer-regex-literals: "error" - prefer-rest-params: "error" - prefer-spread: "error" - prefer-template: "error" - quotes: ["error", "double", { avoidEscape: true }] - quote-props: ["error", "as-needed"] - radix: "error" - require-unicode-regexp: "error" - rest-spread-spacing: "error" - semi: "error" - semi-spacing: ["error", { before: false, after: true }] - semi-style: "error" - space-before-blocks: "error" - space-before-function-paren: - [ - "error", - { "anonymous": "never", "named": "never", "asyncArrow": "always" }, - ] - space-in-parens: "error" - space-infix-ops: "error" - space-unary-ops: ["error", { words: true, nonwords: false }] - spaced-comment: ["error", "always", { exceptions: ["-"] }] - strict: ["error", "global"] - switch-colon-spacing: "error" - symbol-description: "error" - template-curly-spacing: ["error", "never"] - template-tag-spacing: "error" - unicode-bom: "error" - - # Selectively-enable unicorn rules since many of its recommended rules are too aggressive/stylistic for us. - unicorn/prefer-array-find: "error" - unicorn/prefer-array-flat-map: "error" - unicorn/prefer-array-flat: "error" - unicorn/prefer-array-index-of: "error" - unicorn/prefer-array-some: "error" - unicorn/prefer-includes: "error" - unicorn/prefer-set-has: "error" - unicorn/prefer-string-slice: "error" - unicorn/prefer-string-starts-ends-with: "error" - unicorn/prefer-string-trim-start-end: "error" - - wrap-iife: "error" - yield-star-spacing: "error" - yoda: ["error", "never", { exceptRange: true }] diff --git a/packages/eslint-config-eslint/eslintrc.js b/packages/eslint-config-eslint/eslintrc.js new file mode 100644 index 00000000000..31ce7f252c0 --- /dev/null +++ b/packages/eslint-config-eslint/eslintrc.js @@ -0,0 +1,454 @@ +/** + * TODO: the config will be removed in the future, please use the index config. + * @deprecated + * @fileoverview the eslintrc config - it's exported as ESLint VS Code extension + * expects eslintrc config files to be present to work correctly.. + * @author 唯然 + */ +"use strict"; + +module.exports = { + reportUnusedDisableDirectives: true, + extends: [ + "eslint:recommended", + "plugin:n/recommended", + "plugin:jsdoc/recommended", + "plugin:eslint-comments/recommended" + ], + plugins: ["unicorn"], + settings: { + jsdoc: { + mode: "typescript", + tagNamePreference: { + file: "fileoverview", + augments: "extends", + class: "constructor" + }, + preferredTypes: { + "*": { + message: + "Use a more precise type or if necessary use `any` or `ArbitraryCallbackResult`", + replacement: "any" + }, + Any: { + message: + "Use a more precise type or if necessary use `any` or `ArbitraryCallbackResult`", + replacement: "any" + }, + function: { + message: + "Point to a `@callback` namepath or `Function` if truly arbitrary in form", + replacement: "Function" + }, + Promise: { + message: + "Specify the specific Promise type, including, if necessary, the type `any`" + }, + ".<>": { + message: "Prefer type form without dot", + replacement: "<>" + }, + object: { + message: + "Use the specific object type or `Object` if truly arbitrary", + replacement: "Object" + }, + array: "Array" + } + } + }, + rules: { + "array-bracket-spacing": "error", + "array-callback-return": "error", + "arrow-body-style": ["error", "as-needed"], + "arrow-parens": ["error", "as-needed"], + "arrow-spacing": "error", + indent: [ + "error", + 4, + { + SwitchCase: 1 + } + ], + "block-spacing": "error", + "brace-style": ["error", "1tbs"], + camelcase: "error", + "class-methods-use-this": "error", + "comma-dangle": "error", + "comma-spacing": "error", + "comma-style": ["error", "last"], + "computed-property-spacing": "error", + "consistent-return": "error", + curly: ["error", "all"], + "default-case": "error", + "default-case-last": "error", + "default-param-last": "error", + "dot-location": ["error", "property"], + "dot-notation": [ + "error", + { + allowKeywords: true + } + ], + "eol-last": "error", + eqeqeq: "error", + "eslint-comments/disable-enable-pair": ["error"], + "eslint-comments/no-unused-disable": "error", + "eslint-comments/require-description": "error", + "func-call-spacing": "error", + "func-style": ["error", "declaration"], + "function-call-argument-newline": ["error", "consistent"], + "function-paren-newline": ["error", "consistent"], + "generator-star-spacing": "error", + "grouped-accessor-pairs": "error", + "guard-for-in": "error", + "jsdoc/check-syntax": "error", + "jsdoc/check-values": [ + "error", + { + allowedLicenses: true + } + ], + "jsdoc/no-bad-blocks": "error", + "jsdoc/no-defaults": "off", + "jsdoc/require-asterisk-prefix": "error", + "jsdoc/require-description": [ + "error", + { + checkConstructors: false + } + ], + "jsdoc/require-hyphen-before-param-description": ["error", "never"], + "jsdoc/require-returns": [ + "error", + { + forceRequireReturn: true, + forceReturnsWithAsync: true + } + ], + "jsdoc/require-throws": "error", + "jsdoc/tag-lines": [ + "error", + "never", + { + tags: { + example: { + lines: "always" + }, + fileoverview: { + lines: "any" + } + }, + startLines: 0 + } + + ], + "jsdoc/no-undefined-types": "off", + "jsdoc/require-yields": "off", + "jsdoc/check-access": "error", + "jsdoc/check-alignment": "error", + "jsdoc/check-param-names": "error", + "jsdoc/check-property-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/empty-tags": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/multiline-blocks": "error", + "jsdoc/no-multi-asterisks": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + ClassDeclaration: true + } + } + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-name": "error", + "jsdoc/require-param-type": "error", + "jsdoc/require-property": "error", + "jsdoc/require-property-description": "error", + "jsdoc/require-property-name": "error", + "jsdoc/require-property-type": "error", + "jsdoc/require-returns-check": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "error", + "jsdoc/require-yields-check": "error", + "jsdoc/valid-types": "error", + "key-spacing": [ + "error", + { + beforeColon: false, + afterColon: true + } + ], + "keyword-spacing": "error", + "lines-around-comment": [ + "error", + { + beforeBlockComment: true, + afterBlockComment: false, + beforeLineComment: true, + afterLineComment: false + } + ], + "max-len": [ + "error", + 160, + { + ignoreComments: true, + ignoreUrls: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + ignoreRegExpLiterals: true + } + ], + "max-statements-per-line": "error", + "new-cap": "error", + "new-parens": "error", + "no-alert": "error", + "no-array-constructor": "error", + "no-caller": "error", + "no-confusing-arrow": "error", + "no-console": "error", + "no-constant-binary-expression": "error", + "no-constructor-return": "error", + "no-else-return": [ + "error", + { + allowElseIf: false + } + ], + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-floating-decimal": "error", + "no-implied-eval": "error", + "no-invalid-this": "error", + "no-iterator": "error", + "no-label-var": "error", + "no-labels": "error", + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-mixed-spaces-and-tabs": ["error", false], + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-multiple-empty-lines": [ + "error", + { + max: 2, + maxBOF: 0, + maxEOF: 0 + } + ], + "no-nested-ternary": "error", + "no-new": "error", + "no-new-func": "error", + "no-new-object": "error", + "no-new-wrappers": "error", + "no-octal-escape": "error", + "no-param-reassign": "error", + "no-proto": "error", + "no-process-exit": "off", + "no-restricted-properties": [ + "error", + { + property: "substring", + message: "Use .slice instead of .substring." + }, + { + property: "substr", + message: "Use .slice instead of .substr." + }, + { + object: "assert", + property: "equal", + message: "Use assert.strictEqual instead of assert.equal." + }, + { + object: "assert", + property: "notEqual", + message: + "Use assert.notStrictEqual instead of assert.notEqual." + }, + { + object: "assert", + property: "deepEqual", + message: + "Use assert.deepStrictEqual instead of assert.deepEqual." + }, + { + object: "assert", + property: "notDeepEqual", + message: + "Use assert.notDeepStrictEqual instead of assert.notDeepEqual." + } + ], + "no-return-assign": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-shadow": "error", + "no-tabs": "error", + "no-throw-literal": "error", + "no-trailing-spaces": "error", + "no-undef": [ + "error", + { + typeof: true + } + ], + "no-undef-init": "error", + "no-undefined": "error", + "no-underscore-dangle": [ + "error", + { + allowAfterThis: true + } + ], + "no-unmodified-loop-condition": "error", + "no-unneeded-ternary": "error", + "no-unreachable-loop": "error", + "no-unused-expressions": "error", + "no-unused-vars": [ + "error", + { + vars: "all", + args: "after-used", + caughtErrors: "all" + } + ], + "no-use-before-define": "error", + "no-useless-call": "error", + "no-useless-computed-key": "error", + "no-useless-concat": "error", + "no-useless-constructor": "error", + "no-useless-rename": "error", + "no-useless-return": "error", + "no-whitespace-before-property": "error", + "no-var": "error", + "n/callback-return": ["error", ["cb", "callback", "next"]], + "n/handle-callback-err": ["error", "err"], + "n/no-deprecated-api": "error", + "n/no-mixed-requires": "error", + "n/no-new-require": "error", + "n/no-path-concat": "error", + "object-curly-newline": [ + "error", + { + consistent: true, + multiline: true + } + ], + "object-curly-spacing": ["error", "always"], + "object-property-newline": [ + "error", + { + allowAllPropertiesOnSameLine: true + } + ], + "object-shorthand": [ + "error", + "always", + { + avoidExplicitReturnArrows: true + } + ], + "one-var-declaration-per-line": "error", + "operator-assignment": "error", + "operator-linebreak": "error", + "padding-line-between-statements": [ + "error", + { + blankLine: "always", + prev: ["const", "let", "var"], + next: "*" + }, + { + blankLine: "any", + prev: ["const", "let", "var"], + next: ["const", "let", "var"] + } + ], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-exponentiation-operator": "error", + "prefer-numeric-literals": "error", + "prefer-promise-reject-errors": "error", + "prefer-regex-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + quotes: [ + "error", + "double", + { + avoidEscape: true + } + ], + "quote-props": ["error", "as-needed"], + radix: "error", + "require-unicode-regexp": "error", + "rest-spread-spacing": "error", + semi: "error", + "semi-spacing": [ + "error", + { + before: false, + after: true + } + ], + "semi-style": "error", + "space-before-blocks": "error", + "space-before-function-paren": [ + "error", + { + anonymous: "never", + named: "never", + asyncArrow: "always" + } + ], + "space-in-parens": "error", + "space-infix-ops": "error", + "space-unary-ops": [ + "error", + { + words: true, + nonwords: false + } + ], + "spaced-comment": [ + "error", + "always", + { + exceptions: ["-"] + } + ], + strict: ["error", "global"], + "switch-colon-spacing": "error", + "symbol-description": "error", + "template-curly-spacing": ["error", "never"], + "template-tag-spacing": "error", + "unicode-bom": "error", + "unicorn/prefer-array-find": "error", + "unicorn/prefer-array-flat-map": "error", + "unicorn/prefer-array-flat": "error", + "unicorn/prefer-array-index-of": "error", + "unicorn/prefer-array-some": "error", + "unicorn/prefer-includes": "error", + "unicorn/prefer-set-has": "error", + "unicorn/prefer-string-slice": "error", + "unicorn/prefer-string-starts-ends-with": "error", + "unicorn/prefer-string-trim-start-end": "error", + "wrap-iife": "error", + "yield-star-spacing": "error", + yoda: [ + "error", + "never", + { + exceptRange: true + } + ] + } +}; diff --git a/packages/eslint-config-eslint/index.js b/packages/eslint-config-eslint/index.js index 1304f34a870..8717218efe7 100644 --- a/packages/eslint-config-eslint/index.js +++ b/packages/eslint-config-eslint/index.js @@ -1,9 +1,16 @@ -/** - * @fileoverview Index file to allow YAML file to be loaded - * @author Teddy Katz - */ "use strict"; -module.exports = { - extends: ["./default.yml"] -}; +const baseConfigs = require("./base"); +const { esmConfigs, cjsConfigs } = require("./nodejs"); + +module.exports = [ + ...baseConfigs, + ...esmConfigs.map(config => ({ + files: ["**/*.js"], + ...config + })), + ...cjsConfigs.map(config => ({ + files: ["**/*.cjs"], + ...config + })) +]; diff --git a/packages/eslint-config-eslint/nodejs.js b/packages/eslint-config-eslint/nodejs.js new file mode 100644 index 00000000000..368c67dc86b --- /dev/null +++ b/packages/eslint-config-eslint/nodejs.js @@ -0,0 +1,33 @@ +"use strict"; + +const recommendedScriptConfig = require("eslint-plugin-n/configs/recommended-script"); +const recommendedModuleConfig = require("eslint-plugin-n/configs/recommended-module"); + +const sharedRules = { + "n/callback-return": ["error", ["cb", "callback", "next"]], + "n/handle-callback-err": ["error", "err"] +}; + +const cjsConfigs = [ + recommendedScriptConfig, + { + rules: { + ...sharedRules, + "n/no-mixed-requires": "error", + "n/no-new-require": "error", + "n/no-path-concat": "error" + } + } +]; + +const esmConfigs = [ + recommendedModuleConfig, + { + rules: sharedRules + } +]; + +module.exports = { + cjsConfigs, + esmConfigs +}; diff --git a/packages/eslint-config-eslint/package.json b/packages/eslint-config-eslint/package.json index 00c10811fb8..f8c7fab8768 100644 --- a/packages/eslint-config-eslint/package.json +++ b/packages/eslint-config-eslint/package.json @@ -1,17 +1,25 @@ { "name": "eslint-config-eslint", - "version": "7.0.0", + "version": "9.0.0", "author": "Nicholas C. Zakas ", "description": "Default ESLint configuration for ESLint projects.", + "exports": { + "./package.json": "./package.json", + ".": "./index.js", + "./cjs": "./cjs.js", + "./eslintrc": "./eslintrc.js" + }, "scripts": { - "test": "node ./index.js", + "test": "node ./index.js && node ./cjs.js", "prepublish": "npm test" }, "files": [ "LICENSE", "README.md", + "base.js", + "cjs.js", "index.js", - "default.yml" + "nodejs.js" ], "repository": { "type": "git", @@ -19,11 +27,12 @@ }, "homepage": "https://eslint.org", "bugs": "https://github.com/eslint/eslint/issues/", - "peerDependencies": { - "eslint-plugin-eslint-comments": ">=3.2.0", - "eslint-plugin-jsdoc": ">=38.1.6", - "eslint-plugin-n": ">=15.2.4", - "eslint-plugin-unicorn": ">=42.0.0" + "dependencies": { + "@eslint/js": "^8.42.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-jsdoc": "^46.5.1", + "eslint-plugin-n": "^16.0.0", + "eslint-plugin-unicorn": "^42.0.0" }, "keywords": [ "eslintconfig", @@ -32,6 +41,6 @@ ], "license": "MIT", "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } } diff --git a/packages/js/package.json b/packages/js/package.json index 5d65d79b78c..529c630dd99 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/js", - "version": "8.41.0", + "version": "8.53.0", "description": "ESLint JavaScript language implementation", "main": "./src/index.js", "scripts": {}, diff --git a/packages/js/src/configs/eslint-all.js b/packages/js/src/configs/eslint-all.js index f3a415cce5c..f2f7a664af6 100644 --- a/packages/js/src/configs/eslint-all.js +++ b/packages/js/src/configs/eslint-all.js @@ -9,24 +9,13 @@ module.exports = Object.freeze({ "rules": { "accessor-pairs": "error", - "array-bracket-newline": "error", - "array-bracket-spacing": "error", "array-callback-return": "error", - "array-element-newline": "error", "arrow-body-style": "error", - "arrow-parens": "error", - "arrow-spacing": "error", "block-scoped-var": "error", - "block-spacing": "error", - "brace-style": "error", "camelcase": "error", "capitalized-comments": "error", "class-methods-use-this": "error", - "comma-dangle": "error", - "comma-spacing": "error", - "comma-style": "error", "complexity": "error", - "computed-property-spacing": "error", "consistent-return": "error", "consistent-this": "error", "constructor-super": "error", @@ -34,49 +23,30 @@ module.exports = Object.freeze({ "default-case": "error", "default-case-last": "error", "default-param-last": "error", - "dot-location": "error", "dot-notation": "error", - "eol-last": "error", "eqeqeq": "error", "for-direction": "error", - "func-call-spacing": "error", "func-name-matching": "error", "func-names": "error", "func-style": "error", - "function-call-argument-newline": "error", - "function-paren-newline": "error", - "generator-star-spacing": "error", "getter-return": "error", "grouped-accessor-pairs": "error", "guard-for-in": "error", "id-denylist": "error", "id-length": "error", "id-match": "error", - "implicit-arrow-linebreak": "error", - "indent": "error", "init-declarations": "error", - "jsx-quotes": "error", - "key-spacing": "error", - "keyword-spacing": "error", "line-comment-position": "error", - "linebreak-style": "error", - "lines-around-comment": "error", - "lines-between-class-members": "error", "logical-assignment-operators": "error", "max-classes-per-file": "error", "max-depth": "error", - "max-len": "error", "max-lines": "error", "max-lines-per-function": "error", "max-nested-callbacks": "error", "max-params": "error", "max-statements": "error", - "max-statements-per-line": "error", "multiline-comment-style": "error", - "multiline-ternary": "error", "new-cap": "error", - "new-parens": "error", - "newline-per-chained-call": "error", "no-alert": "error", "no-array-constructor": "error", "no-async-promise-executor": "error", @@ -87,7 +57,6 @@ module.exports = Object.freeze({ "no-class-assign": "error", "no-compare-neg-zero": "error", "no-cond-assign": "error", - "no-confusing-arrow": "error", "no-console": "error", "no-const-assign": "error", "no-constant-binary-expression": "error", @@ -117,10 +86,7 @@ module.exports = Object.freeze({ "no-extra-bind": "error", "no-extra-boolean-cast": "error", "no-extra-label": "error", - "no-extra-parens": "error", - "no-extra-semi": "error", "no-fallthrough": "error", - "no-floating-decimal": "error", "no-func-assign": "error", "no-global-assign": "error", "no-implicit-coercion": "error", @@ -141,22 +107,18 @@ module.exports = Object.freeze({ "no-loss-of-precision": "error", "no-magic-numbers": "error", "no-misleading-character-class": "error", - "no-mixed-operators": "error", - "no-mixed-spaces-and-tabs": "error", "no-multi-assign": "error", - "no-multi-spaces": "error", "no-multi-str": "error", - "no-multiple-empty-lines": "error", "no-negated-condition": "error", "no-nested-ternary": "error", "no-new": "error", "no-new-func": "error", "no-new-native-nonconstructor": "error", - "no-new-object": "error", "no-new-symbol": "error", "no-new-wrappers": "error", "no-nonoctal-decimal-escape": "error", "no-obj-calls": "error", + "no-object-constructor": "error", "no-octal": "error", "no-octal-escape": "error", "no-param-reassign": "error", @@ -172,7 +134,6 @@ module.exports = Object.freeze({ "no-restricted-properties": "error", "no-restricted-syntax": "error", "no-return-assign": "error", - "no-return-await": "error", "no-script-url": "error", "no-self-assign": "error", "no-self-compare": "error", @@ -181,12 +142,10 @@ module.exports = Object.freeze({ "no-shadow": "error", "no-shadow-restricted-names": "error", "no-sparse-arrays": "error", - "no-tabs": "error", "no-template-curly-in-string": "error", "no-ternary": "error", "no-this-before-super": "error", "no-throw-literal": "error", - "no-trailing-spaces": "error", "no-undef": "error", "no-undef-init": "error", "no-undefined": "error", @@ -216,19 +175,10 @@ module.exports = Object.freeze({ "no-var": "error", "no-void": "error", "no-warning-comments": "error", - "no-whitespace-before-property": "error", "no-with": "error", - "nonblock-statement-body-position": "error", - "object-curly-newline": "error", - "object-curly-spacing": "error", - "object-property-newline": "error", "object-shorthand": "error", "one-var": "error", - "one-var-declaration-per-line": "error", "operator-assignment": "error", - "operator-linebreak": "error", - "padded-blocks": "error", - "padding-line-between-statements": "error", "prefer-arrow-callback": "error", "prefer-const": "error", "prefer-destructuring": "error", @@ -242,38 +192,20 @@ module.exports = Object.freeze({ "prefer-rest-params": "error", "prefer-spread": "error", "prefer-template": "error", - "quote-props": "error", - "quotes": "error", "radix": "error", "require-atomic-updates": "error", "require-await": "error", "require-unicode-regexp": "error", "require-yield": "error", - "rest-spread-spacing": "error", - "semi": "error", - "semi-spacing": "error", - "semi-style": "error", "sort-imports": "error", "sort-keys": "error", "sort-vars": "error", - "space-before-blocks": "error", - "space-before-function-paren": "error", - "space-in-parens": "error", - "space-infix-ops": "error", - "space-unary-ops": "error", - "spaced-comment": "error", "strict": "error", - "switch-colon-spacing": "error", "symbol-description": "error", - "template-curly-spacing": "error", - "template-tag-spacing": "error", "unicode-bom": "error", "use-isnan": "error", "valid-typeof": "error", "vars-on-top": "error", - "wrap-iife": "error", - "wrap-regex": "error", - "yield-star-spacing": "error", "yoda": "error" } }); diff --git a/templates/formatter-examples.md.ejs b/templates/formatter-examples.md.ejs index 20f46f2d5aa..e18eb18995f 100644 --- a/templates/formatter-examples.md.ejs +++ b/templates/formatter-examples.md.ejs @@ -14,7 +14,7 @@ You can specify a formatter using the `--format` or `-f` flag in the CLI. For ex The built-in formatter options are: -<% Object.keys(formatterResults).forEach(function(formatterName) { -%> +<% Object.keys(formatterResults).forEach(formatterName => { -%> * [<%= formatterName %>](#<%= formatterName %>) <% }) -%> @@ -56,19 +56,36 @@ npx eslint --format fullOfProblems.js ``` ## Built-In Formatter Options -<% Object.keys(formatterResults).forEach(function(formatterName) { -%> +<% Object.keys(formatterResults).forEach(formatterName => { -%> ### <%= formatterName %> <%= formatterResults[formatterName].description %> +<% if (formatterName !== "html") { -%> +<% + let codeFormat = "text"; + let output = formatterResults[formatterName].result; + let outputNote = "Example output:"; -Example output: + if (output.startsWith("\u003C?xml")) { + codeFormat = "xml"; + } -<% if (formatterName !== "html") { -%> -```text -<%- formatterResults[formatterName].result %> + if (formatterName.includes("json")) { + codeFormat = "json"; + output = JSON.stringify(JSON.parse(output), null, 4); + outputNote = "Example output (formatted for easier reading):"; + } +%> +<%= outputNote %> + +```<%= codeFormat %> +<%- output %> ``` <% } else {-%> + +Example output: + <% } -%> <% }) -%> diff --git a/tests/bin/eslint.js b/tests/bin/eslint.js index 848dba14671..0622d48e973 100644 --- a/tests/bin/eslint.js +++ b/tests/bin/eslint.js @@ -111,7 +111,12 @@ describe("bin/eslint.js", () => { fixableErrorCount: 0, fixableWarningCount: 0, output: "var foo = bar;\n", - usedDeprecatedRules: [] + usedDeprecatedRules: [ + { + ruleId: "no-extra-semi", + replacedBy: [] + } + ] } ]); @@ -204,10 +209,10 @@ describe("bin/eslint.js", () => { }); describe("running on files", () => { - it("has exit code 0 if no linting errors occur", () => assertExitCode(runESLint(["bin/eslint.js"]), 0)); + it("has exit code 0 if no linting errors occur", () => assertExitCode(runESLint(["bin/eslint.js", "--no-config-lookup"]), 0)); it("has exit code 0 if a linting warning is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--no-config-lookup", "--rule", "semi: [1, never]"]), 0)); it("has exit code 1 if a linting error is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--no-config-lookup", "--rule", "semi: [2, never]"]), 1)); - it("has exit code 1 if a syntax error is thrown", () => assertExitCode(runESLint(["tests/fixtures/exit-on-fatal-error/fatal-error.js", "--no-ignore"]), 1)); + it("has exit code 1 if a syntax error is thrown", () => assertExitCode(runESLint(["tests/fixtures/exit-on-fatal-error/fatal-error.js", "--no-config-lookup"]), 1)); }); describe("automatically fixing files", () => { @@ -359,7 +364,7 @@ describe("bin/eslint.js", () => { describe("handling crashes", () => { it("prints the error message to stderr in the event of a crash", () => { - const child = runESLint(["--rule=no-restricted-syntax:[error, 'Invalid Selector [[[']", "Makefile.js"]); + const child = runESLint(["--rule=no-restricted-syntax:[error, 'Invalid Selector [[[']", "--no-config-lookup", "Makefile.js"]); const exitCodeAssertion = assertExitCode(child, 2); const outputAssertion = getOutput(child).then(output => { const expectedSubstring = "Syntax error in selector"; @@ -372,7 +377,7 @@ describe("bin/eslint.js", () => { }); it("prints the error message exactly once to stderr in the event of a crash", () => { - const child = runESLint(["--rule=no-restricted-syntax:[error, 'Invalid Selector [[[']", "Makefile.js"]); + const child = runESLint(["--rule=no-restricted-syntax:[error, 'Invalid Selector [[[']", "--no-config-lookup", "Makefile.js"]); const exitCodeAssertion = assertExitCode(child, 2); const outputAssertion = getOutput(child).then(output => { const expectedSubstring = "Syntax error in selector"; @@ -387,6 +392,54 @@ describe("bin/eslint.js", () => { return Promise.all([exitCodeAssertion, outputAssertion]); }); + it("does not exit with zero when there is an error in the next tick", () => { + const config = path.join(__dirname, "../fixtures/bin/eslint.config-promise-tick-throws.js"); + const file = path.join(__dirname, "../fixtures/bin/empty.js"); + const child = runESLint(["--config", config, file]); + const exitCodeAssertion = assertExitCode(child, 2); + const outputAssertion = getOutput(child).then(output => { + + // ensure the expected error was printed + assert.include(output.stderr, "test_error_stack"); + + // ensure that linting the file did not cause an error + assert.notInclude(output.stderr, "empty.js"); + assert.notInclude(output.stdout, "empty.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/conf/eslint-all.js b/tests/conf/eslint-all.js index abee0c69b16..6475965a02a 100644 --- a/tests/conf/eslint-all.js +++ b/tests/conf/eslint-all.js @@ -31,7 +31,7 @@ describe("eslint-all", () => { const someRule = "yoda"; assert.include(ruleNames, someRule); - assert.isAbove(count, 200); + assert.isBelow(count, 200); }); it("should configure all rules as errors", () => { diff --git a/tests/fixtures/bin/empty.js b/tests/fixtures/bin/empty.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/bin/eslint.config-invalid.js b/tests/fixtures/bin/eslint.config-invalid.js new file mode 100644 index 00000000000..6f68e178419 --- /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-promise-tick-throws.js b/tests/fixtures/bin/eslint.config-promise-tick-throws.js new file mode 100644 index 00000000000..6ea53d25386 --- /dev/null +++ b/tests/fixtures/bin/eslint.config-promise-tick-throws.js @@ -0,0 +1,22 @@ +function throwError() { + const error = new Error(); + error.stack = "test_error_stack"; + throw error; +} + +process.nextTick(throwError); + +function delay(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function getConfig() { + await delay(100); + return []; +} + +/* + * Exporting the config as an initially unsettled Promise should ensure that + * the error in next tick will be thrown before any linting is done + */ +module.exports = getConfig(); 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 00000000000..c72f86a840f --- /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/cli-engine/node_modules_cleaner.js b/tests/fixtures/cli-engine/node_modules_cleaner.js new file mode 100644 index 00000000000..ca451e212a7 --- /dev/null +++ b/tests/fixtures/cli-engine/node_modules_cleaner.js @@ -0,0 +1 @@ +// not implemented 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 8bb52f02239..09ca58fade5 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/if-4.js b/tests/fixtures/code-path-analysis/if-4.js index def278a43c4..6229eddcbd0 100644 --- a/tests/fixtures/code-path-analysis/if-4.js +++ b/tests/fixtures/code-path-analysis/if-4.js @@ -16,7 +16,7 @@ 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]; - thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize]; + thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize=true]; s1_1[label="Program\nIfStatement\nIdentifier (a)"]; s1_2[label="BlockStatement\nReturnStatement\nLiteral (0)"]; s1_3[label="BlockStatement:exit"]; 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 00000000000..5ce3853a240 --- /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 4863ac81db3..427cc22ec5a 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/fixtures/code-path-analysis/try--try-catch-2.js b/tests/fixtures/code-path-analysis/try--try-catch-2.js index e6e86b4a4f3..d9b97979969 100644 --- a/tests/fixtures/code-path-analysis/try--try-catch-2.js +++ b/tests/fixtures/code-path-analysis/try--try-catch-2.js @@ -16,7 +16,7 @@ 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]; - thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize]; + thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize=true]; s1_1[label="Program\nTryStatement\nBlockStatement\nExpressionStatement\nCallExpression\nIdentifier (foo)"]; s1_2[label="CallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"]; s1_3[label="CatchClause\nIdentifier (err)\nBlockStatement\nThrowStatement\nIdentifier (err)"]; diff --git a/tests/fixtures/code-path-analysis/try--try-catch-4.js b/tests/fixtures/code-path-analysis/try--try-catch-4.js index b96d220a0c5..cb9f4b2d883 100644 --- a/tests/fixtures/code-path-analysis/try--try-catch-4.js +++ b/tests/fixtures/code-path-analysis/try--try-catch-4.js @@ -29,7 +29,7 @@ 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]; - thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize]; + thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize=true]; s1_1[label="Program\nTryStatement\nBlockStatement\nTryStatement\nBlockStatement\nIfStatement\nIdentifier (a)"]; s1_3[label="BlockStatement\nThrowStatement\nIdentifier (err)"]; s1_4[style="rounded,dashed,filled",fillcolor="#FF9800",label="<>\nBlockStatement:exit"]; diff --git a/tests/fixtures/code-path-analysis/try--try-finally-1.js b/tests/fixtures/code-path-analysis/try--try-finally-1.js index dcaa74d3dcb..94374220394 100644 --- a/tests/fixtures/code-path-analysis/try--try-finally-1.js +++ b/tests/fixtures/code-path-analysis/try--try-finally-1.js @@ -19,7 +19,7 @@ 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]; - thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize]; + thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize=true]; s1_1[label="Program\nTryStatement\nBlockStatement\nExpressionStatement\nCallExpression\nIdentifier (foo)"]; s1_2[label="CallExpression:exit\nExpressionStatement:exit\nBlockStatement:exit"]; s1_3[label="BlockStatement\nExpressionStatement\nCallExpression\nIdentifier (bar)\nExpressionStatement\nCallExpression\nIdentifier (last)"]; diff --git a/tests/fixtures/code-path-analysis/try--try-finally-3.js b/tests/fixtures/code-path-analysis/try--try-finally-3.js index b141013076a..7ecc766c450 100644 --- a/tests/fixtures/code-path-analysis/try--try-finally-3.js +++ b/tests/fixtures/code-path-analysis/try--try-finally-3.js @@ -17,7 +17,7 @@ last(); digraph { node[shape=box,style="rounded,filled",fillcolor=white]; initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25]; - thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize]; + thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize=true]; s1_1[label="Program\nTryStatement\nBlockStatement\nThrowStatement\nIdentifier (err)"]; s1_2[label="ThrowStatement:exit"]; s1_3[style="rounded,dashed,filled",fillcolor="#FF9800",label="<>\nBlockStatement:exit"]; diff --git a/tests/fixtures/code-path-analysis/try--try-finally-4.js b/tests/fixtures/code-path-analysis/try--try-finally-4.js index 8fb73a39f80..4408e1a584d 100644 --- a/tests/fixtures/code-path-analysis/try--try-finally-4.js +++ b/tests/fixtures/code-path-analysis/try--try-finally-4.js @@ -32,7 +32,7 @@ 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]; - thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize]; + thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize=true]; s1_1[label="Program\nTryStatement\nBlockStatement\nTryStatement\nBlockStatement\nIfStatement\nIdentifier (a)"]; s1_3[label="BlockStatement\nReturnStatement"]; s1_4[style="rounded,dashed,filled",fillcolor="#FF9800",label="<>\nBlockStatement:exit"]; diff --git a/tests/fixtures/code-path-analysis/try--try-finally-5.js b/tests/fixtures/code-path-analysis/try--try-finally-5.js index 0081ed8777e..ecf8a6ec5f5 100644 --- a/tests/fixtures/code-path-analysis/try--try-finally-5.js +++ b/tests/fixtures/code-path-analysis/try--try-finally-5.js @@ -31,7 +31,7 @@ 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]; - thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize]; + thrown[label="✘",shape=circle,width=0.3,height=0.3,fixedsize=true]; s1_1[label="Program\nTryStatement\nBlockStatement\nTryStatement\nBlockStatement\nIfStatement\nIdentifier (a)"]; s1_3[label="BlockStatement\nReturnStatement"]; s1_4[style="rounded,dashed,filled",fillcolor="#FF9800",label="<>\nBlockStatement:exit"]; diff --git a/tests/fixtures/example-app3/.eslintignore b/tests/fixtures/example-app3/.eslintignore new file mode 100644 index 00000000000..e5999645160 --- /dev/null +++ b/tests/fixtures/example-app3/.eslintignore @@ -0,0 +1 @@ +src/dist diff --git a/tests/fixtures/example-app3/.eslintrc.js b/tests/fixtures/example-app3/.eslintrc.js new file mode 100644 index 00000000000..86a595d362c --- /dev/null +++ b/tests/fixtures/example-app3/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + parserOptions: { + ecmaVersion: 2020, + }, + 'root': true, + 'rules': { + 'semi': 'error', + }, + }; diff --git a/tests/fixtures/example-app3/eslint.config.js b/tests/fixtures/example-app3/eslint.config.js new file mode 100644 index 00000000000..7f94a016cdc --- /dev/null +++ b/tests/fixtures/example-app3/eslint.config.js @@ -0,0 +1,10 @@ +module.exports = [ + { + ignores: ["src/dist"] + }, + { + rules: { + semi: "error" + } + } +]; diff --git a/tests/fixtures/example-app3/node_modules/eslint-formatter-cwd/index.js b/tests/fixtures/example-app3/node_modules/eslint-formatter-cwd/index.js new file mode 100644 index 00000000000..ca853b3a44d --- /dev/null +++ b/tests/fixtures/example-app3/node_modules/eslint-formatter-cwd/index.js @@ -0,0 +1 @@ +module.exports = (_, { cwd }) => cwd; diff --git a/tests/fixtures/example-app3/node_modules/eslint-formatter-cwd/package.json b/tests/fixtures/example-app3/node_modules/eslint-formatter-cwd/package.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/tests/fixtures/example-app3/node_modules/eslint-formatter-cwd/package.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/example-app3/node_modules/eslint-plugin-test/index.js b/tests/fixtures/example-app3/node_modules/eslint-plugin-test/index.js new file mode 100644 index 00000000000..50d288c6101 --- /dev/null +++ b/tests/fixtures/example-app3/node_modules/eslint-plugin-test/index.js @@ -0,0 +1,19 @@ +module.exports = { + rules: { + "report-cwd": { + meta: { + schema: [] + }, + create(context) { + return { + Program(node) { + context.report({ + node, + message: context.cwd + }); + } + } + } + } + } +} diff --git a/tests/fixtures/example-app3/node_modules/eslint-plugin-test/package.json b/tests/fixtures/example-app3/node_modules/eslint-plugin-test/package.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/tests/fixtures/example-app3/node_modules/eslint-plugin-test/package.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/example-app3/src/1.js b/tests/fixtures/example-app3/src/1.js new file mode 100644 index 00000000000..e14c4f2a956 --- /dev/null +++ b/tests/fixtures/example-app3/src/1.js @@ -0,0 +1 @@ +console.log(1) diff --git a/tests/fixtures/example-app3/src/dist/2.js b/tests/fixtures/example-app3/src/dist/2.js new file mode 100644 index 00000000000..32926e25ddb --- /dev/null +++ b/tests/fixtures/example-app3/src/dist/2.js @@ -0,0 +1 @@ +console.log(2) diff --git a/tests/fixtures/parsers/typescript-parsers/boolean-cast-with-assertion.js b/tests/fixtures/parsers/typescript-parsers/boolean-cast-with-assertion.js new file mode 100644 index 00000000000..caecf5c9191 --- /dev/null +++ b/tests/fixtures/parsers/typescript-parsers/boolean-cast-with-assertion.js @@ -0,0 +1,333 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser 5.59.11 (TS 5.1.3) + * Source code: + * if (!Boolean(a as any)) { } + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "IfStatement", + test: { + type: "UnaryExpression", + operator: "!", + prefix: true, + argument: { + type: "CallExpression", + callee: { + type: "Identifier", + decorators: [], + name: "Boolean", + optional: false, + range: [5, 12], + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 12, + }, + }, + }, + arguments: [ + { + type: "TSAsExpression", + expression: { + type: "Identifier", + decorators: [], + name: "a", + optional: false, + range: [13, 14], + loc: { + start: { + line: 1, + column: 13, + }, + end: { + line: 1, + column: 14, + }, + }, + }, + typeAnnotation: { + type: "TSAnyKeyword", + range: [18, 21], + loc: { + start: { + line: 1, + column: 18, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + range: [13, 21], + loc: { + start: { + line: 1, + column: 13, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + ], + optional: false, + range: [5, 22], + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 22, + }, + }, + }, + range: [4, 22], + loc: { + start: { + line: 1, + column: 4, + }, + end: { + line: 1, + column: 22, + }, + }, + }, + consequent: { + type: "BlockStatement", + body: [], + range: [24, 27], + loc: { + start: { + line: 1, + column: 24, + }, + end: { + line: 1, + column: 27, + }, + }, + }, + alternate: null, + range: [0, 27], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 27, + }, + }, + }, + ], + comments: [], + range: [0, 27], + sourceType: "script", + tokens: [ + { + type: "Keyword", + value: "if", + range: [0, 2], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 2, + }, + }, + }, + { + type: "Punctuator", + value: "(", + range: [3, 4], + loc: { + start: { + line: 1, + column: 3, + }, + end: { + line: 1, + column: 4, + }, + }, + }, + { + type: "Punctuator", + value: "!", + range: [4, 5], + loc: { + start: { + line: 1, + column: 4, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + { + type: "Identifier", + value: "Boolean", + range: [5, 12], + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 12, + }, + }, + }, + { + type: "Punctuator", + value: "(", + range: [12, 13], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + { + type: "Identifier", + value: "a", + range: [13, 14], + loc: { + start: { + line: 1, + column: 13, + }, + end: { + line: 1, + column: 14, + }, + }, + }, + { + type: "Identifier", + value: "as", + range: [15, 17], + loc: { + start: { + line: 1, + column: 15, + }, + end: { + line: 1, + column: 17, + }, + }, + }, + { + type: "Identifier", + value: "any", + range: [18, 21], + loc: { + start: { + line: 1, + column: 18, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + { + type: "Punctuator", + value: ")", + range: [21, 22], + loc: { + start: { + line: 1, + column: 21, + }, + end: { + line: 1, + column: 22, + }, + }, + }, + { + type: "Punctuator", + value: ")", + range: [22, 23], + loc: { + start: { + line: 1, + column: 22, + }, + end: { + line: 1, + column: 23, + }, + }, + }, + { + type: "Punctuator", + value: "{", + range: [24, 25], + loc: { + start: { + line: 1, + column: 24, + }, + end: { + line: 1, + column: 25, + }, + }, + }, + { + type: "Punctuator", + value: "}", + range: [26, 27], + loc: { + start: { + line: 1, + column: 26, + }, + end: { + line: 1, + column: 27, + }, + }, + }, + ], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 27, + }, + }, +}); diff --git a/tests/fixtures/parsers/typescript-parsers/exponentiation-with-assertion-1.js b/tests/fixtures/parsers/typescript-parsers/exponentiation-with-assertion-1.js new file mode 100644 index 00000000000..7921af52872 --- /dev/null +++ b/tests/fixtures/parsers/typescript-parsers/exponentiation-with-assertion-1.js @@ -0,0 +1,321 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser 5.59.11 (TS 5.1.3) + * Source code: + * Math.pow(a, b as any) + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "CallExpression", + callee: { + type: "MemberExpression", + object: { + type: "Identifier", + decorators: [], + name: "Math", + optional: false, + range: [0, 4], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 4, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "pow", + optional: false, + range: [5, 8], + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 8, + }, + }, + }, + computed: false, + optional: false, + range: [0, 8], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 8, + }, + }, + }, + arguments: [ + { + type: "Identifier", + decorators: [], + name: "a", + optional: false, + range: [9, 10], + loc: { + start: { + line: 1, + column: 9, + }, + end: { + line: 1, + column: 10, + }, + }, + }, + { + type: "TSAsExpression", + expression: { + type: "Identifier", + decorators: [], + name: "b", + optional: false, + range: [12, 13], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + typeAnnotation: { + type: "TSAnyKeyword", + range: [17, 20], + loc: { + start: { + line: 1, + column: 17, + }, + end: { + line: 1, + column: 20, + }, + }, + }, + range: [12, 20], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 20, + }, + }, + }, + ], + optional: false, + range: [0, 21], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + range: [0, 21], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + ], + comments: [], + range: [0, 21], + sourceType: "script", + tokens: [ + { + type: "Identifier", + value: "Math", + range: [0, 4], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 4, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [4, 5], + loc: { + start: { + line: 1, + column: 4, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + { + type: "Identifier", + value: "pow", + range: [5, 8], + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 8, + }, + }, + }, + { + type: "Punctuator", + value: "(", + range: [8, 9], + loc: { + start: { + line: 1, + column: 8, + }, + end: { + line: 1, + column: 9, + }, + }, + }, + { + type: "Identifier", + value: "a", + range: [9, 10], + loc: { + start: { + line: 1, + column: 9, + }, + end: { + line: 1, + column: 10, + }, + }, + }, + { + type: "Punctuator", + value: ",", + range: [10, 11], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 11, + }, + }, + }, + { + type: "Identifier", + value: "b", + range: [12, 13], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + { + type: "Identifier", + value: "as", + range: [14, 16], + loc: { + start: { + line: 1, + column: 14, + }, + end: { + line: 1, + column: 16, + }, + }, + }, + { + type: "Identifier", + value: "any", + range: [17, 20], + loc: { + start: { + line: 1, + column: 17, + }, + end: { + line: 1, + column: 20, + }, + }, + }, + { + type: "Punctuator", + value: ")", + range: [20, 21], + loc: { + start: { + line: 1, + column: 20, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + ], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 21, + }, + }, +}); diff --git a/tests/fixtures/parsers/typescript-parsers/exponentiation-with-assertion-2.js b/tests/fixtures/parsers/typescript-parsers/exponentiation-with-assertion-2.js new file mode 100644 index 00000000000..cbdfa49e142 --- /dev/null +++ b/tests/fixtures/parsers/typescript-parsers/exponentiation-with-assertion-2.js @@ -0,0 +1,321 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser 5.59.11 (TS 5.1.3) + * Source code: + * Math.pow(a as any, b) + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "CallExpression", + callee: { + type: "MemberExpression", + object: { + type: "Identifier", + decorators: [], + name: "Math", + optional: false, + range: [0, 4], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 4, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "pow", + optional: false, + range: [5, 8], + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 8, + }, + }, + }, + computed: false, + optional: false, + range: [0, 8], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 8, + }, + }, + }, + arguments: [ + { + type: "TSAsExpression", + expression: { + type: "Identifier", + decorators: [], + name: "a", + optional: false, + range: [9, 10], + loc: { + start: { + line: 1, + column: 9, + }, + end: { + line: 1, + column: 10, + }, + }, + }, + typeAnnotation: { + type: "TSAnyKeyword", + range: [14, 17], + loc: { + start: { + line: 1, + column: 14, + }, + end: { + line: 1, + column: 17, + }, + }, + }, + range: [9, 17], + loc: { + start: { + line: 1, + column: 9, + }, + end: { + line: 1, + column: 17, + }, + }, + }, + { + type: "Identifier", + decorators: [], + name: "b", + optional: false, + range: [19, 20], + loc: { + start: { + line: 1, + column: 19, + }, + end: { + line: 1, + column: 20, + }, + }, + }, + ], + optional: false, + range: [0, 21], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + range: [0, 21], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + ], + comments: [], + range: [0, 21], + sourceType: "script", + tokens: [ + { + type: "Identifier", + value: "Math", + range: [0, 4], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 4, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [4, 5], + loc: { + start: { + line: 1, + column: 4, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + { + type: "Identifier", + value: "pow", + range: [5, 8], + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 8, + }, + }, + }, + { + type: "Punctuator", + value: "(", + range: [8, 9], + loc: { + start: { + line: 1, + column: 8, + }, + end: { + line: 1, + column: 9, + }, + }, + }, + { + type: "Identifier", + value: "a", + range: [9, 10], + loc: { + start: { + line: 1, + column: 9, + }, + end: { + line: 1, + column: 10, + }, + }, + }, + { + type: "Identifier", + value: "as", + range: [11, 13], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + { + type: "Identifier", + value: "any", + range: [14, 17], + loc: { + start: { + line: 1, + column: 14, + }, + end: { + line: 1, + column: 17, + }, + }, + }, + { + type: "Punctuator", + value: ",", + range: [17, 18], + loc: { + start: { + line: 1, + column: 17, + }, + end: { + line: 1, + column: 18, + }, + }, + }, + { + type: "Identifier", + value: "b", + range: [19, 20], + loc: { + start: { + line: 1, + column: 19, + }, + end: { + line: 1, + column: 20, + }, + }, + }, + { + type: "Punctuator", + value: ")", + range: [20, 21], + loc: { + start: { + line: 1, + column: 20, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + ], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 21, + }, + }, +}); diff --git a/tests/fixtures/parsers/typescript-parsers/exponentiation-with-assertion-3.js b/tests/fixtures/parsers/typescript-parsers/exponentiation-with-assertion-3.js new file mode 100644 index 00000000000..422d8736ff3 --- /dev/null +++ b/tests/fixtures/parsers/typescript-parsers/exponentiation-with-assertion-3.js @@ -0,0 +1,321 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser 5.59.11 (TS 5.1.3) + * Source code: + * Math.pow(a, b) as any + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "TSAsExpression", + expression: { + type: "CallExpression", + callee: { + type: "MemberExpression", + object: { + type: "Identifier", + decorators: [], + name: "Math", + optional: false, + range: [0, 4], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 4, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "pow", + optional: false, + range: [5, 8], + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 8, + }, + }, + }, + computed: false, + optional: false, + range: [0, 8], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 8, + }, + }, + }, + arguments: [ + { + type: "Identifier", + decorators: [], + name: "a", + optional: false, + range: [9, 10], + loc: { + start: { + line: 1, + column: 9, + }, + end: { + line: 1, + column: 10, + }, + }, + }, + { + type: "Identifier", + decorators: [], + name: "b", + optional: false, + range: [12, 13], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + ], + optional: false, + range: [0, 14], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 14, + }, + }, + }, + typeAnnotation: { + type: "TSAnyKeyword", + range: [18, 21], + loc: { + start: { + line: 1, + column: 18, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + range: [0, 21], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + range: [0, 21], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + ], + comments: [], + range: [0, 21], + sourceType: "script", + tokens: [ + { + type: "Identifier", + value: "Math", + range: [0, 4], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 4, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [4, 5], + loc: { + start: { + line: 1, + column: 4, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + { + type: "Identifier", + value: "pow", + range: [5, 8], + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 8, + }, + }, + }, + { + type: "Punctuator", + value: "(", + range: [8, 9], + loc: { + start: { + line: 1, + column: 8, + }, + end: { + line: 1, + column: 9, + }, + }, + }, + { + type: "Identifier", + value: "a", + range: [9, 10], + loc: { + start: { + line: 1, + column: 9, + }, + end: { + line: 1, + column: 10, + }, + }, + }, + { + type: "Punctuator", + value: ",", + range: [10, 11], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 11, + }, + }, + }, + { + type: "Identifier", + value: "b", + range: [12, 13], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + { + type: "Punctuator", + value: ")", + range: [13, 14], + loc: { + start: { + line: 1, + column: 13, + }, + end: { + line: 1, + column: 14, + }, + }, + }, + { + type: "Identifier", + value: "as", + range: [15, 17], + loc: { + start: { + line: 1, + column: 15, + }, + end: { + line: 1, + column: 17, + }, + }, + }, + { + type: "Identifier", + value: "any", + range: [18, 21], + loc: { + start: { + line: 1, + column: 18, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + ], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 21, + }, + }, +}); diff --git a/tests/fixtures/parsers/typescript-parsers/logical-assignment-with-assertion.js b/tests/fixtures/parsers/typescript-parsers/logical-assignment-with-assertion.js new file mode 100644 index 00000000000..675559196e1 --- /dev/null +++ b/tests/fixtures/parsers/typescript-parsers/logical-assignment-with-assertion.js @@ -0,0 +1,209 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser 5.59.11 (TS 5.1.3) + * Source code: + * a ||= b as number; + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "AssignmentExpression", + operator: "||=", + left: { + type: "Identifier", + decorators: [], + name: "a", + optional: false, + range: [0, 1], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 1, + }, + }, + }, + right: { + type: "TSAsExpression", + expression: { + type: "Identifier", + decorators: [], + name: "b", + optional: false, + range: [6, 7], + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 7, + }, + }, + }, + typeAnnotation: { + type: "TSNumberKeyword", + range: [11, 17], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 17, + }, + }, + }, + range: [6, 17], + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 17, + }, + }, + }, + range: [0, 17], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 17, + }, + }, + }, + range: [0, 18], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 18, + }, + }, + }, + ], + comments: [], + range: [0, 18], + sourceType: "script", + tokens: [ + { + type: "Identifier", + value: "a", + range: [0, 1], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 1, + }, + }, + }, + { + type: "Punctuator", + value: "||=", + range: [2, 5], + loc: { + start: { + line: 1, + column: 2, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + { + type: "Identifier", + value: "b", + range: [6, 7], + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 7, + }, + }, + }, + { + type: "Identifier", + value: "as", + range: [8, 10], + loc: { + start: { + line: 1, + column: 8, + }, + end: { + line: 1, + column: 10, + }, + }, + }, + { + type: "Identifier", + value: "number", + range: [11, 17], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 17, + }, + }, + }, + { + type: "Punctuator", + value: ";", + range: [17, 18], + loc: { + start: { + line: 1, + column: 17, + }, + end: { + line: 1, + column: 18, + }, + }, + }, + ], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 18, + }, + }, +}); diff --git a/tests/fixtures/parsers/typescript-parsers/logical-with-assignment-with-assertion-1.js b/tests/fixtures/parsers/typescript-parsers/logical-with-assignment-with-assertion-1.js new file mode 100644 index 00000000000..914ff4d0c24 --- /dev/null +++ b/tests/fixtures/parsers/typescript-parsers/logical-with-assignment-with-assertion-1.js @@ -0,0 +1,538 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser 5.59.11 (TS 5.1.3) + * Source code: + * a.b.c || (a.b.c = d as number) + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "LogicalExpression", + operator: "||", + left: { + type: "MemberExpression", + object: { + type: "MemberExpression", + object: { + type: "Identifier", + decorators: [], + name: "a", + optional: false, + range: [0, 1], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 1, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "b", + optional: false, + range: [2, 3], + loc: { + start: { + line: 1, + column: 2, + }, + end: { + line: 1, + column: 3, + }, + }, + }, + computed: false, + optional: false, + range: [0, 3], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 3, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "c", + optional: false, + range: [4, 5], + loc: { + start: { + line: 1, + column: 4, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + computed: false, + optional: false, + range: [0, 5], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + right: { + type: "AssignmentExpression", + operator: "=", + left: { + type: "MemberExpression", + object: { + type: "MemberExpression", + object: { + type: "Identifier", + decorators: [], + name: "a", + optional: false, + range: [10, 11], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 11, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "b", + optional: false, + range: [12, 13], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + computed: false, + optional: false, + range: [10, 13], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "c", + optional: false, + range: [14, 15], + loc: { + start: { + line: 1, + column: 14, + }, + end: { + line: 1, + column: 15, + }, + }, + }, + computed: false, + optional: false, + range: [10, 15], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 15, + }, + }, + }, + right: { + type: "TSAsExpression", + expression: { + type: "Identifier", + decorators: [], + name: "d", + optional: false, + range: [18, 19], + loc: { + start: { + line: 1, + column: 18, + }, + end: { + line: 1, + column: 19, + }, + }, + }, + typeAnnotation: { + type: "TSNumberKeyword", + range: [23, 29], + loc: { + start: { + line: 1, + column: 23, + }, + end: { + line: 1, + column: 29, + }, + }, + }, + range: [18, 29], + loc: { + start: { + line: 1, + column: 18, + }, + end: { + line: 1, + column: 29, + }, + }, + }, + range: [10, 29], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 29, + }, + }, + }, + range: [0, 30], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 30, + }, + }, + }, + range: [0, 30], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 30, + }, + }, + }, + ], + comments: [], + range: [0, 30], + sourceType: "script", + tokens: [ + { + type: "Identifier", + value: "a", + range: [0, 1], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 1, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [1, 2], + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 2, + }, + }, + }, + { + type: "Identifier", + value: "b", + range: [2, 3], + loc: { + start: { + line: 1, + column: 2, + }, + end: { + line: 1, + column: 3, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [3, 4], + loc: { + start: { + line: 1, + column: 3, + }, + end: { + line: 1, + column: 4, + }, + }, + }, + { + type: "Identifier", + value: "c", + range: [4, 5], + loc: { + start: { + line: 1, + column: 4, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + { + type: "Punctuator", + value: "||", + range: [6, 8], + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 8, + }, + }, + }, + { + type: "Punctuator", + value: "(", + range: [9, 10], + loc: { + start: { + line: 1, + column: 9, + }, + end: { + line: 1, + column: 10, + }, + }, + }, + { + type: "Identifier", + value: "a", + range: [10, 11], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 11, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [11, 12], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 12, + }, + }, + }, + { + type: "Identifier", + value: "b", + range: [12, 13], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [13, 14], + loc: { + start: { + line: 1, + column: 13, + }, + end: { + line: 1, + column: 14, + }, + }, + }, + { + type: "Identifier", + value: "c", + range: [14, 15], + loc: { + start: { + line: 1, + column: 14, + }, + end: { + line: 1, + column: 15, + }, + }, + }, + { + type: "Punctuator", + value: "=", + range: [16, 17], + loc: { + start: { + line: 1, + column: 16, + }, + end: { + line: 1, + column: 17, + }, + }, + }, + { + type: "Identifier", + value: "d", + range: [18, 19], + loc: { + start: { + line: 1, + column: 18, + }, + end: { + line: 1, + column: 19, + }, + }, + }, + { + type: "Identifier", + value: "as", + range: [20, 22], + loc: { + start: { + line: 1, + column: 20, + }, + end: { + line: 1, + column: 22, + }, + }, + }, + { + type: "Identifier", + value: "number", + range: [23, 29], + loc: { + start: { + line: 1, + column: 23, + }, + end: { + line: 1, + column: 29, + }, + }, + }, + { + type: "Punctuator", + value: ")", + range: [29, 30], + loc: { + start: { + line: 1, + column: 29, + }, + end: { + line: 1, + column: 30, + }, + }, + }, + ], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 30, + }, + }, +}); diff --git a/tests/fixtures/parsers/typescript-parsers/logical-with-assignment-with-assertion-2.js b/tests/fixtures/parsers/typescript-parsers/logical-with-assignment-with-assertion-2.js new file mode 100644 index 00000000000..644060ab1d7 --- /dev/null +++ b/tests/fixtures/parsers/typescript-parsers/logical-with-assignment-with-assertion-2.js @@ -0,0 +1,568 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser 5.59.11 (TS 5.1.3) + * Source code: + * a.b.c || (a.b.c = (d as number)) + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "LogicalExpression", + operator: "||", + left: { + type: "MemberExpression", + object: { + type: "MemberExpression", + object: { + type: "Identifier", + decorators: [], + name: "a", + optional: false, + range: [0, 1], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 1, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "b", + optional: false, + range: [2, 3], + loc: { + start: { + line: 1, + column: 2, + }, + end: { + line: 1, + column: 3, + }, + }, + }, + computed: false, + optional: false, + range: [0, 3], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 3, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "c", + optional: false, + range: [4, 5], + loc: { + start: { + line: 1, + column: 4, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + computed: false, + optional: false, + range: [0, 5], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + right: { + type: "AssignmentExpression", + operator: "=", + left: { + type: "MemberExpression", + object: { + type: "MemberExpression", + object: { + type: "Identifier", + decorators: [], + name: "a", + optional: false, + range: [10, 11], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 11, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "b", + optional: false, + range: [12, 13], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + computed: false, + optional: false, + range: [10, 13], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "c", + optional: false, + range: [14, 15], + loc: { + start: { + line: 1, + column: 14, + }, + end: { + line: 1, + column: 15, + }, + }, + }, + computed: false, + optional: false, + range: [10, 15], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 15, + }, + }, + }, + right: { + type: "TSAsExpression", + expression: { + type: "Identifier", + decorators: [], + name: "d", + optional: false, + range: [19, 20], + loc: { + start: { + line: 1, + column: 19, + }, + end: { + line: 1, + column: 20, + }, + }, + }, + typeAnnotation: { + type: "TSNumberKeyword", + range: [24, 30], + loc: { + start: { + line: 1, + column: 24, + }, + end: { + line: 1, + column: 30, + }, + }, + }, + range: [19, 30], + loc: { + start: { + line: 1, + column: 19, + }, + end: { + line: 1, + column: 30, + }, + }, + }, + range: [10, 31], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 31, + }, + }, + }, + range: [0, 32], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 32, + }, + }, + }, + range: [0, 32], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 32, + }, + }, + }, + ], + comments: [], + range: [0, 32], + sourceType: "script", + tokens: [ + { + type: "Identifier", + value: "a", + range: [0, 1], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 1, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [1, 2], + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 2, + }, + }, + }, + { + type: "Identifier", + value: "b", + range: [2, 3], + loc: { + start: { + line: 1, + column: 2, + }, + end: { + line: 1, + column: 3, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [3, 4], + loc: { + start: { + line: 1, + column: 3, + }, + end: { + line: 1, + column: 4, + }, + }, + }, + { + type: "Identifier", + value: "c", + range: [4, 5], + loc: { + start: { + line: 1, + column: 4, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + { + type: "Punctuator", + value: "||", + range: [6, 8], + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 8, + }, + }, + }, + { + type: "Punctuator", + value: "(", + range: [9, 10], + loc: { + start: { + line: 1, + column: 9, + }, + end: { + line: 1, + column: 10, + }, + }, + }, + { + type: "Identifier", + value: "a", + range: [10, 11], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 11, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [11, 12], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 12, + }, + }, + }, + { + type: "Identifier", + value: "b", + range: [12, 13], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [13, 14], + loc: { + start: { + line: 1, + column: 13, + }, + end: { + line: 1, + column: 14, + }, + }, + }, + { + type: "Identifier", + value: "c", + range: [14, 15], + loc: { + start: { + line: 1, + column: 14, + }, + end: { + line: 1, + column: 15, + }, + }, + }, + { + type: "Punctuator", + value: "=", + range: [16, 17], + loc: { + start: { + line: 1, + column: 16, + }, + end: { + line: 1, + column: 17, + }, + }, + }, + { + type: "Punctuator", + value: "(", + range: [18, 19], + loc: { + start: { + line: 1, + column: 18, + }, + end: { + line: 1, + column: 19, + }, + }, + }, + { + type: "Identifier", + value: "d", + range: [19, 20], + loc: { + start: { + line: 1, + column: 19, + }, + end: { + line: 1, + column: 20, + }, + }, + }, + { + type: "Identifier", + value: "as", + range: [21, 23], + loc: { + start: { + line: 1, + column: 21, + }, + end: { + line: 1, + column: 23, + }, + }, + }, + { + type: "Identifier", + value: "number", + range: [24, 30], + loc: { + start: { + line: 1, + column: 24, + }, + end: { + line: 1, + column: 30, + }, + }, + }, + { + type: "Punctuator", + value: ")", + range: [30, 31], + loc: { + start: { + line: 1, + column: 30, + }, + end: { + line: 1, + column: 31, + }, + }, + }, + { + type: "Punctuator", + value: ")", + range: [31, 32], + loc: { + start: { + line: 1, + column: 31, + }, + end: { + line: 1, + column: 32, + }, + }, + }, + ], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 32, + }, + }, +}); diff --git a/tests/fixtures/parsers/typescript-parsers/logical-with-assignment-with-assertion-3.js b/tests/fixtures/parsers/typescript-parsers/logical-with-assignment-with-assertion-3.js new file mode 100644 index 00000000000..8824fc2f609 --- /dev/null +++ b/tests/fixtures/parsers/typescript-parsers/logical-with-assignment-with-assertion-3.js @@ -0,0 +1,568 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser 5.59.11 (TS 5.1.3) + * Source code: + * (a.b.c || (a.b.c = d)) as number + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "TSAsExpression", + expression: { + type: "LogicalExpression", + operator: "||", + left: { + type: "MemberExpression", + object: { + type: "MemberExpression", + object: { + type: "Identifier", + decorators: [], + name: "a", + optional: false, + range: [1, 2], + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 2, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "b", + optional: false, + range: [3, 4], + loc: { + start: { + line: 1, + column: 3, + }, + end: { + line: 1, + column: 4, + }, + }, + }, + computed: false, + optional: false, + range: [1, 4], + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 4, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "c", + optional: false, + range: [5, 6], + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 6, + }, + }, + }, + computed: false, + optional: false, + range: [1, 6], + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 6, + }, + }, + }, + right: { + type: "AssignmentExpression", + operator: "=", + left: { + type: "MemberExpression", + object: { + type: "MemberExpression", + object: { + type: "Identifier", + decorators: [], + name: "a", + optional: false, + range: [11, 12], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 12, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "b", + optional: false, + range: [13, 14], + loc: { + start: { + line: 1, + column: 13, + }, + end: { + line: 1, + column: 14, + }, + }, + }, + computed: false, + optional: false, + range: [11, 14], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 14, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "c", + optional: false, + range: [15, 16], + loc: { + start: { + line: 1, + column: 15, + }, + end: { + line: 1, + column: 16, + }, + }, + }, + computed: false, + optional: false, + range: [11, 16], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 16, + }, + }, + }, + right: { + type: "Identifier", + decorators: [], + name: "d", + optional: false, + range: [19, 20], + loc: { + start: { + line: 1, + column: 19, + }, + end: { + line: 1, + column: 20, + }, + }, + }, + range: [11, 20], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 20, + }, + }, + }, + range: [1, 21], + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + typeAnnotation: { + type: "TSNumberKeyword", + range: [26, 32], + loc: { + start: { + line: 1, + column: 26, + }, + end: { + line: 1, + column: 32, + }, + }, + }, + range: [0, 32], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 32, + }, + }, + }, + range: [0, 32], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 32, + }, + }, + }, + ], + comments: [], + range: [0, 32], + sourceType: "script", + tokens: [ + { + type: "Punctuator", + value: "(", + range: [0, 1], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 1, + }, + }, + }, + { + type: "Identifier", + value: "a", + range: [1, 2], + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 2, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [2, 3], + loc: { + start: { + line: 1, + column: 2, + }, + end: { + line: 1, + column: 3, + }, + }, + }, + { + type: "Identifier", + value: "b", + range: [3, 4], + loc: { + start: { + line: 1, + column: 3, + }, + end: { + line: 1, + column: 4, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [4, 5], + loc: { + start: { + line: 1, + column: 4, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + { + type: "Identifier", + value: "c", + range: [5, 6], + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 6, + }, + }, + }, + { + type: "Punctuator", + value: "||", + range: [7, 9], + loc: { + start: { + line: 1, + column: 7, + }, + end: { + line: 1, + column: 9, + }, + }, + }, + { + type: "Punctuator", + value: "(", + range: [10, 11], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 11, + }, + }, + }, + { + type: "Identifier", + value: "a", + range: [11, 12], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 12, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [12, 13], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 13, + }, + }, + }, + { + type: "Identifier", + value: "b", + range: [13, 14], + loc: { + start: { + line: 1, + column: 13, + }, + end: { + line: 1, + column: 14, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [14, 15], + loc: { + start: { + line: 1, + column: 14, + }, + end: { + line: 1, + column: 15, + }, + }, + }, + { + type: "Identifier", + value: "c", + range: [15, 16], + loc: { + start: { + line: 1, + column: 15, + }, + end: { + line: 1, + column: 16, + }, + }, + }, + { + type: "Punctuator", + value: "=", + range: [17, 18], + loc: { + start: { + line: 1, + column: 17, + }, + end: { + line: 1, + column: 18, + }, + }, + }, + { + type: "Identifier", + value: "d", + range: [19, 20], + loc: { + start: { + line: 1, + column: 19, + }, + end: { + line: 1, + column: 20, + }, + }, + }, + { + type: "Punctuator", + value: ")", + range: [20, 21], + loc: { + start: { + line: 1, + column: 20, + }, + end: { + line: 1, + column: 21, + }, + }, + }, + { + type: "Punctuator", + value: ")", + range: [21, 22], + loc: { + start: { + line: 1, + column: 21, + }, + end: { + line: 1, + column: 22, + }, + }, + }, + { + type: "Identifier", + value: "as", + range: [23, 25], + loc: { + start: { + line: 1, + column: 23, + }, + end: { + line: 1, + column: 25, + }, + }, + }, + { + type: "Identifier", + value: "number", + range: [26, 32], + loc: { + start: { + line: 1, + column: 26, + }, + end: { + line: 1, + column: 32, + }, + }, + }, + ], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 32, + }, + }, +}); diff --git a/tests/fixtures/parsers/typescript-parsers/member-call-expr-with-assertion.js b/tests/fixtures/parsers/typescript-parsers/member-call-expr-with-assertion.js new file mode 100644 index 00000000000..e26e115e94e --- /dev/null +++ b/tests/fixtures/parsers/typescript-parsers/member-call-expr-with-assertion.js @@ -0,0 +1,366 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser 5.59.11 (TS 5.1.3) + * Source code: + * const x = (1 satisfies number).toFixed(); + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "VariableDeclaration", + declarations: [ + { + type: "VariableDeclarator", + definite: false, + id: { + type: "Identifier", + decorators: [], + name: "x", + optional: false, + range: [6, 7], + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 7, + }, + }, + }, + init: { + type: "CallExpression", + callee: { + type: "MemberExpression", + object: { + type: "TSSatisfiesExpression", + expression: { + type: "Literal", + value: 1, + raw: "1", + range: [11, 12], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 12, + }, + }, + }, + typeAnnotation: { + type: "TSNumberKeyword", + range: [23, 29], + loc: { + start: { + line: 1, + column: 23, + }, + end: { + line: 1, + column: 29, + }, + }, + }, + range: [11, 29], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 29, + }, + }, + }, + property: { + type: "Identifier", + decorators: [], + name: "toFixed", + optional: false, + range: [31, 38], + loc: { + start: { + line: 1, + column: 31, + }, + end: { + line: 1, + column: 38, + }, + }, + }, + computed: false, + optional: false, + range: [10, 38], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 38, + }, + }, + }, + arguments: [], + optional: false, + range: [10, 40], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 40, + }, + }, + }, + range: [6, 40], + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 40, + }, + }, + }, + ], + declare: false, + kind: "const", + range: [0, 41], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 41, + }, + }, + }, + ], + comments: [], + range: [0, 41], + sourceType: "script", + tokens: [ + { + type: "Keyword", + value: "const", + range: [0, 5], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + { + type: "Identifier", + value: "x", + range: [6, 7], + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 7, + }, + }, + }, + { + type: "Punctuator", + value: "=", + range: [8, 9], + loc: { + start: { + line: 1, + column: 8, + }, + end: { + line: 1, + column: 9, + }, + }, + }, + { + type: "Punctuator", + value: "(", + range: [10, 11], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 11, + }, + }, + }, + { + type: "Numeric", + value: "1", + range: [11, 12], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 12, + }, + }, + }, + { + type: "Identifier", + value: "satisfies", + range: [13, 22], + loc: { + start: { + line: 1, + column: 13, + }, + end: { + line: 1, + column: 22, + }, + }, + }, + { + type: "Identifier", + value: "number", + range: [23, 29], + loc: { + start: { + line: 1, + column: 23, + }, + end: { + line: 1, + column: 29, + }, + }, + }, + { + type: "Punctuator", + value: ")", + range: [29, 30], + loc: { + start: { + line: 1, + column: 29, + }, + end: { + line: 1, + column: 30, + }, + }, + }, + { + type: "Punctuator", + value: ".", + range: [30, 31], + loc: { + start: { + line: 1, + column: 30, + }, + end: { + line: 1, + column: 31, + }, + }, + }, + { + type: "Identifier", + value: "toFixed", + range: [31, 38], + loc: { + start: { + line: 1, + column: 31, + }, + end: { + line: 1, + column: 38, + }, + }, + }, + { + type: "Punctuator", + value: "(", + range: [38, 39], + loc: { + start: { + line: 1, + column: 38, + }, + end: { + line: 1, + column: 39, + }, + }, + }, + { + type: "Punctuator", + value: ")", + range: [39, 40], + loc: { + start: { + line: 1, + column: 39, + }, + end: { + line: 1, + column: 40, + }, + }, + }, + { + type: "Punctuator", + value: ";", + range: [40, 41], + loc: { + start: { + line: 1, + column: 40, + }, + end: { + line: 1, + column: 41, + }, + }, + }, + ], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 41, + }, + }, +}); diff --git a/tests/fixtures/parsers/typescript-parsers/unneeded-ternary-1.js b/tests/fixtures/parsers/typescript-parsers/unneeded-ternary-1.js new file mode 100644 index 00000000000..2b44484d892 --- /dev/null +++ b/tests/fixtures/parsers/typescript-parsers/unneeded-ternary-1.js @@ -0,0 +1,238 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser 5.59.11 (TS 5.1.3) + * Source code: + * foo as any ? false : true + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "ConditionalExpression", + test: { + type: "TSAsExpression", + expression: { + type: "Identifier", + decorators: [], + name: "foo", + optional: false, + range: [0, 3], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 3, + }, + }, + }, + typeAnnotation: { + type: "TSAnyKeyword", + range: [7, 10], + loc: { + start: { + line: 1, + column: 7, + }, + end: { + line: 1, + column: 10, + }, + }, + }, + range: [0, 10], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 10, + }, + }, + }, + consequent: { + type: "Literal", + value: false, + raw: "false", + range: [13, 18], + loc: { + start: { + line: 1, + column: 13, + }, + end: { + line: 1, + column: 18, + }, + }, + }, + alternate: { + type: "Literal", + value: true, + raw: "true", + range: [21, 25], + loc: { + start: { + line: 1, + column: 21, + }, + end: { + line: 1, + column: 25, + }, + }, + }, + range: [0, 25], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 25, + }, + }, + }, + range: [0, 25], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 25, + }, + }, + }, + ], + comments: [], + range: [0, 25], + sourceType: "script", + tokens: [ + { + type: "Identifier", + value: "foo", + range: [0, 3], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 3, + }, + }, + }, + { + type: "Identifier", + value: "as", + range: [4, 6], + loc: { + start: { + line: 1, + column: 4, + }, + end: { + line: 1, + column: 6, + }, + }, + }, + { + type: "Identifier", + value: "any", + range: [7, 10], + loc: { + start: { + line: 1, + column: 7, + }, + end: { + line: 1, + column: 10, + }, + }, + }, + { + type: "Punctuator", + value: "?", + range: [11, 12], + loc: { + start: { + line: 1, + column: 11, + }, + end: { + line: 1, + column: 12, + }, + }, + }, + { + type: "Boolean", + value: "false", + range: [13, 18], + loc: { + start: { + line: 1, + column: 13, + }, + end: { + line: 1, + column: 18, + }, + }, + }, + { + type: "Punctuator", + value: ":", + range: [19, 20], + loc: { + start: { + line: 1, + column: 19, + }, + end: { + line: 1, + column: 20, + }, + }, + }, + { + type: "Boolean", + value: "true", + range: [21, 25], + loc: { + start: { + line: 1, + column: 21, + }, + end: { + line: 1, + column: 25, + }, + }, + }, + ], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 25, + }, + }, +}); diff --git a/tests/fixtures/parsers/typescript-parsers/unneeded-ternary-2.js b/tests/fixtures/parsers/typescript-parsers/unneeded-ternary-2.js new file mode 100644 index 00000000000..eaac150cf1a --- /dev/null +++ b/tests/fixtures/parsers/typescript-parsers/unneeded-ternary-2.js @@ -0,0 +1,240 @@ +"use strict"; + +/** + * Parser: @typescript-eslint/parser 5.59.11 (TS 5.1.3) + * Source code: + * foo ? foo : bar as any + */ + +exports.parse = () => ({ + type: "Program", + body: [ + { + type: "ExpressionStatement", + expression: { + type: "ConditionalExpression", + test: { + type: "Identifier", + decorators: [], + name: "foo", + optional: false, + range: [0, 3], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 3, + }, + }, + }, + consequent: { + type: "Identifier", + decorators: [], + name: "foo", + optional: false, + range: [6, 9], + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 9, + }, + }, + }, + alternate: { + type: "TSAsExpression", + expression: { + type: "Identifier", + decorators: [], + name: "bar", + optional: false, + range: [12, 15], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 15, + }, + }, + }, + typeAnnotation: { + type: "TSAnyKeyword", + range: [19, 22], + loc: { + start: { + line: 1, + column: 19, + }, + end: { + line: 1, + column: 22, + }, + }, + }, + range: [12, 22], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 22, + }, + }, + }, + range: [0, 22], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 22, + }, + }, + }, + range: [0, 22], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 22, + }, + }, + }, + ], + comments: [], + range: [0, 22], + sourceType: "script", + tokens: [ + { + type: "Identifier", + value: "foo", + range: [0, 3], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 3, + }, + }, + }, + { + type: "Punctuator", + value: "?", + range: [4, 5], + loc: { + start: { + line: 1, + column: 4, + }, + end: { + line: 1, + column: 5, + }, + }, + }, + { + type: "Identifier", + value: "foo", + range: [6, 9], + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 9, + }, + }, + }, + { + type: "Punctuator", + value: ":", + range: [10, 11], + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 11, + }, + }, + }, + { + type: "Identifier", + value: "bar", + range: [12, 15], + loc: { + start: { + line: 1, + column: 12, + }, + end: { + line: 1, + column: 15, + }, + }, + }, + { + type: "Identifier", + value: "as", + range: [16, 18], + loc: { + start: { + line: 1, + column: 16, + }, + end: { + line: 1, + column: 18, + }, + }, + }, + { + type: "Identifier", + value: "any", + range: [19, 22], + loc: { + start: { + line: 1, + column: 19, + }, + end: { + line: 1, + column: 22, + }, + }, + }, + ], + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 22, + }, + }, +}); diff --git a/tests/fixtures/promise-config/a.js b/tests/fixtures/promise-config/a.js new file mode 100644 index 00000000000..4ec1fa479e2 --- /dev/null +++ b/tests/fixtures/promise-config/a.js @@ -0,0 +1 @@ +var foo = "bar"; diff --git a/tests/fixtures/promise-config/eslint.config.js b/tests/fixtures/promise-config/eslint.config.js new file mode 100644 index 00000000000..852686881f0 --- /dev/null +++ b/tests/fixtures/promise-config/eslint.config.js @@ -0,0 +1,5 @@ +module.exports = Promise.resolve([{ + rules: { + quotes: ["error", "single"] + } +}]); diff --git a/tests/fixtures/simple-valid-project/.eslintrc.js b/tests/fixtures/simple-valid-project/.eslintrc.js new file mode 100644 index 00000000000..cf2cd6e50a4 --- /dev/null +++ b/tests/fixtures/simple-valid-project/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + quotes: ["error", "single"] + } +}; diff --git a/tests/fixtures/simple-valid-project/eslint.config.js b/tests/fixtures/simple-valid-project/eslint.config.js new file mode 100644 index 00000000000..6817217d965 --- /dev/null +++ b/tests/fixtures/simple-valid-project/eslint.config.js @@ -0,0 +1,5 @@ +module.exports = [{ + rules: { + quotes: ["error", "single"] + } +}]; diff --git a/tests/fixtures/simple-valid-project/foo.js b/tests/fixtures/simple-valid-project/foo.js new file mode 100644 index 00000000000..9e9f472d7ee --- /dev/null +++ b/tests/fixtures/simple-valid-project/foo.js @@ -0,0 +1 @@ +var a = 'b'; diff --git a/tests/fixtures/simple-valid-project/src/foobar.js b/tests/fixtures/simple-valid-project/src/foobar.js new file mode 100644 index 00000000000..9e9f472d7ee --- /dev/null +++ b/tests/fixtures/simple-valid-project/src/foobar.js @@ -0,0 +1 @@ +var a = 'b'; diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index 2528a398ea1..231501b364e 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -138,57 +138,83 @@ describe("CLIEngine", () => { let engine; - it("should report the total and per file errors when using local cwd .eslintrc", () => { + describe("when using local cwd .eslintrc", () => { - engine = new CLIEngine(); + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: path.join(os.tmpdir(), "eslint/multiple-rules-config"), + files: { + ".eslintrc.json": { + root: true, + rules: { + quotes: 2, + "no-var": 2, + "eol-last": 2, + strict: [2, "global"], + "no-unused-vars": 2 + }, + env: { + node: true + } + } + } + }); - const report = engine.executeOnText("var foo = 'bar';"); + beforeEach(prepare); + afterEach(cleanup); - assert.strictEqual(report.results.length, 1); - assert.strictEqual(report.errorCount, 5); - assert.strictEqual(report.warningCount, 0); - assert.strictEqual(report.fatalErrorCount, 0); - assert.strictEqual(report.fixableErrorCount, 3); - assert.strictEqual(report.fixableWarningCount, 0); - assert.strictEqual(report.results[0].messages.length, 5); - assert.strictEqual(report.results[0].messages[0].ruleId, "strict"); - assert.strictEqual(report.results[0].messages[1].ruleId, "no-var"); - assert.strictEqual(report.results[0].messages[2].ruleId, "no-unused-vars"); - assert.strictEqual(report.results[0].messages[3].ruleId, "quotes"); - assert.strictEqual(report.results[0].messages[4].ruleId, "eol-last"); - assert.strictEqual(report.results[0].fixableErrorCount, 3); - assert.strictEqual(report.results[0].fixableWarningCount, 0); - assert.strictEqual(report.results[0].suppressedMessages.length, 0); - }); + it("should report the total and per file errors", () => { - it("should report the total and per file warnings when using local cwd .eslintrc", () => { + engine = new CLIEngine({ cwd: getPath() }); - engine = new CLIEngine({ - rules: { - quotes: 1, - "no-var": 1, - "eol-last": 1, - strict: 1, - "no-unused-vars": 1 - } + const report = engine.executeOnText("var foo = 'bar';"); + + assert.strictEqual(report.results.length, 1); + assert.strictEqual(report.errorCount, 5); + assert.strictEqual(report.warningCount, 0); + assert.strictEqual(report.fatalErrorCount, 0); + assert.strictEqual(report.fixableErrorCount, 3); + assert.strictEqual(report.fixableWarningCount, 0); + assert.strictEqual(report.results[0].messages.length, 5); + assert.strictEqual(report.results[0].messages[0].ruleId, "strict"); + assert.strictEqual(report.results[0].messages[1].ruleId, "no-var"); + assert.strictEqual(report.results[0].messages[2].ruleId, "no-unused-vars"); + assert.strictEqual(report.results[0].messages[3].ruleId, "quotes"); + assert.strictEqual(report.results[0].messages[4].ruleId, "eol-last"); + assert.strictEqual(report.results[0].fixableErrorCount, 3); + assert.strictEqual(report.results[0].fixableWarningCount, 0); + assert.strictEqual(report.results[0].suppressedMessages.length, 0); }); - const report = engine.executeOnText("var foo = 'bar';"); + it("should report the total and per file warnings", () => { - assert.strictEqual(report.results.length, 1); - assert.strictEqual(report.errorCount, 0); - assert.strictEqual(report.warningCount, 5); - assert.strictEqual(report.fixableErrorCount, 0); - assert.strictEqual(report.fixableWarningCount, 3); - assert.strictEqual(report.results[0].messages.length, 5); - assert.strictEqual(report.results[0].messages[0].ruleId, "strict"); - assert.strictEqual(report.results[0].messages[1].ruleId, "no-var"); - assert.strictEqual(report.results[0].messages[2].ruleId, "no-unused-vars"); - assert.strictEqual(report.results[0].messages[3].ruleId, "quotes"); - assert.strictEqual(report.results[0].messages[4].ruleId, "eol-last"); - assert.strictEqual(report.results[0].fixableErrorCount, 0); - assert.strictEqual(report.results[0].fixableWarningCount, 3); - assert.strictEqual(report.results[0].suppressedMessages.length, 0); + engine = new CLIEngine({ + cwd: getPath(), + rules: { + quotes: 1, + "no-var": 1, + "eol-last": 1, + strict: 1, + "no-unused-vars": 1 + } + }); + + const report = engine.executeOnText("var foo = 'bar';"); + + assert.strictEqual(report.results.length, 1); + assert.strictEqual(report.errorCount, 0); + assert.strictEqual(report.warningCount, 5); + assert.strictEqual(report.fixableErrorCount, 0); + assert.strictEqual(report.fixableWarningCount, 3); + assert.strictEqual(report.results[0].messages.length, 5); + assert.strictEqual(report.results[0].messages[0].ruleId, "strict"); + assert.strictEqual(report.results[0].messages[1].ruleId, "no-var"); + assert.strictEqual(report.results[0].messages[2].ruleId, "no-unused-vars"); + assert.strictEqual(report.results[0].messages[3].ruleId, "quotes"); + assert.strictEqual(report.results[0].messages[4].ruleId, "eol-last"); + assert.strictEqual(report.results[0].fixableErrorCount, 0); + assert.strictEqual(report.results[0].fixableWarningCount, 3); + assert.strictEqual(report.results[0].suppressedMessages.length, 0); + }); }); it("should report one message when using specific config file", () => { @@ -334,7 +360,12 @@ describe("CLIEngine", () => { fatalErrorCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - usedDeprecatedRules: [] + usedDeprecatedRules: [ + { + ruleId: "semi", + replacedBy: [] + } + ] }); }); @@ -699,7 +730,7 @@ describe("CLIEngine", () => { it("should return a `source` property when a parsing error has occurred", () => { engine = new CLIEngine({ useEslintrc: false, - rules: { semi: 2 } + rules: { eqeqeq: 2 } }); const report = engine.executeOnText("var bar = foothis is a syntax error.\n return bar;"); @@ -836,10 +867,12 @@ describe("CLIEngine", () => { engine = new CLIEngine({ cwd: originalDir, - configFile: ".eslintrc.js" + useEslintrc: false, + ignore: false, + overrideConfigFile: "tests/fixtures/simple-valid-project/.eslintrc.js" }); - const report = engine.executeOnFiles(["lib/**/cli*.js"]); + const report = engine.executeOnFiles(["tests/fixtures/simple-valid-project/**/foo*.js"]); assert.strictEqual(report.results.length, 2); assert.strictEqual(report.results[0].messages.length, 0); @@ -851,10 +884,16 @@ describe("CLIEngine", () => { engine = new CLIEngine({ cwd: originalDir, - configFile: ".eslintrc.js" + useEslintrc: false, + ignore: false, + overrideConfigFile: "tests/fixtures/simple-valid-project/.eslintrc.js" }); - const report = engine.executeOnFiles(["lib/**/cli*.js", "lib/cli.?s", "lib/{cli,cli-engine/cli-engine}.js"]); + const report = engine.executeOnFiles([ + "tests/fixtures/simple-valid-project/**/foo*.js", + "tests/fixtures/simple-valid-project/foo.?s", + "tests/fixtures/simple-valid-project/{foo,src/foobar}.js" + ]); assert.strictEqual(report.results.length, 2); assert.strictEqual(report.results[0].messages.length, 0); @@ -1394,6 +1433,7 @@ describe("CLIEngine", () => { it("should throw an error when all given files are ignored", () => { engine = new CLIEngine({ + useEslintrc: false, ignorePath: getFixturePath(".eslintignore") }); @@ -1404,6 +1444,7 @@ describe("CLIEngine", () => { it("should throw an error when all given files are ignored even with a `./` prefix", () => { engine = new CLIEngine({ + useEslintrc: false, ignorePath: getFixturePath(".eslintignore") }); @@ -1450,6 +1491,7 @@ describe("CLIEngine", () => { it("should throw an error when all given files are ignored via ignore-pattern", () => { engine = new CLIEngine({ + useEslintrc: false, ignorePattern: "tests/fixtures/single-quoted.js" }); @@ -1712,7 +1754,7 @@ describe("CLIEngine", () => { it("should warn when deprecated rules are configured", () => { engine = new CLIEngine({ cwd: originalDir, - configFile: ".eslintrc.js", + useEslintrc: false, rules: { "indent-legacy": 1, "require-jsdoc": 1, @@ -1736,8 +1778,8 @@ describe("CLIEngine", () => { it("should not warn when deprecated rules are not configured", () => { engine = new CLIEngine({ cwd: originalDir, - configFile: ".eslintrc.js", - rules: { indent: 1, "valid-jsdoc": 0, "require-jsdoc": 0 } + useEslintrc: false, + rules: { eqeqeq: 1, "valid-jsdoc": 0, "require-jsdoc": 0 } }); const report = engine.executeOnFiles(["lib/cli*.js"]); @@ -4881,7 +4923,21 @@ describe("CLIEngine", () => { it("should report 5 error messages when looking for errors only", () => { process.chdir(originalDir); - const engine = new CLIEngine(); + const engine = new CLIEngine({ + useEslintrc: false, + baseConfig: { + rules: { + quotes: 2, + "no-var": 2, + "eol-last": 2, + strict: [2, "global"], + "no-unused-vars": 2 + }, + env: { + node: true + } + } + }); const report = engine.executeOnText("var foo = 'bar';"); const errorResults = CLIEngine.getErrorResults(report.results); @@ -4905,7 +4961,21 @@ describe("CLIEngine", () => { it("should report no error messages when looking for errors only", () => { process.chdir(originalDir); - const engine = new CLIEngine(); + const engine = new CLIEngine({ + useEslintrc: false, + baseConfig: { + rules: { + quotes: 2, + "no-var": 2, + "eol-last": 2, + strict: [2, "global"], + "no-unused-vars": 2 + }, + env: { + node: true + } + } + }); const report = engine.executeOnText("var foo = 'bar'; // eslint-disable-line strict, no-var, no-unused-vars, quotes, eol-last -- justification"); const errorResults = CLIEngine.getErrorResults(report.results); @@ -4916,12 +4986,18 @@ describe("CLIEngine", () => { it("should not mutate passed report.results parameter", () => { process.chdir(originalDir); const engine = new CLIEngine({ - rules: { quotes: [1, "double"] } + useEslintrc: false, + rules: { + quotes: [1, "double"], + "no-var": 2 + } }); const report = engine.executeOnText("var foo = 'bar';"); const reportResultsLength = report.results[0].messages.length; + assert.strictEqual(report.results[0].messages.length, 2); + CLIEngine.getErrorResults(report.results); assert.lengthOf(report.results[0].messages, reportResultsLength); @@ -4930,9 +5006,16 @@ describe("CLIEngine", () => { it("should report no suppressed error messages when looking for errors only", () => { process.chdir(originalDir); const engine = new CLIEngine({ + useEslintrc: false, rules: { - quotes: [1, "double"], - "no-var": 2 + quotes: 1, + "no-var": 2, + "eol-last": 2, + strict: [2, "global"], + "no-unused-vars": 2 + }, + env: { + node: true } }); @@ -4948,7 +5031,21 @@ describe("CLIEngine", () => { it("should report a warningCount of 0 when looking for errors only", () => { process.chdir(originalDir); - const engine = new CLIEngine(); + const engine = new CLIEngine({ + useEslintrc: false, + baseConfig: { + rules: { + quotes: 2, + "no-var": 2, + "eol-last": 2, + strict: [2, "global"], + "no-unused-vars": 2 + }, + env: { + node: true + } + } + }); const report = engine.executeOnText("var foo = 'bar';"); const errorResults = CLIEngine.getErrorResults(report.results); @@ -5092,9 +5189,9 @@ describe("CLIEngine", () => { }); it("should expose the list of plugin rules", () => { - const engine = new CLIEngine({ plugins: ["n"] }); + const engine = new CLIEngine({ plugins: ["internal-rules"] }); - assert(engine.getRules().has("n/no-deprecated-api"), "n/no-deprecated-api is present"); + assert(engine.getRules().has("internal-rules/no-invalid-meta"), "internal-rules/no-invalid-meta is present"); }); it("should expose the list of rules from a preloaded plugin", () => { @@ -5102,11 +5199,11 @@ describe("CLIEngine", () => { plugins: ["foo"] }, { preloadedPlugins: { - foo: require("eslint-plugin-n") + foo: require("eslint-plugin-internal-rules") } }); - assert(engine.getRules().has("foo/no-deprecated-api"), "foo/no-deprecated-api is present"); + assert(engine.getRules().has("foo/no-invalid-meta"), "foo/no-invalid-meta is present"); }); }); diff --git a/tests/lib/cli.js b/tests/lib/cli.js index e8e266edb0e..7d8f8c7ede0 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -114,15 +114,16 @@ describe("cli", () => { describe("execute()", () => { it(`should return error when text with incorrect quotes is passed as argument with configType:${configType}`, async () => { + const flag = useFlatConfig ? "--no-config-lookup" : "--no-eslintrc"; const configFile = getFixturePath("configurations", "quotes-error.js"); - const result = await cli.execute(`-c ${configFile} --stdin --stdin-filename foo.js`, "var foo = 'bar';", useFlatConfig); + const result = await cli.execute(`${flag} -c ${configFile} --stdin --stdin-filename foo.js`, "var foo = 'bar';", useFlatConfig); assert.strictEqual(result, 1); }); it(`should not print debug info when passed the empty string as text with configType:${configType}`, async () => { const flag = useFlatConfig ? "--no-config-lookup" : "--no-eslintrc"; - const result = await cli.execute(["--stdin", flag, "--stdin-filename", "foo.js"], "", useFlatConfig); + const result = await cli.execute(["argv0", "argv1", "--stdin", flag, "--stdin-filename", "foo.js"], "", useFlatConfig); assert.strictEqual(result, 0); assert.isTrue(log.info.notCalled); @@ -206,20 +207,30 @@ describe("cli", () => { describe("when there is a local config file", () => { + const originalCwd = process.cwd; + + beforeEach(() => { + process.cwd = () => getFixturePath(); + }); + + afterEach(() => { + process.cwd = originalCwd; + }); + it(`should load the local config file with configType:${configType}`, async () => { - await cli.execute("lib/cli.js", null, useFlatConfig); + await cli.execute("cli/passing.js --no-ignore", null, useFlatConfig); }); if (useFlatConfig) { it(`should load the local config file with glob pattern and configType:${configType}`, async () => { - await cli.execute("lib/cli*.js", null, useFlatConfig); + await cli.execute("cli/pass*.js --no-ignore", null, useFlatConfig); }); } // only works on Windows if (os.platform() === "win32") { it(`should load the local config file with Windows slashes glob pattern and configType:${configType}`, async () => { - await cli.execute("lib\\cli*.js", null, useFlatConfig); + await cli.execute("cli\\pass*.js --no-ignore", null, useFlatConfig); }); } }); @@ -336,10 +347,21 @@ describe("cli", () => { }); describe("when given an invalid formatter path", () => { + + const originalCwd = process.cwd; + + beforeEach(() => { + process.cwd = () => getFixturePath(); + }); + + afterEach(() => { + process.cwd = originalCwd; + }); + it(`should execute with error with configType:${configType}`, async () => { const formatterPath = getFixturePath("formatters", "file-does-not-exist.js"); const filePath = getFixturePath("passing.js"); - const exit = await cli.execute(`-f ${formatterPath} ${filePath}`, null, useFlatConfig); + const exit = await cli.execute(`--no-ignore -f ${formatterPath} ${filePath}`, null, useFlatConfig); assert.strictEqual(exit, 2); }); @@ -779,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", () => { @@ -869,13 +917,15 @@ describe("cli", () => { }); describe("when supplied with report output file path", () => { + const flag = useFlatConfig ? "--no-config-lookup" : "--no-eslintrc"; + afterEach(() => { sh.rm("-rf", "tests/output"); }); it(`should write the file and create dirs if they don't exist with configType:${configType}`, async () => { const filePath = getFixturePath("single-quoted.js"); - const code = `--no-ignore --rule 'quotes: [1, double]' --o tests/output/eslint-output.txt ${filePath}`; + const code = `${flag} --rule 'quotes: [1, double]' --o tests/output/eslint-output.txt ${filePath}`; await cli.execute(code, null, useFlatConfig); @@ -885,7 +935,7 @@ describe("cli", () => { it(`should return an error if the path is a directory with configType:${configType}`, async () => { const filePath = getFixturePath("single-quoted.js"); - const code = `--no-ignore --rule 'quotes: [1, double]' --o tests/output ${filePath}`; + const code = `${flag} --rule 'quotes: [1, double]' --o tests/output ${filePath}`; fs.mkdirSync("tests/output"); @@ -898,7 +948,7 @@ describe("cli", () => { it(`should return an error if the path could not be written to with configType:${configType}`, async () => { const filePath = getFixturePath("single-quoted.js"); - const code = `--no-ignore --rule 'quotes: [1, double]' --o tests/output/eslint-output.txt ${filePath}`; + const code = `${flag} --rule 'quotes: [1, double]' --o tests/output/eslint-output.txt ${filePath}`; fs.writeFileSync("tests/output", "foo"); @@ -1277,8 +1327,19 @@ describe("cli", () => { }); describe("when passing --print-config", () => { + + const originalCwd = process.cwd; + + beforeEach(() => { + process.cwd = () => getFixturePath(); + }); + + afterEach(() => { + process.cwd = originalCwd; + }); + it(`should print out the configuration with configType:${configType}`, async () => { - const filePath = getFixturePath("xxxx"); + const filePath = getFixturePath("xxx.js"); const exitCode = await cli.execute(`--print-config ${filePath}`, null, useFlatConfig); diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index 4ec9f50516f..accccfec2b6 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([ @@ -1938,5 +1949,109 @@ describe("FlatConfigArray", () => { }); + describe("Invalid Keys", () => { + + [ + "env", + "extends", + "globals", + "ignorePatterns", + "noInlineConfig", + "overrides", + "parser", + "parserOptions", + "reportUnusedDisableDirectives", + "root" + ].forEach(key => { + + it(`should error when a ${key} key is found`, async () => { + await assertInvalidConfig([ + { + [key]: "foo" + } + ], `Key "${key}": This appears to be in eslintrc format rather than flat config format.`); + + }); + }); + + it("should error when plugins is an array", async () => { + await assertInvalidConfig([ + { + plugins: ["foo"] + } + ], "Key \"plugins\": This appears to be in eslintrc format (array of strings) rather than flat config format (object)."); + + }); + + + }); + + }); + + // https://github.com/eslint/eslint/issues/12592 + describe("Shared references between rule configs", () => { + + it("shared rule config should not cause a rule validation error", () => { + + const ruleConfig = ["error", {}]; + + const configs = new FlatConfigArray([{ + rules: { + camelcase: ruleConfig, + "default-case": ruleConfig + } + }]); + + configs.normalizeSync(); + + const config = configs.getConfig("foo.js"); + + assert.deepStrictEqual(config.rules, { + camelcase: [2, { + ignoreDestructuring: false, + ignoreGlobals: false, + ignoreImports: false + }], + "default-case": [2, {}] + }); + + }); + + + it("should throw rule validation error for camelcase", async () => { + + const ruleConfig = ["error", {}]; + + const configs = new FlatConfigArray([ + { + rules: { + camelcase: ruleConfig + } + }, + { + rules: { + "default-case": ruleConfig, + + + camelcase: [ + "error", + { + ignoreDestructuring: Date + } + + ] + } + } + ]); + + configs.normalizeSync(); + + // exact error may differ based on structuredClone implementation so just test prefix + assert.throws(() => { + configs.getConfig("foo.js"); + }, /Key "rules": Key "camelcase":/u); + + }); + }); }); diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index c05a869641d..b9db3543f7c 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -46,7 +46,7 @@ describe("ESLint", () => { const originalDir = process.cwd(); const fixtureDir = path.resolve(fs.realpathSync(os.tmpdir()), "eslint/fixtures"); - /** @type {import("../../../lib/eslint").ESLint} */ + /** @type {import("../../../lib/eslint/eslint").ESLint} */ let ESLint; /** @@ -117,7 +117,7 @@ describe("ESLint", () => { it("the default value of 'options.cwd' should be the current working directory.", async () => { process.chdir(__dirname); try { - const engine = new ESLint(); + const engine = new ESLint({ useEslintrc: false }); const results = await engine.lintFiles("eslint.js"); assert.strictEqual(path.dirname(results[0].filePath), __dirname); @@ -126,6 +126,28 @@ describe("ESLint", () => { } }); + it("should normalize 'options.cwd'.", async () => { + const cwd = getFixturePath("example-app3"); + const engine = new ESLint({ + cwd: `${cwd}${path.sep}foo${path.sep}..`, // `/foo/..` should be normalized to `` + useEslintrc: false, + overrideConfig: { + plugins: ["test"], + rules: { + "test/report-cwd": "error" + } + } + }); + const results = await engine.lintText(""); + + assert.strictEqual(results[0].messages[0].ruleId, "test/report-cwd"); + assert.strictEqual(results[0].messages[0].message, cwd); + + const formatter = await engine.loadFormatter("cwd"); + + assert.strictEqual(formatter.format(results), cwd); + }); + it("should report one fatal message when given a path by --ignore-path that is not a file when ignore is true.", () => { assert.throws(() => { // eslint-disable-next-line no-new -- Check for throwing @@ -239,46 +261,75 @@ describe("ESLint", () => { describe("lintText()", () => { let eslint; - it("should report the total and per file errors when using local cwd .eslintrc", async () => { - eslint = new ESLint(); - const results = await eslint.lintText("var foo = 'bar';"); - - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 5); - assert.strictEqual(results[0].messages[0].ruleId, "strict"); - assert.strictEqual(results[0].messages[1].ruleId, "no-var"); - assert.strictEqual(results[0].messages[2].ruleId, "no-unused-vars"); - assert.strictEqual(results[0].messages[3].ruleId, "quotes"); - assert.strictEqual(results[0].messages[4].ruleId, "eol-last"); - assert.strictEqual(results[0].fixableErrorCount, 3); - assert.strictEqual(results[0].fixableWarningCount, 0); - assert.strictEqual(results[0].usedDeprecatedRules.length, 0); - }); - - it("should report the total and per file warnings when using local cwd .eslintrc", async () => { - eslint = new ESLint({ - overrideConfig: { - rules: { - quotes: 1, - "no-var": 1, - "eol-last": 1, - strict: 1, - "no-unused-vars": 1 + describe("when using local cwd .eslintrc", () => { + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: path.join(os.tmpdir(), "eslint/multiple-rules-config"), + files: { + ".eslintrc.json": { + root: true, + rules: { + quotes: 2, + "no-var": 2, + "eol-last": 2, + strict: [2, "global"], + "no-unused-vars": 2 + }, + env: { + node: true + } } } }); - const results = await eslint.lintText("var foo = 'bar';"); - assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].messages.length, 5); - assert.strictEqual(results[0].messages[0].ruleId, "strict"); - assert.strictEqual(results[0].messages[1].ruleId, "no-var"); - assert.strictEqual(results[0].messages[2].ruleId, "no-unused-vars"); - assert.strictEqual(results[0].messages[3].ruleId, "quotes"); - assert.strictEqual(results[0].messages[4].ruleId, "eol-last"); - assert.strictEqual(results[0].fixableErrorCount, 0); - assert.strictEqual(results[0].fixableWarningCount, 3); - assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + beforeEach(prepare); + afterEach(cleanup); + + it("should report the total and per file errors", async () => { + eslint = new ESLint({ cwd: getPath() }); + const results = await eslint.lintText("var foo = 'bar';"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 5); + assert.strictEqual(results[0].messages[0].ruleId, "strict"); + assert.strictEqual(results[0].messages[1].ruleId, "no-var"); + assert.strictEqual(results[0].messages[2].ruleId, "no-unused-vars"); + assert.strictEqual(results[0].messages[3].ruleId, "quotes"); + assert.strictEqual(results[0].messages[4].ruleId, "eol-last"); + assert.strictEqual(results[0].fixableErrorCount, 3); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 2); + assert.strictEqual(results[0].usedDeprecatedRules[0].ruleId, "quotes"); + assert.strictEqual(results[0].usedDeprecatedRules[1].ruleId, "eol-last"); + }); + + it("should report the total and per file warnings", async () => { + eslint = new ESLint({ + cwd: getPath(), + overrideConfig: { + rules: { + quotes: 1, + "no-var": 1, + "eol-last": 1, + strict: 1, + "no-unused-vars": 1 + } + } + }); + const results = await eslint.lintText("var foo = 'bar';"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 5); + assert.strictEqual(results[0].messages[0].ruleId, "strict"); + assert.strictEqual(results[0].messages[1].ruleId, "no-var"); + assert.strictEqual(results[0].messages[2].ruleId, "no-unused-vars"); + assert.strictEqual(results[0].messages[3].ruleId, "quotes"); + assert.strictEqual(results[0].messages[4].ruleId, "eol-last"); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 3); + assert.strictEqual(results[0].usedDeprecatedRules.length, 2); + assert.strictEqual(results[0].usedDeprecatedRules[0].ruleId, "quotes"); + assert.strictEqual(results[0].usedDeprecatedRules[1].ruleId, "eol-last"); + }); }); it("should report one message when using specific config file", async () => { @@ -296,7 +347,8 @@ describe("ESLint", () => { assert.strictEqual(results[0].errorCount, 1); assert.strictEqual(results[0].fixableErrorCount, 1); assert.strictEqual(results[0].warningCount, 0); - assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 1); + assert.strictEqual(results[0].usedDeprecatedRules[0].ruleId, "quotes"); }); it("should report the filename when passed in", async () => { @@ -408,7 +460,10 @@ describe("ESLint", () => { fixableErrorCount: 0, fixableWarningCount: 0, output: "var bar = foo;", - usedDeprecatedRules: [] + usedDeprecatedRules: [{ + ruleId: "semi", + replacedBy: [] + }] } ]); }); @@ -441,14 +496,14 @@ describe("ESLint", () => { cwd: getFixturePath() }); const options = { filePath: "file.js" }; - const results = await eslint.lintText("foo ()", options); + const results = await eslint.lintText("if (true) { foo() }", options); assert.strictEqual(results.length, 1); const { messages } = results[0]; // Some rules that should report errors in the given code. Not all, as we don't want to update this test when we add new rules. - const expectedRules = ["no-undef", "semi", "func-call-spacing"]; + const expectedRules = ["no-undef", "no-constant-condition"]; expectedRules.forEach(ruleId => { const messageFromRule = messages.find(message => message.ruleId === ruleId); @@ -806,7 +861,7 @@ describe("ESLint", () => { eslint = new ESLint({ useEslintrc: false, overrideConfig: { - rules: { semi: 2 } + rules: { eqeqeq: 2 } } }); const results = await eslint.lintText("var bar = foothis is a syntax error.\n return bar;"); @@ -950,9 +1005,11 @@ describe("ESLint", () => { it("should report zero messages when given a config file and a valid file", async () => { eslint = new ESLint({ cwd: originalDir, - overrideConfigFile: ".eslintrc.js" + useEslintrc: false, + ignore: false, + overrideConfigFile: "tests/fixtures/simple-valid-project/.eslintrc.js" }); - const results = await eslint.lintFiles(["lib/**/cli*.js"]); + const results = await eslint.lintFiles(["tests/fixtures/simple-valid-project/**/foo*.js"]); assert.strictEqual(results.length, 2); assert.strictEqual(results[0].messages.length, 0); @@ -962,9 +1019,15 @@ describe("ESLint", () => { it("should handle multiple patterns with overlapping files", async () => { eslint = new ESLint({ cwd: originalDir, - overrideConfigFile: ".eslintrc.js" + useEslintrc: false, + ignore: false, + overrideConfigFile: "tests/fixtures/simple-valid-project/.eslintrc.js" }); - const results = await eslint.lintFiles(["lib/**/cli*.js", "lib/cli.?s", "lib/{cli,cli-engine/cli-engine}.js"]); + const results = await eslint.lintFiles([ + "tests/fixtures/simple-valid-project/**/foo*.js", + "tests/fixtures/simple-valid-project/foo.?s", + "tests/fixtures/simple-valid-project/{foo,src/foobar}.js" + ]); assert.strictEqual(results.length, 2); assert.strictEqual(results[0].messages.length, 0); @@ -1396,6 +1459,10 @@ describe("ESLint", () => { }); it("should throw an error when all given files are ignored", async () => { + eslint = new ESLint({ + useEslintrc: false, + ignorePath: getFixturePath(".eslintignore") + }); await assert.rejects(async () => { await eslint.lintFiles(["tests/fixtures/cli-engine/"]); }, /All files matched by 'tests\/fixtures\/cli-engine\/' are ignored\./u); @@ -1403,6 +1470,7 @@ describe("ESLint", () => { it("should throw an error when all given files are ignored even with a `./` prefix", async () => { eslint = new ESLint({ + useEslintrc: false, ignorePath: getFixturePath(".eslintignore") }); @@ -1466,6 +1534,7 @@ describe("ESLint", () => { it("should throw an error when all given files are ignored via ignore-pattern", async () => { eslint = new ESLint({ + useEslintrc: false, overrideConfig: { ignorePatterns: "tests/fixtures/single-quoted.js" } @@ -1675,7 +1744,7 @@ describe("ESLint", () => { it("should warn when deprecated rules are configured", async () => { eslint = new ESLint({ cwd: originalDir, - overrideConfigFile: ".eslintrc.js", + useEslintrc: false, overrideConfig: { rules: { "indent-legacy": 1, @@ -1699,9 +1768,9 @@ describe("ESLint", () => { it("should not warn when deprecated rules are not configured", async () => { eslint = new ESLint({ cwd: originalDir, - overrideConfigFile: ".eslintrc.js", + useEslintrc: false, overrideConfig: { - rules: { indent: 1, "valid-jsdoc": 0, "require-jsdoc": 0 } + rules: { eqeqeq: 1, "valid-jsdoc": 0, "require-jsdoc": 0 } } }); const results = await eslint.lintFiles(["lib/cli*.js"]); @@ -1766,7 +1835,20 @@ describe("ESLint", () => { fixableErrorCount: 0, fixableWarningCount: 0, output: "true ? \"yes\" : \"no\";\n", - usedDeprecatedRules: [] + usedDeprecatedRules: [ + { + replacedBy: [], + ruleId: "semi" + }, + { + replacedBy: [], + ruleId: "quotes" + }, + { + replacedBy: [], + ruleId: "space-infix-ops" + } + ] }, { filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/ok.js")), @@ -1777,7 +1859,20 @@ describe("ESLint", () => { fatalErrorCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - usedDeprecatedRules: [] + usedDeprecatedRules: [ + { + replacedBy: [], + ruleId: "semi" + }, + { + replacedBy: [], + ruleId: "quotes" + }, + { + replacedBy: [], + ruleId: "space-infix-ops" + } + ] }, { filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/quotes-semi-eqeqeq.js")), @@ -1801,7 +1896,20 @@ describe("ESLint", () => { fixableErrorCount: 0, fixableWarningCount: 0, output: "var msg = \"hi\";\nif (msg == \"hi\") {\n\n}\n", - usedDeprecatedRules: [] + usedDeprecatedRules: [ + { + replacedBy: [], + ruleId: "semi" + }, + { + replacedBy: [], + ruleId: "quotes" + }, + { + replacedBy: [], + ruleId: "space-infix-ops" + } + ] }, { filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/quotes.js")), @@ -1825,7 +1933,20 @@ describe("ESLint", () => { fixableErrorCount: 0, fixableWarningCount: 0, output: "var msg = \"hi\" + foo;\n", - usedDeprecatedRules: [] + usedDeprecatedRules: [ + { + replacedBy: [], + ruleId: "semi" + }, + { + replacedBy: [], + ruleId: "quotes" + }, + { + replacedBy: [], + ruleId: "space-infix-ops" + } + ] } ]); }); @@ -2260,25 +2381,22 @@ describe("ESLint", () => { } } - /** - * helper method to delete the cache files created during testing - * @returns {void} - */ - function deleteCache() { - doDelete(path.resolve(".eslintcache")); - doDelete(path.resolve(".cache/custom-cache")); - } + let cacheFilePath; beforeEach(() => { - deleteCache(); + cacheFilePath = null; }); afterEach(() => { sinon.restore(); - deleteCache(); + if (cacheFilePath) { + doDelete(cacheFilePath); + } }); - describe("when the cacheFile is a directory or looks like a directory", () => { + describe("when cacheLocation is a directory or looks like a directory", () => { + + const cwd = getFixturePath(); /** * helper method to delete the cache files created during testing @@ -2286,7 +2404,21 @@ describe("ESLint", () => { */ function deleteCacheDir() { try { - fs.unlinkSync("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory"); + + /* + * `fs.rmdir(path, { recursive: true })` is deprecated and will be removed. + * Use `fs.rm(path, { recursive: true })` instead. + * When supporting Node.js 14.14.0+, the compatibility condition can be removed for `fs.rmdir`. + */ + // eslint-disable-next-line n/no-unsupported-features/node-builtins -- just checking if it exists + if (typeof fs.rm === "function") { + + // eslint-disable-next-line n/no-unsupported-features/node-builtins -- conditionally used + fs.rmSync(path.resolve(cwd, "tmp/.cacheFileDir/"), { recursive: true, force: true }); + } else { + fs.rmdirSync(path.resolve(cwd, "tmp/.cacheFileDir/"), { recursive: true, force: true }); + } + } catch { /* @@ -2303,11 +2435,12 @@ describe("ESLint", () => { deleteCacheDir(); }); - it("should create the cache file inside the provided directory", async () => { - assert(!shell.test("-d", path.resolve("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory")), "the cache for eslint does not exist"); + it("should create the directory and the cache file inside it when cacheLocation ends with a slash", async () => { + assert(!shell.test("-d", path.resolve(cwd, "./tmp/.cacheFileDir/")), "the cache directory already exists and wasn't successfully deleted"); eslint = new ESLint({ useEslintrc: false, + cwd, // specifying cache true the cache will be created cache: true, @@ -2325,42 +2458,71 @@ describe("ESLint", () => { await eslint.lintFiles([file]); - assert(shell.test("-f", path.resolve(`./tmp/.cacheFileDir/.cache_${hash(process.cwd())}`)), "the cache for eslint was created"); - - sinon.restore(); + assert(shell.test("-f", path.resolve(cwd, `./tmp/.cacheFileDir/.cache_${hash(cwd)}`)), "the cache for eslint should have been created"); }); - }); - it("should create the cache file inside the provided directory using the cacheLocation option", async () => { - assert(!shell.test("-d", path.resolve("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory")), "the cache for eslint does not exist"); + it("should create the cache file inside existing cacheLocation directory when cacheLocation ends with a slash", async () => { + assert(!shell.test("-d", path.resolve(cwd, "./tmp/.cacheFileDir/")), "the cache directory already exists and wasn't successfully deleted"); - eslint = new ESLint({ - useEslintrc: false, + fs.mkdirSync(path.resolve(cwd, "./tmp/.cacheFileDir/"), { recursive: true }); - // specifying cache true the cache will be created - cache: true, - cacheLocation: "./tmp/.cacheFileDir/", - overrideConfig: { - rules: { - "no-console": 0, - "no-unused-vars": 2 - } - }, - extensions: ["js"], - ignore: false + eslint = new ESLint({ + useEslintrc: false, + cwd, + + // specifying cache true the cache will be created + cache: true, + cacheLocation: "./tmp/.cacheFileDir/", + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + ignore: false + }); + const file = getFixturePath("cache/src", "test-file.js"); + + await eslint.lintFiles([file]); + + assert(shell.test("-f", path.resolve(cwd, `./tmp/.cacheFileDir/.cache_${hash(cwd)}`)), "the cache for eslint should have been created"); }); - const file = getFixturePath("cache/src", "test-file.js"); - await eslint.lintFiles([file]); + it("should create the cache file inside existing cacheLocation directory when cacheLocation doesn't end with a path separator", async () => { + assert(!shell.test("-d", path.resolve(cwd, "./tmp/.cacheFileDir/")), "the cache directory already exists and wasn't successfully deleted"); - assert(shell.test("-f", path.resolve(`./tmp/.cacheFileDir/.cache_${hash(process.cwd())}`)), "the cache for eslint was created"); + fs.mkdirSync(path.resolve(cwd, "./tmp/.cacheFileDir/"), { recursive: true }); - sinon.restore(); + eslint = new ESLint({ + useEslintrc: false, + cwd, + + // specifying cache true the cache will be created + cache: true, + cacheLocation: "./tmp/.cacheFileDir", + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + ignore: false + }); + const file = getFixturePath("cache/src", "test-file.js"); + + await eslint.lintFiles([file]); + + assert(shell.test("-f", path.resolve(cwd, `./tmp/.cacheFileDir/.cache_${hash(cwd)}`)), "the cache for eslint should have been created"); + }); }); it("should create the cache file inside cwd when no cacheLocation provided", async () => { const cwd = path.resolve(getFixturePath("cli-engine")); + cacheFilePath = path.resolve(cwd, ".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); + eslint = new ESLint({ useEslintrc: false, cache: true, @@ -2377,14 +2539,19 @@ describe("ESLint", () => { await eslint.lintFiles([file]); - assert(shell.test("-f", path.resolve(cwd, ".eslintcache")), "the cache for eslint was created at provided cwd"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should have been created at provided cwd"); }); it("should invalidate the cache if the configuration changed between executions", async () => { - assert(!shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint does not exist"); + const cwd = getFixturePath("cache/src"); + + cacheFilePath = path.resolve(cwd, ".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new ESLint({ useEslintrc: false, + cwd, // specifying cache true the cache will be created cache: true, @@ -2406,16 +2573,17 @@ describe("ESLint", () => { const results = await eslint.lintFiles([file]); for (const { errorCount, warningCount } of results) { - assert.strictEqual(errorCount + warningCount, 0, "the file passed without errors or warnings"); + assert.strictEqual(errorCount + warningCount, 0, "the file should have passed linting without errors or warnings"); } - assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed"); - assert(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); + assert(spy.calledWith(file), "ESLint should have read the file because there was no cache file"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should have been created"); // destroy the spy sinon.restore(); eslint = new ESLint({ useEslintrc: false, + cwd, // specifying cache true the cache will be created cache: true, @@ -2432,18 +2600,24 @@ describe("ESLint", () => { // create a new spy spy = sinon.spy(fs, "readFileSync"); - const [cachedResult] = await eslint.lintFiles([file]); + const [newResult] = await eslint.lintFiles([file]); - assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed because the config changed"); - assert.strictEqual(cachedResult.errorCount, 1, "since configuration changed the cache was not used an one error was reported"); - assert(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); + assert(spy.calledWith(file), "ESLint should have read the file again because it's considered changed because the config changed"); + assert.strictEqual(newResult.errorCount, 1, "since configuration changed the cache should have not been used and one error should have been reported"); + assert.strictEqual(newResult.messages[0].ruleId, "no-console"); + assert(shell.test("-f", cacheFilePath), "The cache for ESLint should still exist"); }); it("should remember the files from a previous run and do not operate on them if not changed", async () => { - assert(!shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint does not exist"); + const cwd = getFixturePath("cache/src"); + + cacheFilePath = path.resolve(cwd, ".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new ESLint({ useEslintrc: false, + cwd, // specifying cache true the cache will be created cache: true, @@ -2465,14 +2639,15 @@ describe("ESLint", () => { const result = await eslint.lintFiles([file]); - assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed"); - assert(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); + assert(spy.calledWith(file), "ESLint should have read the file because there was no cache file"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should have been created"); // destroy the spy sinon.restore(); eslint = new ESLint({ useEslintrc: false, + cwd, // specifying cache true the cache will be created cache: true, @@ -2491,20 +2666,23 @@ describe("ESLint", () => { const cachedResult = await eslint.lintFiles([file]); - assert.deepStrictEqual(result, cachedResult, "the result is the same regardless of using cache or not"); + assert.deepStrictEqual(result, cachedResult, "the result should have been the same"); // assert the file was not processed because the cache was used - assert(!spy.calledWith(file), "the file was not loaded because it used the cache"); + assert(!spy.calledWith(file), "the file should not have been reloaded"); }); - it("should remember the files from a previous run and do not operate on then if not changed", async () => { - const cacheLocation = getFixturePath(".eslintcache"); + it("when `cacheLocation` is specified, should create the cache file with `cache:true` and then delete it with `cache:false`", async () => { + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); + const eslintOptions = { useEslintrc: false, // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2515,8 +2693,6 @@ describe("ESLint", () => { cwd: path.join(fixtureDir, "..") }; - assert(!shell.test("-f", cacheLocation), "the cache for eslint does not exist"); - eslint = new ESLint(eslintOptions); let file = getFixturePath("cache/src", "test-file.js"); @@ -2525,20 +2701,20 @@ describe("ESLint", () => { await eslint.lintFiles([file]); - assert(shell.test("-f", cacheLocation), "the cache for eslint was created"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should have been created"); eslintOptions.cache = false; eslint = new ESLint(eslintOptions); await eslint.lintFiles([file]); - assert(!shell.test("-f", cacheLocation), "the cache for eslint was deleted since last run did not used the cache"); + assert(!shell.test("-f", cacheFilePath), "the cache for eslint should have been deleted since last run did not use the cache"); }); - it("should store in the cache a file that failed the test", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - - assert(!shell.test("-f", cacheLocation), "the cache for eslint does not exist"); + it("should store in the cache a file that has lint messages and a file that doesn't have lint messages", async () => { + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), @@ -2546,7 +2722,7 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2558,22 +2734,27 @@ describe("ESLint", () => { const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); const result = await eslint.lintFiles([badFile, goodFile]); + const [badFileResult, goodFileResult] = result; + + assert.notStrictEqual(badFileResult.errorCount + badFileResult.warningCount, 0, "the bad file should have some lint errors or warnings"); + assert.strictEqual(goodFileResult.errorCount + badFileResult.warningCount, 0, "the good file should have passed linting without errors or warnings"); - assert(shell.test("-f", cacheLocation), "the cache for eslint was created"); - const fileCache = fCache.createFromFile(cacheLocation); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should have been created"); + + const fileCache = fCache.createFromFile(cacheFilePath); const { cache } = fileCache; - assert.strictEqual(typeof cache.getKey(goodFile), "object", "the entry for the good file is in the cache"); - assert.strictEqual(typeof cache.getKey(badFile), "object", "the entry for the bad file is in the cache"); + assert.strictEqual(typeof cache.getKey(goodFile), "object", "the entry for the good file should have been in the cache"); + assert.strictEqual(typeof cache.getKey(badFile), "object", "the entry for the bad file should have been in the cache"); const cachedResult = await eslint.lintFiles([badFile, goodFile]); - assert.deepStrictEqual(result, cachedResult, "result is the same with or without cache"); + assert.deepStrictEqual(result, cachedResult, "result should be the same with or without cache"); }); it("should not contain in the cache a file that was deleted", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - - doDelete(cacheLocation); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), @@ -2581,7 +2762,7 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2595,10 +2776,10 @@ describe("ESLint", () => { const toBeDeletedFile = fs.realpathSync(getFixturePath("cache/src", "file-to-delete.js")); await eslint.lintFiles([badFile, goodFile, toBeDeletedFile]); - const fileCache = fCache.createFromFile(cacheLocation); + const fileCache = fCache.createFromFile(cacheFilePath); let { cache } = fileCache; - assert.strictEqual(typeof cache.getKey(toBeDeletedFile), "object", "the entry for the file to be deleted is in the cache"); + assert.strictEqual(typeof cache.getKey(toBeDeletedFile), "object", "the entry for the file to be deleted should have been in the cache"); // delete the file from the file system fs.unlinkSync(toBeDeletedFile); @@ -2609,15 +2790,19 @@ describe("ESLint", () => { */ await eslint.lintFiles([badFile, goodFile]); - cache = JSON.parse(fs.readFileSync(cacheLocation)); + cache = JSON.parse(fs.readFileSync(cacheFilePath)); - assert.strictEqual(typeof cache[toBeDeletedFile], "undefined", "the entry for the file to be deleted is not in the cache"); + assert.strictEqual(typeof cache[0][toBeDeletedFile], "undefined", "the entry for the file to be deleted should not have been in the cache"); + + // make sure that the previos assertion checks the right place + assert.notStrictEqual(typeof cache[0][badFile], "undefined", "the entry for the bad file should have been in the cache"); + assert.notStrictEqual(typeof cache[0][goodFile], "undefined", "the entry for the good file should have been in the cache"); }); it("should contain files that were not visited in the cache provided they still exist", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - - doDelete(cacheLocation); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), @@ -2625,7 +2810,7 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2640,10 +2825,10 @@ describe("ESLint", () => { await eslint.lintFiles([badFile, goodFile, testFile2]); - let fileCache = fCache.createFromFile(cacheLocation); + let fileCache = fCache.createFromFile(cacheFilePath); let { cache } = fileCache; - assert.strictEqual(typeof cache.getKey(testFile2), "object", "the entry for the test-file2 is in the cache"); + assert.strictEqual(typeof cache.getKey(testFile2), "object", "the entry for the test-file2 should have been in the cache"); /* * we pass a different set of files minus test-file2 @@ -2652,19 +2837,23 @@ describe("ESLint", () => { */ await eslint.lintFiles([badFile, goodFile]); - fileCache = fCache.createFromFile(cacheLocation); + fileCache = fCache.createFromFile(cacheFilePath); cache = fileCache.cache; - assert.strictEqual(typeof cache.getKey(testFile2), "object", "the entry for the test-file2 is in the cache"); + assert.strictEqual(typeof cache.getKey(testFile2), "object", "the entry for the test-file2 should have been in the cache"); }); it("should not delete cache when executing on text", async () => { - const cacheLocation = getFixturePath(".eslintcache"); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); + + fs.writeFileSync(cacheFilePath, "[]"); // intenationally invalid to additionally make sure it isn't used eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), useEslintrc: false, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2674,20 +2863,24 @@ describe("ESLint", () => { extensions: ["js"] }); - assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should exist"); await eslint.lintText("var foo = 'bar';"); - assert(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should still exist"); }); it("should not delete cache when executing on text with a provided filename", async () => { - const cacheLocation = getFixturePath(".eslintcache"); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); + + fs.writeFileSync(cacheFilePath, "[]"); // intenationally invalid to additionally make sure it isn't used eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), useEslintrc: false, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2697,21 +2890,25 @@ describe("ESLint", () => { extensions: ["js"] }); - assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should exist"); await eslint.lintText("var bar = foo;", { filePath: "fixtures/passing.js" }); - assert(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should still exist"); }); it("should not delete cache when executing on files with --cache flag", async () => { - const cacheLocation = getFixturePath(".eslintcache"); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); + + fs.writeFileSync(cacheFilePath, ""); eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), useEslintrc: false, cache: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2722,20 +2919,24 @@ describe("ESLint", () => { }); const file = getFixturePath("cli-engine", "console.js"); - assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should exist"); await eslint.lintFiles([file]); - assert(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should still exist"); }); it("should delete cache when executing on files without --cache flag", async () => { - const cacheLocation = getFixturePath(".eslintcache"); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); + + fs.writeFileSync(cacheFilePath, "[]"); // intenationally invalid to additionally make sure it isn't used eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), useEslintrc: false, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2746,58 +2947,57 @@ describe("ESLint", () => { }); const file = getFixturePath("cli-engine", "console.js"); - assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should exist"); await eslint.lintFiles([file]); - assert(!shell.test("-f", cacheLocation), "the cache for eslint has been deleted"); + assert(!shell.test("-f", cacheFilePath), "the cache for eslint should have been deleted"); }); - describe("cacheFile", () => { - it("should use the specified cache file", async () => { - const customCacheFile = path.resolve(".cache/custom-cache"); + it("should use the specified cache file", async () => { + cacheFilePath = path.resolve(".cache/custom-cache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); - assert(!shell.test("-f", customCacheFile), "the cache for eslint does not exist"); + eslint = new ESLint({ + useEslintrc: false, - eslint = new ESLint({ - useEslintrc: false, + // specify a custom cache file + cacheLocation: cacheFilePath, - // specify a custom cache file - cacheLocation: customCacheFile, + // specifying cache true the cache will be created + cache: true, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"], + cwd: path.join(fixtureDir, "..") + }); + const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); + const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); + const result = await eslint.lintFiles([badFile, goodFile]); - // specifying cache true the cache will be created - cache: true, - overrideConfig: { - rules: { - "no-console": 0, - "no-unused-vars": 2 - } - }, - extensions: ["js"], - cwd: path.join(fixtureDir, "..") - }); - const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); - const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); - const result = await eslint.lintFiles([badFile, goodFile]); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should have been created"); - assert(shell.test("-f", customCacheFile), "the cache for eslint was created"); - const fileCache = fCache.createFromFile(customCacheFile); - const { cache } = fileCache; + const fileCache = fCache.createFromFile(cacheFilePath); + const { cache } = fileCache; - assert(typeof cache.getKey(goodFile) === "object", "the entry for the good file is in the cache"); + assert(typeof cache.getKey(goodFile) === "object", "the entry for the good file should have been in the cache"); + assert(typeof cache.getKey(badFile) === "object", "the entry for the bad file should have been in the cache"); - assert(typeof cache.getKey(badFile) === "object", "the entry for the bad file is in the cache"); - const cachedResult = await eslint.lintFiles([badFile, goodFile]); + const cachedResult = await eslint.lintFiles([badFile, goodFile]); - assert.deepStrictEqual(result, cachedResult, "result is the same with or without cache"); - }); + assert.deepStrictEqual(result, cachedResult, "result should be the same with or without cache"); }); describe("cacheStrategy", () => { it("should detect changes using a file's modification time when set to 'metadata'", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - - doDelete(cacheLocation); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), @@ -2805,7 +3005,7 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, cacheStrategy: "metadata", overrideConfig: { rules: { @@ -2819,24 +3019,24 @@ describe("ESLint", () => { const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); await eslint.lintFiles([badFile, goodFile]); - let fileCache = fCache.createFromFile(cacheLocation); + let fileCache = fCache.createFromFile(cacheFilePath); const entries = fileCache.normalizeEntries([badFile, goodFile]); entries.forEach(entry => { - assert(entry.changed === false, `the entry for ${entry.key} is initially unchanged`); + assert(entry.changed === false, `the entry for ${entry.key} should have been initially unchanged`); }); // this should result in a changed entry shell.touch(goodFile); - fileCache = fCache.createFromFile(cacheLocation); - assert(fileCache.getFileDescriptor(badFile).changed === false, `the entry for ${badFile} is unchanged`); - assert(fileCache.getFileDescriptor(goodFile).changed === true, `the entry for ${goodFile} is changed`); + fileCache = fCache.createFromFile(cacheFilePath); + assert(fileCache.getFileDescriptor(badFile).changed === false, `the entry for ${badFile} should have been unchanged`); + assert(fileCache.getFileDescriptor(goodFile).changed === true, `the entry for ${goodFile} should have been changed`); }); it("should not detect changes using a file's modification time when set to 'content'", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - - doDelete(cacheLocation); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), @@ -2844,7 +3044,7 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, cacheStrategy: "content", overrideConfig: { rules: { @@ -2858,26 +3058,26 @@ describe("ESLint", () => { const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); await eslint.lintFiles([badFile, goodFile]); - let fileCache = fCache.createFromFile(cacheLocation, true); + let fileCache = fCache.createFromFile(cacheFilePath, true); let entries = fileCache.normalizeEntries([badFile, goodFile]); entries.forEach(entry => { - assert(entry.changed === false, `the entry for ${entry.key} is initially unchanged`); + assert(entry.changed === false, `the entry for ${entry.key} should have been initially unchanged`); }); // this should NOT result in a changed entry shell.touch(goodFile); - fileCache = fCache.createFromFile(cacheLocation, true); + fileCache = fCache.createFromFile(cacheFilePath, true); entries = fileCache.normalizeEntries([badFile, goodFile]); entries.forEach(entry => { - assert(entry.changed === false, `the entry for ${entry.key} remains unchanged`); + assert(entry.changed === false, `the entry for ${entry.key} should have remained unchanged`); }); }); it("should detect changes using a file's contents when set to 'content'", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - - doDelete(cacheLocation); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new ESLint({ cwd: path.join(fixtureDir, ".."), @@ -2885,7 +3085,7 @@ describe("ESLint", () => { // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, cacheStrategy: "content", overrideConfig: { rules: { @@ -2902,18 +3102,18 @@ describe("ESLint", () => { shell.cp(goodFile, goodFileCopy); await eslint.lintFiles([badFile, goodFileCopy]); - let fileCache = fCache.createFromFile(cacheLocation, true); + let fileCache = fCache.createFromFile(cacheFilePath, true); const entries = fileCache.normalizeEntries([badFile, goodFileCopy]); entries.forEach(entry => { - assert(entry.changed === false, `the entry for ${entry.key} is initially unchanged`); + assert(entry.changed === false, `the entry for ${entry.key} should have been initially unchanged`); }); // this should result in a changed entry shell.sed("-i", "abc", "xzy", goodFileCopy); - fileCache = fCache.createFromFile(cacheLocation, true); - assert(fileCache.getFileDescriptor(badFile).changed === false, `the entry for ${badFile} is unchanged`); - assert(fileCache.getFileDescriptor(goodFileCopy).changed === true, `the entry for ${goodFileCopy} is changed`); + fileCache = fCache.createFromFile(cacheFilePath, true); + assert(fileCache.getFileDescriptor(badFile).changed === false, `the entry for ${badFile} should have been unchanged`); + assert(fileCache.getFileDescriptor(goodFileCopy).changed === true, `the entry for ${goodFileCopy} should have been changed`); }); }); }); @@ -4865,7 +5065,21 @@ describe("ESLint", () => { describe("getErrorResults()", () => { it("should report 5 error messages when looking for errors only", async () => { process.chdir(originalDir); - const engine = new ESLint(); + const engine = new ESLint({ + useEslintrc: false, + overrideConfig: { + rules: { + quotes: 2, + "no-var": 2, + "eol-last": 2, + strict: [2, "global"], + "no-unused-vars": 2 + }, + env: { + node: true + } + } + }); const results = await engine.lintText("var foo = 'bar';"); const errorResults = ESLint.getErrorResults(results); @@ -4888,13 +5102,19 @@ describe("ESLint", () => { it("should not mutate passed report parameter", async () => { process.chdir(originalDir); const engine = new ESLint({ + useEslintrc: false, overrideConfig: { - rules: { quotes: [1, "double"] } + rules: { + quotes: [1, "double"], + "no-var": 2 + } } }); const results = await engine.lintText("var foo = 'bar';"); const reportResultsLength = results[0].messages.length; + assert.strictEqual(results[0].messages.length, 2); + ESLint.getErrorResults(results); assert.strictEqual(results[0].messages.length, reportResultsLength); @@ -4902,7 +5122,21 @@ describe("ESLint", () => { it("should report a warningCount of 0 when looking for errors only", async () => { process.chdir(originalDir); - const engine = new ESLint(); + const engine = new ESLint({ + useEslintrc: false, + overrideConfig: { + rules: { + quotes: 2, + "no-var": 2, + "eol-last": 2, + strict: [2, "global"], + "no-unused-vars": 2 + }, + env: { + node: true + } + } + }); const results = await engine.lintText("var foo = 'bar';"); const errorResults = ESLint.getErrorResults(results); @@ -5030,30 +5264,35 @@ describe("ESLint", () => { }); it("should return multiple rule meta when there are multiple linting errors from a plugin", async () => { - const nodePlugin = require("eslint-plugin-n"); + const customPlugin = { + rules: { + "no-var": require("../../../lib/rules/no-var") + } + }; + const engine = new ESLint({ useEslintrc: false, plugins: { - node: nodePlugin + "custom-plugin": customPlugin }, overrideConfig: { - plugins: ["n"], + plugins: ["custom-plugin"], rules: { - "n/no-new-require": 2, + "custom-plugin/no-var": 2, semi: 2, quotes: [2, "double"] } } }); - const results = await engine.lintText("new require('hi')"); + const results = await engine.lintText("var foo = 0; var bar = '1'"); const rulesMeta = engine.getRulesMetaForResults(results); assert.strictEqual(rulesMeta.semi, coreRules.get("semi").meta); assert.strictEqual(rulesMeta.quotes, coreRules.get("quotes").meta); assert.strictEqual( - rulesMeta["n/no-new-require"], - nodePlugin.rules["no-new-require"].meta + rulesMeta["custom-plugin/no-var"], + customPlugin.rules["no-var"].meta ); }); @@ -6872,4 +7111,50 @@ describe("ESLint", () => { }); }); }); + + // only works on a Windows machine + if (os.platform() === "win32") { + + // https://github.com/eslint/eslint/issues/17042 + describe("with cwd that is using forward slash on Windows", () => { + const cwd = getFixturePath("example-app3"); + const cwdForwardSlash = cwd.replace(/\\/gu, "/"); + + it("should correctly handle ignore patterns", async () => { + const engine = new ESLint({ cwd: cwdForwardSlash }); + const results = await engine.lintFiles(["./src"]); + + // src/dist/2.js should be ignored + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, path.join(cwd, "src\\1.js")); + }); + + it("should pass cwd with backslashes to rules", async () => { + const engine = new ESLint({ + cwd: cwdForwardSlash, + useEslintrc: false, + overrideConfig: { + plugins: ["test"], + rules: { + "test/report-cwd": "error" + } + } + }); + const results = await engine.lintText(""); + + assert.strictEqual(results[0].messages[0].ruleId, "test/report-cwd"); + assert.strictEqual(results[0].messages[0].message, cwd); + }); + + it("should pass cwd with backslashes to formatters", async () => { + const engine = new ESLint({ + cwd: cwdForwardSlash + }); + const results = await engine.lintText(""); + const formatter = await engine.loadFormatter("cwd"); + + assert.strictEqual(formatter.format(results), cwd); + }); + }); + } }); diff --git a/tests/lib/eslint/flat-eslint.js b/tests/lib/eslint/flat-eslint.js index 00a906b0fb7..f9f36d909fd 100644 --- a/tests/lib/eslint/flat-eslint.js +++ b/tests/lib/eslint/flat-eslint.js @@ -69,7 +69,7 @@ describe("FlatESLint", () => { const originalDir = process.cwd(); const fixtureDir = path.resolve(fs.realpathSync(os.tmpdir()), "eslint/fixtures"); - /** @type {import("../../../lib/flat-eslint").FlatESLint} */ + /** @type {import("../../../lib/eslint/flat-eslint").FlatESLint} */ let FlatESLint; /** @@ -140,6 +140,30 @@ describe("FlatESLint", () => { } }); + it("should normalize 'options.cwd'.", async () => { + const cwd = getFixturePath("example-app3"); + const engine = new FlatESLint({ + cwd: `${cwd}${path.sep}foo${path.sep}..`, // `/foo/..` should be normalized to `` + overrideConfigFile: true, + overrideConfig: { + plugins: { + test: require(path.join(cwd, "node_modules", "eslint-plugin-test")) + }, + rules: { + "test/report-cwd": "error" + } + } + }); + const results = await engine.lintText(""); + + assert.strictEqual(results[0].messages[0].ruleId, "test/report-cwd"); + assert.strictEqual(results[0].messages[0].message, cwd); + + const formatter = await engine.loadFormatter("cwd"); + + assert.strictEqual(formatter.format(results), cwd); + }); + // https://github.com/eslint/eslint/issues/2380 it("should not modify baseConfig when format is specified", () => { const customBaseConfig = { root: true }; @@ -187,7 +211,8 @@ describe("FlatESLint", () => { overrideConfig: "", overrideConfigFile: "", plugins: "", - reportUnusedDisableDirectives: "" + reportUnusedDisableDirectives: "", + warnIgnored: "" }), new RegExp(escapeStringRegExp([ "Invalid Options:", @@ -205,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") ); }); @@ -273,7 +299,9 @@ describe("FlatESLint", () => { assert.strictEqual(results[0].messages[3].ruleId, "eol-last"); assert.strictEqual(results[0].fixableErrorCount, 3); assert.strictEqual(results[0].fixableWarningCount, 0); - assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 2); + assert.strictEqual(results[0].usedDeprecatedRules[0].ruleId, "quotes"); + assert.strictEqual(results[0].usedDeprecatedRules[1].ruleId, "eol-last"); assert.strictEqual(results[0].suppressedMessages.length, 0); }); @@ -299,7 +327,9 @@ describe("FlatESLint", () => { assert.strictEqual(results[0].messages[3].ruleId, "eol-last"); assert.strictEqual(results[0].fixableErrorCount, 0); assert.strictEqual(results[0].fixableWarningCount, 3); - assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 2); + assert.strictEqual(results[0].usedDeprecatedRules[0].ruleId, "quotes"); + assert.strictEqual(results[0].usedDeprecatedRules[1].ruleId, "eol-last"); assert.strictEqual(results[0].suppressedMessages.length, 0); }); @@ -318,7 +348,8 @@ describe("FlatESLint", () => { assert.strictEqual(results[0].fixableErrorCount, 1); assert.strictEqual(results[0].warningCount, 0); assert.strictEqual(results[0].fatalErrorCount, 0); - assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 1); + assert.strictEqual(results[0].usedDeprecatedRules[0].ruleId, "quotes"); assert.strictEqual(results[0].suppressedMessages.length, 0); }); @@ -345,7 +376,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); @@ -373,18 +428,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(".."), @@ -433,7 +501,12 @@ describe("FlatESLint", () => { fixableErrorCount: 0, fixableWarningCount: 0, output: "var bar = foo;", - usedDeprecatedRules: [] + usedDeprecatedRules: [ + { + ruleId: "semi", + replacedBy: [] + } + ] } ]); }); @@ -623,7 +696,7 @@ describe("FlatESLint", () => { eslint = new FlatESLint({ overrideConfigFile: true, overrideConfig: { - rules: { semi: 2 } + rules: { eqeqeq: 2 } } }); const results = await eslint.lintText("var bar = foothis is a syntax error.\n return bar;"); @@ -661,7 +734,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")); @@ -682,6 +755,37 @@ describe("FlatESLint", () => { ); }); + it("should throw if eslint.config.js file is not present", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath("..") + }); + await assert.rejects(() => eslint.lintText("var foo = 'bar';"), /Could not find config file/u); + }); + + it("should not throw if eslint.config.js file is not present and overrideConfigFile is `true`", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath(".."), + overrideConfigFile: true + }); + await eslint.lintText("var foo = 'bar';"); + }); + + it("should not throw if eslint.config.js file is not present and overrideConfigFile is path to a config file", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath(".."), + overrideConfigFile: "fixtures/configurations/quotes-error.js" + }); + await eslint.lintText("var foo = 'bar';"); + }); + + it("should throw if overrideConfigFile is path to a file that doesn't exist", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath(""), + overrideConfigFile: "does-not-exist.js" + }); + await assert.rejects(() => eslint.lintText("var foo = 'bar';"), { code: "ENOENT" }); + }); + it("should throw if non-string value is given to 'code' parameter", async () => { eslint = new FlatESLint(); await assert.rejects(() => eslint.lintText(100), /'code' must be a string/u); @@ -706,6 +810,18 @@ describe("FlatESLint", () => { eslint = new FlatESLint(); await assert.rejects(() => eslint.lintText("var a = 0", { warnIgnored: "" }), /'options.warnIgnored' must be a boolean or undefined/u); }); + + it("should work with config file that exports a promise", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath("promise-config") + }); + const results = await eslint.lintText('var foo = "bar";'); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + }); }); describe("lintFiles()", () => { @@ -738,9 +854,9 @@ describe("FlatESLint", () => { it("should report zero messages when given a config file and a valid file", async () => { eslint = new FlatESLint({ cwd: originalDir, - overrideConfigFile: "eslint.config.js" + overrideConfigFile: "tests/fixtures/simple-valid-project/eslint.config.js" }); - const results = await eslint.lintFiles(["lib/**/cli*.js"]); + const results = await eslint.lintFiles(["tests/fixtures/simple-valid-project/**/foo*.js"]); assert.strictEqual(results.length, 2); assert.strictEqual(results[0].messages.length, 0); @@ -751,9 +867,13 @@ describe("FlatESLint", () => { it("should handle multiple patterns with overlapping files", async () => { eslint = new FlatESLint({ cwd: originalDir, - overrideConfigFile: "eslint.config.js" + overrideConfigFile: "tests/fixtures/simple-valid-project/eslint.config.js" }); - const results = await eslint.lintFiles(["lib/**/cli*.js", "lib/cli.?s", "lib/{cli,cli-engine/cli-engine}.js"]); + const results = await eslint.lintFiles([ + "tests/fixtures/simple-valid-project/**/foo*.js", + "tests/fixtures/simple-valid-project/foo.?s", + "tests/fixtures/simple-valid-project/{foo,src/foobar}.js" + ]); assert.strictEqual(results.length, 2); assert.strictEqual(results[0].messages.length, 0); @@ -797,6 +917,37 @@ describe("FlatESLint", () => { assert.strictEqual(results[0].suppressedMessages.length, 0); }); + it("should throw if eslint.config.js file is not present", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath("..") + }); + await assert.rejects(() => eslint.lintFiles("fixtures/undef*.js"), /Could not find config file/u); + }); + + it("should not throw if eslint.config.js file is not present and overrideConfigFile is `true`", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath(".."), + overrideConfigFile: true + }); + await eslint.lintFiles("fixtures/undef*.js"); + }); + + it("should not throw if eslint.config.js file is not present and overrideConfigFile is path to a config file", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath(".."), + overrideConfigFile: "fixtures/configurations/quotes-error.js" + }); + await eslint.lintFiles("fixtures/undef*.js"); + }); + + it("should throw if overrideConfigFile is path to a file that doesn't exist", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath(), + overrideConfigFile: "does-not-exist.js" + }); + await assert.rejects(() => eslint.lintFiles("undef*.js"), { code: "ENOENT" }); + }); + it("should throw an error when given a config file and a valid file and invalid parser", async () => { eslint = new FlatESLint({ overrideConfig: { @@ -901,6 +1052,19 @@ describe("FlatESLint", () => { assert.strictEqual(results[0].suppressedMessages.length, 0); }); + it("should work with config file that exports a promise", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath("promise-config") + }); + const results = await eslint.lintFiles(["a*.js"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, getFixturePath("promise-config", "a.js")); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + }); + // https://github.com/eslint/eslint/issues/16265 describe("Dot files in searches", () => { @@ -1191,12 +1355,12 @@ describe("FlatESLint", () => { describe("Ignoring Files", () => { - it("should report on all files passed explicitly, even if ignored by default", async () => { + it("should report on a file in the node_modules folder passed explicitly, even if ignored by default", async () => { eslint = new 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); @@ -1204,10 +1368,58 @@ describe("FlatESLint", () => { assert.strictEqual(results[0].fatalErrorCount, 0); assert.strictEqual(results[0].fixableErrorCount, 0); assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].messages[0].severity, 1); assert.strictEqual(results[0].messages[0].message, expectedMsg); assert.strictEqual(results[0].suppressedMessages.length, 0); }); + it("should report on a file in a node_modules subfolder passed explicitly, even if ignored by default", async () => { + eslint = new 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 disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."; + + assert.strictEqual(results.length, 1); + 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].messages[0].severity, 1); + assert.strictEqual(results[0].messages[0].message, expectedMsg); + assert.strictEqual(results[0].suppressedMessages.length, 0); + }); + + it("should report on an ignored file with \"node_modules\" in its name", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath("cli-engine"), + 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 disable file ignore settings or use \"--no-warn-ignored\" to suppress this warning."; + + assert.strictEqual(results.length, 1); + 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].messages[0].severity, 1); + assert.strictEqual(results[0].messages[0].message, expectedMsg); + 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"), @@ -1308,11 +1520,10 @@ describe("FlatESLint", () => { }, /All files matched by '\.\/tests\/fixtures\/cli-engine\/' are ignored\./u); }); - it("should throw an error when all given files are ignored via ignore-pattern", async () => { + it("should throw an error when all given files are ignored via ignorePatterns", async () => { eslint = new FlatESLint({ - overrideConfig: { - ignorePatterns: "tests/fixtures/single-quoted.js" - } + overrideConfigFile: true, + ignorePatterns: ["tests/fixtures/single-quoted.js"] }); await assert.rejects(async () => { @@ -1331,7 +1542,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); @@ -1340,6 +1551,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", @@ -1351,7 +1574,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); @@ -1717,6 +1940,7 @@ describe("FlatESLint", () => { it("should warn when deprecated rules are configured", async () => { eslint = new FlatESLint({ cwd: originalDir, + overrideConfigFile: true, overrideConfig: { rules: { "indent-legacy": 1, @@ -1740,8 +1964,9 @@ describe("FlatESLint", () => { it("should not warn when deprecated rules are not configured", async () => { eslint = new FlatESLint({ cwd: originalDir, + overrideConfigFile: true, overrideConfig: { - rules: { indent: 1, "valid-jsdoc": 0, "require-jsdoc": 0 } + rules: { eqeqeq: 1, "valid-jsdoc": 0, "require-jsdoc": 0 } } }); const results = await eslint.lintFiles(["lib/cli*.js"]); @@ -1836,7 +2061,20 @@ describe("FlatESLint", () => { fixableErrorCount: 0, fixableWarningCount: 0, output: "true ? \"yes\" : \"no\";\n", - usedDeprecatedRules: [] + usedDeprecatedRules: [ + { + replacedBy: [], + ruleId: "semi" + }, + { + replacedBy: [], + ruleId: "quotes" + }, + { + replacedBy: [], + ruleId: "space-infix-ops" + } + ] }, { filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/ok.js")), @@ -1847,7 +2085,20 @@ describe("FlatESLint", () => { fatalErrorCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - usedDeprecatedRules: [] + usedDeprecatedRules: [ + { + replacedBy: [], + ruleId: "semi" + }, + { + replacedBy: [], + ruleId: "quotes" + }, + { + replacedBy: [], + ruleId: "space-infix-ops" + } + ] }, { filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/quotes-semi-eqeqeq.js")), @@ -1871,7 +2122,20 @@ describe("FlatESLint", () => { fixableErrorCount: 0, fixableWarningCount: 0, output: "var msg = \"hi\";\nif (msg == \"hi\") {\n\n}\n", - usedDeprecatedRules: [] + usedDeprecatedRules: [ + { + replacedBy: [], + ruleId: "semi" + }, + { + replacedBy: [], + ruleId: "quotes" + }, + { + replacedBy: [], + ruleId: "space-infix-ops" + } + ] }, { filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/quotes.js")), @@ -1895,7 +2159,20 @@ describe("FlatESLint", () => { fixableErrorCount: 0, fixableWarningCount: 0, output: "var msg = \"hi\" + foo;\n", - usedDeprecatedRules: [] + usedDeprecatedRules: [ + { + replacedBy: [], + ruleId: "semi" + }, + { + replacedBy: [], + ruleId: "quotes" + }, + { + replacedBy: [], + ruleId: "space-infix-ops" + } + ] } ]); }); @@ -2000,33 +2277,44 @@ describe("FlatESLint", () => { } } - /** - * helper method to delete the cache files created during testing - * @returns {void} - */ - function deleteCache() { - doDelete(path.resolve(".eslintcache")); - doDelete(path.resolve(".cache/custom-cache")); - } + let cacheFilePath; beforeEach(() => { - deleteCache(); + cacheFilePath = null; }); afterEach(() => { sinon.restore(); - deleteCache(); + if (cacheFilePath) { + doDelete(cacheFilePath); + } }); - describe("when the cacheFile is a directory or looks like a directory", () => { + describe("when cacheLocation is a directory or looks like a directory", () => { + + const cwd = getFixturePath(); /** - * helper method to delete the cache files created during testing + * helper method to delete the directory used in testing * @returns {void} */ function deleteCacheDir() { try { - fs.unlinkSync("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory"); + + /* + * `fs.rmdir(path, { recursive: true })` is deprecated and will be removed. + * Use `fs.rm(path, { recursive: true })` instead. + * When supporting Node.js 14.14.0+, the compatibility condition can be removed for `fs.rmdir`. + */ + // eslint-disable-next-line n/no-unsupported-features/node-builtins -- just checking if it exists + if (typeof fs.rm === "function") { + + // eslint-disable-next-line n/no-unsupported-features/node-builtins -- conditionally used + fs.rmSync(path.resolve(cwd, "tmp/.cacheFileDir/"), { recursive: true, force: true }); + } else { + fs.rmdirSync(path.resolve(cwd, "tmp/.cacheFileDir/"), { recursive: true, force: true }); + } + } catch { /* @@ -2043,11 +2331,12 @@ describe("FlatESLint", () => { deleteCacheDir(); }); - it("should create the cache file inside the provided directory", async () => { - assert(!shell.test("-d", path.resolve("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory")), "the cache for eslint does not exist"); + it("should create the directory and the cache file inside it when cacheLocation ends with a slash", async () => { + assert(!shell.test("-d", path.resolve(cwd, "./tmp/.cacheFileDir/")), "the cache directory already exists and wasn't successfully deleted"); eslint = new FlatESLint({ overrideConfigFile: true, + cwd, // specifying cache true the cache will be created cache: true, @@ -2064,41 +2353,71 @@ describe("FlatESLint", () => { await eslint.lintFiles([file]); - assert(shell.test("-f", path.resolve(`./tmp/.cacheFileDir/.cache_${hash(process.cwd())}`)), "the cache for eslint was created"); - - sinon.restore(); + assert(shell.test("-f", path.resolve(cwd, `./tmp/.cacheFileDir/.cache_${hash(cwd)}`)), "the cache for eslint should have been created"); }); - }); - it("should create the cache file inside the provided directory using the cacheLocation option", async () => { - assert(!shell.test("-d", path.resolve("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory")), "the cache for eslint does not exist"); + it("should create the cache file inside existing cacheLocation directory when cacheLocation ends with a slash", async () => { + assert(!shell.test("-d", path.resolve(cwd, "./tmp/.cacheFileDir/")), "the cache directory already exists and wasn't successfully deleted"); - eslint = new FlatESLint({ - overrideConfigFile: true, + fs.mkdirSync(path.resolve(cwd, "./tmp/.cacheFileDir/"), { recursive: true }); - // specifying cache true the cache will be created - cache: true, - cacheLocation: "./tmp/.cacheFileDir/", - overrideConfig: { - rules: { - "no-console": 0, - "no-unused-vars": 2 - } - }, - ignore: false + eslint = new FlatESLint({ + overrideConfigFile: true, + cwd, + + // specifying cache true the cache will be created + cache: true, + cacheLocation: "./tmp/.cacheFileDir/", + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + ignore: false + }); + const file = getFixturePath("cache/src", "test-file.js"); + + await eslint.lintFiles([file]); + + assert(shell.test("-f", path.resolve(cwd, `./tmp/.cacheFileDir/.cache_${hash(cwd)}`)), "the cache for eslint should have been created"); }); - const file = getFixturePath("cache/src", "test-file.js"); - await eslint.lintFiles([file]); + it("should create the cache file inside existing cacheLocation directory when cacheLocation doesn't end with a path separator", async () => { + assert(!shell.test("-d", path.resolve(cwd, "./tmp/.cacheFileDir/")), "the cache directory already exists and wasn't successfully deleted"); - assert(shell.test("-f", path.resolve(`./tmp/.cacheFileDir/.cache_${hash(process.cwd())}`)), "the cache for eslint was created"); + fs.mkdirSync(path.resolve(cwd, "./tmp/.cacheFileDir/"), { recursive: true }); - sinon.restore(); + eslint = new FlatESLint({ + overrideConfigFile: true, + cwd, + + // specifying cache true the cache will be created + cache: true, + cacheLocation: "./tmp/.cacheFileDir", + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + ignore: false + }); + const file = getFixturePath("cache/src", "test-file.js"); + + await eslint.lintFiles([file]); + + assert(shell.test("-f", path.resolve(cwd, `./tmp/.cacheFileDir/.cache_${hash(cwd)}`)), "the cache for eslint should have been created"); + }); }); it("should create the cache file inside cwd when no cacheLocation provided", async () => { const cwd = path.resolve(getFixturePath("cli-engine")); + cacheFilePath = path.resolve(cwd, ".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); + eslint = new FlatESLint({ overrideConfigFile: true, cache: true, @@ -2114,14 +2433,15 @@ describe("FlatESLint", () => { await eslint.lintFiles([file]); - assert(shell.test("-f", path.resolve(cwd, ".eslintcache")), "the cache for eslint was created at provided cwd"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should have been created at provided cwd"); }); it("should invalidate the cache if the overrideConfig changed between executions", async () => { const cwd = getFixturePath("cache/src"); - const cacheLocation = path.resolve(cwd, ".eslintcache"); - assert(!shell.test("-f", cacheLocation), "the cache for eslint does not exist"); + cacheFilePath = path.resolve(cwd, ".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new FlatESLint({ overrideConfigFile: true, @@ -2146,11 +2466,11 @@ describe("FlatESLint", () => { const results = await eslint.lintFiles([file]); for (const { errorCount, warningCount } of results) { - assert.strictEqual(errorCount + warningCount, 0, "the file passed without errors or warnings"); + assert.strictEqual(errorCount + warningCount, 0, "the file should have passed linting without errors or warnings"); } - assert(spy.calledWith(file), "ESLint should have read the file because it's considered changed"); - assert(shell.test("-f", cacheLocation), "the cache for eslint should still exist"); + assert(spy.calledWith(file), "ESLint should have read the file because there was no cache file"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should have been created"); // destroy the spy sinon.restore(); @@ -2173,17 +2493,20 @@ describe("FlatESLint", () => { // create a new spy spy = sinon.spy(fs.promises, "readFile"); - const [cachedResult] = await eslint.lintFiles([file]); + const [newResult] = await eslint.lintFiles([file]); - assert(spy.calledWith(file), "ESLint should have read the file again because is considered changed because the config changed"); - assert.strictEqual(cachedResult.errorCount, 1, "since configuration changed the cache was not used and one error was reported"); - assert(shell.test("-f", cacheLocation), "The cache for ESLint should still exist (2)"); + assert(spy.calledWith(file), "ESLint should have read the file again because it's considered changed because the config changed"); + assert.strictEqual(newResult.errorCount, 1, "since configuration changed the cache should have not been used and one error should have been reported"); + assert.strictEqual(newResult.messages[0].ruleId, "no-console"); + assert(shell.test("-f", cacheFilePath), "The cache for ESLint should still exist"); }); it("should remember the files from a previous run and do not operate on them if not changed", async () => { - const cwd = getFixturePath("cache/src"); - const cacheLocation = path.resolve(cwd, ".eslintcache"); + + cacheFilePath = path.resolve(cwd, ".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new FlatESLint({ overrideConfigFile: true, @@ -2208,8 +2531,8 @@ describe("FlatESLint", () => { const result = await eslint.lintFiles([file]); - assert(spy.calledWith(file), "the module read the file because is considered changed"); - assert(shell.test("-f", cacheLocation), "the cache for eslint was created"); + assert(spy.calledWith(file), "ESLint should have read the file because there was no cache file"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should have been created"); // destroy the spy sinon.restore(); @@ -2234,20 +2557,23 @@ describe("FlatESLint", () => { const cachedResult = await eslint.lintFiles([file]); - assert.deepStrictEqual(result, cachedResult, "the result is the same regardless of using cache or not"); + assert.deepStrictEqual(result, cachedResult, "the result should have been the same"); // assert the file was not processed because the cache was used - assert(!spy.calledWith(file), "the file was not loaded because it used the cache"); + assert(!spy.calledWith(file), "the file should not have been reloaded"); }); - it("should remember the files from a previous run and do not operate on then if not changed", async () => { - const cacheLocation = getFixturePath(".eslintcache"); + it("when `cacheLocation` is specified, should create the cache file with `cache:true` and then delete it with `cache:false`", async () => { + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); + const eslintOptions = { overrideConfigFile: true, // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2257,8 +2583,6 @@ describe("FlatESLint", () => { cwd: path.join(fixtureDir, "..") }; - assert(!shell.test("-f", cacheLocation), "the cache for eslint does not exist"); - eslint = new FlatESLint(eslintOptions); let file = getFixturePath("cache/src", "test-file.js"); @@ -2267,20 +2591,20 @@ describe("FlatESLint", () => { await eslint.lintFiles([file]); - assert(shell.test("-f", cacheLocation), "the cache for eslint was created"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should have been created"); eslintOptions.cache = false; eslint = new FlatESLint(eslintOptions); await eslint.lintFiles([file]); - assert(!shell.test("-f", cacheLocation), "the cache for eslint was deleted since last run did not used the cache"); + assert(!shell.test("-f", cacheFilePath), "the cache for eslint should have been deleted since last run did not use the cache"); }); - it("should store in the cache a file that failed the test", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - - assert(!shell.test("-f", cacheLocation), "the cache for eslint does not exist"); + it("should store in the cache a file that has lint messages and a file that doesn't have lint messages", async () => { + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new FlatESLint({ cwd: path.join(fixtureDir, ".."), @@ -2288,7 +2612,7 @@ describe("FlatESLint", () => { // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2299,22 +2623,27 @@ describe("FlatESLint", () => { const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); const result = await eslint.lintFiles([badFile, goodFile]); + const [badFileResult, goodFileResult] = result; + + assert.notStrictEqual(badFileResult.errorCount + badFileResult.warningCount, 0, "the bad file should have some lint errors or warnings"); + assert.strictEqual(goodFileResult.errorCount + badFileResult.warningCount, 0, "the good file should have passed linting without errors or warnings"); + + assert(shell.test("-f", cacheFilePath), "the cache for eslint should have been created"); - assert(shell.test("-f", cacheLocation), "the cache for eslint was created"); - const fileCache = fCache.createFromFile(cacheLocation); + const fileCache = fCache.createFromFile(cacheFilePath); const { cache } = fileCache; - assert.strictEqual(typeof cache.getKey(goodFile), "object", "the entry for the good file is in the cache"); - assert.strictEqual(typeof cache.getKey(badFile), "object", "the entry for the bad file is in the cache"); + assert.strictEqual(typeof cache.getKey(goodFile), "object", "the entry for the good file should have been in the cache"); + assert.strictEqual(typeof cache.getKey(badFile), "object", "the entry for the bad file should have been in the cache"); const cachedResult = await eslint.lintFiles([badFile, goodFile]); - assert.deepStrictEqual(result, cachedResult, "result is the same with or without cache"); + assert.deepStrictEqual(result, cachedResult, "result should be the same with or without cache"); }); it("should not contain in the cache a file that was deleted", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - - doDelete(cacheLocation); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new FlatESLint({ cwd: path.join(fixtureDir, ".."), @@ -2322,7 +2651,7 @@ describe("FlatESLint", () => { // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2335,10 +2664,10 @@ describe("FlatESLint", () => { const toBeDeletedFile = fs.realpathSync(getFixturePath("cache/src", "file-to-delete.js")); await eslint.lintFiles([badFile, goodFile, toBeDeletedFile]); - const fileCache = fCache.createFromFile(cacheLocation); + const fileCache = fCache.createFromFile(cacheFilePath); let { cache } = fileCache; - assert.strictEqual(typeof cache.getKey(toBeDeletedFile), "object", "the entry for the file to be deleted is in the cache"); + assert.strictEqual(typeof cache.getKey(toBeDeletedFile), "object", "the entry for the file to be deleted should have been in the cache"); // delete the file from the file system fs.unlinkSync(toBeDeletedFile); @@ -2349,15 +2678,19 @@ describe("FlatESLint", () => { */ await eslint.lintFiles([badFile, goodFile]); - cache = JSON.parse(fs.readFileSync(cacheLocation)); + cache = JSON.parse(fs.readFileSync(cacheFilePath)); + + assert.strictEqual(typeof cache[0][toBeDeletedFile], "undefined", "the entry for the file to be deleted should not have been in the cache"); - assert.strictEqual(typeof cache[toBeDeletedFile], "undefined", "the entry for the file to be deleted is not in the cache"); + // make sure that the previos assertion checks the right place + assert.notStrictEqual(typeof cache[0][badFile], "undefined", "the entry for the bad file should have been in the cache"); + assert.notStrictEqual(typeof cache[0][goodFile], "undefined", "the entry for the good file should have been in the cache"); }); it("should contain files that were not visited in the cache provided they still exist", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - - doDelete(cacheLocation); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new FlatESLint({ cwd: path.join(fixtureDir, ".."), @@ -2365,7 +2698,7 @@ describe("FlatESLint", () => { // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2379,31 +2712,35 @@ describe("FlatESLint", () => { await eslint.lintFiles([badFile, goodFile, testFile2]); - let fileCache = fCache.createFromFile(cacheLocation); + let fileCache = fCache.createFromFile(cacheFilePath); let { cache } = fileCache; - assert.strictEqual(typeof cache.getKey(testFile2), "object", "the entry for the test-file2 is in the cache"); + assert.strictEqual(typeof cache.getKey(testFile2), "object", "the entry for the test-file2 should have been in the cache"); /* - * we pass a different set of files minus test-file2 + * we pass a different set of files (minus test-file2) * previous version of file-entry-cache would remove the non visited * entries. 2.0.0 version will keep them unless they don't exist */ await eslint.lintFiles([badFile, goodFile]); - fileCache = fCache.createFromFile(cacheLocation); + fileCache = fCache.createFromFile(cacheFilePath); cache = fileCache.cache; - assert.strictEqual(typeof cache.getKey(testFile2), "object", "the entry for the test-file2 is in the cache"); + assert.strictEqual(typeof cache.getKey(testFile2), "object", "the entry for the test-file2 should have been in the cache"); }); it("should not delete cache when executing on text", async () => { - const cacheLocation = getFixturePath(".eslintcache"); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); + + fs.writeFileSync(cacheFilePath, "[]"); // intenationally invalid to additionally make sure it isn't used eslint = new FlatESLint({ cwd: path.join(fixtureDir, ".."), overrideConfigFile: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2412,20 +2749,24 @@ describe("FlatESLint", () => { } }); - assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should exist"); await eslint.lintText("var foo = 'bar';"); - assert(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should still exist"); }); it("should not delete cache when executing on text with a provided filename", async () => { - const cacheLocation = getFixturePath(".eslintcache"); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); + + fs.writeFileSync(cacheFilePath, "[]"); // intenationally invalid to additionally make sure it isn't used eslint = new FlatESLint({ cwd: path.join(fixtureDir, ".."), overrideConfigFile: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2434,21 +2775,25 @@ describe("FlatESLint", () => { } }); - assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should exist"); await eslint.lintText("var bar = foo;", { filePath: "fixtures/passing.js" }); - assert(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should still exist"); }); it("should not delete cache when executing on files with --cache flag", async () => { - const cacheLocation = getFixturePath(".eslintcache"); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); + + fs.writeFileSync(cacheFilePath, ""); eslint = new FlatESLint({ cwd: path.join(fixtureDir, ".."), overrideConfigFile: true, cache: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, @@ -2458,82 +2803,84 @@ describe("FlatESLint", () => { }); const file = getFixturePath("cli-engine", "console.js"); - assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should exist"); await eslint.lintFiles([file]); - assert(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should still exist"); }); it("should delete cache when executing on files without --cache flag", async () => { - const cacheLocation = getFixturePath(".eslintcache"); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); + + fs.writeFileSync(cacheFilePath, "[]"); // intenationally invalid to additionally make sure it isn't used eslint = new FlatESLint({ cwd: path.join(fixtureDir, ".."), overrideConfigFile: true, - cacheLocation, + cacheLocation: cacheFilePath, overrideConfig: { rules: { "no-console": 0, "no-unused-vars": 2 } } - }); const file = getFixturePath("cli-engine", "console.js"); - assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should exist"); await eslint.lintFiles([file]); - assert(!shell.test("-f", cacheLocation), "the cache for eslint has been deleted"); + assert(!shell.test("-f", cacheFilePath), "the cache for eslint should have been deleted"); }); - describe("cacheFile", () => { - it("should use the specified cache file", async () => { - const customCacheFile = path.resolve(".cache/custom-cache"); + it("should use the specified cache file", async () => { + cacheFilePath = path.resolve(".cache/custom-cache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); - assert(!shell.test("-f", customCacheFile), "the cache for eslint does not exist"); + eslint = new FlatESLint({ + overrideConfigFile: true, - eslint = new FlatESLint({ - overrideConfigFile: true, + // specify a custom cache file + cacheLocation: cacheFilePath, - // specify a custom cache file - cacheLocation: customCacheFile, + // specifying cache true the cache will be created + cache: true, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, - // specifying cache true the cache will be created - cache: true, - overrideConfig: { - rules: { - "no-console": 0, - "no-unused-vars": 2 - } - }, + cwd: path.join(fixtureDir, "..") + }); + const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); + const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); + const result = await eslint.lintFiles([badFile, goodFile]); - cwd: path.join(fixtureDir, "..") - }); - const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); - const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); - const result = await eslint.lintFiles([badFile, goodFile]); + assert(shell.test("-f", cacheFilePath), "the cache for eslint should have been created"); - assert(shell.test("-f", customCacheFile), "the cache for eslint was created"); - const fileCache = fCache.createFromFile(customCacheFile); - const { cache } = fileCache; + const fileCache = fCache.createFromFile(cacheFilePath); + const { cache } = fileCache; - assert(typeof cache.getKey(goodFile) === "object", "the entry for the good file is in the cache"); + assert(typeof cache.getKey(goodFile) === "object", "the entry for the good file should have been in the cache"); + assert(typeof cache.getKey(badFile) === "object", "the entry for the bad file should have been in the cache"); - assert(typeof cache.getKey(badFile) === "object", "the entry for the bad file is in the cache"); - const cachedResult = await eslint.lintFiles([badFile, goodFile]); + const cachedResult = await eslint.lintFiles([badFile, goodFile]); - assert.deepStrictEqual(result, cachedResult, "result is the same with or without cache"); - }); + assert.deepStrictEqual(result, cachedResult, "result should be the same with or without cache"); }); describe("cacheStrategy", () => { it("should detect changes using a file's modification time when set to 'metadata'", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - - doDelete(cacheLocation); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new FlatESLint({ cwd: path.join(fixtureDir, ".."), @@ -2541,7 +2888,7 @@ describe("FlatESLint", () => { // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, cacheStrategy: "metadata", overrideConfig: { rules: { @@ -2555,24 +2902,24 @@ describe("FlatESLint", () => { const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); await eslint.lintFiles([badFile, goodFile]); - let fileCache = fCache.createFromFile(cacheLocation); + let fileCache = fCache.createFromFile(cacheFilePath); const entries = fileCache.normalizeEntries([badFile, goodFile]); entries.forEach(entry => { - assert(entry.changed === false, `the entry for ${entry.key} is initially unchanged`); + assert(entry.changed === false, `the entry for ${entry.key} should have been initially unchanged`); }); // this should result in a changed entry shell.touch(goodFile); - fileCache = fCache.createFromFile(cacheLocation); - assert(fileCache.getFileDescriptor(badFile).changed === false, `the entry for ${badFile} is unchanged`); - assert(fileCache.getFileDescriptor(goodFile).changed === true, `the entry for ${goodFile} is changed`); + fileCache = fCache.createFromFile(cacheFilePath); + assert(fileCache.getFileDescriptor(badFile).changed === false, `the entry for ${badFile} should have been unchanged`); + assert(fileCache.getFileDescriptor(goodFile).changed === true, `the entry for ${goodFile} should have been changed`); }); it("should not detect changes using a file's modification time when set to 'content'", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - - doDelete(cacheLocation); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new FlatESLint({ cwd: path.join(fixtureDir, ".."), @@ -2580,7 +2927,7 @@ describe("FlatESLint", () => { // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, cacheStrategy: "content", overrideConfig: { rules: { @@ -2594,26 +2941,26 @@ describe("FlatESLint", () => { const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); await eslint.lintFiles([badFile, goodFile]); - let fileCache = fCache.createFromFile(cacheLocation, true); + let fileCache = fCache.createFromFile(cacheFilePath, true); let entries = fileCache.normalizeEntries([badFile, goodFile]); entries.forEach(entry => { - assert(entry.changed === false, `the entry for ${entry.key} is initially unchanged`); + assert(entry.changed === false, `the entry for ${entry.key} should have been initially unchanged`); }); // this should NOT result in a changed entry shell.touch(goodFile); - fileCache = fCache.createFromFile(cacheLocation, true); + fileCache = fCache.createFromFile(cacheFilePath, true); entries = fileCache.normalizeEntries([badFile, goodFile]); entries.forEach(entry => { - assert(entry.changed === false, `the entry for ${entry.key} remains unchanged`); + assert(entry.changed === false, `the entry for ${entry.key} should have remained unchanged`); }); }); it("should detect changes using a file's contents when set to 'content'", async () => { - const cacheLocation = getFixturePath(".eslintcache"); - - doDelete(cacheLocation); + cacheFilePath = getFixturePath(".eslintcache"); + doDelete(cacheFilePath); + assert(!shell.test("-f", cacheFilePath), "the cache file already exists and wasn't successfully deleted"); eslint = new FlatESLint({ cwd: path.join(fixtureDir, ".."), @@ -2621,7 +2968,7 @@ describe("FlatESLint", () => { // specifying cache true the cache will be created cache: true, - cacheLocation, + cacheLocation: cacheFilePath, cacheStrategy: "content", overrideConfig: { rules: { @@ -2638,18 +2985,18 @@ describe("FlatESLint", () => { shell.cp(goodFile, goodFileCopy); await eslint.lintFiles([badFile, goodFileCopy]); - let fileCache = fCache.createFromFile(cacheLocation, true); + let fileCache = fCache.createFromFile(cacheFilePath, true); const entries = fileCache.normalizeEntries([badFile, goodFileCopy]); entries.forEach(entry => { - assert(entry.changed === false, `the entry for ${entry.key} is initially unchanged`); + assert(entry.changed === false, `the entry for ${entry.key} should have been initially unchanged`); }); // this should result in a changed entry shell.sed("-i", "abc", "xzy", goodFileCopy); - fileCache = fCache.createFromFile(cacheLocation, true); - assert(fileCache.getFileDescriptor(badFile).changed === false, `the entry for ${badFile} is unchanged`); - assert(fileCache.getFileDescriptor(goodFileCopy).changed === true, `the entry for ${goodFileCopy} is changed`); + fileCache = fCache.createFromFile(cacheFilePath, true); + assert(fileCache.getFileDescriptor(badFile).changed === false, `the entry for ${badFile} should have been unchanged`); + assert(fileCache.getFileDescriptor(goodFileCopy).changed === true, `the entry for ${goodFileCopy} should have been changed`); }); }); }); @@ -3377,7 +3724,7 @@ describe("FlatESLint", () => { const messages = results[0].messages; assert.strictEqual(messages.length, 1); - assert.strictEqual(messages[0].message, "'/*globals*/' has no effect because you have 'noInlineConfig' setting in your config."); + assert.strictEqual(messages[0].message, "'/* globals foo */' has no effect because you have 'noInlineConfig' setting in your config."); }); }); @@ -4250,29 +4597,33 @@ describe("FlatESLint", () => { }); it("should return multiple rule meta when there are multiple linting errors from a plugin", async () => { - const nodePlugin = require("eslint-plugin-n"); + const customPlugin = { + rules: { + "no-var": require("../../../lib/rules/no-var") + } + }; const engine = new FlatESLint({ overrideConfigFile: true, overrideConfig: { plugins: { - n: nodePlugin + "custom-plugin": customPlugin }, rules: { - "n/no-new-require": 2, + "custom-plugin/no-var": 2, semi: 2, quotes: [2, "double"] } } }); - const results = await engine.lintText("new require('hi')"); + const results = await engine.lintText("var foo = 0; var bar = '1'"); const rulesMeta = engine.getRulesMetaForResults(results); assert.strictEqual(rulesMeta.semi, coreRules.get("semi").meta); assert.strictEqual(rulesMeta.quotes, coreRules.get("quotes").meta); assert.strictEqual( - rulesMeta["n/no-new-require"], - nodePlugin.rules["no-new-require"].meta + rulesMeta["custom-plugin/no-var"], + customPlugin.rules["no-var"].meta ); }); @@ -4320,6 +4671,42 @@ describe("FlatESLint", () => { assert.deepStrictEqual(rulesMeta, { "no-var": coreRules.get("no-var").meta }); }); + + it("should return empty object if all messages are related to unknown rules", async () => { + const engine = new FlatESLint({ + overrideConfigFile: true + }); + + const results = await engine.lintText("// eslint-disable-line foo, bar/baz, bar/baz/qux"); + + assert.strictEqual(results[0].messages.length, 3); + assert.strictEqual(results[0].messages[0].ruleId, "foo"); + assert.strictEqual(results[0].messages[1].ruleId, "bar/baz"); + assert.strictEqual(results[0].messages[2].ruleId, "bar/baz/qux"); + + const rulesMeta = engine.getRulesMetaForResults(results); + + assert.strictEqual(Object.keys(rulesMeta).length, 0); + }); + + it("should return object with meta of known rules if some messages are related to unknown rules", async () => { + const engine = new FlatESLint({ + overrideConfigFile: true, + overrideConfig: { rules: { "no-var": "warn" } } + }); + + const results = await engine.lintText("// eslint-disable-line foo, bar/baz, bar/baz/qux\nvar x;"); + + assert.strictEqual(results[0].messages.length, 4); + assert.strictEqual(results[0].messages[0].ruleId, "foo"); + assert.strictEqual(results[0].messages[1].ruleId, "bar/baz"); + assert.strictEqual(results[0].messages[2].ruleId, "bar/baz/qux"); + assert.strictEqual(results[0].messages[3].ruleId, "no-var"); + + const rulesMeta = engine.getRulesMetaForResults(results); + + assert.deepStrictEqual(rulesMeta, { "no-var": coreRules.get("no-var").meta }); + }); }); describe("outputFixes()", () => { @@ -4531,6 +4918,99 @@ describe("FlatESLint", () => { }); }); + describe("configs with 'ignores' and without 'files'", () => { + + // https://github.com/eslint/eslint/issues/17103 + describe("config with ignores: ['error.js']", () => { + const cwd = getFixturePath("config-with-ignores-without-files"); + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd, + files: { + "eslint.config.js": `module.exports = [ + { + rules: { + "no-unused-vars": "error", + }, + }, + { + ignores: ["error.js"], + rules: { + "no-unused-vars": "warn", + }, + }, + ];`, + "error.js": "let unusedVar;", + "warn.js": "let unusedVar;" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("should apply to all files except for 'error.js'", async () => { + const engine = new FlatESLint({ + cwd + }); + + const results = await engine.lintFiles("{error,warn}.js"); + + assert.strictEqual(results.length, 2); + + const [errorResult, warnResult] = results; + + assert.strictEqual(errorResult.filePath, path.join(getPath(), "error.js")); + assert.strictEqual(errorResult.messages.length, 1); + assert.strictEqual(errorResult.messages[0].ruleId, "no-unused-vars"); + assert.strictEqual(errorResult.messages[0].severity, 2); + + assert.strictEqual(warnResult.filePath, path.join(getPath(), "warn.js")); + assert.strictEqual(warnResult.messages.length, 1); + assert.strictEqual(warnResult.messages[0].ruleId, "no-unused-vars"); + assert.strictEqual(warnResult.messages[0].severity, 1); + }); + }); + + describe("config with ignores: ['**/*.json']", () => { + const cwd = getFixturePath("config-with-ignores-without-files"); + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd, + files: { + "eslint.config.js": `module.exports = [ + { + rules: { + "no-undef": "error", + }, + }, + { + ignores: ["**/*.json"], + rules: { + "no-unused-vars": "error", + }, + }, + ];`, + "foo.js": "", + "foo.json": "" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("should not add json files as lint targets", async () => { + const engine = new FlatESLint({ + cwd + }); + + const results = await engine.lintFiles("foo*"); + + // should not lint `foo.json` + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, path.join(getPath(), "foo.js")); + }); + }); + + }); + describe("with ignores config", () => { const root = getFixturePath("cli-engine/ignore-patterns"); @@ -5113,7 +5593,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 } @@ -5503,6 +5983,54 @@ describe("FlatESLint", () => { }); }); + // only works on a Windows machine + if (os.platform() === "win32") { + + // https://github.com/eslint/eslint/issues/17042 + describe("with cwd that is using forward slash on Windows", () => { + const cwd = getFixturePath("example-app3"); + const cwdForwardSlash = cwd.replace(/\\/gu, "/"); + + it("should correctly handle ignore patterns", async () => { + const engine = new FlatESLint({ cwd: cwdForwardSlash }); + const results = await engine.lintFiles(["./src"]); + + // src/dist/2.js should be ignored + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, path.join(cwd, "src\\1.js")); + }); + + it("should pass cwd with backslashes to rules", async () => { + const engine = new FlatESLint({ + cwd: cwdForwardSlash, + overrideConfigFile: true, + overrideConfig: { + plugins: { + test: require(path.join(cwd, "node_modules", "eslint-plugin-test")) + }, + rules: { + "test/report-cwd": "error" + } + } + }); + const results = await engine.lintText(""); + + assert.strictEqual(results[0].messages[0].ruleId, "test/report-cwd"); + assert.strictEqual(results[0].messages[0].message, cwd); + }); + + it("should pass cwd with backslashes to formatters", async () => { + const engine = new FlatESLint({ + cwd: cwdForwardSlash + }); + const results = await engine.lintText(""); + const formatter = await engine.loadFormatter("cwd"); + + assert.strictEqual(formatter.format(results), cwd); + }); + }); + } + }); describe("shouldUseFlatConfig", () => { diff --git a/tests/lib/linter/apply-disable-directives.js b/tests/lib/linter/apply-disable-directives.js index de56f729627..d56bf5bc1b0 100644 --- a/tests/lib/linter/apply-disable-directives.js +++ b/tests/lib/linter/apply-disable-directives.js @@ -680,7 +680,7 @@ describe("apply-disable-directives", () => { applyDisableDirectives({ directives: [ { parentComment: createParentComment([0, 31]), type: "disable-next-line", line: 1, column: 1, ruleId: null, justification: "j1" }, - { type: "enable", line: 1, column: 5, ruleId: null, justification: "j2" } + { parentComment: createParentComment([31, 50]), type: "enable", line: 1, column: 31, ruleId: null, justification: "j2" } ], problems: [{ line: 2, column: 2, ruleId: "foo" }] }), @@ -1365,28 +1365,140 @@ describe("apply-disable-directives", () => { ); }); - it("Adds a problem for // eslint-disable-line", () => { + it("Adds a problem for /* eslint-enable */", () => { assert.deepStrictEqual( applyDisableDirectives({ directives: [{ - parentComment: createParentComment([0, 22]), - type: "disable-line", + parentComment: createParentComment([0, 20]), + type: "enable", line: 1, column: 1, - ruleId: null, justification: "justification" }], problems: [], reportUnusedDisableDirectives: "error" }), + [{ + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 2, + nodeType: null + }] + ); + }); + + it("Does not fix a problem for /* eslint-enable */ when disableFixes is enabled", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [{ + parentComment: createParentComment([0, 20]), + type: "enable", + line: 1, + column: 1, + justification: "justification" + }], + disableFixes: true, + problems: [], + reportUnusedDisableDirectives: "error" + }), + [{ + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + line: 1, + column: 1, + severity: 2, + nodeType: null + }] + ); + }); + + it("Does not add a problem for /* eslint-disable */ /* (problem) */ /* eslint-enable */", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { type: "disable", line: 1, column: 1, ruleId: null, justification: "justification" }, + { type: "enable", line: 3, column: 1, ruleId: null, justification: "justification" } + ], + problems: [{ line: 2, column: 1, ruleId: "foo" }], + reportUnusedDisableDirectives: "error" + }), + [{ line: 2, column: 1, ruleId: "foo", suppressions: [{ kind: "directive", justification: "justification" }] }] + ); + }); + + it("Adds a problem for /* eslint-enable foo */", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [{ + parentComment: createParentComment([0, 21]), + type: "enable", + line: 1, + column: 1, + ruleId: "foo", + justification: "justification" + }], + problems: [], + reportUnusedDisableDirectives: "error" + }), + [{ + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'foo').", + line: 1, + column: 1, + fix: { + range: [0, 21], + text: " " + }, + severity: 2, + nodeType: null + }] + ); + }); + + it("Adds a problem for /* eslint-disable not-foo */ /* (problem from not-foo) */ /* eslint-enable foo */", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment: createParentComment([0, 24]), + type: "disable", + line: 1, + column: 1, + ruleId: "not-foo", + justification: "justification" + }, + { + parentComment: createParentComment([48, 72]), + type: "enable", + line: 3, + column: 1, + ruleId: "foo", + justification: "justification" + } + ], + problems: [{ line: 2, column: 1, ruleId: "not-foo" }], + reportUnusedDisableDirectives: "error" + }), [ + { + ruleId: "not-foo", + line: 2, + column: 1, + suppressions: [{ justification: "justification", kind: "directive" }] + }, { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", - line: 1, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'foo').", + line: 3, column: 1, fix: { - range: [0, 22], + range: [48, 72], text: " " }, severity: 2, @@ -1396,40 +1508,1155 @@ describe("apply-disable-directives", () => { ); }); + it("Adds a problem for /* eslint-disable foo */ /* (problem from foo) */ /* eslint-enable */ /* eslint-enable */ /* eslint-enable foo */", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment: createParentComment([0, 21]), + type: "disable", + line: 1, + column: 1, + ruleId: "foo", + justification: "j1" + }, + { + parentComment: createParentComment([42, 63]), + type: "enable", + line: 3, + column: 1, + ruleId: null, + justification: "j1" + }, + { + parentComment: createParentComment([63, 84]), + type: "enable", + line: 4, + column: 1, + ruleId: null, + justification: "j1" + }, + { + parentComment: createParentComment([84, 105]), + type: "enable", + line: 5, + column: 1, + ruleId: "foo", + justification: "j2" + } + ], + problems: [{ line: 2, column: 1, ruleId: "foo" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: "foo", + line: 2, + column: 1, + suppressions: [{ justification: "j1", kind: "directive" }] + }, + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + fix: { + range: [63, 84], + text: " " + }, + line: 4, + column: 1, + severity: 2, + nodeType: null + }, + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'foo').", + fix: { + range: [84, 105], + text: " " + }, + line: 5, + column: 1, + severity: 2, + nodeType: null + } + ] + ); + }); - it("Does not add a problem for // eslint-disable-line (problem)", () => { + it("Adds a problem for /* eslint-disable foo */ /* (problem from foo) */ /* eslint-enable */ /* eslint-enable */", () => { assert.deepStrictEqual( applyDisableDirectives({ - directives: [{ type: "disable-line", line: 1, column: 1, ruleId: null, justification: "justification" }], - problems: [{ line: 1, column: 10, ruleId: "foo" }], + directives: [ + { + parentComment: createParentComment([0, 21]), + type: "disable", + line: 1, + column: 1, + ruleId: "foo", + justification: "j1" + }, + { + parentComment: createParentComment([42, 63]), + type: "enable", + line: 3, + column: 1, + ruleId: null, + justification: "j1" + }, + { + parentComment: createParentComment([63, 84]), + type: "enable", + line: 4, + column: 1, + ruleId: null, + justification: "j1" + } + ], + problems: [{ line: 2, column: 1, ruleId: "foo" }], reportUnusedDisableDirectives: "error" }), - [{ line: 1, column: 10, ruleId: "foo", suppressions: [{ kind: "directive", justification: "justification" }] }] + [ + { + ruleId: "foo", + line: 2, + column: 1, + suppressions: [{ justification: "j1", kind: "directive" }] + }, + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + fix: { + range: [63, 84], + text: " " + }, + line: 4, + column: 1, + severity: 2, + nodeType: null + } + ] ); }); - it("Adds a problem for // eslint-disable-next-line", () => { + it("Adds two problems for /* eslint-enable */ /* eslint-enable */", () => { assert.deepStrictEqual( applyDisableDirectives({ - directives: [{ - parentComment: createParentComment([0, 27]), - type: "disable-next-line", + directives: [ + { + parentComment: createParentComment([0, 21]), + type: "enable", + line: 1, + column: 1, + ruleId: null, + justification: "j1" + }, + { + parentComment: createParentComment([21, 42]), + type: "enable", + line: 2, + column: 1, + ruleId: null, + justification: "j2" + } + ], + problems: [], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", line: 1, - column: 2, + column: 1, + fix: { + range: [0, 21], + text: " " + }, + severity: 2, + nodeType: null + }, + { ruleId: null, - justification: "justification" - }], + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + line: 2, + column: 1, + fix: { + range: [21, 42], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); + + it("Adds a problem for /* eslint-enable */ /* eslint-disable */ /* (problem) */ /* eslint-enable */", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment: createParentComment([0, 21]), + type: "enable", + line: 1, + column: 1, + ruleId: null, + justification: "j1" + }, + { + parentComment: createParentComment([21, 42]), + type: "disable", + line: 2, + column: 1, + ruleId: null, + justification: "j2" + }, + { + parentComment: createParentComment([63, 84]), + type: "enable", + line: 4, + column: 1, + ruleId: null, + justification: "j3" + } + ], + problems: [{ line: 3, column: 1, ruleId: "foo" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + line: 1, + column: 1, + fix: { + range: [0, 21], + text: " " + }, + severity: 2, + nodeType: null + }, + { + line: 3, + column: 1, + ruleId: "foo", + suppressions: [{ kind: "directive", justification: "j2" }] + } + ] + ); + }); + + it("Adds a problem for /* eslint-disable foo */ /* (problem from foo) */ /* eslint-enable */ /* eslint-enable foo */", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment: createParentComment([0, 21]), + type: "disable", + line: 1, + column: 1, + ruleId: "foo", + justification: "j1" + }, + { + parentComment: createParentComment([42, 63]), + type: "enable", + line: 3, + column: 1, + ruleId: null, + justification: "j2" + }, + { + parentComment: createParentComment([63, 84]), + type: "enable", + line: 4, + column: 1, + ruleId: "foo", + justification: "j3" + } + ], + problems: [{ line: 2, column: 1, ruleId: "foo" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + line: 2, + column: 1, + ruleId: "foo", + suppressions: [{ kind: "directive", justification: "j1" }] + }, + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'foo').", + line: 4, + column: 1, + fix: { + range: [63, 84], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); + + it("Does not add a problem for /* eslint-disable foo */ /* (problem from foo) */ /* eslint-enable foo */", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { type: "disable", line: 1, column: 1, ruleId: "foo", justification: "j1" }, + { type: "enable", line: 3, column: 1, ruleId: "foo", justification: "j2" } + ], + problems: [{ line: 2, column: 1, ruleId: "foo" }], + reportUnusedDisableDirectives: "error" + }), + [{ line: 2, column: 1, ruleId: "foo", suppressions: [{ kind: "directive", justification: "j1" }] }] + ); + }); + + it("Adds a problem for /* eslint-disable foo */ /* (problem from foo) */ /* eslint-enable foo */ /* eslint-enable */", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment: createParentComment([0, 21]), + type: "disable", + line: 1, + column: 1, + ruleId: "foo", + justification: "j1" + }, + { + parentComment: createParentComment([42, 63]), + type: "enable", + line: 3, + column: 1, + ruleId: "foo", + justification: "j2" + }, + { + parentComment: createParentComment([63, 84]), + type: "enable", + line: 4, + column: 1, + ruleId: null, + justification: "j3" + } + ], + problems: [{ line: 2, column: 1, ruleId: "foo" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + line: 2, + column: 1, + ruleId: "foo", + suppressions: [ + { kind: "directive", justification: "j1" } + ] + }, + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + line: 4, + column: 1, + fix: { + range: [63, 84], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); + + it("Adds a problem for /* eslint-disable bar */ /* (problem from bar) */ /* eslint-enable foo */ /* eslint-enable */", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment: createParentComment([0, 20]), + type: "disable", + line: 1, + column: 1, + ruleId: "bar", + justification: "j1" + }, + { + parentComment: createParentComment([60, 80]), + type: "enable", + line: 3, + column: 1, + ruleId: "foo", + justification: "j2" + }, + { + parentComment: createParentComment([80, 100]), + type: "enable", + line: 4, + column: 1, + ruleId: null, + justification: "j3" + } + ], + problems: [{ line: 2, column: 1, ruleId: "bar" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + line: 2, + column: 1, + ruleId: "bar", + suppressions: [{ kind: "directive", justification: "j1" }] + }, + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'foo').", + line: 3, + column: 1, + fix: { + range: [60, 80], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); + + it("Adds a problem for /* eslint-disable foo */ /* (problem from foo) */ /* eslint-enable foo */ /* eslint-enable */", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment: createParentComment([0, 20]), + type: "disable", + line: 1, + column: 1, + ruleId: "foo", + justification: "j1" + }, + { + parentComment: createParentComment([60, 80]), + type: "enable", + line: 3, + column: 1, + ruleId: "foo", + justification: "j2" + }, + { + parentComment: createParentComment([80, 100]), + type: "enable", + line: 4, + column: 1, + ruleId: "foo", + justification: "j3" + } + ], + problems: [{ line: 2, column: 1, ruleId: "foo" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + line: 2, + column: 1, + ruleId: "foo", + suppressions: [{ kind: "directive", justification: "j1" }] + }, + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'foo').", + line: 4, + column: 1, + fix: { + range: [80, 100], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); + + it("Adds two problems for /* eslint-disable foo */ /* (problem from foo) */ /* eslint-enable foo */ /* eslint-enable foo */ /* eslint-enable */", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment: createParentComment([0, 20]), + type: "disable", + line: 1, + column: 1, + ruleId: "foo", + justification: "j1" + }, + { + parentComment: createParentComment([40, 60]), + type: "enable", + line: 3, + column: 1, + ruleId: "foo", + justification: "j2" + }, + { + parentComment: createParentComment([60, 80]), + type: "enable", + line: 4, + column: 1, + ruleId: "foo", + justification: "j3" + }, + { + parentComment: createParentComment([80, 100]), + type: "enable", + line: 5, + column: 1, + ruleId: null, + justification: "j4" + } + ], + problems: [{ line: 2, column: 1, ruleId: "foo" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: "foo", + line: 2, + column: 1, + suppressions: [{ kind: "directive", justification: "j1" }] + }, + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'foo').", + line: 4, + column: 1, + fix: { + range: [60, 80], + text: " " + }, + severity: 2, + nodeType: null + }, + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + line: 5, + column: 1, + fix: { + range: [80, 100], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); + + it("Adds a problem for /* eslint-disable used */ /* (problem from used) */ /* eslint-disable used */ /* (problem from used) */ /* eslint-enable used */ /* eslint-enable */", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment: createParentComment([0, 20]), + ruleId: "used", + type: "disable", + line: 1, + column: 1, + justification: "j1" + }, + { + parentComment: createParentComment([40, 60]), + ruleId: "used", + type: "disable", + line: 3, + column: 1, + justification: "j2" + }, + { + parentComment: createParentComment([80, 100]), + ruleId: "used", + type: "enable", + line: 5, + column: 1, + justification: "j3" + }, + { + parentComment: createParentComment([100, 120]), + ruleId: null, + type: "enable", + line: 6, + column: 1, + justification: "j4" + } + ], + problems: [{ line: 2, column: 1, ruleId: "used" }, { line: 4, column: 1, ruleId: "used" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j1" }] + }, + { + line: 4, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j1" }, { kind: "directive", justification: "j2" }] + }, + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + line: 6, + column: 1, + fix: { + range: [100, 120], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); + + it("Adds a problem for // eslint-disable-line", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [{ + parentComment: createParentComment([0, 22]), + type: "disable-line", + line: 1, + column: 1, + ruleId: null, + justification: "justification" + }], + problems: [], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 22], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); + + + it("Does not add a problem for // eslint-disable-line (problem)", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [{ type: "disable-line", line: 1, column: 1, ruleId: null, justification: "justification" }], + problems: [{ line: 1, column: 10, ruleId: "foo" }], + reportUnusedDisableDirectives: "error" + }), + [{ line: 1, column: 10, ruleId: "foo", suppressions: [{ kind: "directive", justification: "justification" }] }] + ); + }); + + it("Adds a problem for // eslint-disable-next-line", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [{ + parentComment: createParentComment([0, 27]), + type: "disable-next-line", + line: 1, + column: 2, + ruleId: null, + justification: "justification" + }], + problems: [], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 2, + fix: { + range: [0, 27], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); + + it("Does not add a problem for // eslint-disable-next-line \\n (problem)", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [{ type: "disable-next-line", line: 1, column: 1, ruleId: null, justification: "justification" }], + problems: [{ line: 2, column: 10, ruleId: "foo" }], + reportUnusedDisableDirectives: "error" + }), + [{ line: 2, column: 10, ruleId: "foo", suppressions: [{ kind: "directive", justification: "justification" }] }] + ); + }); + + it("adds two problems for /* eslint-disable */ // eslint-disable-line", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { parentComment: createParentComment([0, 20]), type: "disable", line: 1, column: 1, ruleId: null }, + { parentComment: createParentComment([20, 43]), type: "disable-line", line: 1, column: 22, ruleId: null } + ], + problems: [], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 2, + nodeType: null + }, + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 22, + fix: { + range: [20, 43], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + }); + + it("Does not add problems when reportUnusedDisableDirectives: \"off\" is used", () => { + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [{ + parentComment: createParentComment([0, 27]), + type: "disable-next-line", + line: 1, + column: 1, + ruleId: null, + justification: "justification" + }], + problems: [], + reportUnusedDisableDirectives: "off" + }), + [] + ); + }); + }); + + describe("unused rules within directives", () => { + it("Adds a problem for /* eslint-disable used, unused */", () => { + const parentComment = createParentComment([0, 32], " eslint-disable used, unused ", ["used", "unused"]); + + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment, + ruleId: "used", + type: "disable", + line: 1, + column: 18, + justification: "j1" + }, + { + parentComment, + ruleId: "unused", + type: "disable", + line: 1, + column: 22, + justification: "j2" + } + ], + problems: [{ line: 2, column: 1, ruleId: "used" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'unused').", + line: 1, + column: 22, + fix: { + range: [22, 30], + text: "" + }, + severity: 2, + nodeType: null + }, + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j1" }] + } + ] + ); + }); + it("Adds a problem for /* eslint-disable used , unused , -- unused and used are ok */", () => { + const parentComment = createParentComment([0, 62], " eslint-disable used , unused , -- unused and used are ok ", ["used", "unused"]); + + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment, + ruleId: "used", + type: "disable", + line: 1, + column: 18, + justification: "j1" + }, + { + parentComment, + ruleId: "unused", + type: "disable", + line: 1, + column: 24, + justification: "j2" + } + ], + problems: [{ line: 2, column: 1, ruleId: "used" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'unused').", + line: 1, + column: 24, + fix: { + range: [23, 32], + text: "" + }, + severity: 2, + nodeType: null + }, + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j1" }] + } + ] + ); + }); + + it("Adds a problem for /* eslint-disable unused, used */", () => { + const parentComment = createParentComment([0, 32], " eslint-disable unused, used ", ["unused", "used"]); + + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment, + ruleId: "unused", + type: "disable", + line: 1, + column: 18, + justification: "j1" + }, + { + parentComment, + ruleId: "used", + type: "disable", + line: 1, + column: 25, + justification: "j2" + } + ], + problems: [{ line: 2, column: 1, ruleId: "used" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'unused').", + line: 1, + column: 18, + fix: { + range: [18, 26], + text: "" + }, + severity: 2, + nodeType: null + }, + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j2" }] + } + ] + ); + }); + + it("Adds a problem for /* eslint-disable unused,, ,, used */", () => { + const parentComment = createParentComment([0, 37], " eslint-disable unused,, ,, used ", ["unused", "used"]); + + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment, + ruleId: "unused", + type: "disable", + line: 1, + column: 18, + justification: "j1" + }, + { + parentComment, + ruleId: "used", + type: "disable", + line: 1, + column: 29, + justification: "j2" + } + ], + problems: [{ line: 2, column: 1, ruleId: "used" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'unused').", + line: 1, + column: 18, + fix: { + range: [18, 25], + text: "" + }, + severity: 2, + nodeType: null + }, + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j2" }] + } + ] + ); + }); + + it("Adds a problem for /* eslint-disable unused-1, unused-2, used */", () => { + const parentComment = createParentComment([0, 45], " eslint-disable unused-1, unused-2, used ", ["unused-1", "unused-2", "used"]); + + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment, + ruleId: "unused-1", + type: "disable", + line: 1, + column: 18, + justification: "j1" + }, + { + parentComment, + ruleId: "unused-2", + type: "disable", + line: 1, + column: 28, + justification: "j2" + }, + { + parentComment, + ruleId: "used", + type: "disable", + line: 1, + column: 38, + justification: "j3" + } + ], + problems: [{ line: 2, column: 1, ruleId: "used" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'unused-1').", + line: 1, + column: 18, + fix: { + range: [18, 28], + text: "" + }, + severity: 2, + nodeType: null + }, + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'unused-2').", + line: 1, + column: 28, + fix: { + range: [26, 36], + text: "" + }, + severity: 2, + nodeType: null + }, + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j3" }] + } + ] + ); + }); + + it("Adds a problem for /* eslint-disable unused-1, unused-2, used, unused-3 */", () => { + const parentComment = createParentComment([0, 55], " eslint-disable unused-1, unused-2, used, unused-3 ", ["unused-1", "unused-2", "used", "unused-3"]); + + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment, + ruleId: "unused-1", + type: "disable", + line: 1, + column: 18, + justification: "j1" + }, + { + parentComment, + ruleId: "unused-2", + type: "disable", + line: 1, + column: 28, + justification: "j2" + }, + { + parentComment, + ruleId: "used", + type: "disable", + line: 1, + column: 38, + justification: "j3" + }, + { + parentComment, + ruleId: "unused-3", + type: "disable", + line: 1, + column: 43, + justification: "j4" + } + ], + problems: [{ line: 2, column: 1, ruleId: "used" }], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'unused-1').", + line: 1, + column: 18, + fix: { + range: [18, 28], + text: "" + }, + severity: 2, + nodeType: null + }, + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'unused-2').", + line: 1, + column: 28, + fix: { + range: [26, 36], + text: "" + }, + severity: 2, + nodeType: null + }, + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'unused-3').", + line: 1, + column: 43, + fix: { + range: [42, 52], + text: "" + }, + severity: 2, + nodeType: null + }, + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j3" }] + } + ] + ); + }); + + it("Adds a problem for /* eslint-disable unused-1, unused-2 */", () => { + const parentComment = createParentComment([0, 39], " eslint-disable unused-1, unused-2 ", ["unused-1", "unused-2"]); + + assert.deepStrictEqual( + applyDisableDirectives({ + directives: [ + { + parentComment, + ruleId: "unused-1", + type: "disable", + line: 1, + column: 18, + justification: "j1" + }, + { + parentComment, + ruleId: "unused-2", + type: "disable", + line: 1, + column: 28, + justification: "j2" + } + ], problems: [], reportUnusedDisableDirectives: "error" }), [ { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", + message: "Unused eslint-disable directive (no problems were reported from 'unused-1' or 'unused-2').", line: 1, - column: 2, + column: 18, fix: { - range: [0, 27], + range: [0, 39], text: " " }, severity: 2, @@ -1439,23 +2666,33 @@ describe("apply-disable-directives", () => { ); }); - it("Does not add a problem for // eslint-disable-next-line \\n (problem)", () => { - assert.deepStrictEqual( - applyDisableDirectives({ - directives: [{ type: "disable-next-line", line: 1, column: 1, ruleId: null, justification: "justification" }], - problems: [{ line: 2, column: 10, ruleId: "foo" }], - reportUnusedDisableDirectives: "error" - }), - [{ line: 2, column: 10, ruleId: "foo", suppressions: [{ kind: "directive", justification: "justification" }] }] - ); - }); + it("Adds a problem for /* eslint-disable unused-1, unused-2, unused-3 */", () => { + const parentComment = createParentComment([0, 49], " eslint-disable unused-1, unused-2, unused-3 ", ["unused-1", "unused-2", "unused-3"]); - it("adds two problems for /* eslint-disable */ // eslint-disable-line", () => { assert.deepStrictEqual( applyDisableDirectives({ directives: [ - { parentComment: createParentComment([0, 20]), type: "disable", line: 1, column: 1, ruleId: null }, - { parentComment: createParentComment([20, 43]), type: "disable-line", line: 1, column: 22, ruleId: null } + { + parentComment, + ruleId: "unused-1", + type: "disable", + line: 1, + column: 18 + }, + { + parentComment, + ruleId: "unused-2", + type: "disable", + line: 1, + column: 28 + }, + { + parentComment, + ruleId: "unused-3", + type: "disable", + line: 1, + column: 38 + } ], problems: [], reportUnusedDisableDirectives: "error" @@ -1463,23 +2700,11 @@ describe("apply-disable-directives", () => { [ { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", - line: 1, - column: 1, - fix: { - range: [0, 20], - text: " " - }, - severity: 2, - nodeType: null - }, - { - ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", + message: "Unused eslint-disable directive (no problems were reported from 'unused-1', 'unused-2', or 'unused-3').", line: 1, - column: 22, + column: 18, fix: { - range: [20, 43], + range: [0, 49], text: " " }, severity: 2, @@ -1489,263 +2714,356 @@ describe("apply-disable-directives", () => { ); }); - it("Does not add problems when reportUnusedDisableDirectives: \"off\" is used", () => { + it("Adds a problem for /* eslint-disable foo */ \\n (problem from foo and bar) // eslint-disable-line foo, bar", () => { assert.deepStrictEqual( applyDisableDirectives({ - directives: [{ - parentComment: createParentComment([0, 27]), - type: "disable-next-line", - line: 1, + directives: [ + { + parentComment: createParentComment([0, 29], " eslint-disable foo ", ["foo"]), + ruleId: "foo", + type: "disable", + line: 1, + column: 1, + justification: "j1" + }, + { + parentComment: createParentComment([41, 81], " eslint-disable-line foo, bar", ["foo", "bar"]), + ruleId: "foo", + type: "disable-line", + line: 2, + column: 11, + justification: "j2" + }, + { + parentComment: createParentComment([41, 81], " eslint-disable-line foo, bar ", ["foo", "bar"]), + ruleId: "bar", + type: "disable-line", + line: 2, + column: 11, + justification: "j2" + } + ], + problems: [ + { line: 2, column: 1, ruleId: "bar" }, + { line: 2, column: 6, ruleId: "foo" } + ], + reportUnusedDisableDirectives: "error" + }), + [ + { + ruleId: "bar", + line: 2, column: 1, + suppressions: [{ kind: "directive", justification: "j2" }] + }, + { + ruleId: "foo", + line: 2, + column: 6, + suppressions: [ + { kind: "directive", justification: "j1" }, + { kind: "directive", justification: "j2" } + ] + }, + { ruleId: null, - justification: "justification" - }], - problems: [], - reportUnusedDisableDirectives: "off" - }), - [] + message: "Unused eslint-disable directive (no problems were reported from 'foo').", + line: 2, + column: 11, + fix: { + range: [64, 69], + text: "" + }, + severity: 2, + nodeType: null + } + ] ); }); - }); - describe("unused rules within directives", () => { - it("Adds a problem for /* eslint-disable used, unused */", () => { - const parentComment = createParentComment([0, 32], " eslint-disable used, unused ", ["used", "unused"]); + it("Adds a problem for /* eslint-disable used */ /* (problem from used) */ /* eslint-enable used, unused */", () => { + const parentComment = createParentComment([0, 32], " eslint-enable used, unused ", ["used", "unused"]); assert.deepStrictEqual( applyDisableDirectives({ directives: [ { - parentComment, + parentComment: createParentComment([0, 29], " eslint-disable foo ", ["foo"]), ruleId: "used", type: "disable", line: 1, - column: 18, + column: 1, justification: "j1" }, { parentComment, - ruleId: "unused", - type: "disable", - line: 1, - column: 22, + ruleId: "used", + type: "enable", + line: 3, + column: 1, justification: "j2" + }, + { + parentComment, + ruleId: "unused", + type: "enable", + line: 4, + column: 1, + justification: "j3" } ], problems: [{ line: 2, column: 1, ruleId: "used" }], reportUnusedDisableDirectives: "error" }), [ + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j1" }] + }, { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'unused').", - line: 1, - column: 22, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'unused').", + line: 4, + column: 1, fix: { - range: [22, 30], + range: [21, 29], text: "" }, severity: 2, nodeType: null - }, - { - line: 2, - column: 1, - ruleId: "used", - suppressions: [{ kind: "directive", justification: "j1" }] } ] ); }); - it("Adds a problem for /* eslint-disable used , unused , -- unused and used are ok */", () => { - const parentComment = createParentComment([0, 62], " eslint-disable used , unused , -- unused and used are ok ", ["used", "unused"]); + it("Adds a problem for /* eslint-disable used */ /* (problem from used) */ /* eslint-enable used , unused , -- unused and used are ok */", () => { + const parentComment = createParentComment([0, 62], " eslint-enable used , unused , -- unused and used are ok ", ["used", "unused"]); assert.deepStrictEqual( applyDisableDirectives({ directives: [ { - parentComment, + parentComment: createParentComment([0, 29], " eslint-disable foo ", ["foo"]), ruleId: "used", type: "disable", line: 1, - column: 18, + column: 1, justification: "j1" }, { parentComment, - ruleId: "unused", - type: "disable", - line: 1, - column: 24, + ruleId: "used", + type: "enable", + line: 3, + column: 1, justification: "j2" + }, + { + parentComment, + ruleId: "unused", + type: "enable", + line: 4, + column: 1, + justification: "j3" } ], problems: [{ line: 2, column: 1, ruleId: "used" }], reportUnusedDisableDirectives: "error" }), [ + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j1" }] + }, { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'unused').", - line: 1, - column: 24, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'unused').", + line: 4, + column: 1, fix: { - range: [23, 32], + range: [22, 31], text: "" }, severity: 2, nodeType: null - }, - { - line: 2, - column: 1, - ruleId: "used", - suppressions: [{ kind: "directive", justification: "j1" }] } ] ); }); - it("Adds a problem for /* eslint-disable unused, used */", () => { - const parentComment = createParentComment([0, 32], " eslint-disable unused, used ", ["unused", "used"]); + it("Adds a problem for /* eslint-disable used */ /* (problem from used) */ /* eslint-enable unused, used */", () => { + const parentComment = createParentComment([0, 32], " eslint-enable unused, used ", ["unused", "used"]); assert.deepStrictEqual( applyDisableDirectives({ directives: [ { - parentComment, - ruleId: "unused", + parentComment: createParentComment([0, 29], " eslint-disable foo ", ["foo"]), + ruleId: "used", type: "disable", line: 1, - column: 18, + column: 1, justification: "j1" }, { parentComment, - ruleId: "used", - type: "disable", - line: 1, - column: 25, + ruleId: "unused", + type: "enable", + line: 3, + column: 1, justification: "j2" + }, + { + parentComment, + ruleId: "used", + type: "enable", + line: 4, + column: 1, + justification: "j3" } ], problems: [{ line: 2, column: 1, ruleId: "used" }], reportUnusedDisableDirectives: "error" }), [ + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j1" }] + }, { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'unused').", - line: 1, - column: 18, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'unused').", + line: 3, + column: 1, fix: { - range: [18, 26], + range: [17, 25], text: "" }, severity: 2, nodeType: null - }, - { - line: 2, - column: 1, - ruleId: "used", - suppressions: [{ kind: "directive", justification: "j2" }] } ] ); }); - it("Adds a problem for /* eslint-disable unused,, ,, used */", () => { - const parentComment = createParentComment([0, 37], " eslint-disable unused,, ,, used ", ["unused", "used"]); + it("Adds a problem for /* eslint-disable used */ /* (problem from used) */ /* eslint-enable unused,, ,, used */", () => { + const parentComment = createParentComment([0, 37], " eslint-enable unused,, ,, used ", ["unused", "used"]); assert.deepStrictEqual( applyDisableDirectives({ directives: [ { - parentComment, - ruleId: "unused", + parentComment: createParentComment([0, 29], " eslint-disable foo ", ["foo"]), + ruleId: "used", type: "disable", line: 1, - column: 18, + column: 1, justification: "j1" }, { parentComment, - ruleId: "used", - type: "disable", - line: 1, - column: 29, + ruleId: "unused", + type: "enable", + line: 3, + column: 1, justification: "j2" + }, + { + parentComment, + ruleId: "used", + type: "enable", + line: 4, + column: 1, + justification: "j3" } ], problems: [{ line: 2, column: 1, ruleId: "used" }], reportUnusedDisableDirectives: "error" }), [ + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j1" }] + }, { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'unused').", - line: 1, - column: 18, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'unused').", + line: 3, + column: 1, fix: { - range: [18, 25], + range: [17, 24], text: "" }, severity: 2, nodeType: null - }, - { - line: 2, - column: 1, - ruleId: "used", - suppressions: [{ kind: "directive", justification: "j2" }] } ] ); }); - it("Adds a problem for /* eslint-disable unused-1, unused-2, used */", () => { - const parentComment = createParentComment([0, 45], " eslint-disable unused-1, unused-2, used ", ["unused-1", "unused-2", "used"]); + it("Adds a problem for /* eslint-disable used */ /* (problem from used) */ /* eslint-enable unused-1, unused-2, used */", () => { + const parentComment = createParentComment([0, 45], " eslint-enable unused-1, unused-2, used ", ["unused-1", "unused-2", "used"]); assert.deepStrictEqual( applyDisableDirectives({ directives: [ { - parentComment, - ruleId: "unused-1", + parentComment: createParentComment([0, 29], " eslint-disable foo ", ["foo"]), + ruleId: "used", type: "disable", line: 1, - column: 18, + column: 1, justification: "j1" }, { parentComment, - ruleId: "unused-2", - type: "disable", - line: 1, - column: 28, + ruleId: "unused-1", + type: "enable", + line: 3, + column: 1, justification: "j2" }, { parentComment, - ruleId: "used", - type: "disable", - line: 1, - column: 38, + ruleId: "unused-2", + type: "enable", + line: 4, + column: 1, justification: "j3" + }, + { + parentComment, + ruleId: "used", + type: "enable", + line: 5, + column: 1, + justification: "j4" } ], problems: [{ line: 2, column: 1, ruleId: "used" }], reportUnusedDisableDirectives: "error" }), [ + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j1" }] + }, { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'unused-1').", - line: 1, - column: 18, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'unused-1').", + line: 3, + column: 1, fix: { - range: [18, 28], + range: [17, 27], text: "" }, severity: 2, @@ -1753,76 +3071,84 @@ describe("apply-disable-directives", () => { }, { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'unused-2').", - line: 1, - column: 28, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'unused-2').", + line: 4, + column: 1, fix: { - range: [26, 36], + range: [25, 35], text: "" }, severity: 2, nodeType: null - }, - { - line: 2, - column: 1, - ruleId: "used", - suppressions: [{ kind: "directive", justification: "j3" }] } ] ); }); - it("Adds a problem for /* eslint-disable unused-1, unused-2, used, unused-3 */", () => { - const parentComment = createParentComment([0, 55], " eslint-disable unused-1, unused-2, used, unused-3 ", ["unused-1", "unused-2", "used", "unused-3"]); + it("Adds a problem for /* eslint-disable used */ /* (problem from used) */ /* eslint-enable unused-1, unused-2, used, unused-3 */", () => { + const parentComment = createParentComment([0, 55], " eslint-enable unused-1, unused-2, used, unused-3 ", ["unused-1", "unused-2", "used", "unused-3"]); assert.deepStrictEqual( applyDisableDirectives({ directives: [ { - parentComment, - ruleId: "unused-1", + parentComment: createParentComment([0, 29], " eslint-disable foo ", ["foo"]), + ruleId: "used", type: "disable", line: 1, - column: 18, + column: 1, justification: "j1" }, { parentComment, - ruleId: "unused-2", - type: "disable", - line: 1, - column: 28, + ruleId: "unused-1", + type: "enable", + line: 3, + column: 1, justification: "j2" }, { parentComment, - ruleId: "used", - type: "disable", - line: 1, - column: 38, + ruleId: "unused-2", + type: "enable", + line: 4, + column: 1, justification: "j3" }, { parentComment, - ruleId: "unused-3", - type: "disable", - line: 1, - column: 43, + ruleId: "used", + type: "enable", + line: 5, + column: 1, justification: "j4" + }, + { + parentComment, + ruleId: "unused-3", + type: "enable", + line: 6, + column: 1, + justification: "j5" } ], problems: [{ line: 2, column: 1, ruleId: "used" }], reportUnusedDisableDirectives: "error" }), [ + { + line: 2, + column: 1, + ruleId: "used", + suppressions: [{ kind: "directive", justification: "j1" }] + }, { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'unused-1').", - line: 1, - column: 18, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'unused-1').", + line: 3, + column: 1, fix: { - range: [18, 28], + range: [17, 27], text: "" }, severity: 2, @@ -1830,11 +3156,11 @@ describe("apply-disable-directives", () => { }, { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'unused-2').", - line: 1, - column: 28, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'unused-2').", + line: 4, + column: 1, fix: { - range: [26, 36], + range: [25, 35], text: "" }, severity: 2, @@ -1842,28 +3168,22 @@ describe("apply-disable-directives", () => { }, { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'unused-3').", - line: 1, - column: 43, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'unused-3').", + line: 6, + column: 1, fix: { - range: [42, 52], + range: [41, 51], text: "" }, severity: 2, nodeType: null - }, - { - line: 2, - column: 1, - ruleId: "used", - suppressions: [{ kind: "directive", justification: "j3" }] } ] ); }); - it("Adds a problem for /* eslint-disable unused-1, unused-2 */", () => { - const parentComment = createParentComment([0, 39], " eslint-disable unused-1, unused-2 ", ["unused-1", "unused-2"]); + it("Adds a problem for /* eslint-enable unused-1, unused-2 */", () => { + const parentComment = createParentComment([0, 39], " eslint-enable unused-1, unused-2 ", ["unused-1", "unused-2"]); assert.deepStrictEqual( applyDisableDirectives({ @@ -1871,7 +3191,7 @@ describe("apply-disable-directives", () => { { parentComment, ruleId: "unused-1", - type: "disable", + type: "enable", line: 1, column: 18, justification: "j1" @@ -1879,7 +3199,7 @@ describe("apply-disable-directives", () => { { parentComment, ruleId: "unused-2", - type: "disable", + type: "enable", line: 1, column: 28, justification: "j2" @@ -1891,7 +3211,7 @@ describe("apply-disable-directives", () => { [ { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'unused-1' or 'unused-2').", + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'unused-1' or 'unused-2').", line: 1, column: 18, fix: { @@ -1905,8 +3225,8 @@ describe("apply-disable-directives", () => { ); }); - it("Adds a problem for /* eslint-disable unused-1, unused-2, unused-3 */", () => { - const parentComment = createParentComment([0, 49], " eslint-disable unused-1, unused-2, unused-3 ", ["unused-1", "unused-2", "unused-3"]); + it("Adds a problem for /* eslint-enable unused-1, unused-2, unused-3 */", () => { + const parentComment = createParentComment([0, 49], " eslint-enable unused-1, unused-2, unused-3 ", ["unused-1", "unused-2", "unused-3"]); assert.deepStrictEqual( applyDisableDirectives({ @@ -1914,21 +3234,21 @@ describe("apply-disable-directives", () => { { parentComment, ruleId: "unused-1", - type: "disable", + type: "enable", line: 1, column: 18 }, { parentComment, ruleId: "unused-2", - type: "disable", + type: "enable", line: 1, column: 28 }, { parentComment, ruleId: "unused-3", - type: "disable", + type: "enable", line: 1, column: 38 } @@ -1939,7 +3259,7 @@ describe("apply-disable-directives", () => { [ { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'unused-1', 'unused-2', or 'unused-3').", + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'unused-1', 'unused-2', or 'unused-3').", line: 1, column: 18, fix: { @@ -1952,72 +3272,5 @@ describe("apply-disable-directives", () => { ] ); }); - - it("Adds a problem for /* eslint-disable foo */ \\n (problem from foo and bar) // eslint-disable-line foo, bar", () => { - assert.deepStrictEqual( - applyDisableDirectives({ - directives: [ - { - parentComment: createParentComment([0, 29], " eslint-disable foo ", ["foo"]), - ruleId: "foo", - type: "disable", - line: 1, - column: 1, - justification: "j1" - }, - { - parentComment: createParentComment([41, 81], " eslint-disable-line foo, bar", ["foo", "bar"]), - ruleId: "foo", - type: "disable-line", - line: 2, - column: 11, - justification: "j2" - }, - { - parentComment: createParentComment([41, 81], " eslint-disable-line foo, bar ", ["foo", "bar"]), - ruleId: "bar", - type: "disable-line", - line: 2, - column: 11, - justification: "j2" - } - ], - problems: [ - { line: 2, column: 1, ruleId: "bar" }, - { line: 2, column: 6, ruleId: "foo" } - ], - reportUnusedDisableDirectives: "error" - }), - [ - { - ruleId: "bar", - line: 2, - column: 1, - suppressions: [{ kind: "directive", justification: "j2" }] - }, - { - ruleId: "foo", - line: 2, - column: 6, - suppressions: [ - { kind: "directive", justification: "j1" }, - { kind: "directive", justification: "j2" } - ] - }, - { - ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'foo').", - line: 2, - column: 11, - fix: { - range: [64, 69], - text: "" - }, - severity: 2, - nodeType: null - } - ] - ); - }); }); }); diff --git a/tests/lib/linter/code-path-analysis/code-path-analyzer.js b/tests/lib/linter/code-path-analysis/code-path-analyzer.js index cc2717a7ff8..dbed9b46194 100644 --- a/tests/lib/linter/code-path-analysis/code-path-analyzer.js +++ b/tests/lib/linter/code-path-analysis/code-path-analyzer.js @@ -439,6 +439,164 @@ describe("CodePathAnalyzer", () => { }); }); + describe("onUnreachableCodePathSegmentStart", () => { + it("should be fired after a throw", () => { + let lastCodePathNodeType = null; + + linter.defineRule("test", { + create: () => ({ + onUnreachableCodePathSegmentStart(segment, node) { + lastCodePathNodeType = node.type; + + assert(segment instanceof CodePathSegment); + assert.strictEqual(node.type, "ExpressionStatement"); + }, + ExpressionStatement() { + assert.strictEqual(lastCodePathNodeType, "ExpressionStatement"); + } + }) + }); + linter.verify( + "throw 'boom'; foo();", + { rules: { test: 2 } } + ); + + }); + + it("should be fired after a return", () => { + let lastCodePathNodeType = null; + + linter.defineRule("test", { + create: () => ({ + onUnreachableCodePathSegmentStart(segment, node) { + lastCodePathNodeType = node.type; + + assert(segment instanceof CodePathSegment); + assert.strictEqual(node.type, "ExpressionStatement"); + }, + ExpressionStatement() { + assert.strictEqual(lastCodePathNodeType, "ExpressionStatement"); + } + }) + }); + linter.verify( + "function foo() { return; foo(); }", + { rules: { test: 2 } } + ); + + }); + }); + + describe("onUnreachableCodePathSegmentEnd", () => { + it("should be fired after a throw", () => { + let lastCodePathNodeType = null; + + linter.defineRule("test", { + create: () => ({ + onUnreachableCodePathSegmentEnd(segment, node) { + lastCodePathNodeType = node.type; + + assert(segment instanceof CodePathSegment); + assert.strictEqual(node.type, "Program"); + } + }) + }); + linter.verify( + "throw 'boom'; foo();", + { rules: { test: 2 } } + ); + + assert.strictEqual(lastCodePathNodeType, "Program"); + }); + + it("should be fired after a return", () => { + let lastCodePathNodeType = null; + + linter.defineRule("test", { + create: () => ({ + onUnreachableCodePathSegmentEnd(segment, node) { + lastCodePathNodeType = node.type; + assert(segment instanceof CodePathSegment); + assert.strictEqual(node.type, "FunctionDeclaration"); + }, + "Program:exit"() { + assert.strictEqual(lastCodePathNodeType, "FunctionDeclaration"); + } + }) + }); + linter.verify( + "function foo() { return; foo(); }", + { rules: { test: 2 } } + ); + + }); + + it("should be fired after a return inside of function and if statement", () => { + let lastCodePathNodeType = null; + + linter.defineRule("test", { + create: () => ({ + onUnreachableCodePathSegmentEnd(segment, node) { + lastCodePathNodeType = node.type; + assert(segment instanceof CodePathSegment); + assert.strictEqual(node.type, "BlockStatement"); + }, + "Program:exit"() { + assert.strictEqual(lastCodePathNodeType, "BlockStatement"); + } + }) + }); + linter.verify( + "function foo() { if (bar) { return; foo(); } else {} }", + { rules: { test: 2 } } + ); + + }); + + it("should be fired at the end of programs/functions for the final segment", () => { + let count = 0; + let lastNodeType = null; + + linter.defineRule("test", { + create: () => ({ + onUnreachableCodePathSegmentEnd(cp, node) { + count += 1; + + assert(cp instanceof CodePathSegment); + if (count === 4) { + assert(node.type === "Program"); + } else if (count === 1) { + assert(node.type === "FunctionDeclaration"); + } else if (count === 2) { + assert(node.type === "FunctionExpression"); + } else if (count === 3) { + assert(node.type === "ArrowFunctionExpression"); + } + assert(node.type === lastNodeType); + }, + "Program:exit"() { + lastNodeType = "Program"; + }, + "FunctionDeclaration:exit"() { + lastNodeType = "FunctionDeclaration"; + }, + "FunctionExpression:exit"() { + lastNodeType = "FunctionExpression"; + }, + "ArrowFunctionExpression:exit"() { + lastNodeType = "ArrowFunctionExpression"; + } + }) + }); + linter.verify( + "foo(); function foo() { return; } var foo = function() { return; }; var foo = () => { return; }; throw 'boom';", + { rules: { test: 2 }, env: { es6: true } } + ); + + assert(count === 4); + }); + }); + describe("onCodePathSegmentLoop", () => { it("should be fired in `while` loops", () => { let count = 0; diff --git a/tests/lib/linter/config-comment-parser.js b/tests/lib/linter/config-comment-parser.js index e9f595d7750..39b06b13cf6 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 224cb729fea..d06b901a6c2 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -9,7 +9,7 @@ // Requirements //------------------------------------------------------------------------------ -const assert = require("chai").assert, +const { assert } = require("chai"), sinon = require("sinon"), espree = require("espree"), esprima = require("esprima"), @@ -1197,9 +1197,10 @@ describe("Linter", () => { linter.defineRule("test-service-rule", { create: context => ({ Literal(node) { + assert.strictEqual(context.parserServices, context.sourceCode.parserServices); context.report({ node, - message: context.parserServices.test.getMessage() + message: context.sourceCode.parserServices.test.getMessage() }); } }) @@ -1219,9 +1220,10 @@ describe("Linter", () => { linter.defineRule("test-service-rule", { create: context => ({ Literal(node) { + assert.strictEqual(context.parserServices, context.sourceCode.parserServices); context.report({ node, - message: context.parserServices.test.getMessage() + message: context.sourceCode.parserServices.test.getMessage() }); } }) @@ -1465,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", () => { @@ -1904,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; }"; @@ -2021,6 +2065,44 @@ describe("Linter", () => { assert.strictEqual(suppressedMessages.length, 0); }); + + it("should apply valid configuration even if there is an invalid configuration present", () => { + const code = [ + "/* eslint no-unused-vars: [ */ // <-- this one is invalid JSON", + "/* eslint no-undef: [\"error\"] */ // <-- this one is fine, and thus should apply", + "foo(); // <-- expected no-undef error here" + ].join("\n"); + + const messages = linter.verify(code); + const suppressedMessages = linter.getSuppressedMessages(); + + // different engines have different JSON parsing error messages + assert.match(messages[0].message, /Failed to parse JSON from ' "no-unused-vars": \['/u); + assert.strictEqual(messages[0].severity, 2); + assert.isTrue(messages[0].fatal); + assert.isNull(messages[0].ruleId); + assert.strictEqual(messages[0].line, 1); + assert.strictEqual(messages[0].column, 1); + assert.isNull(messages[0].nodeType); + + assert.deepStrictEqual( + messages[1], + { + severity: 2, + ruleId: "no-undef", + message: "'foo' is not defined.", + messageId: "undef", + line: 3, + column: 1, + endLine: 3, + endColumn: 4, + nodeType: "Identifier" + } + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + }); describe("when evaluating code with comments to disable rules", () => { @@ -2564,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", () => { @@ -2917,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"); + }); }); }); @@ -3193,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", () => { @@ -4134,8 +4323,8 @@ var a = "test2"; assert.strictEqual(suppressedMessages.length, 0); }); - it("reports problems for unused eslint-disable comments (in config)", () => { - const messages = linter.verify("/* eslint-disable */", { reportUnusedDisableDirectives: true }); + it("reports problems for unused eslint-enable comments", () => { + const messages = linter.verify("/* eslint-enable */", {}, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); assert.deepStrictEqual( @@ -4143,14 +4332,14 @@ var a = "test2"; [ { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported).", + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", line: 1, column: 1, fix: { - range: [0, 20], + range: [0, 19], text: " " }, - severity: 1, + severity: 2, nodeType: null } ] @@ -4159,20 +4348,43 @@ var a = "test2"; assert.strictEqual(suppressedMessages.length, 0); }); - it("reports problems for partially unused eslint-disable comments (in config)", () => { - const code = "alert('test'); // eslint-disable-line no-alert, no-redeclare"; + it("reports problems for unused eslint-enable comments with ruleId", () => { + const messages = linter.verify("/* eslint-enable no-alert */", {}, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'no-alert').", + line: 1, + column: 1, + fix: { + range: [0, 28], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("reports problems for unused eslint-enable comments with mismatch ruleId", () => { + const code = [ + "/* eslint-disable no-alert */", + "alert(\"test\");", + "/* eslint-enable no-console */" + ].join("\n"); const config = { - reportUnusedDisableDirectives: true, rules: { - "no-alert": 1, - "no-redeclare": 1 + "no-alert": 2 } }; - - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: true - }); + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); assert.deepStrictEqual( @@ -4180,52 +4392,333 @@ var a = "test2"; [ { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'no-redeclare').", - line: 1, - column: 16, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'no-console').", + line: 3, + column: 1, fix: { - range: [46, 60], - text: "" + range: [45, 75], + text: " " }, - severity: 1, + severity: 2, nodeType: null } ] ); assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); }); - it("reports no problems for no-fallthrough despite comment pattern match", () => { - const code = "switch (foo) { case 0: a(); \n// eslint-disable-next-line no-fallthrough\n case 1: }"; + it("reports problems for unused eslint-enable comments with used eslint-enable comments", () => { + const code = [ + "/* eslint-disable no-alert -- j1 */", + "alert(\"test\");", + "/* eslint-disable no-alert -- j2 */", + "alert(\"test\");", + "/* eslint-enable no-alert -- j3 */", + "/* eslint-enable -- j4 */" + ].join("\n"); const config = { - reportUnusedDisableDirectives: true, rules: { - "no-fallthrough": 2 + "no-alert": 2 } }; - - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: true - }); + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 0); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + line: 6, + column: 1, + fix: { + range: [137, 162], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-fallthrough"); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].suppressions.length, 2); }); - describe("autofix", () => { - const alwaysReportsRule = { - create(context) { - return { - Program(node) { - context.report({ message: "bad code", loc: node.loc.end }); - } - }; + it("reports problems for unused eslint-disable comments with used eslint-enable comments", () => { + const code = [ + "/* eslint-disable no-alert -- j1 */", + "console.log(\"test\"); //", + "/* eslint-enable no-alert -- j2 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'no-alert').", + line: 1, + column: 1, + fix: { + range: [0, 35], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("reports problems for unused eslint-disable comments (in config)", () => { + const messages = linter.verify("/* eslint-disable */", { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + fix: { + range: [0, 20], + text: " " + }, + severity: 1, + nodeType: null + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("reports problems for partially unused eslint-disable comments (in config)", () => { + const code = "alert('test'); // eslint-disable-line no-alert, no-redeclare"; + const config = { + reportUnusedDisableDirectives: true, + rules: { + "no-alert": 1, + "no-redeclare": 1 + } + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'no-redeclare').", + line: 1, + column: 16, + fix: { + range: [46, 60], + text: "" + }, + severity: 1, + nodeType: null + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + }); + + it("reports no problems for no-fallthrough despite comment pattern match", () => { + const code = "switch (foo) { case 0: a(); \n// eslint-disable-next-line no-fallthrough\n case 1: }"; + const config = { + reportUnusedDisableDirectives: true, + rules: { + "no-fallthrough": 2 + } + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0); + + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-fallthrough"); + }); + + it("reports problems for multiple eslint-enable comments with same ruleId", () => { + const code = [ + "/* eslint-disable no-alert -- j1 */", + "alert(\"test\"); //", + "/* eslint-enable no-alert -- j2 */", + "/* eslint-enable no-alert -- j3 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].line, 4); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + }); + + it("reports problems for multiple eslint-enable comments without ruleId (Rule is already enabled)", () => { + const code = [ + "/* eslint-disable no-alert -- j1 */", + "alert(\"test\"); //", + "/* eslint-enable no-alert -- j2 */", + "/* eslint-enable -- j3 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].line, 4); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + }); + + it("reports problems for multiple eslint-enable comments with ruleId (Rule is already enabled by eslint-enable comments without ruleId)", () => { + const code = [ + "/* eslint-disable no-alert -- j1 */", + "alert(\"test\"); //", + "/* eslint-enable -- j3 */", + "/* eslint-enable no-alert -- j2 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].line, 4); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + }); + + it("reports problems for eslint-enable comments without ruleId (Two rules are already enabled)", () => { + const code = [ + "/* eslint-disable no-alert, no-console -- j1 */", + "alert(\"test\"); //", + "console.log(\"test\"); //", + "/* eslint-enable no-alert -- j2 */", + "/* eslint-enable no-console -- j3 */", + "/* eslint-enable -- j4 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2, + "no-console": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].line, 6); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].suppressions.length, 1); + }); + + it("reports problems for multiple eslint-enable comments with ruleId (Two rules are already enabled by eslint-enable comments without ruleId)", () => { + const code = [ + "/* eslint-disable no-alert, no-console -- j1 */", + "alert(\"test\"); //", + "console.log(\"test\"); //", + "/* eslint-enable -- j2 */", + "/* eslint-enable no-console -- j3 */", + "/* eslint-enable no-alert -- j4 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2, + "no-console": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].line, 5); + assert.strictEqual(messages[1].line, 6); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].suppressions.length, 1); + }); + + it("reports problems for multiple eslint-enable comments", () => { + const code = [ + "/* eslint-disable no-alert, no-console -- j1 */", + "alert(\"test\"); //", + "console.log(\"test\"); //", + "/* eslint-enable no-console -- j2 */", + "/* eslint-enable -- j3 */", + "/* eslint-enable no-alert -- j4 */", + "/* eslint-enable -- j5 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2, + "no-console": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].line, 6); + assert.strictEqual(messages[1].line, 7); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].suppressions.length, 1); + }); + + describe("autofix", () => { + const alwaysReportsRule = { + create(context) { + return { + Program(node) { + context.report({ message: "bad code", loc: node.loc.end }); + }, + "Identifier[name=bad]"(node) { + context.report({ message: "bad id", loc: node.loc }); + } + }; } }; @@ -4293,6 +4786,10 @@ var a = "test2"; code: "/* eslint-disable \nunused\n*/", output: " " }, + { + code: "/* eslint-enable \nunused\n*/", + output: " " + }, //----------------------------------------------- // Removing only individual rules @@ -4331,6 +4828,26 @@ var a = "test2"; code: "/*\u00A0eslint-disable unused, used*/", output: "/*\u00A0eslint-disable used*/" }, + { + code: "/* eslint-disable used*/ bad /*\neslint-enable unused, used*/", + output: "/* eslint-disable used*/ bad /*\neslint-enable used*/" + }, + { + code: "/* eslint-disable used*/ bad /*\n eslint-enable unused, used*/", + output: "/* eslint-disable used*/ bad /*\n eslint-enable used*/" + }, + { + code: "/* eslint-disable used*/ bad /*\r\neslint-enable unused, used*/", + output: "/* eslint-disable used*/ bad /*\r\neslint-enable used*/" + }, + { + code: "/* eslint-disable used*/ bad /*\u2028eslint-enable unused, used*/", + output: "/* eslint-disable used*/ bad /*\u2028eslint-enable used*/" + }, + { + code: "/* eslint-disable used*/ bad /*\u00A0eslint-enable unused, used*/", + output: "/* eslint-disable used*/ bad /*\u00A0eslint-enable used*/" + }, { code: "// eslint-disable-line unused, used", output: "// eslint-disable-line used" @@ -4355,6 +4872,26 @@ var a = "test2"; code: "/* eslint-disable\u00A0unused, used*/", output: "/* eslint-disable\u00A0used*/" }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable\nunused, used*/", + output: "/* eslint-disable used*/ bad /* eslint-enable\nused*/" + }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable\n unused, used*/", + output: "/* eslint-disable used*/ bad /* eslint-enable\n used*/" + }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable\r\nunused, used*/", + output: "/* eslint-disable used*/ bad /* eslint-enable\r\nused*/" + }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable\u2028unused, used*/", + output: "/* eslint-disable used*/ bad /* eslint-enable\u2028used*/" + }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable\u00A0unused, used*/", + output: "/* eslint-disable used*/ bad /* eslint-enable\u00A0used*/" + }, // when removing the first rule, the comma and all whitespace up to the next rule (or next lone comma) should also be removed { @@ -4389,6 +4926,18 @@ var a = "test2"; code: "/* eslint-disable unused\u2028,\u2028used */", output: "/* eslint-disable used */" }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable unused\n,\nused */", + output: "/* eslint-disable used*/ bad /* eslint-enable used */" + }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable unused \n \n,\n\n used */", + output: "/* eslint-disable used*/ bad /* eslint-enable used */" + }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable unused\u2028,\u2028used */", + output: "/* eslint-disable used*/ bad /* eslint-enable used */" + }, { code: "// eslint-disable-line unused\u00A0,\u00A0used", output: "// eslint-disable-line used" @@ -4443,6 +4992,18 @@ var a = "test2"; code: "/* eslint-disable used-1,\u2028unused\u2028,used-2 */", output: "/* eslint-disable used-1,used-2 */" }, + { + code: "/* eslint-disable used-1, used-2*/ bad /* eslint-enable used-1,\nunused\n,used-2 */", + output: "/* eslint-disable used-1, used-2*/ bad /* eslint-enable used-1,used-2 */" + }, + { + code: "/* eslint-disable used-1, used-2*/ bad /* eslint-enable used-1,\n\n unused \n \n ,used-2 */", + output: "/* eslint-disable used-1, used-2*/ bad /* eslint-enable used-1,used-2 */" + }, + { + code: "/* eslint-disable used-1, used-2*/ bad /* eslint-enable used-1,\u2028unused\u2028,used-2 */", + output: "/* eslint-disable used-1, used-2*/ bad /* eslint-enable used-1,used-2 */" + }, { code: "// eslint-disable-line used-1,\u00A0unused\u00A0,used-2", output: "// eslint-disable-line used-1,used-2" @@ -4477,6 +5038,14 @@ var a = "test2"; code: "/* eslint-disable used-1\u2028,unused,\u2028used-2 */", output: "/* eslint-disable used-1\u2028,\u2028used-2 */" }, + { + code: "/* eslint-disable used-1, used-2 */ bad /* eslint-enable used-1\n,unused,\nused-2 */", + output: "/* eslint-disable used-1, used-2 */ bad /* eslint-enable used-1\n,\nused-2 */" + }, + { + code: "/* eslint-disable used-1, used-2 */ bad /* eslint-enable used-1\u2028,unused,\u2028used-2 */", + output: "/* eslint-disable used-1, used-2 */ bad /* eslint-enable used-1\u2028,\u2028used-2 */" + }, { code: "// eslint-disable-line used-1\u00A0,unused,\u00A0used-2", output: "// eslint-disable-line used-1\u00A0,\u00A0used-2" @@ -4502,7 +5071,19 @@ var a = "test2"; output: "/* eslint-disable used-1,\n,\n,used-2 */" }, { - code: "// eslint-disable-line used, unused,", + code: "/* eslint-disable used-1, used-2 */ bad /* eslint-enable used-1,\n,unused,used-2 */", + output: "/* eslint-disable used-1, used-2 */ bad /* eslint-enable used-1,\n,used-2 */" + }, + { + code: "/* eslint-disable used-1, used-2 */ bad /* eslint-enable used-1,unused,\n,used-2 */", + output: "/* eslint-disable used-1, used-2 */ bad /* eslint-enable used-1,\n,used-2 */" + }, + { + code: "/* eslint-disable used-1, used-2 */ bad /* eslint-enable used-1,\n,unused,\n,used-2 */", + output: "/* eslint-disable used-1, used-2 */ bad /* eslint-enable used-1,\n,\n,used-2 */" + }, + { + code: "// eslint-disable-line used, unused,", output: "// eslint-disable-line used," }, { @@ -4521,6 +5102,10 @@ var a = "test2"; code: "/* eslint-disable used, unused,\n*/", output: "/* eslint-disable used,\n*/" }, + { + code: "/* eslint-disable used */ bad /* eslint-enable used, unused,\n*/", + output: "/* eslint-disable used */ bad /* eslint-enable used,\n*/" + }, // when removing the last rule, the comma and all whitespace up to the previous rule (or previous lone comma) should also be removed { @@ -4559,6 +5144,18 @@ var a = "test2"; code: "/* eslint-disable used\u2028,\u2028unused */", output: "/* eslint-disable used */" }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable used\n,\nunused */", + output: "/* eslint-disable used*/ bad /* eslint-enable used */" + }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable used \n \n,\n\n unused */", + output: "/* eslint-disable used*/ bad /* eslint-enable used */" + }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable used\u2028,\u2028unused */", + output: "/* eslint-disable used*/ bad /* eslint-enable used */" + }, { code: "// eslint-disable-line used\u00A0,\u00A0unused", output: "// eslint-disable-line used" @@ -4579,6 +5176,14 @@ var a = "test2"; code: "/* eslint-disable used\n, ,unused */", output: "/* eslint-disable used\n, */" }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable used,\n,unused */", + output: "/* eslint-disable used*/ bad /* eslint-enable used, */" + }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable used\n, ,unused */", + output: "/* eslint-disable used*/ bad /* eslint-enable used\n, */" + }, // content after the last rule should not be changed { @@ -4609,6 +5214,10 @@ var a = "test2"; code: "/* eslint-disable used,unused\u2028*/", output: "/* eslint-disable used\u2028*/" }, + { + code: "/* eslint-disable used*/ bad /* eslint-enable used,unused\u2028*/", + output: "/* eslint-disable used*/ bad /* eslint-enable used\u2028*/" + }, { code: "// eslint-disable-line used,unused\u00A0", output: "// eslint-disable-line used\u00A0" @@ -4749,6 +5358,166 @@ var a = "test2"; */ ` }, + { + code: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable unused-1, + used-1, + unused-2, + used-2 + */ + `, + output: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable used-1, + used-2 + */ + ` + }, + { + code: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable unused-1, + used-1, + unused-2, + used-2 + */ + `, + output: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable used-1, + used-2 + */ + ` + }, + { + code: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + unused-1, + used-1, + unused-2, + used-2 + */ + `, + output: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + used-1, + used-2 + */ + ` + }, + { + code: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + used-1, + unused-1, + used-2, + unused-2 + */ + `, + output: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + used-1, + used-2 + */ + ` + }, + { + code: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + used-1, + unused-1, + used-2, + unused-2, + */ + `, + output: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + used-1, + used-2, + */ + ` + }, + { + code: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + ,unused-1 + ,used-1 + ,unused-2 + ,used-2 + */ + `, + output: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + ,used-1 + ,used-2 + */ + ` + }, + { + code: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + ,used-1 + ,unused-1 + ,used-2 + ,unused-2 + */ + `, + output: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + ,used-1 + ,used-2 + */ + ` + }, + { + code: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + used-1, + unused-1, + used-2, + unused-2 + + -- comment + */ + `, + output: ` + /* eslint-disable used-1, used-2*/ + bad + /* eslint-enable + used-1, + used-2 + + -- comment + */ + ` + }, // duplicates in the list { @@ -4765,18 +5534,208 @@ var a = "test2"; } ]; - for (const { code, output } of tests) { - // eslint-disable-next-line no-loop-func -- `linter` is getting updated in beforeEach() - it(code, () => { - assert.strictEqual( - linter.verifyAndFix(code, config).output, - output - ); - }); - } + for (const { code, output } of tests) { + // eslint-disable-next-line no-loop-func -- `linter` is getting updated in beforeEach() + it(code, () => { + assert.strictEqual( + linter.verifyAndFix(code, config).output, + 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 + ); + }); + } + } + }); + }); + + describe("config.noInlineConfig + options.allowInlineConfig", () => { + + it("should report both a rule violation and a warning about inline config", () => { + const code = [ + "/* eslint-disable */ // <-- this should be inline config warning", + "foo(); // <-- this should be no-undef error" + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + }, + noInlineConfig: true + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "'/*eslint-disable*/' has no effect because you have 'noInlineConfig' setting in your config.", + line: 1, + column: 1, + endLine: 1, + endColumn: 21, + severity: 1, + nodeType: null + }, + { + ruleId: "no-undef", + messageId: "undef", + message: "'foo' is not defined.", + line: 2, + endLine: 2, + column: 1, + endColumn: 4, + severity: 2, + nodeType: "Identifier" + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should report both a rule violation without warning about inline config when noInlineConfig is true and allowInlineConfig is false", () => { + const code = [ + "/* eslint-disable */ // <-- this should be inline config warning", + "foo(); // <-- this should be no-undef error" + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + }, + noInlineConfig: true + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: false + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.deepStrictEqual( + messages, + [ + { + ruleId: "no-undef", + messageId: "undef", + message: "'foo' is not defined.", + line: 2, + endLine: 2, + column: 1, + endColumn: 4, + severity: 2, + nodeType: "Identifier" + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should report both a rule violation without warning about inline config when both are false", () => { + const code = [ + "/* eslint-disable */ // <-- this should be inline config warning", + "foo(); // <-- this should be no-undef error" + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + }, + noInlineConfig: false + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: false + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.deepStrictEqual( + messages, + [ + { + ruleId: "no-undef", + messageId: "undef", + message: "'foo' is not defined.", + line: 2, + endLine: 2, + column: 1, + endColumn: 4, + severity: 2, + nodeType: "Identifier" + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should report one suppresed problem when noInlineConfig is false and allowInlineConfig is true", () => { + const code = [ + "/* eslint-disable */ // <-- this should be inline config warning", + "foo(); // <-- this should be no-undef error" + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + }, + noInlineConfig: false + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 1); + assert.deepStrictEqual( + suppressedMessages, + [ + { + ruleId: "no-undef", + messageId: "undef", + message: "'foo' is not defined.", + line: 2, + endLine: 2, + column: 1, + endColumn: 4, + severity: 2, + nodeType: "Identifier", + suppressions: [ + { + justification: "", + kind: "directive" + } + ] + } + ] + ); + }); }); + describe("when evaluating code with comments to change config when allowInlineConfig is disabled", () => { it("should not report a violation", () => { const code = [ @@ -4936,7 +5895,7 @@ var a = "test2"; }); it("supports ECMAScript version 'latest'", () => { - const messages = linter.verify("let x = 5 ** 7;", { + const messages = linter.verify("let x = /[\\q{abc|d}&&[A--B]]/v;", { parserOptions: { ecmaVersion: "latest" } }); const suppressedMessages = linter.getSuppressedMessages(); @@ -7261,12 +8220,12 @@ var a = "test2"; it("should have file path passed to it", () => { const code = "/* this is code */"; - const parseSpy = sinon.spy(testParsers.stubParser, "parse"); + const parseSpy = { parse: sinon.spy() }; - linter.defineParser("stub-parser", testParsers.stubParser); + linter.defineParser("stub-parser", parseSpy); linter.verify(code, { parser: "stub-parser" }, filename, true); - sinon.assert.calledWithMatch(parseSpy, "", { filePath: filename }); + sinon.assert.calledWithMatch(parseSpy.parse, "", { filePath: filename }); }); it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { @@ -7956,9 +8915,10 @@ describe("Linter with FlatConfigArray", () => { "test-service-rule": { create: context => ({ Literal(node) { + assert.strictEqual(context.parserServices, context.sourceCode.parserServices); context.report({ node, - message: context.parserServices.test.getMessage() + message: context.sourceCode.parserServices.test.getMessage() }); } }) @@ -7992,9 +8952,10 @@ describe("Linter with FlatConfigArray", () => { "test-service-rule": { create: context => ({ Literal(node) { + assert.strictEqual(context.parserServices, context.sourceCode.parserServices); context.report({ node, - message: context.parserServices.test.getMessage() + message: context.sourceCode.parserServices.test.getMessage() }); } }) @@ -8064,16 +9025,16 @@ describe("Linter with FlatConfigArray", () => { it("should have file path passed to it", () => { const code = "/* this is code */"; - const parseSpy = sinon.spy(testParsers.stubParser, "parse"); + const parseSpy = { parse: sinon.spy() }; const config = { languageOptions: { - parser: testParsers.stubParser + parser: parseSpy } }; linter.verify(code, config, filename, true); - sinon.assert.calledWithMatch(parseSpy, "", { filePath: filename }); + sinon.assert.calledWithMatch(parseSpy.parse, "", { filePath: filename }); }); it("should not report an error when JSX code contains a spread operator and JSX is enabled", () => { @@ -11669,6 +12630,7 @@ describe("Linter with FlatConfigArray", () => { column: 1, endLine: 1, endColumn: 39, + fatal: true, nodeType: null }, { @@ -11871,12 +12833,12 @@ describe("Linter with FlatConfigArray", () => { const messages = linter.verify(code, config, filename); const suppressedMessages = linter.getSuppressedMessages(); - assert.strictEqual(messages.length, 1); + assert.strictEqual(messages.length, 1, "Incorrect message length"); assert.strictEqual(messages[0].ruleId, "no-alert"); assert.strictEqual(messages[0].message, "Unexpected alert."); assert.include(messages[0].nodeType, "CallExpression"); - assert.strictEqual(suppressedMessages.length, 0); + assert.strictEqual(suppressedMessages.length, 0, "Incorrect suppressed message length"); }); it("rules should not change initial config", () => { @@ -11974,7 +12936,7 @@ describe("Linter with FlatConfigArray", () => { { severity: 2, ruleId: "no-alert", - message: "Configuration for rule \"no-alert\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed 'true').\n", + message: "Inline configuration for rule \"no-alert\" is invalid:\n\tExpected severity of \"off\", 0, \"warn\", 1, \"error\", or 2. You passed \"true\".\n", line: 1, column: 1, endLine: 1, @@ -11987,6 +12949,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(); @@ -11997,7 +12982,7 @@ describe("Linter with FlatConfigArray", () => { { severity: 2, ruleId: "no-alert", - message: "Configuration for rule \"no-alert\" is invalid:\n\tValue [{\"nonExistentPropertyName\":true}] should NOT have more than 0 items.\n", + message: "Inline configuration for rule \"no-alert\" is invalid:\n\tValue [{\"nonExistentPropertyName\":true}] should NOT have more than 0 items.\n", line: 1, column: 1, endLine: 1, @@ -12009,6 +12994,44 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(suppressedMessages.length, 0); }); + + it("should apply valid configuration even if there is an invalid configuration present", () => { + const code = [ + "/* eslint no-unused-vars: [ */ // <-- this one is invalid JSON", + "/* eslint no-undef: [\"error\"] */ // <-- this one is fine, and thus should apply", + "foo(); // <-- expected no-undef error here" + ].join("\n"); + + const messages = linter.verify(code, {}); + const suppressedMessages = linter.getSuppressedMessages(); + + // different engines have different JSON parsing error messages + assert.match(messages[0].message, /Failed to parse JSON from ' "no-unused-vars": \['/u); + assert.strictEqual(messages[0].severity, 2); + assert.isTrue(messages[0].fatal); + assert.isNull(messages[0].ruleId); + assert.strictEqual(messages[0].line, 1); + assert.strictEqual(messages[0].column, 1); + assert.isNull(messages[0].nodeType); + + assert.deepStrictEqual( + messages[1], + { + severity: 2, + ruleId: "no-undef", + message: "'foo' is not defined.", + messageId: "undef", + line: 3, + column: 1, + endLine: 3, + endColumn: 4, + nodeType: "Identifier" + } + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + }); describe("when evaluating code with comments to disable rules", () => { @@ -12803,6 +13826,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*/", () => { @@ -13036,6 +14113,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*/", () => { @@ -13432,6 +14535,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", () => { @@ -14021,7 +15149,7 @@ var a = "test2"; assert.deepStrictEqual(messages[0].fatal, void 0); assert.deepStrictEqual(messages[0].ruleId, null); assert.deepStrictEqual(messages[0].severity, 1); - assert.deepStrictEqual(messages[0].message, `'/*${directive.split(" ")[0]}*/' has no effect because you have 'noInlineConfig' setting in your config.`); + assert.deepStrictEqual(messages[0].message, `'/* ${directive} */' has no effect because you have 'noInlineConfig' setting in your config.`); assert.strictEqual(suppressedMessages.length, 0); }); @@ -14044,7 +15172,7 @@ var a = "test2"; assert.deepStrictEqual(messages[0].fatal, void 0); assert.deepStrictEqual(messages[0].ruleId, null); assert.deepStrictEqual(messages[0].severity, 1); - assert.deepStrictEqual(messages[0].message, `'//${directive.split(" ")[0]}' has no effect because you have 'noInlineConfig' setting in your config.`); + assert.deepStrictEqual(messages[0].message, `'// ${directive}' has no effect because you have 'noInlineConfig' setting in your config.`); assert.strictEqual(suppressedMessages.length, 0); }); @@ -14061,32 +15189,217 @@ var a = "test2"; assert.deepStrictEqual(messages.length, 0); assert.strictEqual(suppressedMessages.length, 0); }); - }); + }); + + describe("when evaluating code with comments to change config when allowInlineConfig is disabled", () => { + it("should not report a violation", () => { + const code = [ + "alert('test'); // eslint-disable-line no-alert" + ].join("\n"); + const config = { + rules: { + "no-alert": 1 + } + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0); + + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + }); + }); + + }); + + + describe("config.noInlineConfig + options.allowInlineConfig", () => { + + it("should report both a rule violation and a warning about inline config", () => { + const code = [ + "/* eslint-disable */ // <-- this should be inline config warning", + "foo(); // <-- this should be no-undef error" + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + }, + linterOptions: { + noInlineConfig: true + } + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "'/* eslint-disable */' has no effect because you have 'noInlineConfig' setting in your config.", + line: 1, + column: 1, + endLine: 1, + endColumn: 21, + severity: 1, + nodeType: null + }, + { + ruleId: "no-undef", + messageId: "undef", + message: "'foo' is not defined.", + line: 2, + endLine: 2, + column: 1, + endColumn: 4, + severity: 2, + nodeType: "Identifier" + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + + it("should report both a rule violation without warning about inline config when noInlineConfig is true and allowInlineConfig is false", () => { + const code = [ + "/* eslint-disable */ // <-- this should be inline config warning", + "foo(); // <-- this should be no-undef error" + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + }, + linterOptions: { + noInlineConfig: true + } + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: false + }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.deepStrictEqual( + messages, + [ + { + ruleId: "no-undef", + messageId: "undef", + message: "'foo' is not defined.", + line: 2, + endLine: 2, + column: 1, + endColumn: 4, + severity: 2, + nodeType: "Identifier" + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("should report both a rule violation without warning about inline config when both are false", () => { + const code = [ + "/* eslint-disable */ // <-- this should be inline config warning", + "foo(); // <-- this should be no-undef error" + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + }, + linterOptions: { + noInlineConfig: false + } + }; + + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: false + }); + const suppressedMessages = linter.getSuppressedMessages(); - describe("when evaluating code with comments to change config when allowInlineConfig is disabled", () => { - it("should not report a violation", () => { - const code = [ - "alert('test'); // eslint-disable-line no-alert" - ].join("\n"); - const config = { - rules: { - "no-alert": 1 + assert.strictEqual(messages.length, 1); + assert.deepStrictEqual( + messages, + [ + { + ruleId: "no-undef", + messageId: "undef", + message: "'foo' is not defined.", + line: 2, + endLine: 2, + column: 1, + endColumn: 4, + severity: 2, + nodeType: "Identifier" } - }; + ] + ); - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: true - }); - const suppressedMessages = linter.getSuppressedMessages(); + assert.strictEqual(suppressedMessages.length, 0); + }); - assert.strictEqual(messages.length, 0); + it("should report one suppresed problem when noInlineConfig is false and allowInlineConfig is true", () => { + const code = [ + "/* eslint-disable */ // <-- this should be inline config warning", + "foo(); // <-- this should be no-undef error" + ].join("\n"); + const config = { + rules: { + "no-undef": 2 + }, + linterOptions: { + noInlineConfig: false + } + }; - assert.strictEqual(suppressedMessages.length, 1); - assert.strictEqual(suppressedMessages[0].ruleId, "no-alert"); + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true }); - }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 0); + assert.strictEqual(suppressedMessages.length, 1); + assert.deepStrictEqual( + suppressedMessages, + [ + { + ruleId: "no-undef", + messageId: "undef", + message: "'foo' is not defined.", + line: 2, + endLine: 2, + column: 1, + endColumn: 4, + severity: 2, + nodeType: "Identifier", + suppressions: [ + { + justification: "", + kind: "directive" + } + ] + } + ] + ); + }); }); describe("reportUnusedDisableDirectives option", () => { @@ -14381,28 +15694,319 @@ var a = "test2"; } }; - const messages = linter.verify(code, config, { - filename, - allowInlineConfig: true - }); + const messages = linter.verify(code, config, { + filename, + allowInlineConfig: true + }); + + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported from 'no-redeclare').", + line: 2, + column: 21, + fix: { + range: [57, 71], + text: "" + }, + severity: 1, + nodeType: null + } + ] + ); + }); + + it("reports problems for unused eslint-enable comments", () => { + const messages = linter.verify("/* eslint-enable */", {}, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + line: 1, + column: 1, + fix: { + range: [0, 19], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("reports problems for unused eslint-enable comments with ruleId", () => { + const messages = linter.verify("/* eslint-enable no-alert */", {}, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'no-alert').", + line: 1, + column: 1, + fix: { + range: [0, 28], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 0); + }); + + it("reports problems for unused eslint-enable comments with mismatch ruleId", () => { + const code = [ + "/* eslint-disable no-alert */", + "alert(\"test\");", + "/* eslint-enable no-console */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found for 'no-console').", + line: 3, + column: 1, + fix: { + range: [45, 75], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 1); + }); + + it("reports problems for unused eslint-enable comments with used eslint-enable comments", () => { + const code = [ + "/* eslint-disable no-alert -- j1 */", + "alert(\"test\");", + "/* eslint-disable no-alert -- j2 */", + "alert(\"test\");", + "/* eslint-enable no-alert -- j3 */", + "/* eslint-enable -- j4 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.deepStrictEqual( + messages, + [ + { + ruleId: null, + message: "Unused eslint-enable directive (no matching eslint-disable directives were found).", + line: 6, + column: 1, + fix: { + range: [137, 162], + text: " " + }, + severity: 2, + nodeType: null + } + ] + ); + + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].suppressions.length, 2); + }); + + it("reports problems for multiple eslint-enable comments with same ruleId", () => { + const code = [ + "/* eslint-disable no-alert -- j1 */", + "alert(\"test\"); //", + "/* eslint-enable no-alert -- j2 */", + "/* eslint-enable no-alert -- j3 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].line, 4); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + }); + + it("reports problems for multiple eslint-enable comments without ruleId (Rule is already enabled)", () => { + const code = [ + "/* eslint-disable no-alert -- j1 */", + "alert(\"test\"); //", + "/* eslint-enable no-alert -- j2 */", + "/* eslint-enable -- j3 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].line, 4); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + }); + + it("reports problems for multiple eslint-enable comments with ruleId (Rule is already enabled by eslint-enable comments without ruleId)", () => { + const code = [ + "/* eslint-disable no-alert -- j1 */", + "alert(\"test\"); //", + "/* eslint-enable -- j3 */", + "/* eslint-enable no-alert -- j2 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].line, 4); + assert.strictEqual(suppressedMessages.length, 1); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + }); + + it("reports problems for eslint-enable comments without ruleId (Two rules are already enabled)", () => { + const code = [ + "/* eslint-disable no-alert, no-console -- j1 */", + "alert(\"test\"); //", + "console.log(\"test\"); //", + "/* eslint-enable no-alert -- j2 */", + "/* eslint-enable no-console -- j3 */", + "/* eslint-enable -- j4 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2, + "no-console": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].line, 6); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].suppressions.length, 1); + }); + + it("reports problems for multiple eslint-enable comments with ruleId (Two rules are already enabled by eslint-enable comments without ruleId)", () => { + const code = [ + "/* eslint-disable no-alert, no-console -- j1 */", + "alert(\"test\"); //", + "console.log(\"test\"); //", + "/* eslint-enable -- j2 */", + "/* eslint-enable no-console -- j3 */", + "/* eslint-enable no-alert -- j4 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2, + "no-console": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].line, 5); + assert.strictEqual(messages[1].line, 6); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].suppressions.length, 1); + }); + + it("reports problems for multiple eslint-enable comments", () => { + const code = [ + "/* eslint-disable no-alert, no-console -- j1 */", + "alert(\"test\"); //", + "console.log(\"test\"); //", + "/* eslint-enable no-console -- j2 */", + "/* eslint-enable -- j3 */", + "/* eslint-enable no-alert -- j4 */", + "/* eslint-enable -- j5 */" + ].join("\n"); + const config = { + rules: { + "no-alert": 2, + "no-console": 2 + } + }; + const messages = linter.verify(code, config, { reportUnusedDisableDirectives: true }); + const suppressedMessages = linter.getSuppressedMessages(); + + assert.strictEqual(messages.length, 2); + assert.strictEqual(messages[0].line, 6); + assert.strictEqual(messages[1].line, 7); + assert.strictEqual(suppressedMessages.length, 2); + assert.strictEqual(suppressedMessages[0].suppressions.length, 1); + assert.strictEqual(suppressedMessages[1].suppressions.length, 1); + }); + + it("reports problems for unused eslint-disable comments (warn)", () => { + const messages = linter.verify("/* eslint-disable */", {}, { reportUnusedDisableDirectives: "warn" }); + const suppressedMessages = linter.getSuppressedMessages(); assert.deepStrictEqual( messages, [ { ruleId: null, - message: "Unused eslint-disable directive (no problems were reported from 'no-redeclare').", - line: 2, - column: 21, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, fix: { - range: [57, 71], - text: "" + range: [0, 20], + text: " " }, severity: 1, nodeType: null } ] ); + + assert.strictEqual(suppressedMessages.length, 0); }); describe("autofix", () => { @@ -14411,6 +16015,9 @@ var a = "test2"; return { Program(node) { context.report({ message: "bad code", loc: node.loc.end }); + }, + "Identifier[name=bad]"(node) { + context.report({ message: "bad id", loc: node.loc }); } }; } @@ -14489,6 +16096,10 @@ var a = "test2"; code: "/* eslint-disable \ntest/unused\n*/", output: " " }, + { + code: "/* eslint-enable \ntest/unused\n*/", + output: " " + }, //----------------------------------------------- // Removing only individual rules @@ -14527,6 +16138,26 @@ var a = "test2"; code: "/*\u00A0eslint-disable test/unused, test/used*/", output: "/*\u00A0eslint-disable test/used*/" }, + { + code: "/* eslint-disable test/used */ bad /*\neslint-enable test/unused, test/used*/", + output: "/* eslint-disable test/used */ bad /*\neslint-enable test/used*/" + }, + { + code: "/* eslint-disable test/used */ bad /*\n eslint-enable test/unused, test/used*/", + output: "/* eslint-disable test/used */ bad /*\n eslint-enable test/used*/" + }, + { + code: "/* eslint-disable test/used */ bad /*\r\neslint-enable test/unused, test/used*/", + output: "/* eslint-disable test/used */ bad /*\r\neslint-enable test/used*/" + }, + { + code: "/* eslint-disable test/used */ bad /*\u2028eslint-enable test/unused, test/used*/", + output: "/* eslint-disable test/used */ bad /*\u2028eslint-enable test/used*/" + }, + { + code: "/* eslint-disable test/used */ bad /*\u00A0eslint-enable test/unused, test/used*/", + output: "/* eslint-disable test/used */ bad /*\u00A0eslint-enable test/used*/" + }, { code: "// eslint-disable-line test/unused, test/used", output: "// eslint-disable-line test/used" @@ -14585,6 +16216,18 @@ var a = "test2"; code: "/* eslint-disable test/unused\u2028,\u2028test/used */", output: "/* eslint-disable test/used */" }, + { + code: "/* eslint-disable test/used */ bad /* eslint-enable test/unused\n,\ntest/used */", + output: "/* eslint-disable test/used */ bad /* eslint-enable test/used */" + }, + { + code: "/* eslint-disable test/used */ bad /* eslint-enable test/unused \n \n,\n\n test/used */", + output: "/* eslint-disable test/used */ bad /* eslint-enable test/used */" + }, + { + code: "/* eslint-disable test/used */ bad /* eslint-enable test/unused\u2028,\u2028test/used */", + output: "/* eslint-disable test/used */ bad /* eslint-enable test/used */" + }, { code: "// eslint-disable-line test/unused\u00A0,\u00A0test/used", output: "// eslint-disable-line test/used" @@ -14639,6 +16282,18 @@ var a = "test2"; code: "/* eslint-disable test/used-1,\u2028test/unused\u2028,test/used-2 */", output: "/* eslint-disable test/used-1,test/used-2 */" }, + { + code: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1,\ntest/unused\n,test/used-2 */", + output: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1,test/used-2 */" + }, + { + code: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1,\n\n test/unused \n \n ,test/used-2 */", + output: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1,test/used-2 */" + }, + { + code: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1,\u2028test/unused\u2028,test/used-2 */", + output: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1,test/used-2 */" + }, { code: "// eslint-disable-line test/used-1,\u00A0test/unused\u00A0,test/used-2", output: "// eslint-disable-line test/used-1,test/used-2" @@ -14673,6 +16328,14 @@ var a = "test2"; code: "/* eslint-disable test/used-1\u2028,test/unused,\u2028test/used-2 */", output: "/* eslint-disable test/used-1\u2028,\u2028test/used-2 */" }, + { + code: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1\n,test/unused,\ntest/used-2 */", + output: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1\n,\ntest/used-2 */" + }, + { + code: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1\u2028,test/unused,\u2028test/used-2 */", + output: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1\u2028,\u2028test/used-2 */" + }, { code: "// eslint-disable-line test/used-1\u00A0,test/unused,\u00A0test/used-2", output: "// eslint-disable-line test/used-1\u00A0,\u00A0test/used-2" @@ -14697,6 +16360,18 @@ var a = "test2"; code: "/* eslint-disable test/used-1,\n,test/unused,\n,test/used-2 */", output: "/* eslint-disable test/used-1,\n,\n,test/used-2 */" }, + { + code: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1,\n,test/unused,test/used-2 */", + output: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1,\n,test/used-2 */" + }, + { + code: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1,test/unused,\n,test/used-2 */", + output: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1,\n,test/used-2 */" + }, + { + code: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1,\n,test/unused,\n,test/used-2 */", + output: "/* eslint-disable test/used-1, test/used-2 */ bad /* eslint-enable test/used-1,\n,\n,test/used-2 */" + }, { code: "// eslint-disable-line test/used, test/unused,", output: "// eslint-disable-line test/used," @@ -14717,6 +16392,10 @@ var a = "test2"; code: "/* eslint-disable test/used, test/unused,\n*/", output: "/* eslint-disable test/used,\n*/" }, + { + code: "/* eslint-disable test/used */ bad /* eslint-enable test/used, test/unused,\n*/", + output: "/* eslint-disable test/used */ bad /* eslint-enable test/used,\n*/" + }, // when removing the last rule, the comma and all whitespace up to the previous rule (or previous lone comma) should also be removed { @@ -14755,6 +16434,18 @@ var a = "test2"; code: "/* eslint-disable test/used\u2028,\u2028test/unused */", output: "/* eslint-disable test/used */" }, + { + code: "/* eslint-disable test/used */ bad /* eslint-enable test/used\n,\ntest/unused */", + output: "/* eslint-disable test/used */ bad /* eslint-enable test/used */" + }, + { + code: "/* eslint-disable test/used */ bad /* eslint-enable test/used \n \n,\n\n test/unused */", + output: "/* eslint-disable test/used */ bad /* eslint-enable test/used */" + }, + { + code: "/* eslint-disable test/used */ bad /* eslint-enable test/used\u2028,\u2028test/unused */", + output: "/* eslint-disable test/used */ bad /* eslint-enable test/used */" + }, { code: "// eslint-disable-line test/used\u00A0,\u00A0test/unused", output: "// eslint-disable-line test/used" @@ -14775,6 +16466,14 @@ var a = "test2"; code: "/* eslint-disable test/used\n, ,test/unused */", output: "/* eslint-disable test/used\n, */" }, + { + code: "/* eslint-disable test/used */ bad /* eslint-enable test/used,\n,test/unused */", + output: "/* eslint-disable test/used */ bad /* eslint-enable test/used, */" + }, + { + code: "/* eslint-disable test/used */ bad /* eslint-enable test/used\n, ,test/unused */", + output: "/* eslint-disable test/used */ bad /* eslint-enable test/used\n, */" + }, // content after the last rule should not be changed { @@ -14805,6 +16504,10 @@ var a = "test2"; code: "/* eslint-disable test/used,test/unused\u2028*/", output: "/* eslint-disable test/used\u2028*/" }, + { + code: "/* eslint-disable test/used */ bad /* eslint-enable test/used,test/unused\u2028*/", + output: "/* eslint-disable test/used */ bad /* eslint-enable test/used\u2028*/" + }, { code: "// eslint-disable-line test/used,test/unused\u00A0", output: "// eslint-disable-line test/used\u00A0" @@ -14945,6 +16648,148 @@ var a = "test2"; */ ` }, + { + code: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable test/unused-1, + test/used-1, + test/unused-2, + test/used-2 + */ + `, + output: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable test/used-1, + test/used-2 + */ + ` + }, + { + code: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable + test/unused-1, + test/used-1, + test/unused-2, + test/used-2 + */ + `, + output: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable + test/used-1, + test/used-2 + */ + ` + }, + { + code: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable + test/used-1, + test/unused-1, + test/used-2, + test/unused-2 + */ + `, + output: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable + test/used-1, + test/used-2 + */ + ` + }, + { + code: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable + test/used-1, + test/unused-1, + test/used-2, + test/unused-2, + */ + `, + output: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable + test/used-1, + test/used-2, + */ + ` + }, + { + code: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable + ,test/unused-1 + ,test/used-1 + ,test/unused-2 + ,test/used-2 + */ + `, + output: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable + ,test/used-1 + ,test/used-2 + */ + ` + }, + { + code: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable + ,test/used-1 + ,test/unused-1 + ,test/used-2 + ,test/unused-2 + */ + `, + output: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable + ,test/used-1 + ,test/used-2 + */ + ` + }, + { + code: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable + test/used-1, + test/unused-1, + test/used-2, + test/unused-2 + + -- comment + */ + `, + output: ` + /* eslint-disable test/used-1, test/used-2 */ + bad + /* eslint-enable + test/used-1, + test/used-2 + + -- comment + */ + ` + }, // duplicates in the list { @@ -14969,6 +16814,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 + ); + }); + } } }); }); @@ -15225,13 +17084,12 @@ var a = "test2"; }); }); - describe("Error Conditions", () => { describe("when evaluating broken code", () => { 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); @@ -15252,7 +17110,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); @@ -15958,6 +17816,171 @@ var a = "test2"; assert.strictEqual(suppressedMessages.length, 0); }); + + // https://github.com/eslint/eslint/issues/16716 + it("should receive unique range arrays in suggestions", () => { + const configs = [ + { + plugins: { + "test-processors": { + processors: { + "line-processor": (() => { + const blocksMap = new Map(); + + return { + preprocess(text, fileName) { + const lines = text.split("\n"); + + blocksMap.set(fileName, lines); + + return lines.map((line, index) => ({ + text: line, + filename: `${index}.js` + })); + }, + + postprocess(messageLists, fileName) { + const lines = blocksMap.get(fileName); + let rangeOffset = 0; + + // intentionaly mutates objects and arrays + messageLists.forEach((messages, index) => { + messages.forEach(message => { + message.line += index; + if (typeof message.endLine === "number") { + message.endLine += index; + } + if (message.fix) { + message.fix.range[0] += rangeOffset; + message.fix.range[1] += rangeOffset; + } + if (message.suggestions) { + message.suggestions.forEach(suggestion => { + suggestion.fix.range[0] += rangeOffset; + suggestion.fix.range[1] += rangeOffset; + }); + } + }); + rangeOffset += lines[index].length + 1; + }); + + return messageLists.flat(); + }, + + supportsAutofix: true + }; + })() + } + }, + + "test-rules": { + rules: { + "no-foo": { + meta: { + hasSuggestions: true, + messages: { + unexpected: "Don't use 'foo'.", + replaceWithBar: "Replace with 'bar'", + replaceWithBaz: "Replace with 'baz'" + } + + }, + create(context) { + return { + Identifier(node) { + const { range } = node; + + if (node.name === "foo") { + context.report({ + node, + messageId: "unexpected", + suggest: [ + { + messageId: "replaceWithBar", + fix: () => ({ range, text: "bar" }) + }, + { + messageId: "replaceWithBaz", + fix: () => ({ range, text: "baz" }) + } + ] + }); + } + } + }; + } + } + } + } + } + }, + { + files: ["**/*.txt"], + processor: "test-processors/line-processor" + }, + { + files: ["**/*.js"], + rules: { + "test-rules/no-foo": 2 + } + } + ]; + + const result = linter.verifyAndFix( + "var a = 5;\nvar foo;\nfoo = a;", + configs, + { filename: "a.txt" } + ); + + assert.deepStrictEqual(result.messages, [ + { + ruleId: "test-rules/no-foo", + severity: 2, + message: "Don't use 'foo'.", + line: 2, + column: 5, + nodeType: "Identifier", + messageId: "unexpected", + endLine: 2, + endColumn: 8, + suggestions: [ + { + messageId: "replaceWithBar", + fix: { range: [15, 18], text: "bar" }, + desc: "Replace with 'bar'" + }, + { + messageId: "replaceWithBaz", + fix: { range: [15, 18], text: "baz" }, + desc: "Replace with 'baz'" + } + ] + }, + { + ruleId: "test-rules/no-foo", + severity: 2, + message: "Don't use 'foo'.", + line: 3, + column: 1, + nodeType: "Identifier", + messageId: "unexpected", + endLine: 3, + endColumn: 4, + suggestions: [ + { + messageId: "replaceWithBar", + fix: { range: [20, 23], text: "bar" }, + desc: "Replace with 'bar'" + }, + { + messageId: "replaceWithBaz", + fix: { range: [20, 23], text: "baz" }, + desc: "Replace with 'baz'" + } + ] + } + ]); + }); }); }); @@ -16023,7 +18046,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", () => { @@ -16134,7 +18157,6 @@ var a = "test2"; assert(spy.calledOnce); }); - describe("when evaluating an empty string", () => { it("runs rules", () => { diff --git a/tests/lib/linter/report-translator.js b/tests/lib/linter/report-translator.js index 6feabb31b96..18c62f20729 100644 --- a/tests/lib/linter/report-translator.js +++ b/tests/lib/linter/report-translator.js @@ -1091,4 +1091,183 @@ describe("createReportTranslator", () => { } }); }); + + // https://github.com/eslint/eslint/issues/16716 + describe("unique `fix` and `fix.range` objects", () => { + const range = [0, 3]; + const fix = { range, text: "baz" }; + const additionalRange = [4, 7]; + const additionalFix = { range: additionalRange, text: "qux" }; + + it("should deep clone returned fix object", () => { + const translatedReport = translateReport({ + node, + messageId: "testMessage", + fix: () => fix + }); + + assert.deepStrictEqual(translatedReport.fix, fix); + assert.notStrictEqual(translatedReport.fix, fix); + assert.notStrictEqual(translatedReport.fix.range, fix.range); + }); + + it("should create a new fix object with a new range array when `fix()` returns an array with a single item", () => { + const translatedReport = translateReport({ + node, + messageId: "testMessage", + fix: () => [fix] + }); + + assert.deepStrictEqual(translatedReport.fix, fix); + assert.notStrictEqual(translatedReport.fix, fix); + assert.notStrictEqual(translatedReport.fix.range, fix.range); + }); + + it("should create a new fix object with a new range array when `fix()` returns an array with multiple items", () => { + const translatedReport = translateReport({ + node, + messageId: "testMessage", + fix: () => [fix, additionalFix] + }); + + assert.notStrictEqual(translatedReport.fix, fix); + assert.notStrictEqual(translatedReport.fix.range, fix.range); + assert.notStrictEqual(translatedReport.fix, additionalFix); + assert.notStrictEqual(translatedReport.fix.range, additionalFix.range); + }); + + it("should create a new fix object with a new range array when `fix()` generator yields a single item", () => { + const translatedReport = translateReport({ + node, + messageId: "testMessage", + *fix() { + yield fix; + } + }); + + assert.deepStrictEqual(translatedReport.fix, fix); + assert.notStrictEqual(translatedReport.fix, fix); + assert.notStrictEqual(translatedReport.fix.range, fix.range); + }); + + it("should create a new fix object with a new range array when `fix()` generator yields multiple items", () => { + const translatedReport = translateReport({ + node, + messageId: "testMessage", + *fix() { + yield fix; + yield additionalFix; + } + }); + + assert.notStrictEqual(translatedReport.fix, fix); + assert.notStrictEqual(translatedReport.fix.range, fix.range); + assert.notStrictEqual(translatedReport.fix, additionalFix); + assert.notStrictEqual(translatedReport.fix.range, additionalFix.range); + }); + + it("should deep clone returned suggestion fix object", () => { + const translatedReport = translateReport({ + node, + messageId: "testMessage", + suggest: [{ + messageId: "suggestion1", + fix: () => fix + }] + }); + + assert.deepStrictEqual(translatedReport.suggestions[0].fix, fix); + assert.notStrictEqual(translatedReport.suggestions[0].fix, fix); + assert.notStrictEqual(translatedReport.suggestions[0].fix.range, fix.range); + }); + + it("should create a new fix object with a new range array when suggestion `fix()` returns an array with a single item", () => { + const translatedReport = translateReport({ + node, + messageId: "testMessage", + suggest: [{ + messageId: "suggestion1", + fix: () => [fix] + }] + }); + + assert.deepStrictEqual(translatedReport.suggestions[0].fix, fix); + assert.notStrictEqual(translatedReport.suggestions[0].fix, fix); + assert.notStrictEqual(translatedReport.suggestions[0].fix.range, fix.range); + }); + + it("should create a new fix object with a new range array when suggestion `fix()` returns an array with multiple items", () => { + const translatedReport = translateReport({ + node, + messageId: "testMessage", + suggest: [{ + messageId: "suggestion1", + fix: () => [fix, additionalFix] + }] + }); + + assert.notStrictEqual(translatedReport.suggestions[0].fix, fix); + assert.notStrictEqual(translatedReport.suggestions[0].fix.range, fix.range); + assert.notStrictEqual(translatedReport.suggestions[0].fix, additionalFix); + assert.notStrictEqual(translatedReport.suggestions[0].fix.range, additionalFix.range); + }); + + it("should create a new fix object with a new range array when suggestion `fix()` generator yields a single item", () => { + const translatedReport = translateReport({ + node, + messageId: "testMessage", + suggest: [{ + messageId: "suggestion1", + *fix() { + yield fix; + } + }] + }); + + assert.deepStrictEqual(translatedReport.suggestions[0].fix, fix); + assert.notStrictEqual(translatedReport.suggestions[0].fix, fix); + assert.notStrictEqual(translatedReport.suggestions[0].fix.range, fix.range); + }); + + it("should create a new fix object with a new range array when suggestion `fix()` generator yields multiple items", () => { + const translatedReport = translateReport({ + node, + messageId: "testMessage", + suggest: [{ + messageId: "suggestion1", + *fix() { + yield fix; + yield additionalFix; + } + }] + }); + + assert.notStrictEqual(translatedReport.suggestions[0].fix, fix); + assert.notStrictEqual(translatedReport.suggestions[0].fix.range, fix.range); + assert.notStrictEqual(translatedReport.suggestions[0].fix, additionalFix); + assert.notStrictEqual(translatedReport.suggestions[0].fix.range, additionalFix.range); + }); + + it("should create different instances of range arrays when suggestions reuse the same instance", () => { + const translatedReport = translateReport({ + node, + messageId: "testMessage", + suggest: [ + { + messageId: "suggestion1", + fix: () => ({ range, text: "baz" }) + }, + { + messageId: "suggestion2", + data: { interpolated: "'interpolated value'" }, + fix: () => ({ range, text: "qux" }) + } + ] + }); + + assert.deepStrictEqual(translatedReport.suggestions[0].fix.range, range); + assert.deepStrictEqual(translatedReport.suggestions[1].fix.range, range); + assert.notStrictEqual(translatedReport.suggestions[0].fix.range, translatedReport.suggestions[1].fix.range); + }); + }); }); diff --git a/tests/lib/options.js b/tests/lib/options.js index d8f795b78a2..b663e8623e3 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/rule-tester/flat-rule-tester.js b/tests/lib/rule-tester/flat-rule-tester.js index f65c3bb1913..679a87b99da 100644 --- a/tests/lib/rule-tester/flat-rule-tester.js +++ b/tests/lib/rule-tester/flat-rule-tester.js @@ -1116,6 +1116,84 @@ describe("FlatRuleTester", () => { }()); }); + it("should allow setting the filename to a non-JavaScript file", () => { + ruleTester.run("", require("../../fixtures/testers/rule-tester/no-test-filename"), { + valid: [ + { + code: "var foo = 'bar'", + filename: "somefile.ts" + } + ], + invalid: [] + }); + }); + + it("should allow setting the filename to a file path without extension", () => { + ruleTester.run("", require("../../fixtures/testers/rule-tester/no-test-filename"), { + valid: [ + { + code: "var foo = 'bar'", + filename: "somefile" + }, + { + code: "var foo = 'bar'", + filename: "path/to/somefile" + } + ], + invalid: [] + }); + }); + + it("should allow setting the filename to a file path with extension", () => { + ruleTester.run("", require("../../fixtures/testers/rule-tester/no-test-filename"), { + valid: [ + { + code: "var foo = 'bar'", + filename: "path/to/somefile.js" + }, + { + code: "var foo = 'bar'", + filename: "src/somefile.ts" + }, + { + code: "var foo = 'bar'", + filename: "components/Component.vue" + } + ], + invalid: [] + }); + }); + + it("should allow setting the filename to a file path without extension", () => { + ruleTester.run("", require("../../fixtures/testers/rule-tester/no-test-filename"), { + valid: [ + { + code: "var foo = 'bar'", + filename: "path/to/somefile" + }, + { + code: "var foo = 'bar'", + filename: "src/somefile" + } + ], + invalid: [] + }); + }); + + it("should keep allowing non-JavaScript files if the default config does not specify files", () => { + FlatRuleTester.setDefaultConfig({ rules: {} }); + ruleTester.run("", require("../../fixtures/testers/rule-tester/no-test-filename"), { + valid: [ + { + code: "var foo = 'bar'", + filename: "somefile.ts" + } + ], + invalid: [] + }); + FlatRuleTester.resetDefaultConfig(); + }); + it("should pass-through the options to the rule", () => { ruleTester.run("no-invalid-args", require("../../fixtures/testers/rule-tester/no-invalid-args"), { valid: [ @@ -1199,7 +1277,9 @@ describe("FlatRuleTester", () => { const disallowHiRule = { create: context => ({ Literal(node) { - const disallowed = context.parserServices.test.getMessage(); // returns "Hi!" + assert.strictEqual(context.parserServices, context.sourceCode.parserServices); + + const disallowed = context.sourceCode.parserServices.test.getMessage(); // returns "Hi!" if (node.value === disallowed) { context.report({ node, message: `Don't use '${disallowed}'` }); @@ -1334,7 +1414,7 @@ describe("FlatRuleTester", () => { ], invalid: [] }); - }, /Unexpected key "env" found./u); + }, /Key "env": This appears to be in eslintrc format rather than flat config format/u); }); it("should pass-through the tester config to the rule", () => { @@ -2246,6 +2326,45 @@ describe("FlatRuleTester", () => { }); }); + describe("deprecations", () => { + let processStub; + + beforeEach(() => { + processStub = sinon.stub(process, "emitWarning"); + }); + + afterEach(() => { + processStub.restore(); + }); + + it("should emit a deprecation warning when CodePath#currentSegments is accessed", () => { + + const useCurrentSegmentsRule = { + create: () => ({ + onCodePathStart(codePath) { + codePath.currentSegments.forEach(() => { }); + } + }) + }; + + ruleTester.run("use-current-segments", useCurrentSegmentsRule, { + valid: ["foo"], + invalid: [] + }); + + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "\"use-current-segments\" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples", + "DeprecationWarning" + ] + ); + + }); + + }); + /** * Asserts that a particular value will be emitted from an EventEmitter. * @param {EventEmitter} emitter The emitter that should emit a value @@ -2579,6 +2698,49 @@ describe("FlatRuleTester", () => { }); }); + describe("SourceCode forbidden methods", () => { + + [ + "applyInlineConfig", + "applyLanguageOptions", + "finalize" + ].forEach(methodName => { + + const useForbiddenMethodRule = { + create: context => ({ + Program() { + const sourceCode = context.sourceCode; + + sourceCode[methodName](); + } + }) + }; + + it(`should throw if ${methodName} is called from a valid test case`, () => { + assert.throws(() => { + ruleTester.run("use-forbidden-method", useForbiddenMethodRule, { + valid: [""], + invalid: [] + }); + }, `\`SourceCode#${methodName}()\` cannot be called inside a rule.`); + }); + + it(`should throw if ${methodName} is called from an invalid test case`, () => { + assert.throws(() => { + ruleTester.run("use-forbidden-method", useForbiddenMethodRule, { + valid: [], + invalid: [{ + code: "", + errors: [{}] + }] + }); + }, `\`SourceCode#${methodName}()\` cannot be called inside a rule.`); + }); + + }); + + }); + describe("Subclassing", () => { it("should allow subclasses to set the describe/it/itOnly statics and should correctly use those values", () => { const assertionDescribe = assertEmitted(ruleTesterTestEmitter, "custom describe", "this-is-a-rule-name"); @@ -2624,4 +2786,73 @@ describe("FlatRuleTester", () => { }); + describe("Optional Test Suites", () => { + let originalRuleTesterDescribe; + let spyRuleTesterDescribe; + + before(() => { + originalRuleTesterDescribe = FlatRuleTester.describe; + spyRuleTesterDescribe = sinon.spy((title, callback) => callback()); + FlatRuleTester.describe = spyRuleTesterDescribe; + }); + after(() => { + FlatRuleTester.describe = originalRuleTesterDescribe; + }); + beforeEach(() => { + spyRuleTesterDescribe.resetHistory(); + ruleTester = new FlatRuleTester(); + }); + + it("should create a test suite with the rule name even if there are no test cases", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [], + invalid: [] + }); + sinon.assert.calledWith(spyRuleTesterDescribe, "no-var"); + }); + + it("should create a valid test suite if there is a valid test case", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: ["value = 0;"], + invalid: [] + }); + sinon.assert.calledWith(spyRuleTesterDescribe, "valid"); + }); + + it("should not create a valid test suite if there are no valid test cases", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [], + invalid: [ + { + code: "var value = 0;", + errors: [/^Bad var/u], + output: " value = 0;" + } + ] + }); + sinon.assert.neverCalledWith(spyRuleTesterDescribe, "valid"); + }); + + it("should create an invalid test suite if there is an invalid test case", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [], + invalid: [ + { + code: "var value = 0;", + errors: [/^Bad var/u], + output: " value = 0;" + } + ] + }); + sinon.assert.calledWith(spyRuleTesterDescribe, "invalid"); + }); + + it("should not create an invalid test suite if there are no invalid test cases", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: ["value = 0;"], + invalid: [] + }); + sinon.assert.neverCalledWith(spyRuleTesterDescribe, "invalid"); + }); + }); }); diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index a36edafd409..a28b345501f 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -1243,37 +1243,6 @@ describe("RuleTester", () => { }); }); - it("should pass-through services from parseForESLint to the rule", () => { - const enhancedParserPath = require.resolve("../../fixtures/parsers/enhanced-parser"); - const disallowHiRule = { - create: context => ({ - Literal(node) { - const disallowed = context.parserServices.test.getMessage(); // returns "Hi!" - - if (node.value === disallowed) { - context.report({ node, message: `Don't use '${disallowed}'` }); - } - } - }) - }; - - ruleTester.run("no-hi", disallowHiRule, { - valid: [ - { - code: "'Hello!'", - parser: enhancedParserPath - } - ], - invalid: [ - { - code: "'Hi!'", - parser: enhancedParserPath, - errors: [{ message: "Don't use 'Hi!'" }] - } - ] - }); - }); - it("should prevent invalid options schemas", () => { assert.throws(() => { ruleTester.run("no-invalid-schema", require("../../fixtures/testers/rule-tester/no-invalid-schema"), { @@ -2487,6 +2456,143 @@ describe("RuleTester", () => { assert.strictEqual(processStub.callCount, 0, "never calls `process.emitWarning()`"); }); + + it("should emit a deprecation warning when CodePath#currentSegments is accessed", () => { + + const useCurrentSegmentsRule = { + create: () => ({ + onCodePathStart(codePath) { + codePath.currentSegments.forEach(() => {}); + } + }) + }; + + ruleTester.run("use-current-segments", useCurrentSegmentsRule, { + valid: ["foo"], + invalid: [] + }); + + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "\"use-current-segments\" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples", + "DeprecationWarning" + ] + ); + }); + + it("should pass-through services from parseForESLint to the rule and log deprecation notice", () => { + const enhancedParserPath = require.resolve("../../fixtures/parsers/enhanced-parser"); + const disallowHiRule = { + create: context => ({ + Literal(node) { + assert.strictEqual(context.parserServices, context.sourceCode.parserServices); + + const disallowed = context.sourceCode.parserServices.test.getMessage(); // returns "Hi!" + + if (node.value === disallowed) { + context.report({ node, message: `Don't use '${disallowed}'` }); + } + } + }) + }; + + ruleTester.run("no-hi", disallowHiRule, { + valid: [ + { + code: "'Hello!'", + parser: enhancedParserPath + } + ], + invalid: [ + { + code: "'Hi!'", + parser: enhancedParserPath, + errors: [{ message: "Don't use 'Hi!'" }] + } + ] + }); + + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + "\"no-hi\" rule is using `context.parserServices`, which is deprecated and will be removed in ESLint v9. Please use `sourceCode.parserServices` instead.", + "DeprecationWarning" + ] + ); + + }); + Object.entries({ + getSource: "getText", + getSourceLines: "getLines", + getAllComments: "getAllComments", + getNodeByRangeIndex: "getNodeByRangeIndex", + getCommentsBefore: "getCommentsBefore", + getCommentsAfter: "getCommentsAfter", + getCommentsInside: "getCommentsInside", + getJSDocComment: "getJSDocComment", + getFirstToken: "getFirstToken", + getFirstTokens: "getFirstTokens", + getLastToken: "getLastToken", + getLastTokens: "getLastTokens", + getTokenAfter: "getTokenAfter", + getTokenBefore: "getTokenBefore", + getTokenByRangeStart: "getTokenByRangeStart", + getTokens: "getTokens", + getTokensAfter: "getTokensAfter", + getTokensBefore: "getTokensBefore", + getTokensBetween: "getTokensBetween", + getScope: "getScope", + getAncestors: "getAncestors", + getDeclaredVariables: "getDeclaredVariables", + markVariableAsUsed: "markVariableAsUsed" + }).forEach(([methodName, replacementName]) => { + + it(`should log a deprecation warning when calling \`context.${methodName}\``, () => { + const ruleToCheckDeprecation = { + meta: { + type: "problem", + schema: [] + }, + create(context) { + return { + Program(node) { + + // special case + if (methodName === "getTokensBetween") { + context[methodName](node, node); + } else { + context[methodName](node); + } + + context.report({ node, message: "bad" }); + } + }; + } + }; + + ruleTester.run("deprecated-method", ruleToCheckDeprecation, { + valid: [], + invalid: [ + { code: "var foo = bar;", options: [], errors: 1 } + ] + }); + + assert.strictEqual(processStub.callCount, 1, "calls `process.emitWarning()` once"); + assert.deepStrictEqual( + processStub.getCall(0).args, + [ + `"deprecated-method" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${replacementName}()\` instead.`, + "DeprecationWarning" + ] + ); + }); + + }); + + }); /** @@ -2822,6 +2928,50 @@ describe("RuleTester", () => { }); }); + + describe("SourceCode forbidden methods", () => { + + [ + "applyInlineConfig", + "applyLanguageOptions", + "finalize" + ].forEach(methodName => { + + const useForbiddenMethodRule = { + create: context => ({ + Program() { + const sourceCode = context.sourceCode; + + sourceCode[methodName](); + } + }) + }; + + it(`should throw if ${methodName} is called from a valid test case`, () => { + assert.throws(() => { + ruleTester.run("use-forbidden-method", useForbiddenMethodRule, { + valid: [""], + invalid: [] + }); + }, `\`SourceCode#${methodName}()\` cannot be called inside a rule.`); + }); + + it(`should throw if ${methodName} is called from an invalid test case`, () => { + assert.throws(() => { + ruleTester.run("use-forbidden-method", useForbiddenMethodRule, { + valid: [], + invalid: [{ + code: "", + errors: [{}] + }] + }); + }, `\`SourceCode#${methodName}()\` cannot be called inside a rule.`); + }); + + }); + + }); + describe("Subclassing", () => { it("should allow subclasses to set the describe/it/itOnly statics and should correctly use those values", () => { @@ -2868,4 +3018,73 @@ describe("RuleTester", () => { }); + describe("Optional Test Suites", () => { + let originalRuleTesterDescribe; + let spyRuleTesterDescribe; + + before(() => { + originalRuleTesterDescribe = RuleTester.describe; + spyRuleTesterDescribe = sinon.spy((title, callback) => callback()); + RuleTester.describe = spyRuleTesterDescribe; + }); + after(() => { + RuleTester.describe = originalRuleTesterDescribe; + }); + beforeEach(() => { + spyRuleTesterDescribe.resetHistory(); + ruleTester = new RuleTester(); + }); + + it("should create a test suite with the rule name even if there are no test cases", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [], + invalid: [] + }); + sinon.assert.calledWith(spyRuleTesterDescribe, "no-var"); + }); + + it("should create a valid test suite if there is a valid test case", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: ["value = 0;"], + invalid: [] + }); + sinon.assert.calledWith(spyRuleTesterDescribe, "valid"); + }); + + it("should not create a valid test suite if there are no valid test cases", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [], + invalid: [ + { + code: "var value = 0;", + errors: [/^Bad var/u], + output: " value = 0;" + } + ] + }); + sinon.assert.neverCalledWith(spyRuleTesterDescribe, "valid"); + }); + + it("should create an invalid test suite if there is an invalid test case", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [], + invalid: [ + { + code: "var value = 0;", + errors: [/^Bad var/u], + output: " value = 0;" + } + ] + }); + sinon.assert.calledWith(spyRuleTesterDescribe, "invalid"); + }); + + it("should not create an invalid test suite if there are no invalid test cases", () => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: ["value = 0;"], + invalid: [] + }); + sinon.assert.neverCalledWith(spyRuleTesterDescribe, "invalid"); + }); + }); }); diff --git a/tests/lib/rules/array-callback-return.js b/tests/lib/rules/array-callback-return.js index 6343d3e4027..39114dbc582 100644 --- a/tests/lib/rules/array-callback-return.js +++ b/tests/lib/rules/array-callback-return.js @@ -24,6 +24,8 @@ const checkForEachOptions = [{ checkForEach: true }]; const allowImplicitCheckForEach = [{ allowImplicit: true, checkForEach: true }]; +const checkForEachAllowVoid = [{ checkForEach: true, allowVoid: true }]; + ruleTester.run("array-callback-return", rule, { valid: [ @@ -114,6 +116,13 @@ ruleTester.run("array-callback-return", rule, { { code: "foo.every(function() { try { bar(); } finally { return 1; } })", options: checkForEachOptions }, { code: "foo.every(function() { return; })", options: allowImplicitCheckForEach }, + // options: { checkForEach: true, allowVoid: true } + { code: "foo.forEach((x) => void x)", options: checkForEachAllowVoid, parserOptions: { ecmaVersion: 6 } }, + { code: "foo.forEach((x) => void bar(x))", options: checkForEachAllowVoid, parserOptions: { ecmaVersion: 6 } }, + { code: "foo.forEach(function (x) { return void bar(x); })", options: checkForEachAllowVoid, parserOptions: { ecmaVersion: 6 } }, + { code: "foo.forEach((x) => { return void bar(x); })", options: checkForEachAllowVoid, parserOptions: { ecmaVersion: 6 } }, + { code: "foo.forEach((x) => { if (a === b) { return void a; } bar(x) })", options: checkForEachAllowVoid, parserOptions: { ecmaVersion: 6 } }, + "Arrow.from(x, function() {})", "foo.abc(function() {})", "every(function() {})", @@ -200,10 +209,66 @@ ruleTester.run("array-callback-return", rule, { { code: "foo.forEach(function bar(x) { return x;})", options: allowImplicitCheckForEach, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function 'bar'", arrayMethodName: "Array.prototype.forEach" } }] }, // // options: { checkForEach: true } - { code: "foo.forEach(x => x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, - { code: "foo.forEach(val => y += val)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, - { code: "[\"foo\",\"bar\"].forEach(x => ++x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, - { code: "foo.bar().forEach(x => x === y)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { + code: "foo.forEach(x => x)", + options: checkForEachOptions, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach(x => {x})", messageId: "wrapBraces" } + ] + }] + }, + { + code: "foo.forEach(x => (x))", + options: checkForEachOptions, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach(x => {(x)})", messageId: "wrapBraces" } + ] + }] + }, + { + code: "foo.forEach(val => y += val)", + options: checkForEachOptions, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach(val => {y += val})", messageId: "wrapBraces" } + ] + }] + }, + { + code: "[\"foo\",\"bar\"].forEach(x => ++x)", + options: checkForEachOptions, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "[\"foo\",\"bar\"].forEach(x => {++x})", messageId: "wrapBraces" } + ] + }] + }, + { + code: "foo.bar().forEach(x => x === y)", + options: checkForEachOptions, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.bar().forEach(x => {x === y})", messageId: "wrapBraces" } + ] + }] + }, { code: "foo.forEach(function() {return function() { if (a == b) { return a; }}}())", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, { code: "foo.forEach(function(x) { if (a == b) {return x;}})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, { code: "foo.forEach(function(x) { if (a == b) {return undefined;}})", options: checkForEachOptions, errors: [{ messageId: "expectedNoReturnValue", data: { name: "function", arrayMethodName: "Array.prototype.forEach" } }] }, @@ -217,6 +282,136 @@ ruleTester.run("array-callback-return", rule, { { code: "foo.filter(function foo() {})", options: checkForEachOptions, errors: [{ messageId: "expectedInside", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, { code: "foo.filter(function foo() { return; })", options: checkForEachOptions, errors: [{ messageId: "expectedReturnValue", data: { name: "function 'foo'", arrayMethodName: "Array.prototype.filter" } }] }, { code: "foo.every(cb || function() {})", options: checkForEachOptions, errors: ["Array.prototype.every() expects a return value from function."] }, + { code: "foo.forEach((x) => void x)", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach((x) => void bar(x))", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach((x) => { return void bar(x); })", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + { code: "foo.forEach((x) => { if (a === b) { return void a; } bar(x) })", options: checkForEachOptions, parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "expectedNoReturnValue", data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" } }] }, + + // options: { checkForEach: true, allowVoid: true } + + { + code: "foo.forEach(x => x)", + options: checkForEachAllowVoid, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach(x => {x})", messageId: "wrapBraces" }, + { output: "foo.forEach(x => void x)", messageId: "prependVoid" } + ] + }] + }, + { + code: "foo.forEach(x => !x)", + options: checkForEachAllowVoid, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach(x => {!x})", messageId: "wrapBraces" }, + { output: "foo.forEach(x => void !x)", messageId: "prependVoid" } + ] + }] + }, + { + code: "foo.forEach(x => (x))", + options: checkForEachAllowVoid, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach(x => {(x)})", messageId: "wrapBraces" }, + { output: "foo.forEach(x => void (x))", messageId: "prependVoid" } + ] + }] + }, + { + code: "foo.forEach((x) => { return x; })", + options: checkForEachAllowVoid, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach((x) => { return void x; })", messageId: "prependVoid" } + ] + }] + }, + { + code: "foo.forEach((x) => { return !x; })", + options: checkForEachAllowVoid, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach((x) => { return void !x; })", messageId: "prependVoid" } + ] + }] + }, + { + code: "foo.forEach((x) => { return(x); })", + options: checkForEachAllowVoid, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach((x) => { return void (x); })", messageId: "prependVoid" } + ] + }] + }, + { + code: "foo.forEach((x) => { return (x + 1); })", + options: checkForEachAllowVoid, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach((x) => { return void (x + 1); })", messageId: "prependVoid" } + ] + }] + }, + { + code: "foo.forEach((x) => { if (a === b) { return x; } })", + options: checkForEachAllowVoid, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach((x) => { if (a === b) { return void x; } })", messageId: "prependVoid" } + ] + }] + }, + { + code: "foo.forEach((x) => { if (a === b) { return !x; } })", + options: checkForEachAllowVoid, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach((x) => { if (a === b) { return void !x; } })", messageId: "prependVoid" } + ] + }] + }, + { + code: "foo.forEach((x) => { if (a === b) { return (x + a); } })", + options: checkForEachAllowVoid, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "expectedNoReturnValue", + data: { name: "arrow function", arrayMethodName: "Array.prototype.forEach" }, + suggestions: [ + { output: "foo.forEach((x) => { if (a === b) { return void (x + a); } })", messageId: "prependVoid" } + ] + }] + }, // full location tests { diff --git a/tests/lib/rules/block-scoped-var.js b/tests/lib/rules/block-scoped-var.js index f401332b116..175676b3a58 100644 --- a/tests/lib/rules/block-scoped-var.js +++ b/tests/lib/rules/block-scoped-var.js @@ -121,63 +121,345 @@ ruleTester.run("block-scoped-var", rule, { { code: "foo; class C { static {} } var foo; ", parserOptions: { ecmaVersion: 2022 } } ], invalid: [ - { code: "function f(){ x; { var x; } }", errors: [{ messageId: "outOfScope", data: { name: "x" }, type: "Identifier" }] }, - { code: "function f(){ { var x; } x; }", errors: [{ messageId: "outOfScope", data: { name: "x" }, type: "Identifier" }] }, - { code: "function f() { var a; { var b = 0; } a = b; }", errors: [{ messageId: "outOfScope", data: { name: "b" }, type: "Identifier" }] }, - { code: "function f() { try { var a = 0; } catch (e) { var b = a; } }", errors: [{ messageId: "outOfScope", data: { name: "a" }, type: "Identifier" }] }, + { + code: "function f(){ x; { var x; } }", + errors: [{ + messageId: "outOfScope", + data: { + name: "x", + definitionLine: 1, + definitionColumn: 24 + }, + line: 1, + column: 15, + type: "Identifier" + }] + }, + { + code: "function f(){ { var x; } x; }", + errors: [{ + messageId: "outOfScope", + data: { + name: "x", + definitionLine: 1, + definitionColumn: 21 + }, + line: 1, + column: 26, + type: "Identifier" + }] + }, + { + code: "function f() { var a; { var b = 0; } a = b; }", + errors: [{ + messageId: "outOfScope", + data: { + name: "b", + definitionLine: 1, + definitionColumn: 29 + }, + line: 1, + column: 42, + type: "Identifier" + }] + }, + { + code: "function f() { try { var a = 0; } catch (e) { var b = a; } }", + errors: [{ + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 26 + }, + line: 1, + column: 55, + type: "Identifier" + }] + }, { code: "function a() { for(var b in {}) { var c = b; } c; }", - errors: [{ messageId: "outOfScope", data: { name: "c" }, type: "Identifier" }] + errors: [{ + messageId: "outOfScope", + data: { + name: "c", + definitionLine: 1, + definitionColumn: 39 + }, + line: 1, + column: 48, + type: "Identifier" + }] }, { code: "function a() { for(var b of {}) { var c = b; } c; }", parserOptions: { ecmaVersion: 6 }, - errors: [{ messageId: "outOfScope", data: { name: "c" }, type: "Identifier" }] + errors: [{ + messageId: "outOfScope", + data: { + name: "c", + definitionLine: 1, + definitionColumn: 39 + }, + line: 1, + column: 48, + type: "Identifier" + }] }, { code: "function f(){ switch(2) { case 1: var b = 2; b; break; default: b; break;} b; }", - errors: [{ messageId: "outOfScope", data: { name: "b" }, type: "Identifier" }] + errors: [{ + messageId: "outOfScope", + data: { + name: "b", + definitionLine: 1, + definitionColumn: 39 + }, + line: 1, + column: 76, + type: "Identifier" + }] }, { code: "for (var a = 0;;) {} a;", - errors: [{ messageId: "outOfScope", data: { name: "a" }, type: "Identifier" }] + errors: [{ + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 10 + }, + line: 1, + column: 22, + type: "Identifier" + }] }, { code: "for (var a in []) {} a;", - errors: [{ messageId: "outOfScope", data: { name: "a" }, type: "Identifier" }] + errors: [{ + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 10 + }, + line: 1, + column: 22, + type: "Identifier" + }] }, { code: "for (var a of []) {} a;", parserOptions: { ecmaVersion: 6 }, - errors: [{ messageId: "outOfScope", data: { name: "a" }, type: "Identifier" }] + errors: [{ + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 10 + }, + line: 1, + column: 22, + type: "Identifier" + }] }, { code: "{ var a = 0; } a;", parserOptions: { ecmaVersion: 6, sourceType: "module" }, - errors: [{ messageId: "outOfScope", data: { name: "a" }, type: "Identifier" }] + errors: [{ + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 7 + }, + line: 1, + column: 16, + type: "Identifier" + }] }, { code: "if (true) { var a; } a;", - errors: [{ messageId: "outOfScope", data: { name: "a" }, type: "Identifier" }] + errors: [{ + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 17 + }, + line: 1, + column: 22, + type: "Identifier" + }] }, { code: "if (true) { var a = 1; } else { var a = 2; }", errors: [ - { messageId: "outOfScope", data: { name: "a" }, type: "Identifier" }, - { messageId: "outOfScope", data: { name: "a" }, type: "Identifier" } + { + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 37 + }, + line: 1, + column: 17, + type: "Identifier" + }, + { + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 17 + }, + line: 1, + column: 37, + type: "Identifier" + } ] }, { code: "for (var i = 0;;) {} for(var i = 0;;) {}", errors: [ - { messageId: "outOfScope", data: { name: "i" }, type: "Identifier" }, - { messageId: "outOfScope", data: { name: "i" }, type: "Identifier" } + { + messageId: "outOfScope", + data: { + name: "i", + definitionLine: 1, + definitionColumn: 30 + }, + line: 1, + column: 10, + type: "Identifier" + }, + { + messageId: "outOfScope", + data: { + name: "i", + definitionLine: 1, + definitionColumn: 10 + }, + line: 1, + column: 30, + type: "Identifier" + } ] }, { code: "class C { static { if (bar) { var foo; } foo; } }", parserOptions: { ecmaVersion: 2022 }, - errors: [{ messageId: "outOfScope", data: { name: "foo" }, type: "Identifier" }] + errors: [{ + messageId: "outOfScope", + data: { + name: "foo", + definitionLine: 1, + definitionColumn: 35 + }, + line: 1, + column: 42, + type: "Identifier" + }] + }, + { + code: "{ var foo,\n bar; } bar;", + errors: [{ + messageId: "outOfScope", + data: { + name: "bar", + definitionLine: 2, + definitionColumn: 3 + }, + line: 2, + column: 10, + type: "Identifier" + }] + }, + { + code: "{ var { foo,\n bar } = baz; } bar;", + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "outOfScope", + data: { + name: "bar", + definitionLine: 2, + definitionColumn: 3 + }, + line: 2, + column: 18, + type: "Identifier" + }] + }, + { + code: "if (foo) { var a = 1; } else if (bar) { var a = 2; } else { var a = 3; }", + errors: [ + { + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 45 + }, + line: 1, + column: 16, + type: "Identifier" + }, + { + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 65 + }, + line: 1, + column: 16, + type: "Identifier" + }, + { + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 16 + }, + line: 1, + column: 45, + type: "Identifier" + }, + { + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 65 + }, + line: 1, + column: 45, + type: "Identifier" + }, + { + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 16 + }, + line: 1, + column: 65, + type: "Identifier" + }, + { + messageId: "outOfScope", + data: { + name: "a", + definitionLine: 1, + definitionColumn: 45 + }, + line: 1, + column: 65, + type: "Identifier" + } + ] } ] }); diff --git a/tests/lib/rules/for-direction.js b/tests/lib/rules/for-direction.js index 8ecd843c238..6e59f6bacf4 100644 --- a/tests/lib/rules/for-direction.js +++ b/tests/lib/rules/for-direction.js @@ -16,7 +16,7 @@ const { RuleTester } = require("../../../lib/rule-tester"); // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester(); +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2020 } }); const incorrectDirection = { messageId: "incorrectDirection" }; ruleTester.run("for-direction", rule, { @@ -37,6 +37,12 @@ ruleTester.run("for-direction", rule, { "for(var i = 10; i >= 0; i-=1){}", "for(var i = 10; i > 0; i+=-1){}", "for(var i = 10; i >= 0; i+=-1){}", + "for(var i = 0n; i > l; i-=1n){}", + "for(var i = 0n; i < l; i-=-1n){}", + "for(var i = MIN; i <= MAX; i+=true){}", + "for(var i = 0; i < 10; i+=+5e-7){}", + "for(var i = 0; i < MAX; i -= ~2);", + "for(var i = 0, n = -1; i < MAX; i += -n);", // test if no update. "for(var i = 10; i > 0;){}", @@ -54,6 +60,13 @@ ruleTester.run("for-direction", rule, { "for(var i = 0; i < MAX; i += STEP_SIZE);", "for(var i = 0; i < MAX; i -= STEP_SIZE);", "for(var i = 10; i > 0; i += STEP_SIZE);", + "for(var i = 10; i >= 0; i += 0);", + "for(var i = 10n; i >= 0n; i += 0n);", + "for(var i = 10; i >= 0; i += this.step);", + "for(var i = 10; i >= 0; i += 'foo');", + "for(var i = 10; i > 0; i += !foo);", + "for(var i = MIN; i <= MAX; i -= false);", + "for(var i = MIN; i <= MAX; i -= 0/0);", // other cond-expressions. "for(var i = 0; i !== 10; i+=1){}", @@ -77,6 +90,12 @@ ruleTester.run("for-direction", rule, { { code: "for(var i = 0; i < 10; i+=-1){}", errors: [incorrectDirection] }, { code: "for(var i = 0; i <= 10; i+=-1){}", errors: [incorrectDirection] }, { code: "for(var i = 10; i > 10; i-=-1){}", errors: [incorrectDirection] }, - { code: "for(var i = 10; i >= 0; i-=-1){}", errors: [incorrectDirection] } + { code: "for(var i = 10; i >= 0; i-=-1){}", errors: [incorrectDirection] }, + { code: "for(var i = 0n; i > l; i+=1n){}", errors: [incorrectDirection] }, + { code: "for(var i = 0n; i < l; i+=-1n){}", errors: [incorrectDirection] }, + { code: "for(var i = MIN; i <= MAX; i-=true){}", errors: [incorrectDirection] }, + { code: "for(var i = 0; i < 10; i-=+5e-7){}", errors: [incorrectDirection] }, + { code: "for(var i = 0; i < MAX; i += (2 - 3));", errors: [incorrectDirection] }, + { code: "var n = -2; for(var i = 0; i < 10; i += n);", errors: [incorrectDirection] } ] }); diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js index 0785083d0ac..4880fa5ec02 100644 --- a/tests/lib/rules/indent.js +++ b/tests/lib/rules/indent.js @@ -6361,7 +6361,156 @@ ruleTester.run("indent", rule, { ;[1, 2, 3].forEach(x=>console.log(x)) `, options: [4] - } + }, + + // https://github.com/eslint/eslint/issues/17316 + { + code: unIndent` + if (foo) + \tif (bar) doSomething(); + \telse doSomething(); + else + \tif (bar) doSomething(); + \telse doSomething(); + `, + options: ["tab"] + }, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) doSomething(); + else doSomething(); + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) + doSomething(); + else doSomething(); + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) doSomething(); + else + doSomething(); + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) + doSomething(); + else + doSomething(); + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (bar) doSomething(); + else doSomething(); + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (bar) + doSomething(); + else doSomething(); + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (bar) doSomething(); + else + doSomething(); + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (bar) + doSomething(); + else + doSomething(); + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) doSomething(); + else doSomething(); + + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (foo) + if (bar) doSomething(); + else + if (bar) doSomething(); + else doSomething(); + else doSomething(); + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (foo) doSomething(); + else doSomething(); + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (foo) { + doSomething(); + } + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (foo) + { + doSomething(); + } + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (foo) { + doSomething(); + } + `, + unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (foo) + { + doSomething(); + } + ` ], invalid: [ @@ -13381,6 +13530,432 @@ ruleTester.run("indent", rule, { `, options: [4], errors: expectedErrors([4, 0, 4, "Punctuator"]) + }, + + // https://github.com/eslint/eslint/issues/17316 + { + code: unIndent` + if (foo) + \tif (bar) doSomething(); + \telse doSomething(); + else + if (bar) doSomething(); + else doSomething(); + `, + output: unIndent` + if (foo) + \tif (bar) doSomething(); + \telse doSomething(); + else + \tif (bar) doSomething(); + \telse doSomething(); + `, + options: ["tab"], + errors: expectedErrors("tab", [ + [5, 1, 0, "Keyword"], + [6, 1, 0, "Keyword"] + ]) + }, + { + code: unIndent` + if (foo) + \tif (bar) doSomething(); + \telse doSomething(); + else + \t\tif (bar) doSomething(); + \t\telse doSomething(); + `, + output: unIndent` + if (foo) + \tif (bar) doSomething(); + \telse doSomething(); + else + \tif (bar) doSomething(); + \telse doSomething(); + `, + options: ["tab"], + errors: expectedErrors("tab", [ + [5, 1, 2, "Keyword"], + [6, 1, 2, "Keyword"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) doSomething(); + else doSomething(); + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) doSomething(); + else doSomething(); + `, + errors: expectedErrors([ + [5, 4, 0, "Keyword"], + [6, 4, 0, "Keyword"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) + doSomething(); + else doSomething(); + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) + doSomething(); + else doSomething(); + `, + errors: expectedErrors([ + [5, 4, 0, "Keyword"], + [6, 8, 0, "Identifier"], + [7, 4, 0, "Keyword"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) doSomething(); + else + doSomething(); + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) doSomething(); + else + doSomething(); + `, + errors: expectedErrors([ + [5, 4, 0, "Keyword"], + [6, 4, 0, "Keyword"], + [7, 8, 0, "Identifier"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) + doSomething(); + else + doSomething(); + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) + doSomething(); + else + doSomething(); + `, + errors: expectedErrors([ + [5, 4, 0, "Keyword"], + [6, 8, 4, "Identifier"], + [7, 4, 0, "Keyword"], + [8, 8, 0, "Identifier"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (bar) doSomething(); + else doSomething(); + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (bar) doSomething(); + else doSomething(); + `, + errors: expectedErrors([ + [5, 0, 4, "Keyword"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (bar) + doSomething(); + else doSomething(); + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (bar) + doSomething(); + else doSomething(); + `, + errors: expectedErrors([ + [4, 0, 4, "Keyword"], + [5, 4, 8, "Identifier"], + [6, 0, 4, "Keyword"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (bar) doSomething(); + else + doSomething(); + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (bar) doSomething(); + else + doSomething(); + `, + errors: expectedErrors([ + [5, 0, 5, "Keyword"], + [6, 4, 9, "Identifier"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (bar) + doSomething(); + else + doSomething(); + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (bar) + doSomething(); + else + doSomething(); + `, + errors: expectedErrors([ + [5, 4, 0, "Identifier"], + [7, 4, 0, "Identifier"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) doSomething(); + else doSomething(); + + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (bar) doSomething(); + else doSomething(); + + `, + errors: expectedErrors([ + [5, 4, 0, "Keyword"], + [6, 8, 4, "Keyword"], + [7, 8, 4, "Keyword"], + [8, 4, 0, "Keyword"], + [9, 8, 4, "Keyword"], + [10, 8, 4, "Keyword"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (foo) + if (bar) doSomething(); + else + if (bar) doSomething(); + else doSomething(); + else doSomething(); + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (foo) + if (bar) doSomething(); + else + if (bar) doSomething(); + else doSomething(); + else doSomething(); + `, + errors: expectedErrors([ + [5, 4, 0, "Keyword"], + [6, 8, 0, "Keyword"], + [7, 8, 0, "Keyword"], + [8, 12, 0, "Keyword"], + [9, 12, 0, "Keyword"], + [10, 4, 0, "Keyword"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (foo) doSomething(); + else doSomething(); + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (foo) doSomething(); + else doSomething(); + `, + errors: expectedErrors([ + [2, 4, 0, "Keyword"], + [3, 4, 0, "Keyword"], + [5, 0, 4, "Keyword"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (foo) { + doSomething(); + } + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (foo) { + doSomething(); + } + `, + errors: expectedErrors([ + [5, 4, 0, "Identifier"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (foo) + { + doSomething(); + } + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else if (foo) + { + doSomething(); + } + `, + errors: expectedErrors([ + [5, 0, 4, "Punctuator"], + [6, 4, 8, "Identifier"], + [7, 0, 4, "Punctuator"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (foo) { + doSomething(); + } + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (foo) { + doSomething(); + } + `, + errors: expectedErrors([ + [5, 4, 0, "Keyword"], + [6, 8, 4, "Identifier"], + [7, 4, 0, "Punctuator"] + ]) + }, + { + code: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (foo) + { + doSomething(); + } + `, + output: unIndent` + if (foo) + if (bar) doSomething(); + else doSomething(); + else + if (foo) + { + doSomething(); + } + `, + errors: expectedErrors([ + [5, 4, 0, "Keyword"], + [6, 4, 0, "Punctuator"], + [7, 8, 4, "Identifier"], + [8, 4, 0, "Punctuator"] + ]) } ] }); diff --git a/tests/lib/rules/key-spacing.js b/tests/lib/rules/key-spacing.js index 9ac345d39c1..96103129e9f 100644 --- a/tests/lib/rules/key-spacing.js +++ b/tests/lib/rules/key-spacing.js @@ -942,10 +942,10 @@ ruleTester.run("key-spacing", rule, { { code: ` var foo = { - "🌷": "bar", // 2 code points - "🎁": "baz", // 2 code points - "🇮🇳": "qux", // 4 code points - "🏳️‍🌈": "xyz", // 6 code points + "🌷": "bar", // 1 grapheme, 1 code point, 2 code units + "🎁": "baz", // 1 grapheme, 1 code point, 2 code units + "🇮🇳": "qux", // 1 grapheme, 2 code points, 4 code units + "🏳️‍🌈": "xyz", // 1 grapheme, 4 code points, 6 code units }; `, options: [{ @@ -2467,18 +2467,18 @@ ruleTester.run("key-spacing", rule, { { code: ` var foo = { - "🌷": "bar", // 2 code points - "🎁": "baz", // 2 code points - "🇮🇳": "qux", // 4 code points - "🏳️‍🌈": "xyz", // 6 code points + "🌷": "bar", // 1 grapheme, 1 code point, 2 code units + "🎁": "baz", // 1 grapheme, 1 code point, 2 code units + "🇮🇳": "qux", // 1 grapheme, 2 code points, 4 code units + "🏳️‍🌈": "xyz", // 1 grapheme, 4 code points, 6 code units }; `, output: ` var foo = { - "🌷": "bar", // 2 code points - "🎁": "baz", // 2 code points - "🇮🇳": "qux", // 4 code points - "🏳️‍🌈": "xyz", // 6 code points + "🌷": "bar", // 1 grapheme, 1 code point, 2 code units + "🎁": "baz", // 1 grapheme, 1 code point, 2 code units + "🇮🇳": "qux", // 1 grapheme, 2 code points, 4 code units + "🏳️‍🌈": "xyz", // 1 grapheme, 4 code points, 6 code units }; `, options: [{ diff --git a/tests/lib/rules/lines-between-class-members.js b/tests/lib/rules/lines-between-class-members.js index feb9c085e37..2ee17f713c3 100644 --- a/tests/lib/rules/lines-between-class-members.js +++ b/tests/lib/rules/lines-between-class-members.js @@ -50,24 +50,810 @@ ruleTester.run("lines-between-class-members", rule, { "class C {\naaa;\n\n#bbb;\n\nccc(){}\n\n#ddd(){}\n}", { code: "class foo{ bar(){}\nbaz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}\n/*comments*/baz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}\n//comments\nbaz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}/* comments\n\n*/baz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}/* \ncomments\n*/baz(){}}", options: ["never"] }, - { code: "class foo{ bar(){}\n/* \ncomments\n*/\nbaz(){}}", options: ["never"] }, + { + code: "class foo{ bar(){}\n/*comments*/baz(){}}", + options: ["never"] + }, + { + code: "class foo{ bar(){}\n//comments\nbaz(){}}", + options: ["never"] + }, + { + code: "class foo{ bar(){}/* comments\n\n*/baz(){}}", + options: ["never"] + }, + { + code: "class foo{ bar(){}/* \ncomments\n*/baz(){}}", + options: ["never"] + }, + { + code: "class foo{ bar(){}\n/* \ncomments\n*/\nbaz(){}}", + options: ["never"] + }, { code: "class foo{ bar(){}\n\nbaz(){}}", options: ["always"] }, - { code: "class foo{ bar(){}\n\n/*comments*/baz(){}}", options: ["always"] }, - { code: "class foo{ bar(){}\n\n//comments\nbaz(){}}", options: ["always"] }, + { + code: "class foo{ bar(){}\n\n/*comments*/baz(){}}", + options: ["always"] + }, + { + code: "class foo{ bar(){}\n\n//comments\nbaz(){}}", + options: ["always"] + }, - { code: "class foo{ bar(){}\nbaz(){}}", options: ["always", { exceptAfterSingleLine: true }] }, - { code: "class foo{ bar(){\n}\n\nbaz(){}}", options: ["always", { exceptAfterSingleLine: true }] }, - { code: "class foo{\naaa;\n#bbb;\nccc(){\n}\n\n#ddd(){\n}\n}", options: ["always", { exceptAfterSingleLine: true }] }, + { + code: "class foo{ bar(){}\nbaz(){}}", + options: ["always", { exceptAfterSingleLine: true }] + }, + { + code: "class foo{ bar(){\n}\n\nbaz(){}}", + options: ["always", { exceptAfterSingleLine: true }] + }, + { + code: "class foo{\naaa;\n#bbb;\nccc(){\n}\n\n#ddd(){\n}\n}", + options: ["always", { exceptAfterSingleLine: true }] + }, // semicolon-less style (semicolons are at the beginning of lines) { code: "class C { foo\n\n;bar }", options: ["always"] }, - { code: "class C { foo\n;bar }", options: ["always", { exceptAfterSingleLine: true }] }, - { code: "class C { foo\n;bar }", options: ["never"] } + { + code: "class C { foo\n;bar }", + options: ["always", { exceptAfterSingleLine: true }] + }, + { code: "class C { foo\n;bar }", options: ["never"] }, + + // enforce option with blankLine: "always" + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "*" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "method" } + ] + } + ] + }, + + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "*" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "*", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "*", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { enforce: [{ blankLine: "always", prev: "*", next: "*" }] } + ] + }, + + // enforce option - blankLine: "never" + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "*" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "method" } + ] + } + ] + }, + + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [{ blankLine: "never", prev: "field", next: "*" }] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "*", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [{ blankLine: "never", prev: "*", next: "field" }] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { enforce: [{ blankLine: "never", prev: "*", next: "*" }] } + ] + }, + + // enforce option - multiple configurations + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods, disallows blank lines between fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "never", prev: "field", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + + // requires blank lines around fields, disallows blank lines between methods + enforce: [ + { blankLine: "always", prev: "*", next: "field" }, + { blankLine: "always", prev: "field", next: "*" }, + { blankLine: "never", prev: "method", next: "method" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "never", prev: "*", next: "method" }, + { blankLine: "never", prev: "method", next: "*" }, + { blankLine: "never", prev: "field", next: "field" }, + + // This should take precedence over the above + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + } + ] + }, + + // enforce with exceptAfterSingleLine option + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + }, + { + exceptAfterSingleLine: true + } + ] + } ], invalid: [ { @@ -75,97 +861,116 @@ ruleTester.run("lines-between-class-members", rule, { output: "class foo{ bar(){}\n\nbaz(){}}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){}\n\nbaz(){}}", output: "class foo{ bar(){}\nbaz(){}}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){\n}\nbaz(){}}", output: "class foo{ bar(){\n}\n\nbaz(){}}", options: ["always", { exceptAfterSingleLine: true }], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){\n}\n/* comment */\nbaz(){}}", output: "class foo{ bar(){\n}\n\n/* comment */\nbaz(){}}", options: ["always", { exceptAfterSingleLine: true }], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){}\n\n// comment\nbaz(){}}", output: "class foo{ bar(){}\n// comment\nbaz(){}}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n\n/* comment */\nbaz(){}}", output: "class foo{ bar(){}\n/* comment */\nbaz(){}}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n/* comment-1 */\n\n/* comment-2 */\nbaz(){}}", output: "class foo{ bar(){}\n/* comment-1 */\n/* comment-2 */\nbaz(){}}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n\n/* comment */\n\nbaz(){}}", output: null, options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n\n// comment\n\nbaz(){}}", output: null, options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n/* comment-1 */\n\n/* comment-2 */\n\n/* comment-3 */\nbaz(){}}", output: null, options: ["never"], errors: [neverError] - }, { + }, + { code: "class foo{ bar(){}\n/* comment-1 */\n\n;\n\n/* comment-3 */\nbaz(){}}", output: null, options: ["never"], errors: [neverError] - }, { + }, + { code: "class A {\nfoo() {}// comment\n;\n/* comment */\nbar() {}\n}", output: "class A {\nfoo() {}// comment\n\n;\n/* comment */\nbar() {}\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class A {\nfoo() {}\n/* comment */;\n;\n/* comment */\nbar() {}\n}", output: "class A {\nfoo() {}\n\n/* comment */;\n;\n/* comment */\nbar() {}\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){};\nbaz(){}}", output: "class foo{ bar(){};\n\nbaz(){}}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class foo{ bar(){} // comment \nbaz(){}}", output: "class foo{ bar(){} // comment \n\nbaz(){}}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class A {\nfoo() {}\n/* comment */;\n;\nbar() {}\n}", output: "class A {\nfoo() {}\n\n/* comment */;\n;\nbar() {}\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class C {\nfield1\nfield2\n}", output: "class C {\nfield1\n\nfield2\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class C {\n#field1\n#field2\n}", output: "class C {\n#field1\n\n#field2\n}", options: ["always"], errors: [alwaysError] - }, { + }, + { code: "class C {\nfield1\n\nfield2\n}", output: "class C {\nfield1\nfield2\n}", options: ["never"], errors: [neverError] - }, { + }, + { code: "class C {\nfield1 = () => {\n}\nfield2\nfield3\n}", output: "class C {\nfield1 = () => {\n}\n\nfield2\nfield3\n}", options: ["always", { exceptAfterSingleLine: true }], @@ -208,6 +1013,1610 @@ ruleTester.run("lines-between-class-members", rule, { output: "class C { foo\n\n;;bar }", options: ["always"], errors: [alwaysError] + }, + + // enforce option with blankLine: "always" + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 11, + column: 17 + }, + { + messageId: "always", + line: 14, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 13, + column: 17 + }, + { + messageId: "always", + line: 16, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "method", next: "*" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 11, + column: 17 + }, + { + messageId: "always", + line: 14, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 9, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 8, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "field", next: "*" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "*", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, + { + messageId: "always", + line: 13, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "always", prev: "*", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { enforce: [{ blankLine: "always", prev: "*", next: "*" }] } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, + { + messageId: "always", + line: 13, + column: 17 + } + ] + }, + + // enforce option - blankLine: "never" + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 11, + column: 17 + }, + { + messageId: "never", + line: 15, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 14, + column: 17 + }, + { + messageId: "never", + line: 18, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "field" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "field" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "method", next: "*" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + }, + { + messageId: "never", + line: 14, + column: 17 + }, + { + messageId: "never", + line: 18, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; +method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "method" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 12, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; +#fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 10, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; +#fieldB = 'Field B'; +method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "field", next: "*" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 10, + column: 17 + }, + { + messageId: "never", + line: 12, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; +method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "*", next: "method" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 12, + column: 17 + }, + { + messageId: "never", + line: 14, + column: 17 + }, + { + messageId: "never", + line: 18, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; +#fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + method2() {} + } + `, + options: [ + { + enforce: [ + { blankLine: "never", prev: "*", next: "field" } + ] + } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + }, + { + messageId: "never", + line: 10, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } +fieldA = 'Field A'; +#fieldB = 'Field B'; +method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { enforce: [{ blankLine: "never", prev: "*", next: "*" }] } + ], + errors: [ + { + messageId: "never", + line: 8, + column: 17 + }, + { + messageId: "never", + line: 10, + column: 17 + }, + { + messageId: "never", + line: 12, + column: 17 + }, + { + messageId: "never", + line: 14, + column: 17 + }, { + messageId: "never", + line: 18, + column: 17 + } + ] + }, + + // enforce option - multiple configurations + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; +#fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods, disallows blank lines between fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "never", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "never", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, + { + messageId: "always", + line: 11, + column: 17 + }, { + messageId: "always", + line: 14, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} +get area() { + return this.method1(); + } +method2() {} + } + `, + options: [ + { + + // requires blank lines around fields, disallows blank lines between methods + enforce: [ + { blankLine: "always", prev: "*", next: "field" }, + { blankLine: "always", prev: "field", next: "*" }, + { blankLine: "never", prev: "method", next: "method" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "never", + line: 11, + column: 17 + }, { + messageId: "never", + line: 15, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, { + messageId: "always", + line: 13, + column: 17 + } + ] + }, + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + + #fieldB = 'Field B'; + + method1() {} + + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "never", prev: "*", next: "method" }, + { blankLine: "never", prev: "method", next: "*" }, + { blankLine: "never", prev: "field", next: "field" }, + + // This should take precedence over the above + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 8, + column: 17 + }, + { + messageId: "always", + line: 9, + column: 17 + }, + { + messageId: "always", + line: 10, + column: 17 + }, { + messageId: "always", + line: 13, + column: 17 + } + ] + }, + + // enforce with exceptAfterSingleLine option + { + code: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + method2() {} + } + `, + output: ` + class MyClass { + constructor(height, width) { + this.height = height; + this.width = width; + } + + fieldA = 'Field A'; + #fieldB = 'Field B'; + method1() {} + get area() { + return this.method1(); + } + + method2() {} + } + `, + options: [ + { + + // requires blank lines around methods and fields + enforce: [ + { blankLine: "always", prev: "*", next: "method" }, + { blankLine: "always", prev: "method", next: "*" }, + { blankLine: "always", prev: "field", next: "field" } + ] + }, + { + exceptAfterSingleLine: true + } + ], + errors: [ + { + messageId: "always", + line: 7, + column: 17 + }, + { + messageId: "always", + line: 13, + column: 17 + } + ] } ] }); diff --git a/tests/lib/rules/logical-assignment-operators.js b/tests/lib/rules/logical-assignment-operators.js index ba839b5c6a8..471416322d2 100644 --- a/tests/lib/rules/logical-assignment-operators.js +++ b/tests/lib/rules/logical-assignment-operators.js @@ -9,7 +9,8 @@ //------------------------------------------------------------------------------ const rule = require("../../../lib/rules/logical-assignment-operators"), - { RuleTester } = require("../../../lib/rule-tester"); + { RuleTester } = require("../../../lib/rule-tester"), + parser = require("../../fixtures/fixture-parser"); //------------------------------------------------------------------------------ // Tests @@ -353,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: [ @@ -1456,5 +1479,205 @@ ruleTester.run("logical-assignment-operators", rule, { output: "a = a ?? b + c", options: ["never"], errors: [{ messageId: "unexpected", type: "AssignmentExpression", data: { operator: "??=" } }] - }] + }, + + // https://github.com/eslint/eslint/issues/17173 + { + code: "a ||= b as number;", + output: "a = a || (b as number);", + options: ["never"], + parser: parser("typescript-parsers/logical-assignment-with-assertion"), + errors: [{ messageId: "unexpected", type: "AssignmentExpression", data: { operator: "||=" } }] + }, + { + code: "a.b.c || (a.b.c = d as number)", + output: null, + parser: parser("typescript-parsers/logical-with-assignment-with-assertion-1"), + errors: [{ + messageId: "logical", + type: "LogicalExpression", + data: { operator: "||=" }, + suggestions: [{ + messageId: "convertLogical", + data: { operator: "||=" }, + output: "a.b.c ||= d as number" + }] + }] + }, + { + code: "a.b.c || (a.b.c = (d as number))", + output: null, + parser: parser("typescript-parsers/logical-with-assignment-with-assertion-2"), + errors: [{ + messageId: "logical", + type: "LogicalExpression", + data: { operator: "||=" }, + suggestions: [{ + messageId: "convertLogical", + data: { operator: "||=" }, + output: "a.b.c ||= (d as number)" + }] + }] + }, + { + code: "(a.b.c || (a.b.c = d)) as number", + output: null, + parser: parser("typescript-parsers/logical-with-assignment-with-assertion-3"), + errors: [{ + messageId: "logical", + type: "LogicalExpression", + data: { operator: "||=" }, + suggestions: [{ + messageId: "convertLogical", + data: { operator: "||=" }, + 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: [] + }] + } + ] }); diff --git a/tests/lib/rules/no-case-declarations.js b/tests/lib/rules/no-case-declarations.js index e1bd6b101a1..bee630a1bad 100644 --- a/tests/lib/rules/no-case-declarations.js +++ b/tests/lib/rules/no-case-declarations.js @@ -35,9 +35,42 @@ ruleTester.run("no-case-declarations", rule, { { code: "switch (a) { case 1: { class C {} break; } default: { class C {} break; } }", parserOptions: { ecmaVersion: 6 } - } + }, + ` + switch (a) { + case 1: + case 2: {} + } + `, + ` + switch (a) { + case 1: var x; + } + ` ], invalid: [ + { + code: ` + switch (a) { + case 1: + {} + function f() {} + break; + } + `, + errors: [{ messageId: "unexpected", type: "FunctionDeclaration" }] + }, + { + code: ` + switch (a) { + case 1: + case 2: + let x; + } + `, + parserOptions: { ecmaVersion: 6 }, + errors: [{ messageId: "unexpected", type: "VariableDeclaration" }] + }, { code: "switch (a) { case 1: let x = 1; break; }", parserOptions: { ecmaVersion: 6 }, diff --git a/tests/lib/rules/no-control-regex.js b/tests/lib/rules/no-control-regex.js index 14abfbce450..3bfc87bace1 100644 --- a/tests/lib/rules/no-control-regex.js +++ b/tests/lib/rules/no-control-regex.js @@ -33,7 +33,10 @@ ruleTester.run("no-control-regex", rule, { String.raw`new RegExp("\\u{20}", "u")`, String.raw`new RegExp("\\u{1F}")`, String.raw`new RegExp("\\u{1F}", "g")`, - String.raw`new RegExp("\\u{1F}", flags)` // when flags are unknown, this rule assumes there's no `u` flag + String.raw`new RegExp("\\u{1F}", flags)`, // when flags are unknown, this rule assumes there's no `u` flag + String.raw`new RegExp("[\\q{\\u{20}}]", "v")`, + { code: String.raw`/[\u{20}--B]/v`, parserOptions: { ecmaVersion: 2024 } } + ], invalid: [ { code: String.raw`var regex = /\x1f/`, errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }] }, @@ -85,6 +88,20 @@ ruleTester.run("no-control-regex", rule, { { code: String.raw`new RegExp("\\u{1F}", "gui")`, errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }] + }, + { + code: String.raw`new RegExp("[\\q{\\u{1F}}]", "v")`, + errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }] + }, + { + code: String.raw`/[\u{1F}--B]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ messageId: "unexpected", data: { controlChars: "\\x1f" }, type: "Literal" }] + }, + { + code: String.raw`/\x11/; RegExp("foo", "uv");`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ messageId: "unexpected", data: { controlChars: "\\x11" }, type: "Literal", column: 1 }] } ] }); diff --git a/tests/lib/rules/no-empty-character-class.js b/tests/lib/rules/no-empty-character-class.js index fd4cef8ed77..81b66c4b300 100644 --- a/tests/lib/rules/no-empty-character-class.js +++ b/tests/lib/rules/no-empty-character-class.js @@ -25,15 +25,26 @@ ruleTester.run("no-empty-character-class", rule, { "var foo = /^abc/;", "var foo = /[\\[]/;", "var foo = /[\\]]/;", + "var foo = /\\[][\\]]/;", "var foo = /[a-zA-Z\\[]/;", "var foo = /[[]/;", "var foo = /[\\[a-z[]]/;", "var foo = /[\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\^\\$\\|]/g;", "var foo = /\\s*:\\s*/gim;", + "var foo = /[^]/;", // this rule allows negated empty character classes + "var foo = /\\[][^]/;", { code: "var foo = /[\\]]/uy;", parserOptions: { ecmaVersion: 6 } }, { code: "var foo = /[\\]]/s;", parserOptions: { ecmaVersion: 2018 } }, { code: "var foo = /[\\]]/d;", parserOptions: { ecmaVersion: 2022 } }, - "var foo = /\\[]/" + "var foo = /\\[]/", + { code: "var foo = /[[^]]/v;", parserOptions: { ecmaVersion: 2024 } }, + { code: "var foo = /[[\\]]]/v;", parserOptions: { ecmaVersion: 2024 } }, + { code: "var foo = /[[\\[]]/v;", parserOptions: { ecmaVersion: 2024 } }, + { code: "var foo = /[a--b]/v;", parserOptions: { ecmaVersion: 2024 } }, + { code: "var foo = /[a&&b]/v;", parserOptions: { ecmaVersion: 2024 } }, + { code: "var foo = /[[a][b]]/v;", parserOptions: { ecmaVersion: 2024 } }, + { code: "var foo = /[\\q{}]/v;", parserOptions: { ecmaVersion: 2024 } }, + { code: "var foo = /[[^]--\\p{ASCII}]/v;", parserOptions: { ecmaVersion: 2024 } } ], invalid: [ { code: "var foo = /^abc[]/;", errors: [{ messageId: "unexpected", type: "Literal" }] }, @@ -43,6 +54,15 @@ ruleTester.run("no-empty-character-class", rule, { { code: "var foo = /[]]/;", errors: [{ messageId: "unexpected", type: "Literal" }] }, { code: "var foo = /\\[[]/;", errors: [{ messageId: "unexpected", type: "Literal" }] }, { code: "var foo = /\\[\\[\\]a-z[]/;", errors: [{ messageId: "unexpected", type: "Literal" }] }, - { code: "var foo = /[]]/d;", parserOptions: { ecmaVersion: 2022 }, errors: [{ messageId: "unexpected", type: "Literal" }] } + { code: "var foo = /[]]/d;", parserOptions: { ecmaVersion: 2022 }, errors: [{ messageId: "unexpected", type: "Literal" }] }, + { code: "var foo = /[(]\\u{0}*[]/u;", parserOptions: { ecmaVersion: 2015 }, errors: [{ messageId: "unexpected", type: "Literal" }] }, + { code: "var foo = /[]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] }, + { code: "var foo = /[[]]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] }, + { code: "var foo = /[[a][]]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] }, + { code: "var foo = /[a[[b[]c]]d]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] }, + { code: "var foo = /[a--[]]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] }, + { code: "var foo = /[[]--b]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] }, + { code: "var foo = /[a&&[]]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] }, + { code: "var foo = /[[]&&b]/v;", parserOptions: { ecmaVersion: 2024 }, errors: [{ messageId: "unexpected", type: "Literal" }] } ] }); diff --git a/tests/lib/rules/no-empty-pattern.js b/tests/lib/rules/no-empty-pattern.js index 0d2d7ea80aa..2cd06c4be37 100644 --- a/tests/lib/rules/no-empty-pattern.js +++ b/tests/lib/rules/no-empty-pattern.js @@ -26,7 +26,13 @@ ruleTester.run("no-empty-pattern", rule, { { code: "var {a = []} = foo;", parserOptions: { ecmaVersion: 6 } }, { code: "function foo({a = {}}) {}", parserOptions: { ecmaVersion: 6 } }, { code: "function foo({a = []}) {}", parserOptions: { ecmaVersion: 6 } }, - { code: "var [a] = foo", parserOptions: { ecmaVersion: 6 } } + { code: "var [a] = foo", parserOptions: { ecmaVersion: 6 } }, + { code: "function foo({}) {}", options: [{ allowObjectPatternsAsParameters: true }], parserOptions: { ecmaVersion: 6 } }, + { code: "var foo = function({}) {}", options: [{ allowObjectPatternsAsParameters: true }], parserOptions: { ecmaVersion: 6 } }, + { code: "var foo = ({}) => {}", options: [{ allowObjectPatternsAsParameters: true }], parserOptions: { ecmaVersion: 6 } }, + { code: "function foo({} = {}) {}", options: [{ allowObjectPatternsAsParameters: true }], parserOptions: { ecmaVersion: 6 } }, + { code: "var foo = function({} = {}) {}", options: [{ allowObjectPatternsAsParameters: true }], parserOptions: { ecmaVersion: 6 } }, + { code: "var foo = ({} = {}) => {}", options: [{ allowObjectPatternsAsParameters: true }], parserOptions: { ecmaVersion: 6 } } ], // Examples of code that should trigger the rule @@ -111,6 +117,106 @@ ruleTester.run("no-empty-pattern", rule, { data: { type: "array" }, type: "ArrayPattern" }] + }, + { + code: "function foo({}) {}", + options: [{}], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "unexpected", + data: { type: "object" }, + type: "ObjectPattern" + }] + }, + { + code: "var foo = function({}) {}", + options: [{}], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "unexpected", + data: { type: "object" }, + type: "ObjectPattern" + }] + }, + { + code: "var foo = ({}) => {}", + options: [{}], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "unexpected", + data: { type: "object" }, + type: "ObjectPattern" + }] + }, + { + code: "function foo({} = {}) {}", + options: [{}], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "unexpected", + data: { type: "object" }, + type: "ObjectPattern" + }] + }, + { + code: "var foo = function({} = {}) {}", + options: [{}], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "unexpected", + data: { type: "object" }, + type: "ObjectPattern" + }] + }, + { + code: "var foo = ({} = {}) => {}", + options: [{}], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "unexpected", + data: { type: "object" }, + type: "ObjectPattern" + }] + }, + { + code: "var foo = ({a: {}}) => {}", + options: [{ allowObjectPatternsAsParameters: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "unexpected", + data: { type: "object" }, + type: "ObjectPattern" + }] + }, + { + code: "var foo = ({} = bar) => {}", + options: [{ allowObjectPatternsAsParameters: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "unexpected", + data: { type: "object" }, + type: "ObjectPattern" + }] + }, + { + code: "var foo = ({} = { bar: 1 }) => {}", + options: [{ allowObjectPatternsAsParameters: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "unexpected", + data: { type: "object" }, + type: "ObjectPattern" + }] + }, + { + code: "var foo = ([]) => {}", + options: [{ allowObjectPatternsAsParameters: true }], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "unexpected", + data: { type: "array" }, + type: "ArrayPattern" + }] } ] }); diff --git a/tests/lib/rules/no-eval.js b/tests/lib/rules/no-eval.js index 840bcb20f1d..de1e11df19e 100644 --- a/tests/lib/rules/no-eval.js +++ b/tests/lib/rules/no-eval.js @@ -57,6 +57,11 @@ ruleTester.run("no-eval", rule, { { code: "class A { field = () => this.eval(); }", parserOptions: { ecmaVersion: 2022 } }, { code: "class A { static { this.eval(); } }", parserOptions: { ecmaVersion: 2022 } }, + // User-defined this.eval in callbacks + "array.findLast(function (x) { return this.eval.includes(x); }, { eval: ['foo', 'bar'] });", + "callbacks.findLastIndex(function (cb) { return cb(this.eval); }, this);", + "['1+1'].flatMap(function (str) { return this.eval(str); }, new Evaluator);", + // Allows indirect eval { code: "(0, eval)('foo')", options: [{ allowIndirect: true }] }, { code: "(0, window.eval)('foo')", options: [{ allowIndirect: true }], env: { browser: true } }, @@ -162,6 +167,25 @@ ruleTester.run("no-eval", rule, { code: "function foo() { 'use strict'; this.eval(); }", parserOptions: { ecmaVersion: 3 }, errors: [{ messageId: "unexpected" }] + }, + + // this.eval in callbacks (not user-defined) + { + code: "array.findLast(x => this.eval.includes(x), { eval: 'abc' });", + parserOptions: { ecmaVersion: 2023 }, + errors: [{ messageId: "unexpected" }] + }, + { + code: "callbacks.findLastIndex(function (cb) { return cb(eval); }, this);", + errors: [{ messageId: "unexpected" }] + }, + { + code: "['1+1'].flatMap(function (str) { return this.eval(str); });", + errors: [{ messageId: "unexpected" }] + }, + { + code: "['1'].reduce(function (a, b) { return this.eval(a) ? a : b; }, '0');", + errors: [{ messageId: "unexpected" }] } ] }); diff --git a/tests/lib/rules/no-extra-boolean-cast.js b/tests/lib/rules/no-extra-boolean-cast.js index 8aecb4ca37f..2e95cb740bb 100644 --- a/tests/lib/rules/no-extra-boolean-cast.js +++ b/tests/lib/rules/no-extra-boolean-cast.js @@ -10,7 +10,8 @@ //------------------------------------------------------------------------------ const rule = require("../../../lib/rules/no-extra-boolean-cast"), - { RuleTester } = require("../../../lib/rule-tester"); + { RuleTester } = require("../../../lib/rule-tester"), + parser = require("../../fixtures/fixture-parser"); //------------------------------------------------------------------------------ // Tests @@ -2423,6 +2424,15 @@ ruleTester.run("no-extra-boolean-cast", rule, { options: [{ enforceForLogicalOperands: true }], parserOptions: { ecmaVersion: 2020 }, errors: [{ messageId: "unexpectedCall" }] + }, + + // https://github.com/eslint/eslint/issues/17173 + { + code: "if (!Boolean(a as any)) { }", + output: "if (!(a as any)) { }", + parser: parser("typescript-parsers/boolean-cast-with-assertion"), + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "unexpectedCall" }] } ] }); diff --git a/tests/lib/rules/no-extra-parens.js b/tests/lib/rules/no-extra-parens.js index c51a47cfd3f..2d61522cb72 100644 --- a/tests/lib/rules/no-extra-parens.js +++ b/tests/lib/rules/no-extra-parens.js @@ -10,7 +10,8 @@ //------------------------------------------------------------------------------ const rule = require("../../../lib/rules/no-extra-parens"), - { RuleTester } = require("../../../lib/rule-tester"); + { RuleTester } = require("../../../lib/rule-tester"), + parser = require("../../fixtures/fixture-parser"); //------------------------------------------------------------------------------ // Helpers @@ -303,6 +304,14 @@ ruleTester.run("no-extra-parens", rule, { { code: "while (((foo = bar()))) {}", options: ["all", { conditionalAssign: false }] }, { code: "var a = (((b = c))) ? foo : bar;", options: ["all", { conditionalAssign: false }] }, + // ["all", { ternaryOperandBinaryExpressions: false }] enables extra parens around conditional ternary + { code: "(a && b) ? foo : bar", options: ["all", { ternaryOperandBinaryExpressions: false }] }, + { code: "(a - b > a) ? foo : bar", options: ["all", { ternaryOperandBinaryExpressions: false }] }, + { code: "foo ? (bar || baz) : qux", options: ["all", { ternaryOperandBinaryExpressions: false }] }, + { code: "foo ? bar : (baz || qux)", options: ["all", { ternaryOperandBinaryExpressions: false }] }, + { code: "(a, b) ? (c, d) : (e, f)", options: ["all", { ternaryOperandBinaryExpressions: false }] }, + { code: "(a = b) ? c : d", options: ["all", { ternaryOperandBinaryExpressions: false }] }, + // ["all", { nestedBinaryExpressions: false }] enables extra parens around conditional assignments { code: "a + (b * c)", options: ["all", { nestedBinaryExpressions: false }] }, { code: "(a * b) + c", options: ["all", { nestedBinaryExpressions: false }] }, @@ -783,6 +792,12 @@ ruleTester.run("no-extra-parens", rule, { { code: "((a)) = function () {};", options: ["functions"] + }, + + // https://github.com/eslint/eslint/issues/17173 + { + code: "const x = (1 satisfies number).toFixed();", + parser: parser("typescript-parsers/member-call-expr-with-assertion") } ], @@ -915,6 +930,10 @@ ruleTester.run("no-extra-parens", rule, { invalid("a ? b : (c = d)", "a ? b : c = d", "AssignmentExpression"), invalid("(c = d) ? (b) : c", "(c = d) ? b : c", "Identifier", null, { options: ["all", { conditionalAssign: false }] }), invalid("(c = d) ? b : (c)", "(c = d) ? b : c", "Identifier", null, { options: ["all", { conditionalAssign: false }] }), + invalid("(a) ? foo : bar", "a ? foo : bar", "Identifier", null, { options: ["all", { ternaryOperandBinaryExpressions: false }] }), + invalid("(a()) ? foo : bar", "a() ? foo : bar", "CallExpression", null, { options: ["all", { ternaryOperandBinaryExpressions: false }] }), + invalid("(a.b) ? foo : bar", "a.b ? foo : bar", "MemberExpression", null, { options: ["all", { ternaryOperandBinaryExpressions: false }] }), + invalid("(a || b) ? foo : (bar)", "(a || b) ? foo : bar", "Identifier", null, { options: ["all", { ternaryOperandBinaryExpressions: false }] }), invalid("f((a = b))", "f(a = b)", "AssignmentExpression"), invalid("a, (b = c)", "a, b = c", "AssignmentExpression"), invalid("a = (b * c)", "a = b * c", "BinaryExpression"), @@ -3444,6 +3463,24 @@ ruleTester.run("no-extra-parens", rule, { `a ${operator} function () {};`, "Identifier" ) - ) + ), + + // Potential directives (no autofix) + invalid("('use strict');", null), + invalid("function f() { ('abc'); }", null), + invalid("(function () { ('abc'); })();", null), + invalid("_ = () => { ('abc'); };", null), + invalid("'use strict';(\"foobar\");", null), + invalid("foo(); ('bar');", null), + invalid("foo = { bar() { ; (\"baz\"); } };", null), + + // Directive lookalikes + invalid("(12345);", "12345;"), + invalid("(('foobar'));", "('foobar');"), + invalid("(`foobar`);", "`foobar`;"), + invalid("void ('foobar');", "void 'foobar';"), + invalid("_ = () => ('abc');", "_ = () => 'abc';"), + invalid("if (foo) ('bar');", "if (foo) 'bar';"), + invalid("const foo = () => ('bar');", "const foo = () => 'bar';") ] }); diff --git a/tests/lib/rules/no-extra-semi.js b/tests/lib/rules/no-extra-semi.js index 4f19fb722fa..9a2d9a31bee 100644 --- a/tests/lib/rules/no-extra-semi.js +++ b/tests/lib/rules/no-extra-semi.js @@ -190,6 +190,43 @@ ruleTester.run("no-extra-semi", rule, { output: "class A { static { a; } foo(){} }", parserOptions: { ecmaVersion: 2022 }, errors: [{ messageId: "unexpected", type: "Punctuator", column: 24 }] + }, + + // https://github.com/eslint/eslint/issues/16988 + { + code: "; 'use strict'", + output: null, + errors: [{ messageId: "unexpected", type: "EmptyStatement" }] + }, + { + code: "; ; 'use strict'", + output: " ; 'use strict'", + errors: [{ messageId: "unexpected", type: "EmptyStatement" }, { messageId: "unexpected", type: "EmptyStatement" }] + }, + { + code: "debugger;\n;\n'use strict'", + output: null, + errors: [{ messageId: "unexpected", type: "EmptyStatement", line: 2 }] + }, + { + code: "function foo() { ; 'bar'; }", + output: null, + errors: [{ messageId: "unexpected", type: "EmptyStatement" }] + }, + { + code: "{ ; 'foo'; }", + output: "{ 'foo'; }", + errors: [{ messageId: "unexpected", type: "EmptyStatement" }] + }, + { + code: "; ('use strict');", + output: " ('use strict');", + errors: [{ messageId: "unexpected", type: "EmptyStatement" }] + }, + { + code: "; 1;", + output: " 1;", + errors: [{ messageId: "unexpected", type: "EmptyStatement" }] } ] }); diff --git a/tests/lib/rules/no-invalid-regexp.js b/tests/lib/rules/no-invalid-regexp.js index ceaa8f13a44..dfd662bad27 100644 --- a/tests/lib/rules/no-invalid-regexp.js +++ b/tests/lib/rules/no-invalid-regexp.js @@ -81,6 +81,14 @@ ruleTester.run("no-invalid-regexp", rule, { "new RegExp('\\\\p{Script=Vith}', 'u')", "new RegExp('\\\\p{Script=Vithkuqi}', 'u')", + // ES2024 + "new RegExp('[A--B]', 'v')", + "new RegExp('[A&&B]', 'v')", + "new RegExp('[A--[0-9]]', 'v')", + "new RegExp('[\\\\p{Basic_Emoji}--\\\\q{a|bc|def}]', 'v')", + "new RegExp('[A--B]', flags)", // valid only with `v` flag + "new RegExp('[[]\\\\u{0}*', flags)", // valid only with `u` flag + // allowConstructorFlags { code: "new RegExp('.', 'g')", @@ -288,6 +296,48 @@ ruleTester.run("no-invalid-regexp", rule, { data: { message: "Invalid flags supplied to RegExp constructor 'z'" }, type: "NewExpression" }] + }, + + // ES2024 + { + code: "new RegExp('[[]', 'v');", + errors: [{ + messageId: "regexMessage", + data: { message: "Invalid regular expression: /[[]/v: Unterminated character class" }, + type: "NewExpression" + }] + }, + { + code: "new RegExp('.', 'uv');", + errors: [{ + messageId: "regexMessage", + data: { message: "Regex 'u' and 'v' flags cannot be used together" }, + type: "NewExpression" + }] + }, + { + code: "new RegExp(pattern, 'uv');", + errors: [{ + messageId: "regexMessage", + data: { message: "Regex 'u' and 'v' flags cannot be used together" }, + type: "NewExpression" + }] + }, + { + code: "new RegExp('[A--B]' /* valid only with `v` flag */, 'u')", + errors: [{ + messageId: "regexMessage", + data: { message: "Invalid regular expression: /[A--B]/u: Range out of order in character class" }, + type: "NewExpression" + }] + }, + { + code: "new RegExp('[[]\\\\u{0}*' /* valid only with `u` flag */, 'v')", + errors: [{ + messageId: "regexMessage", + data: { message: "Invalid regular expression: /[[]\\u{0}*/v: Unterminated character class" }, + type: "NewExpression" + }] } ] }); diff --git a/tests/lib/rules/no-invalid-this.js b/tests/lib/rules/no-invalid-this.js index dd72be49991..7e8d4776d9b 100644 --- a/tests/lib/rules/no-invalid-this.js +++ b/tests/lib/rules/no-invalid-this.js @@ -474,103 +474,47 @@ const patterns = [ valid: [NORMAL], invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] }, - { - code: "foo.every(function() { console.log(this); z(x => console.log(x, this)); });", - parserOptions: { ecmaVersion: 6 }, - errors, - valid: [NORMAL], - invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] - }, - { - code: "foo.filter(function() { console.log(this); z(x => console.log(x, this)); });", - parserOptions: { ecmaVersion: 6 }, - errors, - valid: [NORMAL], - invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] - }, - { - code: "foo.find(function() { console.log(this); z(x => console.log(x, this)); });", - parserOptions: { ecmaVersion: 6 }, - errors, - valid: [NORMAL], - invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] - }, - { - code: "foo.findIndex(function() { console.log(this); z(x => console.log(x, this)); });", - parserOptions: { ecmaVersion: 6 }, - errors, - valid: [NORMAL], - invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] - }, - { - code: "foo.forEach(function() { console.log(this); z(x => console.log(x, this)); });", - parserOptions: { ecmaVersion: 6 }, - errors, - valid: [NORMAL], - invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] - }, - { - code: "foo.map(function() { console.log(this); z(x => console.log(x, this)); });", - parserOptions: { ecmaVersion: 6 }, - errors, - valid: [NORMAL], - invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] - }, - { - code: "foo.some(function() { console.log(this); z(x => console.log(x, this)); });", + ...[ + "every", + "filter", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flatMap", + "forEach", + "map", + "some" + ].map(methodName => ({ + code: `foo.${methodName}(function() { console.log(this); z(x => console.log(x, this)); });`, parserOptions: { ecmaVersion: 6 }, errors, valid: [NORMAL], invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] - }, + })), { code: "Array.from([], function() { console.log(this); z(x => console.log(x, this)); }, obj);", parserOptions: { ecmaVersion: 6 }, valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], invalid: [] }, - { - code: "foo.every(function() { console.log(this); z(x => console.log(x, this)); }, obj);", - parserOptions: { ecmaVersion: 6 }, - valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], - invalid: [] - }, - { - code: "foo.filter(function() { console.log(this); z(x => console.log(x, this)); }, obj);", - parserOptions: { ecmaVersion: 6 }, - valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], - invalid: [] - }, - { - code: "foo.find(function() { console.log(this); z(x => console.log(x, this)); }, obj);", + ...[ + "every", + "filter", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flatMap", + "forEach", + "map", + "some" + ].map(methodName => ({ + code: `foo.${methodName}(function() { console.log(this); z(x => console.log(x, this)); }, obj);`, parserOptions: { ecmaVersion: 6 }, valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], invalid: [] - }, - { - code: "foo.findIndex(function() { console.log(this); z(x => console.log(x, this)); }, obj);", - parserOptions: { ecmaVersion: 6 }, - valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], - invalid: [] - }, - { - code: "foo.forEach(function() { console.log(this); z(x => console.log(x, this)); }, obj);", - parserOptions: { ecmaVersion: 6 }, - valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], - invalid: [] - }, - { - code: "foo.map(function() { console.log(this); z(x => console.log(x, this)); }, obj);", - parserOptions: { ecmaVersion: 6 }, - valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], - invalid: [] - }, - { - code: "foo.some(function() { console.log(this); z(x => console.log(x, this)); }, obj);", - parserOptions: { ecmaVersion: 6 }, - valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], - invalid: [] - }, + })), { code: "foo.forEach(function() { console.log(this); z(x => console.log(x, this)); }, null);", parserOptions: { ecmaVersion: 6 }, diff --git a/tests/lib/rules/no-irregular-whitespace.js b/tests/lib/rules/no-irregular-whitespace.js index 9becb5a9ecf..e6ca5d111b6 100644 --- a/tests/lib/rules/no-irregular-whitespace.js +++ b/tests/lib/rules/no-irregular-whitespace.js @@ -171,6 +171,28 @@ ruleTester.run("no-irregular-whitespace", rule, { { code: "const error = `\n\u3000\n`;", options: [{ skipTemplates: true }], parserOptions: { ecmaVersion: 6 } }, { code: "const error = `foo\u3000bar\nfoo\u3000bar`;", options: [{ skipTemplates: true }], parserOptions: { ecmaVersion: 6 } }, + { code: "
\u000B
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u000C
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u0085
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u00A0
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u180E
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\ufeff
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u2000
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u2001
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u2002
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u2003
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u2004
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u2005
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u2006
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u2007
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u2008
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u2009
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u200A
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u200B
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u202F
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u205f
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + { code: "
\u3000
;", options: [{ skipJSXText: true }], parserOptions: { ecmaFeatures: { jsx: true } } }, + // Unicode BOM. "\uFEFFconsole.log('hello BOM');" ], @@ -993,6 +1015,342 @@ ruleTester.run("no-irregular-whitespace", rule, { endColumn: 2 } ] + }, + { + code: "
\u000B
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u000C
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u0085
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u00A0
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u180E
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\ufeff
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u2000
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u2001
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u2002
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u2003
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u2004
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u2005
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u2006
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u2007
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u2008
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u2009
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u200A
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u200B
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u202F
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u205f
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] + }, + { + code: "
\u3000
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { + messageId: "noIrregularWhitespace", + type: "Program", + line: 1, + column: 6 + } + ] } ] }); diff --git a/tests/lib/rules/no-loop-func.js b/tests/lib/rules/no-loop-func.js index ba161ec49d6..f1311000f91 100644 --- a/tests/lib/rules/no-loop-func.js +++ b/tests/lib/rules/no-loop-func.js @@ -111,7 +111,53 @@ ruleTester.run("no-loop-func", rule, { "let a;" ].join("\n"), parserOptions: { ecmaVersion: 6 } + }, + + /* + * These loops _look_ like they might be unsafe, but because i is undeclared, they're fine + * at least as far as this rule is concerned - the loop doesn't declare/generate the variable. + */ + "while(i) { (function() { i; }) }", + "do { (function() { i; }) } while (i)", + + /** + * These loops _look_ like they might be unsafe, but because i is declared outside the loop + * and is not updated in or after the loop, they're fine as far as this rule is concerned. + * The variable that's captured is just the one variable shared by all the loops, but that's + * explicitly expected in these cases. + */ + "var i; while(i) { (function() { i; }) }", + "var i; do { (function() { i; }) } while (i)", + + /** + * These loops use an undeclared variable, and so shouldn't be flagged by this rule, + * they'll be picked up by no-undef. + */ + { + code: "for (var i=0; i x != undeclared)) { } }", + parserOptions: { ecmaVersion: 6 } } + ], invalid: [ { @@ -152,14 +198,6 @@ ruleTester.run("no-loop-func", rule, { code: "for (var i=0; i new Object()", + "var Object; new Object;", + { + code: "new Object()", + globals: { + Object: "off" + } + } + ], + invalid: [ + { + code: "new Object", + errors: [{ + messageId: "preferLiteral", + type: "NewExpression", + suggestions: [{ + desc: "Replace with '({})'.", + messageId: "useLiteral", + output: "({})" + }] + }] + }, + { + code: "Object()", + errors: [{ + messageId: "preferLiteral", + type: "CallExpression", + suggestions: [{ + desc: "Replace with '({})'.", + messageId: "useLiteral", + output: "({})" + }] + }] + }, + { + code: "const fn = () => Object();", + errors: [{ + messageId: "preferLiteral", + type: "CallExpression", + suggestions: [{ + desc: "Replace with '({})'.", + messageId: "useLiteral", + output: "const fn = () => ({});" + }] + }] + }, + { + code: "Object() instanceof Object;", + errors: [{ + messageId: "preferLiteral", + type: "CallExpression", + suggestions: [{ + desc: "Replace with '({})'.", + messageId: "useLiteral", + output: "({}) instanceof Object;" + }] + }] + }, + { + code: "const obj = Object?.();", + errors: [{ + messageId: "preferLiteral", + type: "CallExpression", + suggestions: [{ + desc: "Replace with '{}'.", + messageId: "useLiteral", + output: "const obj = {};" + }] + }] + }, + { + code: "(new Object() instanceof Object);", + errors: [{ + messageId: "preferLiteral", + type: "NewExpression", + suggestions: [{ + desc: "Replace with '{}'.", + messageId: "useLiteral", + output: "({} instanceof Object);" + }] + }] + }, + + ...[ + + // Semicolon required before `({})` to compensate for ASI + { + code: ` + foo + Object() + ` + }, + { + code: ` + foo() + Object() + ` + }, + { + code: ` + new foo + Object() + ` + }, + { + code: ` + (a++) + Object() + ` + }, + { + code: ` + ++a + Object() + ` + }, + { + code: ` + const foo = function() {} + Object() + ` + }, + { + code: ` + const foo = class {} + Object() + ` + }, + { + code: ` + foo = this.return + Object() + ` + }, + { + code: ` + var yield = bar.yield + Object() + ` + }, + { + code: ` + var foo = { bar: baz } + Object() + ` + }, + { + code: ` + + Object() + `, + parserOptions: { ecmaFeatures: { jsx: true } } + }, + { + code: ` + + Object() + `, + parserOptions: { ecmaFeatures: { jsx: true } } + } + ].map(props => ({ + ...props, + errors: [{ + messageId: "preferLiteral", + suggestions: [{ + desc: "Replace with '({})', add preceding semicolon.", + messageId: "useLiteralAfterSemicolon", + output: props.code.replace(/(new )?Object\(\)/u, ";({})") + }] + }] + })), + + ...[ + + // No semicolon required before `({})` because ASI does not occur + { code: "Object()" }, + { + code: ` + {} + Object() + ` + }, + { + code: ` + function foo() {} + Object() + ` + }, + { + code: ` + class Foo {} + Object() + ` + }, + { code: "foo: Object();" }, + { code: "foo();Object();" }, + { code: "{ Object(); }" }, + { code: "if (a) Object();" }, + { code: "if (a); else Object();" }, + { code: "while (a) Object();" }, + { + code: ` + do Object(); + while (a); + ` + }, + { code: "for (let i = 0; i < 10; i++) Object();" }, + { code: "for (const prop in obj) Object();" }, + { code: "for (const element of iterable) Object();" }, + { code: "with (obj) Object();" }, + + // No semicolon required before `({})` because ASI still occurs + { + code: ` + const foo = () => {} + Object() + ` + }, + { + code: ` + a++ + Object() + ` + }, + { + code: ` + a-- + Object() + ` + }, + { + code: ` + function foo() { + return + Object(); + } + ` + }, + { + code: ` + function * foo() { + yield + Object(); + } + ` + }, + { + code: ` + do {} + while (a) + Object() + ` + }, + { + code: ` + debugger + Object() + ` + }, + { + code: ` + for (;;) { + break + Object() + } + ` + }, + { + code: ` + for (;;) { + continue + Object() + } + ` + }, + { + code: ` + foo: break foo + Object() + ` + }, + { + code: ` + foo: while (true) continue foo + Object() + ` + }, + { + code: ` + const foo = bar + export { foo } + Object() + `, + parserOptions: { sourceType: "module" } + }, + { + code: ` + export { foo } from 'bar' + Object() + `, + parserOptions: { sourceType: "module" } + }, + { + code: ` + export * as foo from 'bar' + Object() + `, + parserOptions: { sourceType: "module" } + }, + { + code: ` + import foo from 'bar' + Object() + `, + parserOptions: { sourceType: "module" } + }, + { + code: ` + var yield = 5; + + yield: while (foo) { + if (bar) + break yield + new Object(); + } + ` + } + ].map(props => ({ + ...props, + errors: [{ + messageId: "preferLiteral", + suggestions: [{ + desc: "Replace with '({})'.", + messageId: "useLiteral", + output: props.code.replace(/(new )?Object\(\)/u, "({})") + }] + }] + })) + ] +}); diff --git a/tests/lib/rules/no-promise-executor-return.js b/tests/lib/rules/no-promise-executor-return.js index a24629b6a44..ee8a53929fb 100644 --- a/tests/lib/rules/no-promise-executor-return.js +++ b/tests/lib/rules/no-promise-executor-return.js @@ -12,29 +12,6 @@ const rule = require("../../../lib/rules/no-promise-executor-return"); const { RuleTester } = require("../../../lib/rule-tester"); -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -/** - * Creates an error object. - * @param {number} [column] Reported column. - * @param {string} [type="ReturnStatement"] Reported node type. - * @returns {Object} The error object. - */ -function error(column, type = "ReturnStatement") { - const errorObject = { - messageId: "returnsValue", - type - }; - - if (column) { - errorObject.column = column; - } - - return errorObject; -} - //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ @@ -149,7 +126,37 @@ ruleTester.run("no-promise-executor-return", rule, { { code: "new Promise(function (resolve, reject) {}); return 1;", env: { node: true } - } + }, + + /* + * allowVoid: true + * `=> void` and `return void` are allowed + */ + { + code: "new Promise((r) => void cbf(r));", + options: [{ + allowVoid: true + }] + }, + { + code: "new Promise(r => void 0)", + options: [{ + allowVoid: true + }] + }, + { + code: "new Promise(r => { return void 0 })", + options: [{ + allowVoid: true + }] + }, + { + code: "new Promise(r => { if (foo) { return void 0 } return void 0 })", + options: [{ + allowVoid: true + }] + }, + "new Promise(r => {0})" ], invalid: [ @@ -157,147 +164,791 @@ ruleTester.run("no-promise-executor-return", rule, { // full error tests { code: "new Promise(function (resolve, reject) { return 1; })", - errors: [{ message: "Return values from promise executor functions cannot be read.", type: "ReturnStatement", column: 42, endColumn: 51 }] + errors: [{ + message: "Return values from promise executor functions cannot be read.", + type: "ReturnStatement", + column: 42, + endColumn: 51, + suggestions: null + }] }, { code: "new Promise((resolve, reject) => resolve(1))", - errors: [{ message: "Return values from promise executor functions cannot be read.", type: "CallExpression", column: 34, endColumn: 44 }] + options: [{ + allowVoid: true + }], + errors: [{ + message: "Return values from promise executor functions cannot be read.", + type: "CallExpression", + column: 34, + endColumn: 44, + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise((resolve, reject) => void resolve(1))" + }, + { + messageId: "wrapBraces", + output: "new Promise((resolve, reject) => {resolve(1)})" + } + ] + }] + }, + { + code: "new Promise((resolve, reject) => { return 1 })", + options: [{ + allowVoid: true + }], + errors: [{ + message: "Return values from promise executor functions cannot be read.", + type: "ReturnStatement", + column: 36, + endColumn: 44, + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise((resolve, reject) => { return void 1 })" + } + ] + }] + }, + + // suggestions arrow function expression + { + code: "new Promise(r => 1)", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "Literal", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(r => void 1)" + }, + { + messageId: "wrapBraces", + output: "new Promise(r => {1})" + } + ] + }] + }, + { + code: "new Promise(r => 1 ? 2 : 3)", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "ConditionalExpression", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(r => void (1 ? 2 : 3))" + }, + { + messageId: "wrapBraces", + output: "new Promise(r => {1 ? 2 : 3})" + } + ] + }] + }, + { + code: "new Promise(r => (1 ? 2 : 3))", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "ConditionalExpression", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(r => void (1 ? 2 : 3))" + }, + { + messageId: "wrapBraces", + output: "new Promise(r => {(1 ? 2 : 3)})" + } + ] + }] + }, + { + code: + "new Promise(r => (1))", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "Literal", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(r => void (1))" + }, + { + messageId: "wrapBraces", + output: "new Promise(r => {(1)})" + } + ] + }] + }, + { + code: + "new Promise(r => () => {})", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "ArrowFunctionExpression", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(r => void (() => {}))" + }, + { + messageId: "wrapBraces", + output: "new Promise(r => {() => {}})" + } + ] + }] + }, + + // primitives + { + code: + "new Promise(r => null)", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "Literal", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(r => void null)" + }, + { + messageId: "wrapBraces", + output: "new Promise(r => {null})" + } + ] + }] + }, + { + code: + "new Promise(r => null)", + options: [{ + allowVoid: false + }], + errors: [{ + messageId: "returnsValue", + type: "Literal", + suggestions: [ + { + messageId: "wrapBraces", + output: "new Promise(r => {null})" + } + ] + }] + }, + + // inline comments + { + code: + "new Promise(r => /*hi*/ ~0)", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "UnaryExpression", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(r => /*hi*/ void ~0)" + }, + { + messageId: "wrapBraces", + output: "new Promise(r => /*hi*/ {~0})" + } + ] + }] + }, + { + code: + "new Promise(r => /*hi*/ ~0)", + options: [{ + allowVoid: false + }], + errors: [{ + messageId: "returnsValue", + type: "UnaryExpression", + suggestions: [ + { + messageId: "wrapBraces", + output: "new Promise(r => /*hi*/ {~0})" + } + ] + }] + }, + + // suggestions function + { + code: + "new Promise(r => { return 0 })", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(r => { return void 0 })" + } + ] + }] + }, + { + code: "new Promise(r => { return 0 })", + options: [{ + allowVoid: false + }], + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] + }, + + // multiple returns + { + code: + "new Promise(r => { if (foo) { return void 0 } return 0 })", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(r => { if (foo) { return void 0 } return void 0 })" + } + ] + }] + }, + + // return assignment + { + code: "new Promise(resolve => { return (foo = resolve(1)); })", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(resolve => { return void (foo = resolve(1)); })" + } + ] + }] + }, + { + code: "new Promise(resolve => r = resolve)", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "AssignmentExpression", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(resolve => void (r = resolve))" + }, + { + messageId: "wrapBraces", + output: "new Promise(resolve => {r = resolve})" + } + ] + }] + }, + + // return (range check) + { + code: + "new Promise(r => { return(1) })", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(r => { return void (1) })" + } + ] + }] + }, + { + code: + "new Promise(r =>1)", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "Literal", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(r =>void 1)" + }, + { + messageId: "wrapBraces", + output: "new Promise(r =>{1})" + } + ] + }] + }, + + // snapshot + { + code: + "new Promise(r => ((1)))", + options: [{ + allowVoid: true + }], + errors: [{ + messageId: "returnsValue", + type: "Literal", + suggestions: [ + { + messageId: "prependVoid", + output: "new Promise(r => void ((1)))" + }, + { + messageId: "wrapBraces", + output: "new Promise(r => {((1))})" + } + ] + }] }, // other basic tests { code: "new Promise(function foo(resolve, reject) { return 1; })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, { code: "new Promise((resolve, reject) => { return 1; })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, // any returned value { code: "new Promise(function (resolve, reject) { return undefined; })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, { code: "new Promise((resolve, reject) => { return null; })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, { code: "new Promise(function (resolve, reject) { return false; })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, { code: "new Promise((resolve, reject) => resolve)", - errors: [error(34, "Identifier")] + errors: [{ + messageId: "returnsValue", + type: "Identifier", + column: 34, + suggestions: [ + { + messageId: "wrapBraces", + output: "new Promise((resolve, reject) => {resolve})" + } + ] + }] }, { code: "new Promise((resolve, reject) => null)", - errors: [error(34, "Literal")] + errors: [{ + messageId: "returnsValue", + type: "Literal", + column: 34, + suggestions: [ + { + messageId: "wrapBraces", + output: "new Promise((resolve, reject) => {null})" + } + ] + }] }, { code: "new Promise(function (resolve, reject) { return resolve(foo); })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, { code: "new Promise((resolve, reject) => { return reject(foo); })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, { code: "new Promise((resolve, reject) => x + y)", - errors: [error(34, "BinaryExpression")] + errors: [{ + messageId: "returnsValue", + type: "BinaryExpression", + column: 34, + suggestions: [ + { + messageId: "wrapBraces", + output: "new Promise((resolve, reject) => {x + y})" + } + ] + }] }, { code: "new Promise((resolve, reject) => { return Promise.resolve(42); })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, // any return statement location { code: "new Promise(function (resolve, reject) { if (foo) { return 1; } })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, { code: "new Promise((resolve, reject) => { try { return 1; } catch(e) {} })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, { code: "new Promise(function (resolve, reject) { while (foo){ if (bar) break; else return 1; } })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] + }, + + // `return void` is not allowed without `allowVoid: true` + { + code: "new Promise(() => { return void 1; })", + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] + }, + + { + code: "new Promise(() => (1))", + errors: [{ + messageId: "returnsValue", + type: "Literal", + column: 20, + suggestions: [ + { + messageId: "wrapBraces", + output: "new Promise(() => {(1)})" + } + ] + }] + }, + { + code: "() => new Promise(() => ({}));", + errors: [{ + messageId: "returnsValue", + type: "ObjectExpression", + column: 26, + suggestions: [ + { + messageId: "wrapBraces", + output: "() => new Promise(() => {({})});" + } + ] + }] }, // absence of arguments has no effect { code: "new Promise(function () { return 1; })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, { code: "new Promise(() => { return 1; })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, { code: "new Promise(() => 1)", - errors: [error(19, "Literal")] + errors: [{ + messageId: "returnsValue", + type: "Literal", + column: 19, + suggestions: [ + { + messageId: "wrapBraces", + output: "new Promise(() => {1})" + } + ] + }] }, // various scope tracking tests { code: "function foo() {} new Promise(function () { return 1; });", - errors: [error(45)] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + column: 45, + suggestions: null + }] }, { code: "function foo() { return; } new Promise(() => { return 1; });", - errors: [error(48)] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + column: 48, + suggestions: null + }] }, { code: "function foo() { return 1; } new Promise(() => { return 2; });", - errors: [error(50)] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + column: 50, + suggestions: null + }] }, { code: "function foo () { return new Promise(function () { return 1; }); }", - errors: [error(52)] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + column: 52, + suggestions: null + }] }, { code: "function foo() { return new Promise(() => { bar(() => { return 1; }); return false; }); }", - errors: [error(71)] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + column: 71, + suggestions: null + }] }, { code: "() => new Promise(() => { if (foo) { return 0; } else bar(() => { return 1; }); })", - errors: [error(38)] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + column: 38, + suggestions: null + }] }, { code: "function foo () { return 1; return new Promise(function () { return 2; }); return 3;}", - errors: [error(62)] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + column: 62, + suggestions: null + }] }, { code: "() => 1; new Promise(() => { return 1; })", - errors: [error(30)] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + column: 30, + suggestions: null + }] }, { code: "new Promise(function () { return 1; }); function foo() { return 1; } ", - errors: [error(27)] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + column: 27, + suggestions: null + }] }, { code: "() => new Promise(() => { return 1; });", - errors: [error(27)] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + column: 27, + suggestions: null + }] }, { code: "() => new Promise(() => 1);", - errors: [error(25, "Literal")] + errors: [{ + messageId: "returnsValue", + type: "Literal", + column: 25, + suggestions: [ + { + messageId: "wrapBraces", + output: "() => new Promise(() => {1});" + } + ] + }] }, { code: "() => new Promise(() => () => 1);", - errors: [error(25, "ArrowFunctionExpression")] + errors: [{ + messageId: "returnsValue", + type: "ArrowFunctionExpression", + column: 25, + suggestions: [ + { + messageId: "wrapBraces", + output: "() => new Promise(() => {() => 1});" + } + ] + }] + }, + { + code: "() => new Promise(() => async () => 1);", + parserOptions: { ecmaVersion: 2017 }, + + // for async + errors: [{ + messageId: "returnsValue", + type: "ArrowFunctionExpression", + column: 25, + suggestions: [ + { + messageId: "wrapBraces", + output: "() => new Promise(() => {async () => 1});" + } + ] + }] + }, + { + code: "() => new Promise(() => function () {});", + errors: [{ + messageId: "returnsValue", + type: "FunctionExpression", + column: 25, + suggestions: [ + { + messageId: "wrapBraces", + output: "() => new Promise(() => {function () {}});" + } + ] + }] + }, + { + code: "() => new Promise(() => function foo() {});", + errors: [{ + messageId: "returnsValue", + type: "FunctionExpression", + column: 25, + suggestions: [ + { + messageId: "wrapBraces", + output: "() => new Promise(() => {function foo() {}});" + } + ] + }] + }, + { + code: "() => new Promise(() => []);", + errors: [{ + messageId: "returnsValue", + type: "ArrayExpression", + column: 25, + suggestions: [ + { + messageId: "wrapBraces", + output: "() => new Promise(() => {[]});" + } + ] + }] }, // edge cases for global Promise reference { code: "new Promise((Promise) => { return 1; })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] }, { code: "new Promise(function Promise(resolve, reject) { return 1; })", - errors: [error()] + errors: [{ + messageId: "returnsValue", + type: "ReturnStatement", + suggestions: null + }] } ] }); diff --git a/tests/lib/rules/no-prototype-builtins.js b/tests/lib/rules/no-prototype-builtins.js index 6152e8acf6e..80755a6d878 100644 --- a/tests/lib/rules/no-prototype-builtins.js +++ b/tests/lib/rules/no-prototype-builtins.js @@ -61,6 +61,12 @@ ruleTester.run("no-prototype-builtins", rule, { endColumn: 19, messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, + suggestions: [ + { + messageId: "callObjectPrototype", + output: "Object.prototype.hasOwnProperty.call(foo, 'bar')" + } + ], type: "CallExpression" }] }, @@ -73,6 +79,12 @@ ruleTester.run("no-prototype-builtins", rule, { endColumn: 18, messageId: "prototypeBuildIn", data: { prop: "isPrototypeOf" }, + suggestions: [ + { + messageId: "callObjectPrototype", + output: "Object.prototype.isPrototypeOf.call(foo, 'bar')" + } + ], type: "CallExpression" }] }, @@ -84,6 +96,12 @@ ruleTester.run("no-prototype-builtins", rule, { endLine: 1, endColumn: 25, messageId: "prototypeBuildIn", + suggestions: [ + { + messageId: "callObjectPrototype", + output: "Object.prototype.propertyIsEnumerable.call(foo, 'bar')" + } + ], data: { prop: "propertyIsEnumerable" } }] }, @@ -96,6 +114,12 @@ ruleTester.run("no-prototype-builtins", rule, { endColumn: 23, messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, + suggestions: [ + { + messageId: "callObjectPrototype", + output: "Object.prototype.hasOwnProperty.call(foo.bar, 'bar')" + } + ], type: "CallExpression" }] }, @@ -108,6 +132,12 @@ ruleTester.run("no-prototype-builtins", rule, { endColumn: 26, messageId: "prototypeBuildIn", data: { prop: "isPrototypeOf" }, + suggestions: [ + { + messageId: "callObjectPrototype", + output: "Object.prototype.isPrototypeOf.call(foo.bar.baz, 'bar')" + } + ], type: "CallExpression" }] }, @@ -120,6 +150,12 @@ ruleTester.run("no-prototype-builtins", rule, { endColumn: 21, messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, + suggestions: [ + { + messageId: "callObjectPrototype", + output: "Object.prototype.hasOwnProperty.call(foo, 'bar')" + } + ], type: "CallExpression" }] }, @@ -133,6 +169,12 @@ ruleTester.run("no-prototype-builtins", rule, { endColumn: 20, messageId: "prototypeBuildIn", data: { prop: "isPrototypeOf" }, + suggestions: [ + { + messageId: "callObjectPrototype", + output: "Object.prototype.isPrototypeOf.call(foo, 'bar').baz" + } + ], type: "CallExpression" }] }, @@ -145,30 +187,116 @@ ruleTester.run("no-prototype-builtins", rule, { endColumn: 31, messageId: "prototypeBuildIn", data: { prop: "propertyIsEnumerable" }, + suggestions: [ + { + messageId: "callObjectPrototype", + output: String.raw`Object.prototype.propertyIsEnumerable.call(foo.bar, 'baz')` + } + ], type: "CallExpression" }] }, + { + + // Can't suggest Object.prototype when Object is shadowed + code: "(function(Object) {return foo.hasOwnProperty('bar');})", + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }] + }, + { + code: "foo.hasOwnProperty('bar')", + globals: { + Object: "off" + }, + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }], + name: "Can't suggest Object.prototype when there is no Object global variable" + }, // Optional chaining { code: "foo?.hasOwnProperty('bar')", parserOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }] + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }] + }, + { + code: "foo?.bar.hasOwnProperty('baz')", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }] + }, + { + code: "foo.hasOwnProperty?.('bar')", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }] + }, + { + + /* + * If hasOwnProperty is part of a ChainExpresion + * and the optional part is before it, then don't suggest the fix + */ + code: "foo?.hasOwnProperty('bar').baz", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }] }, { + + /* + * If hasOwnProperty is part of a ChainExpresion + * but the optional part is after it, then the fix is safe + */ + code: "foo.hasOwnProperty('bar')?.baz", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "prototypeBuildIn", + data: { prop: "hasOwnProperty" }, + suggestions: [ + { + messageId: "callObjectPrototype", + output: "Object.prototype.hasOwnProperty.call(foo, 'bar')?.baz" + } + ] + }] + }, + { + + code: "(a,b).hasOwnProperty('bar')", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ + messageId: "prototypeBuildIn", + data: { prop: "hasOwnProperty" }, + suggestions: [ + + // Make sure the SequenceExpression has parentheses before other arguments + { + messageId: "callObjectPrototype", + output: "Object.prototype.hasOwnProperty.call((a,b), 'bar')" + } + ] + }] + }, + { + + // No suggestion where no-unsafe-optional-chaining is reported on the call code: "(foo?.hasOwnProperty)('bar')", parserOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }] + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }] + + }, + { + code: "(foo?.hasOwnProperty)?.('bar')", + parserOptions: { ecmaVersion: 2020 }, + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }] }, { code: "foo?.['hasOwnProperty']('bar')", parserOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }] + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }] }, { + + // No suggestion where no-unsafe-optional-chaining is reported on the call code: "(foo?.[`hasOwnProperty`])('bar')", parserOptions: { ecmaVersion: 2020 }, - errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" } }] + errors: [{ messageId: "prototypeBuildIn", data: { prop: "hasOwnProperty" }, suggestions: [] }] } ] }); diff --git a/tests/lib/rules/no-regex-spaces.js b/tests/lib/rules/no-regex-spaces.js index 89952297549..fdc3eaa1817 100644 --- a/tests/lib/rules/no-regex-spaces.js +++ b/tests/lib/rules/no-regex-spaces.js @@ -62,9 +62,18 @@ ruleTester.run("no-regex-spaces", rule, { "var foo = new RegExp(' \\[ ');", "var foo = new RegExp(' \\[ \\] ');", + // ES2024 + { code: "var foo = / {2}/v;", parserOptions: { ecmaVersion: 2024 } }, + { code: "var foo = /[\\q{ }]/v;", parserOptions: { ecmaVersion: 2024 } }, + // don't report invalid regex "var foo = new RegExp('[ ');", - "var foo = new RegExp('{ ', 'u');" + "var foo = new RegExp('{ ', 'u');", + + // don't report if flags cannot be determined + "new RegExp(' ', flags)", + "new RegExp('[[abc] ]', flags + 'v')", + "new RegExp('[[abc]\\\\q{ }]', flags + 'v')" ], invalid: [ @@ -371,6 +380,33 @@ ruleTester.run("no-regex-spaces", rule, { type: "NewExpression" } ] + }, + + // ES2024 + { + code: "var foo = /[[ ] ] /v;", + output: "var foo = /[[ ] ] {4}/v;", + parserOptions: { + ecmaVersion: 2024 + }, + errors: [ + { + messageId: "multipleSpaces", + data: { length: "4" }, + type: "Literal" + } + ] + }, + { + code: "var foo = new RegExp('[[ ] ] ', 'v');", + output: "var foo = new RegExp('[[ ] ] {4}', 'v');", + errors: [ + { + messageId: "multipleSpaces", + data: { length: "4" }, + type: "NewExpression" + } + ] } ] }); diff --git a/tests/lib/rules/no-unneeded-ternary.js b/tests/lib/rules/no-unneeded-ternary.js index 7ad11d2b2e5..3714e70bec8 100644 --- a/tests/lib/rules/no-unneeded-ternary.js +++ b/tests/lib/rules/no-unneeded-ternary.js @@ -10,7 +10,8 @@ //------------------------------------------------------------------------------ const rule = require("../../../lib/rules/no-unneeded-ternary"), - { RuleTester } = require("../../../lib/rule-tester"); + { RuleTester } = require("../../../lib/rule-tester"), + parser = require("../../fixtures/fixture-parser"); //------------------------------------------------------------------------------ // Tests @@ -427,6 +428,29 @@ ruleTester.run("no-unneeded-ternary", rule, { endLine: 1, endColumn: 27 }] + }, + + // https://github.com/eslint/eslint/issues/17173 + { + code: "foo as any ? false : true", + output: "!(foo as any)", + parser: parser("typescript-parsers/unneeded-ternary-1"), + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "unnecessaryConditionalExpression", + type: "ConditionalExpression" + }] + }, + { + code: "foo ? foo : bar as any", + output: "foo || (bar as any)", + options: [{ defaultAssignment: false }], + parser: parser("typescript-parsers/unneeded-ternary-2"), + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "unnecessaryConditionalAssignment", + type: "ConditionalExpression" + }] } ] }); diff --git a/tests/lib/rules/no-unused-labels.js b/tests/lib/rules/no-unused-labels.js index feb6c82d5f0..46b7b1e13e2 100644 --- a/tests/lib/rules/no-unused-labels.js +++ b/tests/lib/rules/no-unused-labels.js @@ -73,6 +73,69 @@ ruleTester.run("no-unused-labels", rule, { code: "A /* comment */: foo", output: null, errors: [{ messageId: "unused" }] + }, + + // https://github.com/eslint/eslint/issues/16988 + { + code: 'A: "use strict"', + output: null, + errors: [{ messageId: "unused" }] + }, + { + code: '"use strict"; foo: "bar"', + output: null, + errors: [{ messageId: "unused" }] + }, + { + code: 'A: ("use strict")', // Parentheses may be removed by another rule. + output: null, + errors: [{ messageId: "unused" }] + }, + { + code: "A: `use strict`", // `use strict` may be changed to "use strict" by another rule. + output: null, + parserOptions: { ecmaVersion: 6 }, + errors: [{ messageId: "unused" }] + }, + { + code: "if (foo) { bar: 'baz' }", + output: "if (foo) { 'baz' }", + errors: [{ messageId: "unused" }] + }, + { + code: "A: B: 'foo'", + output: "B: 'foo'", + errors: [{ messageId: "unused" }, { messageId: "unused" }] + }, + { + code: "A: B: C: 'foo'", + output: "B: C: 'foo'", // Becomes "C: 'foo'" on the second pass. + errors: [{ messageId: "unused" }, { messageId: "unused" }, { messageId: "unused" }] + }, + { + code: "A: B: C: D: 'foo'", + output: "B: D: 'foo'", // Becomes "D: 'foo'" on the second pass. + errors: [ + { messageId: "unused" }, + { messageId: "unused" }, + { messageId: "unused" }, + { messageId: "unused" }] + }, + { + code: "A: B: C: D: E: 'foo'", + output: "B: D: E: 'foo'", // Becomes "E: 'foo'" on the third pass. + errors: [ + { messageId: "unused" }, + { messageId: "unused" }, + { messageId: "unused" }, + { messageId: "unused" }, + { messageId: "unused" } + ] + }, + { + code: "A: 42", + output: "42", + errors: [{ messageId: "unused" }] } /* diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js index 3c1997b8468..2dea2614fd2 100644 --- a/tests/lib/rules/no-unused-vars.js +++ b/tests/lib/rules/no-unused-vars.js @@ -417,6 +417,20 @@ ruleTester.run("no-unused-vars", rule, { { code: "import.meta", parserOptions: { ecmaVersion: 2020, sourceType: "module" } + }, + + // https://github.com/eslint/eslint/issues/17299 + { + code: "var a; a ||= 1;", + parserOptions: { ecmaVersion: 2021 } + }, + { + code: "var a; a &&= 1;", + parserOptions: { ecmaVersion: 2021 } + }, + { + code: "var a; a ??= 1;", + parserOptions: { ecmaVersion: 2021 } } ], invalid: [ diff --git a/tests/lib/rules/no-useless-backreference.js b/tests/lib/rules/no-useless-backreference.js index d51d5bf76e7..3db83c65a4e 100644 --- a/tests/lib/rules/no-useless-backreference.js +++ b/tests/lib/rules/no-useless-backreference.js @@ -142,7 +142,11 @@ ruleTester.run("no-useless-backreference", rule, { String.raw`new RegExp('\\1(a)\\2', 'ug')`, // \1 would be an error, but \2 is syntax error because of the 'u' flag String.raw`const flags = 'gus'; RegExp('\\1(a){', flags);`, // \1 would be an error, but the rule is aware of the 'u' flag so this is a syntax error String.raw`RegExp('\\1(a)\\k', 'u')`, // \1 would be an error, but \k produces syntax error because of the u flag - String.raw`new RegExp('\\k(?a)\\k')` // \k would be an error, but \k produces syntax error because group doesn't exist + String.raw`new RegExp('\\k(?a)\\k')`, // \k would be an error, but \k produces syntax error because group doesn't exist + + // ES2024 + String.raw`new RegExp('([[A--B]])\\1', 'v')`, + String.raw`new RegExp('[[]\\1](a)', 'v')` // SyntaxError ], invalid: [ @@ -508,6 +512,13 @@ ruleTester.run("no-useless-backreference", rule, { { code: String.raw`const r = RegExp, p = '\\1', s = '(a)'; new r(p + s);`, errors: [{ messageId: "forward", data: { bref: String.raw`\1`, group: String.raw`(a)` }, type: "NewExpression" }] + }, + + + // ES2024 + { + code: String.raw`new RegExp('\\1([[A--B]])', 'v')`, + errors: [{ messageId: "forward", data: { bref: String.raw`\1`, group: String.raw`([[A--B]])` }, type: "NewExpression" }] } ] }); diff --git a/tests/lib/rules/no-useless-escape.js b/tests/lib/rules/no-useless-escape.js index 09f146ef8c6..3130d52a0aa 100644 --- a/tests/lib/rules/no-useless-escape.js +++ b/tests/lib/rules/no-useless-escape.js @@ -127,7 +127,65 @@ ruleTester.run("no-useless-escape", rule, { { code: String.raw`var foo = /\p{ASCII}/u`, parserOptions: { ecmaVersion: 2018 } }, { code: String.raw`var foo = /\P{ASCII}/u`, parserOptions: { ecmaVersion: 2018 } }, { code: String.raw`var foo = /[\p{ASCII}]/u`, parserOptions: { ecmaVersion: 2018 } }, - { code: String.raw`var foo = /[\P{ASCII}]/u`, parserOptions: { ecmaVersion: 2018 } } + { code: String.raw`var foo = /[\P{ASCII}]/u`, parserOptions: { ecmaVersion: 2018 } }, + + // Carets + String.raw`/[^^]/`, + { code: String.raw`/[^^]/u`, parserOptions: { ecmaVersion: 2015 } }, + + // ES2024 + { code: String.raw`/[\q{abc}]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\(]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\)]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\{]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\]]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\}]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\/]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\-]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\|]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\$$]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\&&]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\!!]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\##]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\%%]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\**]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\++]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\,,]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\..]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\::]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\;;]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\<<]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\==]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\>>]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\??]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\@@]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: "/[\\``]/v", parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\~~]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[^\^^]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[_\^^]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[$\$]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[&\&]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[!\!]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[#\#]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[%\%]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[*\*]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[+\+]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[,\,]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[.\.]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[:\:]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[;\;]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[<\<]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[=\=]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[>\>]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[?\?]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[@\@]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: "/[`\\`]/v", parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[~\~]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[^^\^]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[_^\^]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\&&&\&]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[[\-]\-]/v`, parserOptions: { ecmaVersion: 2024 } }, + { code: String.raw`/[\^]/v`, parserOptions: { ecmaVersion: 2024 } } ], invalid: [ @@ -1066,6 +1124,717 @@ ruleTester.run("no-useless-escape", rule, { output: "`\\\\a```" }] }] + }, + + // https://github.com/eslint/eslint/issues/16988 + { + code: String.raw`"use\ strict";`, + errors: [{ + line: 1, + column: 5, + endColumn: 6, + message: "Unnecessary escape character: \\ .", + type: "Literal", + suggestions: [{ + messageId: "removeEscapeDoNotKeepSemantics", + output: String.raw`"use strict";` + }, { + messageId: "escapeBackslash", + output: String.raw`"use\\ strict";` + }] + }] + }, + { + code: String.raw`({ foo() { "foo"; "bar"; "ba\z" } })`, + parserOptions: { ecmaVersion: 6 }, + errors: [{ + line: 1, + column: 29, + endColumn: 30, + message: "Unnecessary escape character: \\z.", + type: "Literal", + suggestions: [{ + messageId: "removeEscapeDoNotKeepSemantics", + output: String.raw`({ foo() { "foo"; "bar"; "baz" } })` + }, { + messageId: "escapeBackslash", + output: String.raw`({ foo() { "foo"; "bar"; "ba\\z" } })` + }] + }] + }, + + // Carets + { + code: String.raw`/[^\^]/`, + errors: [{ + line: 1, + column: 4, + message: "Unnecessary escape character: \\^.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: "/[^^]/" + }, + { + messageId: "escapeBackslash", + output: String.raw`/[^\\^]/` + } + ] + }] + }, + { + code: String.raw`/[^\^]/u`, + parserOptions: { ecmaVersion: 2015 }, + errors: [{ + line: 1, + column: 4, + message: "Unnecessary escape character: \\^.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: "/[^^]/u" + }, + { + messageId: "escapeBackslash", + output: String.raw`/[^\\^]/u` + } + ] + }] + }, + + // ES2024 + { + code: String.raw`/[\$]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + endColumn: 4, + message: "Unnecessary escape character: \\$.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: "/[$]/v" + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\$]/v` + } + ] + }] + }, + { + code: String.raw`/[\&\&]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\&.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[&\&]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\&\&]/v` + } + ] + }] + }, + { + code: String.raw`/[\!\!]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\!.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[!\!]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\!\!]/v` + } + ] + }] + }, + { + code: String.raw`/[\#\#]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\#.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[#\#]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\#\#]/v` + } + ] + }] + }, + { + code: String.raw`/[\%\%]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\%.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[%\%]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\%\%]/v` + } + ] + }] + }, + { + code: String.raw`/[\*\*]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\*.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[*\*]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\*\*]/v` + } + ] + }] + }, + { + code: String.raw`/[\+\+]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\+.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[+\+]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\+\+]/v` + } + ] + }] + }, + { + code: String.raw`/[\,\,]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\,.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[,\,]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\,\,]/v` + } + ] + }] + }, + { + code: String.raw`/[\.\.]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[.\.]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\.\.]/v` + } + ] + }] + }, + { + code: String.raw`/[\:\:]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\:.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[:\:]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\:\:]/v` + } + ] + }] + }, + { + code: String.raw`/[\;\;]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\;.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[;\;]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\;\;]/v` + } + ] + }] + }, + { + code: String.raw`/[\<\<]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\<.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[<\<]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\<\<]/v` + } + ] + }] + }, + { + code: String.raw`/[\=\=]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\=.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[=\=]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\=\=]/v` + } + ] + }] + }, + { + code: String.raw`/[\>\>]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\>.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[>\>]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\>\>]/v` + } + ] + }] + }, + { + code: String.raw`/[\?\?]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\?.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[?\?]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\?\?]/v` + } + ] + }] + }, + { + code: String.raw`/[\@\@]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\@.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[@\@]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\@\@]/v` + } + ] + }] + }, + { + code: "/[\\`\\`]/v", + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\`.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: "/[`\\`]/v" + }, + { + messageId: "escapeBackslash", + output: "/[\\\\`\\`]/v" + } + ] + }] + }, + { + code: String.raw`/[\~\~]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\~.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[~\~]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\~\~]/v` + } + ] + }] + }, + { + code: String.raw`/[^\^\^]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 4, + message: "Unnecessary escape character: \\^.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[^^\^]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[^\\^\^]/v` + } + ] + }] + }, + { + code: String.raw`/[_\^\^]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 4, + message: "Unnecessary escape character: \\^.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[_^\^]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[_\\^\^]/v` + } + ] + }] + }, + { + code: String.raw`/[\&\&&\&]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\&.", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[&\&&\&]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[\\&\&&\&]/v` + } + ] + }] + }, + { + code: String.raw`/[\p{ASCII}--\.]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 14, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[\p{ASCII}--.]/v` + } + ] + }] + }, + { + code: String.raw`/[\p{ASCII}&&\.]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 14, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[\p{ASCII}&&.]/v` + } + ] + }] + }, + { + code: String.raw`/[\.--[.&]]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[.--[.&]]/v` + } + ] + }] + }, + { + code: String.raw`/[\.&&[.&]]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[.&&[.&]]/v` + } + ] + }] + }, + { + code: String.raw`/[\.--\.--\.]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[.--\.--\.]/v` + } + ] + }, { + line: 1, + column: 7, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[\.--.--\.]/v` + } + ] + }, { + line: 1, + column: 11, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[\.--\.--.]/v` + } + ] + }] + }, + { + code: String.raw`/[\.&&\.&&\.]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 3, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[.&&\.&&\.]/v` + } + ] + }, { + line: 1, + column: 7, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[\.&&.&&\.]/v` + } + ] + }, { + line: 1, + column: 11, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[\.&&\.&&.]/v` + } + ] + }] + }, + { + code: String.raw`/[[\.&]--[\.&]]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 4, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[[.&]--[\.&]]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[[\\.&]--[\.&]]/v` + } + ] + }, { + line: 1, + column: 11, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[[\.&]--[.&]]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[[\.&]--[\\.&]]/v` + } + ] + }] + }, + { + code: String.raw`/[[\.&]&&[\.&]]/v`, + parserOptions: { ecmaVersion: 2024 }, + errors: [{ + line: 1, + column: 4, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[[.&]&&[\.&]]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[[\\.&]&&[\.&]]/v` + } + ] + }, { + line: 1, + column: 11, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [ + { + messageId: "removeEscape", + output: String.raw`/[[\.&]&&[.&]]/v` + }, + { + messageId: "escapeBackslash", + output: String.raw`/[[\.&]&&[\\.&]]/v` + } + ] + }] } ] }); diff --git a/tests/lib/rules/no-useless-return.js b/tests/lib/rules/no-useless-return.js index 434700cc2d8..5b090dbb365 100644 --- a/tests/lib/rules/no-useless-return.js +++ b/tests/lib/rules/no-useless-return.js @@ -19,7 +19,6 @@ const rule = require("../../../lib/rules/no-useless-return"), const ruleTester = new RuleTester(); ruleTester.run("no-useless-return", rule, { - valid: [ "function foo() { return 5; }", "function foo() { return null; }", @@ -96,6 +95,26 @@ ruleTester.run("no-useless-return", rule, { } } `, + ` + function foo() { + try { + bar(); + return; + } catch (err) {} + baz(); + } + `, + ` + function foo() { + if (something) { + try { + bar(); + return; + } catch (err) {} + } + baz(); + } + `, ` function foo() { return; @@ -176,6 +195,19 @@ ruleTester.run("no-useless-return", rule, { } console.log(arg); } + `, + + // https://github.com/eslint/eslint/pull/16996#discussion_r1138622844 + ` + function foo() { + try { + bar(); + return; + } finally { + baz(); + } + qux(); + } ` ], @@ -386,12 +418,120 @@ ruleTester.run("no-useless-return", rule, { } ` }, - - /* - * FIXME: Re-add this case (removed due to https://github.com/eslint/eslint/issues/7481): - * https://github.com/eslint/eslint/blob/261d7287820253408ec87c344beccdba2fe829a4/tests/lib/rules/no-useless-return.js#L308-L329 - */ - + { + code: ` + function foo() { + try { + foo(); + return; + } catch (err) { + return 5; + } + } + `, + output: ` + function foo() { + try { + foo(); + + } catch (err) { + return 5; + } + } + ` + }, + { + code: ` + function foo() { + if (something) { + try { + bar(); + return; + } catch (err) {} + } + } + `, + output: ` + function foo() { + if (something) { + try { + bar(); + + } catch (err) {} + } + } + ` + }, + { + code: ` + function foo() { + try { + return; + } catch (err) { + foo(); + } + } + `, + output: ` + function foo() { + try { + + } catch (err) { + foo(); + } + } + ` + }, + { + code: ` + function foo() { + try { + return; + } finally { + bar(); + } + } + `, + output: ` + function foo() { + try { + + } finally { + bar(); + } + } + ` + }, + { + code: ` + function foo() { + try { + bar(); + } catch (e) { + try { + baz(); + return; + } catch (e) { + qux(); + } + } + } + `, + output: ` + function foo() { + try { + bar(); + } catch (e) { + try { + baz(); + + } catch (e) { + qux(); + } + } + } + ` + }, { code: ` function foo() { @@ -438,11 +578,21 @@ ruleTester.run("no-useless-return", rule, { { code: "function foo() { return; return; }", output: "function foo() { return; }", - errors: [{ - messageId: "unnecessaryReturn", - type: "ReturnStatement", - column: 18 - }] + errors: [ + { + messageId: "unnecessaryReturn", + type: "ReturnStatement", + column: 18 + } + ] } - ].map(invalidCase => Object.assign({ errors: [{ messageId: "unnecessaryReturn", type: "ReturnStatement" }] }, invalidCase)) + ].map(invalidCase => + Object.assign( + { + errors: [ + { messageId: "unnecessaryReturn", type: "ReturnStatement" } + ] + }, + invalidCase + )) }); diff --git a/tests/lib/rules/prefer-exponentiation-operator.js b/tests/lib/rules/prefer-exponentiation-operator.js index 2de358e2c8f..8765330c00f 100644 --- a/tests/lib/rules/prefer-exponentiation-operator.js +++ b/tests/lib/rules/prefer-exponentiation-operator.js @@ -11,6 +11,7 @@ const rule = require("../../../lib/rules/prefer-exponentiation-operator"); const { RuleTester } = require("../../../lib/rule-tester"); +const parser = require("../../fixtures/fixture-parser"); //------------------------------------------------------------------------------ // Helpers @@ -357,6 +358,41 @@ ruleTester.run("prefer-exponentiation-operator", rule, { invalid("Math?.pow(a, b)", "a**b"), invalid("Math?.pow?.(a, b)", "a**b"), invalid("(Math?.pow)(a, b)", "a**b"), - invalid("(Math?.pow)?.(a, b)", "a**b") + invalid("(Math?.pow)?.(a, b)", "a**b"), + + // https://github.com/eslint/eslint/issues/17173 + { + code: "Math.pow(a, b as any)", + output: "a**(b as any)", + parser: parser("typescript-parsers/exponentiation-with-assertion-1"), + errors: [ + { + messageId: "useExponentiation", + type: "CallExpression" + } + ] + }, + { + code: "Math.pow(a as any, b)", + output: "(a as any)**b", + parser: parser("typescript-parsers/exponentiation-with-assertion-2"), + errors: [ + { + messageId: "useExponentiation", + type: "CallExpression" + } + ] + }, + { + code: "Math.pow(a, b) as any", + output: "(a**b) as any", + parser: parser("typescript-parsers/exponentiation-with-assertion-3"), + errors: [ + { + messageId: "useExponentiation", + type: "CallExpression" + } + ] + } ] }); diff --git a/tests/lib/rules/prefer-named-capture-group.js b/tests/lib/rules/prefer-named-capture-group.js index dad3d7c0290..31dcc70f332 100644 --- a/tests/lib/rules/prefer-named-capture-group.js +++ b/tests/lib/rules/prefer-named-capture-group.js @@ -70,7 +70,17 @@ ruleTester.run("prefer-named-capture-group", rule, { } `, env: { es2020: true } - } + }, + + // ES2024 + "new RegExp('(?[[A--B]])', 'v')", + + /* + * This testcase checks if the rule understands the v flag correctly. + * Without the v flag, `([\q])` is considered a valid regex and the rule reports, + * but if the v flag is understood correctly the rule does not because of a syntax error. + */ + String.raw`new RegExp('([\\q])', 'v')` // SyntaxError ], invalid: [ @@ -591,6 +601,27 @@ ruleTester.run("prefer-named-capture-group", rule, { } ] }] + }, + + // ES2024 + { + code: "new RegExp('([[A--B]])', 'v')", + errors: [{ + messageId: "required", + type: "NewExpression", + data: { group: "([[A--B]])" }, + line: 1, + column: 1, + suggestions: [ + { + messageId: "addGroupName", + output: "new RegExp('(?[[A--B]])', 'v')" + }, + { + messageId: "addNonCapture", + output: "new RegExp('(?:[[A--B]])', 'v')" + }] + }] } ] }); diff --git a/tests/lib/rules/prefer-regex-literals.js b/tests/lib/rules/prefer-regex-literals.js index 054d89be1d7..11f23cac3d5 100644 --- a/tests/lib/rules/prefer-regex-literals.js +++ b/tests/lib/rules/prefer-regex-literals.js @@ -134,7 +134,10 @@ ruleTester.run("prefer-regex-literals", rule, { { code: "class C { #RegExp; foo() { globalThis.#RegExp('a'); } }", env: { es2020: true } - } + }, + + // ES2024 + "new RegExp('[[A--B]]' + a, 'v')" ], invalid: [ @@ -2808,6 +2811,183 @@ ruleTester.run("prefer-regex-literals", rule, { suggestions: null } ] + }, + + // ES2024 + { + code: "new RegExp('[[A--B]]', 'v')", + parserOptions: { ecmaVersion: 2024 }, + errors: [ + { + messageId: "unexpectedRegExp", + suggestions: [ + { + messageId: "replaceWithLiteral", + output: "/[[A--B]]/v" + } + ] + } + ] + }, + { + code: "new RegExp('[[A--B]]', 'v')", + parserOptions: { ecmaVersion: 2023 }, + errors: [ + { + messageId: "unexpectedRegExp", + suggestions: null + } + ] + }, + { + code: "new RegExp('[[A&&&]]', 'v')", + parserOptions: { ecmaVersion: 2024 }, + errors: [ + { + messageId: "unexpectedRegExp", + suggestions: null + } + ] + }, + { + code: "new RegExp('a', 'uv')", + parserOptions: { ecmaVersion: 2024 }, + errors: [ + { + messageId: "unexpectedRegExp", + suggestions: null + } + ] + }, + { + code: "new RegExp(/a/, 'v')", + options: [{ disallowRedundantWrapping: true }], + parserOptions: { ecmaVersion: 2024 }, + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/v", + data: { + flags: "v" + } + } + ] + } + ] + }, + { + code: "new RegExp(/a/, 'v')", + options: [{ disallowRedundantWrapping: true }], + parserOptions: { ecmaVersion: 2023 }, + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + suggestions: null + } + ] + }, + { + code: "new RegExp(/a/g, 'v')", + options: [{ disallowRedundantWrapping: true }], + parserOptions: { ecmaVersion: 2024 }, + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/v", + data: { + flags: "v" + } + }, + { + messageId: "replaceWithIntendedLiteralAndFlags", + output: "/a/gv", + data: { + flags: "gv" + } + } + ] + } + ] + }, + { + code: "new RegExp(/[[A--B]]/v, 'g')", + options: [{ disallowRedundantWrapping: true }], + parserOptions: { ecmaVersion: 2024 }, + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + suggestions: [ + { + messageId: "replaceWithIntendedLiteralAndFlags", + output: "/[[A--B]]/vg", + data: { + flags: "vg" + } + } + + // suggestion with flags `g` would be invalid + ] + } + ] + }, + { + code: "new RegExp(/a/u, 'v')", + options: [{ disallowRedundantWrapping: true }], + parserOptions: { ecmaVersion: 2024 }, + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/v", + data: { + flags: "v" + } + } + + // suggestion with merged flags `uv` would be invalid + ] + } + ] + }, + { + code: "new RegExp(/a/v, 'u')", + options: [{ disallowRedundantWrapping: true }], + parserOptions: { ecmaVersion: 2024 }, + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + suggestions: [ + { + messageId: "replaceWithLiteralAndFlags", + output: "/a/u", + data: { + flags: "u" + } + } + + // suggestion with merged flags `vu` would be invalid + ] + } + ] + }, + { + code: "new RegExp(/[[A--B]]/v, 'u')", + options: [{ disallowRedundantWrapping: true }], + parserOptions: { ecmaVersion: 2024 }, + errors: [ + { + messageId: "unexpectedRedundantRegExpWithFlags", + suggestions: null + } + ] } ] }); diff --git a/tests/lib/rules/quotes.js b/tests/lib/rules/quotes.js index d1c42b73e84..11ae90248b7 100644 --- a/tests/lib/rules/quotes.js +++ b/tests/lib/rules/quotes.js @@ -440,7 +440,7 @@ ruleTester.run("quotes", rule, { }, { code: "() => { foo(); `use strict`; }", - output: "() => { foo(); \"use strict\"; }", + output: null, // no autofix parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "wrongQuotes", @@ -450,7 +450,7 @@ ruleTester.run("quotes", rule, { }, { code: "foo(); `use strict`;", - output: "foo(); \"use strict\";", + output: null, // no autofix parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "wrongQuotes", @@ -725,6 +725,62 @@ ruleTester.run("quotes", rule, { type: "Literal" } ] + }, + + // https://github.com/eslint/eslint/pull/17022 + { + code: "() => { foo(); (`use strict`); }", + output: "() => { foo(); (\"use strict\"); }", + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "wrongQuotes", + data: { description: "doublequote" }, + type: "TemplateLiteral" + }] + }, + { + code: "('foo'); \"bar\";", + output: "(`foo`); `bar`;", + options: ["backtick"], + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "wrongQuotes", + data: { description: "backtick" }, + type: "Literal" + }, { + messageId: "wrongQuotes", + data: { description: "backtick" }, + type: "Literal" + }] + }, + { + code: "; 'use asm';", + output: "; \"use asm\";", + errors: [{ + messageId: "wrongQuotes", + data: { description: "doublequote" }, + type: "Literal" + }] + }, + { + code: "{ `foobar`; }", + output: "{ \"foobar\"; }", + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "wrongQuotes", + data: { description: "doublequote" }, + type: "TemplateLiteral" + }] + }, + { + code: "foo(() => `bar`);", + output: "foo(() => \"bar\");", + parserOptions: { ecmaVersion: 6 }, + errors: [{ + messageId: "wrongQuotes", + data: { description: "doublequote" }, + type: "TemplateLiteral" + }] } ] }); diff --git a/tests/lib/rules/require-unicode-regexp.js b/tests/lib/rules/require-unicode-regexp.js index a75f6863168..eab11b70fed 100644 --- a/tests/lib/rules/require-unicode-regexp.js +++ b/tests/lib/rules/require-unicode-regexp.js @@ -45,7 +45,20 @@ ruleTester.run("require-unicode-regexp", rule, { { code: "const flags = 'u'; new globalThis.RegExp('', flags)", env: { es2020: true } }, { code: "const flags = 'g'; new globalThis.RegExp('', flags + 'u')", env: { es2020: true } }, { code: "const flags = 'gimu'; new globalThis.RegExp('foo', flags[3])", env: { es2020: true } }, - { code: "class C { #RegExp; foo() { new globalThis.#RegExp('foo') } }", parserOptions: { ecmaVersion: 2022 }, env: { es2020: true } } + { code: "class C { #RegExp; foo() { new globalThis.#RegExp('foo') } }", parserOptions: { ecmaVersion: 2022 }, env: { es2020: true } }, + + // for v flag + { code: "/foo/v", parserOptions: { ecmaVersion: 2024 } }, + { code: "/foo/gimvy", parserOptions: { ecmaVersion: 2024 } }, + { code: "RegExp('', 'v')", parserOptions: { ecmaVersion: 2024 } }, + { code: "RegExp('', `v`)", parserOptions: { ecmaVersion: 2024 } }, + { code: "new RegExp('', 'v')", parserOptions: { ecmaVersion: 2024 } }, + { code: "RegExp('', 'gimvy')", parserOptions: { ecmaVersion: 2024 } }, + { code: "RegExp('', `gimvy`)", parserOptions: { ecmaVersion: 2024 } }, + { code: "new RegExp('', 'gimvy')", parserOptions: { ecmaVersion: 2024 } }, + { code: "const flags = 'v'; new RegExp('', flags)", parserOptions: { ecmaVersion: 2024 } }, + { code: "const flags = 'g'; new RegExp('', flags + 'v')", parserOptions: { ecmaVersion: 2024 } }, + { code: "const flags = 'gimv'; new RegExp('foo', flags[3])", parserOptions: { ecmaVersion: 2024 } } ], invalid: [ { diff --git a/tests/lib/rules/utils/ast-utils.js b/tests/lib/rules/utils/ast-utils.js index c2a9201ac73..bc03c7e47cd 100644 --- a/tests/lib/rules/utils/ast-utils.js +++ b/tests/lib/rules/utils/ast-utils.js @@ -1802,4 +1802,122 @@ describe("ast-utils", () => { }); }); }); + + describe("isTopLevelExpressionStatement", () => { + it("should return false for a Program node", () => { + const node = { type: "Program", parent: null }; + + assert.strictEqual(astUtils.isTopLevelExpressionStatement(node), false); + }); + + it("should return false if the node is not an ExpressionStatement", () => { + linter.defineRule("checker", { + create: mustCall(() => ({ + ":expression": mustCall(node => { + assert.strictEqual(astUtils.isTopLevelExpressionStatement(node), false); + }) + })) + }); + + linter.verify("var foo = () => \"use strict\";", { rules: { checker: "error" }, parserOptions: { ecmaVersion: 2022 } }); + }); + + const expectedResults = [ + ["if (foo) { \"use strict\"; }", "\"use strict\";", false], + ["{ \"use strict\"; }", "\"use strict\";", false], + ["switch (foo) { case bar: \"use strict\"; }", "\"use strict\";", false], + ["foo; bar;", "foo;", true], + ["foo; bar;", "bar;", true], + ["function foo() { bar; }", "bar;", true], + ["var foo = function () { foo(); };", "foo();", true], + ["var foo = () => { 'bar'; }", "'bar';", true], + ["\"use strict\"", "\"use strict\"", true], + ["(`use strict`)", "(`use strict`)", true] + ]; + + expectedResults.forEach(([code, nodeText, expectedRetVal]) => { + it(`should return ${expectedRetVal} for \`${nodeText}\` in \`${code}\``, () => { + linter.defineRule("checker", { + create: mustCall(context => { + const assertForNode = mustCall( + node => assert.strictEqual(astUtils.isTopLevelExpressionStatement(node), expectedRetVal) + ); + + return ({ + ExpressionStatement(node) { + if (context.sourceCode.getText(node) === nodeText) { + assertForNode(node); + } + } + }); + }) + }); + + linter.verify(code, { rules: { checker: "error" }, parserOptions: { ecmaVersion: 2022 } }); + }); + }); + }); + + describe("isStaticTemplateLiteral", () => { + const expectedResults = { + "``": true, + "`foo`": true, + "`foo${bar}`": false, + "\"foo\"": false, + "foo`bar`": false + }; + + Object.entries(expectedResults).forEach(([code, expectedResult]) => { + it(`returns ${expectedResult} for ${code}`, () => { + const ast = espree.parse(code, { ecmaVersion: 6 }); + + assert.strictEqual(astUtils.isStaticTemplateLiteral(ast.body[0].expression), expectedResult); + }); + }); + }); + + describe("isDirective", () => { + const expectedResults = [ + { code: '"use strict";', expectedRetVal: true }, + { code: '"use strict"; "use asm";', nodeText: '"use asm";', expectedRetVal: true }, + { code: 'const a = () => { "foo"; }', nodeText: '"foo";', expectedRetVal: true }, + { code: '"";', expectedRetVal: true }, + { code: '{ "foo"; }', nodeText: '"foo";', expectedRetVal: false }, + { code: "foo();", expectedRetVal: false }, + { code: '"foo" + "bar";', expectedRetVal: false }, + { code: "12345;", expectedRetVal: false }, + { code: "`foo`;", expectedRetVal: false }, + { code: "('foo');", expectedRetVal: false }, + { code: 'foo(); "use strict";', nodeText: '"use strict";', expectedRetVal: false } + ]; + + expectedResults.forEach(({ code, nodeText = code, expectedRetVal }) => { + it(`should return ${expectedRetVal} for \`${nodeText}\` in \`${code}\``, () => { + linter.defineRule("checker", { + create: mustCall(({ sourceCode }) => { + const assertForNode = mustCall( + node => assert.strictEqual(astUtils.isDirective(node), expectedRetVal) + ); + + return ({ + ExpressionStatement(node) { + if (sourceCode.getText(node) === nodeText) { + assertForNode(node); + + if (!expectedRetVal) { + + // The flow parser sets `directive` to null on non-directive ExpressionStatement nodes. + node.directive = null; + assertForNode(node); + } + } + } + }); + }) + }); + + linter.verify(code, { rules: { checker: "error" }, parserOptions: { ecmaVersion: 2022 } }); + }); + }); + }); }); diff --git a/tests/lib/source-code/source-code.js b/tests/lib/source-code/source-code.js index dae9bcf4c3d..763db27bc46 100644 --- a/tests/lib/source-code/source-code.js +++ b/tests/lib/source-code/source-code.js @@ -12,6 +12,7 @@ const fs = require("fs"), path = require("path"), assert = require("chai").assert, espree = require("espree"), + eslintScope = require("eslint-scope"), sinon = require("sinon"), { Linter } = require("../../../lib/linter"), SourceCode = require("../../../lib/source-code/source-code"), @@ -3787,4 +3788,257 @@ describe("SourceCode", () => { }); }); + + describe("getInlineConfigNodes()", () => { + + it("should return inline config comments", () => { + + const code = "/*eslint foo: 1*/ foo; /* non-config comment*/ /* eslint-disable bar */ bar; /* eslint-enable bar */"; + const ast = espree.parse(code, DEFAULT_CONFIG); + const sourceCode = new SourceCode(code, ast); + const configComments = sourceCode.getInlineConfigNodes(); + + // not sure why but without the JSON parse/stringify Chai won't see these as equal + assert.deepStrictEqual(JSON.parse(JSON.stringify(configComments)), [ + { + type: "Block", + value: "eslint foo: 1", + start: 0, + end: 17, + range: [ + 0, + 17 + ], + loc: { + start: { + line: 1, + column: 0 + }, + end: { + line: 1, + column: 17 + } + } + }, + { + type: "Block", + value: " eslint-disable bar ", + start: 47, + end: 71, + range: [ + 47, + 71 + ], + loc: { + start: { + line: 1, + column: 47 + }, + end: { + line: 1, + column: 71 + } + } + }, + { + type: "Block", + value: " eslint-enable bar ", + start: 77, + end: 100, + range: [ + 77, + 100 + ], + loc: { + start: { + line: 1, + column: 77 + }, + end: { + line: 1, + column: 100 + } + } + } + ]); + + }); + + }); + + describe("applyLanguageOptions()", () => { + + it("should add ES6 globals", () => { + + const code = "foo"; + const ast = espree.parse(code, DEFAULT_CONFIG); + const scopeManager = eslintScope.analyze(ast, { + ignoreEval: true, + ecmaVersion: 6 + }); + const sourceCode = new SourceCode({ text: code, ast, scopeManager }); + + sourceCode.applyLanguageOptions({ + ecmaVersion: 2015 + }); + + sourceCode.finalize(); + + const globalScope = sourceCode.scopeManager.scopes[0]; + const variable = globalScope.set.get("Promise"); + + assert.isDefined(variable); + + }); + + it("should add custom globals", () => { + + const code = "foo"; + const ast = espree.parse(code, DEFAULT_CONFIG); + const scopeManager = eslintScope.analyze(ast, { + ignoreEval: true, + ecmaVersion: 6 + }); + const sourceCode = new SourceCode({ text: code, ast, scopeManager }); + + sourceCode.applyLanguageOptions({ + ecmaVersion: 2015, + globals: { + FOO: true + } + }); + + sourceCode.finalize(); + + const globalScope = sourceCode.scopeManager.scopes[0]; + const variable = globalScope.set.get("FOO"); + + assert.isDefined(variable); + assert.isTrue(variable.writeable); + }); + + it("should add commonjs globals", () => { + + const code = "foo"; + const ast = espree.parse(code, DEFAULT_CONFIG); + const scopeManager = eslintScope.analyze(ast, { + ignoreEval: true, + nodejsScope: true, + ecmaVersion: 6 + }); + const sourceCode = new SourceCode({ text: code, ast, scopeManager }); + + sourceCode.applyLanguageOptions({ + ecmaVersion: 2015, + sourceType: "commonjs" + }); + + sourceCode.finalize(); + + const globalScope = sourceCode.scopeManager.scopes[0]; + const variable = globalScope.set.get("require"); + + assert.isDefined(variable); + + }); + + }); + + describe("applyInlineConfig()", () => { + + it("should add inline globals", () => { + + const code = "/*global bar: true */ foo"; + const ast = espree.parse(code, DEFAULT_CONFIG); + const scopeManager = eslintScope.analyze(ast, { + ignoreEval: true, + ecmaVersion: 6 + }); + const sourceCode = new SourceCode({ text: code, ast, scopeManager }); + + sourceCode.applyInlineConfig(); + sourceCode.finalize(); + + const globalScope = sourceCode.scopeManager.scopes[0]; + const variable = globalScope.set.get("bar"); + + assert.isDefined(variable); + assert.isTrue(variable.writeable); + }); + + + it("should mark exported variables", () => { + + const code = "/*exported foo */ var foo;"; + const ast = espree.parse(code, DEFAULT_CONFIG); + const scopeManager = eslintScope.analyze(ast, { + ignoreEval: true, + ecmaVersion: 6 + }); + const sourceCode = new SourceCode({ text: code, ast, scopeManager }); + + sourceCode.applyInlineConfig(); + sourceCode.finalize(); + + const globalScope = sourceCode.scopeManager.scopes[0]; + const variable = globalScope.set.get("foo"); + + assert.isDefined(variable); + assert.isTrue(variable.eslintUsed); + assert.isTrue(variable.eslintExported); + }); + + it("should extract rule configuration", () => { + + const code = "/*eslint some-rule: 2 */ var foo;"; + const ast = espree.parse(code, DEFAULT_CONFIG); + const sourceCode = new SourceCode(code, ast); + const result = sourceCode.applyInlineConfig(); + + assert.strictEqual(result.configs.length, 1); + assert.strictEqual(result.configs[0].config.rules["some-rule"], 2); + }); + + it("should extract multiple rule configurations", () => { + + const code = "/*eslint some-rule: 2, other-rule: [\"error\", { skip: true }] */ var foo;"; + const ast = espree.parse(code, DEFAULT_CONFIG); + const sourceCode = new SourceCode(code, ast); + const result = sourceCode.applyInlineConfig(); + + assert.strictEqual(result.configs.length, 1); + assert.strictEqual(result.configs[0].config.rules["some-rule"], 2); + assert.deepStrictEqual(result.configs[0].config.rules["other-rule"], ["error", { skip: true }]); + }); + + it("should extract multiple comments into multiple configurations", () => { + + const code = "/*eslint some-rule: 2*/ /*eslint other-rule: [\"error\", { skip: true }] */ var foo;"; + const ast = espree.parse(code, DEFAULT_CONFIG); + const sourceCode = new SourceCode(code, ast); + const result = sourceCode.applyInlineConfig(); + + assert.strictEqual(result.configs.length, 2); + assert.strictEqual(result.configs[0].config.rules["some-rule"], 2); + assert.deepStrictEqual(result.configs[1].config.rules["other-rule"], ["error", { skip: true }]); + }); + + it("should report problem with rule configuration parsing", () => { + + const code = "/*eslint some-rule::, */ var foo;"; + const ast = espree.parse(code, DEFAULT_CONFIG); + const sourceCode = new SourceCode(code, ast); + const result = sourceCode.applyInlineConfig(); + const problem = result.problems[0]; + + // Node.js 19 changes the JSON parsing error format, so we need to check each field separately to use a regex + assert.strictEqual(problem.column, 1); + assert.strictEqual(problem.line, 1); + assert.isTrue(problem.fatal); + assert.match(problem.message, /Failed to parse JSON from ' "some-rule"::,': Unexpected token '?:'?/u); + assert.isNull(problem.nodeType); + assert.isNull(problem.ruleId); + assert.strictEqual(problem.severity, 2); + }); + }); }); diff --git a/tests/lib/unsupported-api.js b/tests/lib/unsupported-api.js index 3a65ba230f8..4ad46c3d217 100644 --- a/tests/lib/unsupported-api.js +++ b/tests/lib/unsupported-api.js @@ -27,6 +27,14 @@ describe("unsupported-api", () => { assert.isFunction(api.FlatESLint); }); + it("should have LegacyESLint exposed", () => { + assert.isFunction(api.LegacyESLint); + }); + + it("should not have ESLint exposed", () => { + assert.isUndefined(api.ESLint); + }); + it("should have shouldUseFlatConfig exposed", () => { assert.isFunction(api.shouldUseFlatConfig); }); diff --git a/tools/rule-types.json b/tools/rule-types.json index f3fe8f80cd1..35895f3838a 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -162,6 +162,7 @@ "no-new-wrappers": "suggestion", "no-nonoctal-decimal-escape": "suggestion", "no-obj-calls": "problem", + "no-object-constructor": "suggestion", "no-octal": "suggestion", "no-octal-escape": "suggestion", "no-param-reassign": "suggestion", diff --git a/wdio.conf.js b/wdio.conf.js new file mode 100644 index 00000000000..f32d757133e --- /dev/null +++ b/wdio.conf.js @@ -0,0 +1,387 @@ +"use strict"; + +const path = require("path"); +const commonjs = require("vite-plugin-commonjs").default; + +exports.config = { + + /* + * + * ==================== + * Runner Configuration + * ==================== + * WebdriverIO supports running e2e tests as well as unit and component tests. + */ + runner: ["browser", { + viteConfig: { + resolve: { + alias: { + util: "rollup-plugin-node-polyfills/polyfills/util", + path: "rollup-plugin-node-polyfills/polyfills/path", + assert: "rollup-plugin-node-polyfills/polyfills/assert" + } + }, + plugins: [ + commonjs(), + { + name: "wdio:import-fix", + enforce: "pre", + transform(source, id) { + if (!id.endsWith("/tests/lib/linter/linter.js")) { + return source; + } + + return source.replace( + 'const { Linter } = require("../../../lib/linter");', + 'const { Linter } = require("../../../build/eslint");\n' + + 'process.cwd = () => "/";' + ); + } + } + ] + } + }], + + /* + * + * ================== + * Specify Test Files + * ================== + * Define which test specs should run. The pattern is relative to the directory + * of the configuration file being run. + * + * The specs are defined as an array of spec files (optionally using wildcards + * that will be expanded). The test for each spec file will be run in a separate + * worker process. In order to have a group of spec files run in the same worker + * process simply enclose them in an array within the specs array. + * + * If you are calling `wdio` from an NPM script (see https://docs.npmjs.com/cli/run-script), + * then the current working directory is where your `package.json` resides, so `wdio` + * will be called from there. + * + */ + specs: [ + path.join(__dirname, "tests", "lib", "linter", "linter.js") + ], + + // Patterns to exclude. + exclude: [], + + /* + * + * ============ + * Capabilities + * ============ + * Define your capabilities here. WebdriverIO can run multiple capabilities at the same + * time. Depending on the number of capabilities, WebdriverIO launches several test + * sessions. Within your capabilities you can overwrite the spec and exclude options in + * order to group specific specs to a specific capability. + * + * First, you can define how many instances should be started at the same time. Let"s + * say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have + * set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec + * files and you set maxInstances to 10, all spec files will get tested at the same time + * and 30 processes will get spawned. The property handles how many capabilities + * from the same test should run tests. + * + */ + maxInstances: 10, + + /* + * + * If you have trouble getting all important capabilities together, check out the + * Sauce Labs platform configurator - a great tool to configure your capabilities: + * https://saucelabs.com/platform/platform-configurator + * + */ + capabilities: [{ + browserName: "chrome", + "goog:chromeOptions": { + args: process.env.CI ? ["headless", "disable-gpu"] : [] + } + }], + + /* + * + * =================== + * Test Configurations + * =================== + * Define all options that are relevant for the WebdriverIO instance here + * + * Level of logging verbosity: trace | debug | info | warn | error | silent + */ + logLevel: "trace", + outputDir: "./wdio-logs", + + /* + * + * Set specific log levels per logger + * loggers: + * - webdriver, webdriverio + * - @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service + * - @wdio/mocha-framework, @wdio/jasmine-framework + * - @wdio/local-runner + * - @wdio/sumologic-reporter + * - @wdio/cli, @wdio/config, @wdio/utils + * Level of logging verbosity: trace | debug | info | warn | error | silent + * logLevels: { + * webdriver: 'info', + * '@wdio/appium-service': 'info' + * }, + * + * If you only want to run your tests until a specific amount of tests have failed use + * bail (default is 0 - don't bail, run all tests). + */ + bail: 0, + + /* + * + * Set a base URL in order to shorten url command calls. If your `url` parameter starts + * with `/`, the base url gets prepended, not including the path portion of your baseUrl. + * If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url + * gets prepended directly. + */ + baseUrl: "", + + /* + * + * Default timeout for all waitFor* commands. + */ + waitforTimeout: 10000, + + /* + * + * Default timeout in milliseconds for request + * if browser driver or grid doesn't send response + */ + connectionRetryTimeout: 120000, + + /* + * + * Default request retries count + */ + connectionRetryCount: 3, + + /* + * Framework you want to run your specs with. + * The following are supported: Mocha, Jasmine, and Cucumber + * see also: https://webdriver.io/docs/frameworks + * + * Make sure you have the wdio adapter package for the specific framework installed + * before running any tests. + */ + framework: "mocha", + + /* + * + * The number of times to retry the entire specfile when it fails as a whole + * specFileRetries: 1, + * + * Delay in seconds between the spec file retry attempts + * specFileRetriesDelay: 0, + * + * Whether or not retried specfiles should be retried immediately or deferred to the end of the queue + * specFileRetriesDeferred: false, + * + * Test reporter for stdout. + * The only one supported by default is 'dot' + * see also: https://webdriver.io/docs/dot-reporter + */ + reporters: ["concise"], + + /* + * + * Options to be passed to Mocha. + * See the full list at http://mochajs.org/ + */ + mochaOpts: { + ui: "bdd", + timeout: 5 * 60 * 1000, // 5min + grep: "@skipWeb", + invert: true + } + + /* + * + * ===== + * Hooks + * ===== + * WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance + * it and to build services around it. You can either apply a single function or an array of + * methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got + * resolved to continue. + */ + /** + * Gets executed once before all workers get launched. + * @param {Object} config wdio configuration object + * @param {Array} capabilities list of capabilities details + */ + /* + * onPrepare: function (config, capabilities) { + * }, + */ + /** + * Gets executed before a worker process is spawned and can be used to initialise specific service + * for that worker as well as modify runtime environments in an async fashion. + * @param {string} cid capability id (e.g 0-0) + * @param {Object} caps object containing capabilities for session that will be spawn in the worker + * @param {Object} specs specs to be run in the worker process + * @param {Object} args object that will be merged with the main configuration once worker is initialized + * @param {Object} execArgv list of string arguments passed to the worker process + */ + /* + * onWorkerStart: function (cid, caps, specs, args, execArgv) { + * }, + */ + /** + * Gets executed just after a worker process has exited. + * @param {string} cid capability id (e.g 0-0) + * @param {number} exitCode 0 - success, 1 - fail + * @param {Object} specs specs to be run in the worker process + * @param {number} retries number of retries used + */ + /* + * onWorkerEnd: function (cid, exitCode, specs, retries) { + * }, + */ + /** + * Gets executed just before initialising the webdriver session and test framework. It allows you + * to manipulate configurations depending on the capability or spec. + * @param {Object} config wdio configuration object + * @param {Array} capabilities list of capabilities details + * @param {Array} specs List of spec file paths that are to be run + * @param {string} cid worker id (e.g. 0-0) + */ + /* + * beforeSession: function (config, capabilities, specs, cid) { + * }, + */ + /** + * Gets executed before test execution begins. At this point you can access to all global + * variables like `browser`. It is the perfect place to define custom commands. + * @param {Array} capabilities list of capabilities details + * @param {Array} specs List of spec file paths that are to be run + * @param {Object} browser instance of created browser/device session + */ + /* + * before: function (capabilities, specs) { + * }, + */ + /** + * Runs before a WebdriverIO command gets executed. + * @param {string} commandName hook command name + * @param {Array} args arguments that command would receive + */ + /* + * beforeCommand: function (commandName, args) { + * }, + */ + /** + * Hook that gets executed before the suite starts + * @param {Object} suite suite details + */ + /* + * beforeSuite: function (suite) { + * }, + */ + /** + * Function to be executed before a test (in Mocha/Jasmine) starts. + */ + /* + * beforeTest: function (test, context) { + * }, + */ + /** + * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling + * beforeEach in Mocha) + */ + /* + * beforeHook: function (test, context) { + * }, + */ + /** + * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling + * afterEach in Mocha) + */ + /* + * afterHook: function (test, context, { error, result, duration, passed, retries }) { + * }, + */ + /** + * Function to be executed after a test (in Mocha/Jasmine only) + * @param {Object} test test object + * @param {Object} context scope object the test was executed with + * @param {Error} result.error error object in case the test fails, otherwise `undefined` + * @param {any} result.result return object of test function + * @param {number} result.duration duration of test + * @param {boolean} result.passed true if test has passed, otherwise false + * @param {Object} result.retries informations to spec related retries, e.g. `{ attempts: 0, limit: 0 }` + */ + /* + * afterTest: function(test, context, { error, result, duration, passed, retries }) { + * }, + */ + + + /** + * Hook that gets executed after the suite has ended + * @param {Object} suite suite details + */ + /* + * afterSuite: function (suite) { + * }, + */ + /** + * Runs after a WebdriverIO command gets executed + * @param {string} commandName hook command name + * @param {Array} args arguments that command would receive + * @param {number} result 0 - command success, 1 - command error + * @param {Object} error error object if any + */ + /* + * afterCommand: function (commandName, args, result, error) { + * }, + */ + /** + * Gets executed after all tests are done. You still have access to all global variables from + * the test. + * @param {number} result 0 - test pass, 1 - test fail + * @param {Array} capabilities list of capabilities details + * @param {Array} specs List of spec file paths that ran + */ + /* + * after: function (result, capabilities, specs) { + * }, + */ + /** + * Gets executed right after terminating the webdriver session. + * @param {Object} config wdio configuration object + * @param {Array} capabilities list of capabilities details + * @param {Array} specs List of spec file paths that ran + */ + /* + * afterSession: function (config, capabilities, specs) { + * }, + */ + /** + * Gets executed after all workers got shut down and the process is about to exit. An error + * thrown in the onComplete hook will result in the test run failing. + * @param {Object} exitCode 0 - success, 1 - fail + * @param {Object} config wdio configuration object + * @param {Array} capabilities list of capabilities details + * @param {Object} results object containing test results + */ + /* + * onComplete: function(exitCode, config, capabilities, results) { + * }, + */ + /** + * Gets executed when a refresh happens. + * @param {string} oldSessionId session ID of the old session + * @param {string} newSessionId session ID of the new session + */ + /* + * onReload: function(oldSessionId, newSessionId) { + * } + */ +};