diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..4a2f57108 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +.release-please-manifest.json @eslint/eslint-tsc +.github/workflows/release-please.yml @eslint/eslint-tsc +.github/workflows/manual-publish.yml @eslint/eslint-tsc \ No newline at end of file diff --git a/.github/renovate.json5 b/.github/renovate.json5 index d226b6334..bf48b2b56 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,34 +1,13 @@ { $schema: "https://docs.renovatebot.com/renovate-schema.json", - extends: [ - "config:recommended", - ":approveMajorUpdates", - ":semanticCommitScopeDisabled", - ], - ignorePresets: [":semanticPrefixFixDepsChoreOthers"], - labels: ["dependencies"], - - // Wait well over npm's three day window for any new package as a precaution against malicious publishes - // https://docs.npmjs.com/policies/unpublish/#packages-published-less-than-72-hours-ago - minimumReleaseAge: "7 days", - + extends: ["github>eslint/workflows//.github/renovate/eslint-base.json5"], packageRules: [ { - description: "Use the deps:actions label for github-action manager updates (this means Renovate's github-action manager).", - addLabels: ["deps:actions"], - matchManagers: ["github-actions"], - }, - { - description: "Use the deps:npm label for npm manager packages (this means Renovate's npm manager).", - addLabels: ["deps:npm"], - matchManagers: ["npm"], - }, - { - description: "Update ESLint packages together.", - groupName: "eslint", - matchPackagePrefixes: ["@eslint/"], - matchPackageNames: ["eslint", "eslint-config-eslint", "espree"], + description: "Update `eslint` in dependencies", + matchDepTypes: ["dependencies"], + matchPackageNames: ["eslint"], minimumReleaseAge: null, // Don't wait for these packages + rangeStrategy: "bump", }, ], } diff --git a/.github/workflows/bun-test.yml b/.github/workflows/bun-test.yml deleted file mode 100644 index 1de262fe6..000000000 --- a/.github/workflows/bun-test.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Bun CI - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - build: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [windows-latest, macOS-latest, ubuntu-latest] - bun: [latest] - - steps: - - uses: actions/checkout@v4 - - name: Use Bun ${{ matrix.bun }} ${{ matrix.os }} - uses: oven-sh/setup-bun@v2 - with: - bun-version: ${{ matrix.bun }} - - name: bun install, build, and test - run: | - bun install - bun run --bun build - bun run --bun test - env: - CI: true diff --git a/.github/workflows/ci-bun.yml b/.github/workflows/ci-bun.yml new file mode 100644 index 000000000..9806f0555 --- /dev/null +++ b/.github/workflows/ci-bun.yml @@ -0,0 +1,14 @@ +name: ci-bun + +on: + push: + branches: + - main + + pull_request: + branches: + - main + +jobs: + ci-bun: + uses: eslint/workflows/.github/workflows/ci-bun.yml@main diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d85142c21..1a206121b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,9 +13,9 @@ jobs: name: Verify Files runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: "lts/*" @@ -36,13 +36,13 @@ jobs: strategy: matrix: os: [windows-latest, macOS-latest, ubuntu-latest] - node-version: [18.x, 20.x, 22.x, 24.x] + node-version: [25.x, 24.x, 22.x, 20.x, "20.19.0"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} @@ -58,10 +58,10 @@ jobs: name: Test Types runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "lts/*" @@ -70,11 +70,24 @@ jobs: npm install npm run build + - name: Run TypeScript Compiler + run: npx tsc + - name: Test types (core) working-directory: packages/core run: | npm run test:types + - name: Test types (object-schema) + working-directory: packages/object-schema + run: | + npm run test:types + + - name: Test types (config-array) + working-directory: packages/config-array + run: | + npm run test:types + - name: Test types (config-helpers) working-directory: packages/config-helpers run: | @@ -89,9 +102,9 @@ jobs: name: Verify JSR Publish runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: "lts/*" @@ -102,3 +115,25 @@ jobs: run: | npm run build npm run test:jsr + + pnpm_test: + name: Test pnpm Type Support + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: pnpm/action-setup@v4 + with: + version: latest + + - uses: actions/setup-node@v6 + with: + node-version: "lts/*" + + - name: Install Packages + run: npm install + + - name: Run pnpm test + run: | + npm run build + npm run test:pnpm --ws --if-present diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index bdfe9cb4b..d20b470cd 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -1,3 +1,6 @@ +# IMPORTANT: Do not run this workflow because currently it doesn't work. +# Trusted publishing allows only one workflow file, and we've set it to release-please.yml + name: Manual Package Publish on: @@ -28,13 +31,17 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: lts/* registry-url: "https://registry.npmjs.org" + # npm 11.5.1 or later is required so update to latest to be sure + - name: Update npm + run: npm install -g npm@latest + - name: Install dependencies run: npm install @@ -61,8 +68,6 @@ jobs: - name: Publish to npm run: npm publish -w packages/${{ inputs.package }} --provenance - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - name: Publish to JSR run: | diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 9190217f3..730ec7479 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -31,16 +31,21 @@ jobs: echo "packages/plugin-kit--release_created" ${{ steps.release.outputs['packages/plugin-kit--release_created'] }} # Check to see if we need to do any releases and if so check out the repo - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 if: ${{ steps.release.outputs.releases_created == 'true' }} # Node.js release - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 if: ${{ steps.release.outputs.releases_created == 'true' }} with: node-version: lts/* registry-url: "https://registry.npmjs.org" + # npm 11.5.1 or later is required so update to latest to be sure + - name: Update npm + run: npm install -g npm@latest + if: ${{ steps.release.outputs.releases_created == 'true' }} + - run: | npm install npm run build @@ -59,7 +64,6 @@ jobs: env: STEPS_RELEASE_OUTPUTS: ${{ toJson(steps.release.outputs) }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} TWITTER_API_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} TWITTER_API_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..fe232e62b --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,15 @@ +name: stale + +on: + schedule: + - cron: "31 22 * * *" # Runs every day at 10:31 PM UTC + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + uses: eslint/workflows/.github/workflows/stale.yml@main + secrets: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml index bd20161ad..96d098665 100644 --- a/.github/workflows/update-readme.yml +++ b/.github/workflows/update-readme.yml @@ -1,34 +1,13 @@ -name: Data Fetch +name: update-readme on: schedule: - - cron: "0 8 * * *" # Every day at 1am PDT + - cron: "0 8 * * *" # Runs every day at 08:00 AM UTC + workflow_dispatch: jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Check out repo - uses: actions/checkout@v4 - with: - token: ${{ secrets.WORKFLOW_PUSH_BOT_TOKEN }} - - - name: Set up Node.js - uses: actions/setup-node@v4 - - - name: Install npm packages - run: npm install --force - - - name: Update README with latest team and sponsor data - run: npm run build:readme - - - name: Setup Git - run: | - git config user.name "GitHub Actions Bot" - git config user.email "" - - - name: Save updated files - run: | - chmod +x ./tools/commit-readme.sh - ./tools/commit-readme.sh + update-readme: + uses: eslint/workflows/.github/workflows/update-readme.yml@main + secrets: + workflow_push_bot_token: ${{ secrets.WORKFLOW_PUSH_BOT_TOKEN }} diff --git a/.gitignore b/.gitignore index bfadbe843..a0ed0cff0 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,9 @@ yarn.lock .vscode *.code-workspace bun.lockb + +# Automatically generated files by GitHub Actions workflow +/.shared-workflows + +# pnpm +pnpm-lock.yaml diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 97a3bb780..d488d19c4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,10 +1,10 @@ { - "packages/compat": "1.3.1", - "packages/config-array": "0.21.0", - "packages/config-helpers": "0.3.0", - "packages/core": "0.15.1", - "packages/mcp": "0.1.1", - "packages/migrate-config": "1.5.2", - "packages/object-schema": "2.1.6", - "packages/plugin-kit": "0.3.4" + "packages/compat": "2.0.0", + "packages/config-array": "0.22.0", + "packages/config-helpers": "0.5.0", + "packages/core": "1.0.0", + "packages/mcp": "0.2.0", + "packages/migrate-config": "2.0.0", + "packages/object-schema": "3.0.0", + "packages/plugin-kit": "0.5.0" } diff --git a/README.md b/README.md index 063321046..b96ce6202 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

Qlty Software trunk.io Shopify

Silver Sponsors

-

Vite Liftoff American Express StackBlitz

Bronze Sponsors

-

Cybozu Sentry Anagram Solver Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

+

Qlty Software Shopify

Silver Sponsors

+

Vite Liftoff American Express StackBlitz

Bronze Sponsors

+

Cybozu Syntax Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

diff --git a/package.json b/package.json index 78cafa698..c8b66d7fb 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,14 @@ "scripts": { "test": "npm test --workspaces --if-present", "build": "node scripts/build.js", - "build:readme": "node tools/update-readme.js", "build:new-pkg": "node tools/new-pkg.js", "prepare": "npm run build", - "lint": "eslint .", - "lint:fix": "eslint --fix .", + "lint": "eslint", + "lint:fix": "eslint --fix", "fmt": "prettier --write .", "fmt:check": "prettier --check .", "test:jsr": "npm run test:jsr --workspaces --if-present", - "test:types": "tsc" + "test:types": "tsc && npm run test:types --workspaces --if-present" }, "workspaces": [ "packages/*" @@ -30,21 +29,25 @@ "!(*.{js,ts})": "prettier --write --ignore-unknown" }, "engines": { - "node": ">= 22.3.0" + "node": "^22.13.0 || >=24" }, "devDependencies": { "@eslint/config-helpers": "file:packages/config-helpers", "@types/mocha": "^10.0.7", "c8": "^10.1.3", - "eslint": "^9.27.0", - "eslint-config-eslint": "^11.0.0", - "got": "^14.4.1", + "eslint": "^9.35.0", + "eslint-config-eslint": "^13.0.0", "lint-staged": "^15.2.0", "mocha": "^11.5.0", "prettier": "^3.4.1", - "rollup": "^4.42.0", + "rollup": "^4.52.3", "typescript": "^5.8.3", "typescript-eslint": "^8.0.0", "yorkie": "^2.0.0" + }, + "overrides": { + "eslint": { + "@eslint/core": "file:packages/core" + } } } diff --git a/packages/compat/CHANGELOG.md b/packages/compat/CHANGELOG.md index a07872631..488adf4aa 100644 --- a/packages/compat/CHANGELOG.md +++ b/packages/compat/CHANGELOG.md @@ -1,5 +1,56 @@ # Changelog +## [2.0.0](https://github.com/eslint/rewrite/compare/compat-v1.4.1...compat-v2.0.0) (2025-11-14) + + +### ⚠ BREAKING CHANGES + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) + +### Features + +* patch missing context and SourceCode methods for v10 ([#311](https://github.com/eslint/rewrite/issues/311)) ([a40d8c6](https://github.com/eslint/rewrite/commit/a40d8c60af5bc09ea5e1c778655312a34ddc9f83)) +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) ([acc623c](https://github.com/eslint/rewrite/commit/acc623c807bf8237a26b18291f04dd99e4e4981a)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/core bumped from ^0.17.0 to ^1.0.0 + +## [1.4.1](https://github.com/eslint/rewrite/compare/compat-v1.4.0...compat-v1.4.1) (2025-10-27) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/core bumped from ^0.16.0 to ^0.17.0 + +## [1.4.0](https://github.com/eslint/rewrite/compare/compat-v1.3.2...compat-v1.4.0) (2025-09-16) + + +### Features + +* Add config types in @eslint/core ([#237](https://github.com/eslint/rewrite/issues/237)) ([7b6dd37](https://github.com/eslint/rewrite/commit/7b6dd370a598ea7fc94fba427a2579342b50b90f)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/core bumped from ^0.15.2 to ^0.16.0 + +## [1.3.2](https://github.com/eslint/rewrite/compare/compat-v1.3.1...compat-v1.3.2) (2025-08-05) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @eslint/core bumped from ^0.15.1 to ^0.15.2 + ## [1.3.1](https://github.com/eslint/rewrite/compare/compat-v1.3.0...compat-v1.3.1) (2025-06-25) diff --git a/packages/compat/README.md b/packages/compat/README.md index 90a68d9a0..c493dfe10 100644 --- a/packages/compat/README.md +++ b/packages/compat/README.md @@ -2,9 +2,9 @@ ## 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. +This package contains functions that allow you to wrap existing ESLint rules, plugins, and configurations that were intended for use with ESLint v8.x or v9.x to allow them to work as-is in ESLint v9.x and v10.x. -**Note:** All plugins are not guaranteed to work in ESLint v9.x. This package fixes the most common issues but can't fix everything. +**Note:** All plugins are not guaranteed to work in ESLint v9.x or v10.x. This package fixes the most common issues but can't fix everything. ## Installation @@ -37,7 +37,7 @@ This package exports the following functions in both ESM and CommonJS format: ### 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: +If you have a rule that you'd like to make compatible with ESLint v9.x or v10.x, you can do so using the `fixupRule()` function: ```js // ESM example @@ -71,14 +71,15 @@ 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: +If you are using a plugin in your `eslint.config.js` that is not yet compatible with ESLint v9.x or v10.x, you can wrap it using the `fixupPluginRules()` function: ```js // eslint.config.js - ESM example +import { defineConfig } from "eslint/config"; import { fixupPluginRules } from "@eslint/compat"; import somePlugin from "eslint-plugin-some-plugin"; -export default [ +export default defineConfig([ { plugins: { // insert the fixed plugin instead of the original @@ -88,17 +89,18 @@ export default [ "somePlugin/rule-name": "error", }, }, -]; +]); ``` Or in CommonJS: ```js // eslint.config.js - CommonJS example +const { defineConfig } = require("eslint/config"); const { fixupPluginRules } = require("@eslint/compat"); const somePlugin = require("eslint-plugin-some-plugin"); -module.exports = [ +module.exports = defineConfig([ { plugins: { // insert the fixed plugin instead of the original @@ -108,39 +110,41 @@ module.exports = [ "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 or a single object using the `fixupConfigRules()` function: +If you are importing other configs into your `eslint.config.js` that use plugins that are not yet compatible with ESLint v9.x or v10.x, you can wrap the entire array or a single object using the `fixupConfigRules()` function: ```js // eslint.config.js - ESM example +import { defineConfig } from "eslint/config"; import { fixupConfigRules } from "@eslint/compat"; import someConfig from "eslint-config-some-config"; -export default [ +export default defineConfig([ ...fixupConfigRules(someConfig), { // your overrides }, -]; +]); ``` Or in CommonJS: ```js // eslint.config.js - CommonJS example +const { defineConfig } = require("eslint/config"); const { fixupConfigRules } = require("@eslint/compat"); const someConfig = require("eslint-config-some-config"); -module.exports = [ +module.exports = defineConfig([ ...fixupConfigRules(someConfig), { // your overrides }, -]; +]); ``` ### Including Ignore Files @@ -151,6 +155,7 @@ The `includeIgnoreFile()` function also accepts a second optional `name` paramet ```js // eslint.config.js - ESM example +import { defineConfig } from "eslint/config"; import { includeIgnoreFile } from "@eslint/compat"; import path from "node:path"; import { fileURLToPath } from "node:url"; @@ -159,28 +164,29 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const gitignorePath = path.resolve(__dirname, ".gitignore"); -export default [ +export default defineConfig([ includeIgnoreFile(gitignorePath, "Imported .gitignore patterns"), // second argument is optional. { // your overrides }, -]; +]); ``` Or in CommonJS: ```js // eslint.config.js - CommonJS example +const { defineConfig } = require("eslint/config"); const { includeIgnoreFile } = require("@eslint/compat"); const path = require("node:path"); const gitignorePath = path.resolve(__dirname, ".gitignore"); -module.exports = [ +module.exports = defineConfig([ includeIgnoreFile(gitignorePath, "Imported .gitignore patterns"), // second argument is optional. { // your overrides }, -]; +]); ``` **Limitation:** This works without modification when the ignore file is in the same directory as your config file. If the ignore file is in a different directory, you may need to modify the patterns manually. @@ -199,9 +205,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

Qlty Software trunk.io Shopify

Silver Sponsors

-

Vite Liftoff American Express StackBlitz

Bronze Sponsors

-

Cybozu Sentry Anagram Solver Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

+

Qlty Software Shopify

Silver Sponsors

+

Vite Liftoff American Express StackBlitz

Bronze Sponsors

+

Cybozu Syntax Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

