From 395706420764e0bba6cb047d3ccac6c8f779d50b Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Fri, 26 Apr 2024 13:33:26 -0700 Subject: [PATCH] feat: @eslint/compat package closes #4 --- .github/ISSUE_TEMPLATE/bug-report.yml | 4 + .github/ISSUE_TEMPLATE/change.yml | 4 + .github/workflows/release-please.yml | 39 ++ .release-please-manifest.json | 3 +- packages/compat/CHANGELOG.md | 0 packages/compat/LICENSE | 201 +++++++ packages/compat/README.md | 146 +++++ packages/compat/jsr.json | 10 + packages/compat/package.json | 55 ++ packages/compat/rollup.config.js | 23 + packages/compat/src/fixup-rules.js | 256 +++++++++ packages/compat/src/index.js | 5 + packages/compat/src/types.ts | 36 ++ .../tests/fixtures/rules/consistent-this.js | 170 ++++++ .../tests/fixtures/rules/global-require.js | 94 +++ .../fixtures/rules/handle-callback-err.js | 100 ++++ .../tests/fixtures/rules/no-lone-blocks.js | 137 +++++ .../tests/fixtures/rules/no-loop-func.js | 207 +++++++ .../fixtures/rules/prefer-rest-params.js | 114 ++++ .../fixtures/rules/require-atomic-updates.js | 359 ++++++++++++ packages/compat/tests/fixup-rules.js | 496 ++++++++++++++++ .../compat/tests/rules/consistent-this.js | 189 ++++++ packages/compat/tests/rules/global-require.js | 98 ++++ .../compat/tests/rules/handle-callback-err.js | 176 ++++++ packages/compat/tests/rules/no-lone-blocks.js | 540 ++++++++++++++++++ packages/compat/tests/rules/no-loop-func.js | 411 +++++++++++++ .../compat/tests/rules/prefer-rest-params.js | 53 ++ .../tests/rules/require-atomic-updates.js | 480 ++++++++++++++++ packages/compat/tsconfig.cjs.json | 9 + packages/compat/tsconfig.esm.json | 4 + packages/compat/tsconfig.json | 13 + release-please-config.json | 10 + 32 files changed, 4441 insertions(+), 1 deletion(-) create mode 100644 packages/compat/CHANGELOG.md create mode 100644 packages/compat/LICENSE create mode 100644 packages/compat/README.md create mode 100644 packages/compat/jsr.json create mode 100644 packages/compat/package.json create mode 100644 packages/compat/rollup.config.js create mode 100644 packages/compat/src/fixup-rules.js create mode 100644 packages/compat/src/index.js create mode 100644 packages/compat/src/types.ts create mode 100644 packages/compat/tests/fixtures/rules/consistent-this.js create mode 100644 packages/compat/tests/fixtures/rules/global-require.js create mode 100644 packages/compat/tests/fixtures/rules/handle-callback-err.js create mode 100644 packages/compat/tests/fixtures/rules/no-lone-blocks.js create mode 100644 packages/compat/tests/fixtures/rules/no-loop-func.js create mode 100644 packages/compat/tests/fixtures/rules/prefer-rest-params.js create mode 100644 packages/compat/tests/fixtures/rules/require-atomic-updates.js create mode 100644 packages/compat/tests/fixup-rules.js create mode 100644 packages/compat/tests/rules/consistent-this.js create mode 100644 packages/compat/tests/rules/global-require.js create mode 100644 packages/compat/tests/rules/handle-callback-err.js create mode 100644 packages/compat/tests/rules/no-lone-blocks.js create mode 100644 packages/compat/tests/rules/no-loop-func.js create mode 100644 packages/compat/tests/rules/prefer-rest-params.js create mode 100644 packages/compat/tests/rules/require-atomic-updates.js create mode 100644 packages/compat/tsconfig.cjs.json create mode 100644 packages/compat/tsconfig.esm.json create mode 100644 packages/compat/tsconfig.json diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 5afcc4d..d4a1947 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -9,6 +9,10 @@ body: attributes: label: Which packages are affected? options: + - label: "`@eslint/compat`" + required: false + - label: "`@eslint/config-array`" + required: false - label: "`@eslint/object-schema`" required: false - type: textarea diff --git a/.github/ISSUE_TEMPLATE/change.yml b/.github/ISSUE_TEMPLATE/change.yml index 0c1a17b..da8f045 100644 --- a/.github/ISSUE_TEMPLATE/change.yml +++ b/.github/ISSUE_TEMPLATE/change.yml @@ -8,6 +8,10 @@ body: attributes: label: Which packages would you like to change? options: + - label: "`@eslint/compat`" + required: false + - label: "`@eslint/config-array`" + required: false - label: "`@eslint/object-schema`" required: false - type: textarea diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 36485cf..205c35d 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -38,6 +38,45 @@ jobs: if: ${{ steps.release.outputs.releases_created }} #----------------------------------------------------------------------------- + # NOTE: Packages are released in order of dependency. The packages with the + # fewest internal dependencies are released first and the packages with the + # most internal dependencies are released last. + #----------------------------------------------------------------------------- + + #----------------------------------------------------------------------------- + # @eslint/compat + #----------------------------------------------------------------------------- + + - name: Publish @eslint/compat package to npm + run: npm publish -w packages/compat + if: ${{ steps.release.outputs['packages/compat--release_created'] }} + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + + - name: Publish @eslint/compat package to JSR + run: | + npm run build --if-present + npx jsr publish + working-directory: packages/compat + if: ${{ steps.release.outputs['packages/compat--release_created'] }} + + - name: Tweet Release Announcement + run: npx @humanwhocodes/tweet "@eslint/compat v${{ steps.release.outputs['packages/compat--major'] }}.${{ steps.release.outputs['packages/compat--minor'] }}.${{ steps.release.outputs['packages/compat--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/v${{ steps.release.outputs['packages/compat--tag_name'] }}" + if: ${{ steps.release.outputs['packages/compat--release_created'] }} + env: + TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} + TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} + TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} + + - name: Toot Release Announcement + run: npx @humanwhocodes/toot "@eslint/compat v${{ steps.release.outputs['packages/compat--major'] }}.${{ steps.release.outputs['packages/compat--minor'] }}.${{ steps.release.outputs['packages/compat--patch'] }} has been released!\n\n${{ github.event.repository.html_url }}/releases/tag/v${{ steps.release.outputs['packages/compat--tag_name'] }}"' + if: ${{ steps.release.outputs['packages/compat--release_created'] }} + env: + MASTODON_ACCESS_TOKEN: ${{ secrets.MASTODON_ACCESS_TOKEN }} + MASTODON_HOST: ${{ secrets.MASTODON_HOST }} + + #----------------------------------------------------------------------------- # @eslint/object-schema #----------------------------------------------------------------------------- diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 431d353..7114c6f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,4 +1,5 @@ { "packages/object-schema": "2.0.3", - "packages/config-array": "0.13.0" + "packages/config-array": "0.13.0", + "packages/compat": "0.0.0" } diff --git a/packages/compat/CHANGELOG.md b/packages/compat/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/compat/LICENSE b/packages/compat/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/packages/compat/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/compat/README.md b/packages/compat/README.md new file mode 100644 index 0000000..761bdb4 --- /dev/null +++ b/packages/compat/README.md @@ -0,0 +1,146 @@ +# ESLint Compatibility Utilities + + +## Overview + +This packages contains functions that allow you to wrap existing ESLint rules, plugins, and configurations that were intended for use with ESLint v8.x to allow them to work as-is in ESLint v9.x. + +## Installation + +For Node.js and compatible runtimes: + +```shell +npm install @eslint/compat -D +# or +yarn add @eslint/compat -D +# or +pnpm install @eslint/compat -D +# or +bun install @eslint/compat -D +``` + +For Deno: + +```shell +deno add @eslint/compat +``` + +## Usage + +This package exports the following functions in both ESM and CommonJS format: + +* `fixupRule(rule)` - wraps the given rule in a compatibility layer and returns the result +* `fixupPluginRules(plugin)` - wraps each rule in the given plugin using `fixupRule()` and returns a new object that represents the plugin with the fixed-up rules +* `fixupConfigRules(configs)` - wraps all plugins found in an array of config objects using `fixupPluginRules()` + +### Fixing Rules + +If you have a rule that you'd like to make compatible with ESLint v9.x, you can do so using the `fixupRule()` function: + +```js +// ESM example +import { fixupRule } from "@eslint/compat"; + +// Step 1: Import your rule +import myRule from "./local-rule.js"; + +// Step 2: Create backwards-compatible rule +const compatRule = fixupRule(myRule); + +// Step 3 (optional): Export fixed rule +export default compatRule; +``` + +Or in CommonJS: + +```js +// CommonJS example +const { fixupRule } = require("@eslint/compat"); + +// Step 1: Import your rule +const myRule = require("./local-rule.js"); + +// Step 2: Create backwards-compatible rule +const compatRule = fixupRule(myRule); + +// Step 3 (optional): Export fixed rule +module.exports = compatRule; +``` + +### Fixing Plugins + +If you are using a plugin in your `eslint.config.js` that is not yet compatible with ESLint 9.x, you can wrap it using the `fixupPluginRules()` function: + +```js +// eslint.config.js - ESM example +import { fixupPluginRules } from "@eslint/compat"; +import somePlugin from "eslint-plugin-some-plugin"; + +export default [ + { + plugins: { + // insert the fixed plugin instead of the original + somePlugin: fixupPluginRules(somePlugin) + }, + rules: { + "somePlugin/rule-name": "error" + } + } +]; +``` + +Or in CommonJS: + +```js +// eslint.config.js - CommonJS example +const { fixupPluginRules } = require("@eslint/compat"); +const somePlugin = require("eslint-plugin-some-plugin"); + +module.exports = [ + { + plugins: { + // insert the fixed plugin instead of the original + somePlugin: fixupPluginRules(somePlugin) + }, + rules: { + "somePlugin/rule-name": "error" + } + } +]; +``` + +### Fixing Configs + +If you are importing other configs into your `eslint.config.js` that use plugins that are not yet compatible with ESLint 9.x, you can wrap the entire array using the `fixupConfigRules()` function: + +```js +// eslint.config.js - ESM example +import { fixupConfigRules } from "@eslint/compat"; +import someConfig from "eslint-config-some-config"; + +export default [ + ...fixupConfigRules(someConfig), + { + // your overrides + } +]; +``` + +Or in CommonJS: + +```js +// eslint.config.js - CommonJS example +const { fixupConfigRules } = require("@eslint/compat"); +const someConfig = require("eslint-config-some-config"); + +module.exports = [ + ...fixupConfigRules(someConfig), + { + // your overrides + } +]; +``` + +## License + +Apache 2.0 diff --git a/packages/compat/jsr.json b/packages/compat/jsr.json new file mode 100644 index 0000000..5993ad6 --- /dev/null +++ b/packages/compat/jsr.json @@ -0,0 +1,10 @@ +{ + "name": "@eslint/compat", + "version": "0.0.0", + "exports": "./dist/esm/index.js", + "publish": { + "exclude": [ + "!dist" + ] + } +} diff --git a/packages/compat/package.json b/packages/compat/package.json new file mode 100644 index 0000000..6a4f55b --- /dev/null +++ b/packages/compat/package.json @@ -0,0 +1,55 @@ +{ + "name": "@eslint/compat", + "version": "0.0.0", + "description": "Compatibility utilities for ESLint", + "type": "module", + "exports": { + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + }, + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "directories": { + "test": "tests" + }, + "scripts": { + "build": "rollup -c && tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json", + "prepare": "npm run build", + "test": "mocha tests/*.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/eslint/rewrite.git" + }, + "keywords": [ + "eslint", + "compatibility", + "eslintplugin", + "eslint-plugin" + ], + "author": "Nicholas C. Zakas", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/eslint/rewrite/issues" + }, + "homepage": "https://github.com/eslint/rewrite#readme", + "devDependencies": { + "eslint": "^9.0.0", + "mocha": "^10.4.0", + "rollup": "^4.16.2", + "typescript": "^5.4.5", + "rollup-plugin-copy": "^3.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } +} diff --git a/packages/compat/rollup.config.js b/packages/compat/rollup.config.js new file mode 100644 index 0000000..9af63ce --- /dev/null +++ b/packages/compat/rollup.config.js @@ -0,0 +1,23 @@ +import copy from "rollup-plugin-copy"; + +export default { + input: "src/index.js", + output: [ + { + file: "dist/cjs/index.cjs", + format: "cjs", + }, + { + file: "dist/esm/index.js", + format: "esm", + }, + ], + plugins: [ + copy({ + targets: [ + { src: "src/types.ts", dest: "dist/cjs" }, + { src: "src/types.ts", dest: "dist/esm" }, + ], + }), + ], +}; diff --git a/packages/compat/src/fixup-rules.js b/packages/compat/src/fixup-rules.js new file mode 100644 index 0000000..829d54c --- /dev/null +++ b/packages/compat/src/fixup-rules.js @@ -0,0 +1,256 @@ +/** + * @filedescription Object Schema + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Types +//----------------------------------------------------------------------------- + +/** @typedef {import("./types.ts").FixupRuleDefinition} FixupRuleDefinition */ +/** @typedef {import("./types.ts").FixupLegacyRuleDefinition} FixupLegacyRuleDefinition */ +/** @typedef {import("./types.ts").FixupPluginDefinition} FixupPluginDefinition */ +/** @typedef {import("./types.ts").FixupConfig} FixupConfig */ +/** @typedef {import("./types.ts").FixupConfigArray} FixupConfigArray */ + +//----------------------------------------------------------------------------- +// Data +//----------------------------------------------------------------------------- + +/** + * The removed methods from the `context` object that need to be added back. + * The keys are the name of the method on the `context` object and the values + * are the name of the method on the `sourceCode` object. + * @type {Map} + */ +const removedMethodNames = new Map([ + ["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"], +]); + +/** + * Tracks the original rule definition and the fixed-up rule definition. + * @type {WeakMap} + */ +const fixedUpRuleReplacements = new WeakMap(); + +/** + * Tracks all of the fixed up rule definitions so we don't duplicate effort. + * @type {WeakSet} + */ +const fixedUpRules = new WeakSet(); + +/** + * Tracks the original plugin definition and the fixed-up plugin definition. + * @type {WeakMap} + */ +const fixedUpPluginReplacements = new WeakMap(); + +/** + * Tracks all of the fixed up plugin definitions so we don't duplicate effort. + * @type {WeakSet} + */ +const fixedUpPlugins = new WeakSet(); + +//----------------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------------- + +/** + * Takes the given rule and creates a new rule with the `create()` method wrapped + * to provide the missing methods on the `context` object. + * @param {FixupRuleDefinition|FixupLegacyRuleDefinition} ruleDefinition The rule to fix up. + * @returns {FixupRuleDefinition} The fixed-up rule. + */ +export function fixupRule(ruleDefinition) { + // first check if we've already fixed up this rule + if (fixedUpRuleReplacements.has(ruleDefinition)) { + return fixedUpRuleReplacements.get(ruleDefinition); + } + + const isLegacyRule = typeof ruleDefinition === "function"; + + // check to see if this rule definition has already been fixed up + if (!isLegacyRule && fixedUpRules.has(ruleDefinition)) { + return ruleDefinition; + } + + const originalCreate = isLegacyRule + ? ruleDefinition + : ruleDefinition.create.bind(ruleDefinition); + + const ruleCreate = context => { + // if getScope is already there then no need to create old methods + if ("getScope" in context) { + return originalCreate(context); + } + + const sourceCode = context.sourceCode; + let currentNode = sourceCode.ast; + + const newContext = Object.assign(Object.create(context), { + parserServices: sourceCode.parserServices, + + /* + * The following methods rely on the current node in the traversal, + * so we need to add them manually. + */ + getScope() { + return sourceCode.getScope(currentNode); + }, + + getAncestors() { + return sourceCode.getAncestors(currentNode); + }, + + getDeclaredVariables() { + return sourceCode.getDeclaredVariables(currentNode); + }, + + markVariableAsUsed(variable) { + sourceCode.markVariableAsUsed(variable, currentNode); + }, + }); + + // add passthrough methods + for (const [ + contextMethodName, + sourceCodeMethodName, + ] of removedMethodNames) { + newContext[contextMethodName] = + sourceCode[sourceCodeMethodName].bind(sourceCode); + } + + // freeze just like the original context + Object.freeze(newContext); + + /* + * Create the visitor object using the original create() method. + * This is necessary to ensure that the visitor object is created + * with the correct context. + */ + const visitor = originalCreate(newContext); + + /* + * Wrap each method in the visitor object to update the currentNode + * before calling the original method. This is necessary because the + * methods like `getScope()` need to know the current node. + */ + for (const [methodName, method] of Object.entries(visitor)) { + // node is the second argument for code path methods + if (methodName.startsWith("on")) { + visitor[methodName] = (...args) => { + currentNode = args[1]; + + return method.call(visitor, ...args); + }; + + continue; + } + + visitor[methodName] = (...args) => { + currentNode = args[0]; + + return method.call(visitor, ...args); + }; + } + + return visitor; + }; + + const newRuleDefinition = { + ...(isLegacyRule ? undefined : ruleDefinition), + create: ruleCreate, + }; + + // cache the fixed up rule + fixedUpRuleReplacements.set(ruleDefinition, newRuleDefinition); + fixedUpRules.add(newRuleDefinition); + + return newRuleDefinition; +} + +/** + * Takes the given plugin and creates a new plugin with all of the rules wrapped + * to provide the missing methods on the `context` object. + * @param {FixupPluginDefinition} plugin The plugin to fix up. + * @returns {FixupPluginDefinition} The fixed-up plugin. + */ +export function fixupPluginRules(plugin) { + // first check if we've already fixed up this plugin + if (fixedUpPluginReplacements.has(plugin)) { + return fixedUpPluginReplacements.get(plugin); + } + + /* + * If the plugin has already been fixed up, or if the plugin + * doesn't have any rules, we can just return it. + */ + if (fixedUpPlugins.has(plugin) || !plugin.rules) { + return plugin; + } + + const newPlugin = { + ...plugin, + rules: Object.fromEntries( + Object.entries(plugin.rules).map(([ruleId, ruleDefinition]) => [ + ruleId, + fixupRule(ruleDefinition), + ]), + ), + }; + + // cache the fixed up plugin + fixedUpPluginReplacements.set(plugin, newPlugin); + fixedUpPlugins.add(newPlugin); + + return newPlugin; +} + +/** + * Takes the given configuration and creates a new configuration with all of the + * rules wrapped to provide the missing methods on the `context` object. + * @param {FixupConfigArray} configs The configuration to fix up. + * @returns {FixupConfigArray} The fixed-up configuration. + */ +export function fixupConfigRules(configs) { + return configs.map(config => { + if (!config.plugins) { + return config; + } + + const newPlugins = Object.fromEntries( + Object.entries(config.plugins).map(([pluginName, plugin]) => [ + pluginName, + fixupPluginRules(plugin), + ]), + ); + + return { + ...config, + plugins: newPlugins, + }; + }); +} diff --git a/packages/compat/src/index.js b/packages/compat/src/index.js new file mode 100644 index 0000000..5407939 --- /dev/null +++ b/packages/compat/src/index.js @@ -0,0 +1,5 @@ +/** + * @filedescription Object Schema Package + */ + +export * from "./fixup-rules.js"; diff --git a/packages/compat/src/types.ts b/packages/compat/src/types.ts new file mode 100644 index 0000000..2eda075 --- /dev/null +++ b/packages/compat/src/types.ts @@ -0,0 +1,36 @@ +/** + * @filedescription Types for backcompat package. + */ + +/* + * NOTE: These are minimal type definitions to help avoid errors in the + * backcompat package. They are not intended to be complete and should be + * replaced when the actual types are available. + */ + +export type FixupLegacyRuleDefinition = (context: Object) => Object; + +export interface FixupRuleDefinition { + meta: Object; + create(context: Object): Object; +} + +export interface FixupPluginDefinition { + meta: Object; + rules: Record; + configs: Record; + processors: Record; +} + +export interface FixupConfig { + files: Array; + ignores: Array; + name: string; + languageOptions: Record; + linterOptions: Record; + processor: string|Object; + plugins: Record; + rules: Record; +} + +export type FixupConfigArray = Array; diff --git a/packages/compat/tests/fixtures/rules/consistent-this.js b/packages/compat/tests/fixtures/rules/consistent-this.js new file mode 100644 index 0000000..6f7eddf --- /dev/null +++ b/packages/compat/tests/fixtures/rules/consistent-this.js @@ -0,0 +1,170 @@ +/** + * @fileoverview Rule to enforce consistent naming of "this" context variables + * @author Raphael Pigulla + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +export default { + meta: { + type: "suggestion", + + docs: { + description: + "Enforce consistent naming when capturing the current execution context", + recommended: false, + url: "https://eslint.org/docs/latest/rules/consistent-this", + }, + + schema: { + type: "array", + items: { + type: "string", + minLength: 1, + }, + uniqueItems: true, + }, + + messages: { + aliasNotAssignedToThis: + "Designated alias '{{name}}' is not assigned to 'this'.", + unexpectedAlias: "Unexpected alias '{{name}}' for 'this'.", + }, + }, + + create(context) { + let aliases = []; + + if (context.options.length === 0) { + aliases.push("that"); + } else { + aliases = context.options; + } + + /** + * Reports that a variable declarator or assignment expression is assigning + * a non-'this' value to the specified alias. + * @param {ASTNode} node The assigning node. + * @param {string} name the name of the alias that was incorrectly used. + * @returns {void} + */ + function reportBadAssignment(node, name) { + context.report({ + node, + messageId: "aliasNotAssignedToThis", + data: { name }, + }); + } + + /** + * Checks that an assignment to an identifier only assigns 'this' to the + * appropriate alias, and the alias is only assigned to 'this'. + * @param {ASTNode} node The assigning node. + * @param {Identifier} name The name of the variable assigned to. + * @param {Expression} value The value of the assignment. + * @returns {void} + */ + function checkAssignment(node, name, value) { + const isThis = value.type === "ThisExpression"; + + if (aliases.includes(name)) { + if (!isThis || (node.operator && node.operator !== "=")) { + reportBadAssignment(node, name); + } + } else if (isThis) { + context.report({ + node, + messageId: "unexpectedAlias", + data: { name }, + }); + } + } + + /** + * Ensures that a variable declaration of the alias in a program or function + * is assigned to the correct value. + * @param {string} alias alias the check the assignment of. + * @param {Object} scope scope of the current code we are checking. + * @private + * @returns {void} + */ + function checkWasAssigned(alias, scope) { + const variable = scope.set.get(alias); + + if (!variable) { + return; + } + + if ( + variable.defs.some( + def => + def.node.type === "VariableDeclarator" && + def.node.init !== null, + ) + ) { + return; + } + + /* + * The alias has been declared and not assigned: check it was + * assigned later in the same scope. + */ + if ( + !variable.references.some(reference => { + const write = reference.writeExpr; + + return ( + reference.from === scope && + write && + write.type === "ThisExpression" && + write.parent.operator === "=" + ); + }) + ) { + variable.defs + .map(def => def.node) + .forEach(node => { + reportBadAssignment(node, alias); + }); + } + } + + /** + * Check each alias to ensure that is was assigned to the correct value. + * @param {ASTNode} node The node that represents the scope to check. + * @returns {void} + */ + function ensureWasAssigned() { + const scope = context.getScope(); + + aliases.forEach(alias => { + checkWasAssigned(alias, scope); + }); + } + + return { + "Program:exit": ensureWasAssigned, + "FunctionExpression:exit": ensureWasAssigned, + "FunctionDeclaration:exit": ensureWasAssigned, + + VariableDeclarator(node) { + const id = node.id; + const isDestructuring = + id.type === "ArrayPattern" || id.type === "ObjectPattern"; + + if (node.init !== null && !isDestructuring) { + checkAssignment(node, id.name, node.init); + } + }, + + AssignmentExpression(node) { + if (node.left.type === "Identifier") { + checkAssignment(node, node.left.name, node.right); + } + }, + }; + }, +}; diff --git a/packages/compat/tests/fixtures/rules/global-require.js b/packages/compat/tests/fixtures/rules/global-require.js new file mode 100644 index 0000000..531dff5 --- /dev/null +++ b/packages/compat/tests/fixtures/rules/global-require.js @@ -0,0 +1,94 @@ +/** + * @fileoverview Rule for disallowing require() outside of the top-level module context + * @author Jamund Ferguson + */ + +const ACCEPTABLE_PARENTS = new Set([ + "AssignmentExpression", + "VariableDeclarator", + "MemberExpression", + "ExpressionStatement", + "CallExpression", + "ConditionalExpression", + "Program", + "VariableDeclaration", + "ChainExpression", +]); + +/** + * Finds the eslint-scope reference in the given scope. + * @param {Object} scope The scope to search. + * @param {ASTNode} node The identifier node. + * @returns {Reference|null} Returns the found reference or null if none were found. + */ +function findReference(scope, node) { + const references = scope.references.filter( + reference => + reference.identifier.range[0] === node.range[0] && + reference.identifier.range[1] === node.range[1], + ); + + if (references.length === 1) { + return references[0]; + } + + /* c8 ignore next */ + return null; +} + +/** + * Checks if the given identifier node is shadowed in the given scope. + * @param {Object} scope The current scope. + * @param {ASTNode} node The identifier node to check. + * @returns {boolean} Whether or not the name is shadowed. + */ +function isShadowed(scope, node) { + const reference = findReference(scope, node); + + return ( + reference && reference.resolved && reference.resolved.defs.length > 0 + ); +} + +export default { + meta: { + deprecated: true, + + replacedBy: [], + + type: "suggestion", + + docs: { + description: + "Require `require()` calls to be placed at top-level module scope", + recommended: false, + url: "https://eslint.org/docs/latest/rules/global-require", + }, + + schema: [], + messages: { + unexpected: "Unexpected require().", + }, + }, + + create(context) { + return { + CallExpression(node) { + const currentScope = context.getScope(); + + if ( + node.callee.name === "require" && + !isShadowed(currentScope, node.callee) + ) { + const isGoodRequire = context + .getAncestors() + .every(parent => ACCEPTABLE_PARENTS.has(parent.type)); + + if (!isGoodRequire) { + context.report({ node, messageId: "unexpected" }); + } + } + }, + }; + }, +}; diff --git a/packages/compat/tests/fixtures/rules/handle-callback-err.js b/packages/compat/tests/fixtures/rules/handle-callback-err.js new file mode 100644 index 0000000..31eb06a --- /dev/null +++ b/packages/compat/tests/fixtures/rules/handle-callback-err.js @@ -0,0 +1,100 @@ +/** + * @fileoverview Ensure handling of errors when we know they exist. + * @author Jamund Ferguson + */ + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +export default { + meta: { + deprecated: true, + + replacedBy: [], + + type: "suggestion", + + docs: { + description: "Require error handling in callbacks", + recommended: false, + url: "https://eslint.org/docs/latest/rules/handle-callback-err", + }, + + schema: [ + { + type: "string", + }, + ], + messages: { + expected: "Expected error to be handled.", + }, + }, + + create(context) { + const errorArgument = context.options[0] || "err"; + + /** + * Checks if the given argument should be interpreted as a regexp pattern. + * @param {string} stringToCheck The string which should be checked. + * @returns {boolean} Whether or not the string should be interpreted as a pattern. + */ + function isPattern(stringToCheck) { + const firstChar = stringToCheck[0]; + + return firstChar === "^"; + } + + /** + * Checks if the given name matches the configured error argument. + * @param {string} name The name which should be compared. + * @returns {boolean} Whether or not the given name matches the configured error variable name. + */ + function matchesConfiguredErrorName(name) { + if (isPattern(errorArgument)) { + const regexp = new RegExp(errorArgument, "u"); + + return regexp.test(name); + } + return name === errorArgument; + } + + /** + * Get the parameters of a given function scope. + * @param {Object} scope The function scope. + * @returns {Array} All parameters of the given scope. + */ + function getParameters(scope) { + return scope.variables.filter( + variable => + variable.defs[0] && variable.defs[0].type === "Parameter", + ); + } + + /** + * Check to see if we're handling the error object properly. + * @param {ASTNode} node The AST node to check. + * @returns {void} + */ + function checkForError(node) { + const scope = context.getScope(), + parameters = getParameters(scope), + firstParameter = parameters[0]; + + if ( + firstParameter && + matchesConfiguredErrorName(firstParameter.name) + ) { + if (firstParameter.references.length === 0) { + context.report({ node, messageId: "expected" }); + } + } + } + + return { + FunctionDeclaration: checkForError, + FunctionExpression: checkForError, + ArrowFunctionExpression: checkForError, + }; + }, +}; diff --git a/packages/compat/tests/fixtures/rules/no-lone-blocks.js b/packages/compat/tests/fixtures/rules/no-lone-blocks.js new file mode 100644 index 0000000..857f70c --- /dev/null +++ b/packages/compat/tests/fixtures/rules/no-lone-blocks.js @@ -0,0 +1,137 @@ +/** + * @fileoverview Rule to flag blocks with no reason to exist + * @author Brandon Mills + */ + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +export default { + meta: { + type: "suggestion", + + docs: { + description: "Disallow unnecessary nested blocks", + recommended: false, + url: "https://eslint.org/docs/latest/rules/no-lone-blocks", + }, + + schema: [], + + messages: { + redundantBlock: "Block is redundant.", + redundantNestedBlock: "Nested block is redundant.", + }, + }, + + create(context) { + // A stack of lone blocks to be checked for block-level bindings + const loneBlocks = []; + let ruleDef; + const sourceCode = context.sourceCode; + + /** + * Reports a node as invalid. + * @param {ASTNode} node The node to be reported. + * @returns {void} + */ + function report(node) { + const messageId = + node.parent.type === "BlockStatement" || + node.parent.type === "StaticBlock" + ? "redundantNestedBlock" + : "redundantBlock"; + + context.report({ + node, + messageId, + }); + } + + /** + * Checks for any occurrence of a BlockStatement in a place where lists of statements can appear + * @param {ASTNode} node The node to check + * @returns {boolean} True if the node is a lone block. + */ + function isLoneBlock(node) { + return ( + node.parent.type === "BlockStatement" || + node.parent.type === "StaticBlock" || + node.parent.type === "Program" || + // Don't report blocks in switch cases if the block is the only statement of the case. + (node.parent.type === "SwitchCase" && + !( + node.parent.consequent[0] === node && + node.parent.consequent.length === 1 + )) + ); + } + + /** + * Checks the enclosing block of the current node for block-level bindings, + * and "marks it" as valid if any. + * @param {ASTNode} node The current node to check. + * @returns {void} + */ + function markLoneBlock(node) { + if (loneBlocks.length === 0) { + return; + } + + const block = node.parent; + + if (loneBlocks.at(-1) === block) { + loneBlocks.pop(); + } + } + + // Default rule definition: report all lone blocks + ruleDef = { + BlockStatement(node) { + if (isLoneBlock(node)) { + report(node); + } + }, + }; + + // ES6: report blocks without block-level bindings, or that's only child of another block + if (context.languageOptions.ecmaVersion >= 2015) { + ruleDef = { + BlockStatement(node) { + if (isLoneBlock(node)) { + loneBlocks.push(node); + } + }, + "BlockStatement:exit"(node) { + if (loneBlocks.length > 0 && loneBlocks.at(-1) === node) { + loneBlocks.pop(); + report(node); + } else if ( + (node.parent.type === "BlockStatement" || + node.parent.type === "StaticBlock") && + node.parent.body.length === 1 + ) { + report(node); + } + }, + }; + + ruleDef.VariableDeclaration = function (node) { + if (node.kind !== "var") { + markLoneBlock(node); + } + }; + + ruleDef.FunctionDeclaration = function (node) { + if (sourceCode.getScope(node).isStrict) { + markLoneBlock(node); + } + }; + + ruleDef.ClassDeclaration = markLoneBlock; + } + + return ruleDef; + }, +}; diff --git a/packages/compat/tests/fixtures/rules/no-loop-func.js b/packages/compat/tests/fixtures/rules/no-loop-func.js new file mode 100644 index 0000000..8acf582 --- /dev/null +++ b/packages/compat/tests/fixtures/rules/no-loop-func.js @@ -0,0 +1,207 @@ +/** + * @fileoverview Rule to flag creation of function inside a loop + * @author Ilya Volodin + */ + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Gets the containing loop node of a specified node. + * + * We don't need to check nested functions, so this ignores those. + * `Scope.through` contains references of nested functions. + * @param {ASTNode} node An AST node to get. + * @returns {ASTNode|null} The containing loop node of the specified node, or + * `null`. + */ +function getContainingLoopNode(node) { + for ( + let currentNode = node; + currentNode.parent; + currentNode = currentNode.parent + ) { + const parent = currentNode.parent; + + switch (parent.type) { + case "WhileStatement": + case "DoWhileStatement": + return parent; + + case "ForStatement": + // `init` is outside of the loop. + if (parent.init !== currentNode) { + return parent; + } + break; + + case "ForInStatement": + case "ForOfStatement": + // `right` is outside of the loop. + if (parent.right !== currentNode) { + return parent; + } + break; + + case "ArrowFunctionExpression": + case "FunctionExpression": + case "FunctionDeclaration": + // We don't need to check nested functions. + return null; + + default: + break; + } + } + + return null; +} + +/** + * Gets the containing loop node of a given node. + * If the loop was nested, this returns the most outer loop. + * @param {ASTNode} node A node to get. This is a loop node. + * @param {ASTNode|null} excludedNode A node that the result node should not + * include. + * @returns {ASTNode} The most outer loop node. + */ +function getTopLoopNode(node, excludedNode) { + const border = excludedNode ? excludedNode.range[1] : 0; + let retv = node; + let containingLoopNode = node; + + while (containingLoopNode && containingLoopNode.range[0] >= border) { + retv = containingLoopNode; + containingLoopNode = getContainingLoopNode(containingLoopNode); + } + + return retv; +} + +/** + * Checks whether a given reference which refers to an upper scope's variable is + * safe or not. + * @param {ASTNode} loopNode A containing loop node. + * @param {eslint-scope.Reference} reference A reference to check. + * @returns {boolean} `true` if the reference is safe or not. + */ +function isSafe(loopNode, reference) { + const variable = reference.resolved; + const definition = variable && variable.defs[0]; + const declaration = definition && definition.parent; + const kind = + declaration && declaration.type === "VariableDeclaration" + ? declaration.kind + : ""; + + // Variables which are declared by `const` is safe. + if (kind === "const") { + return true; + } + + /* + * Variables which are declared by `let` in the loop is safe. + * It's a different instance from the next loop step's. + */ + if ( + kind === "let" && + declaration.range[0] > loopNode.range[0] && + declaration.range[1] < loopNode.range[1] + ) { + return true; + } + + /* + * WriteReferences which exist after this border are unsafe because those + * can modify the variable. + */ + const border = getTopLoopNode(loopNode, kind === "let" ? declaration : null) + .range[0]; + + /** + * Checks whether a given reference is safe or not. + * The reference is every reference of the upper scope's variable we are + * looking now. + * + * It's safe if the reference matches one of the following condition. + * - is readonly. + * - doesn't exist inside a local function and after the border. + * @param {eslint-scope.Reference} upperRef A reference to check. + * @returns {boolean} `true` if the reference is safe. + */ + function isSafeReference(upperRef) { + const id = upperRef.identifier; + + return ( + !upperRef.isWrite() || + (variable.scope.variableScope === upperRef.from.variableScope && + id.range[0] < border) + ); + } + + return Boolean(variable) && variable.references.every(isSafeReference); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +export default { + meta: { + type: "suggestion", + + docs: { + description: + "Disallow function declarations that contain unsafe references inside loop statements", + recommended: false, + url: "https://eslint.org/docs/latest/rules/no-loop-func", + }, + + schema: [], + + messages: { + unsafeRefs: + "Function declared in a loop contains unsafe references to variable(s) {{ varNames }}.", + }, + }, + + create(context) { + const sourceCode = context.sourceCode; + + /** + * Reports functions which match the following condition: + * + * - has a loop node in ancestors. + * - has any references which refers to an unsafe variable. + * @param {ASTNode} node The AST node to check. + * @returns {void} + */ + function checkForLoops(node) { + const loopNode = getContainingLoopNode(node); + + if (!loopNode) { + return; + } + + const references = sourceCode.getScope(node).through; + const unsafeRefs = references + .filter(r => r.resolved && !isSafe(loopNode, r)) + .map(r => r.identifier.name); + + if (unsafeRefs.length > 0) { + context.report({ + node, + messageId: "unsafeRefs", + data: { varNames: `'${unsafeRefs.join("', '")}'` }, + }); + } + } + + return { + ArrowFunctionExpression: checkForLoops, + FunctionExpression: checkForLoops, + FunctionDeclaration: checkForLoops, + }; + }, +}; diff --git a/packages/compat/tests/fixtures/rules/prefer-rest-params.js b/packages/compat/tests/fixtures/rules/prefer-rest-params.js new file mode 100644 index 0000000..c704322 --- /dev/null +++ b/packages/compat/tests/fixtures/rules/prefer-rest-params.js @@ -0,0 +1,114 @@ +/** + * @fileoverview Rule to + * @author Toru Nagashima + */ + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Gets the variable object of `arguments` which is defined implicitly. + * @param {eslint-scope.Scope} scope A scope to get. + * @returns {eslint-scope.Variable} The found variable object. + */ +function getVariableOfArguments(scope) { + const variables = scope.variables; + + for (let i = 0; i < variables.length; ++i) { + const variable = variables[i]; + + if (variable.name === "arguments") { + /* + * If there was a parameter which is named "arguments", the implicit "arguments" is not defined. + * So does fast return with null. + */ + return variable.identifiers.length === 0 ? variable : null; + } + } + + /* c8 ignore next */ + return null; +} + +/** + * Checks if the given reference is not normal member access. + * + * - arguments .... true // not member access + * - arguments[i] .... true // computed member access + * - arguments[0] .... true // computed member access + * - arguments.length .... false // normal member access + * @param {eslint-scope.Reference} reference The reference to check. + * @returns {boolean} `true` if the reference is not normal member access. + */ +function isNotNormalMemberAccess(reference) { + const id = reference.identifier; + const parent = id.parent; + + return !( + parent.type === "MemberExpression" && + parent.object === id && + !parent.computed + ); +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +export default { + meta: { + type: "suggestion", + + docs: { + description: "Require rest parameters instead of `arguments`", + recommended: false, + url: "https://eslint.org/docs/latest/rules/prefer-rest-params", + }, + + schema: [], + + messages: { + preferRestParams: "Use the rest parameters instead of 'arguments'.", + }, + }, + + create(context) { + const sourceCode = context.sourceCode; + + /** + * Reports a given reference. + * @param {eslint-scope.Reference} reference A reference to report. + * @returns {void} + */ + function report(reference) { + context.report({ + node: reference.identifier, + loc: reference.identifier.loc, + messageId: "preferRestParams", + }); + } + + /** + * Reports references of the implicit `arguments` variable if exist. + * @param {ASTNode} node The node representing the function. + * @returns {void} + */ + function checkForArguments(node) { + const argumentsVar = getVariableOfArguments( + sourceCode.getScope(node), + ); + + if (argumentsVar) { + argumentsVar.references + .filter(isNotNormalMemberAccess) + .forEach(report); + } + } + + return { + "FunctionDeclaration:exit": checkForArguments, + "FunctionExpression:exit": checkForArguments, + }; + }, +}; diff --git a/packages/compat/tests/fixtures/rules/require-atomic-updates.js b/packages/compat/tests/fixtures/rules/require-atomic-updates.js new file mode 100644 index 0000000..6d99095 --- /dev/null +++ b/packages/compat/tests/fixtures/rules/require-atomic-updates.js @@ -0,0 +1,359 @@ +/** + * @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield` + * @author Teddy Katz + * @author Toru Nagashima + */ + +/** + * Make the map from identifiers to each reference. + * @param {escope.Scope} scope The scope to get references. + * @param {Map} [outReferenceMap] The map from identifier nodes to each reference object. + * @returns {Map} `referenceMap`. + */ +function createReferenceMap(scope, outReferenceMap = new Map()) { + for (const reference of scope.references) { + if (reference.resolved === null) { + continue; + } + + outReferenceMap.set(reference.identifier, reference); + } + for (const childScope of scope.childScopes) { + if (childScope.type !== "function") { + createReferenceMap(childScope, outReferenceMap); + } + } + + return outReferenceMap; +} + +/** + * Get `reference.writeExpr` of a given reference. + * If it's the read reference of MemberExpression in LHS, returns RHS in order to address `a.b = await a` + * @param {escope.Reference} reference The reference to get. + * @returns {Expression|null} The `reference.writeExpr`. + */ +function getWriteExpr(reference) { + if (reference.writeExpr) { + return reference.writeExpr; + } + let node = reference.identifier; + + while (node) { + const t = node.parent.type; + + if (t === "AssignmentExpression" && node.parent.left === node) { + return node.parent.right; + } + if (t === "MemberExpression" && node.parent.object === node) { + node = node.parent; + continue; + } + + break; + } + + return null; +} + +/** + * Checks if an expression is a variable that can only be observed within the given function. + * @param {Variable|null} variable The variable to check + * @param {boolean} isMemberAccess If `true` then this is a member access. + * @returns {boolean} `true` if the variable is local to the given function, and is never referenced in a closure. + */ +function isLocalVariableWithoutEscape(variable, isMemberAccess) { + if (!variable) { + return false; // A global variable which was not defined. + } + + // If the reference is a property access and the variable is a parameter, it handles the variable is not local. + if (isMemberAccess && variable.defs.some(d => d.type === "Parameter")) { + return false; + } + + const functionScope = variable.scope.variableScope; + + return variable.references.every( + reference => reference.from.variableScope === functionScope, + ); +} + +/** + * Represents segment information. + */ +class SegmentInfo { + constructor() { + this.info = new WeakMap(); + } + + /** + * Initialize the segment information. + * @param {PathSegment} segment The segment to initialize. + * @returns {void} + */ + initialize(segment) { + const outdatedReadVariables = new Set(); + const freshReadVariables = new Set(); + + for (const prevSegment of segment.prevSegments) { + const info = this.info.get(prevSegment); + + if (info) { + info.outdatedReadVariables.forEach( + Set.prototype.add, + outdatedReadVariables, + ); + info.freshReadVariables.forEach( + Set.prototype.add, + freshReadVariables, + ); + } + } + + this.info.set(segment, { outdatedReadVariables, freshReadVariables }); + } + + /** + * Mark a given variable as read on given segments. + * @param {PathSegment[]} segments The segments that it read the variable on. + * @param {Variable} variable The variable to be read. + * @returns {void} + */ + markAsRead(segments, variable) { + for (const segment of segments) { + const info = this.info.get(segment); + + if (info) { + info.freshReadVariables.add(variable); + + // If a variable is freshly read again, then it's no more out-dated. + info.outdatedReadVariables.delete(variable); + } + } + } + + /** + * Move `freshReadVariables` to `outdatedReadVariables`. + * @param {PathSegment[]} segments The segments to process. + * @returns {void} + */ + makeOutdated(segments) { + for (const segment of segments) { + const info = this.info.get(segment); + + if (info) { + info.freshReadVariables.forEach( + Set.prototype.add, + info.outdatedReadVariables, + ); + info.freshReadVariables.clear(); + } + } + } + + /** + * Check if a given variable is outdated on the current segments. + * @param {PathSegment[]} segments The current segments. + * @param {Variable} variable The variable to check. + * @returns {boolean} `true` if the variable is outdated on the segments. + */ + isOutdated(segments, variable) { + for (const segment of segments) { + const info = this.info.get(segment); + + if (info && info.outdatedReadVariables.has(variable)) { + return true; + } + } + return false; + } +} + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +export default { + meta: { + type: "problem", + + docs: { + description: + "Disallow assignments that can lead to race conditions due to usage of `await` or `yield`", + recommended: false, + url: "https://eslint.org/docs/latest/rules/require-atomic-updates", + }, + + fixable: null, + + schema: [ + { + type: "object", + properties: { + allowProperties: { + type: "boolean", + default: false, + }, + }, + additionalProperties: false, + }, + ], + + messages: { + nonAtomicUpdate: + "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`.", + nonAtomicObjectUpdate: + "Possible race condition: `{{value}}` might be assigned based on an outdated state of `{{object}}`.", + }, + }, + + create(context) { + const allowProperties = + !!context.options[0] && context.options[0].allowProperties; + + const sourceCode = context.sourceCode; + const assignmentReferences = new Map(); + const segmentInfo = new SegmentInfo(); + let stack = null; + + return { + onCodePathStart(codePath, node) { + const scope = sourceCode.getScope(node); + const shouldVerify = + scope.type === "function" && + (scope.block.async || scope.block.generator); + + stack = { + upper: stack, + codePath, + referenceMap: shouldVerify + ? createReferenceMap(scope) + : null, + currentSegments: new Set(), + }; + }, + onCodePathEnd() { + stack = stack.upper; + }, + + // 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 { referenceMap } = stack; + const reference = referenceMap && referenceMap.get(node); + + // Ignore if this is not a valid variable reference. + if (!reference) { + return; + } + const variable = reference.resolved; + const writeExpr = getWriteExpr(reference); + const isMemberAccess = + reference.identifier.parent.type === "MemberExpression"; + + // Add a fresh read variable. + if ( + reference.isRead() && + !(writeExpr && writeExpr.parent.operator === "=") + ) { + segmentInfo.markAsRead(stack.currentSegments, variable); + } + + /* + * Register the variable to verify after ESLint traversed the `writeExpr` node + * if this reference is an assignment to a variable which is referred from other closure. + */ + if ( + writeExpr && + writeExpr.parent.right === writeExpr && // ← exclude variable declarations. + !isLocalVariableWithoutEscape(variable, isMemberAccess) + ) { + let refs = assignmentReferences.get(writeExpr); + + if (!refs) { + refs = []; + assignmentReferences.set(writeExpr, refs); + } + + refs.push(reference); + } + }, + + /* + * Verify assignments. + * If the reference exists in `outdatedReadVariables` list, report it. + */ + ":expression:exit"(node) { + // referenceMap exists if this is in a resumable function scope. + if (!stack.referenceMap) { + return; + } + + // Mark the read variables on this code path as outdated. + if ( + node.type === "AwaitExpression" || + node.type === "YieldExpression" + ) { + segmentInfo.makeOutdated(stack.currentSegments); + } + + // Verify. + const references = assignmentReferences.get(node); + + if (references) { + assignmentReferences.delete(node); + + for (const reference of references) { + const variable = reference.resolved; + + if ( + segmentInfo.isOutdated( + stack.currentSegments, + variable, + ) + ) { + if (node.parent.left === reference.identifier) { + context.report({ + node: node.parent, + messageId: "nonAtomicUpdate", + data: { + value: variable.name, + }, + }); + } else if (!allowProperties) { + context.report({ + node: node.parent, + messageId: "nonAtomicObjectUpdate", + data: { + value: sourceCode.getText( + node.parent.left, + ), + object: variable.name, + }, + }); + } + } + } + } + }, + }; + }, +}; diff --git a/packages/compat/tests/fixup-rules.js b/packages/compat/tests/fixup-rules.js new file mode 100644 index 0000000..432fad8 --- /dev/null +++ b/packages/compat/tests/fixup-rules.js @@ -0,0 +1,496 @@ +/** + * @filedescription Fixup tests + */ +/* global it, describe */ + +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import assert from "node:assert"; +import { + fixupRule, + fixupPluginRules, + fixupConfigRules, +} from "../src/fixup-rules.js"; +import { Linter } from "eslint"; + +//----------------------------------------------------------------------------- +// Data +//----------------------------------------------------------------------------- + +const REPLACEMENT_METHODS = [ + "getScope", + "getAncestors", + "getDeclaredVariables", +]; + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("@eslint/backcompat", () => { + describe("fixupRule()", () => { + it("should return a new rule object with the same own properties", () => { + const rule = { + create(context) { + return { + Identifier(node) { + context.report(node, "Identifier"); + }, + }; + }, + }; + const fixedUpRule = fixupRule(rule); + + assert.notStrictEqual(rule, fixedUpRule); + assert.deepStrictEqual(Object.keys(rule), Object.keys(fixedUpRule)); + }); + + it("should return the same fixed up rule when applied to the same rule multiple times", () => { + const rule = { + create(context) { + return { + Identifier(node) { + context.report(node, "Identifier"); + }, + }; + }, + }; + + const fixedUpRule1 = fixupRule(rule); + const fixedUpRule2 = fixupRule(rule); + + assert.strictEqual(fixedUpRule1, fixedUpRule2); + }); + + it("should return the same fixed up rule when a fixed up rule is passed to fixupRule", () => { + const rule = { + create(context) { + return { + Identifier(node) { + context.report(node, "Identifier"); + }, + }; + }, + }; + + const fixedUpRule = fixupRule(rule); + const fixedUpRule2 = fixupRule(fixedUpRule); + + assert.strictEqual(fixedUpRule, fixedUpRule2); + }); + + REPLACEMENT_METHODS.forEach(method => { + it(`should create a rule where context.${method}() returns the same value as sourceCode.${method}(node)`, () => { + const rule = { + create(context) { + const { sourceCode } = context; + + return { + Program(node) { + const result = context[method](); + const expected = sourceCode[method](node); + assert.deepStrictEqual(result, expected); + context.report(node, "Program"); + }, + + FunctionDeclaration(node) { + const result = context[method](); + const expected = sourceCode[method](node); + assert.deepStrictEqual(result, expected); + context.report(node, "FunctionDeclaration"); + }, + + ArrowFunctionExpression(node) { + const result = context[method](); + const expected = sourceCode[method](node); + assert.deepStrictEqual(result, expected); + context.report(node, "ArrowFunctionExpression"); + }, + + Identifier(node) { + const result = context[method](); + const expected = sourceCode[method](node); + assert.deepStrictEqual(result, expected); + context.report(node, "Identifier"); + }, + }; + }, + }; + + const config = { + plugins: { + test: { + rules: { + "test-rule": fixupRule(rule), + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }; + + const linter = new Linter(); + const code = + "var foo = () => 123; function bar() { return 123; }"; + const messages = linter.verify(code, config, { + filename: "test.js", + }); + + assert.deepStrictEqual( + messages.map(message => message.message), + [ + "Program", + "Identifier", + "ArrowFunctionExpression", + "FunctionDeclaration", + "Identifier", + ], + ); + }); + }); + }); + + describe("fixupPluginRules()", () => { + it("should return a new plugin object with the same own properties", () => { + const plugin = { + configs: { + recommended: { + rules: { + "test-rule": "error", + }, + }, + }, + rules: { + "test-rule": { + create(context) { + return { + Identifier(node) { + context.report(node, "Identifier"); + }, + }; + }, + }, + }, + }; + + const fixedUpPlugin = fixupPluginRules(plugin); + + assert.notStrictEqual(plugin, fixedUpPlugin); + assert.deepStrictEqual( + Object.keys(plugin), + Object.keys(fixedUpPlugin), + ); + assert.strictEqual( + plugin.configs.recommended, + fixedUpPlugin.configs.recommended, + ); + }); + + it("should return the same fixed up plugin when applied to the same plugin multiple times", () => { + const plugin = { + configs: { + recommended: { + rules: { + "test-rule": "error", + }, + }, + }, + rules: { + "test-rule": { + create(context) { + return { + Identifier(node) { + context.report(node, "Identifier"); + }, + }; + }, + }, + }, + }; + + const fixedUpPlugin1 = fixupPluginRules(plugin); + const fixedUpPlugin2 = fixupPluginRules(plugin); + + assert.strictEqual(fixedUpPlugin1, fixedUpPlugin2); + }); + + it("should return the same fixed up plugin when a fixed up plugin is passed to fixupPlugin", () => { + const plugin = { + configs: { + recommended: { + rules: { + "test-rule": "error", + }, + }, + }, + rules: { + "test-rule": { + create(context) { + return { + Identifier(node) { + context.report(node, "Identifier"); + }, + }; + }, + }, + }, + }; + + const fixedUpPlugin = fixupPluginRules(plugin); + const fixedUpPlugin2 = fixupPluginRules(fixedUpPlugin); + + assert.strictEqual(fixedUpPlugin, fixedUpPlugin2); + }); + + it("should return the original plugin when it doesn't have rules", () => { + const plugin = { + configs: { + recommended: { + rules: { + "test-rule": "error", + }, + }, + }, + }; + + const fixedUpPlugin = fixupPluginRules(plugin); + + assert.strictEqual(plugin, fixedUpPlugin); + }); + + REPLACEMENT_METHODS.forEach(method => { + it(`should create a plugin where context.${method}() returns the same value as sourceCode.${method}(node)`, () => { + const plugin = { + configs: { + recommended: { + rules: { + "test-rule": "error", + }, + }, + }, + rules: { + "test-rule": { + create(context) { + const { sourceCode } = context; + + return { + Program(node) { + const result = context[method](); + const expected = + sourceCode[method](node); + assert.deepStrictEqual( + result, + expected, + ); + context.report(node, "Program"); + }, + FunctionDeclaration(node) { + const result = context[method](); + const expected = + sourceCode[method](node); + assert.deepStrictEqual( + result, + expected, + ); + context.report( + node, + "FunctionDeclaration", + ); + }, + ArrowFunctionExpression(node) { + const result = context[method](); + const expected = + sourceCode[method](node); + assert.deepStrictEqual( + result, + expected, + ); + context.report( + node, + "ArrowFunctionExpression", + ); + }, + Identifier(node) { + const result = context[method](); + const expected = + sourceCode[method](node); + assert.deepStrictEqual( + result, + expected, + ); + context.report(node, "Identifier"); + }, + }; + }, + }, + }, + }; + + const linter = new Linter(); + const code = + "var foo = () => 123; function bar() { return 123; }"; + const messages = linter.verify( + code, + { + plugins: { + test: fixupPluginRules(plugin), + }, + rules: { + "test/test-rule": "error", + }, + }, + { + filename: "test.js", + }, + ); + + assert.deepStrictEqual( + messages.map(message => message.message), + [ + "Program", + "Identifier", + "ArrowFunctionExpression", + "FunctionDeclaration", + "Identifier", + ], + ); + }); + }); + }); + + describe("fixupConfigRules()", () => { + it("should return an array with the same number of items and objects with the same properties", () => { + const config = [ + { + rules: { + "test-rule": "error", + }, + }, + { + plugins: { + foo: {}, + }, + rules: { + "foo/bar": "error", + }, + }, + ]; + + const fixedUpConfig = fixupConfigRules(config); + + assert.notStrictEqual(config, fixedUpConfig); + assert.deepStrictEqual(config, fixedUpConfig); + }); + + REPLACEMENT_METHODS.forEach(method => { + it(`should create a configuration where context.${method}() returns the same value as sourceCode.${method}(node)`, () => { + const config = [ + { + plugins: { + test: { + rules: { + "test-rule": fixupRule({ + create(context) { + const { sourceCode } = context; + + return { + Program(node) { + const result = + context[method](); + const expected = + sourceCode[method]( + node, + ); + assert.deepStrictEqual( + result, + expected, + ); + context.report( + node, + "Program", + ); + }, + FunctionDeclaration(node) { + const result = + context[method](); + const expected = + sourceCode[method]( + node, + ); + assert.deepStrictEqual( + result, + expected, + ); + context.report( + node, + "FunctionDeclaration", + ); + }, + ArrowFunctionExpression(node) { + const result = + context[method](); + const expected = + sourceCode[method]( + node, + ); + assert.deepStrictEqual( + result, + expected, + ); + context.report( + node, + "ArrowFunctionExpression", + ); + }, + Identifier(node) { + const result = + context[method](); + const expected = + sourceCode[method]( + node, + ); + assert.deepStrictEqual( + result, + expected, + ); + context.report( + node, + "Identifier", + ); + }, + }; + }, + }), + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }, + ]; + + const linter = new Linter(); + const code = + "var foo = () => 123; function bar() { return 123; }"; + const messages = linter.verify(code, fixupConfigRules(config), { + filename: "test.js", + }); + + assert.deepStrictEqual( + messages.map(message => message.message), + [ + "Program", + "Identifier", + "ArrowFunctionExpression", + "FunctionDeclaration", + "Identifier", + ], + ); + }); + }); + }); +}); diff --git a/packages/compat/tests/rules/consistent-this.js b/packages/compat/tests/rules/consistent-this.js new file mode 100644 index 0000000..5adc163 --- /dev/null +++ b/packages/compat/tests/rules/consistent-this.js @@ -0,0 +1,189 @@ +/** + * @fileoverview Tests for consistent-this rule. + * @author Raphael Pigulla + */ + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import rule from "../fixtures/rules/consistent-this.js"; +import { RuleTester } from "eslint"; +import { fixupRule } from "../../src/fixup-rules.js"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * A destructuring Test + * @param {string} code source code + * @returns {Object} Suitable object + * @private + */ +function destructuringTest(code) { + return { + code, + options: ["self"], + languageOptions: { ecmaVersion: 6 }, + }; +} + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 5, + sourceType: "script", + }, +}); + +const fixedUpRule = fixupRule(rule); + +ruleTester.run("consistent-this", fixedUpRule, { + valid: [ + "var foo = 42, that = this", + { code: "var foo = 42, self = this", options: ["self"] }, + { code: "var self = 42", options: ["that"] }, + { code: "var self", options: ["that"] }, + { code: "var self; self = this", options: ["self"] }, + { code: "var foo, self; self = this", options: ["self"] }, + { code: "var foo, self; foo = 42; self = this", options: ["self"] }, + { code: "self = 42", options: ["that"] }, + { code: "var foo = {}; foo.bar = this", options: ["self"] }, + { code: "var self = this; var vm = this;", options: ["self", "vm"] }, + destructuringTest("var {foo, bar} = this"), + destructuringTest("({foo, bar} = this)"), + destructuringTest("var [foo, bar] = this"), + destructuringTest("[foo, bar] = this"), + ], + invalid: [ + { + code: "var context = this", + errors: [ + { + messageId: "unexpectedAlias", + data: { name: "context" }, + type: "VariableDeclarator", + }, + ], + }, + { + code: "var that = this", + options: ["self"], + errors: [ + { + messageId: "unexpectedAlias", + data: { name: "that" }, + type: "VariableDeclarator", + }, + ], + }, + { + code: "var foo = 42, self = this", + options: ["that"], + errors: [ + { + messageId: "unexpectedAlias", + data: { name: "self" }, + type: "VariableDeclarator", + }, + ], + }, + { + code: "var self = 42", + options: ["self"], + errors: [ + { + messageId: "aliasNotAssignedToThis", + data: { name: "self" }, + type: "VariableDeclarator", + }, + ], + }, + { + code: "var self", + options: ["self"], + errors: [ + { + messageId: "aliasNotAssignedToThis", + data: { name: "self" }, + type: "VariableDeclarator", + }, + ], + }, + { + code: "var self; self = 42", + options: ["self"], + errors: [ + { + messageId: "aliasNotAssignedToThis", + data: { name: "self" }, + type: "VariableDeclarator", + }, + { + messageId: "aliasNotAssignedToThis", + data: { name: "self" }, + type: "AssignmentExpression", + }, + ], + }, + { + code: "context = this", + options: ["that"], + errors: [ + { + messageId: "unexpectedAlias", + data: { name: "context" }, + type: "AssignmentExpression", + }, + ], + }, + { + code: "that = this", + options: ["self"], + errors: [ + { + messageId: "unexpectedAlias", + data: { name: "that" }, + type: "AssignmentExpression", + }, + ], + }, + { + code: "self = this", + options: ["that"], + errors: [ + { + messageId: "unexpectedAlias", + data: { name: "self" }, + type: "AssignmentExpression", + }, + ], + }, + { + code: "self += this", + options: ["self"], + errors: [ + { + messageId: "aliasNotAssignedToThis", + data: { name: "self" }, + type: "AssignmentExpression", + }, + ], + }, + { + code: "var self; (function() { self = this; }())", + options: ["self"], + errors: [ + { + messageId: "aliasNotAssignedToThis", + data: { name: "self" }, + type: "VariableDeclarator", + }, + ], + }, + ], +}); diff --git a/packages/compat/tests/rules/global-require.js b/packages/compat/tests/rules/global-require.js new file mode 100644 index 0000000..e69d78d --- /dev/null +++ b/packages/compat/tests/rules/global-require.js @@ -0,0 +1,98 @@ +/** + * @fileoverview Tests for global-require + * @author Jamund Ferguson + */ + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import rule from "../fixtures/rules/global-require.js"; +import { RuleTester } from "eslint"; +import { fixupRule } from "../../src/fixup-rules.js"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 5, + sourceType: "script", + }, +}); + +const valid = [ + { code: "var x = require('y');" }, + { code: "if (x) { x.require('y'); }" }, + { code: "var x;\nx = require('y');" }, + { code: "var x = 1, y = require('y');" }, + { code: "var x = require('y'), y = require('y'), z = require('z');" }, + { code: "var x = require('y').foo;" }, + { code: "require('y').foo();" }, + { code: "require('y');" }, + { + code: "function x(){}\n\n\nx();\n\n\nif (x > y) {\n\tdoSomething()\n\n}\n\nvar x = require('y').foo;", + }, + { code: "var logger = require(DEBUG ? 'dev-logger' : 'logger');" }, + { code: "var logger = DEBUG ? require('dev-logger') : require('logger');" }, + { code: "function localScopedRequire(require) { require('y'); }" }, + { + code: "var someFunc = require('./someFunc'); someFunc(function(require) { return('bananas'); });", + }, + + // Optional chaining + { + code: "var x = require('y')?.foo;", + languageOptions: { ecmaVersion: 2020 }, + }, +]; + +const error = { messageId: "unexpected", type: "CallExpression" }; + +const invalid = [ + // block statements + { + code: "if (process.env.NODE_ENV === 'DEVELOPMENT') {\n\trequire('debug');\n}", + errors: [error], + }, + { + code: "var x; if (y) { x = require('debug'); }", + errors: [error], + }, + { + code: "var x; if (y) { x = require('debug').baz; }", + errors: [error], + }, + { + code: "function x() { require('y') }", + errors: [error], + }, + { + code: "try { require('x'); } catch (e) { console.log(e); }", + errors: [error], + }, + + // non-block statements + { + code: "var getModule = x => require(x);", + languageOptions: { ecmaVersion: 6 }, + errors: [error], + }, + { + code: "var x = (x => require(x))('weird')", + languageOptions: { ecmaVersion: 6 }, + errors: [error], + }, + { + code: "switch(x) { case '1': require('1'); break; }", + errors: [error], + }, +]; + +const fixedUpRule = fixupRule(rule); + +ruleTester.run("global-require", fixedUpRule, { + valid, + invalid, +}); diff --git a/packages/compat/tests/rules/handle-callback-err.js b/packages/compat/tests/rules/handle-callback-err.js new file mode 100644 index 0000000..c132884 --- /dev/null +++ b/packages/compat/tests/rules/handle-callback-err.js @@ -0,0 +1,176 @@ +/** + * @fileoverview Tests for missing-err rule. + * @author Jamund Ferguson + */ + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import rule from "../fixtures/rules/handle-callback-err.js"; +import { RuleTester } from "eslint"; +import { fixupRule } from "../../src/fixup-rules.js"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); + +const expectedFunctionDeclarationError = { + messageId: "expected", + type: "FunctionDeclaration", +}; +const expectedFunctionExpressionError = { + messageId: "expected", + type: "FunctionExpression", +}; +const fixedUpRule = fixupRule(rule); + +ruleTester.run("handle-callback-err", fixedUpRule, { + valid: [ + "function test(error) {}", + "function test(err) {console.log(err);}", + "function test(err, data) {if(err){ data = 'ERROR';}}", + "var test = function(err) {console.log(err);};", + "var test = function(err) {if(err){/* do nothing */}};", + "var test = function(err) {if(!err){doSomethingHere();}else{};}", + "var test = function(err, data) {if(!err) { good(); } else { bad(); }}", + "try { } catch(err) {}", + "getData(function(err, data) {if (err) {}getMoreDataWith(data, function(err, moreData) {if (err) {}getEvenMoreDataWith(moreData, function(err, allOfTheThings) {if (err) {}});});});", + "var test = function(err) {if(! err){doSomethingHere();}};", + "function test(err, data) {if (data) {doSomething(function(err) {console.error(err);});} else if (err) {console.log(err);}}", + "function handler(err, data) {if (data) {doSomethingWith(data);} else if (err) {console.log(err);}}", + "function handler(err) {logThisAction(function(err) {if (err) {}}); console.log(err);}", + "function userHandler(err) {process.nextTick(function() {if (err) {}})}", + "function help() { function userHandler(err) {function tester() { err; process.nextTick(function() { err; }); } } }", + "function help(done) { var err = new Error('error'); done(); }", + { code: "var test = err => err;", languageOptions: { ecmaVersion: 6 } }, + { + code: "var test = err => !err;", + languageOptions: { ecmaVersion: 6 }, + }, + { + code: "var test = err => err.message;", + languageOptions: { ecmaVersion: 6 }, + }, + { + code: "var test = function(error) {if(error){/* do nothing */}};", + options: ["error"], + }, + { + code: "var test = (error) => {if(error){/* do nothing */}};", + options: ["error"], + languageOptions: { ecmaVersion: 6 }, + }, + { + code: "var test = function(error) {if(! error){doSomethingHere();}};", + options: ["error"], + }, + { + code: "var test = function(err) { console.log(err); };", + options: ["^(err|error)$"], + }, + { + code: "var test = function(error) { console.log(error); };", + options: ["^(err|error)$"], + }, + { + code: "var test = function(anyError) { console.log(anyError); };", + options: ["^.+Error$"], + }, + { + code: "var test = function(any_error) { console.log(anyError); };", + options: ["^.+Error$"], + }, + { + code: "var test = function(any_error) { console.log(any_error); };", + options: ["^.+(e|E)rror$"], + }, + ], + invalid: [ + { + code: "function test(err) {}", + errors: [expectedFunctionDeclarationError], + }, + { + code: "function test(err, data) {}", + errors: [expectedFunctionDeclarationError], + }, + { + code: "function test(err) {errorLookingWord();}", + errors: [expectedFunctionDeclarationError], + }, + { + code: "function test(err) {try{} catch(err) {}}", + errors: [expectedFunctionDeclarationError], + }, + { + code: "function test(err, callback) { foo(function(err, callback) {}); }", + errors: [ + expectedFunctionDeclarationError, + expectedFunctionExpressionError, + ], + }, + { + code: "var test = (err) => {};", + languageOptions: { ecmaVersion: 6 }, + errors: [{ messageId: "expected" }], + }, + { + code: "var test = function(err) {};", + errors: [expectedFunctionExpressionError], + }, + { + code: "var test = function test(err, data) {};", + errors: [expectedFunctionExpressionError], + }, + { + code: "var test = function test(err) {/* if(err){} */};", + errors: [expectedFunctionExpressionError], + }, + { + code: "function test(err) {doSomethingHere(function(err){console.log(err);})}", + errors: [expectedFunctionDeclarationError], + }, + { + code: "function test(error) {}", + options: ["error"], + errors: [expectedFunctionDeclarationError], + }, + { + code: "getData(function(err, data) {getMoreDataWith(data, function(err, moreData) {if (err) {}getEvenMoreDataWith(moreData, function(err, allOfTheThings) {if (err) {}});}); });", + errors: [expectedFunctionExpressionError], + }, + { + code: "getData(function(err, data) {getMoreDataWith(data, function(err, moreData) {getEvenMoreDataWith(moreData, function(err, allOfTheThings) {if (err) {}});}); });", + errors: [ + expectedFunctionExpressionError, + expectedFunctionExpressionError, + ], + }, + { + code: "function userHandler(err) {logThisAction(function(err) {if (err) { console.log(err); } })}", + errors: [expectedFunctionDeclarationError], + }, + { + code: "function help() { function userHandler(err) {function tester(err) { err; process.nextTick(function() { err; }); } } }", + errors: [expectedFunctionDeclarationError], + }, + { + code: "var test = function(anyError) { console.log(otherError); };", + options: ["^.+Error$"], + errors: [expectedFunctionExpressionError], + }, + { + code: "var test = function(anyError) { };", + options: ["^.+Error$"], + errors: [expectedFunctionExpressionError], + }, + { + code: "var test = function(err) { console.log(error); };", + options: ["^(err|error)$"], + errors: [expectedFunctionExpressionError], + }, + ], +}); diff --git a/packages/compat/tests/rules/no-lone-blocks.js b/packages/compat/tests/rules/no-lone-blocks.js new file mode 100644 index 0000000..f93e94c --- /dev/null +++ b/packages/compat/tests/rules/no-lone-blocks.js @@ -0,0 +1,540 @@ +/** + * @fileoverview Tests for no-lone-blocks rule. + * @author Brandon Mills + */ + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import rule from "../fixtures/rules/no-lone-blocks.js"; +import { RuleTester } from "eslint"; +import { fixupRule } from "../../src/fixup-rules.js"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 5, + sourceType: "script", + }, +}); + +const fixedUpRule = fixupRule(rule); + +ruleTester.run("no-lone-blocks", fixedUpRule, { + valid: [ + "if (foo) { if (bar) { baz(); } }", + "do { bar(); } while (foo)", + "function foo() { while (bar) { baz() } }", + + // Block-level bindings + { code: "{ let x = 1; }", languageOptions: { ecmaVersion: 6 } }, + { code: "{ const x = 1; }", languageOptions: { ecmaVersion: 6 } }, + { + code: "'use strict'; { function bar() {} }", + languageOptions: { ecmaVersion: 6 }, + }, + { + code: "{ function bar() {} }", + languageOptions: { + ecmaVersion: 6, + parserOptions: { ecmaFeatures: { impliedStrict: true } }, + }, + }, + { code: "{ class Bar {} }", languageOptions: { ecmaVersion: 6 } }, + + { + code: "{ {let y = 1;} let x = 1; }", + languageOptions: { ecmaVersion: 6 }, + }, + ` + switch (foo) { + case bar: { + baz; + } + } + `, + ` + switch (foo) { + case bar: { + baz; + } + case qux: { + boop; + } + } + `, + ` + switch (foo) { + case bar: + { + baz; + } + } + `, + { + code: "function foo() { { const x = 4 } const x = 3 }", + languageOptions: { ecmaVersion: 6 }, + }, + + { + code: "class C { static {} }", + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: "class C { static { foo; } }", + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: "class C { static { if (foo) { block; } } }", + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: "class C { static { lbl: { block; } } }", + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: "class C { static { { let block; } something; } }", + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: "class C { static { something; { const block = 1; } } }", + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: "class C { static { { function block(){} } something; } }", + languageOptions: { ecmaVersion: 2022 }, + }, + { + code: "class C { static { something; { class block {} } } }", + languageOptions: { ecmaVersion: 2022 }, + }, + ], + invalid: [ + { + code: "{}", + errors: [ + { + messageId: "redundantBlock", + type: "BlockStatement", + }, + ], + }, + { + code: "{var x = 1;}", + errors: [ + { + messageId: "redundantBlock", + type: "BlockStatement", + }, + ], + }, + { + code: "foo(); {} bar();", + errors: [ + { + messageId: "redundantBlock", + type: "BlockStatement", + }, + ], + }, + { + code: "if (foo) { bar(); {} baz(); }", + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + }, + ], + }, + { + code: "{ \n{ } }", + errors: [ + { + messageId: "redundantBlock", + type: "BlockStatement", + line: 1, + }, + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 2, + }, + ], + }, + { + code: "function foo() { bar(); {} baz(); }", + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + }, + ], + }, + { + code: "while (foo) { {} }", + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + }, + ], + }, + + // Non-block-level bindings, even in ES6 + { + code: "{ function bar() {} }", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "redundantBlock", + type: "BlockStatement", + }, + ], + }, + { + code: "{var x = 1;}", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "redundantBlock", + type: "BlockStatement", + }, + ], + }, + + { + code: "{ \n{var x = 1;}\n let y = 2; } {let z = 1;}", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 2, + }, + ], + }, + { + code: "{ \n{let x = 1;}\n var y = 2; } {let z = 1;}", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "redundantBlock", + type: "BlockStatement", + line: 1, + }, + ], + }, + { + code: "{ \n{var x = 1;}\n var y = 2; }\n {var z = 1;}", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "redundantBlock", + type: "BlockStatement", + line: 1, + }, + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 2, + }, + { + messageId: "redundantBlock", + type: "BlockStatement", + line: 4, + }, + ], + }, + { + code: ` + switch (foo) { + case 1: + foo(); + { + bar; + } + } + `, + errors: [ + { + messageId: "redundantBlock", + type: "BlockStatement", + line: 5, + }, + ], + }, + { + code: ` + switch (foo) { + case 1: + { + bar; + } + foo(); + } + `, + errors: [ + { + messageId: "redundantBlock", + type: "BlockStatement", + line: 4, + }, + ], + }, + { + code: ` + function foo () { + { + const x = 4; + } + } + `, + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 3, + }, + ], + }, + { + code: ` + function foo () { + { + var x = 4; + } + } + `, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 3, + }, + ], + }, + { + code: ` + class C { + static { + if (foo) { + { + let block; + } + } + } + } + `, + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 5, + }, + ], + }, + { + code: ` + class C { + static { + if (foo) { + { + block; + } + something; + } + } + } + `, + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 5, + }, + ], + }, + { + code: ` + class C { + static { + { + block; + } + } + } + `, + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 4, + }, + ], + }, + { + code: ` + class C { + static { + { + let block; + } + } + } + `, + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 4, + }, + ], + }, + { + code: ` + class C { + static { + { + const block = 1; + } + } + } + `, + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 4, + }, + ], + }, + { + code: ` + class C { + static { + { + function block() {} + } + } + } + `, + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 4, + }, + ], + }, + { + code: ` + class C { + static { + { + class block {} + } + } + } + `, + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 4, + }, + ], + }, + { + code: ` + class C { + static { + { + var block; + } + something; + } + } + `, + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 4, + }, + ], + }, + { + code: ` + class C { + static { + something; + { + var block; + } + } + } + `, + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 5, + }, + ], + }, + { + code: ` + class C { + static { + { + block; + } + something; + } + } + `, + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 4, + }, + ], + }, + { + code: ` + class C { + static { + something; + { + block; + } + } + } + `, + languageOptions: { ecmaVersion: 2022 }, + errors: [ + { + messageId: "redundantNestedBlock", + type: "BlockStatement", + line: 5, + }, + ], + }, + ], +}); diff --git a/packages/compat/tests/rules/no-loop-func.js b/packages/compat/tests/rules/no-loop-func.js new file mode 100644 index 0000000..ad596c3 --- /dev/null +++ b/packages/compat/tests/rules/no-loop-func.js @@ -0,0 +1,411 @@ +/** + * @fileoverview Tests for no-loop-func rule. + * @author Ilya Volodin + */ + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import rule from "../fixtures/rules/no-loop-func.js"; +import { RuleTester } from "eslint"; +import { fixupRule } from "../../src/fixup-rules.js"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); +const fixedUpRule = fixupRule(rule); + +ruleTester.run("no-loop-func", fixedUpRule, { + valid: [ + "string = 'function a() {}';", + "for (var i=0; i x != i)) { } }", + languageOptions: { ecmaVersion: 6 }, + }, + { + code: "let a = 0; for (let i=0; i { (function() { a; }); }); }", + languageOptions: { ecmaVersion: 6 }, + }, + { + code: "var a = 0; for (let i=0; i {", + " result[letter] = score;", + " });", + "}", + "result.__default = 6;", + ].join("\n"), + languageOptions: { ecmaVersion: 6 }, + }, + { + code: [ + "while (true) {", + " (function() { a; });", + "}", + "let a;", + ].join("\n"), + languageOptions: { 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)) { } }", + languageOptions: { ecmaVersion: 6 }, + }, + ], + invalid: [ + { + code: "for (var i=0; i { i; }) }", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "unsafeRefs", + data: { varNames: "'i'" }, + type: "ArrowFunctionExpression", + }, + ], + }, + { + code: "for (var i=0; i < l; i++) { var a = function() { i; } }", + errors: [ + { + messageId: "unsafeRefs", + data: { varNames: "'i'" }, + type: "FunctionExpression", + }, + ], + }, + { + code: "for (var i=0; i < l; i++) { function a() { i; }; a(); }", + errors: [ + { + messageId: "unsafeRefs", + data: { varNames: "'i'" }, + type: "FunctionDeclaration", + }, + ], + }, + { + code: "for (var i=0; (function() { i; })(), i { (function() { a; }); }); } a = 1;", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "unsafeRefs", + data: { varNames: "'a'" }, + type: "ArrowFunctionExpression", + }, + ], + }, + { + code: "for (var i = 0; i < 10; ++i) { for (let x in xs.filter(x => x != i)) { } }", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "unsafeRefs", + data: { varNames: "'i'" }, + type: "ArrowFunctionExpression", + }, + ], + }, + { + code: "for (let x of xs) { let a; for (let y of ys) { a = 1; (function() { a; }); } }", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "unsafeRefs", + data: { varNames: "'a'" }, + type: "FunctionExpression", + }, + ], + }, + { + code: "for (var x of xs) { for (let y of ys) { (function() { x; }); } }", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "unsafeRefs", + data: { varNames: "'x'" }, + type: "FunctionExpression", + }, + ], + }, + { + code: "for (var x of xs) { (function() { x; }); }", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "unsafeRefs", + data: { varNames: "'x'" }, + type: "FunctionExpression", + }, + ], + }, + { + code: "var a; for (let x of xs) { a = 1; (function() { a; }); }", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "unsafeRefs", + data: { varNames: "'a'" }, + type: "FunctionExpression", + }, + ], + }, + { + code: "var a; for (let x of xs) { (function() { a; }); a = 1; }", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "unsafeRefs", + data: { varNames: "'a'" }, + type: "FunctionExpression", + }, + ], + }, + { + code: "let a; function foo() { a = 10; } for (let x of xs) { (function() { a; }); } foo();", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "unsafeRefs", + data: { varNames: "'a'" }, + type: "FunctionExpression", + }, + ], + }, + { + code: "let a; function foo() { a = 10; for (let x of xs) { (function() { a; }); } } foo();", + languageOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: "unsafeRefs", + data: { varNames: "'a'" }, + type: "FunctionExpression", + }, + ], + }, + ], +}); diff --git a/packages/compat/tests/rules/prefer-rest-params.js b/packages/compat/tests/rules/prefer-rest-params.js new file mode 100644 index 0000000..29b669f --- /dev/null +++ b/packages/compat/tests/rules/prefer-rest-params.js @@ -0,0 +1,53 @@ +/** + * @fileoverview Tests for prefer-rest-params rule. + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import rule from "../fixtures/rules/prefer-rest-params.js"; +import { RuleTester } from "eslint"; +import { fixupRule } from "../../src/fixup-rules.js"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + languageOptions: { ecmaVersion: 6, sourceType: "script" }, +}); +const fixedUpRule = fixupRule(rule); + +ruleTester.run("prefer-rest-params", fixedUpRule, { + valid: [ + "arguments;", + "function foo(arguments) { arguments; }", + "function foo() { var arguments; arguments; }", + "var foo = () => arguments;", // Arrows don't have "arguments"., + "function foo(...args) { args; }", + "function foo() { arguments.length; }", + "function foo() { arguments.callee; }", + ], + invalid: [ + { + code: "function foo() { arguments; }", + errors: [{ type: "Identifier", messageId: "preferRestParams" }], + }, + { + code: "function foo() { arguments[0]; }", + errors: [{ type: "Identifier", messageId: "preferRestParams" }], + }, + { + code: "function foo() { arguments[1]; }", + errors: [{ type: "Identifier", messageId: "preferRestParams" }], + }, + { + code: "function foo() { arguments[Symbol.iterator]; }", + errors: [{ type: "Identifier", messageId: "preferRestParams" }], + }, + ], +}); diff --git a/packages/compat/tests/rules/require-atomic-updates.js b/packages/compat/tests/rules/require-atomic-updates.js new file mode 100644 index 0000000..41fe32b --- /dev/null +++ b/packages/compat/tests/rules/require-atomic-updates.js @@ -0,0 +1,480 @@ +/** + * @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield` + * @author Teddy Katz + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import rule from "../fixtures/rules/require-atomic-updates.js"; +import { RuleTester } from "eslint"; +import { fixupRule } from "../../src/fixup-rules.js"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + languageOptions: { ecmaVersion: 2022, sourceType: "script" }, +}); +const fixedUpRule = fixupRule(rule); + +const VARIABLE_ERROR = { + messageId: "nonAtomicUpdate", + data: { value: "foo" }, + type: "AssignmentExpression", +}; + +const STATIC_PROPERTY_ERROR = { + messageId: "nonAtomicObjectUpdate", + data: { value: "foo.bar", object: "foo" }, + type: "AssignmentExpression", +}; + +const COMPUTED_PROPERTY_ERROR = { + messageId: "nonAtomicObjectUpdate", + data: { value: "foo[bar].baz", object: "foo" }, + type: "AssignmentExpression", +}; + +const PRIVATE_PROPERTY_ERROR = { + messageId: "nonAtomicObjectUpdate", + data: { value: "foo.#bar", object: "foo" }, + type: "AssignmentExpression", +}; + +ruleTester.run("require-atomic-updates", fixedUpRule, { + valid: [ + "let foo; async function x() { foo += bar; }", + "let foo; async function x() { foo = foo + bar; }", + "let foo; async function x() { foo = await bar + foo; }", + "async function x() { let foo; foo += await bar; }", + "let foo; async function x() { foo = (await result)(foo); }", + "let foo; async function x() { foo = bar(await something, foo) }", + "function* x() { let foo; foo += yield bar; }", + "const foo = {}; async function x() { foo.bar = await baz; }", + "const foo = []; async function x() { foo[x] += 1; }", + "let foo; function* x() { foo = bar + foo; }", + "async function x() { let foo; bar(() => baz += 1); foo += await amount; }", + "let foo; async function x() { foo = condition ? foo : await bar; }", + "async function x() { let foo; bar(() => { let foo; blah(foo); }); foo += await result; }", + "let foo; async function x() { foo = foo + 1; await bar; }", + "async function x() { foo += await bar; }", + + /* + * Ensure rule doesn't take exponential time in the number of branches + * (see https://github.com/eslint/eslint/issues/10893) + */ + ` + async function foo() { + if (1); + if (2); + if (3); + if (4); + if (5); + if (6); + if (7); + if (8); + if (9); + if (10); + if (11); + if (12); + if (13); + if (14); + if (15); + if (16); + if (17); + if (18); + if (19); + if (20); + } + `, + ` + async function foo() { + return [ + 1 ? a : b, + 2 ? a : b, + 3 ? a : b, + 4 ? a : b, + 5 ? a : b, + 6 ? a : b, + 7 ? a : b, + 8 ? a : b, + 9 ? a : b, + 10 ? a : b, + 11 ? a : b, + 12 ? a : b, + 13 ? a : b, + 14 ? a : b, + 15 ? a : b, + 16 ? a : b, + 17 ? a : b, + 18 ? a : b, + 19 ? a : b, + 20 ? a : b + ]; + } + `, + + // https://github.com/eslint/eslint/issues/11194 + ` + async function f() { + let records + records = await a.records + g(() => { records }) + } + `, + + // https://github.com/eslint/eslint/issues/11687 + ` + async function f() { + try { + this.foo = doSomething(); + } catch (e) { + this.foo = null; + await doElse(); + } + } + `, + + // https://github.com/eslint/eslint/issues/11723 + ` + async function f(foo) { + let bar = await get(foo.id); + bar.prop = foo.prop; + } + `, + ` + async function f(foo) { + let bar = await get(foo.id); + foo = bar.prop; + } + `, + ` + async function f() { + let foo = {} + let bar = await get(foo.id); + foo.prop = bar.prop; + } + `, + + // https://github.com/eslint/eslint/issues/11954 + ` + let count = 0 + let queue = [] + async function A(...args) { + count += 1 + await new Promise(resolve=>resolve()) + count -= 1 + return + } + `, + + // https://github.com/eslint/eslint/issues/14208 + ` + async function foo(e) { + } + + async function run() { + const input = []; + const props = []; + + for(const entry of input) { + const prop = props.find(a => a.id === entry.id) || null; + await foo(entry); + } + + for(const entry of input) { + const prop = props.find(a => a.id === entry.id) || null; + } + + for(const entry2 of input) { + const prop = props.find(a => a.id === entry2.id) || null; + } + } + `, + + ` + async function run() { + { + let entry; + await entry; + } + { + let entry; + () => entry; + + entry = 1; + } + } + `, + + ` + async function run() { + await a; + b = 1; + } + `, + + // allowProperties + { + code: ` + async function a(foo) { + if (foo.bar) { + foo.bar = await something; + } + } + `, + options: [{ allowProperties: true }], + }, + { + code: ` + function* g(foo) { + baz = foo.bar; + yield something; + foo.bar = 1; + } + `, + options: [{ allowProperties: true }], + }, + ], + + invalid: [ + { + code: "let foo; async function x() { foo += await amount; }", + errors: [{ messageId: "nonAtomicUpdate", data: { value: "foo" } }], + }, + { + code: "if (1); let foo; async function x() { foo += await amount; }", + errors: [{ messageId: "nonAtomicUpdate", data: { value: "foo" } }], + }, + { + code: "let foo; async function x() { while (condition) { foo += await amount; } }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo; async function x() { foo = foo + await amount; }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo; async function x() { foo = foo + (bar ? baz : await amount); }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo; async function x() { foo = foo + (bar ? await amount : baz); }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo; async function x() { foo = condition ? foo + await amount : somethingElse; }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo; async function x() { foo = (condition ? foo : await bar) + await bar; }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo; async function x() { foo += bar + await amount; }", + errors: [VARIABLE_ERROR], + }, + { + code: "async function x() { let foo; bar(() => foo); foo += await amount; }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo; function* x() { foo += yield baz }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo; async function x() { foo = bar(foo, await something) }", + errors: [VARIABLE_ERROR], + }, + { + code: "const foo = {}; async function x() { foo.bar += await baz }", + errors: [STATIC_PROPERTY_ERROR], + }, + { + code: "const foo = []; async function x() { foo[bar].baz += await result; }", + errors: [COMPUTED_PROPERTY_ERROR], + }, + { + code: "const foo = {}; class C { #bar; async wrap() { foo.#bar += await baz } }", + errors: [PRIVATE_PROPERTY_ERROR], + }, + { + code: "let foo; async function* x() { foo = (yield foo) + await bar; }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo; async function x() { foo = foo + await result(foo); }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo; async function x() { foo = await result(foo, await somethingElse); }", + errors: [VARIABLE_ERROR], + }, + { + code: "function* x() { let foo; yield async function y() { foo += await bar; } }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo; async function* x() { foo = await foo + (yield bar); }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo; async function x() { foo = bar + await foo; }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo = {}; async function x() { foo[bar].baz = await (foo.bar += await foo[bar].baz) }", + errors: [COMPUTED_PROPERTY_ERROR, STATIC_PROPERTY_ERROR], + }, + { + code: "let foo = ''; async function x() { foo += await bar; }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo = 0; async function x() { foo = (a ? b : foo) + await bar; if (baz); }", + errors: [VARIABLE_ERROR], + }, + { + code: "let foo = 0; async function x() { foo = (a ? b ? c ? d ? foo : e : f : g : h) + await bar; if (baz); }", + errors: [VARIABLE_ERROR], + }, + + // https://github.com/eslint/eslint/issues/11723 + { + code: ` + async function f(foo) { + let buz = await get(foo.id); + foo.bar = buz.bar; + } + `, + errors: [STATIC_PROPERTY_ERROR], + }, + + // https://github.com/eslint/eslint/issues/15076 + { + code: ` + async () => { + opts.spec = process.stdin; + try { + const { exit_code } = await run(opts); + process.exitCode = exit_code; + } catch (e) { + process.exitCode = 1; + } + }; + `, + languageOptions: { + sourceType: "commonjs", + globals: { process: "readonly" }, + }, + errors: [ + { + messageId: "nonAtomicObjectUpdate", + data: { value: "process.exitCode", object: "process" }, + type: "AssignmentExpression", + line: 6, + }, + { + messageId: "nonAtomicObjectUpdate", + data: { value: "process.exitCode", object: "process" }, + type: "AssignmentExpression", + line: 8, + }, + ], + }, + + // allowProperties + { + code: ` + async function a(foo) { + if (foo.bar) { + foo.bar = await something; + } + } + `, + errors: [STATIC_PROPERTY_ERROR], + }, + { + code: ` + function* g(foo) { + baz = foo.bar; + yield something; + foo.bar = 1; + } + `, + errors: [STATIC_PROPERTY_ERROR], + }, + { + code: ` + async function a(foo) { + if (foo.bar) { + foo.bar = await something; + } + } + `, + options: [{}], + errors: [STATIC_PROPERTY_ERROR], + }, + { + code: ` + function* g(foo) { + baz = foo.bar; + yield something; + foo.bar = 1; + } + `, + options: [{}], + errors: [STATIC_PROPERTY_ERROR], + }, + { + code: ` + async function a(foo) { + if (foo.bar) { + foo.bar = await something; + } + } + `, + options: [{ allowProperties: false }], + errors: [STATIC_PROPERTY_ERROR], + }, + { + code: ` + function* g(foo) { + baz = foo.bar; + yield something; + foo.bar = 1; + } + `, + options: [{ allowProperties: false }], + errors: [STATIC_PROPERTY_ERROR], + }, + { + code: ` + let foo; + async function a() { + if (foo) { + foo = await something; + } + } + `, + options: [{ allowProperties: true }], + errors: [VARIABLE_ERROR], + }, + { + code: ` + let foo; + function* g() { + baz = foo; + yield something; + foo = 1; + } + `, + options: [{ allowProperties: true }], + errors: [VARIABLE_ERROR], + }, + ], +}); diff --git a/packages/compat/tsconfig.cjs.json b/packages/compat/tsconfig.cjs.json new file mode 100644 index 0000000..9e63c30 --- /dev/null +++ b/packages/compat/tsconfig.cjs.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "files": ["dist/cjs/index.cjs"], + "compilerOptions": { + "outDir": "dist/cjs", + "moduleResolution": "Bundler", + "module": "Preserve" + }, +} diff --git a/packages/compat/tsconfig.esm.json b/packages/compat/tsconfig.esm.json new file mode 100644 index 0000000..0760586 --- /dev/null +++ b/packages/compat/tsconfig.esm.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "files": ["dist/esm/index.js"], +} diff --git a/packages/compat/tsconfig.json b/packages/compat/tsconfig.json new file mode 100644 index 0000000..3fa504c --- /dev/null +++ b/packages/compat/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": ["src/index.js"], + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "allowJs": true, + "checkJs": true, + "outDir": "dist/esm", + "target": "ES2022", + "moduleResolution": "NodeNext", + "module": "NodeNext" + } +} diff --git a/release-please-config.json b/release-please-config.json index c5210d7..b0b18ed 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -2,6 +2,16 @@ "plugins": ["node-workspace"], "bump-minor-pre-major": true, "packages": { + "packages/compat": { + "release-type": "node", + "extra-files": [ + { + "type": "json", + "path": "jsr.json", + "jsonpath": "$.version" + } + ] + }, "packages/object-schema": { "release-type": "node", "extra-files": [