diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index de2207d9..00000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,42 +0,0 @@ -"use strict"; - -module.exports = { - root: true, - extends: [ - "eslint" - ], - parserOptions: { - ecmaVersion: 2020 - }, - - /* - * it fixes eslint-plugin-jsdoc's reports: "Invalid JSDoc tag name "template" jsdoc/check-tag-names" - * refs: https://github.com/gajus/eslint-plugin-jsdoc#check-tag-names - */ - settings: { - jsdoc: { - mode: "typescript" - } - }, - - overrides: [ - { - files: ["tests/**/*"], - env: { mocha: true }, - rules: { - "no-restricted-syntax": ["error", { - selector: "CallExpression[callee.object.name='assert'][callee.property.name='doesNotThrow']", - message: "`assert.doesNotThrow()` should be replaced with a comment next to the code." - }], - - // Overcome https://github.com/mysticatea/eslint-plugin-node/issues/250 - "node/no-unsupported-features/es-syntax": ["error", { - ignores: [ - "modules", - "dynamicImport" - ] - }] - } - } - ] -}; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..1a950cb4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +.release-please-manifest.json @eslint/eslint-tsc +.github/workflows/release-please.yml @eslint/eslint-tsc \ No newline at end of file diff --git a/.github/workflows/add-to-triage.yml b/.github/workflows/add-to-triage.yml new file mode 100644 index 00000000..06dd5b63 --- /dev/null +++ b/.github/workflows/add-to-triage.yml @@ -0,0 +1,19 @@ +name: add-to-triage + +on: + issues: + types: + - opened + - reopened + - transferred + + pull_request_target: + types: + - opened + - reopened + +jobs: + add-to-triage: + uses: eslint/workflows/.github/workflows/add-to-triage.yml@main + secrets: + project_bot_token: ${{ secrets.PROJECT_BOT_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4909f16e..06f78358 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,10 @@ jobs: name: Verify Files runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: '16.x' + node-version: "lts/*" - name: Install Packages run: npm install - name: Lint Files @@ -24,19 +24,32 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node: [21.x, 20.x, 19.x, 18.x, 17.x, 16.x, 14.x, 12.x, "12.22.0"] + node: [25.x, 24.x, 23.x, 22.x, 21.x, 20.x, 18.x, "18.18.0"] include: - os: windows-latest - node: "12.x" + node: "lts/*" - os: macOS-latest - node: "12.x" + node: "lts/*" runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - name: Install Packages run: npm install - name: Test run: npm test + + test_types: + name: Test Types + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install Packages + run: npm install + - name: Test + run: npm run test:types diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index c60c453b..cef408df 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -2,6 +2,7 @@ on: push: branches: - main + name: release-please jobs: release-please: @@ -11,37 +12,27 @@ jobs: pull-requests: write id-token: write steps: - - uses: google-github-actions/release-please-action@v3 + - uses: googleapis/release-please-action@v4 id: release - with: - release-type: node - package-name: '@eslint/eslintrc' - pull-request-title-pattern: 'chore: release ${version}' - changelog-types: > - [ - { "type": "feat", "section": "Features", "hidden": false }, - { "type": "fix", "section": "Bug Fixes", "hidden": false }, - { "type": "docs", "section": "Documentation", "hidden": false }, - { "type": "build", "section": "Build Related", "hidden": false }, - { "type": "chore", "section": "Chores", "hidden": false }, - { "type": "perf", "section": "Chores", "hidden": false }, - { "type": "ci", "section": "Chores", "hidden": false }, - { "type": "refactor", "section": "Chores", "hidden": false }, - { "type": "test", "section": "Chores", "hidden": false } - ] - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 if: ${{ steps.release.outputs.release_created }} - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: lts/* registry-url: https://registry.npmjs.org if: ${{ steps.release.outputs.release_created }} - - run: npm install + + # 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.release_created }} - - run: npm publish --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish to npm + run: | + npm install + npm publish --provenance if: ${{ steps.release.outputs.release_created }} + - run: 'npx @humanwhocodes/tweet "eslint/eslintrc ${{ steps.release.outputs.tag_name }} has been released: ${{ steps.release.outputs.html_url }}"' if: ${{ steps.release.outputs.release_created }} env: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..fe232e62 --- /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 new file mode 100644 index 00000000..96d09866 --- /dev/null +++ b/.github/workflows/update-readme.yml @@ -0,0 +1,13 @@ +name: update-readme + +on: + schedule: + - cron: "0 8 * * *" # Runs every day at 08:00 AM UTC + + workflow_dispatch: + +jobs: + 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 5423bd14..38bb2a34 100644 --- a/.gitignore +++ b/.gitignore @@ -110,3 +110,10 @@ pnpm-lock.yaml # used in tests /tmp/ + +# IDEs and editors +/.vscode +*.code-workspace + +# Automatically generated files by GitHub Actions workflow +/.shared-workflows diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..f5258250 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "3.3.3" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f1f1b32..b6593e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,98 @@ # Changelog +## [3.3.3](https://github.com/eslint/eslintrc/compare/eslintrc-v3.3.2...eslintrc-v3.3.3) (2025-11-28) + + +### Bug Fixes + +* release v3.3.3 because publishing v3.3.2 failed ([#211](https://github.com/eslint/eslintrc/issues/211)) ([8aa555a](https://github.com/eslint/eslintrc/commit/8aa555a3f5fcfb7d99249fb57e819a7b6f635496)) + +## [3.3.2](https://github.com/eslint/eslintrc/compare/eslintrc-v3.3.1...eslintrc-v3.3.2) (2025-11-25) + + +### Bug Fixes + +* Remove name property from all and recommended configs ([#200](https://github.com/eslint/eslintrc/issues/200)) ([344da49](https://github.com/eslint/eslintrc/commit/344da491898a2a3595943d4528ba78fe2f238217)) + +## [3.3.1](https://github.com/eslint/eslintrc/compare/v3.3.0...v3.3.1) (2025-03-11) + + +### Bug Fixes + +* correct `types` field in package.json ([#184](https://github.com/eslint/eslintrc/issues/184)) ([2f4cf3f](https://github.com/eslint/eslintrc/commit/2f4cf3fe36ee0df93c1c53f32c030c58db1816a2)) + +## [3.3.0](https://github.com/eslint/eslintrc/compare/v3.2.0...v3.3.0) (2025-02-21) + + +### Features + +* Add types to package ([#179](https://github.com/eslint/eslintrc/issues/179)) ([cb546be](https://github.com/eslint/eslintrc/commit/cb546be8ba53abcb4c64ed2fdd3a729dd1337f61)) + +## [3.2.0](https://github.com/eslint/eslintrc/compare/v3.1.0...v3.2.0) (2024-11-14) + + +### Features + +* merge rule.meta.defaultOptions before validation ([#166](https://github.com/eslint/eslintrc/issues/166)) ([d02f914](https://github.com/eslint/eslintrc/commit/d02f91452b81caff971f7895237cc4fb002e31da)) + +## [3.1.0](https://github.com/eslint/eslintrc/compare/v3.0.2...v3.1.0) (2024-05-17) + + +### Features + +* Expose loadConfigFile() function ([#160](https://github.com/eslint/eslintrc/issues/160)) ([59e890f](https://github.com/eslint/eslintrc/commit/59e890fcd9e03663ac185640d5bed5f1a85bd39b)) + + +### Chores + +* run tests in Node.js 22 ([#154](https://github.com/eslint/eslintrc/issues/154)) ([5e526f2](https://github.com/eslint/eslintrc/commit/5e526f2e2897b87d7a704391cec74702d4bed38c)) +* update dependency shelljs to ^0.8.5 ([#156](https://github.com/eslint/eslintrc/issues/156)) ([903b887](https://github.com/eslint/eslintrc/commit/903b8875581ee731fd1a9424f83f785359cfb22e)) + +## [3.0.2](https://github.com/eslint/eslintrc/compare/v3.0.1...v3.0.2) (2024-02-12) + + +### Chores + +* maintenance update of `globals` to `v14` ([#152](https://github.com/eslint/eslintrc/issues/152)) ([4151865](https://github.com/eslint/eslintrc/commit/4151865b09084369e89d591eb2e01b9617287982)) + +## [3.0.1](https://github.com/eslint/eslintrc/compare/v3.0.0...v3.0.1) (2024-02-09) + + +### Documentation + +* fix changelog for v3.0.0 ([#144](https://github.com/eslint/eslintrc/issues/144)) ([a613847](https://github.com/eslint/eslintrc/commit/a61384731aff386a8260a80d9710c912e4f62aaa)) +* More explicit about all and recommended configs ([#150](https://github.com/eslint/eslintrc/issues/150)) ([0fabc74](https://github.com/eslint/eslintrc/commit/0fabc7406e5a281a4e72be33de6e3bf8642aa746)) + + +### Chores + +* upgrade espree@10.0.1 ([#151](https://github.com/eslint/eslintrc/issues/151)) ([8c39944](https://github.com/eslint/eslintrc/commit/8c399441f47009344888e181c6aa2ecdc74ce8ea)) + +## [3.0.0](https://github.com/eslint/eslintrc/compare/v2.1.4...v3.0.0) (2023-12-27) + + +### ⚠ BREAKING CHANGES + +* Require Node.js `^18.18.0 || ^20.9.0 || >=21.1.0` ([#142](https://github.com/eslint/eslintrc/issues/142)) +* Set default `schema: []`, drop support for function-style rules ([#139](https://github.com/eslint/eslintrc/issues/139)) + +### Features + +* Require Node.js `^18.18.0 || ^20.9.0 || >=21.1.0` ([#142](https://github.com/eslint/eslintrc/issues/142)) ([737eb25](https://github.com/eslint/eslintrc/commit/737eb25ac686550020b838ccf6efd5cd2aaa449e)) +* Set default `schema: []`, drop support for function-style rules ([#139](https://github.com/eslint/eslintrc/issues/139)) ([a6c240d](https://github.com/eslint/eslintrc/commit/a6c240de244b0e94ace4a518f2c67876a91f5882)) + + +### Chores + +* upgrade github actions ([#143](https://github.com/eslint/eslintrc/issues/143)) ([de34faf](https://github.com/eslint/eslintrc/commit/de34fafed28aaf1936845d51309f28484ed29e2f)) + +## [2.1.4](https://github.com/eslint/eslintrc/compare/v2.1.3...v2.1.4) (2023-11-27) + + +### Bug Fixes + +* Use original plugin from disk in FlatCompat ([#137](https://github.com/eslint/eslintrc/issues/137)) ([1c4cf6a](https://github.com/eslint/eslintrc/commit/1c4cf6a71378d480323bfdd404c9585bd0d21d65)) + ## [2.1.3](https://github.com/eslint/eslintrc/compare/v2.1.2...v2.1.3) (2023-11-01) diff --git a/README.md b/README.md index 7641c741..846cd120 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,14 @@ This repository contains the legacy ESLintRC configuration file format for ESLin You can install the package as follows: -``` -npm install @eslint/eslintrc --save-dev - +```shell +npm install @eslint/eslintrc -D # or - yarn add @eslint/eslintrc -D +# or +pnpm install @eslint/eslintrc -D +# or +bun install @eslint/eslintrc -D ``` ## Usage (ESM) @@ -33,14 +35,14 @@ const __dirname = path.dirname(__filename); const compat = new FlatCompat({ baseDirectory: __dirname, // optional; default: process.cwd() resolvePluginsRelativeTo: __dirname, // optional - recommendedConfig: js.configs.recommended, // optional - allConfig: js.configs.all, // optional + recommendedConfig: js.configs.recommended, // optional unless you're using "eslint:recommended" + allConfig: js.configs.all, // optional unless you're using "eslint:all" }); export default [ // mimic ESLintRC-style extends - ...compat.extends("standard", "example"), + ...compat.extends("standard", "example", "plugin:react/recommended"), // mimic environments ...compat.env({ @@ -49,11 +51,11 @@ export default [ }), // mimic plugins - ...compat.plugins("airbnb", "react"), + ...compat.plugins("jsx-a11y", "react"), // translate an entire config ...compat.config({ - plugins: ["airbnb", "react"], + plugins: ["jsx-a11y", "react"], extends: "standard", env: { es2020: true, @@ -77,14 +79,14 @@ const js = require("@eslint/js"); const compat = new FlatCompat({ baseDirectory: __dirname, // optional; default: process.cwd() resolvePluginsRelativeTo: __dirname, // optional - recommendedConfig: js.configs.recommended, // optional - allConfig: js.configs.all, // optional + recommendedConfig: js.configs.recommended, // optional unless using "eslint:recommended" + allConfig: js.configs.all, // optional unless using "eslint:all" }); module.exports = [ // mimic ESLintRC-style extends - ...compat.extends("standard", "example"), + ...compat.extends("standard", "example", "plugin:react/recommended"), // mimic environments ...compat.env({ @@ -93,11 +95,11 @@ module.exports = [ }), // mimic plugins - ...compat.plugins("airbnb", "react"), + ...compat.plugins("jsx-a11y", "react"), // translate an entire config ...compat.config({ - plugins: ["airbnb", "react"], + plugins: ["jsx-a11y", "react"], extends: "standard", env: { es2020: true, @@ -110,6 +112,34 @@ module.exports = [ ]; ``` +## Troubleshooting + +**TypeError: Missing parameter 'recommendedConfig' in FlatCompat constructor** + +The `recommendedConfig` option is required when any config uses `eslint:recommended`, including any config in an `extends` clause. To fix this, follow the example above using `@eslint/js` to provide the `eslint:recommended` config. + +**TypeError: Missing parameter 'allConfig' in FlatCompat constructor** + +The `allConfig` option is required when any config uses `eslint:all`, including any config in an `extends` clause. To fix this, follow the example above using `@eslint/js` to provide the `eslint:all` config. + ## License MIT License + + + + +## Sponsors + +The following companies, organizations, and individuals support ESLint's ongoing maintenance and development. [Become a Sponsor](https://eslint.org/donate) +to get your logo on our READMEs and [website](https://eslint.org/sponsors). + +

Platinum Sponsors

+

Automattic Airbnb

Gold Sponsors

+

Qlty Software Shopify

Silver Sponsors

+

Vite Liftoff American Express StackBlitz

Bronze Sponsors

+

Cybozu Syntax N-iX Ltd 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/conf/environments.js b/conf/environments.js index 50d1b1d1..e296fae7 100644 --- a/conf/environments.js +++ b/conf/environments.js @@ -23,7 +23,7 @@ function getDiff(current, prev) { const retv = {}; for (const [key, value] of Object.entries(current)) { - if (!Object.hasOwnProperty.call(prev, key)) { + if (!Object.hasOwn(prev, key)) { retv[key] = value; } } diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..0981a822 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,52 @@ +/** + * @fileoverview ESLint configuration file + * @author Nicholas C. Zakas + */ + +//----------------------------------------------------------------------------- +// Imports +//----------------------------------------------------------------------------- + +import eslintConfigESLint from "eslint-config-eslint"; +import eslintConfigESLintFormatting from "eslint-config-eslint/formatting"; + +//----------------------------------------------------------------------------- +// Config +//----------------------------------------------------------------------------- + +export default [ + + { + ignores: [ + "tests/fixtures", + "coverage", + "docs", + "jsdoc", + "dist" + ] + }, + + ...eslintConfigESLint, + eslintConfigESLintFormatting, + { + files: ["tests/**/*"], + languageOptions: { + globals: { + describe: "readonly", + xdescribe: "readonly", + it: "readonly", + xit: "readonly", + beforeEach: "readonly", + afterEach: "readonly", + before: "readonly", + after: "readonly" + } + }, + rules: { + "no-restricted-syntax": ["error", { + selector: "CallExpression[callee.object.name='assert'][callee.property.name='doesNotThrow']", + message: "`assert.doesNotThrow()` should be replaced with a comment next to the code." + }] + } + } +]; diff --git a/lib/cascading-config-array-factory.js b/lib/cascading-config-array-factory.js index 597352e4..71549107 100644 --- a/lib/cascading-config-array-factory.js +++ b/lib/cascading-config-array-factory.js @@ -23,8 +23,8 @@ //------------------------------------------------------------------------------ import debugOrig from "debug"; -import os from "os"; -import path from "path"; +import os from "node:os"; +import path from "node:path"; import { ConfigArrayFactory } from "./config-array-factory.js"; import { @@ -193,7 +193,7 @@ function createCLIConfigArray({ */ class ConfigurationNotFoundError extends Error { - // eslint-disable-next-line jsdoc/require-description + /** * @param {string} directoryPath The directory path. */ @@ -345,6 +345,7 @@ class CascadingConfigArrayFactory { * @param {string} directoryPath The path to a leaf directory. * @param {boolean} configsExistInSubdirs `true` if configurations exist in subdirectories. * @returns {ConfigArray} The loaded config. + * @throws {Error} If a config file is invalid. * @private */ _loadConfigInAncestors(directoryPath, configsExistInSubdirs = false) { @@ -446,6 +447,7 @@ class CascadingConfigArrayFactory { * @param {string} directoryPath The path to the leaf directory to find config files. * @param {boolean} ignoreNotFoundError If `true` then it doesn't throw `ConfigurationNotFoundError`. * @returns {ConfigArray} The loaded config. + * @throws {Error} If a config file is invalid. * @private */ _finalizeConfigArray(configArray, directoryPath, ignoreNotFoundError) { @@ -482,7 +484,7 @@ class CascadingConfigArrayFactory { !directoryPath.startsWith(homePath) ) { const lastElement = - personalConfigArray[personalConfigArray.length - 1]; + personalConfigArray.at(-1); emitDeprecationWarning( lastElement.filePath, diff --git a/lib/config-array-factory.js b/lib/config-array-factory.js index 23cb3529..4f55ae2e 100644 --- a/lib/config-array-factory.js +++ b/lib/config-array-factory.js @@ -39,10 +39,10 @@ //------------------------------------------------------------------------------ import debugOrig from "debug"; -import fs from "fs"; +import fs from "node:fs"; import importFresh from "import-fresh"; -import { createRequire } from "module"; -import path from "path"; +import { createRequire } from "node:module"; +import path from "node:path"; import stripComments from "strip-json-comments"; import { @@ -254,7 +254,7 @@ function loadPackageJSONConfigFile(filePath) { try { const packageData = loadJSONConfigFile(filePath); - if (!Object.hasOwnProperty.call(packageData, "eslintConfig")) { + if (!Object.hasOwn(packageData, "eslintConfig")) { throw Object.assign( new Error("package.json file doesn't have 'eslintConfig' field."), { code: "ESLINT_CONFIG_FIELD_NOT_FOUND" } @@ -273,6 +273,7 @@ function loadPackageJSONConfigFile(filePath) { * Loads a `.eslintignore` from a file. * @param {string} filePath The filename to load. * @returns {string[]} The ignore patterns from the file. + * @throws {Error} If the file cannot be read. * @private */ function loadESLintIgnoreFile(filePath) { @@ -345,7 +346,7 @@ function loadConfigFile(filePath) { function writeDebugLogForLoading(request, relativeTo, filePath) { /* istanbul ignore next */ if (debug.enabled) { - let nameAndVersion = null; + let nameAndVersion = null; // eslint-disable-line no-useless-assignment -- known bug in the rule try { const packageJsonPath = ModuleResolver.resolve( @@ -510,6 +511,7 @@ class ConfigArrayFactory { * @param {Object} [options] The options. * @param {string} [options.basePath] The base path to resolve relative paths in `overrides[].files`, `overrides[].excludedFiles`, and `ignorePatterns`. * @param {string} [options.name] The config name. + * @throws {Error} If the config file is invalid. * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist. */ loadInDirectory(directoryPath, { basePath, name } = {}) { @@ -595,6 +597,7 @@ class ConfigArrayFactory { /** * Load `.eslintignore` file in the current working directory. * @returns {ConfigArray} Loaded config. An empty `ConfigArray` if any config doesn't exist. + * @throws {Error} If the ignore file is invalid. */ loadDefaultESLintIgnore() { const slots = internalSlotsMap.get(this); @@ -607,7 +610,7 @@ class ConfigArrayFactory { if (fs.existsSync(packageJsonPath)) { const data = loadJSONConfigFile(packageJsonPath); - if (Object.hasOwnProperty.call(data, "eslintIgnore")) { + if (Object.hasOwn(data, "eslintIgnore")) { if (!Array.isArray(data.eslintIgnore)) { throw new Error("Package.json eslintIgnore property requires an array of paths"); } @@ -796,6 +799,7 @@ class ConfigArrayFactory { * @param {string} extendName The name of a base config. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context. * @returns {IterableIterator} The normalized config. + * @throws {Error} If the extended config file can't be loaded. * @private */ _loadExtends(extendName, ctx) { @@ -819,6 +823,7 @@ class ConfigArrayFactory { * @param {string} extendName The name of a base config. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context. * @returns {IterableIterator} The normalized config. + * @throws {Error} If the extended config file can't be loaded. * @private */ _loadExtendedBuiltInConfig(extendName, ctx) { @@ -868,6 +873,7 @@ class ConfigArrayFactory { * @param {string} extendName The name of a base config. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context. * @returns {IterableIterator} The normalized config. + * @throws {Error} If the extended config file can't be loaded. * @private */ _loadExtendedPluginConfig(extendName, ctx) { @@ -905,6 +911,7 @@ class ConfigArrayFactory { * @param {string} extendName The name of a base config. * @param {ConfigArrayFactoryLoadingContext} ctx The loading context. * @returns {IterableIterator} The normalized config. + * @throws {Error} If the extended config file can't be loaded. * @private */ _loadExtendedShareableConfig(extendName, ctx) { @@ -1106,6 +1113,7 @@ class ConfigArrayFactory { if (plugin) { return new ConfigDependency({ definition: normalizePlugin(plugin), + original: plugin, filePath: "", // It's unknown where the plugin came from. id, importerName: ctx.name, @@ -1142,6 +1150,7 @@ class ConfigArrayFactory { return new ConfigDependency({ definition: normalizePlugin(pluginDefinition), + original: pluginDefinition, filePath, id, importerName: ctx.name, @@ -1219,4 +1228,8 @@ class ConfigArrayFactory { } } -export { ConfigArrayFactory, createContext }; +export { + ConfigArrayFactory, + createContext, + loadConfigFile +}; diff --git a/lib/config-array/config-array.js b/lib/config-array/config-array.js index 133f5a24..8b3ec28c 100644 --- a/lib/config-array/config-array.js +++ b/lib/config-array/config-array.js @@ -178,6 +178,7 @@ class PluginConflictError extends Error { * @param {Record} target The destination to merge * @param {Record|undefined} source The source to merge. * @returns {void} + * @throws {PluginConflictError} When a plugin was conflicted. */ function mergePlugins(target, source) { if (!isNonNullObject(source)) { @@ -258,6 +259,7 @@ function mergeRuleConfigs(target, source) { * @param {ConfigArray} instance The config elements. * @param {number[]} indices The indices to use. * @returns {ExtractedConfig} The extracted config. + * @throws {Error} When a plugin is conflicted. */ function createConfig(instance, indices) { const config = new ExtractedConfig(); @@ -319,31 +321,18 @@ function createConfig(instance, indices) { * @param {string} pluginId The plugin ID for prefix. * @param {Record} defs The definitions to collect. * @param {Map} map The map to output. - * @param {function(T): U} [normalize] The normalize function for each value. * @returns {void} */ -function collect(pluginId, defs, map, normalize) { +function collect(pluginId, defs, map) { if (defs) { const prefix = pluginId && `${pluginId}/`; for (const [key, value] of Object.entries(defs)) { - map.set( - `${prefix}${key}`, - normalize ? normalize(value) : value - ); + map.set(`${prefix}${key}`, value); } } } -/** - * Normalize a rule definition. - * @param {Function|Rule} rule The rule definition to normalize. - * @returns {Rule} The normalized rule definition. - */ -function normalizePluginRule(rule) { - return typeof rule === "function" ? { create: rule } : rule; -} - /** * Delete the mutation methods from a given map. * @param {Map} map The map object to delete. @@ -385,7 +374,7 @@ function initPluginMemberMaps(elements, slots) { collect(pluginId, plugin.environments, slots.envMap); collect(pluginId, plugin.processors, slots.processorMap); - collect(pluginId, plugin.rules, slots.ruleMap, normalizePluginRule); + collect(pluginId, plugin.rules, slots.ruleMap); } } diff --git a/lib/config-array/config-dependency.js b/lib/config-array/config-dependency.js index 2883c3a2..80968950 100644 --- a/lib/config-array/config-dependency.js +++ b/lib/config-array/config-dependency.js @@ -15,7 +15,7 @@ * @author Toru Nagashima */ -import util from "util"; +import util from "node:util"; /** * The class is to store parsers or plugins. @@ -28,6 +28,7 @@ class ConfigDependency { * Initialize this instance. * @param {Object} data The dependency data. * @param {T} [data.definition] The dependency if the loading succeeded. + * @param {T} [data.original] The original, non-normalized dependency if the loading succeeded. * @param {Error} [data.error] The error object if the loading failed. * @param {string} [data.filePath] The actual path to the dependency if the loading succeeded. * @param {string} data.id The ID of this dependency. @@ -36,6 +37,7 @@ class ConfigDependency { */ constructor({ definition = null, + original = null, error = null, filePath = null, id, @@ -49,6 +51,12 @@ class ConfigDependency { */ this.definition = definition; + /** + * The original dependency as loaded directly from disk if the loading succeeded. + * @type {T|null} + */ + this.original = original; + /** * The error object if the loading failed. * @type {Error|null} @@ -80,8 +88,8 @@ class ConfigDependency { this.importerPath = importerPath; } - // eslint-disable-next-line jsdoc/require-description /** + * Converts this instance to a JSON compatible object. * @returns {Object} a JSON compatible object. */ toJSON() { @@ -95,13 +103,14 @@ class ConfigDependency { return obj; } - // eslint-disable-next-line jsdoc/require-description /** + * Custom inspect method for Node.js `console.log()`. * @returns {Object} an object to display by `console.log()`. */ [util.inspect.custom]() { const { - definition: _ignore, // eslint-disable-line no-unused-vars + definition: _ignore1, // eslint-disable-line no-unused-vars -- needed to make `obj` correct + original: _ignore2, // eslint-disable-line no-unused-vars -- needed to make `obj` correct ...obj } = this; diff --git a/lib/config-array/extracted-config.js b/lib/config-array/extracted-config.js index e93b0b67..65206f27 100644 --- a/lib/config-array/extracted-config.js +++ b/lib/config-array/extracted-config.js @@ -120,10 +120,10 @@ class ExtractedConfig { */ toCompatibleObjectAsConfigFileContent() { const { - /* eslint-disable no-unused-vars */ + /* eslint-disable no-unused-vars -- needed to make `config` correct */ configNameOfNoInlineConfig: _ignore1, processor: _ignore2, - /* eslint-enable no-unused-vars */ + /* eslint-enable no-unused-vars -- needed to make `config` correct */ ignores, ...config } = this; diff --git a/lib/config-array/ignore-pattern.js b/lib/config-array/ignore-pattern.js index 3022ba9f..edb52872 100644 --- a/lib/config-array/ignore-pattern.js +++ b/lib/config-array/ignore-pattern.js @@ -32,8 +32,8 @@ // Requirements //------------------------------------------------------------------------------ -import assert from "assert"; -import path from "path"; +import assert from "node:assert"; +import path from "node:path"; import ignore from "ignore"; import debugOrig from "debug"; @@ -117,6 +117,9 @@ const DotPatterns = Object.freeze([".*", "!.eslintrc.*", "!../"]); // Public //------------------------------------------------------------------------------ +/** + * Represents a set of glob patterns to ignore against a base path. + */ class IgnorePattern { /** @@ -153,9 +156,7 @@ class IgnorePattern { debug("Create with: %o", ignorePatterns); const basePath = getCommonAncestorPath(ignorePatterns.map(p => p.basePath)); - const patterns = [].concat( - ...ignorePatterns.map(p => p.getPatternsRelativeTo(basePath)) - ); + const patterns = ignorePatterns.flatMap(p => p.getPatternsRelativeTo(basePath)); const ig = ignore({ allowRelativePaths: true }).add([...DotPatterns, ...patterns]); const dotIg = ignore({ allowRelativePaths: true }).add(patterns); diff --git a/lib/config-array/override-tester.js b/lib/config-array/override-tester.js index 460aafcf..3a445b1a 100644 --- a/lib/config-array/override-tester.js +++ b/lib/config-array/override-tester.js @@ -17,9 +17,9 @@ * @author Toru Nagashima */ -import assert from "assert"; -import path from "path"; -import util from "util"; +import assert from "node:assert"; +import path from "node:path"; +import util from "node:util"; import minimatch from "minimatch"; const { Minimatch } = minimatch; @@ -94,6 +94,7 @@ class OverrideTester { * @param {string|string[]} excludedFiles The glob patterns for excluded files. * @param {string} basePath The path to the base directory to test paths. * @returns {OverrideTester|null} The created instance or `null`. + * @throws {Error} When invalid patterns are given. */ static create(files, excludedFiles, basePath) { const includePatterns = normalizePatterns(files); @@ -183,6 +184,7 @@ class OverrideTester { * Test if a given path is matched or not. * @param {string} filePath The absolute path to the target file. * @returns {boolean} `true` if the path was matched. + * @throws {Error} When invalid `filePath` is given. */ test(filePath) { if (typeof filePath !== "string" || !path.isAbsolute(filePath)) { @@ -196,8 +198,8 @@ class OverrideTester { )); } - // eslint-disable-next-line jsdoc/require-description /** + * Converts this instance to a JSON compatible object. * @returns {Object} a JSON compatible object. */ toJSON() { @@ -213,8 +215,8 @@ class OverrideTester { }; } - // eslint-disable-next-line jsdoc/require-description /** + * Custom inspect method for Node.js `console.log()`. * @returns {Object} an object to display by `console.log()`. */ [util.inspect.custom]() { diff --git a/lib/flat-compat.js b/lib/flat-compat.js index e0d7ab66..c5c4e1c2 100644 --- a/lib/flat-compat.js +++ b/lib/flat-compat.js @@ -8,7 +8,7 @@ //----------------------------------------------------------------------------- import createDebug from "debug"; -import path from "path"; +import path from "node:path"; import environments from "../conf/environments.js"; import { ConfigArrayFactory } from "./config-array-factory.js"; @@ -37,6 +37,7 @@ const cafactory = Symbol("cafactory"); * @param {ReadOnlyMap} options.pluginProcessors A map of plugin processor * names to objects. * @returns {Object} A flag-config-style config object. + * @throws {Error} If a plugin or environment cannot be resolved. */ function translateESLintRC(eslintrcConfig, { resolveConfigRelativeTo, @@ -132,7 +133,7 @@ function translateESLintRC(eslintrcConfig, { debug(`Translating plugin: ${pluginName}`); debug(`Resolving plugin '${pluginName} relative to ${resolvePluginsRelativeTo}`); - const { definition: plugin, error } = eslintrcConfig.plugins[pluginName]; + const { original: plugin, error } = eslintrcConfig.plugins[pluginName]; if (error) { throw error; @@ -219,21 +220,31 @@ class FlatCompat { this[cafactory] = new ConfigArrayFactory({ cwd: baseDirectory, resolvePluginsRelativeTo, - getEslintAllConfig: () => { + getEslintAllConfig() { if (!allConfig) { throw new TypeError("Missing parameter 'allConfig' in FlatCompat constructor."); } - return allConfig; + // remove name property if it exists + const config = { ...allConfig }; + + delete config.name; + + return config; }, - getEslintRecommendedConfig: () => { + getEslintRecommendedConfig() { if (!recommendedConfig) { throw new TypeError("Missing parameter 'recommendedConfig' in FlatCompat constructor."); } - return recommendedConfig; + // remove name property if it exists + const config = { ...recommendedConfig }; + + delete config.name; + + return config; } }); } diff --git a/lib/index.js b/lib/index.js index 9e3d13f5..a37e5746 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,7 +8,8 @@ import { ConfigArrayFactory, - createContext as createConfigArrayFactoryContext + createContext as createConfigArrayFactoryContext, + loadConfigFile } from "./config-array-factory.js"; import { CascadingConfigArrayFactory } from "./cascading-config-array-factory.js"; @@ -39,6 +40,7 @@ const Legacy = { OverrideTester, getUsedExtractedConfigs, environments, + loadConfigFile, // shared ConfigOps, diff --git a/lib/shared/ajv.js b/lib/shared/ajv.js index b79ad36c..7e53d12a 100644 --- a/lib/shared/ajv.js +++ b/lib/shared/ajv.js @@ -184,7 +184,7 @@ export default (additionalOptions = {}) => { }); ajv.addMetaSchema(metaSchema); - // eslint-disable-next-line no-underscore-dangle + // eslint-disable-next-line no-underscore-dangle -- part of the API ajv._opts.defaultMeta = metaSchema.id; return ajv; diff --git a/lib/shared/config-ops.js b/lib/shared/config-ops.js index d203be0e..465d9b86 100644 --- a/lib/shared/config-ops.js +++ b/lib/shared/config-ops.js @@ -13,7 +13,7 @@ const RULE_SEVERITY_STRINGS = ["off", "warn", "error"], map[value] = index; return map; }, {}), - VALID_SEVERITIES = [0, 1, 2, "off", "warn", "error"]; + VALID_SEVERITIES = new Set([0, 1, 2, "off", "warn", "error"]); //------------------------------------------------------------------------------ // Public Interface @@ -83,7 +83,7 @@ function isValidSeverity(ruleConfig) { if (typeof severity === "string") { severity = severity.toLowerCase(); } - return VALID_SEVERITIES.indexOf(severity) !== -1; + return VALID_SEVERITIES.has(severity); } /** diff --git a/lib/shared/config-validator.js b/lib/shared/config-validator.js index 32174a56..6857e7a0 100644 --- a/lib/shared/config-validator.js +++ b/lib/shared/config-validator.js @@ -3,16 +3,23 @@ * @author Brandon Mills */ -/* eslint class-methods-use-this: "off" */ +/* eslint class-methods-use-this: "off" -- not needed in this file */ + +//------------------------------------------------------------------------------ +// Typedefs +//------------------------------------------------------------------------------ + +/** @typedef {import("../shared/types").Rule} Rule */ //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ -import util from "util"; +import util from "node:util"; import * as ConfigOps from "./config-ops.js"; import { emitDeprecationWarning } from "./deprecation-warnings.js"; import ajvOrig from "./ajv.js"; +import { deepMergeArrays } from "./deep-merge-arrays.js"; import configSchema from "../../conf/config-schema.js"; import BuiltInEnvironments from "../../conf/environments.js"; @@ -33,10 +40,20 @@ const severityMap = { const validated = new WeakSet(); +// JSON schema that disallows passing any options +const noOptionsSchema = Object.freeze({ + type: "array", + minItems: 0, + maxItems: 0 +}); + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- +/** + * Validator for configuration objects. + */ export default class ConfigValidator { constructor({ builtInRules = new Map() } = {}) { this.builtInRules = builtInRules; @@ -44,17 +61,36 @@ export default class ConfigValidator { /** * Gets a complete options schema for a rule. - * @param {{create: Function, schema: (Array|null)}} rule A new-style rule object - * @returns {Object} JSON Schema for the rule's options. + * @param {Rule} rule A rule object + * @throws {TypeError} If `meta.schema` is specified but is not an array, object or `false`. + * @returns {Object|null} JSON Schema for the rule's options. + * `null` if rule wasn't passed or its `meta.schema` is `false`. */ getRuleOptionsSchema(rule) { if (!rule) { return null; } - const schema = rule.schema || rule.meta && rule.meta.schema; + if (!rule.meta) { + return { ...noOptionsSchema }; // default if `meta.schema` is not specified + } + + const schema = rule.meta.schema; + + if (typeof schema === "undefined") { + return { ...noOptionsSchema }; // default if `meta.schema` is not specified + } - // Given a tuple of schemas, insert warning level at the beginning + // `schema:false` is an allowed explicit opt-out of options validation for the rule + if (schema === false) { + return null; + } + + if (typeof schema !== "object" || schema === null) { + throw new TypeError("Rule's `meta.schema` must be an array or object"); + } + + // ESLint-specific array form needs to be converted into a valid JSON Schema definition if (Array.isArray(schema)) { if (schema.length) { return { @@ -64,22 +100,20 @@ export default class ConfigValidator { maxItems: schema.length }; } - return { - type: "array", - minItems: 0, - maxItems: 0 - }; + // `schema:[]` is an explicit way to specify that the rule does not accept any options + return { ...noOptionsSchema }; } - // Given a full schema, leave it alone - return schema || null; + // `schema:` is assumed to be a valid JSON Schema definition + return schema; } /** * Validates a rule's severity and returns the severity value. Throws an error if the severity is invalid. * @param {options} options The given options for the rule. * @returns {number|string} The rule's severity value + * @throws {Error} If the severity is invalid. */ validateRuleSeverity(options) { const severity = Array.isArray(options) ? options[0] : options; @@ -98,20 +132,32 @@ export default class ConfigValidator { * @param {{create: Function}} rule The rule to validate * @param {Array} localOptions The options for the rule, excluding severity * @returns {void} + * @throws {Error} If the options are invalid. */ validateRuleSchema(rule, localOptions) { if (!ruleValidators.has(rule)) { - const schema = this.getRuleOptionsSchema(rule); + try { + const schema = this.getRuleOptionsSchema(rule); + + if (schema) { + ruleValidators.set(rule, ajv.compile(schema)); + } + } catch (err) { + const errorWithCode = new Error(err.message, { cause: err }); + + errorWithCode.code = "ESLINT_INVALID_RULE_OPTIONS_SCHEMA"; - if (schema) { - ruleValidators.set(rule, ajv.compile(schema)); + throw errorWithCode; } } const validateRule = ruleValidators.get(rule); if (validateRule) { - validateRule(localOptions); + const mergedOptions = deepMergeArrays(rule.meta?.defaultOptions, localOptions); + + validateRule(mergedOptions); + if (validateRule.errors) { throw new Error(validateRule.errors.map( error => `\tValue ${JSON.stringify(error.data)} ${error.message}.\n` @@ -128,6 +174,7 @@ export default class ConfigValidator { * @param {string|null} source The name of the configuration source to report in any errors. If null or undefined, * no source is prepended to the message. * @returns {void} + * @throws {Error} If the options are invalid. */ validateRuleOptions(rule, ruleId, options, source = null) { try { @@ -137,13 +184,21 @@ export default class ConfigValidator { this.validateRuleSchema(rule, Array.isArray(options) ? options.slice(1) : []); } } catch (err) { - const enhancedMessage = `Configuration for rule "${ruleId}" is invalid:\n${err.message}`; + let enhancedMessage = err.code === "ESLINT_INVALID_RULE_OPTIONS_SCHEMA" + ? `Error while processing options validation schema of rule '${ruleId}': ${err.message}` + : `Configuration for rule "${ruleId}" is invalid:\n${err.message}`; if (typeof source === "string") { - throw new Error(`${source}:\n\t${enhancedMessage}`); - } else { - throw new Error(enhancedMessage); + enhancedMessage = `${source}:\n\t${enhancedMessage}`; + } + + const enhancedError = new Error(enhancedMessage, { cause: err }); + + if (err.code) { + enhancedError.code = err.code; } + + throw enhancedError; } } @@ -151,8 +206,9 @@ export default class ConfigValidator { * Validates an environment object * @param {Object} environment The environment config object to validate. * @param {string} source The name of the configuration source to report in any errors. - * @param {function(envId:string): Object} [getAdditionalEnv] A map from strings to loaded environments. + * @param {(envId:string) => Object} [getAdditionalEnv] A map from strings to loaded environments. * @returns {void} + * @throws {Error} If the environment is invalid. */ validateEnvironment( environment, @@ -180,7 +236,7 @@ export default class ConfigValidator { * Validates a rules config object * @param {Object} rulesConfig The rules config object to validate. * @param {string} source The name of the configuration source to report in any errors. - * @param {function(ruleId:string): Object} getAdditionalRule A map from strings to loaded rules + * @param {(ruleId:string) => Object} getAdditionalRule A map from strings to loaded rules * @returns {void} */ validateRules( @@ -224,8 +280,9 @@ export default class ConfigValidator { * Validate `processor` configuration. * @param {string|undefined} processorName The processor name. * @param {string} source The name of config file. - * @param {function(id:string): Processor} getProcessor The getter of defined processors. + * @param {(id:string) => Processor} getProcessor The getter of defined processors. * @returns {void} + * @throws {Error} If the processor is invalid. */ validateProcessor(processorName, source, getProcessor) { if (processorName && !getProcessor(processorName)) { @@ -264,6 +321,7 @@ export default class ConfigValidator { * @param {Object} config The config object to validate. * @param {string} source The name of the configuration source to report in any errors. * @returns {void} + * @throws {Error} If the config is invalid. */ validateConfigSchema(config, source = null) { validateSchema = validateSchema || ajv.compile(configSchema); @@ -272,7 +330,7 @@ export default class ConfigValidator { throw new Error(`ESLint configuration in ${source} is invalid:\n${this.formatErrors(validateSchema.errors)}`); } - if (Object.hasOwnProperty.call(config, "ecmaFeatures")) { + if (Object.hasOwn(config, "ecmaFeatures")) { emitDeprecationWarning(source, "ESLINT_LEGACY_ECMAFEATURES"); } } @@ -281,8 +339,8 @@ export default class ConfigValidator { * Validates an entire config object. * @param {Object} config The config object to validate. * @param {string} source The name of the configuration source to report in any errors. - * @param {function(ruleId:string): Object} [getAdditionalRule] A map from strings to loaded rules. - * @param {function(envId:string): Object} [getAdditionalEnv] A map from strings to loaded envs. + * @param {(ruleId:string) => Object} [getAdditionalRule] A map from strings to loaded rules. + * @param {(envId:string) => Object} [getAdditionalEnv] A map from strings to loaded envs. * @returns {void} */ validate(config, source, getAdditionalRule, getAdditionalEnv) { diff --git a/lib/shared/deep-merge-arrays.js b/lib/shared/deep-merge-arrays.js new file mode 100644 index 00000000..0c7ec349 --- /dev/null +++ b/lib/shared/deep-merge-arrays.js @@ -0,0 +1,58 @@ +/** + * @fileoverview Applies default rule options + * @author JoshuaKGoldberg + */ + +/** + * Check if the variable contains an object strictly rejecting arrays + * @param {unknown} value an object + * @returns {boolean} Whether value is an object + */ +function isObjectNotArray(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** + * Deeply merges second on top of first, creating a new {} object if needed. + * @param {T} first Base, default value. + * @param {U} second User-specified value. + * @returns {T | U | (T & U)} Merged equivalent of second on top of first. + */ +function deepMergeObjects(first, second) { + if (second === void 0) { + return first; + } + + if (!isObjectNotArray(first) || !isObjectNotArray(second)) { + return second; + } + + const result = { ...first, ...second }; + + for (const key of Object.keys(second)) { + if (Object.prototype.propertyIsEnumerable.call(first, key)) { + result[key] = deepMergeObjects(first[key], second[key]); + } + } + + return result; +} + +/** + * Deeply merges second on top of first, creating a new [] array if needed. + * @param {T[] | undefined} first Base, default values. + * @param {U[] | undefined} second User-specified values. + * @returns {(T | U | (T & U))[]} Merged equivalent of second on top of first. + */ +function deepMergeArrays(first, second) { + if (!first || !second) { + return second || first || []; + } + + return [ + ...first.map((value, i) => deepMergeObjects(value, second[i])), + ...second.slice(first.length) + ]; +} + +export { deepMergeArrays }; diff --git a/lib/shared/deprecation-warnings.js b/lib/shared/deprecation-warnings.js index 91907b13..39cfe2c6 100644 --- a/lib/shared/deprecation-warnings.js +++ b/lib/shared/deprecation-warnings.js @@ -7,7 +7,7 @@ // Requirements //------------------------------------------------------------------------------ -import path from "path"; +import path from "node:path"; //------------------------------------------------------------------------------ // Private diff --git a/lib/shared/relative-module-resolver.js b/lib/shared/relative-module-resolver.js index 1df0ca80..81415b42 100644 --- a/lib/shared/relative-module-resolver.js +++ b/lib/shared/relative-module-resolver.js @@ -3,7 +3,7 @@ * @author Teddy Katz */ -import Module from "module"; +import Module from "node:module"; /* * `Module.createRequire` is added in v12.2.0. It supports URL as well. @@ -17,6 +17,7 @@ const createRequire = Module.createRequire; * @param {string} relativeToPath An absolute path indicating the module that `moduleName` should be resolved relative to. This must be * a file rather than a directory, but the file need not actually exist. * @returns {string} The absolute path that would result from calling `require.resolve(moduleName)` in a file located at `relativeToPath` + * @throws {Error} When the module cannot be resolved. */ function resolve(moduleName, relativeToPath) { try { diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts new file mode 100644 index 00000000..258d3a5f --- /dev/null +++ b/lib/types/index.d.ts @@ -0,0 +1,76 @@ +/** + * @fileoverview This file contains the core types for ESLint. It was initially extracted + * from the `@types/eslint__eslintrc` package. + */ + +/* + * MIT License + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + +import type { Linter } from "eslint"; + +/** + * A compatibility class for working with configs. + */ +export class FlatCompat { + constructor({ + baseDirectory, + resolvePluginsRelativeTo, + recommendedConfig, + allConfig, + }?: { + /** + * default: process.cwd() + */ + baseDirectory?: string; + resolvePluginsRelativeTo?: string; + recommendedConfig?: Linter.LegacyConfig; + allConfig?: Linter.LegacyConfig; + }); + + /** + * Translates an ESLintRC-style config into a flag-config-style config. + * @param eslintrcConfig The ESLintRC-style config object. + * @returns A flag-config-style config object. + */ + config(eslintrcConfig: Linter.LegacyConfig): Linter.Config[]; + + /** + * Translates the `env` section of an ESLintRC-style config. + * @param envConfig The `env` section of an ESLintRC config. + * @returns An array of flag-config objects representing the environments. + */ + env(envConfig: { [name: string]: boolean }): Linter.Config[]; + + /** + * Translates the `extends` section of an ESLintRC-style config. + * @param configsToExtend The names of the configs to load. + * @returns An array of flag-config objects representing the config. + */ + extends(...configsToExtend: string[]): Linter.Config[]; + + /** + * Translates the `plugins` section of an ESLintRC-style config. + * @param plugins The names of the plugins to load. + * @returns An array of flag-config objects representing the plugins. + */ + plugins(...plugins: string[]): Linter.Config[]; +} diff --git a/package.json b/package.json index 6f148ac7..030d3fc7 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,15 @@ { "name": "@eslint/eslintrc", - "version": "2.1.3", + "version": "3.3.3", "description": "The legacy ESLintRC config file format for ESLint", "type": "module", "main": "./dist/eslintrc.cjs", + "types": "./dist/eslintrc.d.cts", "exports": { ".": { "import": "./lib/index.js", - "require": "./dist/eslintrc.cjs" + "require": "./dist/eslintrc.cjs", + "types": "./lib/types/index.d.ts" }, "./package.json": "./package.json", "./universal": { @@ -26,16 +28,12 @@ "access": "public" }, "scripts": { - "build": "rollup -c", + "build": "rollup -c && node -e \"fs.copyFileSync('./lib/types/index.d.ts', './dist/eslintrc.d.cts')\"", "lint": "eslint . --report-unused-disable-directives", "lint:fix": "npm run lint -- --fix", "prepare": "npm run build", - "release:generate:latest": "eslint-generate-release", - "release:generate:alpha": "eslint-generate-prerelease alpha", - "release:generate:beta": "eslint-generate-prerelease beta", - "release:generate:rc": "eslint-generate-prerelease rc", - "release:publish": "eslint-publish-release", - "test": "mocha -R progress -c 'tests/lib/*.cjs' && c8 mocha -R progress -c 'tests/lib/**/*.js'" + "test": "mocha -R progress -c 'tests/lib/*.cjs' && c8 mocha -R progress -c 'tests/lib/**/*.js'", + "test:types": "tsc -p tests/lib/types/tsconfig.json" }, "repository": "eslint/eslintrc", "funding": "https://opencollective.com/eslint", @@ -53,30 +51,28 @@ "devDependencies": { "c8": "^7.7.3", "chai": "^4.3.4", - "eslint": "^7.31.0", - "eslint-config-eslint": "^7.0.0", - "eslint-plugin-jsdoc": "^35.4.1", - "eslint-plugin-node": "^11.1.0", - "eslint-release": "^3.2.0", + "eslint": "^9.20.1", + "eslint-config-eslint": "^11.0.0", "fs-teardown": "^0.1.3", "mocha": "^9.0.3", "rollup": "^2.70.1", - "shelljs": "^0.8.4", + "shelljs": "^0.8.5", "sinon": "^11.1.2", - "temp-dir": "^2.0.0" + "temp-dir": "^2.0.0", + "typescript": "^5.7.3" }, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } } diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..7fb338a4 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,9 @@ +{ + "bootstrap-sha": "556e80029f01d07758ab1f5801bc9421bca4b072", + "packages": { + ".": { + "release-type": "node", + "pull-request-title-pattern": "chore: release ${version} 🚀" + } + } +} diff --git a/rollup.config.js b/rollup.config.js index c8f1a330..6e151eff 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,3 +1,21 @@ +/** List of modules not included in the bundle. */ +const external = [ + "node:assert", + "node:fs", + "node:module", + "node:os", + "node:path", + "node:url", + "node:util", + "ajv", + "debug", + "globals", + "ignore", + "import-fresh", + "minimatch", + "strip-json-comments" +]; + /** * Custom Rollup plugin for `import.meta.url` transformation to commonjs. * The default transformation ('file:' + __filename) does not check characters in __filename, @@ -20,11 +38,7 @@ function importMetaURLPlugin() { export default [ { input: "./lib/index.js", - external: [ - "module", "util", "os", "path", "debug", "fs", "import-fresh", - "strip-json-comments", "assert", "ignore", "minimatch", "url", "ajv", - "globals" - ], + external, treeshake: false, output: { format: "cjs", @@ -36,11 +50,7 @@ export default [ }, { input: "./lib/index-universal.js", - external: [ - "module", "util", "os", "path", "debug", "fs", "import-fresh", - "strip-json-comments", "assert", "ignore", "minimatch", "url", "ajv", - "globals" - ], + external, treeshake: false, output: { format: "cjs", diff --git a/tests/fixtures/rules/custom-rule.cjs b/tests/fixtures/rules/custom-rule.cjs index 6d8b662b..a7cc1340 100644 --- a/tests/fixtures/rules/custom-rule.cjs +++ b/tests/fixtures/rules/custom-rule.cjs @@ -1,15 +1,17 @@ -module.exports = function(context) { +"use strict"; - "use strict"; +module.exports = { + meta: { + schema: [] + }, - return { - "Identifier": function(node) { - if (node.name === "foo") { - context.report(node, "Identifier cannot be named 'foo'."); + create(context) { + return { + "Identifier": function(node) { + if (node.name === "foo") { + context.report(node, "Identifier cannot be named 'foo'."); + } } - } - }; - + }; + } }; - -module.exports.schema = []; diff --git a/tests/fixtures/rules/dir1/no-strings.cjs b/tests/fixtures/rules/dir1/no-strings.cjs index 1f566ac0..4997bbbe 100644 --- a/tests/fixtures/rules/dir1/no-strings.cjs +++ b/tests/fixtures/rules/dir1/no-strings.cjs @@ -1,14 +1,15 @@ "use strict"; -module.exports = function(context) { +module.exports = { + create(context) { + return { - return { + "Literal": function(node) { + if (typeof node.value === 'string') { + context.report(node, "String!"); + } - "Literal": function(node) { - if (typeof node.value === 'string') { - context.report(node, "String!"); } - - } - }; + }; + } }; diff --git a/tests/fixtures/rules/dir2/no-literals.cjs b/tests/fixtures/rules/dir2/no-literals.cjs index fdaa2d08..d872718a 100644 --- a/tests/fixtures/rules/dir2/no-literals.cjs +++ b/tests/fixtures/rules/dir2/no-literals.cjs @@ -1,11 +1,12 @@ "use strict"; -module.exports = function(context) { +module.exports = { + create (context) { + return { - return { - - "Literal": function(node) { - context.report(node, "Literal!"); - } - }; + "Literal": function(node) { + context.report(node, "Literal!"); + } + }; + } }; diff --git a/tests/fixtures/rules/make-syntax-error-rule.cjs b/tests/fixtures/rules/make-syntax-error-rule.cjs index 528b4b0f..3d34d9d1 100644 --- a/tests/fixtures/rules/make-syntax-error-rule.cjs +++ b/tests/fixtures/rules/make-syntax-error-rule.cjs @@ -1,14 +1,18 @@ -module.exports = function(context) { - return { - Program: function(node) { - context.report({ - node: node, - message: "ERROR", - fix: function(fixer) { - return fixer.insertTextAfter(node, "this is a syntax error."); - } - }); - } - }; +module.exports = { + meta: { + schema: [] + }, + create(context) { + return { + Program: function(node) { + context.report({ + node: node, + message: "ERROR", + fix: function(fixer) { + return fixer.insertTextAfter(node, "this is a syntax error."); + } + }); + } + }; + } }; -module.exports.schema = []; diff --git a/tests/fixtures/rules/wrong/custom-rule.cjs b/tests/fixtures/rules/wrong/custom-rule.cjs index 9a64230c..b3923e4d 100644 --- a/tests/fixtures/rules/wrong/custom-rule.cjs +++ b/tests/fixtures/rules/wrong/custom-rule.cjs @@ -1,5 +1,7 @@ -module.exports = function() { +"use strict"; - "use strict"; - return (null).something; +module.exports = { + create() { + return (null).something; + } }; diff --git a/tests/lib/cascading-config-array-factory.js b/tests/lib/cascading-config-array-factory.js index 7250f9a8..7567e423 100644 --- a/tests/lib/cascading-config-array-factory.js +++ b/tests/lib/cascading-config-array-factory.js @@ -8,14 +8,14 @@ //----------------------------------------------------------------------------- import { assert } from "chai"; -import fs from "fs"; -import { createRequire } from "module"; -import os from "os"; -import path from "path"; +import fs from "node:fs"; +import { createRequire } from "node:module"; +import os from "node:os"; +import path from "node:path"; import sh from "shelljs"; import sinon from "sinon"; import systemTempDir from "temp-dir"; -import { fileURLToPath } from "url"; +import { fileURLToPath } from "node:url"; import { Legacy } from "../../lib/index.js"; import { createCustomTeardown } from "../_utils/index.js"; @@ -557,7 +557,8 @@ describe("CascadingConfigArrayFactory", () => { // key is path, value is file content (string) const flattened = {}; - /** Recursively joins path segments and populates `flattened` object + /** + * Recursively joins path segments and populates `flattened` object * @param {Object} object key is path segment, value is file content (string) or another object of the same kind * @param {string} prefix parent directory * @returns {void} @@ -654,7 +655,7 @@ describe("CascadingConfigArrayFactory", () => { * exceeds the default test timeout, so raise it just for this hook. * Mocha uses `this` to set timeouts on an individual hook level. */ - this.timeout(60 * 1000); // eslint-disable-line no-invalid-this + this.timeout(60 * 1000); // eslint-disable-line no-invalid-this -- needed for test fixtureDir = `${systemTempDir}/eslint/fixtures`; sh.mkdir("-p", fixtureDir); @@ -696,7 +697,7 @@ describe("CascadingConfigArrayFactory", () => { }); // TODO: Tests should not rely on project files!!! - it("should return the project config when called in current working directory", () => { + it.skip("should return the project config when called in current working directory", () => { const factory = new CascadingConfigArrayFactory({ eslintAllPath, eslintRecommendedPath @@ -1882,7 +1883,7 @@ describe("CascadingConfigArrayFactory", () => { * exceeds the default test timeout, so raise it just for this hook. * Mocha uses `this` to set timeouts on an individual hook level. */ - this.timeout(60 * 1000); // eslint-disable-line no-invalid-this + this.timeout(60 * 1000); // eslint-disable-line no-invalid-this -- needed for test fixtureDir = `${systemTempDir}/eslint/fixtures`; sh.mkdir("-p", fixtureDir); @@ -1924,7 +1925,7 @@ describe("CascadingConfigArrayFactory", () => { }); // TODO: Tests should not rely on project files!!! - it("should return the project config when called in current working directory", () => { + it.skip("should return the project config when called in current working directory", () => { const factory = new CascadingConfigArrayFactory({ getEslintAllConfig, getEslintRecommendedConfig diff --git a/tests/lib/commonjs.cjs b/tests/lib/commonjs.cjs index a6840cde..3d17a017 100644 --- a/tests/lib/commonjs.cjs +++ b/tests/lib/commonjs.cjs @@ -9,10 +9,10 @@ // Requirements //------------------------------------------------------------------------------ -const assert = require("assert"); +const assert = require("node:assert"); const eslintrc = require("../../dist/eslintrc.cjs"); const universal = require("../../dist/eslintrc-universal.cjs"); -const path = require("path"); +const path = require("node:path"); const sh = require("shelljs"); //------------------------------------------------------------------------------ diff --git a/tests/lib/config-array-factory.js b/tests/lib/config-array-factory.js index 6b991381..4951770e 100644 --- a/tests/lib/config-array-factory.js +++ b/tests/lib/config-array-factory.js @@ -8,12 +8,12 @@ //----------------------------------------------------------------------------- import { assert } from "chai"; -import fs from "fs"; -import { createRequire } from "module"; -import path from "path"; +import fs from "node:fs"; +import { createRequire } from "node:module"; +import path from "node:path"; import sinon from "sinon"; import systemTempDir from "temp-dir"; -import { fileURLToPath } from "url"; +import { fileURLToPath } from "node:url"; import { Legacy } from "../../lib/index.js"; import { createCustomTeardown } from "../_utils/index.js"; @@ -173,7 +173,7 @@ describe("ConfigArrayFactory", () => { it("should return a config array that contains the yielded elements from '_normalizeConfigData(configData, ctx)'.", () => { const elements = [{}, {}]; - factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle + factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle -- needed for test const configArray = factory.create({}); @@ -237,7 +237,7 @@ describe("ConfigArrayFactory", () => { }); for (const filePath of Object.keys(basicFiles)) { - it(`should load '${filePath}' then return a config array what contains that file content.`, () => { // eslint-disable-line no-loop-func + it(`should load '${filePath}' then return a config array what contains that file content.`, () => { // eslint-disable-line no-loop-func -- needed for test const configArray = factory.loadFile(filePath); assert.strictEqual(configArray.length, 1); @@ -267,7 +267,7 @@ describe("ConfigArrayFactory", () => { it("should return a config array that contains the yielded elements from '_normalizeConfigData(configData, ctx)'.", () => { const elements = [{}, {}]; - factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle + factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle -- needed for test const configArray = factory.loadFile("js/.eslintrc.js"); @@ -335,7 +335,7 @@ describe("ConfigArrayFactory", () => { for (const filePath of Object.keys(basicFiles)) { const directoryPath = filePath.split("/")[0]; - it(`should load '${directoryPath}' then return a config array what contains the config file of that directory.`, () => { // eslint-disable-line no-loop-func + it(`should load '${directoryPath}' then return a config array what contains the config file of that directory.`, () => { // eslint-disable-line no-loop-func -- needed for test const configArray = factory.loadInDirectory(directoryPath); assert.strictEqual(configArray.length, 1); @@ -365,7 +365,7 @@ describe("ConfigArrayFactory", () => { it("should return a config array that contains the yielded elements from '_normalizeConfigData(configData, ctx)'.", () => { const elements = [{}, {}]; - factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle + factory._normalizeConfigData = () => elements; // eslint-disable-line no-underscore-dangle -- needed for test const configArray = factory.loadInDirectory("js"); @@ -395,7 +395,7 @@ describe("ConfigArrayFactory", () => { function create(configData, { filePath, name } = {}) { const ctx = createContext({ cwd: tempDir }, void 0, name, filePath, void 0); - return new ConfigArray(...factory._normalizeConfigData(configData, ctx)); // eslint-disable-line no-underscore-dangle + return new ConfigArray(...factory._normalizeConfigData(configData, ctx)); // eslint-disable-line no-underscore-dangle -- needed for test } describe("misc", () => { @@ -631,6 +631,10 @@ describe("ConfigArrayFactory", () => { it("should have the path to the package at 'plugins[id].filePath' property.", () => { assert.strictEqual(element.plugins.xxx.filePath, path.join(getPath(), "node_modules/custom-eslint-plugin-xxx/index.js")); }); + + it("should have the original definition equal to the origina plugin object", () => { + assert.strictEqual(element.plugins.xxx.original, require(path.join(getPath(), "node_modules/custom-eslint-plugin-xxx/index.js"))); + }); }); describe("if 'extends' property was 'foo', the returned value", () => { diff --git a/tests/lib/config-array/config-array.js b/tests/lib/config-array/config-array.js index d0808ee8..2883671e 100644 --- a/tests/lib/config-array/config-array.js +++ b/tests/lib/config-array/config-array.js @@ -3,8 +3,8 @@ * @author Toru Nagashima */ -import path from "path"; -import { fileURLToPath } from "url"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; import { assert } from "chai"; import { ConfigArray, OverrideTester, getUsedExtractedConfigs } from "../../../lib/config-array/index.js"; @@ -37,7 +37,7 @@ describe("ConfigArray", () => { }); for (let i = 0; i < elements.length; ++i) { - it(`should have ${JSON.stringify(elements[i])} at configArray[${i}].`, () => { // eslint-disable-line no-loop-func + it(`should have ${JSON.stringify(elements[i])} at configArray[${i}].`, () => { // eslint-disable-line no-loop-func -- needed for test assert.strictEqual(configArray[i], elements[i]); }); } @@ -702,7 +702,7 @@ describe("ConfigArray", () => { { filePaths: [filename, `${filename}.ts`] }, { filePaths: [filename, `${filename}.ts`, path.join(dirname, "foo.js")] } ]) { - describe(`after it called 'extractConfig(filePath)' ${filePaths.length} time(s) with ${JSON.stringify(filePaths, null, 4)}, the returned array`, () => { // eslint-disable-line no-loop-func + describe(`after it called 'extractConfig(filePath)' ${filePaths.length} time(s) with ${JSON.stringify(filePaths, null, 4)}, the returned array`, () => { // eslint-disable-line no-loop-func -- needed for test let configs; let usedConfigs; @@ -716,7 +716,7 @@ describe("ConfigArray", () => { }); for (let i = 0; i < filePaths.length; ++i) { - it(`should contain 'configs[${i}]'.`, () => { // eslint-disable-line no-loop-func + it(`should contain 'configs[${i}]'.`, () => { // eslint-disable-line no-loop-func -- needed for test assert(usedConfigs.includes(configs[i])); }); } diff --git a/tests/lib/config-array/config-dependency.js b/tests/lib/config-array/config-dependency.js index 3087b8ca..ddb04b88 100644 --- a/tests/lib/config-array/config-dependency.js +++ b/tests/lib/config-array/config-dependency.js @@ -3,9 +3,9 @@ * @author Toru Nagashima */ -import assert from "assert"; -import { Console } from "console"; -import { Writable } from "stream"; +import assert from "node:assert"; +import { Console } from "node:console"; +import { Writable } from "node:stream"; import { ConfigDependency } from "../../../lib/config-array/config-dependency.js"; describe("ConfigDependency", () => { @@ -80,7 +80,7 @@ describe("ConfigDependency", () => { let output = ""; const localConsole = new Console( new class extends Writable { - write(chunk) { // eslint-disable-line class-methods-use-this + write(chunk) { // eslint-disable-line class-methods-use-this -- needed for test output += chunk; } }() diff --git a/tests/lib/config-array/extracted-config.js b/tests/lib/config-array/extracted-config.js index cf2a8c71..52d8c4a9 100644 --- a/tests/lib/config-array/extracted-config.js +++ b/tests/lib/config-array/extracted-config.js @@ -3,7 +3,7 @@ * @author Toru Nagashima */ -import assert from "assert"; +import assert from "node:assert"; import { ExtractedConfig } from "../../../lib/config-array/extracted-config.js"; describe("'ExtractedConfig' class", () => { diff --git a/tests/lib/config-array/ignore-pattern.js b/tests/lib/config-array/ignore-pattern.js index 9be97452..10030199 100644 --- a/tests/lib/config-array/ignore-pattern.js +++ b/tests/lib/config-array/ignore-pattern.js @@ -3,8 +3,8 @@ * @author Toru Nagashima */ -import assert from "assert"; -import path from "path"; +import assert from "node:assert"; +import path from "node:path"; import sinon from "sinon"; import { IgnorePattern } from "../../../lib/config-array/ignore-pattern.js"; diff --git a/tests/lib/config-array/override-tester.js b/tests/lib/config-array/override-tester.js index ba54cfe5..a9837800 100644 --- a/tests/lib/config-array/override-tester.js +++ b/tests/lib/config-array/override-tester.js @@ -3,10 +3,10 @@ * @author Toru Nagashima */ -import assert from "assert"; -import { Console } from "console"; -import path from "path"; -import { Writable } from "stream"; +import assert from "node:assert"; +import { Console } from "node:console"; +import path from "node:path"; +import { Writable } from "node:stream"; import { OverrideTester } from "../../../lib/config-array/override-tester.js"; describe("OverrideTester", () => { diff --git a/tests/lib/flat-compat.js b/tests/lib/flat-compat.js index 2afb38a0..a46ebb62 100644 --- a/tests/lib/flat-compat.js +++ b/tests/lib/flat-compat.js @@ -7,13 +7,15 @@ // Requirements //----------------------------------------------------------------------------- -import path from "path"; -import { fileURLToPath, pathToFileURL } from "url"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { assert } from "chai"; import { FlatCompat } from "../../lib/index.js"; import environments from "../../conf/environments.js"; +import { createRequire } from "node:module"; const dirname = path.dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); //----------------------------------------------------------------------------- // Helpers @@ -21,22 +23,6 @@ const dirname = path.dirname(fileURLToPath(import.meta.url)); const FIXTURES_BASE_PATH = path.resolve(dirname, "../fixtures/flat-compat/"); -/** - * Normalizes a plugin object to have all available keys. This matches what - * ConfigArrayFactory does. - * @param {Object} plugin The plugin object to normalize. - * @returns {Object} The normalized plugin object. - */ -function normalizePlugin(plugin) { - return { - configs: {}, - rules: {}, - environments: {}, - processors: {}, - ...plugin - }; -} - /** * Returns the full directory path for a fixture directory. * @param {string} dirName The directory name to resolve. @@ -56,9 +42,9 @@ describe("FlatCompat", () => { let compat; const baseDirectory = getFixturePath("config"); - const pluginFixture1 = normalizePlugin((await import(pathToFileURL(path.join(baseDirectory, "node_modules/eslint-plugin-fixture1.js")))).default); - const pluginFixture2 = normalizePlugin((await import(pathToFileURL(path.join(baseDirectory, "node_modules/eslint-plugin-fixture2.js")))).default); - const pluginFixture3 = normalizePlugin((await import(pathToFileURL(path.join(baseDirectory, "node_modules/eslint-plugin-fixture3.js")))).default); + const pluginFixture1 = (await import(pathToFileURL(path.join(baseDirectory, "node_modules/eslint-plugin-fixture1.js")))).default; + const pluginFixture2 = (await import(pathToFileURL(path.join(baseDirectory, "node_modules/eslint-plugin-fixture2.js")))).default; + const pluginFixture3 = (await import(pathToFileURL(path.join(baseDirectory, "node_modules/eslint-plugin-fixture3.js")))).default; beforeEach(() => { compat = new FlatCompat({ @@ -1041,6 +1027,28 @@ describe("FlatCompat", () => { }, /Missing parameter 'recommendedConfig'/gu); }); + it("should remove name property from eslint:all and eslint:recommended configs", () => { + const compatWithNames = new FlatCompat({ + baseDirectory: getFixturePath("config"), + recommendedConfig: { + name: "eslint:recommended", + settings: { "eslint:recommended": true } + }, + allConfig: { + name: "eslint:all", + settings: { "eslint:all": true } + } + }); + + const allResult = compatWithNames.extends("eslint:all"); + const recommendedResult = compatWithNames.extends("eslint:recommended"); + + assert.strictEqual(allResult.length, 1); + assert.isTrue(allResult[0].settings["eslint:all"]); + + assert.strictEqual(recommendedResult.length, 1); + assert.isTrue(recommendedResult[0].settings["eslint:recommended"]); + }); }); describe("plugins()", () => { @@ -1059,13 +1067,7 @@ describe("FlatCompat", () => { assert.strictEqual(result.length, 1); assert.deepStrictEqual(result[0], { plugins: { - fixture1: { - configs: {}, - rules: {}, - environments: {}, - processors: {}, - ...(await import(pathToFileURL(path.join(compat.baseDirectory, "node_modules/eslint-plugin-fixture1.js")))).default - } + fixture1: (await import(pathToFileURL(path.join(compat.baseDirectory, "node_modules/eslint-plugin-fixture1.js")))).default } }); }); @@ -1087,13 +1089,7 @@ describe("FlatCompat", () => { }); assert.deepStrictEqual(result[1], { plugins: { - fixture2: { - configs: {}, - rules: {}, - environments: {}, - processors: {}, - ...plugin - } + fixture2: plugin } }); }); @@ -1109,24 +1105,19 @@ describe("FlatCompat", () => { }); assert.deepStrictEqual(result[1], { plugins: { - fixture1: { - configs: {}, - rules: {}, - environments: {}, - processors: {}, - ...(await import(pathToFileURL(path.join(compat.baseDirectory, "node_modules/eslint-plugin-fixture1.js")))).default - }, - fixture2: { - configs: {}, - rules: {}, - environments: {}, - processors: {}, - ...plugin - } + fixture1: (await import(pathToFileURL(path.join(compat.baseDirectory, "node_modules/eslint-plugin-fixture1.js")))).default, + fixture2: plugin } }); }); + it("should use the same plugin instance as require()", async () => { + const result = compat.config({ plugins: ["fixture2"] }); + const plugin = require(path.join(compat.baseDirectory, "node_modules/eslint-plugin-fixture2.js")); + + assert.strictEqual(result[1].plugins.fixture2, plugin); + }); + }); }); diff --git a/tests/lib/shared/config-ops.js b/tests/lib/shared/config-ops.js index 574918e3..cb1a92c4 100644 --- a/tests/lib/shared/config-ops.js +++ b/tests/lib/shared/config-ops.js @@ -9,7 +9,7 @@ import { assert } from "chai"; -import util from "util"; +import util from "node:util"; import * as ConfigOps from "../../../lib/shared/config-ops.js"; diff --git a/tests/lib/shared/config-validator.js b/tests/lib/shared/config-validator.js new file mode 100644 index 00000000..dd2bdcea --- /dev/null +++ b/tests/lib/shared/config-validator.js @@ -0,0 +1,286 @@ +/** + * @fileoverview Tests for ConfigValidator class + */ + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import { assert } from "chai"; +import nodeAssert from "node:assert"; + +import ConfigValidator from "../../../lib/shared/config-validator.js"; + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const mockRule = { + meta: { + schema: [{ + enum: ["first", "second"] + }] + }, + create(context) { + return { + Program(node) { + context.report(node, "Expected a validation error."); + } + }; + } +}; + +const mockObjectRule = { + meta: { + schema: { + enum: ["first", "second"] + } + }, + create(context) { + return { + Program(node) { + context.report(node, "Expected a validation error."); + } + }; + } +}; + +const mockInvalidSchemaTypeRule = { + meta: { + schema: true + }, + create(context) { + return { + Program(node) { + context.report(node, "Expected a validation error."); + } + }; + } +}; + +const mockInvalidJSONSchemaRule = { + meta: { + schema: { + minItems: [] + } + }, + create(context) { + return { + Program(node) { + context.report(node, "Expected a validation error."); + } + }; + } +}; + +const mockMaxPropertiesSchema = { + meta: { + defaultOptions: [{ + foo: 42 + }], + schema: [{ + type: "object", + maxProperties: 2 + }] + }, + create() { + return {}; + } +}; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe("ConfigValidator", () => { + let validator; + + beforeEach(() => { + validator = new ConfigValidator(); + }); + + describe("getRuleOptionsSchema", () => { + + const noOptionsSchema = { + type: "array", + minItems: 0, + maxItems: 0 + }; + + it("should return null for a missing rule", () => { + assert.strictEqual(validator.getRuleOptionsSchema(void 0), null); + assert.strictEqual(validator.getRuleOptionsSchema(null), null); + }); + + it("should not modify object schema", () => { + assert.deepStrictEqual(validator.getRuleOptionsSchema(mockObjectRule), { + enum: ["first", "second"] + }); + }); + + it("should return schema that doesn't accept options if rule doesn't have `meta`", () => { + const rule = {}; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return schema that doesn't accept options if rule doesn't have `meta.schema`", () => { + const rule = { meta: {} }; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return schema that doesn't accept options if `meta.schema` is `undefined`", () => { + const rule = { meta: { schema: void 0 } }; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return schema that doesn't accept options if `meta.schema` is `[]`", () => { + const rule = { meta: { schema: [] } }; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + it("should return JSON Schema definition object if `meta.schema` is in the array form", () => { + const firstOption = { enum: ["always", "never"] }; + const rule = { meta: { schema: [firstOption] } }; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual( + result, + { + type: "array", + items: [firstOption], + minItems: 0, + maxItems: 1 + } + ); + }); + + it("should return `meta.schema` as is if `meta.schema` is an object", () => { + const schema = { + type: "array", + items: [{ + enum: ["always", "never"] + }] + }; + const rule = { meta: { schema } }; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, schema); + }); + + it("should return `null` if `meta.schema` is `false`", () => { + const rule = { meta: { schema: false } }; + const result = validator.getRuleOptionsSchema(rule); + + assert.strictEqual(result, null); + }); + + [null, true, 0, 1, "", "always", () => {}].forEach(schema => { + it(`should throw an error if \`meta.schema\` is ${typeof schema} ${schema}`, () => { + const rule = { meta: { schema } }; + + assert.throws(() => { + validator.getRuleOptionsSchema(rule); + }, "Rule's `meta.schema` must be an array or object"); + }); + }); + + it("should ignore top-level `schema` property", () => { + const rule = { schema: { enum: ["always", "never"] } }; + const result = validator.getRuleOptionsSchema(rule); + + assert.deepStrictEqual(result, noOptionsSchema); + }); + + }); + + describe("validateRuleOptions", () => { + + it("should throw for incorrect warning level number", () => { + const fn = validator.validateRuleOptions.bind(validator, mockRule, "mock-rule", 3, "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); + }); + + it("should throw for incorrect warning level string", () => { + const fn = validator.validateRuleOptions.bind(validator, mockRule, "mock-rule", "booya", "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '\"booya\"').\n"); + }); + + it("should throw for invalid-type warning level", () => { + const fn = validator.validateRuleOptions.bind(validator, mockRule, "mock-rule", [["error"]], "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '[ \"error\" ]').\n"); + }); + + it("should only check warning level for nonexistent rules", () => { + const fn1 = validator.validateRuleOptions.bind(validator, void 0, "non-existent-rule", [3, "foobar"], "tests"); + + assert.throws(fn1, "tests:\n\tConfiguration for rule \"non-existent-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); + + const fn2 = validator.validateRuleOptions.bind(validator, null, "non-existent-rule", [3, "foobar"], "tests"); + + assert.throws(fn2, "tests:\n\tConfiguration for rule \"non-existent-rule\" is invalid:\n\tSeverity should be one of the following: 0 = off, 1 = warn, 2 = error (you passed '3').\n"); + }); + + it("should throw for incorrect configuration values", () => { + const fn = validator.validateRuleOptions.bind(validator, mockRule, "mock-rule", [2, "frist"], "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue \"frist\" should be equal to one of the allowed values.\n"); + }); + + it("should throw for too many configuration values", () => { + const fn = validator.validateRuleOptions.bind(validator, mockRule, "mock-rule", [2, "first", "second"], "tests"); + + assert.throws(fn, "tests:\n\tConfiguration for rule \"mock-rule\" is invalid:\n\tValue [\"first\",\"second\"] should NOT have more than 1 items.\n"); + }); + + it("should throw with error code ESLINT_INVALID_RULE_OPTIONS_SCHEMA for rule with an invalid schema type", () => { + const fn = validator.validateRuleOptions.bind(validator, mockInvalidSchemaTypeRule, "invalid-schema-rule", [2], "tests"); + + nodeAssert.throws( + fn, + { + code: "ESLINT_INVALID_RULE_OPTIONS_SCHEMA", + message: "tests:\n\tError while processing options validation schema of rule 'invalid-schema-rule': Rule's `meta.schema` must be an array or object" + } + ); + }); + + it("should throw with error code ESLINT_INVALID_RULE_OPTIONS_SCHEMA for rule with an invalid JSON schema", () => { + const fn = validator.validateRuleOptions.bind(validator, mockInvalidJSONSchemaRule, "invalid-schema-rule", [2], "tests"); + + nodeAssert.throws( + fn, + { + code: "ESLINT_INVALID_RULE_OPTIONS_SCHEMA", + message: "tests:\n\tError while processing options validation schema of rule 'invalid-schema-rule': minItems must be number" + } + ); + }); + + }); + + describe("validateRuleSchema", () => { + + it("should throw when rule options are invalid after defaults are applied", () => { + const fn = validator.validateRuleSchema.bind(validator, mockMaxPropertiesSchema, [{ bar: 6, baz: 7 }]); + + nodeAssert.throws( + fn, + { + message: '\tValue {"foo":42,"bar":6,"baz":7} should NOT have more than 2 properties.\n' + } + ); + }); + + }); +}); diff --git a/tests/lib/shared/deep-merge-arrays.js b/tests/lib/shared/deep-merge-arrays.js new file mode 100644 index 00000000..55cc272a --- /dev/null +++ b/tests/lib/shared/deep-merge-arrays.js @@ -0,0 +1,134 @@ +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import assert from "node:assert"; + +import { deepMergeArrays } from "../../../lib/shared/deep-merge-arrays.js"; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +/** + * Turns a value into its string equivalent for a test name. + * @param {unknown} value Value to be stringified. + * @returns {string} String equivalent of the value. + */ +function toTestCaseName(value) { + return typeof value === "object" ? JSON.stringify(value) : `${value}`; +} + +describe("deepMerge", () => { + for (const [first, second, result] of [ + [void 0, void 0, []], + [[], void 0, []], + [["abc"], void 0, ["abc"]], + [void 0, ["abc"], ["abc"]], + [[], ["abc"], ["abc"]], + [[void 0], ["abc"], ["abc"]], + [[void 0, void 0], ["abc"], ["abc", void 0]], + [[void 0, void 0], ["abc", "def"], ["abc", "def"]], + [[void 0, null], ["abc"], ["abc", null]], + [[void 0, null], ["abc", "def"], ["abc", "def"]], + [[null], ["abc"], ["abc"]], + [[123], [void 0], [123]], + [[123], [null], [null]], + [[123], [{ a: 0 }], [{ a: 0 }]], + [["abc"], [void 0], ["abc"]], + [["abc"], [null], [null]], + [["abc"], ["def"], ["def"]], + [["abc"], [{ a: 0 }], [{ a: 0 }]], + [[["abc"]], [null], [null]], + [[["abc"]], ["def"], ["def"]], + [[["abc"]], [{ a: 0 }], [{ a: 0 }]], + [[{ abc: true }], ["def"], ["def"]], + [[{ abc: true }], [["def"]], [["def"]]], + [[null], [{ abc: true }], [{ abc: true }]], + [[{ a: void 0 }], [{ a: 0 }], [{ a: 0 }]], + [[{ a: null }], [{ a: 0 }], [{ a: 0 }]], + [[{ a: null }], [{ a: { b: 0 } }], [{ a: { b: 0 } }]], + [[{ a: 0 }], [{ a: 1 }], [{ a: 1 }]], + [[{ a: 0 }], [{ a: null }], [{ a: null }]], + [[{ a: 0 }], [{ a: void 0 }], [{ a: 0 }]], + [[{ a: 0 }], ["abc"], ["abc"]], + [[{ a: 0 }], [123], [123]], + [[[{ a: 0 }]], [123], [123]], + [ + [{ a: ["b"] }], + [{ a: ["c"] }], + [{ a: ["c"] }] + ], + [ + [{ a: [{ b: "c" }] }], + [{ a: [{ d: "e" }] }], + [{ a: [{ d: "e" }] }] + ], + [ + [{ a: { b: "c" }, d: true }], + [{ a: { e: "f" } }], + [{ a: { b: "c", e: "f" }, d: true }] + ], + [ + [{ a: { b: "c" } }], + [{ a: { e: "f" }, d: true }], + [{ a: { b: "c", e: "f" }, d: true }] + ], + [ + [{ a: { b: "c" } }, { d: true }], + [{ a: { e: "f" } }, { f: 123 }], + [{ a: { b: "c", e: "f" } }, { d: true, f: 123 }] + ], + [ + [{ hasOwnProperty: true }], + [{}], + [{ hasOwnProperty: true }] + ], + [ + [{ hasOwnProperty: false }], + [{}], + [{ hasOwnProperty: false }] + ], + [ + [{ hasOwnProperty: null }], + [{}], + [{ hasOwnProperty: null }] + ], + [ + [{ hasOwnProperty: void 0 }], + [{}], + [{ hasOwnProperty: void 0 }] + ], + [ + [{}], + [{ hasOwnProperty: null }], + [{ hasOwnProperty: null }] + ], + [ + [{}], + [{ hasOwnProperty: void 0 }], + [{ hasOwnProperty: void 0 }] + ], + [ + [{ + allow: [], + ignoreDestructuring: false, + ignoreGlobals: false, + ignoreImports: false, + properties: "always" + }], + [], + [{ + allow: [], + ignoreDestructuring: false, + ignoreGlobals: false, + ignoreImports: false, + properties: "always" + }] + ] + ]) { + it(`${toTestCaseName(first)}, ${toTestCaseName(second)}`, () => { + assert.deepStrictEqual(deepMergeArrays(first, second), result); + }); + } +}); diff --git a/tests/lib/shared/relative-module-resolver.js b/tests/lib/shared/relative-module-resolver.js index 92eaa6ed..33c18361 100644 --- a/tests/lib/shared/relative-module-resolver.js +++ b/tests/lib/shared/relative-module-resolver.js @@ -3,7 +3,7 @@ */ import { assert } from "chai"; -import path from "path"; +import path from "node:path"; import { Legacy } from "../../../lib/index.js"; diff --git a/tests/lib/types/tsconfig.json b/tests/lib/types/tsconfig.json new file mode 100644 index 00000000..03a89990 --- /dev/null +++ b/tests/lib/types/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "node16", + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true, + "exactOptionalPropertyTypes": true + }, + "files": [ + "../../../lib/types/index.d.ts", + "types.test.mts" + ] +} diff --git a/tests/lib/types/types.test.mts b/tests/lib/types/types.test.mts new file mode 100644 index 00000000..375a1798 --- /dev/null +++ b/tests/lib/types/types.test.mts @@ -0,0 +1,62 @@ +/** + * @fileoverview This file contains tests for types. It was initially extracted + * from the `@types/eslint__eslintrc` package. + */ + +/* + * MIT License + * Copyright (c) Microsoft Corporation. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE + */ + + +import { FlatCompat } from "../../../lib/types/index.js"; +import { Linter } from "eslint"; + +const __dirname = "/path/to/project"; + +const compat = new FlatCompat({ + baseDirectory: __dirname, + resolvePluginsRelativeTo: __dirname, +}); + +const config: Linter.Config[] = [ + ...compat.extends("standard", "example"), + + ...compat.env({ + es2020: true, + node: true, + }), + + ...compat.plugins("airbnb", "react"), + + ...compat.config({ + plugins: ["airbnb", "react"], + extends: "standard", + env: { + es2020: true, + node: true, + }, + rules: { + semi: "error", + }, + }), +]; + +export default config; diff --git a/universal.js b/universal.js index 4e1846ee..2257383e 100644 --- a/universal.js +++ b/universal.js @@ -1,3 +1,5 @@ +/* global module, require -- required for CJS file */ + // Jest (and probably some other runtimes with custom implementations of // `require`) doesn't support `exports` in `package.json`, so this file is here // to help them load this module. Note that it is also `.js` and not `.cjs` for @@ -5,5 +7,4 @@ // since Jest doesn't respect `module` outside of ESM mode it still works in // this case (and the `require` in _this_ file does specify the extension). -// eslint-disable-next-line no-undef module.exports = require("./dist/eslintrc-universal.cjs");