diff --git a/packages/compat/jsr.json b/packages/compat/jsr.json index 27007e2dc..fd8dea790 100644 --- a/packages/compat/jsr.json +++ b/packages/compat/jsr.json @@ -1,6 +1,6 @@ { "name": "@eslint/compat", - "version": "1.3.1", + "version": "2.0.0", "exports": "./dist/esm/index.js", "publish": { "include": [ diff --git a/packages/compat/package.json b/packages/compat/package.json index 1f0479bd0..9a81d961f 100644 --- a/packages/compat/package.json +++ b/packages/compat/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/compat", - "version": "1.3.1", + "version": "2.0.0", "description": "Compatibility utilities for ESLint", "type": "module", "main": "dist/esm/index.js", @@ -28,7 +28,7 @@ "build:cts": "node ../../tools/build-cts.js dist/esm/index.d.ts dist/cjs/index.d.cts", "build": "rollup -c && tsc -p tsconfig.esm.json && npm run build:cts", "test:jsr": "npx jsr@latest publish --dry-run", - "test": "mocha tests/*.js", + "test": "mocha \"tests/**/*.test.js\"", "test:coverage": "c8 npm test" }, "repository": { @@ -48,8 +48,11 @@ "url": "https://github.com/eslint/rewrite/issues" }, "homepage": "https://github.com/eslint/rewrite/tree/main/packages/compat#readme", + "dependencies": { + "@eslint/core": "^1.0.0" + }, "devDependencies": { - "@eslint/core": "^0.15.1", + "@types/node": "^24.7.2", "eslint": "^9.27.0" }, "peerDependencies": { @@ -61,6 +64,6 @@ } }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } } diff --git a/packages/compat/src/fixup-rules.js b/packages/compat/src/fixup-rules.js index 34752d56a..e893f52e7 100644 --- a/packages/compat/src/fixup-rules.js +++ b/packages/compat/src/fixup-rules.js @@ -1,5 +1,5 @@ /** - * @filedescription Functions to fix up rules to provide missing methods on the `context` object. + * @fileoverview Functions to fix up rules to provide missing methods on the `context` and `sourceCode` objects. * @author Nicholas C. Zakas */ @@ -7,10 +7,10 @@ // Types //----------------------------------------------------------------------------- -/** @typedef {import("eslint").ESLint.Plugin} FixupPluginDefinition */ -/** @typedef {import("eslint").Rule.RuleModule} FixupRuleDefinition */ +/** @typedef {import("@eslint/core").Plugin} FixupPluginDefinition */ +/** @typedef {import("@eslint/core").RuleDefinition} FixupRuleDefinition */ /** @typedef {FixupRuleDefinition["create"]} FixupLegacyRuleDefinition */ -/** @typedef {import("eslint").Linter.Config} FixupConfig */ +/** @typedef {import("@eslint/core").ConfigObject} FixupConfig */ /** @typedef {Array} FixupConfigArray */ //----------------------------------------------------------------------------- @@ -70,13 +70,67 @@ const fixedUpPluginReplacements = new WeakMap(); */ const fixedUpPlugins = new WeakSet(); +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +/** + * Determines if two nodes or tokens overlap. + * @param {object} first The first node or token to check. + * @param {object} second The second node or token to check. + * @returns {boolean} True if the two nodes or tokens overlap. + */ +function nodesOrTokensOverlap(first, second) { + return ( + (first.range[0] <= second.range[0] && + first.range[1] >= second.range[0]) || + (second.range[0] <= first.range[0] && second.range[1] >= first.range[0]) + ); +} + +/** + * Checks whether a node is an export declaration. + * @param {object} node An AST node. + * @returns {boolean} True if the node is an export declaration. + */ +function looksLikeExport(node) { + return ( + node.type === "ExportDefaultDeclaration" || + node.type === "ExportNamedDeclaration" + ); +} + +/** + * Checks for the presence of a JSDoc comment for the given node and returns it. + * @param {object} node The AST node to get the comment for. + * @param {object} sourceCode A SourceCode instance to get comments. + * @returns {object|null} The Block comment token containing the JSDoc comment + * for the given node or null if not found. + */ +function findJSDocComment(node, sourceCode) { + const tokenBefore = sourceCode.getTokenBefore(node, { + includeComments: true, + }); + + if ( + tokenBefore && + tokenBefore.type === "Block" && + tokenBefore.value.charAt(0) === "*" && + node.loc.start.line - tokenBefore.loc.end.line <= 1 + ) { + return tokenBefore; + } + + return null; +} + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- /** * Takes the given rule and creates a new rule with the `create()` method wrapped - * to provide the missing methods on the `context` object. + * to provide missing methods on the `context` and `sourceCode` objects. * @param {FixupRuleDefinition|FixupLegacyRuleDefinition} ruleDefinition The rule to fix up. * @returns {FixupRuleDefinition} The fixed-up rule. */ @@ -98,52 +152,193 @@ export function fixupRule(ruleDefinition) { : ruleDefinition.create.bind(ruleDefinition); function ruleCreate(context) { - // if getScope is already there then no need to create old methods + const sourceCode = context.sourceCode; + + // No need to create old methods for ESLint < 9 if ("getScope" in context) { return originalCreate(context); } - const sourceCode = context.sourceCode; - let currentNode = sourceCode.ast; + let eslintVersion = 9; + if (!("getCwd" in context)) { + eslintVersion = 10; + } + + let compatSourceCode = sourceCode; + if (eslintVersion >= 10) { + compatSourceCode = Object.assign(Object.create(sourceCode), { + getTokenOrCommentBefore(node, skip) { + return sourceCode.getTokenBefore(node, { + includeComments: true, + skip, + }); + }, + getTokenOrCommentAfter(node, skip) { + return sourceCode.getTokenAfter(node, { + includeComments: true, + skip, + }); + }, + isSpaceBetweenTokens(first, second) { + if (nodesOrTokensOverlap(first, second)) { + return false; + } + + const [startingNodeOrToken, endingNodeOrToken] = + first.range[1] <= second.range[0] + ? [first, second] + : [second, first]; + const firstToken = + sourceCode.getLastToken(startingNodeOrToken) || + startingNodeOrToken; + const finalToken = + sourceCode.getFirstToken(endingNodeOrToken) || + endingNodeOrToken; + let currentToken = firstToken; + + while (currentToken !== finalToken) { + const nextToken = sourceCode.getTokenAfter( + currentToken, + { + includeComments: true, + }, + ); + + if ( + currentToken.range[1] !== nextToken.range[0] || + (nextToken !== finalToken && + nextToken.type === "JSXText" && + /\s/u.test(nextToken.value)) + ) { + return true; + } + + currentToken = nextToken; + } + + return false; + }, + getJSDocComment(node) { + let parent = node.parent; + + switch (node.type) { + case "ClassDeclaration": + case "FunctionDeclaration": + return findJSDocComment( + looksLikeExport(parent) ? parent : node, + sourceCode, + ); + + case "ClassExpression": + return findJSDocComment(parent.parent, sourceCode); + + case "ArrowFunctionExpression": + case "FunctionExpression": + if ( + parent.type !== "CallExpression" && + parent.type !== "NewExpression" + ) { + while ( + !sourceCode.getCommentsBefore(parent) + .length && + !/Function/u.test(parent.type) && + parent.type !== "MethodDefinition" && + parent.type !== "Property" + ) { + parent = parent.parent; + + if (!parent) { + break; + } + } + + if ( + parent && + parent.type !== "FunctionDeclaration" && + parent.type !== "Program" + ) { + return findJSDocComment(parent, sourceCode); + } + } + + return findJSDocComment(node, sourceCode); + + default: + return null; + } + }, + }); + + Object.freeze(compatSourceCode); + } - const newContext = Object.assign(Object.create(context), { - parserServices: sourceCode.parserServices, + let currentNode = compatSourceCode.ast; + + const compatContext = Object.assign(Object.create(context), { + parserServices: compatSourceCode.parserServices, /* * The following methods rely on the current node in the traversal, * so we need to add them manually. */ getScope() { - return sourceCode.getScope(currentNode); + return compatSourceCode.getScope(currentNode); }, getAncestors() { - return sourceCode.getAncestors(currentNode); + return compatSourceCode.getAncestors(currentNode); }, markVariableAsUsed(variable) { - sourceCode.markVariableAsUsed(variable, currentNode); + compatSourceCode.markVariableAsUsed(variable, currentNode); }, }); + if (eslintVersion >= 10) { + Object.assign(compatContext, { + parserOptions: compatContext.languageOptions.parserOptions, + + getCwd() { + return compatContext.cwd; + }, + + getFilename() { + return compatContext.filename; + }, + + getPhysicalFilename() { + return compatContext.physicalFilename; + }, + + getSourceCode() { + return compatSourceCode; + }, + }); + + Object.defineProperty(compatContext, "sourceCode", { + enumerable: true, + value: compatSourceCode, + }); + } + // add passthrough methods for (const [ contextMethodName, sourceCodeMethodName, ] of removedMethodNames) { - newContext[contextMethodName] = - sourceCode[sourceCodeMethodName].bind(sourceCode); + compatContext[contextMethodName] = + compatSourceCode[sourceCodeMethodName].bind(compatSourceCode); } // freeze just like the original context - Object.freeze(newContext); + Object.freeze(compatContext); /* * 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); + const visitor = originalCreate(compatContext); /* * Wrap each method in the visitor object to update the currentNode @@ -184,7 +379,7 @@ export function fixupRule(ruleDefinition) { }; // copy `schema` property of function-style rule or top-level `schema` property of object-style rule into `meta` object - // @ts-ignore -- top-level `schema` property was not offically supported for object-style rules so it doesn't exist in types + // @ts-ignore -- top-level `schema` property was not officially supported for object-style rules so it doesn't exist in types const { schema } = ruleDefinition; if (schema) { if (!newRuleDefinition.meta) { @@ -207,7 +402,7 @@ export function fixupRule(ruleDefinition) { /** * Takes the given plugin and creates a new plugin with all of the rules wrapped - * to provide the missing methods on the `context` object. + * to provide missing methods on the `context` and `sourceCode` objects. * @param {FixupPluginDefinition} plugin The plugin to fix up. * @returns {FixupPluginDefinition} The fixed-up plugin. */ @@ -244,7 +439,7 @@ export function fixupPluginRules(plugin) { /** * Takes the given configuration and creates a new configuration with all of the - * rules wrapped to provide the missing methods on the `context` object. + * rules wrapped to provide missing methods on the `context` and `sourceCode` objects. * @param {FixupConfigArray|FixupConfig} config The configuration to fix up. * @returns {FixupConfigArray} The fixed-up configuration. */ diff --git a/packages/compat/src/ignore-file.js b/packages/compat/src/ignore-file.js index c5f996bf0..a3f3b4474 100644 --- a/packages/compat/src/ignore-file.js +++ b/packages/compat/src/ignore-file.js @@ -14,7 +14,7 @@ import path from "node:path"; // Types //----------------------------------------------------------------------------- -/** @typedef {import("eslint").Linter.Config} FlatConfig */ +/** @typedef {import("@eslint/core").ConfigObject} FlatConfig */ //----------------------------------------------------------------------------- // Exports @@ -56,6 +56,7 @@ export function convertIgnorePatternToMinimatch(pattern) { */ const escapedPatternWithoutLeadingSlash = patternWithoutLeadingSlash.replaceAll( + // eslint-disable-next-line regexp/no-empty-lookarounds-assertion -- False positive /(?=((?:\\.|[^{(])*))\1([{(])/guy, "$1\\$2", ); diff --git a/packages/compat/tests/fixup-rules.js b/packages/compat/tests/fixup-rules.test.js similarity index 69% rename from packages/compat/tests/fixup-rules.js rename to packages/compat/tests/fixup-rules.test.js index 8f4b92bad..23260cd7d 100644 --- a/packages/compat/tests/fixup-rules.js +++ b/packages/compat/tests/fixup-rules.test.js @@ -24,7 +24,7 @@ const REPLACEMENT_METHODS = ["getScope", "getAncestors"]; // Tests //----------------------------------------------------------------------------- -describe("@eslint/backcompat", () => { +describe("@eslint/compat", () => { describe("fixupRule()", () => { it("should return a new rule object with the same own properties", () => { const rule = { @@ -493,6 +493,319 @@ describe("@eslint/backcompat", () => { ); }); }); + + it("should restore context.parserOptions", () => { + const rule = { + create(context) { + assert.deepStrictEqual( + context.parserOptions, + context.languageOptions.parserOptions, + ); + + return { + Identifier(node) { + context.report(node, "Identifier"); + }, + }; + }, + }; + + const config = { + plugins: { + test: { + rules: { + "test-rule": fixupRule(rule), + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }; + + const linter = new Linter(); + const code = "let foo;"; + const messages = linter.verify(code, config); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Identifier"); + }); + + it("should restore context.getCwd()", () => { + const rule = { + create(context) { + assert.strictEqual(context.getCwd(), context.cwd); + + return { + Identifier(node) { + context.report(node, "Identifier"); + }, + }; + }, + }; + + const config = { + plugins: { + test: { + rules: { + "test-rule": fixupRule(rule), + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }; + + const linter = new Linter(); + const code = "let foo;"; + const messages = linter.verify(code, config); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Identifier"); + }); + + it("should restore context.getFilename()", () => { + const rule = { + create(context) { + assert.strictEqual(context.getFilename(), context.filename); + + return { + Identifier(node) { + context.report(node, "Identifier"); + }, + }; + }, + }; + + const config = { + plugins: { + test: { + rules: { + "test-rule": fixupRule(rule), + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }; + + const linter = new Linter(); + const code = "let foo;"; + const messages = linter.verify(code, config); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Identifier"); + }); + + it("should restore context.getPhysicalFilename()", () => { + const rule = { + create(context) { + assert.strictEqual( + context.getPhysicalFilename(), + context.physicalFilename, + ); + + return { + Identifier(node) { + context.report(node, "Identifier"); + }, + }; + }, + }; + + const config = { + plugins: { + test: { + rules: { + "test-rule": fixupRule(rule), + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }; + + const linter = new Linter(); + const code = "let foo;"; + const messages = linter.verify(code, config); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Identifier"); + }); + + it("should restore context.getSourceCode()", () => { + const rule = { + create(context) { + assert.strictEqual( + context.getSourceCode(), + context.sourceCode, + ); + + return { + Identifier(node) { + context.report(node, "Identifier"); + }, + }; + }, + }; + + const config = { + plugins: { + test: { + rules: { + "test-rule": fixupRule(rule), + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }; + + const linter = new Linter(); + const code = "let foo;"; + const messages = linter.verify(code, config); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Identifier"); + }); + + it("should restore sourceCode.getTokenOrCommentBefore()", () => { + const rule = { + create(context) { + return { + Identifier(node) { + assert.strictEqual( + context.sourceCode.getTokenOrCommentBefore(node) + .value, + "let", + ); + }, + }; + }, + }; + + const config = { + plugins: { + test: { + rules: { + "test-rule": fixupRule(rule), + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }; + + const linter = new Linter(); + const code = "let foo;"; + linter.verify(code, config); + }); + + it("should restore sourceCode.getTokenOrCommentAfter()", () => { + const rule = { + create(context) { + return { + Identifier(node) { + assert.strictEqual( + context.sourceCode.getTokenOrCommentAfter(node) + .value, + "=", + ); + }, + }; + }, + }; + + const config = { + plugins: { + test: { + rules: { + "test-rule": fixupRule(rule), + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }; + + const linter = new Linter(); + const code = "let foo = 0;"; + linter.verify(code, config); + }); + + it("should restore sourceCode.isSpaceBetweenTokens()", () => { + const rule = { + create(context) { + return { + Identifier(node) { + assert.strictEqual( + context.sourceCode.isSpaceBetweenTokens( + node, + context.sourceCode.getTokenBefore(node), + ), + true, + ); + }, + }; + }, + }; + + const config = { + plugins: { + test: { + rules: { + "test-rule": fixupRule(rule), + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }; + + const linter = new Linter(); + const code = "let foo"; + linter.verify(code, config); + }); + + it("should restore sourceCode.getJSDocComment()", () => { + const rule = { + create(context) { + return { + FunctionDeclaration(node) { + const jsdoc = + context.sourceCode.getJSDocComment(node); + + assert.strictEqual(jsdoc.type, "Block"); + assert.strictEqual(jsdoc.value, "* Desc"); + }, + }; + }, + }; + + const config = { + plugins: { + test: { + rules: { + "test-rule": fixupRule(rule), + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }; + + const linter = new Linter(); + const code = ["/** Desc*/", "function foo(){}"].join("\n"); + linter.verify(code, config); + }); }); describe("fixupPluginRules()", () => { @@ -701,6 +1014,80 @@ describe("@eslint/backcompat", () => { ); }); }); + + it("should restore context.getFilename()", () => { + const plugin = { + configs: { + recommended: { + rules: { + "test-rule": "error", + }, + }, + }, + rules: { + "test-rule": { + create(context) { + assert.strictEqual( + context.getFilename(), + context.filename, + ); + return { + Identifier(node) { + context.report(node, "Identifier"); + }, + }; + }, + }, + }, + }; + + const linter = new Linter(); + const code = "let foo;"; + const config = { + plugins: { test: fixupPluginRules(plugin) }, + rules: { "test/test-rule": "error" }, + }; + const messages = linter.verify(code, config); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Identifier"); + }); + + it("should restore sourceCode.getTokenOrCommentBefore()", () => { + const plugin = { + configs: { + recommended: { + rules: { + "test-rule": "error", + }, + }, + }, + rules: { + "test-rule": { + create(context) { + return { + Identifier(node) { + assert.strictEqual( + context.sourceCode.getTokenOrCommentBefore( + node, + ).value, + "let", + ); + }, + }; + }, + }, + }, + }; + + const linter = new Linter(); + const code = "let foo;"; + const config = { + plugins: { test: fixupPluginRules(plugin) }, + rules: { "test/test-rule": "error" }, + }; + linter.verify(code, config); + }); }); describe("fixupConfigRules()", () => { @@ -834,5 +1221,78 @@ describe("@eslint/backcompat", () => { ); }); }); + + it("should restore context.getFilename()", () => { + const config = [ + { + plugins: { + test: { + rules: { + "test-rule": { + create(context) { + assert.strictEqual( + context.getFilename(), + context.filename, + ); + return { + Identifier(node) { + context.report( + node, + "Identifier", + ); + }, + }; + }, + }, + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }, + ]; + + const linter = new Linter(); + const code = "let foo;"; + const messages = linter.verify(code, fixupConfigRules(config)); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "Identifier"); + }); + + it("should restore sourceCode.getTokenOrCommentBefore()", () => { + const config = [ + { + plugins: { + test: { + rules: { + "test-rule": { + create(context) { + return { + Identifier(node) { + assert.strictEqual( + context.sourceCode.getTokenOrCommentBefore( + node, + ).value, + "let", + ); + }, + }; + }, + }, + }, + }, + }, + rules: { + "test/test-rule": "error", + }, + }, + ]; + + const linter = new Linter(); + const code = "let foo;"; + linter.verify(code, fixupConfigRules(config)); + }); }); }); diff --git a/packages/compat/tests/ignore-file.js b/packages/compat/tests/ignore-file.test.js similarity index 100% rename from packages/compat/tests/ignore-file.js rename to packages/compat/tests/ignore-file.test.js diff --git a/packages/compat/tests/rules/consistent-this.js b/packages/compat/tests/rules/consistent-this.test.js similarity index 90% rename from packages/compat/tests/rules/consistent-this.js rename to packages/compat/tests/rules/consistent-this.test.js index 5adc163e8..f82d28209 100644 --- a/packages/compat/tests/rules/consistent-this.js +++ b/packages/compat/tests/rules/consistent-this.test.js @@ -66,7 +66,6 @@ ruleTester.run("consistent-this", fixedUpRule, { { messageId: "unexpectedAlias", data: { name: "context" }, - type: "VariableDeclarator", }, ], }, @@ -77,7 +76,6 @@ ruleTester.run("consistent-this", fixedUpRule, { { messageId: "unexpectedAlias", data: { name: "that" }, - type: "VariableDeclarator", }, ], }, @@ -88,7 +86,6 @@ ruleTester.run("consistent-this", fixedUpRule, { { messageId: "unexpectedAlias", data: { name: "self" }, - type: "VariableDeclarator", }, ], }, @@ -99,7 +96,6 @@ ruleTester.run("consistent-this", fixedUpRule, { { messageId: "aliasNotAssignedToThis", data: { name: "self" }, - type: "VariableDeclarator", }, ], }, @@ -110,7 +106,6 @@ ruleTester.run("consistent-this", fixedUpRule, { { messageId: "aliasNotAssignedToThis", data: { name: "self" }, - type: "VariableDeclarator", }, ], }, @@ -121,12 +116,10 @@ ruleTester.run("consistent-this", fixedUpRule, { { messageId: "aliasNotAssignedToThis", data: { name: "self" }, - type: "VariableDeclarator", }, { messageId: "aliasNotAssignedToThis", data: { name: "self" }, - type: "AssignmentExpression", }, ], }, @@ -137,7 +130,6 @@ ruleTester.run("consistent-this", fixedUpRule, { { messageId: "unexpectedAlias", data: { name: "context" }, - type: "AssignmentExpression", }, ], }, @@ -148,7 +140,6 @@ ruleTester.run("consistent-this", fixedUpRule, { { messageId: "unexpectedAlias", data: { name: "that" }, - type: "AssignmentExpression", }, ], }, @@ -159,7 +150,6 @@ ruleTester.run("consistent-this", fixedUpRule, { { messageId: "unexpectedAlias", data: { name: "self" }, - type: "AssignmentExpression", }, ], }, @@ -170,7 +160,6 @@ ruleTester.run("consistent-this", fixedUpRule, { { messageId: "aliasNotAssignedToThis", data: { name: "self" }, - type: "AssignmentExpression", }, ], }, @@ -181,7 +170,6 @@ ruleTester.run("consistent-this", fixedUpRule, { { messageId: "aliasNotAssignedToThis", data: { name: "self" }, - type: "VariableDeclarator", }, ], }, diff --git a/packages/compat/tests/rules/global-require.js b/packages/compat/tests/rules/global-require.test.js similarity index 97% rename from packages/compat/tests/rules/global-require.js rename to packages/compat/tests/rules/global-require.test.js index e69d78d68..b93229683 100644 --- a/packages/compat/tests/rules/global-require.js +++ b/packages/compat/tests/rules/global-require.test.js @@ -48,7 +48,7 @@ const valid = [ }, ]; -const error = { messageId: "unexpected", type: "CallExpression" }; +const error = { messageId: "unexpected" }; const invalid = [ // block statements diff --git a/packages/compat/tests/rules/handle-callback-err.js b/packages/compat/tests/rules/handle-callback-err.test.js similarity index 98% rename from packages/compat/tests/rules/handle-callback-err.js rename to packages/compat/tests/rules/handle-callback-err.test.js index c13288421..978769d90 100644 --- a/packages/compat/tests/rules/handle-callback-err.js +++ b/packages/compat/tests/rules/handle-callback-err.test.js @@ -19,11 +19,9 @@ const ruleTester = new RuleTester(); const expectedFunctionDeclarationError = { messageId: "expected", - type: "FunctionDeclaration", }; const expectedFunctionExpressionError = { messageId: "expected", - type: "FunctionExpression", }; const fixedUpRule = fixupRule(rule); diff --git a/packages/compat/tests/rules/no-lone-blocks.js b/packages/compat/tests/rules/no-lone-blocks.test.js similarity index 91% rename from packages/compat/tests/rules/no-lone-blocks.js rename to packages/compat/tests/rules/no-lone-blocks.test.js index f93e94c59..8aedb8fe1 100644 --- a/packages/compat/tests/rules/no-lone-blocks.js +++ b/packages/compat/tests/rules/no-lone-blocks.test.js @@ -119,7 +119,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantBlock", - type: "BlockStatement", }, ], }, @@ -128,7 +127,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantBlock", - type: "BlockStatement", }, ], }, @@ -137,7 +135,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantBlock", - type: "BlockStatement", }, ], }, @@ -146,7 +143,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", }, ], }, @@ -155,12 +151,10 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantBlock", - type: "BlockStatement", line: 1, }, { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 2, }, ], @@ -170,7 +164,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", }, ], }, @@ -179,7 +172,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", }, ], }, @@ -191,7 +183,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantBlock", - type: "BlockStatement", }, ], }, @@ -201,7 +192,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantBlock", - type: "BlockStatement", }, ], }, @@ -212,7 +202,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 2, }, ], @@ -223,7 +212,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantBlock", - type: "BlockStatement", line: 1, }, ], @@ -234,17 +222,14 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantBlock", - type: "BlockStatement", line: 1, }, { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 2, }, { messageId: "redundantBlock", - type: "BlockStatement", line: 4, }, ], @@ -262,7 +247,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantBlock", - type: "BlockStatement", line: 5, }, ], @@ -280,7 +264,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantBlock", - type: "BlockStatement", line: 4, }, ], @@ -297,7 +280,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 3, }, ], @@ -313,7 +295,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 3, }, ], @@ -334,7 +315,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 5, }, ], @@ -356,7 +336,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 5, }, ], @@ -375,7 +354,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 4, }, ], @@ -394,7 +372,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 4, }, ], @@ -413,7 +390,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 4, }, ], @@ -432,7 +408,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 4, }, ], @@ -451,7 +426,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 4, }, ], @@ -471,7 +445,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 4, }, ], @@ -491,7 +464,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 5, }, ], @@ -511,7 +483,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { errors: [ { messageId: "redundantNestedBlock", - type: "BlockStatement", line: 4, }, ], @@ -531,7 +502,6 @@ ruleTester.run("no-lone-blocks", fixedUpRule, { 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.test.js similarity index 92% rename from packages/compat/tests/rules/no-loop-func.js rename to packages/compat/tests/rules/no-loop-func.test.js index ad596c37c..cc230e8b1 100644 --- a/packages/compat/tests/rules/no-loop-func.js +++ b/packages/compat/tests/rules/no-loop-func.test.js @@ -165,7 +165,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'i'" }, - type: "FunctionExpression", }, ], }, @@ -175,7 +174,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'i', 'j'" }, - type: "FunctionExpression", }, ], }, @@ -185,7 +183,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'i'" }, - type: "FunctionExpression", }, ], }, @@ -196,7 +193,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'i'" }, - type: "FunctionExpression", }, ], }, @@ -207,7 +203,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'i'" }, - type: "ArrowFunctionExpression", }, ], }, @@ -217,7 +212,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'i'" }, - type: "FunctionExpression", }, ], }, @@ -227,7 +221,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'i'" }, - type: "FunctionDeclaration", }, ], }, @@ -237,7 +230,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'i'" }, - type: "FunctionExpression", }, ], }, @@ -247,7 +239,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'i'" }, - type: "FunctionExpression", }, ], }, @@ -260,7 +251,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'a'" }, - type: "FunctionExpression", }, ], }, @@ -271,7 +261,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'a'" }, - type: "FunctionExpression", }, ], }, @@ -282,7 +271,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'a'" }, - type: "FunctionExpression", }, ], }, @@ -293,7 +281,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'a'" }, - type: "FunctionExpression", }, ], }, @@ -304,7 +291,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'a'" }, - type: "FunctionDeclaration", }, ], }, @@ -315,7 +301,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'a'" }, - type: "ArrowFunctionExpression", }, ], }, @@ -326,7 +311,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'i'" }, - type: "ArrowFunctionExpression", }, ], }, @@ -337,7 +321,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'a'" }, - type: "FunctionExpression", }, ], }, @@ -348,7 +331,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'x'" }, - type: "FunctionExpression", }, ], }, @@ -359,7 +341,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'x'" }, - type: "FunctionExpression", }, ], }, @@ -370,7 +351,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'a'" }, - type: "FunctionExpression", }, ], }, @@ -381,7 +361,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'a'" }, - type: "FunctionExpression", }, ], }, @@ -392,7 +371,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { messageId: "unsafeRefs", data: { varNames: "'a'" }, - type: "FunctionExpression", }, ], }, @@ -403,7 +381,6 @@ ruleTester.run("no-loop-func", fixedUpRule, { { 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.test.js similarity index 83% rename from packages/compat/tests/rules/prefer-rest-params.js rename to packages/compat/tests/rules/prefer-rest-params.test.js index 7ce96c865..9984c2bd2 100644 --- a/packages/compat/tests/rules/prefer-rest-params.js +++ b/packages/compat/tests/rules/prefer-rest-params.test.js @@ -33,19 +33,19 @@ ruleTester.run("prefer-rest-params", fixedUpRule, { invalid: [ { code: "function foo() { arguments; }", - errors: [{ type: "Identifier", messageId: "preferRestParams" }], + errors: [{ messageId: "preferRestParams" }], }, { code: "function foo() { arguments[0]; }", - errors: [{ type: "Identifier", messageId: "preferRestParams" }], + errors: [{ messageId: "preferRestParams" }], }, { code: "function foo() { arguments[1]; }", - errors: [{ type: "Identifier", messageId: "preferRestParams" }], + errors: [{ messageId: "preferRestParams" }], }, { code: "function foo() { arguments[Symbol.iterator]; }", - errors: [{ type: "Identifier", messageId: "preferRestParams" }], + errors: [{ messageId: "preferRestParams" }], }, ], }); diff --git a/packages/compat/tests/rules/require-atomic-updates.js b/packages/compat/tests/rules/require-atomic-updates.test.js similarity index 98% rename from packages/compat/tests/rules/require-atomic-updates.js rename to packages/compat/tests/rules/require-atomic-updates.test.js index 40f68d42d..35a0bced5 100644 --- a/packages/compat/tests/rules/require-atomic-updates.js +++ b/packages/compat/tests/rules/require-atomic-updates.test.js @@ -23,25 +23,21 @@ 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, { @@ -374,13 +370,11 @@ ruleTester.run("require-atomic-updates", fixedUpRule, { { messageId: "nonAtomicObjectUpdate", data: { value: "process.exitCode", object: "process" }, - type: "AssignmentExpression", line: 6, }, { messageId: "nonAtomicObjectUpdate", data: { value: "process.exitCode", object: "process" }, - type: "AssignmentExpression", line: 8, }, ], diff --git a/packages/config-array/CHANGELOG.md b/packages/config-array/CHANGELOG.md index e498595e6..019c1db54 100644 --- a/packages/config-array/CHANGELOG.md +++ b/packages/config-array/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## [0.22.0](https://github.com/eslint/rewrite/compare/config-array-v0.21.1...config-array-v0.22.0) (2025-11-14) + + +### ⚠ BREAKING CHANGES + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) + +### Features + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) ([acc623c](https://github.com/eslint/rewrite/commit/acc623c807bf8237a26b18291f04dd99e4e4981a)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/object-schema bumped from ^2.1.7 to ^3.0.0 + +## [0.21.1](https://github.com/eslint/rewrite/compare/config-array-v0.21.0...config-array-v0.21.1) (2025-10-17) + + +### Bug Fixes + +* fix `config-array` and `object-schema` types ([#294](https://github.com/eslint/rewrite/issues/294)) ([a902bc4](https://github.com/eslint/rewrite/commit/a902bc4e27639ba5975b5d793314235737dc2c1a)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/object-schema bumped from ^2.1.6 to ^2.1.7 + ## [0.21.0](https://github.com/eslint/rewrite/compare/config-array-v0.20.1...config-array-v0.21.0) (2025-06-25) diff --git a/packages/config-array/README.md b/packages/config-array/README.md index ab9e5cfd2..19af407e3 100644 --- a/packages/config-array/README.md +++ b/packages/config-array/README.md @@ -359,9 +359,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

Qlty Software trunk.io Shopify

Silver Sponsors

-

Vite Liftoff American Express StackBlitz

Bronze Sponsors

-

Cybozu Sentry Anagram Solver Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

+

Qlty Software Shopify

Silver Sponsors

+

Vite Liftoff American Express StackBlitz

Bronze Sponsors

+

Cybozu Syntax Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

diff --git a/packages/config-array/jsr.json b/packages/config-array/jsr.json index c68018a72..ad5bc4619 100644 --- a/packages/config-array/jsr.json +++ b/packages/config-array/jsr.json @@ -1,6 +1,6 @@ { "name": "@eslint/config-array", - "version": "0.21.0", + "version": "0.22.0", "exports": "./dist/esm/index.js", "publish": { "include": [ diff --git a/packages/config-array/package.json b/packages/config-array/package.json index f9fc01eb6..9d16ddbe9 100644 --- a/packages/config-array/package.json +++ b/packages/config-array/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/config-array", - "version": "0.21.0", + "version": "0.22.0", "description": "General purpose glob-based configuration matching.", "author": "Nicholas C. Zakas", "type": "module", @@ -36,10 +36,11 @@ "build:cts": "node ../../tools/build-cts.js dist/esm/index.d.ts dist/cjs/index.d.cts", "build:std__path": "rollup -c rollup.std__path-config.js && node fix-std__path-imports", "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts && npm run build:std__path", - "test:jsr": "npx jsr@latest publish --dry-run", "pretest": "npm run build", - "test": "mocha tests/", - "test:coverage": "c8 npm test" + "test": "mocha \"tests/**/*.test.js\"", + "test:coverage": "c8 npm test", + "test:jsr": "npx jsr@latest publish --dry-run", + "test:types": "tsc -p tests/types/tsconfig.json" }, "keywords": [ "configuration", @@ -48,7 +49,7 @@ ], "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^3.0.0", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -58,6 +59,6 @@ "rollup-plugin-copy": "^3.5.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } } diff --git a/packages/config-array/rollup.config.js b/packages/config-array/rollup.config.js index 03a11e126..5b813a884 100644 --- a/packages/config-array/rollup.config.js +++ b/packages/config-array/rollup.config.js @@ -16,7 +16,7 @@ export default { plugins: [ copy({ targets: [ - { src: "src/types.ts", dest: "dist/cjs" }, + { src: "src/types.ts", dest: "dist/cjs", rename: "types.cts" }, { src: "src/types.ts", dest: "dist/esm" }, ], }), diff --git a/packages/config-array/rollup.std__path-config.js b/packages/config-array/rollup.std__path-config.js index 2a9317ec7..44912051b 100644 --- a/packages/config-array/rollup.std__path-config.js +++ b/packages/config-array/rollup.std__path-config.js @@ -7,10 +7,12 @@ export default [ input: resolve("@jsr/std__path/posix"), output: [ { + banner: "// @ts-nocheck", file: "./dist/cjs/std__path/posix.cjs", format: "cjs", }, { + banner: "// @ts-nocheck", file: "./dist/esm/std__path/posix.js", format: "esm", }, @@ -20,10 +22,12 @@ export default [ input: resolve("@jsr/std__path/windows"), output: [ { + banner: "// @ts-nocheck", file: "./dist/cjs/std__path/windows.cjs", format: "cjs", }, { + banner: "// @ts-nocheck", file: "./dist/esm/std__path/windows.js", format: "esm", }, diff --git a/packages/config-array/src/config-array.js b/packages/config-array/src/config-array.js index 075cef701..cbd373ba8 100644 --- a/packages/config-array/src/config-array.js +++ b/packages/config-array/src/config-array.js @@ -20,12 +20,10 @@ import { filesAndIgnoresSchema } from "./files-and-ignores-schema.js"; // Types //------------------------------------------------------------------------------ -/** @typedef {import("@eslint/object-schema").PropertyDefinition} PropertyDefinition */ -/** @typedef {import("@eslint/object-schema").ObjectDefinition} ObjectDefinition */ /** @typedef {import("./types.ts").ConfigObject} ConfigObject */ /** @typedef {import("minimatch").IMinimatchStatic} IMinimatchStatic */ /** @typedef {import("minimatch").IMinimatch} IMinimatch */ -/** @typedef {import("@jsr/std__path")} PathImpl */ +/** @import * as PathImpl from "@jsr/std__path" */ /* * This is a bit of a hack to make TypeScript happy with the Rollup-created @@ -34,7 +32,7 @@ import { filesAndIgnoresSchema } from "./files-and-ignores-schema.js"; * for `ObjectSchema`. To work around that, we just import the type manually * and give it a different name to use in the JSDoc comments. */ -/** @typedef {import("@eslint/object-schema").ObjectSchema} ObjectSchemaInstance */ +/** @typedef {ObjectSchema} ObjectSchemaInstance */ //------------------------------------------------------------------------------ // Helpers @@ -968,7 +966,8 @@ export class ConfigArray extends Array { * @param {Object} config The config to finalize. * @returns {Object} The finalized config. */ - [ConfigArraySymbol.finalizeConfig](config) { + // Cast key to `never` to prevent TypeScript from adding the signature `[x: symbol]: (config: any) => any` to the type of the class. + [/** @type {never} */ (ConfigArraySymbol.finalizeConfig)](config) { return config; } @@ -980,7 +979,8 @@ export class ConfigArray extends Array { * @param {Object} config The config to preprocess. * @returns {Object} The config to use in place of the argument. */ - [ConfigArraySymbol.preprocessConfig](config) { + // Cast key to `never` to prevent TypeScript from adding the signature `[x: symbol]: (config: any) => any` to the type of the class. + [/** @type {never} */ (ConfigArraySymbol.preprocessConfig)](config) { return config; } diff --git a/packages/config-array/tests/types/cjs-import.test.cts b/packages/config-array/tests/types/cjs-import.test.cts new file mode 100644 index 000000000..f80a15616 --- /dev/null +++ b/packages/config-array/tests/types/cjs-import.test.cts @@ -0,0 +1,10 @@ +/** + * @fileoverview CommonJS type import test for Config Array package. + * @author Francesco Trotta + */ + +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import "@eslint/config-array"; diff --git a/packages/config-array/tests/types/tsconfig.json b/packages/config-array/tests/types/tsconfig.json new file mode 100644 index 000000000..638c0f3a5 --- /dev/null +++ b/packages/config-array/tests/types/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "../.." + }, + "include": [".", "../../dist"] +} diff --git a/packages/config-array/tests/types/types.test.ts b/packages/config-array/tests/types/types.test.ts new file mode 100644 index 000000000..7b9ff36d2 --- /dev/null +++ b/packages/config-array/tests/types/types.test.ts @@ -0,0 +1,10 @@ +/** + * @fileoverview Type tests for Config Array package. + * @author Francesco Trotta + */ + +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import "@eslint/config-array"; diff --git a/packages/config-helpers/CHANGELOG.md b/packages/config-helpers/CHANGELOG.md index adf8e775d..055d31b81 100644 --- a/packages/config-helpers/CHANGELOG.md +++ b/packages/config-helpers/CHANGELOG.md @@ -1,5 +1,69 @@ # Changelog +## [0.5.0](https://github.com/eslint/rewrite/compare/config-helpers-v0.4.2...config-helpers-v0.5.0) (2025-11-14) + + +### ⚠ BREAKING CHANGES + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) + +### Features + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) ([acc623c](https://github.com/eslint/rewrite/commit/acc623c807bf8237a26b18291f04dd99e4e4981a)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/core bumped from ^0.17.0 to ^1.0.0 + +## [0.4.2](https://github.com/eslint/rewrite/compare/config-helpers-v0.4.1...config-helpers-v0.4.2) (2025-10-27) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/core bumped from ^0.16.0 to ^0.17.0 + +## [0.4.1](https://github.com/eslint/rewrite/compare/config-helpers-v0.4.0...config-helpers-v0.4.1) (2025-10-17) + + +### Bug Fixes + +* add validation for `plugins` in isLegacyConfig ([#292](https://github.com/eslint/rewrite/issues/292)) ([74f9427](https://github.com/eslint/rewrite/commit/74f9427b47de313582793ab6fc4c723f1526fdc0)) +* improve type support for isolated dependencies in pnpm ([#289](https://github.com/eslint/rewrite/issues/289)) ([f8df139](https://github.com/eslint/rewrite/commit/f8df139631694431ecfc651e656932e283d4d14f)) +* use flat config when eslintrc config does not exist ([#288](https://github.com/eslint/rewrite/issues/288)) ([ddc8577](https://github.com/eslint/rewrite/commit/ddc857781bacab1cdd7c540e599d3ed968607a09)) + +## [0.4.0](https://github.com/eslint/rewrite/compare/config-helpers-v0.3.1...config-helpers-v0.4.0) (2025-09-16) + + +### Features + +* Add config types in @eslint/core ([#237](https://github.com/eslint/rewrite/issues/237)) ([7b6dd37](https://github.com/eslint/rewrite/commit/7b6dd370a598ea7fc94fba427a2579342b50b90f)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/core bumped from ^0.15.2 to ^0.16.0 + +## [0.3.1](https://github.com/eslint/rewrite/compare/config-helpers-v0.3.0...config-helpers-v0.3.1) (2025-08-05) + + +### Bug Fixes + +* relax type for rule.meta.docs.recommended ([#235](https://github.com/eslint/rewrite/issues/235)) ([9a4fe34](https://github.com/eslint/rewrite/commit/9a4fe343c309b7a000ffb5cd420b557809e4d58e)) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @eslint/core bumped from ^0.15.1 to ^0.15.2 + ## [0.3.0](https://github.com/eslint/rewrite/compare/config-helpers-v0.2.3...config-helpers-v0.3.0) (2025-06-25) diff --git a/packages/config-helpers/README.md b/packages/config-helpers/README.md index d54818af3..a06051357 100644 --- a/packages/config-helpers/README.md +++ b/packages/config-helpers/README.md @@ -88,9 +88,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

Qlty Software trunk.io Shopify

Silver Sponsors

-

Vite Liftoff American Express StackBlitz

Bronze Sponsors

-

Cybozu Sentry Anagram Solver Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

+

Qlty Software Shopify

Silver Sponsors

+

Vite Liftoff American Express StackBlitz

Bronze Sponsors

+

Cybozu Syntax Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

diff --git a/packages/config-helpers/jsr.json b/packages/config-helpers/jsr.json index 2ff2cf24b..da784f3a7 100644 --- a/packages/config-helpers/jsr.json +++ b/packages/config-helpers/jsr.json @@ -1,6 +1,6 @@ { "name": "@eslint/config-helpers", - "version": "0.3.0", + "version": "0.5.0", "exports": "./dist/esm/index.js", "publish": { "include": [ diff --git a/packages/config-helpers/package.json b/packages/config-helpers/package.json index 7c455ea3c..504ef34fe 100644 --- a/packages/config-helpers/package.json +++ b/packages/config-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/config-helpers", - "version": "0.3.0", + "version": "0.5.0", "description": "Helper utilities for creating ESLint configuration", "type": "module", "main": "dist/esm/index.js", @@ -28,9 +28,10 @@ "build:dedupe-types": "node ../../tools/dedupe-types.js dist/cjs/index.cjs dist/esm/index.js", "build:cts": "node ../../tools/build-cts.js dist/esm/index.d.ts dist/cjs/index.d.cts", "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts", - "test:jsr": "npx jsr@latest publish --dry-run", - "test": "mocha tests/*.js", + "test": "mocha \"tests/**/*.test.js\"", "test:coverage": "c8 npm test", + "test:jsr": "npx jsr@latest publish --dry-run", + "test:pnpm": "cd tests/pnpm && pnpm install && pnpm exec tsc", "test:types": "tsc -p tests/types/tsconfig.json" }, "repository": { @@ -46,12 +47,14 @@ "url": "https://github.com/eslint/rewrite/issues" }, "homepage": "https://github.com/eslint/rewrite/tree/main/packages/config-helpers#readme", + "dependencies": { + "@eslint/core": "^1.0.0" + }, "devDependencies": { - "@eslint/core": "^0.15.1", "eslint": "^9.27.0", "rollup-plugin-copy": "^3.5.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } } diff --git a/packages/config-helpers/src/define-config.js b/packages/config-helpers/src/define-config.js index eaf06f85b..b2fb096fd 100644 --- a/packages/config-helpers/src/define-config.js +++ b/packages/config-helpers/src/define-config.js @@ -7,10 +7,10 @@ // Type Definitions //----------------------------------------------------------------------------- -/** @typedef {import("eslint").Linter.Config} Config */ -/** @typedef {import("eslint").Linter.LegacyConfig} LegacyConfig */ -/** @typedef {import("eslint").ESLint.Plugin} Plugin */ -/** @typedef {import("eslint").Linter.RuleEntry} RuleEntry */ +/** @typedef {import("@eslint/core").ConfigObject} Config */ +/** @typedef {import("@eslint/core").LegacyConfigObject} LegacyConfig */ +/** @typedef {import("@eslint/core").Plugin} Plugin */ +/** @typedef {import("@eslint/core").RuleConfig} RuleConfig */ /** @typedef {import("./types.ts").ExtendsElement} ExtendsElement */ /** @typedef {import("./types.ts").SimpleExtendsElement} SimpleExtendsElement */ /** @typedef {import("./types.ts").ConfigWithExtends} ConfigWithExtends */ @@ -74,6 +74,11 @@ function getExtensionName(extension, indexPath) { * @return {config is LegacyConfig} `true` if the config object is a legacy config. */ function isLegacyConfig(config) { + // eslintrc's plugins must be an array; while flat config's must be an object. + if (Array.isArray(config.plugins)) { + return true; + } + for (const key of eslintrcKeys) { if (key in config) { return true; @@ -153,7 +158,7 @@ function normalizePluginConfig(userNamespace, plugin, config) { if (result.rules) { const ruleIds = Object.keys(result.rules); - /** @type {Record} */ + /** @type {Record} */ const newRules = {}; for (let i = 0; i < ruleIds.length; i++) { @@ -251,6 +256,8 @@ function findPluginConfig(config, pluginConfigName) { } const directConfig = plugin.configs?.[configName]; + + // Prefer direct config, but fall back to flat config if available if (directConfig) { // Arrays are always flat configs, and non-legacy configs can be used directly if (Array.isArray(directConfig) || !isLegacyConfig(directConfig)) { @@ -261,30 +268,28 @@ function findPluginConfig(config, pluginConfigName) { pluginConfigName, ); } + } - // If it's a legacy config, look for the flat version - const flatConfig = plugin.configs?.[`flat/${configName}`]; - - if ( - flatConfig && - (Array.isArray(flatConfig) || !isLegacyConfig(flatConfig)) - ) { - return deepNormalizePluginConfig( - userPluginNamespace, - plugin, - flatConfig, - pluginConfigName, - ); - } - - throw new TypeError( - `Plugin config "${configName}" in plugin "${userPluginNamespace}" is an eslintrc config and cannot be used in this context.`, + // If it's a legacy config, or the config does not exist => look for the flat version + const flatConfig = plugin.configs?.[`flat/${configName}`]; + if ( + flatConfig && + (Array.isArray(flatConfig) || !isLegacyConfig(flatConfig)) + ) { + return deepNormalizePluginConfig( + userPluginNamespace, + plugin, + flatConfig, + pluginConfigName, ); } - throw new TypeError( - `Plugin config "${configName}" not found in plugin "${userPluginNamespace}".`, - ); + // If we get here, then the config was either not found or is a legacy config + const message = + directConfig || flatConfig + ? `Plugin config "${configName}" in plugin "${userPluginNamespace}" is an eslintrc config and cannot be used in this context.` + : `Plugin config "${configName}" not found in plugin "${userPluginNamespace}".`; + throw new TypeError(message); } /** diff --git a/packages/config-helpers/src/global-ignores.js b/packages/config-helpers/src/global-ignores.js index 062bb15d4..b351b0cbe 100644 --- a/packages/config-helpers/src/global-ignores.js +++ b/packages/config-helpers/src/global-ignores.js @@ -7,7 +7,7 @@ // Type Definitions //----------------------------------------------------------------------------- -/** @typedef {import("eslint").Linter.Config} Config */ +/** @typedef {import("@eslint/core").ConfigObject} Config */ //----------------------------------------------------------------------------- // Helpers diff --git a/packages/config-helpers/src/types.ts b/packages/config-helpers/src/types.ts index 084f7b280..a313ea259 100644 --- a/packages/config-helpers/src/types.ts +++ b/packages/config-helpers/src/types.ts @@ -2,7 +2,7 @@ * @fileoverview Types for this package. */ -import type { Linter } from "eslint"; +import type { ConfigObject } from "@eslint/core"; /** * Infinite array type. @@ -12,19 +12,17 @@ export type InfiniteArray = T | InfiniteArray[]; /** * The type of array element in the `extends` property after flattening. */ -export type SimpleExtendsElement = string | Linter.Config; +export type SimpleExtendsElement = string | ConfigObject; /** * The type of array element in the `extends` property before flattening. */ -export type ExtendsElement = - | SimpleExtendsElement - | InfiniteArray; +export type ExtendsElement = SimpleExtendsElement | InfiniteArray; /** * Config with extends. Valid only inside of `defineConfig()`. */ -export interface ConfigWithExtends extends Linter.Config { +export interface ConfigWithExtends extends ConfigObject { extends?: ExtendsElement[]; } diff --git a/packages/config-helpers/tests/define-config.test.js b/packages/config-helpers/tests/define-config.test.js index af5df3cd9..6386d4fac 100644 --- a/packages/config-helpers/tests/define-config.test.js +++ b/packages/config-helpers/tests/define-config.test.js @@ -790,6 +790,41 @@ describe("defineConfig()", () => { }); }, /Plugin config "config1" in plugin "test" is an eslintrc config and cannot be used in this context\./u); }); + it("should throw an error when a plugin config 'flat/recommended' is in eslintrc format", () => { + const testPlugin = { + configs: { + "flat/recommended": { plugins: [] }, + }, + }; + + assert.throws(() => { + defineConfig({ + plugins: { + test: testPlugin, + }, + extends: ["test/recommended"], + }); + }, /Plugin config "recommended" in plugin "test" is an eslintrc config and cannot be used in this context\./u); + }); + + it("should throw an error when a plugin config is in eslintrc format (`plugins` is an array)", () => { + const testPlugin = { + configs: { + config1: { + plugins: [], + }, + }, + }; + + assert.throws(() => { + defineConfig({ + plugins: { + test: testPlugin, + }, + extends: ["test/config1"], + }); + }, /Plugin config "config1" in plugin "test" is an eslintrc config and cannot be used in this context\./u); + }); it("should use flat config when base config is legacy", () => { const testPlugin = { @@ -826,6 +861,37 @@ describe("defineConfig()", () => { ]); }); + it("should use flat config when eslintrc does not exist", () => { + const testPlugin = { + configs: { + "flat/recommended": { + rules: { "no-console": "error" }, + }, + }, + }; + + const config = defineConfig({ + plugins: { + test: testPlugin, + }, + extends: ["test/recommended"], + rules: { + "no-debugger": "error", + }, + }); + + assert.deepStrictEqual(config, [ + { + name: "UserConfig[0] > test/recommended", + rules: { "no-console": "error" }, + }, + { + plugins: { test: testPlugin }, + rules: { "no-debugger": "error" }, + }, + ]); + }); + it("should throw error when both base and flat configs are legacy", () => { const testPlugin = { configs: { diff --git a/packages/config-helpers/tests/pnpm/package.json b/packages/config-helpers/tests/pnpm/package.json new file mode 100644 index 000000000..67eed0696 --- /dev/null +++ b/packages/config-helpers/tests/pnpm/package.json @@ -0,0 +1,7 @@ +{ + "type": "module", + "dependencies": { + "@eslint/config-helpers": "file:../..", + "typescript": "^5.9.3" + } +} diff --git a/packages/config-helpers/tests/pnpm/test-eslint.config.js b/packages/config-helpers/tests/pnpm/test-eslint.config.js new file mode 100644 index 000000000..16d8a42d2 --- /dev/null +++ b/packages/config-helpers/tests/pnpm/test-eslint.config.js @@ -0,0 +1,5 @@ +import { defineConfig, globalIgnores } from "@eslint/config-helpers"; + +export const ignoresConfig = globalIgnores([]); + +export default defineConfig([ignoresConfig]); diff --git a/packages/config-helpers/tests/pnpm/tsconfig.json b/packages/config-helpers/tests/pnpm/tsconfig.json new file mode 100644 index 000000000..731223ab7 --- /dev/null +++ b/packages/config-helpers/tests/pnpm/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "checkJs": true, + "declaration": true, + "module": "nodenext", + "noEmit": true + }, + "files": ["test-eslint.config.js"] +} diff --git a/packages/config-helpers/tests/types/tsconfig.json b/packages/config-helpers/tests/types/tsconfig.json index b3220a777..852b0e519 100644 --- a/packages/config-helpers/tests/types/tsconfig.json +++ b/packages/config-helpers/tests/types/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "noEmit": true, "rootDir": "../..", - "strict": true + "strict": true, + "exactOptionalPropertyTypes": true }, "include": [".", "../../dist"] } diff --git a/packages/config-helpers/tests/types/types.test.ts b/packages/config-helpers/tests/types/types.test.ts index d79acb526..912b273e9 100644 --- a/packages/config-helpers/tests/types/types.test.ts +++ b/packages/config-helpers/tests/types/types.test.ts @@ -71,3 +71,23 @@ defineConfig( globalIgnores(["node_modules"]); globalIgnores(["dist", "build"], "my name"); + +defineConfig({ + plugins: { + "some-plugin": { + rules: { + "some-rule": { + meta: { + docs: { + recommended: "not a boolean!", + }, + }, + + create() { + return {}; + }, + }, + }, + }, + }, +}); diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 54c2d24b9..92e17caf7 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,55 @@ # Changelog +## [1.0.0](https://github.com/eslint/rewrite/compare/core-v0.17.0...core-v1.0.0) (2025-11-14) + + +### ⚠ BREAKING CHANGES + +* Remove deprecated RuleContext methods ([#263](https://github.com/eslint/rewrite/issues/263)) +* remove deprecated `nodeType` property ([#265](https://github.com/eslint/rewrite/issues/265)) +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) + +### Features + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) ([acc623c](https://github.com/eslint/rewrite/commit/acc623c807bf8237a26b18291f04dd99e4e4981a)) + + +### Bug Fixes + +* remove deprecated `nodeType` property ([#265](https://github.com/eslint/rewrite/issues/265)) ([7d6a2a8](https://github.com/eslint/rewrite/commit/7d6a2a8dfb73203790403dea240669b6ab543340)) +* Remove deprecated RuleContext methods ([#263](https://github.com/eslint/rewrite/issues/263)) ([0455323](https://github.com/eslint/rewrite/commit/0455323682227ba2e219645a49c20085ab76cbf0)) + +## [0.17.0](https://github.com/eslint/rewrite/compare/core-v0.16.0...core-v0.17.0) (2025-10-27) + + +### Features + +* export additional core types ([#304](https://github.com/eslint/rewrite/issues/304)) ([5ccde5b](https://github.com/eslint/rewrite/commit/5ccde5bc9442c572d740c063fcb50392bf13c3db)) + + +### Bug Fixes + +* require `fix` in suggestion objects ([#298](https://github.com/eslint/rewrite/issues/298)) ([02bac50](https://github.com/eslint/rewrite/commit/02bac50b8a053f12a97afbe65b126ccd2c469d9e)) + +## [0.16.0](https://github.com/eslint/rewrite/compare/core-v0.15.2...core-v0.16.0) (2025-09-16) + + +### Features + +* Add config types in @eslint/core ([#237](https://github.com/eslint/rewrite/issues/237)) ([7b6dd37](https://github.com/eslint/rewrite/commit/7b6dd370a598ea7fc94fba427a2579342b50b90f)) + + +### Bug Fixes + +* remove unsupported `nodeType` from types ([#268](https://github.com/eslint/rewrite/issues/268)) ([d800559](https://github.com/eslint/rewrite/commit/d8005593158f55ba32f5279f3385db95ab87075a)) + +## [0.15.2](https://github.com/eslint/rewrite/compare/core-v0.15.1...core-v0.15.2) (2025-08-05) + + +### Bug Fixes + +* relax type for rule.meta.docs.recommended ([#235](https://github.com/eslint/rewrite/issues/235)) ([9a4fe34](https://github.com/eslint/rewrite/commit/9a4fe343c309b7a000ffb5cd420b557809e4d58e)) + ## [0.15.1](https://github.com/eslint/rewrite/compare/core-v0.15.0...core-v0.15.1) (2025-06-25) diff --git a/packages/core/README.md b/packages/core/README.md index 04575ea93..700abb165 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -20,9 +20,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

Qlty Software trunk.io Shopify

Silver Sponsors

-

Vite Liftoff American Express StackBlitz

Bronze Sponsors

-

Cybozu Sentry Anagram Solver Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

+

Qlty Software Shopify

Silver Sponsors

+

Vite Liftoff American Express StackBlitz

Bronze Sponsors

+

Cybozu Syntax Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

diff --git a/packages/core/jsr.json b/packages/core/jsr.json index b87c6a0e2..57e3d3bea 100644 --- a/packages/core/jsr.json +++ b/packages/core/jsr.json @@ -1,6 +1,6 @@ { "name": "@eslint/core", - "version": "0.15.1", + "version": "1.0.0", "exports": "./dist/esm/types.d.ts", "publish": { "include": [ diff --git a/packages/core/package.json b/packages/core/package.json index 3033322e3..0220d6cbe 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/core", - "version": "0.15.1", + "version": "1.0.0", "description": "Runtime-agnostic core of ESLint", "type": "module", "types": "./dist/esm/types.d.ts", @@ -44,6 +44,6 @@ "json-schema": "^0.4.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 5dd9b3c01..95604e70d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -127,8 +127,10 @@ export interface RulesMetaDocs { /** * Indicates if the rule is generally recommended for all users. + * + * Note - this will always be a boolean for core rules, but may be used in any way by plugins. */ - recommended?: boolean | undefined; + recommended?: unknown; /** * Indicates if the rule is frozen (no longer accepting feature requests). @@ -161,7 +163,7 @@ export interface RulesMeta< /** * Any default options to be recursively merged on top of any user-provided options. - **/ + */ defaultOptions?: RuleOptions; /** @@ -301,67 +303,31 @@ export interface RuleContext< */ cwd: string; - /** - * Returns the current working directory for the session. - * @deprecated Use `cwd` instead. - */ - getCwd(): string; - /** * The filename of the file being linted. */ filename: string; - /** - * Returns the filename of the file being linted. - * @deprecated Use `filename` instead. - */ - getFilename(): string; - /** * The physical filename of the file being linted. */ physicalFilename: string; - /** - * Returns the physical filename of the file being linted. - * @deprecated Use `physicalFilename` instead. - */ - getPhysicalFilename(): string; - /** * The source code object that the rule is running on. */ sourceCode: Options["Code"]; - /** - * Returns the source code object that the rule is running on. - * @deprecated Use `sourceCode` instead. - */ - getSourceCode(): Options["Code"]; - /** * Shared settings for the configuration. */ settings: SettingsConfig; - /** - * Parser-specific options for the configuration. - * @deprecated Use `languageOptions.parserOptions` instead. - */ - parserOptions: Record; - /** * The language options for the configuration. */ languageOptions: Options["LangOptions"]; - /** - * The CommonJS path to the parser used while parsing this file. - * @deprecated No longer used. - */ - parserPath: string | undefined; - /** * The rule ID. */ @@ -477,21 +443,15 @@ export interface RuleTextEdit { * @param fixer The text editor to apply the fix. * @returns The fix(es) for the violation. */ -type RuleFixer = ( +export type RuleFixer = ( fixer: RuleTextEditor, ) => RuleTextEdit | Iterable | null; -interface ViolationReportBase { - /** - * The type of node that the violation is for. - * @deprecated May be removed in the future. - */ - nodeType?: string | undefined; - +export interface ViolationReportBase { /** * The data to insert into the message. */ - data?: Record | undefined; + data?: Record | undefined; /** * The fix to be applied for the violation. @@ -505,10 +465,10 @@ interface ViolationReportBase { suggest?: SuggestedEdit[] | null | undefined; } -type ViolationMessage = +export type ViolationMessage = | { message: string } | { messageId: MessageIds }; -type ViolationLocation = +export type ViolationLocation = | { loc: SourceLocation | Position } | { node: Node }; @@ -521,25 +481,77 @@ export type ViolationReport< // #region Suggestions -interface SuggestedEditBase { +export interface SuggestedEditBase { /** * The data to insert into the message. */ - data?: Record | undefined; + data?: Record | undefined; /** * The fix to be applied for the suggestion. */ - fix?: RuleFixer | null | undefined; + fix: RuleFixer; } -type SuggestionMessage = { desc: string } | { messageId: string }; +export type SuggestionMessage = { desc: string } | { messageId: string }; /** * A suggested edit for a rule violation. */ export type SuggestedEdit = SuggestedEditBase & SuggestionMessage; +/** + * The normalized version of a lint suggestion. + */ +export interface LintSuggestion { + /** A short description. */ + desc: string; + + /** Fix result info. */ + fix: RuleTextEdit; + + /** Id referencing a message for the description. */ + messageId?: string | undefined; +} + +/** + * The normalized version of a lint violation message. + */ +export interface LintMessage { + /** The 1-based column number. */ + column: number; + + /** The 1-based line number. */ + line: number; + + /** The 1-based column number of the end location. */ + endColumn?: number | undefined; + + /** The 1-based line number of the end location. */ + endLine?: number | undefined; + + /** The ID of the rule which makes this message. */ + ruleId: string | null; + + /** The reported message. */ + message: string; + + /** The ID of the message in the rule's meta. */ + messageId?: string | undefined; + + /** If `true` then this is a fatal error. */ + fatal?: true | undefined; + + /** The severity of this message. */ + severity: Exclude; + + /** Information for autofix. */ + fix?: RuleTextEdit | undefined; + + /** Information for suggestions. */ + suggestions?: LintSuggestion[] | undefined; +} + // #endregion /** @@ -634,6 +646,8 @@ export type CustomRuleDefinitionType< // Config //------------------------------------------------------------------------------ +// #region Severities + /** * The human readable severity level used in a configuration. */ @@ -653,6 +667,24 @@ export type SeverityLevel = 0 | 1 | 2; */ export type Severity = SeverityName | SeverityLevel; +// #endregion + +/** + * Represents the metadata for an object, such as a plugin or processor. + */ +export interface ObjectMetaProperties { + /** @deprecated Use `meta.name` instead. */ + name?: string | undefined; + + /** @deprecated Use `meta.version` instead. */ + version?: string | undefined; + + meta?: { + name?: string | undefined; + version?: string | undefined; + }; +} + /** * Represents the configuration options for the core linter. */ @@ -697,6 +729,392 @@ export interface SettingsConfig { } /* eslint-enable @typescript-eslint/consistent-indexed-object-style -- needed to allow extension */ +/** + * The configuration for a set of files. + */ +export interface ConfigObject { + /** + * A string to identify the configuration object. Used in error messages and + * inspection tools. + */ + name?: string; + + /** + * Path to the directory where the configuration object should apply. + * `files` and `ignores` patterns in the configuration object are + * interpreted as relative to this path. + */ + basePath?: string; + + /** + * An array of glob patterns indicating the files that the configuration + * object should apply to. If not specified, the configuration object applies + * to all files + */ + files?: (string | string[])[]; + + /** + * An array of glob patterns indicating the files that the configuration + * object should not apply to. If not specified, the configuration object + * applies to all files matched by files + */ + ignores?: string[]; + + /** + * The name of the language used for linting. This is used to determine the + * parser and other language-specific settings. + * @since 9.7.0 + */ + language?: string; + + /** + * An object containing settings related to how the language is configured for + * linting. + */ + languageOptions?: LanguageOptions; + + /** + * An object containing settings related to the linting process + */ + linterOptions?: LinterOptionsConfig; + + /** + * Either an object containing preprocess() and postprocess() methods or a + * string indicating the name of a processor inside of a plugin + * (i.e., "pluginName/processorName"). + */ + processor?: string | Processor; + + /** + * An object containing a name-value mapping of plugin names to plugin objects. + * When files is specified, these plugins are only available to the matching files. + */ + plugins?: Record; + + /** + * An object containing the configured rules. When files or ignores are specified, + * these rule configurations are only available to the matching files. + */ + rules?: Partial; + + /** + * An object containing name-value pairs of information that should be + * available to all rules. + */ + settings?: Record; +} + +//------------------------------------------------------------------------------ +// Legacy Config +// https://eslint.org/docs/latest/use/configure/configuration-files#legacy-config +//------------------------------------------------------------------------------ + +/* eslint-disable @typescript-eslint/consistent-indexed-object-style, @typescript-eslint/no-explicit-any -- needed for backward compatibility */ + +/** @deprecated Only supported in legacy eslintrc config format. */ +export type GlobalAccess = + | boolean + | "off" + | "readable" + | "readonly" + | "writable" + | "writeable"; + +/** @deprecated Only supported in legacy eslintrc config format. */ +export interface GlobalsConfig { + [name: string]: GlobalAccess; +} + +/** + * The ECMAScript version of the code being linted. + * @deprecated Only supported in legacy eslintrc config format. + */ +export type EcmaVersion = + | 3 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 13 + | 14 + | 15 + | 16 + | 17 + | 2015 + | 2016 + | 2017 + | 2018 + | 2019 + | 2020 + | 2021 + | 2022 + | 2023 + | 2024 + | 2025 + | 2026 + | "latest"; + +/** + * The type of JavaScript source code. + * @deprecated Only supported in legacy eslintrc config format. + */ +export type JavaScriptSourceType = "script" | "module" | "commonjs"; + +/** + * Parser options. + * @deprecated Only supported in legacy eslintrc config format. + * @see [Specifying Parser Options](https://eslint.org/docs/latest/use/configure/language-options#specifying-parser-options) + */ +export interface JavaScriptParserOptionsConfig { + /** + * Allow the use of reserved words as identifiers (if `ecmaVersion` is 3). + * + * @default false + */ + allowReserved?: boolean | undefined; + + /** + * Accepts any valid ECMAScript version number or `'latest'`: + * + * - A version: es3, es5, es6, es7, es8, es9, es10, es11, es12, es13, es14, ..., or + * - A year: es2015, es2016, es2017, es2018, es2019, es2020, es2021, es2022, es2023, ..., or + * - `'latest'` + * + * When it's a version or a year, the value must be a number - so do not include the `es` prefix. + * + * Specifies the version of ECMAScript syntax you want to use. This is used by the parser to determine how to perform scope analysis, and it affects the default + * + * @default 5 + */ + ecmaVersion?: EcmaVersion | undefined; + + /** + * The type of JavaScript source code. Possible values are "script" for + * traditional script files, "module" for ECMAScript modules (ESM), and + * "commonjs" for CommonJS files. + * + * @default 'script' + * + * @see https://eslint.org/docs/latest/use/configure/language-options-deprecated#specifying-parser-options + */ + sourceType?: JavaScriptSourceType | undefined; + + /** + * An object indicating which additional language features you'd like to use. + * + * @see https://eslint.org/docs/latest/use/configure/language-options-deprecated#specifying-parser-options + */ + ecmaFeatures?: + | { + globalReturn?: boolean | undefined; + impliedStrict?: boolean | undefined; + jsx?: boolean | undefined; + experimentalObjectRestSpread?: boolean | undefined; + [key: string]: any; + } + | undefined; + [key: string]: any; +} + +/** @deprecated Only supported in legacy eslintrc config format. */ +export interface EnvironmentConfig { + /** The definition of global variables. */ + globals?: GlobalsConfig | undefined; + + /** The parser options that will be enabled under this environment. */ + parserOptions?: JavaScriptParserOptionsConfig | undefined; +} + +/** + * A configuration object that may have a `rules` block. + */ +export interface HasRules { + rules?: Partial | undefined; +} + +/** + * ESLint legacy configuration. + * + * @see [ESLint Legacy Configuration](https://eslint.org/docs/latest/use/configure/) + */ +export interface BaseConfig< + Rules extends RulesConfig = RulesConfig, + OverrideRules extends RulesConfig = Rules, +> extends HasRules { + $schema?: string | undefined; + + /** + * An environment provides predefined global variables. + * + * @see [Environments](https://eslint.org/docs/latest/use/configure/language-options-deprecated#specifying-environments) + */ + env?: { [name: string]: boolean } | undefined; + + /** + * Extending configuration files. + * + * @see [Extends](https://eslint.org/docs/latest/use/configure/configuration-files-deprecated#extending-configuration-files) + */ + extends?: string | string[] | undefined; + + /** + * Specifying globals. + * + * @see [Globals](https://eslint.org/docs/latest/use/configure/language-options-deprecated#specifying-globals) + */ + globals?: GlobalsConfig | undefined; + + /** + * Disable processing of inline comments. + * + * @see [Disabling Inline Comments](https://eslint.org/docs/latest/use/configure/rules-deprecated#disabling-inline-comments) + */ + noInlineConfig?: boolean | undefined; + + /** + * Overrides can be used to use a differing configuration for matching sub-directories and files. + * + * @see [How do overrides work](https://eslint.org/docs/latest/use/configure/configuration-files-deprecated#how-do-overrides-work) + */ + overrides?: ConfigOverride[] | undefined; + + /** + * Parser. + * + * @see [Working with Custom Parsers](https://eslint.org/docs/latest/extend/custom-parsers) + * @see [Specifying Parser](https://eslint.org/docs/latest/use/configure/parser-deprecated) + */ + parser?: string | undefined; + + /** + * Parser options. + * + * @see [Working with Custom Parsers](https://eslint.org/docs/latest/extend/custom-parsers) + * @see [Specifying Parser Options](https://eslint.org/docs/latest/use/configure/language-options-deprecated#specifying-parser-options) + */ + parserOptions?: JavaScriptParserOptionsConfig | undefined; + + /** + * Which third-party plugins define additional rules, environments, configs, etc. for ESLint to use. + * + * @see [Configuring Plugins](https://eslint.org/docs/latest/use/configure/plugins-deprecated#configure-plugins) + */ + plugins?: string[] | undefined; + + /** + * Specifying processor. + * + * @see [processor](https://eslint.org/docs/latest/use/configure/plugins-deprecated#specify-a-processor) + */ + processor?: string | undefined; + + /** + * Report unused eslint-disable comments as warning. + * + * @see [Report unused eslint-disable comments](https://eslint.org/docs/latest/use/configure/rules-deprecated#report-unused-eslint-disable-comments) + */ + reportUnusedDisableDirectives?: boolean | undefined; + + /** + * Settings. + * + * @see [Settings](https://eslint.org/docs/latest/use/configure/configuration-files-deprecated#adding-shared-settings) + */ + settings?: SettingsConfig | undefined; +} + +/** + * The overwrites that apply more differing configuration to specific files or directories. + */ +export interface ConfigOverride + extends BaseConfig { + /** + * The glob patterns for excluded files. + */ + excludedFiles?: string | string[] | undefined; + + /** + * The glob patterns for target files. + */ + files: string | string[]; +} + +/** + * ESLint legacy configuration. + * + * @see [ESLint Legacy Configuration](https://eslint.org/docs/latest/use/configure/) + */ +// https://github.com/eslint/eslint/blob/v8.57.0/conf/config-schema.js +export interface LegacyConfigObject< + Rules extends RulesConfig = RulesConfig, + OverrideRules extends RulesConfig = Rules, +> extends BaseConfig { + /** + * Tell ESLint to ignore specific files and directories. + * + * @see [Ignore Patterns](https://eslint.org/docs/latest/use/configure/ignore-deprecated#ignorepatterns-in-config-files) + */ + ignorePatterns?: string | string[] | undefined; + + /** + * @see [Using Configuration Files](https://eslint.org/docs/latest/use/configure/configuration-files-deprecated#using-configuration-files) + */ + root?: boolean | undefined; +} + +/* eslint-enable @typescript-eslint/consistent-indexed-object-style, @typescript-eslint/no-explicit-any -- needed for backward compatibility */ + +//------------------------------------------------------------------------------ +// Processors +// https://eslint.org/docs/latest/extend/plugins#processors-in-plugins +//------------------------------------------------------------------------------ + +/** + * File information passed to a processor. + */ +export interface ProcessorFile { + text: string; + filename: string; +} + +/** + * A processor is an object that can preprocess and postprocess files. + */ +export interface Processor< + T extends string | ProcessorFile = string | ProcessorFile, +> extends ObjectMetaProperties { + /** If `true` then it means the processor supports autofix. */ + supportsAutofix?: boolean | undefined; + + /** The function to extract code blocks. */ + preprocess?(text: string, filename: string): T[]; + + /** The function to merge messages. */ + postprocess?(messages: LintMessage[][], filename: string): LintMessage[]; +} + +//------------------------------------------------------------------------------ +// Plugins +// https://eslint.org/docs/latest/extend/plugins +//------------------------------------------------------------------------------ + +export interface Plugin extends ObjectMetaProperties { + meta?: ObjectMetaProperties["meta"] & { + namespace?: string | undefined; + }; + configs?: + | Record + | undefined; + environments?: Record | undefined; + languages?: Record | undefined; + processors?: Record | undefined; + rules?: Record | undefined; +} + //------------------------------------------------------------------------------ // Languages //------------------------------------------------------------------------------ diff --git a/packages/core/tests/types/types.test.ts b/packages/core/tests/types/types.test.ts index 938ffd148..14935c2fe 100644 --- a/packages/core/tests/types/types.test.ts +++ b/packages/core/tests/types/types.test.ts @@ -23,6 +23,7 @@ import type { RuleDefinition, RulesConfig, RulesMeta, + RulesMetaDocs, RuleTextEdit, RuleTextEditor, RuleVisitor, @@ -33,6 +34,8 @@ import type { TraversalStep, } from "@eslint/core"; +import type { Linter } from "eslint"; + //----------------------------------------------------------------------------- // Helper types //----------------------------------------------------------------------------- @@ -240,6 +243,9 @@ const testRule: RuleDefinition<{ meta: { type: "problem", fixable: "code", + docs: { + recommended: true, + }, deprecated: { message: "use something else", url: "https://example.com", @@ -292,6 +298,7 @@ const testRule: RuleDefinition<{ start: { line: node.start, column: 1 }, end: { line: node.start + 1, column: Infinity }, }, + data: undefined, fix(fixer: RuleTextEditor): RuleTextEdit { return fixer.replaceText( node, @@ -312,6 +319,12 @@ const testRule: RuleDefinition<{ suggest: [ { messageId: "Bar", + data: { + foo: "foo", + bar: 1, + baz: true, + }, + // @ts-expect-error -- 'fix' is required in suggestion objects fix: null, }, ], @@ -322,6 +335,11 @@ const testRule: RuleDefinition<{ context.report({ message: "This baz is foobar", loc: { line: node.start, column: 1 }, + data: { + foo: "foo", + bar: 1, + baz: true, + }, fix: null, suggest: null, }); @@ -413,3 +431,67 @@ export type Rule5 = TestRuleDefinition<{ Code: TestSourceCode }>; // @ts-expect-error -- undefined value not allow for optional property (assumes `exactOptionalPropertyTypes` tsc compiler option) export type Rule6 = TestRuleDefinition<{ RuleOptions: undefined }>; + +export const shouldAllowRecommendedBoolean: RulesMetaDocs = { + recommended: true, +}; +export const shouldAllowRecommendedString: RulesMetaDocs = { + recommended: "strict", +}; + +export const shouldAllowRecommendedObject: RulesMetaDocs = { + recommended: { + someKey: "some value", + }, +}; + +//------------------------------------------------------------------------------ +// Tests for config object types +//------------------------------------------------------------------------------ + +import type { ConfigObject, LegacyConfigObject } from "@eslint/core"; + +// Example ConfigObject (flat config) +const configObjectExample: ConfigObject = { + name: "example config", + files: ["**/*.js", ["**/*.ts", "**/src/*.*"]], + ignores: ["**/vendor/**"], + language: "js/js", + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + }, + linterOptions: { + noInlineConfig: false, + reportUnusedDisableDirectives: true, + }, + plugins: { + custom: { meta: { name: "custom-plugin", version: "1.0.0" } }, + }, + rules: { + "no-console": "warn", + eqeqeq: ["error", "always"], + }, + settings: { + foo: "bar", + }, +}; + +// check back compat +const oldConfigObjectExample: Linter.Config = configObjectExample; + +// Example LegacyConfigObject (eslintrc config) +const legacyConfigObjectExample: LegacyConfigObject = { + $schema: "https://json.schemastore.org/eslintrc", + env: { node: true, es2021: true }, + extends: ["eslint:recommended", "plugin:custom/recommended"], + globals: { myGlobal: "readonly", foo: "writable", bar: "off" }, + rules: { + "no-console": 2, + eqeqeq: ["error", "always"], + }, +}; + +// check back compat +const oldLegacyConfigObjectExample: Linter.LegacyConfig = + legacyConfigObjectExample; diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index ed1ddf274..5de7eb399 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.2.0](https://github.com/eslint/rewrite/compare/mcp-v0.1.1...mcp-v0.2.0) (2025-11-14) + + +### ⚠ BREAKING CHANGES + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) + +### Features + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) ([acc623c](https://github.com/eslint/rewrite/commit/acc623c807bf8237a26b18291f04dd99e4e4981a)) + ## [0.1.1](https://github.com/eslint/rewrite/compare/mcp-v0.1.0...mcp-v0.1.1) (2025-07-30) diff --git a/packages/mcp/README.md b/packages/mcp/README.md index f09f4dc6f..aa68dcd3d 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -34,9 +34,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

Qlty Software trunk.io Shopify

Silver Sponsors

-

Vite Liftoff American Express StackBlitz

Bronze Sponsors

-

Cybozu Sentry Anagram Solver Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

+

Qlty Software Shopify

Silver Sponsors

+

Vite Liftoff American Express StackBlitz

Bronze Sponsors

+

Cybozu Syntax Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

diff --git a/packages/mcp/package.json b/packages/mcp/package.json index e90520418..7227000f6 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/mcp", - "version": "0.1.1", + "version": "0.2.0", "description": "MCP server for ESLint", "type": "module", "bin": "./src/mcp-cli.js", @@ -15,7 +15,7 @@ }, "scripts": { "build": "tsc", - "test": "mocha tests/*.js", + "test": "mocha \"tests/**/*.test.js\"", "test:coverage": "c8 npm test" }, "repository": { @@ -33,11 +33,15 @@ }, "homepage": "https://github.com/eslint/rewrite/tree/main/packages/mcp#readme", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.11.0", - "eslint": "^9.26.0", + "@modelcontextprotocol/sdk": "^1.22.0", + "eslint": "^9.39.1", "zod": "^3.24.4" + }, + "devDependencies": { + "@cfworker/json-schema": "^4.1.1", + "@types/node": "^24.7.2" } } diff --git a/packages/mcp/src/mcp-server.js b/packages/mcp/src/mcp-server.js index af3604cf6..554b85adc 100644 --- a/packages/mcp/src/mcp-server.js +++ b/packages/mcp/src/mcp-server.js @@ -17,7 +17,7 @@ import { ESLint } from "eslint"; const mcpServer = new McpServer({ name: "ESLint", - version: "0.1.1", // x-release-please-version + version: "0.2.0", // x-release-please-version }); // Important: Cursor throws an error when `describe()` is used in the schema. diff --git a/packages/mcp/tests/mcp-server.test.js b/packages/mcp/tests/mcp-server.test.js index d7be975a2..fdc6088fb 100644 --- a/packages/mcp/tests/mcp-server.test.js +++ b/packages/mcp/tests/mcp-server.test.js @@ -23,8 +23,6 @@ const passingFilePath = path.join(dirname, "fixtures", "passing.js"); const syntaxErrorFilePath = path.join(dirname, "fixtures", "syntax-error.js"); const filePathsJsonSchema = { - $schema: "http://json-schema.org/draft-07/schema#", - additionalProperties: false, properties: { filePaths: { items: { diff --git a/packages/migrate-config/CHANGELOG.md b/packages/migrate-config/CHANGELOG.md index 3d44999e3..ff216a1d1 100644 --- a/packages/migrate-config/CHANGELOG.md +++ b/packages/migrate-config/CHANGELOG.md @@ -1,5 +1,61 @@ # Changelog +## [2.0.0](https://github.com/eslint/rewrite/compare/migrate-config-v1.6.1...migrate-config-v2.0.0) (2025-11-14) + + +### ⚠ BREAKING CHANGES + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) + +### Features + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) ([acc623c](https://github.com/eslint/rewrite/commit/acc623c807bf8237a26b18291f04dd99e4e4981a)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/compat bumped from ^1.4.1 to ^2.0.0 + * devDependencies + * @eslint/core bumped from ^0.17.0 to ^1.0.0 + +## [1.6.1](https://github.com/eslint/rewrite/compare/migrate-config-v1.6.0...migrate-config-v1.6.1) (2025-10-27) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/compat bumped from ^1.4.0 to ^1.4.1 + * devDependencies + * @eslint/core bumped from ^0.16.0 to ^0.17.0 + +## [1.6.0](https://github.com/eslint/rewrite/compare/migrate-config-v1.5.3...migrate-config-v1.6.0) (2025-09-16) + + +### Features + +* Add config types in @eslint/core ([#237](https://github.com/eslint/rewrite/issues/237)) ([7b6dd37](https://github.com/eslint/rewrite/commit/7b6dd370a598ea7fc94fba427a2579342b50b90f)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/compat bumped from ^1.3.2 to ^1.4.0 + * devDependencies + * @eslint/core bumped from ^0.15.2 to ^0.16.0 + +## [1.5.3](https://github.com/eslint/rewrite/compare/migrate-config-v1.5.2...migrate-config-v1.5.3) (2025-08-05) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/compat bumped from ^1.3.1 to ^1.3.2 + ## [1.5.2](https://github.com/eslint/rewrite/compare/migrate-config-v1.5.1...migrate-config-v1.5.2) (2025-06-25) diff --git a/packages/migrate-config/README.md b/packages/migrate-config/README.md index 58f4be3dc..69601014c 100644 --- a/packages/migrate-config/README.md +++ b/packages/migrate-config/README.md @@ -101,9 +101,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

Qlty Software trunk.io Shopify

Silver Sponsors

-

Vite Liftoff American Express StackBlitz

Bronze Sponsors

-

Cybozu Sentry Anagram Solver Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

+

Qlty Software Shopify

Silver Sponsors

+

Vite Liftoff American Express StackBlitz

Bronze Sponsors

+

Cybozu Syntax Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

diff --git a/packages/migrate-config/package.json b/packages/migrate-config/package.json index b6e123170..171746973 100644 --- a/packages/migrate-config/package.json +++ b/packages/migrate-config/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/migrate-config", - "version": "1.5.2", + "version": "2.0.0", "description": "Configuration migration for ESLint", "type": "module", "bin": { @@ -16,7 +16,7 @@ "test": "tests" }, "scripts": { - "test": "mocha tests/*.js", + "test": "mocha \"tests/**/*.test.js\"", "test:coverage": "c8 npm test" }, "repository": { @@ -39,17 +39,17 @@ }, "homepage": "https://github.com/eslint/rewrite/tree/main/packages/migrate-config#readme", "devDependencies": { - "@types/eslint": "^9.6.0", + "@eslint/core": "^1.0.0", "eslint": "^9.27.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "dependencies": { - "@eslint/compat": "^1.3.1", - "@eslint/eslintrc": "^3.1.0", + "@eslint/compat": "^2.0.0", + "@eslint/eslintrc": "^3.3.1", "camelcase": "^8.0.0", - "espree": "^10.3.0", + "espree": "^10.4.0", "recast": "^0.23.7" } } diff --git a/packages/migrate-config/src/migrate-config-cli.js b/packages/migrate-config/src/migrate-config-cli.js index b2802ab91..c881de639 100755 --- a/packages/migrate-config/src/migrate-config-cli.js +++ b/packages/migrate-config/src/migrate-config-cli.js @@ -23,6 +23,7 @@ import fsp from "node:fs/promises"; import path from "node:path"; import { migrateConfig, migrateJSConfig } from "./migrate-config.js"; +// @ts-ignore: No types available import { Legacy } from "@eslint/eslintrc"; //----------------------------------------------------------------------------- diff --git a/packages/migrate-config/src/migrate-config.js b/packages/migrate-config/src/migrate-config.js index c3093279e..b1c33fd7b 100644 --- a/packages/migrate-config/src/migrate-config.js +++ b/packages/migrate-config/src/migrate-config.js @@ -8,6 +8,7 @@ //----------------------------------------------------------------------------- import * as recast from "recast"; +// @ts-ignore: No types available import { Legacy } from "@eslint/eslintrc"; import camelCase from "camelcase"; import pluginsNeedingCompat from "./compat-plugins.js"; @@ -19,9 +20,9 @@ import * as espree from "espree"; // Types //----------------------------------------------------------------------------- -/** @typedef {import("eslint").Linter.FlatConfig} FlatConfig */ -/** @typedef {import("eslint").Linter.LegacyConfig} LegacyConfig */ -/** @typedef {import("eslint").Linter.ConfigOverride} ConfigOverride */ +/** @typedef {import("@eslint/core").ConfigObject} FlatConfig */ +/** @typedef {import("@eslint/core").LegacyConfigObject} LegacyConfig */ +/** @typedef {import("@eslint/core").ConfigOverride} ConfigOverride */ /** @typedef {import("recast").types.namedTypes.ObjectExpression} ObjectExpression */ /** @typedef {import("recast").types.namedTypes.ArrayExpression} ArrayExpression */ /** @typedef {import("recast").types.namedTypes.CallExpression} CallExpression */ @@ -144,7 +145,7 @@ const { naming } = Legacy; * @returns {boolean} `true` if the name is a valid identifier. */ function isValidIdentifier(name) { - return /^[a-z_$][0-9a-z_$]*$/iu.test(name); + return /^[a-z_$][\w$]*$/iu.test(name); } /** diff --git a/packages/object-schema/CHANGELOG.md b/packages/object-schema/CHANGELOG.md index 65023434e..736a27e8d 100644 --- a/packages/object-schema/CHANGELOG.md +++ b/packages/object-schema/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [3.0.0](https://github.com/eslint/rewrite/compare/object-schema-v2.1.7...object-schema-v3.0.0) (2025-11-14) + + +### ⚠ BREAKING CHANGES + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) + +### Features + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) ([acc623c](https://github.com/eslint/rewrite/commit/acc623c807bf8237a26b18291f04dd99e4e4981a)) + +## [2.1.7](https://github.com/eslint/rewrite/compare/object-schema-v2.1.6...object-schema-v2.1.7) (2025-10-17) + + +### Bug Fixes + +* fix `config-array` and `object-schema` types ([#294](https://github.com/eslint/rewrite/issues/294)) ([a902bc4](https://github.com/eslint/rewrite/commit/a902bc4e27639ba5975b5d793314235737dc2c1a)) +* improve type support for isolated dependencies in pnpm ([#289](https://github.com/eslint/rewrite/issues/289)) ([f8df139](https://github.com/eslint/rewrite/commit/f8df139631694431ecfc651e656932e283d4d14f)) + ## [2.1.6](https://github.com/eslint/rewrite/compare/object-schema-v2.1.5...object-schema-v2.1.6) (2025-01-31) diff --git a/packages/object-schema/README.md b/packages/object-schema/README.md index 7914fa7dd..ecd0835c8 100644 --- a/packages/object-schema/README.md +++ b/packages/object-schema/README.md @@ -233,9 +233,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

Qlty Software trunk.io Shopify

Silver Sponsors

-

Vite Liftoff American Express StackBlitz

Bronze Sponsors

-

Cybozu Sentry Anagram Solver Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

+

Qlty Software Shopify

Silver Sponsors

+

Vite Liftoff American Express StackBlitz

Bronze Sponsors

+

Cybozu Syntax Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

diff --git a/packages/object-schema/jsr.json b/packages/object-schema/jsr.json index 397f8a860..7d958927c 100644 --- a/packages/object-schema/jsr.json +++ b/packages/object-schema/jsr.json @@ -1,6 +1,6 @@ { "name": "@eslint/object-schema", - "version": "2.1.6", + "version": "3.0.0", "exports": "./dist/esm/index.js", "publish": { "include": [ diff --git a/packages/object-schema/package.json b/packages/object-schema/package.json index ea0d939af..9944a0c2e 100644 --- a/packages/object-schema/package.json +++ b/packages/object-schema/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/object-schema", - "version": "2.1.6", + "version": "3.0.0", "description": "An object schema merger/validator", "type": "module", "main": "dist/esm/index.js", @@ -25,11 +25,13 @@ "test": "tests" }, "scripts": { + "build:dedupe-types": "node ../../tools/dedupe-types.js dist/cjs/index.cjs dist/esm/index.js", "build:cts": "node ../../tools/build-cts.js dist/esm/index.d.ts dist/cjs/index.d.cts", - "build": "rollup -c && tsc -p tsconfig.esm.json && npm run build:cts", + "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts", + "test": "mocha \"tests/**/*.test.js\"", + "test:coverage": "c8 npm test", "test:jsr": "npx jsr@latest publish --dry-run", - "test": "mocha tests/", - "test:coverage": "c8 npm test" + "test:types": "tsc -p tests/types/tsconfig.json" }, "repository": { "type": "git", @@ -52,6 +54,6 @@ "rollup-plugin-copy": "^3.5.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } } diff --git a/packages/object-schema/rollup.config.js b/packages/object-schema/rollup.config.js index 03a11e126..5b813a884 100644 --- a/packages/object-schema/rollup.config.js +++ b/packages/object-schema/rollup.config.js @@ -16,7 +16,7 @@ export default { plugins: [ copy({ targets: [ - { src: "src/types.ts", dest: "dist/cjs" }, + { src: "src/types.ts", dest: "dist/cjs", rename: "types.cts" }, { src: "src/types.ts", dest: "dist/esm" }, ], }), diff --git a/packages/object-schema/tests/merge-strategy.js b/packages/object-schema/tests/merge-strategy.test.js similarity index 100% rename from packages/object-schema/tests/merge-strategy.js rename to packages/object-schema/tests/merge-strategy.test.js diff --git a/packages/object-schema/tests/object-schema.js b/packages/object-schema/tests/object-schema.test.js similarity index 100% rename from packages/object-schema/tests/object-schema.js rename to packages/object-schema/tests/object-schema.test.js diff --git a/packages/object-schema/tests/types/cjs-import.test.cts b/packages/object-schema/tests/types/cjs-import.test.cts new file mode 100644 index 000000000..df767d4a3 --- /dev/null +++ b/packages/object-schema/tests/types/cjs-import.test.cts @@ -0,0 +1,10 @@ +/** + * @fileoverview CommonJS type import test for ObjectSchema package. + * @author Francesco Trotta + */ + +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import "@eslint/object-schema"; diff --git a/packages/object-schema/tests/types/tsconfig.json b/packages/object-schema/tests/types/tsconfig.json new file mode 100644 index 000000000..638c0f3a5 --- /dev/null +++ b/packages/object-schema/tests/types/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "../.." + }, + "include": [".", "../../dist"] +} diff --git a/packages/object-schema/tests/types/types.test.ts b/packages/object-schema/tests/types/types.test.ts new file mode 100644 index 000000000..1cd89a875 --- /dev/null +++ b/packages/object-schema/tests/types/types.test.ts @@ -0,0 +1,10 @@ +/** + * @fileoverview Type tests for ObjectSchema package. + * @author Francesco Trotta + */ + +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import "@eslint/object-schema"; diff --git a/packages/object-schema/tests/validation-strategy.js b/packages/object-schema/tests/validation-strategy.test.js similarity index 100% rename from packages/object-schema/tests/validation-strategy.js rename to packages/object-schema/tests/validation-strategy.test.js diff --git a/packages/plugin-kit/CHANGELOG.md b/packages/plugin-kit/CHANGELOG.md index 8d4b98356..30969659a 100644 --- a/packages/plugin-kit/CHANGELOG.md +++ b/packages/plugin-kit/CHANGELOG.md @@ -1,5 +1,55 @@ # Changelog +## [0.5.0](https://github.com/eslint/rewrite/compare/plugin-kit-v0.4.1...plugin-kit-v0.5.0) (2025-11-14) + + +### ⚠ BREAKING CHANGES + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) + +### Features + +* Require Node.js ^20.19.0 || ^22.13.0 || >=24 ([#297](https://github.com/eslint/rewrite/issues/297)) ([acc623c](https://github.com/eslint/rewrite/commit/acc623c807bf8237a26b18291f04dd99e4e4981a)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/core bumped from ^0.17.0 to ^1.0.0 + +## [0.4.1](https://github.com/eslint/rewrite/compare/plugin-kit-v0.4.0...plugin-kit-v0.4.1) (2025-10-27) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/core bumped from ^0.16.0 to ^0.17.0 + +## [0.4.0](https://github.com/eslint/rewrite/compare/plugin-kit-v0.3.5...plugin-kit-v0.4.0) (2025-09-16) + + +### Features + +* add support for `getLocFromIndex` and `getIndexFromLoc` ([#212](https://github.com/eslint/rewrite/issues/212)) ([a3588d8](https://github.com/eslint/rewrite/commit/a3588d8fb2dc6b9a0b39b26a49d0cdd437646d49)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/core bumped from ^0.15.2 to ^0.16.0 + +## [0.3.5](https://github.com/eslint/rewrite/compare/plugin-kit-v0.3.4...plugin-kit-v0.3.5) (2025-08-05) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @eslint/core bumped from ^0.15.1 to ^0.15.2 + ## [0.3.4](https://github.com/eslint/rewrite/compare/plugin-kit-v0.3.3...plugin-kit-v0.3.4) (2025-07-21) diff --git a/packages/plugin-kit/README.md b/packages/plugin-kit/README.md index 9196372a5..52da7a968 100644 --- a/packages/plugin-kit/README.md +++ b/packages/plugin-kit/README.md @@ -204,9 +204,11 @@ class MySourceCode { The `TextSourceCodeBase` class is intended to be a base class that has several of the common members found in `SourceCode` objects already implemented. Those members are: - `lines` - an array of text lines that is created automatically when the constructor is called. -- `getLoc(node)` - gets the location of a node. Works for nodes that have the ESLint-style `loc` property and nodes that have the Unist-style [`position` property](https://github.com/syntax-tree/unist?tab=readme-ov-file#position). If you're using an AST with a different location format, you'll still need to implement this method yourself. -- `getRange(node)` - gets the range of a node within the source text. Works for nodes that have the ESLint-style `range` property and nodes that have the Unist-style [`position` property](https://github.com/syntax-tree/unist?tab=readme-ov-file#position). If you're using an AST with a different range format, you'll still need to implement this method yourself. -- `getText(nodeOrToken, charsBefore, charsAfter)` - gets the source text for the given node or token that has range information attached. Optionally, can return additional characters before and after the given node or token. As long as `getRange()` is properly implemented, this method will just work. +- `getLoc(nodeOrToken)` - gets the location of a node or token. Works for nodes that have the ESLint-style `loc` property and nodes that have the Unist-style [`position` property](https://github.com/syntax-tree/unist?tab=readme-ov-file#position). If you're using an AST with a different location format, you'll still need to implement this method yourself. +- `getLocFromIndex(index)` - Converts a source text index into a `{ line: number, column: number }` pair. (For this method to work, the root node should always cover the entire source code text, and the `getLoc()` method needs to be implemented correctly.) +- `getIndexFromLoc(loc)` - Converts a `{ line: number, column: number }` pair into a source text index. (For this method to work, the root node should always cover the entire source code text, and the `getLoc()` method needs to be implemented correctly.) +- `getRange(nodeOrToken)` - gets the range of a node or token within the source text. Works for nodes that have the ESLint-style `range` property and nodes that have the Unist-style [`position` property](https://github.com/syntax-tree/unist?tab=readme-ov-file#position). If you're using an AST with a different range format, you'll still need to implement this method yourself. +- `getText(node, beforeCount, afterCount)` - gets the source text for the given node that has range information attached. Optionally, can return additional characters before and after the given node. As long as `getRange()` is properly implemented, this method will just work. - `getAncestors(node)` - returns the ancestry of the node. In order for this to work, you must implement the `getParent()` method yourself. Here's an example: @@ -262,9 +264,9 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).

Platinum Sponsors

Automattic Airbnb

Gold Sponsors

-

Qlty Software trunk.io Shopify

Silver Sponsors

-

Vite Liftoff American Express StackBlitz

Bronze Sponsors

-

Cybozu Sentry Anagram Solver Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

+

Qlty Software Shopify

Silver Sponsors

+

Vite Liftoff American Express StackBlitz

Bronze Sponsors

+

Cybozu Syntax Icons8 Discord GitBook Nx Mercedes-Benz Group HeroCoders LambdaTest

Technology Sponsors

Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.

Netlify Algolia 1Password

diff --git a/packages/plugin-kit/jsr.json b/packages/plugin-kit/jsr.json index 737dad060..8a3a66e36 100644 --- a/packages/plugin-kit/jsr.json +++ b/packages/plugin-kit/jsr.json @@ -1,6 +1,6 @@ { "name": "@eslint/plugin-kit", - "version": "0.3.4", + "version": "0.5.0", "exports": "./dist/esm/index.js", "publish": { "include": [ diff --git a/packages/plugin-kit/package.json b/packages/plugin-kit/package.json index 4c67b9191..fd1017a4a 100644 --- a/packages/plugin-kit/package.json +++ b/packages/plugin-kit/package.json @@ -1,6 +1,6 @@ { "name": "@eslint/plugin-kit", - "version": "0.3.4", + "version": "0.5.0", "description": "Utilities for building ESLint plugins.", "author": "Nicholas C. Zakas", "type": "module", @@ -36,7 +36,7 @@ "build:cts": "node ../../tools/build-cts.js dist/esm/index.d.ts dist/cjs/index.d.cts", "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts", "pretest": "npm run build", - "test": "mocha tests/", + "test": "mocha \"tests/**/*.test.js\"", "test:coverage": "c8 npm test", "test:jsr": "npx jsr@latest publish --dry-run", "test:types": "tsc -p tests/types/tsconfig.json" @@ -48,7 +48,7 @@ ], "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^1.0.0", "levn": "^0.4.1" }, "devDependencies": { @@ -56,6 +56,6 @@ "rollup-plugin-copy": "^3.5.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } } diff --git a/packages/plugin-kit/src/config-comment-parser.js b/packages/plugin-kit/src/config-comment-parser.js index b3bc90e34..c0f08be02 100644 --- a/packages/plugin-kit/src/config-comment-parser.js +++ b/packages/plugin-kit/src/config-comment-parser.js @@ -156,7 +156,7 @@ export class ConfigCommentParser { */ const normalizedString = string .replace(/(?} */ export class TextSourceCodeBase { @@ -224,7 +250,19 @@ export class TextSourceCodeBase { * The lines of text in the source code. * @type {Array} */ - #lines; + #lines = []; + + /** + * The indices of the start of each line in the source code. + * @type {Array} + */ + #lineStartIndices = [0]; + + /** + * The pattern to match lineEndings in the source code. + * @type {RegExp} + */ + #lineEndingPattern; /** * The AST of the source code. @@ -243,12 +281,105 @@ export class TextSourceCodeBase { * @param {Object} options The options for the instance. * @param {string} options.text The source code text. * @param {Options['RootNode']} options.ast The root AST node. - * @param {RegExp} [options.lineEndingPattern] The pattern to match lineEndings in the source code. + * @param {RegExp} [options.lineEndingPattern] The pattern to match lineEndings in the source code. Defaults to `/\r?\n/u`. */ constructor({ text, ast, lineEndingPattern = /\r?\n/u }) { this.ast = ast; this.text = text; - this.#lines = text.split(lineEndingPattern); + // Remove the global(`g`) and sticky(`y`) flags from the `lineEndingPattern` to avoid issues with lastIndex. + this.#lineEndingPattern = new RegExp( + lineEndingPattern.source, + lineEndingPattern.flags.replace(/[gy]/gu, ""), + ); + } + + /** + * Finds the next line in the source text and updates `#lines` and `#lineStartIndices`. + * @param {string} text The text to search for the next line. + * @returns {boolean} `true` if a next line was found, `false` otherwise. + */ + #findNextLine(text) { + const match = this.#lineEndingPattern.exec(text); + + if (!match) { + return false; + } + + this.#lines.push(text.slice(0, match.index)); + this.#lineStartIndices.push( + (this.#lineStartIndices.at(-1) ?? 0) + + match.index + + match[0].length, + ); + + return true; + } + + /** + * Ensures `#lines` is lazily calculated from the source text. + * @returns {void} + */ + #ensureLines() { + // If `#lines` has already been calculated, do nothing. + if (this.#lines.length === this.#lineStartIndices.length) { + return; + } + + while ( + this.#findNextLine(this.text.slice(this.#lineStartIndices.at(-1))) + ) { + // Continue parsing until no more matches are found. + } + + this.#lines.push(this.text.slice(this.#lineStartIndices.at(-1))); + + Object.freeze(this.#lines); + } + + /** + * Ensures `#lineStartIndices` is lazily calculated up to the specified index. + * @param {number} index The index of a character in a file. + * @returns {void} + */ + #ensureLineStartIndicesFromIndex(index) { + // If we've already parsed up to or beyond this index, do nothing. + if (index <= (this.#lineStartIndices.at(-1) ?? 0)) { + return; + } + + while ( + index > (this.#lineStartIndices.at(-1) ?? 0) && + this.#findNextLine(this.text.slice(this.#lineStartIndices.at(-1))) + ) { + // Continue parsing until no more matches are found. + } + } + + /** + * Ensures `#lineStartIndices` is lazily calculated up to the specified loc. + * @param {Object} loc A line/column location. + * @param {number} loc.line The line number of the location. (0 or 1-indexed based on language.) + * @param {number} lineStart The line number at which the parser starts counting. + * @returns {void} + */ + #ensureLineStartIndicesFromLoc(loc, lineStart) { + // Calculate line indices up to the potentially next line, as it is needed for the follow‑up calculation. + const nextLocLineIndex = loc.line - lineStart + 1; + const lastCalculatedLineIndex = this.#lineStartIndices.length - 1; + let additionalLinesNeeded = nextLocLineIndex - lastCalculatedLineIndex; + + // If we've already parsed up to or beyond this line, do nothing. + if (additionalLinesNeeded <= 0) { + return; + } + + while ( + additionalLinesNeeded > 0 && + this.#findNextLine(this.text.slice(this.#lineStartIndices.at(-1))) + ) { + // Continue parsing until no more matches are found or we have enough lines. + additionalLinesNeeded -= 1; + } } /** @@ -271,6 +402,135 @@ export class TextSourceCodeBase { ); } + /** + * Converts a source text index into a `{ line: number, column: number }` pair. + * @param {number} index The index of a character in a file. + * @throws {TypeError|RangeError} If non-numeric index or index out of range. + * @returns {{line: number, column: number}} A `{ line: number, column: number }` location object with 0 or 1-indexed line and 0 or 1-indexed column based on language. + * @public + */ + getLocFromIndex(index) { + if (typeof index !== "number") { + throw new TypeError("Expected `index` to be a number."); + } + + if (index < 0 || index > this.text.length) { + throw new RangeError( + `Index out of range (requested index ${index}, but source text has length ${this.text.length}).`, + ); + } + + const { + start: { line: lineStart, column: columnStart }, + end: { line: lineEnd, column: columnEnd }, + } = this.getLoc(this.ast); + + // If the index is at the start, return the start location of the root node. + if (index === 0) { + return { + line: lineStart, + column: columnStart, + }; + } + + // If the index is `this.text.length`, return the location one "spot" past the last character of the file. + if (index === this.text.length) { + return { + line: lineEnd, + column: columnEnd, + }; + } + + // Ensure `#lineStartIndices` are lazily calculated. + this.#ensureLineStartIndicesFromIndex(index); + + /* + * To figure out which line `index` is on, determine the last place at which index could + * be inserted into `#lineStartIndices` to keep the list sorted. + */ + const lineNumber = + (index >= (this.#lineStartIndices.at(-1) ?? 0) + ? this.#lineStartIndices.length + : findLineNumberBinarySearch(this.#lineStartIndices, index)) - + 1 + + lineStart; + + return { + line: lineNumber, + column: + index - + this.#lineStartIndices[lineNumber - lineStart] + + columnStart, + }; + } + + /** + * Converts a `{ line: number, column: number }` pair into a source text index. + * @param {Object} loc A line/column location. + * @param {number} loc.line The line number of the location. (0 or 1-indexed based on language.) + * @param {number} loc.column The column number of the location. (0 or 1-indexed based on language.) + * @throws {TypeError|RangeError} If `loc` is not an object with a numeric + * `line` and `column`, if the `line` is less than or equal to zero or + * the `line` or `column` is out of the expected range. + * @returns {number} The index of the line/column location in a file. + * @public + */ + getIndexFromLoc(loc) { + if ( + loc === null || + typeof loc !== "object" || + typeof loc.line !== "number" || + typeof loc.column !== "number" + ) { + throw new TypeError( + "Expected `loc` to be an object with numeric `line` and `column` properties.", + ); + } + + const { + start: { line: lineStart, column: columnStart }, + end: { line: lineEnd, column: columnEnd }, + } = this.getLoc(this.ast); + + if (loc.line < lineStart || lineEnd < loc.line) { + throw new RangeError( + `Line number out of range (line ${loc.line} requested). Valid range: ${lineStart}-${lineEnd}`, + ); + } + + // If the loc is at the start, return the start index of the root node. + if (loc.line === lineStart && loc.column === columnStart) { + return 0; + } + + // If the loc is at the end, return the index one "spot" past the last character of the file. + if (loc.line === lineEnd && loc.column === columnEnd) { + return this.text.length; + } + + // Ensure `#lineStartIndices` are lazily calculated. + this.#ensureLineStartIndicesFromLoc(loc, lineStart); + + const isLastLine = loc.line === lineEnd; + const lineStartIndex = this.#lineStartIndices[loc.line - lineStart]; + const lineEndIndex = isLastLine + ? this.text.length + : this.#lineStartIndices[loc.line - lineStart + 1]; + const positionIndex = lineStartIndex + loc.column - columnStart; + + if ( + loc.column < columnStart || + (isLastLine && positionIndex > lineEndIndex) || + (!isLastLine && positionIndex >= lineEndIndex) + ) { + throw new RangeError( + `Column number out of range (column ${loc.column} requested). Valid range for line ${loc.line}: ${columnStart}-${lineEndIndex - lineStartIndex + columnStart + (isLastLine ? 0 : -1)}`, + ); + } + + return positionIndex; + } + /** * Returns the range information for the given node or token. * @param {Options['SyntaxElementWithLoc']} nodeOrToken The node or token to get the range information for. @@ -356,6 +616,8 @@ export class TextSourceCodeBase { * @public */ get lines() { + this.#ensureLines(); // Ensure `#lines` is lazily calculated. + return this.#lines; } diff --git a/packages/plugin-kit/tests/source-code.test.js b/packages/plugin-kit/tests/source-code.test.js index dcb908428..cdd3f5ebd 100644 --- a/packages/plugin-kit/tests/source-code.test.js +++ b/packages/plugin-kit/tests/source-code.test.js @@ -155,6 +155,1712 @@ describe("source-code", () => { }); }); + describe("getLocFromIndex()", () => { + it("should throw an error for non-numeric index", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getLocFromIndex("5"); + }, + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, + ); + assert.throws( + () => { + sourceCode.getLocFromIndex(null); + }, + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, + ); + assert.throws( + () => { + sourceCode.getLocFromIndex(undefined); + }, + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, + ); + assert.throws( + () => { + sourceCode.getLocFromIndex(true); + }, + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, + ); + assert.throws( + () => { + sourceCode.getLocFromIndex(false); + }, + { + name: "TypeError", + message: "Expected `index` to be a number.", + }, + ); + }); + + it("should throw an error for negative index", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getLocFromIndex(-1); + }, + { + name: "RangeError", + message: + "Index out of range (requested index -1, but source text has length 7).", + }, + ); + }); + + it("should throw an error for index beyond text length", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getLocFromIndex(text.length + 1); + }, + { + name: "RangeError", + message: + "Index out of range (requested index 8, but source text has length 7).", + }, + ); + }); + + it("should throw an error when `ast.loc` or `ast.position` is not defined", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getLocFromIndex(0); + }, + { + name: "Error", + message: + "Custom getLoc() method must be implemented in the subclass.", + }, + ); + }); + + it("should handle the special case of `text.length` when lineStart is 1 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 2, + column: 3, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.deepStrictEqual( + sourceCode.getLocFromIndex(text.length), + { + line: 2, + column: 3, + }, + ); + }); + + it("should handle the special case of `text.length` when lineStart is 0 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 1, + column: 4, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.deepStrictEqual( + sourceCode.getLocFromIndex(text.length), + { + line: 1, + column: 4, + }, + ); + }); + + it("should handle the special case of `text.length` when lineStart is 0 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 1, + column: 3, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.deepStrictEqual( + sourceCode.getLocFromIndex(text.length), + { + line: 1, + column: 3, + }, + ); + }); + + it("should handle the special case of `text.length` when lineStart is 1 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 2, + column: 4, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.deepStrictEqual( + sourceCode.getLocFromIndex(text.length), + { + line: 2, + column: 4, + }, + ); + }); + + it("should convert index to location when random index is given", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 3, + column: 3, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(3), { + line: 1, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(9), { + line: 3, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(1), { + line: 1, + column: 1, + }); // Please do not change the order of these tests. It's for checking lazy calculation. + }); + + it("should convert index to location when lineStart is 1 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 3, + column: 3, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 1, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(1), { + line: 1, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(2), { + line: 1, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(3), { + line: 1, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(4), { + line: 2, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(5), { + line: 2, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(6), { + line: 2, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(7), { + line: 2, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(8), { + line: 2, + column: 4, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(9), { + line: 3, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(10), { + line: 3, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(11), { + line: 3, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(12), { + line: 3, + column: 3, + }); + }); + + it("should convert index to location when lineStart is 0 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 2, + column: 4, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 0, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(1), { + line: 0, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(2), { + line: 0, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(3), { + line: 0, + column: 4, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(4), { + line: 1, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(5), { + line: 1, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(6), { + line: 1, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(7), { + line: 1, + column: 4, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(8), { + line: 1, + column: 5, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(9), { + line: 2, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(10), { + line: 2, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(11), { + line: 2, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(12), { + line: 2, + column: 4, + }); + }); + + it("should convert index to location when lineStart is 0 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 2, + column: 3, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 0, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(1), { + line: 0, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(2), { + line: 0, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(3), { + line: 0, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(4), { + line: 1, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(5), { + line: 1, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(6), { + line: 1, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(7), { + line: 1, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(8), { + line: 1, + column: 4, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(9), { + line: 2, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(10), { + line: 2, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(11), { + line: 2, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(12), { + line: 2, + column: 3, + }); + }); + + it("should convert index to location when lineStart is 1 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 3, + column: 4, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 1, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(1), { + line: 1, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(2), { + line: 1, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(3), { + line: 1, + column: 4, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(4), { + line: 2, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(5), { + line: 2, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(6), { + line: 2, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(7), { + line: 2, + column: 4, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(8), { + line: 2, + column: 5, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(9), { + line: 3, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(10), { + line: 3, + column: 2, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(11), { + line: 3, + column: 3, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(12), { + line: 3, + column: 4, + }); + }); + + it("should handle empty text", () => { + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 0, + }, + }, + }, + text: "", + }).getLocFromIndex(0), + { + line: 1, + column: 0, + }, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 0, + column: 1, + }, + }, + }, + text: "", + }).getLocFromIndex(0), + { + line: 0, + column: 1, + }, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 0, + column: 0, + }, + }, + }, + text: "", + }).getLocFromIndex(0), + { + line: 0, + column: 0, + }, + ); + assert.deepStrictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 1, + }, + }, + }, + text: "", + }).getLocFromIndex(0), + { + line: 1, + column: 1, + }, + ); + }); + + it("should handle text with only line breaks", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 3, + column: 0, + }, + }, + }; + const text = "\n\r\n"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual(sourceCode.getLocFromIndex(0), { + line: 1, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(1), { + line: 2, + column: 0, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(2), { + line: 2, + column: 1, + }); + assert.deepStrictEqual(sourceCode.getLocFromIndex(3), { + line: 3, + column: 0, + }); + }); + + it("should symmetric with getIndexFromLoc() when lineStart is 1 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 3, + column: 3, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + for (let index = 0; index <= text.length; index++) { + assert.strictEqual( + index, + sourceCode.getIndexFromLoc( + sourceCode.getLocFromIndex(index), + ), + ); + } + }); + + it("should symmetric with getIndexFromLoc() when lineStart is 0 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 2, + column: 4, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + for (let index = 0; index <= text.length; index++) { + assert.strictEqual( + index, + sourceCode.getIndexFromLoc( + sourceCode.getLocFromIndex(index), + ), + ); + } + }); + + it("should symmetric with getIndexFromLoc() when lineStart is 0 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 2, + column: 3, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + for (let index = 0; index <= text.length; index++) { + assert.strictEqual( + index, + sourceCode.getIndexFromLoc( + sourceCode.getLocFromIndex(index), + ), + ); + } + }); + + it("should symmetric with getIndexFromLoc() when lineStart is 1 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 3, + column: 4, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + for (let index = 0; index <= text.length; index++) { + assert.strictEqual( + index, + sourceCode.getIndexFromLoc( + sourceCode.getLocFromIndex(index), + ), + ); + } + }); + }); + + describe("getIndexFromLoc()", () => { + it("should throw an error for non-object loc", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc("invalid"); + }, + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc(null); + }, + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc(undefined); + }, + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, + ); + }); + + it("should throw an error for missing or non-numeric line/column properties", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({}); + }, + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: "1", column: 0 }); + }, + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: "0" }); + }, + { + name: "TypeError", + message: + "Expected `loc` to be an object with numeric `line` and `column` properties.", + }, + ); + }); + + it("should throw an error when `ast.loc` or `ast.position` is not defined", () => { + const ast = {}; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 0 }); + }, + { + name: "Error", + message: + "Custom getLoc() method must be implemented in the subclass.", + }, + ); + }); + + it("should throw an error for line number out of range when lineStart is 1 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 2, + column: 3, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 0 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line 0 requested). Valid range: 1-2", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 3, column: 0 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line 3 requested). Valid range: 1-2", + }, + ); + }); + + it("should throw an error for line number out of range when lineStart is 0 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 1, + column: 4, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: -1, column: 1 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line -1 requested). Valid range: 0-1", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 2, column: 1 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line 2 requested). Valid range: 0-1", + }, + ); + }); + + it("should throw an error for line number out of range when lineStart is 0 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 1, + column: 3, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: -1, column: 0 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line -1 requested). Valid range: 0-1", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 2, column: 0 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line 2 requested). Valid range: 0-1", + }, + ); + }); + + it("should throw an error for line number out of range when lineStart is 1 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 2, + column: 4, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 1 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line 0 requested). Valid range: 1-2", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 3, column: 1 }); + }, + { + name: "RangeError", + message: + "Line number out of range (line 3 requested). Valid range: 1-2", + }, + ); + }); + + it("should throw an error for column number out of range when lineStart is 1 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 2, + column: 3, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: -1 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column -1 requested). Valid range for line 1: 0-3", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 4 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 4 requested). Valid range for line 1: 0-3", + }, + ); + + assert.doesNotThrow(() => { + sourceCode.getIndexFromLoc({ line: 2, column: 3 }); + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 2, column: 4 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 4 requested). Valid range for line 2: 0-3", + }, + ); + }); + + it("should throw an error for column number out of range when lineStart is 0 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 1, + column: 4, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 0 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 0 requested). Valid range for line 0: 1-4", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 5 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 5 requested). Valid range for line 0: 1-4", + }, + ); + + assert.doesNotThrow(() => { + sourceCode.getIndexFromLoc({ line: 1, column: 4 }); + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 5 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 5 requested). Valid range for line 1: 1-4", + }, + ); + }); + + it("should throw an error for column number out of range when lineStart is 0 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 1, + column: 3, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: -1 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column -1 requested). Valid range for line 0: 0-3", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 0, column: 4 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 4 requested). Valid range for line 0: 0-3", + }, + ); + + assert.doesNotThrow(() => { + sourceCode.getIndexFromLoc({ line: 1, column: 3 }); + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 4 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 4 requested). Valid range for line 1: 0-3", + }, + ); + }); + + it("should throw an error for column number out of range when lineStart is 1 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 2, + column: 4, + }, + }, + }; + const text = "foo\nbar"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 0 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 0 requested). Valid range for line 1: 1-4", + }, + ); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 1, column: 5 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 5 requested). Valid range for line 1: 1-4", + }, + ); + + assert.doesNotThrow(() => { + sourceCode.getIndexFromLoc({ line: 2, column: 4 }); + }); + + assert.throws( + () => { + sourceCode.getIndexFromLoc({ line: 2, column: 5 }); + }, + { + name: "RangeError", + message: + "Column number out of range (column 5 requested). Valid range for line 2: 1-4", + }, + ); + }); + + it("should convert loc to index when random loc is given", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 3, + column: 3, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 3 }), + 3, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 0 }), + 9, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 1 }), + 1, + ); // Please do not change the order of these tests. It's for checking lazy calculation. + }); + + it("should convert loc to index when lineStart is 1 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 3, + column: 3, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 0 }), + 0, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 1 }), + 1, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 2 }), + 2, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 3 }), + 3, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 0 }), + 4, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 1 }), + 5, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 2 }), + 6, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 3 }), + 7, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 4 }), + 8, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 0 }), + 9, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 1 }), + 10, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 2 }), + 11, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 3 }), + 12, + ); + }); + + it("should convert loc to index when lineStart is 0 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 2, + column: 4, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 1 }), + 0, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 2 }), + 1, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 3 }), + 2, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 4 }), + 3, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 1 }), + 4, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 2 }), + 5, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 3 }), + 6, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 4 }), + 7, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 5 }), + 8, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 1 }), + 9, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 2 }), + 10, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 3 }), + 11, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 4 }), + 12, + ); + }); + + it("should convert loc to index when lineStart is 0 and columnStart is 0", () => { + const ast = { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 2, + column: 3, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 0 }), + 0, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 1 }), + 1, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 2 }), + 2, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 0, column: 3 }), + 3, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 0 }), + 4, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 1 }), + 5, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 2 }), + 6, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 3 }), + 7, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 4 }), + 8, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 0 }), + 9, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 1 }), + 10, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 2 }), + 11, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 3 }), + 12, + ); + }); + + it("should convert loc to index when lineStart is 1 and columnStart is 1", () => { + const ast = { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 3, + column: 4, + }, + }, + }; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + }); + + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 1 }), + 0, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 2 }), + 1, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 3 }), + 2, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 4 }), + 3, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 1 }), + 4, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 2 }), + 5, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 3 }), + 6, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 4 }), + 7, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 5 }), + 8, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 1 }), + 9, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 2 }), + 10, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 3 }), + 11, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 4 }), + 12, + ); + }); + + it("should handle empty text", () => { + assert.strictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 0, + }, + }, + }, + text: "", + }).getIndexFromLoc({ line: 1, column: 0 }), + 0, + ); + assert.strictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 0, + column: 1, + }, + end: { + line: 0, + column: 1, + }, + }, + }, + text: "", + }).getIndexFromLoc({ line: 0, column: 1 }), + 0, + ); + assert.strictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 0, + column: 0, + }, + end: { + line: 0, + column: 0, + }, + }, + }, + text: "", + }).getIndexFromLoc({ line: 0, column: 0 }), + 0, + ); + assert.strictEqual( + new TextSourceCodeBase({ + ast: { + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 1, + }, + }, + }, + text: "", + }).getIndexFromLoc({ line: 1, column: 1 }), + 0, + ); + }); + + it("should handle text with only line breaks", () => { + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 3, + column: 0, + }, + }, + }; + const text = "\n\r\n"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 1, column: 0 }), + 0, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 0 }), + 1, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 2, column: 1 }), + 2, + ); + assert.strictEqual( + sourceCode.getIndexFromLoc({ line: 3, column: 0 }), + 3, + ); + }); + }); + describe("getRange()", () => { it("should return a range object when a range property is present", () => { const ast = { @@ -249,5 +1955,161 @@ describe("source-code", () => { ]); }); }); + + describe("lines", () => { + it("should return an array of lines", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual(sourceCode.lines, ["foo", "bar", "baz"]); + }); + + it("should return an array of lines when line ending pattern is specified", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineEndingPattern: /\n/u, // Avoid using the `g` or `y` flag here, as this test is meant to run without it. + }); + + assert.deepStrictEqual(sourceCode.lines, [ + "foo", + "bar\r", + "baz", + ]); + }); + + it("should return an array of lines when line ending pattern uses `g` flag", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineEndingPattern: /\n/gu, + }); + + assert.deepStrictEqual(sourceCode.lines, [ + "foo", + "bar\r", + "baz", + ]); + }); + + it("should return an array of lines when line ending pattern uses `y` flag", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineEndingPattern: /\n/uy, + }); + + assert.deepStrictEqual(sourceCode.lines, [ + "foo", + "bar\r", + "baz", + ]); + }); + + it("should return an array of lines when line ending pattern uses `g` and `y` flag", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ + ast, + text, + lineEndingPattern: /\n/guy, + }); + + assert.deepStrictEqual(sourceCode.lines, [ + "foo", + "bar\r", + "baz", + ]); + }); + + it("should return an array of lines when no line endings are present", () => { + const ast = {}; + const text = "foo"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual(sourceCode.lines, ["foo"]); + }); + + it("should return an empty array when text is empty", () => { + const ast = {}; + const text = ""; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.deepStrictEqual(sourceCode.lines, [""]); + }); + + it("should split lines correctly when the first character of a multi-character linebreak sequence is a valid linebreak", () => { + // Please refer to https://github.com/eslint/rewrite/pull/212#discussion_r2242088769 for the motivation behind this test. + const ast = { + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 2, + column: 14, + }, + }, + }; + const text = ["// first line", "// second line"].join("\r\n"); + const lineEndingPattern = /\r\n|[\r\n]/u; // , or , or + + const sourceCode1 = new TextSourceCodeBase({ + ast, + text, + lineEndingPattern, + }); + + assert.deepStrictEqual(sourceCode1.lines, [ + "// first line", + "// second line", + ]); + + const sourceCode2 = new TextSourceCodeBase({ + ast, + text, + lineEndingPattern, + }); + + sourceCode2.getLocFromIndex(13); // linebreak sequence at the end of the first line + + assert.deepStrictEqual(sourceCode2.lines, [ + "// first line", + "// second line", + ]); + }); + + it("should throw an error when writing to lines", () => { + const ast = {}; + const text = "foo\nbar\r\nbaz"; + const sourceCode = new TextSourceCodeBase({ ast, text }); + + assert.throws( + () => { + sourceCode.lines = ["bar"]; + }, + { + name: "TypeError", // Cannot use `message` here because it behaves differently in other runtimes, such as Bun. + }, + ); + + assert.throws( + () => { + sourceCode.lines.push("qux"); + }, + { + name: "TypeError", // Cannot use `message` here because it behaves differently in other runtimes, such as Bun. + }, + ); + }); + }); }); }); diff --git a/packages/plugin-kit/tests/types/tsconfig.json b/packages/plugin-kit/tests/types/tsconfig.json index b3220a777..852b0e519 100644 --- a/packages/plugin-kit/tests/types/tsconfig.json +++ b/packages/plugin-kit/tests/types/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "noEmit": true, "rootDir": "../..", - "strict": true + "strict": true, + "exactOptionalPropertyTypes": true }, "include": [".", "../../dist"] } diff --git a/packages/plugin-kit/tests/types/types.test.ts b/packages/plugin-kit/tests/types/types.test.ts index 4ca94fd29..0f2eaa0de 100644 --- a/packages/plugin-kit/tests/types/types.test.ts +++ b/packages/plugin-kit/tests/types/types.test.ts @@ -87,6 +87,8 @@ sourceCode.text satisfies string; sourceCode.lines satisfies string[]; sourceCode.getAncestors({}) satisfies object[]; sourceCode.getLoc({}) satisfies SourceLocation; +sourceCode.getLocFromIndex(0) satisfies { line: number; column: number }; +sourceCode.getIndexFromLoc({ line: 1, column: 0 }) satisfies number; sourceCode.getParent({}) satisfies object | undefined; sourceCode.getRange({}) satisfies SourceRange; sourceCode.getText() satisfies string; @@ -141,6 +143,11 @@ sourceCodeWithOptions.getAncestors({ value: "" }) satisfies { value: string; }[] satisfies CustomOptions["SyntaxElementWithLoc"][]; sourceCodeWithOptions.getLoc({ value: "" }) satisfies SourceLocation; +sourceCodeWithOptions.getLocFromIndex(0) satisfies { + line: number; + column: number; +}; +sourceCodeWithOptions.getIndexFromLoc({ line: 1, column: 0 }) satisfies number; sourceCodeWithOptions.getParent({ value: "" }) satisfies | { value: string } | undefined satisfies CustomOptions["SyntaxElementWithLoc"] | undefined; @@ -153,6 +160,10 @@ sourceCodeWithOptions.getAncestors({}); // @ts-expect-error Wrong type should be caught sourceCodeWithOptions.getLoc({}); // @ts-expect-error Wrong type should be caught +sourceCodeWithOptions.getLocFromIndex("foo"); +// @ts-expect-error Wrong type should be caught +sourceCodeWithOptions.getIndexFromLoc({ line: "1", column: 0 }); +// @ts-expect-error Wrong type should be caught sourceCodeWithOptions.getParent({}); // @ts-expect-error Wrong type should be caught sourceCodeWithOptions.getRange({}); diff --git a/release-please-config.json b/release-please-config.json index 51eb9b1b3..a3f48e976 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -37,6 +37,7 @@ }, "packages/core": { "release-type": "node", + "release-as": "1.0.0", "extra-files": [ { "type": "json", diff --git a/templates/package/README.md b/templates/package/README.md index b625319ea..a3ee655a9 100644 --- a/templates/package/README.md +++ b/templates/package/README.md @@ -15,7 +15,7 @@ yarn add @eslint/<%= name %> # or pnpm install @eslint/<%= name %> # or -bun install @eslint/<%= name %> +bun add @eslint/<%= name %> ``` For Deno: diff --git a/templates/package/package.json b/templates/package/package.json index 77d48c0e5..0fc4d9760 100644 --- a/templates/package/package.json +++ b/templates/package/package.json @@ -28,9 +28,10 @@ "build:dedupe-types": "node ../../tools/dedupe-types.js dist/cjs/index.cjs dist/esm/index.js", "build:cts": "node ../../tools/build-cts.js dist/esm/index.d.ts dist/cjs/index.d.cts", "build": "rollup -c && npm run build:dedupe-types && tsc -p tsconfig.esm.json && npm run build:cts", + "test": "mocha \"tests/**/*.test.js\"", + "test:coverage": "c8 npm test", "test:jsr": "npx jsr@latest publish --dry-run", - "test": "mocha tests/*.js", - "test:coverage": "c8 npm test" + "test:types": "tsc -p tests/types/tsconfig.json" }, "repository": { "type": "git", @@ -46,11 +47,11 @@ }, "homepage": "https://github.com/eslint/rewrite/tree/main/packages/<%= name %>#readme", "devDependencies": { - "@eslint/core": "^0.15.0", + "@eslint/core": "^0.17.0", "eslint": "^9.27.0", "rollup-plugin-copy": "^3.5.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } } diff --git a/templates/package/tests/types/tsconfig.json b/templates/package/tests/types/tsconfig.json new file mode 100644 index 000000000..638c0f3a5 --- /dev/null +++ b/templates/package/tests/types/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "../.." + }, + "include": [".", "../../dist"] +} diff --git a/tools/build-cts.js b/tools/build-cts.js index ee6350949..35a976bb8 100644 --- a/tools/build-cts.js +++ b/tools/build-cts.js @@ -25,8 +25,8 @@ if (!newFilename) { const oldSourceText = await readFile(filename, "utf-8"); const newSourceText = oldSourceText.replaceAll( - 'import("./types.ts")', - 'import("./types.cts")', + ' from "./types.ts";\n', + ' from "./types.cts";\n', ); await writeFile(newFilename, newSourceText); diff --git a/tools/commit-readme.sh b/tools/commit-readme.sh deleted file mode 100644 index baa69a50d..000000000 --- a/tools/commit-readme.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -#------------------------------------------------------------------------------ -# Commits the data files if any have changed -#------------------------------------------------------------------------------ - -if [ -z "$(git status --porcelain)" ]; then - echo "Data did not change." -else - echo "Data changed!" - - # commit the result - git add README.md packages/**/README.md - git commit -m "docs: Update README sponsors" - - # push back to source control - git push origin HEAD -fi diff --git a/tools/dedupe-types.js b/tools/dedupe-types.js index ce8b16d1f..ca7086bff 100644 --- a/tools/dedupe-types.js +++ b/tools/dedupe-types.js @@ -25,19 +25,31 @@ const files = process.argv.slice(2); files.forEach(filePath => { const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/gu); const typedefs = new Set(); - - const remainingLines = lines.filter(line => { - if (!line.startsWith("/** @typedef {import")) { - return true; - } - - if (typedefs.has(line)) { - return false; + const importSources = new Set(); + const outputLines = []; + + for (const line of lines) { + const match = line.match( + /^(?\/\*\*\s*@typedef\s+\{)import\("(?.+?)"\)(?.*)/u, + ); + if (!match) { + // not a typedef, so just copy the line + outputLines.push(line); + } else if (!typedefs.has(line)) { + // we haven't seen this typedef before, so process it + typedefs.add(line); + const { start, importSource, end } = match.groups; + const importName = `$${importSource.replace(/\W/gu, "")}`; + if (!importSources.has(importSource)) { + // we haven't seen this import before, so add an @import comment + importSources.add(importSource); + outputLines.push( + `/** @import * as ${importName} from "${importSource}"; */`, + ); + } + outputLines.push(`${start}${importName}${end}`); } + } - typedefs.add(line); - return true; - }); - - fs.writeFileSync(filePath, remainingLines.join("\n"), "utf8"); + fs.writeFileSync(filePath, outputLines.join("\n"), "utf8"); }); diff --git a/tools/update-readme.js b/tools/update-readme.js deleted file mode 100644 index 34051e151..000000000 --- a/tools/update-readme.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @fileoverview Script to update the README with sponsors details in all packages. - * - * node tools/update-readme.js - * - * @author Harish Kumar S S - */ - -//----------------------------------------------------------------------------- -// Requirements -//----------------------------------------------------------------------------- - -import { readFileSync, readdirSync, writeFileSync } from "node:fs"; -import got from "got"; - -//----------------------------------------------------------------------------- -// Data -//----------------------------------------------------------------------------- - -const SPONSORS_URL = - "https://raw.githubusercontent.com/eslint/eslint.org/main/includes/sponsors.md"; - -const README_FILE_PATHS = [ - "./README.md", - ...readdirSync("./packages").map(dir => `./packages/${dir}/README.md`), -]; - -//----------------------------------------------------------------------------- -// Helpers -//----------------------------------------------------------------------------- - -/** - * Fetches the latest sponsors from the website. - * @returns {Promise} Prerendered sponsors markdown. - */ -async function fetchSponsorsMarkdown() { - return got(SPONSORS_URL).text(); -} - -//----------------------------------------------------------------------------- -// Main -//----------------------------------------------------------------------------- - -const allSponsors = await fetchSponsorsMarkdown(); - -README_FILE_PATHS.forEach(filePath => { - // read readme file - const readme = readFileSync(filePath, "utf8"); - - let newReadme = readme.replace( - /[\w\W]*?/u, - `\n\n${allSponsors}\n`, - ); - - // replace multiple consecutive blank lines with just one blank line - newReadme = newReadme.replace(/(?<=^|\n)\n{2,}/gu, "\n"); - - // output to the files - writeFileSync(filePath, newReadme, "utf8"); -}); diff --git a/tsconfig.base.json b/tsconfig.base.json index c5740a825..d7fac4dbe 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "allowImportingTsExtensions": true, "allowJs": true, "checkJs": true, "declaration": true,