├── .node-version ├── .husky └── pre-commit ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── integration_test ├── vscode.spec.ts ├── eslint-globals.spec.ts ├── nuxt-auth.spec.ts ├── pupeeteer.spec.ts ├── react-project.spec.ts ├── typescript.spec.ts ├── vuejs-core.spec.ts ├── autoprefixer.spec.ts ├── many-extends.spec.ts ├── duplicate-overrides.spec.ts ├── eslint-plugin-oxlint.spec.ts ├── typescript-simple.spec.ts ├── overriding-config-merge.spec.ts ├── projects │ ├── next-eslint-config-project.config.mjs │ ├── react-project.eslint.config.mjs │ ├── typescript-simple.eslint.config.mjs │ ├── eslint-plugin-oxlint.eslint.config.ts │ ├── overriding-config-merge-with-files.config.js │ ├── autoprefixer.eslint.config.mjs │ ├── overriding-config-merge.config.js │ ├── duplicate-overrides-eslint.config.ts │ ├── nuxt-auth.eslint.config.js │ ├── eslint-globals.config.ts │ ├── .eslint-ignore │ ├── many-extends.config.mjs │ ├── vuejs-core.eslint.config.js │ ├── typescript.eslint.config.mjs │ └── puppeteer.eslint.config.mjs ├── overriding-config-merge-with-files.spec.ts ├── __snapshots__ │ ├── overriding-config-merge.spec.ts.snap │ ├── overriding-config-merge-with-files.spec.ts.snap │ ├── duplicate-overrides.spec.ts.snap │ ├── eslint-globals.spec.ts.snap │ └── react-project.spec.ts.snap ├── e2e │ ├── override-merging.spec.ts │ └── js-plugins-with-overrides.spec.ts ├── next-eslint-config-project.spec.ts └── utils.ts ├── .oxfmtrc.jsonc ├── vitest.config.ts ├── tsdown.config.js ├── bin ├── project-loader.ts ├── config-loader.ts └── oxlint-migrate.ts ├── .oxlintrc.json ├── .github ├── actions │ └── pnpm │ │ └── action.yml ├── workflows │ ├── build.yml │ ├── test.yml │ ├── generate.yml │ ├── ci_security.yml │ ├── release.yml │ └── bump_oxlint.yml └── renovate.json ├── src ├── utilities.ts ├── reporter.ts ├── ignorePatterns.ts ├── overrides.ts ├── js_plugin_fixes.spec.ts ├── walker │ ├── index.ts │ ├── comments │ │ ├── index.ts │ │ ├── replaceRuleDirectiveComment.ts │ │ ├── CheatSheet.md │ │ └── replaceRuleDirectiveComment.spec.ts │ ├── replaceCommentsInFile.ts │ ├── partialSourceTextLoader.ts │ ├── partialSourceTextLoader.spec.ts │ └── replaceCommentsInFile.spec.ts ├── types.ts ├── js_plugin_fixes.ts ├── jsPlugins.ts ├── jsPlugins.spec.ts ├── index.ts ├── constants.ts ├── index.spec.ts ├── cleanup.test.ts ├── cleanup.ts ├── env_globals.spec.ts └── env_globals.ts ├── scripts ├── generate.ts ├── generator.ts ├── constants.ts └── traverse-rules.ts ├── tsconfig.json ├── LICENSE ├── .gitignore ├── package.json └── README.md /.node-version: -------------------------------------------------------------------------------- 1 | 22.14.0 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | dist/ 3 | ~ -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "oxc.oxc-vscode", 5 | "vitest.explorer" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /integration_test/vscode.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import vscode_test from './projects/vscode.eslint.config.js'; 3 | import { testProject } from './utils.js'; 4 | 5 | testProject('vscode', vscode_test); 6 | -------------------------------------------------------------------------------- /.oxfmtrc.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/oxfmt/configuration_schema.json", 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "printWidth": 80, 8 | } 9 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | include: ['src', 'scripts'], 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /integration_test/eslint-globals.spec.ts: -------------------------------------------------------------------------------- 1 | import eslint_globals_test from './projects/eslint-globals.config.js'; 2 | import { testProject } from './utils.js'; 3 | 4 | testProject('eslint-globals', eslint_globals_test); 5 | -------------------------------------------------------------------------------- /integration_test/nuxt-auth.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import nuxt_auth_test from './projects/nuxt-auth.eslint.config.js'; 3 | import { testProject } from './utils.js'; 4 | 5 | testProject('nuxt-auth', nuxt_auth_test); 6 | -------------------------------------------------------------------------------- /integration_test/pupeeteer.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import puppeteer_test from './projects/puppeteer.eslint.config.mjs'; 3 | import { testProject } from './utils.js'; 4 | 5 | testProject('puppeteer', puppeteer_test); 6 | -------------------------------------------------------------------------------- /integration_test/react-project.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import react_test from './projects/react-project.eslint.config.mjs'; 3 | import { testProject } from './utils.js'; 4 | 5 | testProject('react-project', react_test); 6 | -------------------------------------------------------------------------------- /integration_test/typescript.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import typescript_test from './projects/typescript.eslint.config.mjs'; 3 | import { testProject } from './utils.js'; 4 | 5 | testProject('typescript', typescript_test); 6 | -------------------------------------------------------------------------------- /integration_test/vuejs-core.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import vuejs_core_test from './projects/vuejs-core.eslint.config.js'; 3 | import { testProject } from './utils.js'; 4 | 5 | testProject('vuejs/core', vuejs_core_test); 6 | -------------------------------------------------------------------------------- /tsdown.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown'; 2 | 3 | export default defineConfig({ 4 | dts: true, 5 | target: 'node18', 6 | entry: ['src/index.ts', 'bin/oxlint-migrate.ts'], 7 | fixedExtension: true, 8 | }); 9 | -------------------------------------------------------------------------------- /integration_test/autoprefixer.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import autoprefixer_test from './projects/autoprefixer.eslint.config.mjs'; 3 | import { testProject } from './utils.js'; 4 | 5 | testProject('autoprefixer', autoprefixer_test); 6 | -------------------------------------------------------------------------------- /integration_test/many-extends.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import many_extends_test from './projects/many-extends.config.mjs'; 3 | import { testProject } from './utils.js'; 4 | 5 | testProject('many-extends.spec.ts', many_extends_test); 6 | -------------------------------------------------------------------------------- /integration_test/duplicate-overrides.spec.ts: -------------------------------------------------------------------------------- 1 | import duplicate_overrides_test from './projects/duplicate-overrides-eslint.config.js'; 2 | import { testProject } from './utils.js'; 3 | 4 | testProject('duplicate-overrides', duplicate_overrides_test); 5 | -------------------------------------------------------------------------------- /integration_test/eslint-plugin-oxlint.spec.ts: -------------------------------------------------------------------------------- 1 | import eslint_plugin_oxlint_test from './projects/eslint-plugin-oxlint.eslint.config.js'; 2 | import { testProject } from './utils.js'; 3 | 4 | testProject('eslint-plugin-oxlint', eslint_plugin_oxlint_test); 5 | -------------------------------------------------------------------------------- /integration_test/typescript-simple.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import typescript_simple_test from './projects/typescript-simple.eslint.config.mjs'; 3 | import { testProject } from './utils.js'; 4 | 5 | testProject('typescript-simple', typescript_simple_test); 6 | -------------------------------------------------------------------------------- /integration_test/overriding-config-merge.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import overriding_config_merge_test from './projects/overriding-config-merge.config.js'; 3 | import { testProject } from './utils.js'; 4 | 5 | testProject('overriding-config-merge', overriding_config_merge_test); 6 | -------------------------------------------------------------------------------- /integration_test/projects/next-eslint-config-project.config.mjs: -------------------------------------------------------------------------------- 1 | import next from 'eslint-config-next'; 2 | 3 | const eslintConfig = [ 4 | ...next, 5 | { 6 | rules: { 7 | 'node/prefer-global/process': ['error', 'always'], 8 | }, 9 | }, 10 | ]; 11 | 12 | export default eslintConfig; 13 | -------------------------------------------------------------------------------- /integration_test/overriding-config-merge-with-files.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-expect-error 2 | import overriding_config_merge_with_files_test from './projects/overriding-config-merge-with-files.config.js'; 3 | import { testProject } from './utils.js'; 4 | 5 | testProject( 6 | 'overriding-config-merge-with-files', 7 | overriding_config_merge_with_files_test 8 | ); 9 | -------------------------------------------------------------------------------- /bin/project-loader.ts: -------------------------------------------------------------------------------- 1 | import { glob } from 'tinyglobby'; 2 | 3 | export const getAllProjectFiles = (): Promise => { 4 | return glob( 5 | [ 6 | '**/*.{js,cjs,mjs,ts,cts,mts,jsx,tsx,vue,astro,svelte}', 7 | '!**/node_modules/**', 8 | '!**/dist/**', 9 | ], 10 | { 11 | absolute: true, 12 | } 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Editor values 3 | "editor.formatOnSave": true, 4 | // Eslint Configuration 5 | "eslint.workingDirectories": ["."], 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": "explicit", 8 | "source.organizeImports": "never" 9 | }, 10 | "oxc.typeAware": true, 11 | "editor.defaultFormatter": "oxc.oxc-vscode" 12 | } 13 | -------------------------------------------------------------------------------- /.oxlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["unicorn", "typescript", "oxc"], 3 | "categories": { 4 | "correctness": "error", 5 | "perf": "error" 6 | }, 7 | "rules": { 8 | "unicorn/prefer-set-has": "off", 9 | "no-unused-expressions": ["error", { "allowShortCircuit": true }], 10 | "eqeqeq": ["error", "always"], 11 | "@typescript-eslint/no-array-delete": "off" // FIXME 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /integration_test/projects/react-project.eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import reactPerfPlugin from 'eslint-plugin-react-perf'; 2 | import reactHooks from 'eslint-plugin-react-hooks'; 3 | import reactPlugin from 'eslint-plugin-react'; 4 | 5 | export default [ 6 | reactPlugin.configs.flat.recommended, 7 | reactPlugin.configs.flat['jsx-runtime'], 8 | reactHooks.configs['recommended-latest'], 9 | reactPerfPlugin.configs.flat.recommended, 10 | ]; 11 | -------------------------------------------------------------------------------- /integration_test/projects/typescript-simple.eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import tseslint from 'typescript-eslint'; 2 | 3 | export default tseslint.config( 4 | { 5 | rules: { 6 | 'no-invalid-regexp': 'error', 7 | }, 8 | }, 9 | { 10 | files: ['**/*.ts', '**/*.tsx'], 11 | 12 | extends: [tseslint.configs.strictTypeChecked], 13 | 14 | rules: { 15 | '@typescript-eslint/no-deprecated': 'error', 16 | }, 17 | } 18 | ); 19 | -------------------------------------------------------------------------------- /.github/actions/pnpm/action.yml: -------------------------------------------------------------------------------- 1 | name: pnpm 2 | 3 | runs: 4 | using: composite 5 | steps: 6 | - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 7 | 8 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 9 | with: 10 | node-version-file: .node-version 11 | registry-url: 'https://registry.npmjs.org' 12 | cache: pnpm 13 | 14 | - run: pnpm install --frozen-lockfile 15 | shell: bash 16 | -------------------------------------------------------------------------------- /src/utilities.ts: -------------------------------------------------------------------------------- 1 | // thanks to https://stackoverflow.com/a/77278013/7387397 2 | export const isEqualDeep = (a: T, b: T): boolean => { 3 | if (a === b) { 4 | return true; 5 | } 6 | 7 | const bothAreObjects = 8 | a && b && typeof a === 'object' && typeof b === 'object'; 9 | 10 | return Boolean( 11 | bothAreObjects && 12 | Object.keys(a).length === Object.keys(b).length && 13 | Object.entries(a).every(([k, v]) => isEqualDeep(v, b[k as keyof T])) 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | push: 7 | branches: 8 | - main 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 18 | with: 19 | persist-credentials: false 20 | 21 | - uses: ./.github/actions/pnpm 22 | 23 | - name: Build 24 | run: pnpm run build 25 | -------------------------------------------------------------------------------- /integration_test/projects/eslint-plugin-oxlint.eslint.config.ts: -------------------------------------------------------------------------------- 1 | import unicorn from 'eslint-plugin-unicorn'; 2 | import eslint from '@eslint/js'; 3 | import eslintConfigPrettier from 'eslint-config-prettier'; 4 | import tseslint from 'typescript-eslint'; 5 | import oxlint from 'eslint-plugin-oxlint'; 6 | 7 | export default [ 8 | { 9 | ignores: ['dist/'], 10 | }, 11 | eslint.configs.recommended, 12 | unicorn.configs['recommended'], 13 | ...tseslint.configs.recommended, 14 | eslintConfigPrettier, 15 | ...oxlint.buildFromOxlintConfig({}), 16 | ]; 17 | -------------------------------------------------------------------------------- /scripts/generate.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import { RulesGenerator } from './generator.js'; 4 | import { traverseRules } from './traverse-rules.js'; 5 | 6 | const result = traverseRules(); 7 | 8 | const __dirname = new URL('.', import.meta.url).pathname; 9 | const generateFolder = path.resolve(__dirname, '..', `src/generated`); 10 | 11 | if (!fs.existsSync(generateFolder)) { 12 | fs.mkdirSync(generateFolder); 13 | } 14 | 15 | const generator = new RulesGenerator(result); 16 | await generator.generateRules(generateFolder); 17 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>Boshen/renovate", 5 | "helpers:pinGitHubActionDigestsToSemver" 6 | ], 7 | "packageRules": [ 8 | { 9 | "groupName": "npm packages", 10 | "matchManagers": ["npm"], 11 | "ignoreDeps": ["oxlint", "oxlint-tsgolint"] 12 | }, 13 | { 14 | "groupName": "oxlint", 15 | "matchManagers": ["npm"], 16 | "matchPackageNames": ["oxlint", "oxlint-tsgolint"], 17 | "schedule": ["at any time"] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /integration_test/projects/overriding-config-merge-with-files.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'eslint/config'; 2 | import regexpPlugin from 'eslint-plugin-regexp'; 3 | 4 | export default defineConfig([ 5 | { 6 | plugins: { regexp: regexpPlugin }, 7 | rules: { 8 | 'regexp/no-lazy-ends': ['error', { ignorePartial: false }], 9 | }, 10 | }, 11 | { 12 | plugins: { regexp: regexpPlugin }, 13 | files: ['**/*.js'], 14 | rules: { 15 | // This should only result in a change for .js files, not *all* files. 16 | 'regexp/no-lazy-ends': ['off'], 17 | }, 18 | }, 19 | ]); 20 | -------------------------------------------------------------------------------- /integration_test/projects/autoprefixer.eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import loguxConfig from '@logux/eslint-config'; 2 | 3 | export default [ 4 | { 5 | ignores: ['coverage'], 6 | }, 7 | ...loguxConfig, 8 | { 9 | rules: { 10 | 'n/prefer-node-protocol': 'off', 11 | 'no-console': 'off', 12 | }, 13 | }, 14 | { 15 | files: ['bin/autoprefixer'], 16 | rules: { 17 | 'n/global-require': 'off', 18 | 'n/no-unsupported-features/es-syntax': 'off', 19 | }, 20 | }, 21 | { 22 | files: ['data/prefixes.js'], 23 | rules: { 24 | 'import/order': 'off', 25 | }, 26 | }, 27 | ]; 28 | -------------------------------------------------------------------------------- /src/reporter.ts: -------------------------------------------------------------------------------- 1 | import { Reporter } from './types.js'; 2 | 3 | export class DefaultReporter implements Reporter { 4 | private reports = new Set(); 5 | 6 | public report(message: string): void { 7 | this.reports.add(message); 8 | } 9 | 10 | public remove(message: string): void { 11 | this.reports.delete(message); 12 | } 13 | 14 | public getReports(): string[] { 15 | return Array.from(this.reports); 16 | } 17 | } 18 | 19 | export class SilentReporter implements Reporter { 20 | public report(_message: string): void { 21 | // Do nothing 22 | } 23 | 24 | public remove(_message: string): void { 25 | // Do nothing 26 | } 27 | 28 | public getReports(): string[] { 29 | return []; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /integration_test/projects/overriding-config-merge.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'eslint/config'; 2 | import regexpPlugin from 'eslint-plugin-regexp'; 3 | 4 | const configs = { 5 | strict: { 6 | name: 'config/strict', 7 | plugins: { regexp: regexpPlugin }, 8 | rules: { 9 | 'regexp/no-lazy-ends': ['error', { ignorePartial: false }], 10 | }, 11 | }, 12 | }; 13 | 14 | export default defineConfig([ 15 | configs.strict, 16 | { 17 | plugins: { regexp: regexpPlugin }, 18 | rules: { 19 | // This should end up overriding the no-lazy-ends from the configs.strict config. 20 | // So it should end up as "off" in the final oxlint config. 21 | 'regexp/no-lazy-ends': 'off', 22 | }, 23 | }, 24 | ]); 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | push: 7 | branches: 8 | - main 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 18 | with: 19 | persist-credentials: false 20 | 21 | - uses: ./.github/actions/pnpm 22 | 23 | - name: Run oxlint 24 | run: npx oxlint --type-aware 25 | 26 | - name: Run Format (oxfmt) 27 | run: npx oxfmt . --check 28 | 29 | - name: Type Check 30 | run: npx tsc --noEmit 31 | 32 | - name: Run tests 33 | run: pnpm run test --coverage 34 | -------------------------------------------------------------------------------- /src/ignorePatterns.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from 'eslint'; 2 | import { Options, OxlintConfigOrOverride } from './types.js'; 3 | 4 | export const transformIgnorePatterns = ( 5 | eslintConfig: Linter.Config, 6 | targetConfig: OxlintConfigOrOverride, 7 | options?: Options 8 | ) => { 9 | if (eslintConfig.ignores === undefined) { 10 | return; 11 | } 12 | 13 | if ('files' in targetConfig) { 14 | options?.reporter?.report('ignore list inside overrides is not supported'); 15 | return; 16 | } 17 | 18 | if (targetConfig.ignorePatterns === undefined) { 19 | targetConfig.ignorePatterns = []; 20 | } 21 | 22 | for (const ignores of eslintConfig.ignores) { 23 | if (!targetConfig.ignorePatterns.includes(ignores)) { 24 | targetConfig.ignorePatterns.push(ignores); 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/overrides.ts: -------------------------------------------------------------------------------- 1 | import { OxlintConfig, OxlintConfigOverride } from './types.js'; 2 | import { isEqualDeep } from './utilities.js'; 3 | 4 | export const detectSameOverride = ( 5 | config: OxlintConfig, 6 | override: OxlintConfigOverride 7 | ): [boolean, OxlintConfigOverride] => { 8 | if (config.overrides === undefined) { 9 | return [true, override]; 10 | } 11 | 12 | // only when override has no categories to avoid merging rules which requires a plugin 13 | // plugins array will be later filled 14 | const matchedOverride = config.overrides.find(({ files, categories }) => { 15 | return categories === undefined && isEqualDeep(files, override.files); 16 | }); 17 | 18 | if (matchedOverride !== undefined) { 19 | return [false, matchedOverride]; 20 | } 21 | 22 | return [true, override]; 23 | }; 24 | -------------------------------------------------------------------------------- /integration_test/projects/duplicate-overrides-eslint.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'eslint/config'; 2 | 3 | export default defineConfig([ 4 | { 5 | files: ['**/*.js', '**/*.cjs', '**/*.mjs'], 6 | rules: { 7 | 'no-unused-vars': 'error', 8 | }, 9 | }, 10 | // These intentionally have identical rulesets to test merging when converting, this can happen in some complex 11 | // configs, but it's easier to understand with an arbitrary example like this. 12 | // As long as the plugins and rules and globals are all the same, these can 13 | // be safely merged by merging the file globs. 14 | { 15 | files: ['**/*.ts'], 16 | rules: { 17 | 'arrow-body-style': 'error', 18 | }, 19 | }, 20 | { 21 | files: ['**/*.mts', '**/*.cts'], 22 | rules: { 23 | 'arrow-body-style': 'error', 24 | }, 25 | }, 26 | ]); 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Visit https://aka.ms/tsconfig to read more about this file */ 3 | "compilerOptions": { 4 | "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 5 | "module": "NodeNext" /* Specify what module code is generated. */, 6 | "moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */, 7 | "resolveJsonModule": true /* Enable importing .json files. */, 8 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 9 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 10 | "strict": true /* Enable all strict type-checking options. */, 11 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 12 | }, 13 | "exclude": ["dist"] 14 | } 15 | -------------------------------------------------------------------------------- /src/js_plugin_fixes.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import fixForJsPlugins from './js_plugin_fixes.js'; 3 | 4 | describe('fixForJsPlugins', () => { 5 | test('should fix @antfu/eslint-config', () => { 6 | class TestConfig extends Promise { 7 | public plugins: string[]; 8 | constructor(plugins: string[]) { 9 | super(() => {}); 10 | this.plugins = plugins; 11 | } 12 | renamePlugins(plugins: Record) { 13 | this.plugins = this.plugins.map((plugin) => plugins[plugin] || plugin); 14 | return this; 15 | } 16 | } 17 | // Create an instance of TestConfig 18 | const config = new TestConfig(['ts', 'next', 'test']); 19 | 20 | void fixForJsPlugins(config); 21 | 22 | expect(config.plugins.length).toBe(3); 23 | expect(config.plugins).toStrictEqual([ 24 | '@typescript-eslint', 25 | '@next/next', 26 | 'vitest', 27 | ]); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /integration_test/projects/nuxt-auth.eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config'; 2 | 3 | const ignores = [ 4 | '.nuxt', 5 | '**/.nuxt/**', 6 | '.output', 7 | '**/.output/**', 8 | 'dist', 9 | '**/dist/**', 10 | 'node_modules', 11 | '**/node_modules/**', 12 | '**/public/**', 13 | ]; 14 | 15 | export default antfu({ 16 | // .eslintignore is no longer supported in Flat config, use ignores instead 17 | ignores, 18 | 19 | // Stylistic formatting rules 20 | stylistic: { 21 | indent: 2, 22 | quotes: 'single', 23 | }, 24 | 25 | // TypeScript and Vue are auto-detected, you can also explicitly enable them 26 | typescript: true, 27 | vue: true, 28 | 29 | // Disable jsonc and yaml support 30 | jsonc: false, 31 | yaml: false, 32 | 33 | // Overwrite certain rules to your preference 34 | rules: { 35 | 'no-console': 'off', 36 | 'style/comma-dangle': 'off', 37 | curly: ['error', 'all'], 38 | 'node/prefer-global/process': ['error', 'always'], 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /src/walker/index.ts: -------------------------------------------------------------------------------- 1 | import { Options } from '../types.js'; 2 | import replaceCommentsInFile from './replaceCommentsInFile.js'; 3 | 4 | export const walkAndReplaceProjectFiles = ( 5 | /** all projects files to check */ 6 | projectFiles: string[], 7 | /** function for reading the file */ 8 | readFileSync: (filePath: string) => string | undefined, 9 | /** function for writing the file */ 10 | writeFile: (filePath: string, content: string) => Promise, 11 | /** options for the walker, for `reporter` and `withNurseryRules` */ 12 | options: Options 13 | ): Promise => { 14 | return Promise.all( 15 | projectFiles.map((file): Promise => { 16 | const sourceText = readFileSync(file); 17 | 18 | if (!sourceText) { 19 | return Promise.resolve(); 20 | } 21 | 22 | const newSourceText = replaceCommentsInFile(file, sourceText, options); 23 | 24 | if (newSourceText === sourceText) { 25 | return Promise.resolve(); 26 | } 27 | 28 | return writeFile(file, newSourceText); 29 | }) 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /.github/workflows/generate.yml: -------------------------------------------------------------------------------- 1 | name: Code generation 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | types: [opened, synchronize] 7 | paths: 8 | - 'pnpm-lock.yaml' 9 | - 'scripts/**' 10 | - '.github/workflows/generate.yml' 11 | push: 12 | branches: 13 | - main 14 | paths: 15 | - 'pnpm-lock.yaml' 16 | - 'scripts/**' 17 | - '.github/workflows/generate.yml' 18 | 19 | permissions: {} 20 | 21 | jobs: 22 | generate: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 27 | with: 28 | persist-credentials: false 29 | 30 | - uses: ./.github/actions/pnpm 31 | 32 | - name: Remove current generated code 33 | run: rm -r ./src/generated/ 34 | 35 | - name: Generate from source code 36 | run: pnpm run generate 37 | 38 | - name: Format generated code 39 | run: pnpm run format 40 | 41 | - name: Check for git diff 42 | run: git diff --exit-code 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025-present VoidZero Inc. & Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /integration_test/projects/eslint-globals.config.ts: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import { defineConfig } from 'eslint/config'; 3 | 4 | export default defineConfig([ 5 | { 6 | files: ['**/*.js', '**/*.cjs', '**/*.mjs'], 7 | languageOptions: { 8 | globals: { 9 | // Exclude the last 10 globals from the browser set 10 | // To ensure we are permissive and still match things, 11 | // even if there are minor differences between the 12 | // latest globals release and the user's globals release. 13 | ...Object.fromEntries(Object.entries(globals.browser).slice(0, -10)), 14 | ...globals.node, 15 | }, 16 | ecmaVersion: 'latest', 17 | sourceType: 'module', 18 | }, 19 | rules: { 20 | 'no-unused-vars': 'error', 21 | }, 22 | }, 23 | { 24 | files: ['**/*.ts'], 25 | languageOptions: { 26 | globals: { 27 | ...globals.browser, 28 | ...globals.worker, 29 | ...Object.fromEntries( 30 | Object.entries(globals.serviceworker).slice(0, -5) 31 | ), 32 | }, 33 | }, 34 | rules: { 35 | 'arrow-body-style': 'error', 36 | }, 37 | }, 38 | ]); 39 | -------------------------------------------------------------------------------- /.github/workflows/ci_security.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Security Analysis 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | types: [opened, synchronize] 7 | paths: 8 | - '.github/workflows/**' 9 | push: 10 | branches: 11 | - main 12 | paths: 13 | - '.github/workflows/**' 14 | 15 | permissions: {} 16 | 17 | jobs: 18 | zizmor: 19 | name: zizmor 20 | runs-on: ubuntu-latest 21 | permissions: 22 | security-events: write 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 26 | with: 27 | persist-credentials: false 28 | 29 | - uses: taiki-e/install-action@5818d9684d2a52207bb7475983a18ba56127e337 # v2.63.2 30 | with: 31 | tool: zizmor 32 | 33 | - name: Run zizmor 34 | run: zizmor --format sarif . > results.sarif 35 | env: 36 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Upload SARIF file 39 | uses: github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 40 | with: 41 | sarif_file: results.sarif 42 | category: zizmor 43 | -------------------------------------------------------------------------------- /scripts/generator.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | import type { Rule } from './traverse-rules.js'; 4 | 5 | export type ResultMap = Map; 6 | 7 | export class RulesGenerator { 8 | private rulesArray: Rule[]; 9 | constructor(rulesArray: Rule[] = []) { 10 | this.rulesArray = rulesArray; 11 | } 12 | 13 | public generateRulesCode() { 14 | console.log(`Generating rules`); 15 | 16 | const rulesArray = Object.groupBy( 17 | this.rulesArray, 18 | ({ category }) => category 19 | ); 20 | let code = 21 | '// These rules are automatically generated by scripts/generator.ts\n\n'; 22 | 23 | for (const [category, rules] of Object.entries(rulesArray)) { 24 | code += `export const ${category}Rules = [\n`; 25 | code += rules! 26 | .map((rule) => { 27 | return ` '${rule.value.replaceAll('_', '-')}'`; 28 | }) 29 | .join(',\n'); 30 | code += '\n]\n\n'; 31 | } 32 | 33 | return code; 34 | } 35 | 36 | public generateRules(folderPath: string): Promise { 37 | const output = this.generateRulesCode(); 38 | 39 | return writeFile(path.resolve(folderPath, `rules.ts`), output); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/walker/comments/index.ts: -------------------------------------------------------------------------------- 1 | import { Options } from '../../types.js'; 2 | import replaceRuleDirectiveComment from './replaceRuleDirectiveComment.js'; 3 | 4 | export default function replaceComments( 5 | comment: string, 6 | type: 'Line' | 'Block', 7 | options: Options 8 | ): string { 9 | const originalComment = comment; 10 | comment = comment.trim(); // trim the end too, so we can check for standalone "eslint" comments 11 | 12 | // "eslint-disable" or "eslint-enable" 13 | if (comment.startsWith('eslint-')) { 14 | return replaceRuleDirectiveComment(originalComment, type, options); 15 | } else if (type === 'Block') { 16 | // these eslint comments are only valid in block comments 17 | 18 | if (comment.startsWith('eslint ')) { 19 | // we do not check for supported rules, we just inform the user about the missing support 20 | throw new Error( 21 | 'changing eslint rules with inline comment is not supported' 22 | ); 23 | } else if (comment.startsWith('global ')) { 24 | // we do not check for supported globals, we just inform the user about the missing support 25 | throw new Error('changing globals with inline comment is not supported'); 26 | } 27 | } 28 | 29 | return originalComment; 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | permissions: {} 8 | 9 | jobs: 10 | release: 11 | if: startsWith(github.event.head_commit.message, 'release:') 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | id-token: write # for `npm publish --provenance` 16 | steps: 17 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 18 | with: 19 | fetch-depth: 0 20 | persist-credentials: true 21 | 22 | - uses: ./.github/actions/pnpm 23 | 24 | - name: Build 25 | run: pnpm run build 26 | 27 | - name: Extract version from commit message 28 | env: 29 | COMMIT_MESSAGE: ${{ github.event.head_commit.message }} 30 | run: | 31 | VERSION=$(echo "${COMMIT_MESSAGE}" | grep -oP 'release: \Kv[0-9]+\.[0-9]+\.[0-9]+') 32 | echo "VERSION=$VERSION" >> $GITHUB_ENV 33 | 34 | - run: npm install -g npm@latest # For trusted publishing support 35 | 36 | - name: Publish to NPM 37 | run: npm publish --tag latest --provenance --access public 38 | 39 | - name: Create and push tag 40 | run: | 41 | git tag ${VERSION} 42 | git push origin ${VERSION} 43 | 44 | - run: npx changelogithub 45 | continue-on-error: true 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from 'eslint'; 2 | 3 | type OxlintConfigPlugins = string[]; 4 | type OxlintConfigJsPlugins = string[]; 5 | type OxlintConfigCategories = Partial>; 6 | type OxlintConfigEnv = Record; 7 | type OxlintConfigIgnorePatterns = string[]; 8 | 9 | export type OxlintConfigOverride = { 10 | files: string[]; 11 | env?: OxlintConfigEnv; 12 | globals?: Linter.Globals; 13 | plugins?: OxlintConfigPlugins; 14 | jsPlugins?: OxlintConfigJsPlugins; 15 | categories?: OxlintConfigCategories; 16 | rules?: Partial; 17 | }; 18 | 19 | export type OxlintConfig = { 20 | $schema?: string; 21 | env?: OxlintConfigEnv; 22 | globals?: Linter.Globals; 23 | plugins?: OxlintConfigPlugins; 24 | jsPlugins?: OxlintConfigJsPlugins; 25 | categories?: OxlintConfigCategories; 26 | rules?: Partial; 27 | overrides?: OxlintConfigOverride[]; 28 | ignorePatterns?: OxlintConfigIgnorePatterns; 29 | }; 30 | 31 | export type OxlintConfigOrOverride = OxlintConfig | OxlintConfigOverride; 32 | 33 | export type Reporter = { 34 | report(message: string): void; 35 | remove(message: string): void; 36 | getReports(): string[]; 37 | }; 38 | 39 | export type Options = { 40 | reporter?: Reporter; 41 | merge?: boolean; 42 | withNursery?: boolean; 43 | typeAware?: boolean; 44 | jsPlugins?: boolean; 45 | }; 46 | 47 | export type Category = 48 | | 'style' 49 | | 'correctness' 50 | | 'nursery' 51 | | 'suspicious' 52 | | 'pedantic' 53 | | 'perf' 54 | | 'restriction'; 55 | -------------------------------------------------------------------------------- /integration_test/projects/.eslint-ignore: -------------------------------------------------------------------------------- 1 | **/build/*/**/*.js 2 | **/dist/**/*.js 3 | **/extensions/**/*.d.ts 4 | **/extensions/**/build/** 5 | **/extensions/**/colorize-fixtures/** 6 | **/extensions/css-language-features/server/test/pathCompletionFixtures/** 7 | **/extensions/html-language-features/server/lib/jquery.d.ts 8 | **/extensions/html-language-features/server/src/test/pathCompletionFixtures/** 9 | **/extensions/ipynb/notebook-out/** 10 | **/extensions/markdown-language-features/media/** 11 | **/extensions/markdown-language-features/notebook-out/** 12 | **/extensions/markdown-math/notebook-out/** 13 | **/extensions/notebook-renderers/renderer-out/index.js 14 | **/extensions/simple-browser/media/index.js 15 | **/extensions/terminal-suggest/src/completions/upstream/** 16 | **/extensions/terminal-suggest/third_party/** 17 | **/extensions/typescript-language-features/test-workspace/** 18 | **/extensions/typescript-language-features/extension.webpack.config.js 19 | **/extensions/typescript-language-features/extension-browser.webpack.config.js 20 | **/extensions/typescript-language-features/package-manager/node-maintainer/** 21 | **/extensions/vscode-api-tests/testWorkspace/** 22 | **/extensions/vscode-api-tests/testWorkspace2/** 23 | **/fixtures/** 24 | **/node_modules/** 25 | **/out-*/**/*.js 26 | **/out-editor-*/** 27 | **/out/**/*.js 28 | **/src/**/dompurify.js 29 | **/src/**/marked.js 30 | **/src/**/semver.js 31 | **/src/typings/**/*.d.ts 32 | **/src/vs/*/**/*.d.ts 33 | **/src/vs/base/test/common/filters.perf.data.js 34 | **/src/vs/loader.js 35 | **/test/unit/assert.js 36 | **/test/automation/out/** 37 | **/typings/** 38 | !.vscode -------------------------------------------------------------------------------- /integration_test/__snapshots__/overriding-config-merge.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`overriding-config-merge > overriding-config-merge 1`] = ` 4 | { 5 | "config": { 6 | "$schema": "./node_modules/oxlint/configuration_schema.json", 7 | "categories": { 8 | "correctness": "off", 9 | }, 10 | "env": { 11 | "builtin": true, 12 | }, 13 | "plugins": [], 14 | }, 15 | "warnings": [], 16 | } 17 | `; 18 | 19 | exports[`overriding-config-merge --js-plugins > overriding-config-merge--js-plugins 1`] = ` 20 | { 21 | "config": { 22 | "$schema": "./node_modules/oxlint/configuration_schema.json", 23 | "categories": { 24 | "correctness": "off", 25 | }, 26 | "env": { 27 | "builtin": true, 28 | }, 29 | "jsPlugins": [ 30 | "eslint-plugin-regexp", 31 | ], 32 | "plugins": [], 33 | }, 34 | "warnings": [], 35 | } 36 | `; 37 | 38 | exports[`overriding-config-merge --type-aware > overriding-config-merge--type-aware 1`] = ` 39 | { 40 | "config": { 41 | "$schema": "./node_modules/oxlint/configuration_schema.json", 42 | "categories": { 43 | "correctness": "off", 44 | }, 45 | "env": { 46 | "builtin": true, 47 | }, 48 | "plugins": [], 49 | }, 50 | "warnings": [], 51 | } 52 | `; 53 | 54 | exports[`overriding-config-merge merge > overriding-config-merge--merge 1`] = ` 55 | { 56 | "config": { 57 | "$schema": "./node_modules/oxlint/configuration_schema.json", 58 | "categories": { 59 | "correctness": "error", 60 | "perf": "error", 61 | }, 62 | "env": { 63 | "builtin": true, 64 | }, 65 | "plugins": [], 66 | }, 67 | "warnings": [], 68 | } 69 | `; 70 | -------------------------------------------------------------------------------- /integration_test/e2e/override-merging.spec.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'eslint/config'; 2 | import { describe, expect, test } from 'vitest'; 3 | import migrateConfig from '../../src/index.js'; 4 | 5 | const eslintConfig = defineConfig([ 6 | { 7 | rules: { 8 | 'no-unused-vars': 'error', 9 | }, 10 | }, 11 | // These intentionally have identical rulesets to test merging when converting, this can happen in some complex 12 | // configs, but it's easier to understand with an arbitrary example like this. 13 | // As long as the plugins and rules and globals are all the same, these can 14 | // be safely merged by merging the file globs. 15 | { 16 | files: ['**/*.ts'], 17 | rules: { 18 | 'arrow-body-style': 'error', 19 | }, 20 | }, 21 | { 22 | files: ['**/*.mts', '**/*.cts'], 23 | rules: { 24 | 'arrow-body-style': 'error', 25 | }, 26 | }, 27 | ]); 28 | 29 | describe('override-merging', () => { 30 | test('should merge identical override rulesets by combining the file globs', async () => { 31 | const oxlintConfig = await migrateConfig(eslintConfig); 32 | 33 | // Should have the base rule from the first override 34 | expect(oxlintConfig.rules?.['no-unused-vars']).toBe('error'); 35 | 36 | // Should have overrides for the remaining patterns 37 | expect(oxlintConfig.overrides).toBeDefined(); 38 | 39 | // The two overrides with identical rules should be merged into one 40 | const arrowBodyStyleOverrides = oxlintConfig.overrides!; 41 | expect(arrowBodyStyleOverrides).toHaveLength(1); 42 | 43 | // The merged override should include all relevant file patterns. 44 | const mergedOverride = arrowBodyStyleOverrides[0]; 45 | expect(mergedOverride.files).toHaveLength(3); 46 | expect(mergedOverride.files).toContain('**/*.ts'); 47 | expect(mergedOverride.files).toContain('**/*.mts'); 48 | expect(mergedOverride.files).toContain('**/*.cts'); 49 | expect(mergedOverride.rules?.['arrow-body-style']).toBe('error'); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /integration_test/next-eslint-config-project.spec.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, expect, test } from 'vitest'; 2 | import { getSnapshotResult, getSnapShotMergeResult } from './utils.js'; 3 | import { preFixForJsPlugins } from '../src/js_plugin_fixes.js'; 4 | 5 | let reset: () => void; 6 | 7 | beforeAll(async () => { 8 | reset = await preFixForJsPlugins(); 9 | }); 10 | 11 | afterAll(() => { 12 | reset(); 13 | }); 14 | 15 | test('next-eslint-config-project', async () => { 16 | const next_config_test = await import( 17 | // @ts-expect-error 18 | './projects/next-eslint-config-project.config.mjs' 19 | ); 20 | const result = await getSnapshotResult(next_config_test.default); 21 | expect(result).toMatchSnapshot('next-eslint-config-project'); 22 | }); 23 | 24 | test('next-eslint-config-project --type-aware', async () => { 25 | const next_config_test = await import( 26 | // @ts-expect-error 27 | './projects/next-eslint-config-project.config.mjs' 28 | ); 29 | const result = await getSnapshotResult(next_config_test.default, undefined, { 30 | typeAware: true, 31 | }); 32 | expect(result).toMatchSnapshot('next-eslint-config-project--type-aware'); 33 | }); 34 | 35 | test('next-eslint-config-project merge', async () => { 36 | const next_config_test = await import( 37 | // @ts-expect-error 38 | './projects/next-eslint-config-project.config.mjs' 39 | ); 40 | const result = await getSnapShotMergeResult(next_config_test.default, { 41 | categories: { 42 | correctness: 'error', 43 | perf: 'error', 44 | }, 45 | }); 46 | expect(result).toMatchSnapshot('next-eslint-config-project--merge'); 47 | }); 48 | 49 | test(`next-eslint-config-project --js-plugins`, async () => { 50 | const next_config_test = await import( 51 | // @ts-expect-error 52 | './projects/next-eslint-config-project.config.mjs' 53 | ); 54 | const result = await getSnapshotResult(next_config_test.default, undefined, { 55 | jsPlugins: true, 56 | }); 57 | expect(result).toMatchSnapshot('next-eslint-config-project--js-plugins'); 58 | }); 59 | -------------------------------------------------------------------------------- /.github/workflows/bump_oxlint.yml: -------------------------------------------------------------------------------- 1 | name: Bump oxlint 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'The version of oxlint to bump to' 8 | required: true 9 | type: string 10 | 11 | env: 12 | OXLINT_PACKAGE_NAME: oxlint 13 | 14 | permissions: {} 15 | 16 | jobs: 17 | bump: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | pull-requests: write 21 | steps: 22 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 23 | with: 24 | persist-credentials: false # should be fine, we give another token for PR creation 25 | 26 | - uses: ./.github/actions/pnpm 27 | 28 | - name: Generate version ${{ inputs.version }} 29 | env: 30 | OXLINT_VERSION: ${{ inputs.version }} 31 | run: | 32 | pnpm install oxlint@${OXLINT_VERSION} 33 | pnpm run generate # Generate rules 34 | pnpm run format # run oxfmt over it 35 | 36 | - name: Test and update snapshot 37 | continue-on-error: true # we check in PR why it fails 38 | run: pnpm run test -u # Update test snapshots 39 | 40 | - name: Bump Version 41 | env: 42 | OXLINT_VERSION: ${{ inputs.version }} 43 | run: npm version ${OXLINT_VERSION} --no-git-tag-version 44 | 45 | - uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 46 | with: 47 | # bot account with PAT required for triggering workflow runs 48 | # See https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs 49 | token: ${{ secrets.OXC_BOT_PAT }} 50 | commit-message: 'release: v${{ inputs.version }}' 51 | committer: Boshen 52 | author: Boshen 53 | branch: release 54 | branch-suffix: timestamp 55 | title: 'release: v${{ inputs.version }}' 56 | assignees: camc314, Sysix 57 | base: main 58 | -------------------------------------------------------------------------------- /src/js_plugin_fixes.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from 'eslint'; 2 | 3 | type PossibleConfigs = 4 | | Linter.Config 5 | | Linter.Config[] 6 | | Promise 7 | | Promise; 8 | 9 | /** 10 | * @link https://github.com/antfu/eslint-config?tab=readme-ov-file#plugins-renaming 11 | */ 12 | const fixForAntfuEslintConfig = (config: T): T => { 13 | if ('renamePlugins' in config && typeof config.renamePlugins === 'function') { 14 | return config.renamePlugins({ 15 | ts: '@typescript-eslint', 16 | test: 'vitest', 17 | next: '@next/next', 18 | style: '@stylistic', 19 | }); 20 | } 21 | 22 | return config; 23 | }; 24 | 25 | /** 26 | * @link https://github.com/oxc-project/oxlint-migrate/issues/160 27 | */ 28 | const fixForNextEslintConfig = async (): Promise<() => void> => { 29 | // this fix can only be done in `Node.js` environment. 30 | // `module` does only exist there. 31 | if ('Deno' in globalThis || 'Bun' in globalThis) { 32 | return () => {}; 33 | } 34 | 35 | type ModuleType = typeof import('module') & { 36 | // `_load` is Node.js's internal module loading function. We access this private API here 37 | // to intercept and mock the loading of '@rushstack/eslint-patch', preventing side effects 38 | // during ESLint config processing. This is necessary because there is no public API for this. 39 | _load: (request: string, ...args: unknown[]) => any; 40 | }; 41 | const Module = await import('module'); 42 | const mod = (Module.default || Module) as ModuleType; 43 | const originalLoad = mod._load; 44 | mod._load = function (request: string, ...args: unknown[]) { 45 | if (request && request.includes('@rushstack/eslint-patch')) { 46 | // Return a harmless mock to avoid side effects 47 | return {}; 48 | } 49 | return originalLoad.apply(mod, [request, ...args]); 50 | }; 51 | 52 | return () => { 53 | mod._load = originalLoad; 54 | }; 55 | }; 56 | 57 | export default function fixForJsPlugins( 58 | configs: PossibleConfigs 59 | ): PossibleConfigs { 60 | return fixForAntfuEslintConfig(configs); 61 | } 62 | 63 | export const preFixForJsPlugins = (): Promise<() => void> => { 64 | return fixForNextEslintConfig(); 65 | }; 66 | -------------------------------------------------------------------------------- /bin/config-loader.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'node:fs'; 2 | import path from 'node:path'; 3 | import { pathToFileURL } from 'node:url'; 4 | 5 | // @link 6 | const FLAT_CONFIG_FILENAMES = [ 7 | 'eslint.config.js', 8 | 'eslint.config.mjs', 9 | 'eslint.config.cjs', 10 | 'eslint.config.ts', 11 | 'eslint.config.mts', 12 | 'eslint.config.cts', 13 | ]; 14 | 15 | export const getAutodetectedEslintConfigName = ( 16 | cwd: string 17 | ): string | undefined => { 18 | for (const filename of FLAT_CONFIG_FILENAMES) { 19 | const filePath = path.join(cwd, filename); 20 | if (existsSync(filePath)) { 21 | return filePath; 22 | } 23 | } 24 | }; 25 | 26 | export const loadESLintConfig = async (filePath: string): Promise => { 27 | // report when json file is found 28 | if (filePath.endsWith('json')) { 29 | throw new Error( 30 | `json format is not supported. @oxlint/migrate only supports the eslint flat configuration` 31 | ); 32 | } 33 | 34 | // windows allows only file:// prefix to be imported, reported Error: 35 | // Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:' 36 | let url = pathToFileURL(filePath).toString(); 37 | 38 | // report when file does not exists 39 | if (!existsSync(filePath)) { 40 | throw new Error(`eslint config file not found: ${filePath}`); 41 | } 42 | 43 | // Bun and Deno supports TS import natively, only Node needs a custom loader 44 | if ('Bun' in globalThis || 'Deno' in globalThis) { 45 | return import(url); 46 | } 47 | 48 | // jiti is used to load TypeScript files in a Node.js environment 49 | if ( 50 | filePath.endsWith('.ts') || 51 | filePath.endsWith('.mts') || 52 | filePath.endsWith('.cts') 53 | ) { 54 | const { createJiti } = await import('jiti'); 55 | const jitiInstance = createJiti(filePath, { 56 | interopDefault: false, 57 | moduleCache: false, 58 | }); 59 | 60 | return jitiInstance.import(url); 61 | } 62 | 63 | // for .js, .mjs, .cjs files we can use the native import 64 | return import(url); 65 | }; 66 | -------------------------------------------------------------------------------- /integration_test/__snapshots__/overriding-config-merge-with-files.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`overriding-config-merge-with-files > overriding-config-merge-with-files 1`] = ` 4 | { 5 | "config": { 6 | "$schema": "./node_modules/oxlint/configuration_schema.json", 7 | "categories": { 8 | "correctness": "off", 9 | }, 10 | "env": { 11 | "builtin": true, 12 | }, 13 | "plugins": [], 14 | }, 15 | "warnings": [ 16 | "unsupported rule: regexp/no-lazy-ends", 17 | ], 18 | } 19 | `; 20 | 21 | exports[`overriding-config-merge-with-files --js-plugins > overriding-config-merge-with-files--js-plugins 1`] = ` 22 | { 23 | "config": { 24 | "$schema": "./node_modules/oxlint/configuration_schema.json", 25 | "categories": { 26 | "correctness": "off", 27 | }, 28 | "env": { 29 | "builtin": true, 30 | }, 31 | "jsPlugins": [ 32 | "eslint-plugin-regexp", 33 | ], 34 | "overrides": [ 35 | { 36 | "files": [ 37 | "**/*.js", 38 | ], 39 | "rules": { 40 | "regexp/no-lazy-ends": [ 41 | "off", 42 | ], 43 | }, 44 | }, 45 | ], 46 | "plugins": [], 47 | "rules": { 48 | "regexp/no-lazy-ends": [ 49 | "error", 50 | { 51 | "ignorePartial": false, 52 | }, 53 | ], 54 | }, 55 | }, 56 | "warnings": [], 57 | } 58 | `; 59 | 60 | exports[`overriding-config-merge-with-files --type-aware > overriding-config-merge-with-files--type-aware 1`] = ` 61 | { 62 | "config": { 63 | "$schema": "./node_modules/oxlint/configuration_schema.json", 64 | "categories": { 65 | "correctness": "off", 66 | }, 67 | "env": { 68 | "builtin": true, 69 | }, 70 | "plugins": [], 71 | }, 72 | "warnings": [ 73 | "unsupported rule: regexp/no-lazy-ends", 74 | ], 75 | } 76 | `; 77 | 78 | exports[`overriding-config-merge-with-files merge > overriding-config-merge-with-files--merge 1`] = ` 79 | { 80 | "config": { 81 | "$schema": "./node_modules/oxlint/configuration_schema.json", 82 | "categories": { 83 | "correctness": "error", 84 | "perf": "error", 85 | }, 86 | "env": { 87 | "builtin": true, 88 | }, 89 | "plugins": [], 90 | }, 91 | "warnings": [ 92 | "unsupported rule: regexp/no-lazy-ends", 93 | ], 94 | } 95 | `; 96 | -------------------------------------------------------------------------------- /integration_test/projects/many-extends.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import js from '@eslint/js'; 4 | import importPlugin from 'eslint-plugin-import'; 5 | import jsdoc from 'eslint-plugin-jsdoc'; 6 | import react from 'eslint-plugin-react'; 7 | import reactHooks from 'eslint-plugin-react-hooks'; 8 | import globals from 'globals'; 9 | import tseslint from 'typescript-eslint'; 10 | 11 | /** @type {import('typescript-eslint').ConfigArray} */ 12 | export const baseConfig = [ 13 | js.configs.recommended, 14 | importPlugin.flatConfigs.recommended, 15 | jsdoc.configs['flat/recommended'], 16 | { 17 | linterOptions: { 18 | reportUnusedDisableDirectives: 'error', 19 | reportUnusedInlineConfigs: 'error', 20 | }, 21 | rules: { 22 | 'no-unused-vars': 'off', 23 | }, 24 | }, 25 | ]; 26 | 27 | export default tseslint.config([ 28 | baseConfig, 29 | react.configs.flat.recommended, 30 | react.configs.flat['jsx-runtime'], 31 | reactHooks.configs.flat.recommended, 32 | importPlugin.flatConfigs.react, 33 | { 34 | rules: { 35 | 'react/button-has-type': 'error', 36 | }, 37 | }, 38 | { 39 | files: ['foo.js'], 40 | 41 | languageOptions: { 42 | globals: { 43 | ...globals.commonjs, 44 | ...globals.node, 45 | }, 46 | }, 47 | 48 | rules: { 49 | 'import/no-commonjs': 'off', 50 | }, 51 | }, 52 | { 53 | files: ['**/*.ts', '**/*.tsx'], 54 | 55 | extends: [ 56 | tseslint.configs.strictTypeChecked, 57 | tseslint.configs.stylisticTypeChecked, 58 | react.configs.flat.recommended, 59 | react.configs.flat['jsx-runtime'], 60 | reactHooks.configs.flat.recommended, 61 | importPlugin.flatConfigs.react, 62 | importPlugin.flatConfigs.typescript, 63 | jsdoc.configs['flat/recommended-typescript'], 64 | ], 65 | 66 | languageOptions: { 67 | parserOptions: { 68 | projectService: true, 69 | }, 70 | }, 71 | 72 | rules: { 73 | 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 74 | 'import/no-default-export': 'warn', 75 | 76 | 'jsdoc/require-jsdoc': 'off', 77 | 'jsdoc/require-param': 'off', 78 | 'jsdoc/require-returns': 'off', 79 | 80 | 'react/prefer-stateless-function': 'warn', 81 | 'react/function-component-definition': [ 82 | 'error', 83 | { 84 | namedComponents: 'arrow-function', 85 | }, 86 | ], 87 | }, 88 | }, 89 | ]); 90 | -------------------------------------------------------------------------------- /src/walker/comments/replaceRuleDirectiveComment.ts: -------------------------------------------------------------------------------- 1 | import * as rules from '../../generated/rules.js'; 2 | import { Options } from '../../types.js'; 3 | 4 | const allRules = Object.values(rules).flat(); 5 | 6 | export default function replaceRuleDirectiveComment( 7 | comment: string, 8 | type: 'Line' | 'Block', 9 | options: Options 10 | ): string { 11 | const originalComment = comment; 12 | // "--" is a separator for describing the directive 13 | comment = comment.split(' -- ')[0].trimStart(); 14 | 15 | // this is not an eslint comment 16 | if (!comment.startsWith('eslint-')) { 17 | return originalComment; 18 | } 19 | 20 | comment = comment.substring(7); // "eslint-" is 7 chars long 21 | 22 | if (comment.startsWith('enable')) { 23 | comment = comment.substring(6); 24 | } else if (comment.startsWith('disable')) { 25 | comment = comment.substring(7); 26 | 27 | if (type === 'Line') { 28 | if (comment.startsWith('-next-line')) { 29 | comment = comment.substring(10); 30 | } else if (comment.startsWith('-line')) { 31 | comment = comment.substring(5); 32 | } 33 | } 34 | } else { 35 | // "eslint-" needs to follow up with "disable" or "enable" 36 | return originalComment; 37 | } 38 | 39 | // next char must be a space 40 | if (!comment.startsWith(' ')) { 41 | return originalComment; 42 | } 43 | 44 | comment = comment.trimStart(); 45 | 46 | if (comment.length === 0) { 47 | return originalComment; 48 | } 49 | 50 | while (comment.length) { 51 | let foundRule = false; 52 | for (const rule of allRules) { 53 | if (comment.startsWith(rule)) { 54 | // skip nursery rules when not enabled 55 | if (!options.withNursery && rules.nurseryRules.includes(rule)) { 56 | continue; 57 | } 58 | foundRule = true; 59 | comment = comment.substring(rule.length).trimStart(); 60 | break; 61 | } 62 | } 63 | 64 | if (!foundRule) { 65 | return originalComment; 66 | } 67 | 68 | // we reached the end of the comment 69 | if (!comment.length) { 70 | break; 71 | } 72 | 73 | // when the comment is not empty, we expect a next rule, separated with a comma 74 | if (!comment.startsWith(', ')) { 75 | return originalComment; 76 | } 77 | 78 | // remove comma and all whitespaces that follows 79 | comment = comment.substring(1).trimStart(); 80 | } 81 | 82 | // only the replace the first entry, spaces can be before the comment 83 | return originalComment.replace(/eslint-/, 'oxlint-'); 84 | } 85 | -------------------------------------------------------------------------------- /integration_test/utils.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | import main from '../src/index.js'; 3 | import { Options, OxlintConfig } from '../src/types.js'; 4 | import { DefaultReporter } from '../src/reporter.js'; 5 | 6 | export const getSnapshotResult = async ( 7 | config: Parameters[0], 8 | oxlintConfig?: OxlintConfig, 9 | options?: Pick 10 | ) => { 11 | const reporter = new DefaultReporter(); 12 | const result = await main(config, oxlintConfig, { 13 | reporter: reporter, 14 | merge: oxlintConfig !== undefined, 15 | ...options, 16 | }); 17 | 18 | return { 19 | config: result, 20 | warnings: reporter 21 | .getReports() 22 | // filter out unsupported rules 23 | .filter((error) => !error.startsWith('unsupported rule: local/')) 24 | // .filter((error) => !error.startsWith('unsupported rule: perfectionist/')) 25 | .filter((error) => !error.startsWith('unsupported rule: toml/')) 26 | .filter((error) => !error.startsWith('unsupported rule: style/')), 27 | }; 28 | }; 29 | 30 | export const getSnapShotMergeResult = async ( 31 | config: Parameters[0], 32 | oxlintConfig: OxlintConfig 33 | ) => { 34 | const result = await getSnapshotResult(config, oxlintConfig); 35 | const mergedResult = structuredClone(result); 36 | const result2 = await getSnapshotResult(config, mergedResult.config); 37 | 38 | expect(result2).toStrictEqual(result); 39 | 40 | return result2; 41 | }; 42 | 43 | export const testProject = ( 44 | project: string, 45 | projectConfig: Parameters[0] 46 | ) => { 47 | test(`${project}`, async () => { 48 | const result = await getSnapshotResult(projectConfig); 49 | expect(result).toMatchSnapshot(project); 50 | }); 51 | 52 | test(`${project} --type-aware`, async () => { 53 | const result = await getSnapshotResult(projectConfig, undefined, { 54 | typeAware: true, 55 | }); 56 | expect(result).toMatchSnapshot(`${project}--type-aware`); 57 | }); 58 | 59 | test(`${project} merge`, async () => { 60 | const result = await getSnapShotMergeResult(projectConfig, { 61 | categories: { 62 | correctness: 'error', 63 | perf: 'error', 64 | }, 65 | }); 66 | expect(result).toMatchSnapshot(`${project}--merge`); 67 | }); 68 | 69 | test(`${project} --js-plugins`, async () => { 70 | const result = await getSnapshotResult(projectConfig, undefined, { 71 | jsPlugins: true, 72 | }); 73 | expect(result).toMatchSnapshot(`${project}--js-plugins`); 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # Jetbrains 126 | .idea 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | -------------------------------------------------------------------------------- /src/walker/replaceCommentsInFile.ts: -------------------------------------------------------------------------------- 1 | import { type Comment, parseSync } from 'oxc-parser'; 2 | import { Options } from '../types.js'; 3 | import replaceComments from './comments/index.js'; 4 | import partialSourceTextLoader, { 5 | PartialSourceText, 6 | } from './partialSourceTextLoader.js'; 7 | 8 | const getComments = ( 9 | absoluteFilePath: string, 10 | partialSourceText: PartialSourceText, 11 | options: Pick 12 | ): Comment[] => { 13 | const parserResult = parseSync( 14 | absoluteFilePath, 15 | partialSourceText.sourceText, 16 | { 17 | lang: partialSourceText.lang, 18 | sourceType: partialSourceText.sourceType, 19 | } 20 | ); 21 | 22 | if (parserResult.errors.length > 0) { 23 | options.reporter?.report(`${absoluteFilePath}: failed to parse`); 24 | } 25 | 26 | return parserResult.comments; 27 | }; 28 | 29 | function replaceCommentsInSourceText( 30 | absoluteFilePath: string, 31 | partialSourceText: PartialSourceText, 32 | options: Options 33 | ): string { 34 | const comments = getComments(absoluteFilePath, partialSourceText, options); 35 | let sourceText = partialSourceText.sourceText; 36 | 37 | for (const comment of comments) { 38 | try { 39 | const replacedStr = replaceComments(comment.value, comment.type, options); 40 | // we got a new string, replace it in the source code 41 | if (replacedStr !== comment.value) { 42 | // we know that the length of the comment will not change, 43 | // no need to sort them in reversed order to avoid shifting offsets. 44 | const newComment = 45 | comment.type === 'Line' ? `//${replacedStr}` : `/*${replacedStr}*/`; 46 | sourceText = 47 | sourceText.slice(0, comment.start) + 48 | newComment + 49 | sourceText.slice(comment.end); 50 | } 51 | } catch (error: unknown) { 52 | if (error instanceof Error) { 53 | options.reporter?.report( 54 | `${absoluteFilePath}, char offset ${comment.start + partialSourceText.offset}: ${error.message}` 55 | ); 56 | continue; 57 | } 58 | throw error; 59 | } 60 | } 61 | 62 | return sourceText; 63 | } 64 | 65 | export default function replaceCommentsInFile( 66 | absoluteFilePath: string, 67 | fileContent: string, 68 | options: Options 69 | ): string { 70 | for (const partialSourceText of partialSourceTextLoader( 71 | absoluteFilePath, 72 | fileContent 73 | )) { 74 | const newSourceText = replaceCommentsInSourceText( 75 | absoluteFilePath, 76 | partialSourceText, 77 | options 78 | ); 79 | 80 | if (newSourceText !== partialSourceText.sourceText) { 81 | fileContent = 82 | fileContent.slice(0, partialSourceText.offset) + 83 | newSourceText + 84 | fileContent.slice( 85 | partialSourceText.offset + partialSourceText.sourceText.length 86 | ); 87 | } 88 | } 89 | 90 | return fileContent; 91 | } 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@oxlint/migrate", 3 | "version": "1.34.0", 4 | "description": "Generates a `.oxlintrc.json` from a existing eslint flat config", 5 | "keywords": [ 6 | "eslint", 7 | "oxc", 8 | "oxlint", 9 | "rules" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/oxc-project/oxlint-migrate" 14 | }, 15 | "author": "Sysix ", 16 | "license": "MIT", 17 | "type": "module", 18 | "exports": { 19 | "types": "./dist/src/index.d.mts", 20 | "import": "./dist/src/index.mjs" 21 | }, 22 | "bin": { 23 | "@oxlint/migrate": "./dist/bin/oxlint-migrate.mjs" 24 | }, 25 | "files": [ 26 | "README.md", 27 | "dist/*" 28 | ], 29 | "scripts": { 30 | "prepare": "husky", 31 | "generate": "node --import @oxc-node/core/register ./scripts/generate.ts", 32 | "format": "oxfmt", 33 | "type-check": "tsc --noEmit", 34 | "lint": "oxlint --type-aware", 35 | "test": "vitest", 36 | "build": "tsdown", 37 | "manual-test": "pnpm build; chmod +x dist/bin/oxlint-migrate.mjs; npx ." 38 | }, 39 | "lint-staged": { 40 | "*": "oxfmt --no-error-on-unmatched-pattern" 41 | }, 42 | "dependencies": { 43 | "commander": "^14.0.0", 44 | "globals": "^16.3.0", 45 | "oxc-parser": "^0.103.0", 46 | "tinyglobby": "^0.2.14" 47 | }, 48 | "devDependencies": { 49 | "@antfu/eslint-config": "^6.0.0", 50 | "@eslint/js": "^9.29.0", 51 | "@logux/eslint-config": "^57.0.0", 52 | "@oxc-node/core": "^0.0.35", 53 | "@stylistic/eslint-plugin": "^5.0.0", 54 | "@stylistic/eslint-plugin-ts": "^4.4.1", 55 | "@types/eslint-config-prettier": "^6.11.3", 56 | "@types/node": "^25.0.0", 57 | "@typescript-eslint/eslint-plugin": "^8.35.0", 58 | "@typescript-eslint/parser": "^8.35.0", 59 | "@vitest/coverage-v8": "^4.0.0", 60 | "eslint": "^9.29.0", 61 | "eslint-config-next": "^16.0.0", 62 | "eslint-config-prettier": "^10.1.5", 63 | "eslint-plugin-header": "^3.1.1", 64 | "eslint-plugin-import": "^2.32.0", 65 | "eslint-plugin-import-x": "^4.16.0", 66 | "eslint-plugin-jsdoc": "^61.0.0", 67 | "eslint-plugin-local": "^6.0.0", 68 | "eslint-plugin-mocha": "^11.2.0", 69 | "eslint-plugin-oxlint": "^1.3.0", 70 | "eslint-plugin-prettier": "^5.5.4", 71 | "eslint-plugin-react": "^7.37.5", 72 | "eslint-plugin-react-hooks": "^7.0.1", 73 | "eslint-plugin-react-perf": "^3.3.3", 74 | "eslint-plugin-regexp": "^2.9.0", 75 | "eslint-plugin-tsdoc": "^0.5.0", 76 | "eslint-plugin-unicorn": "^62.0.0", 77 | "husky": "^9.1.7", 78 | "jiti": "^2.4.2", 79 | "lint-staged": "^16.1.2", 80 | "next": "^16.0.0", 81 | "oxfmt": "^0.18.0", 82 | "oxlint": "^1.34.0", 83 | "oxlint-tsgolint": "^0.8.3", 84 | "tsdown": "^0.18.0", 85 | "typescript": "^5.8.3", 86 | "typescript-eslint": "^8.35.0", 87 | "vitest": "^4.0.0" 88 | }, 89 | "peerDependencies": { 90 | "jiti": "*" 91 | }, 92 | "packageManager": "pnpm@10.25.0" 93 | } 94 | -------------------------------------------------------------------------------- /src/jsPlugins.ts: -------------------------------------------------------------------------------- 1 | import { Linter } from 'eslint'; 2 | import { rulesPrefixesForPlugins } from './constants.js'; 3 | import { OxlintConfigOrOverride } from './types.js'; 4 | 5 | const ignorePlugins = new Set([ 6 | ...Object.keys(rulesPrefixesForPlugins), 7 | ...Object.values(rulesPrefixesForPlugins), 8 | 'local', // ToDo: handle local plugin rules 9 | ]); 10 | 11 | const guessEslintPluginName = (pluginName: string): string => { 12 | if (pluginName.startsWith('@')) { 13 | // Scoped plugin. If it contains a sub-id (e.g. @scope/id), map to @scope/eslint-plugin-id 14 | const [scope, maybeSub] = pluginName.split('/'); 15 | if (maybeSub) { 16 | return `${scope}/eslint-plugin-${maybeSub}`; 17 | } 18 | // Plain scoped plugin (e.g. @stylistic) 19 | return `${scope}/eslint-plugin`; 20 | } 21 | return `eslint-plugin-${pluginName}`; 22 | }; 23 | 24 | const extractPluginId = (ruleId: string): string | undefined => { 25 | // ESLint rule ids are either "core" (no slash) or "/". 26 | // For scoped plugin ids, the plugin id can itself contain a slash, e.g. 27 | // @stylistic/ts/member-delimiter-style -> pluginId = @stylistic/ts 28 | // @eslint-community/eslint-comments/disable-enable-pair -> pluginId = @eslint-community/eslint-comments 29 | const firstSlash = ruleId.indexOf('/'); 30 | if (firstSlash === -1) { 31 | return; 32 | } 33 | 34 | if (ruleId.startsWith('@')) { 35 | // Find the second slash which separates pluginId and rule name 36 | const secondSlash = ruleId.indexOf('/', firstSlash + 1); 37 | if (secondSlash !== -1) { 38 | return ruleId.substring(0, secondSlash); 39 | } 40 | } 41 | 42 | // Unscoped plugin: pluginId is before the first slash 43 | return ruleId.substring(0, firstSlash); 44 | }; 45 | 46 | export const isIgnoredPluginRule = (ruleId: string): boolean => { 47 | const pluginName = extractPluginId(ruleId); 48 | // Return true because the rule comes from core ESLint, and so 49 | // should not be considered a plugin rule. 50 | if (pluginName === undefined) { 51 | return true; 52 | } 53 | return ignorePlugins.has(pluginName); 54 | }; 55 | 56 | // Enables the given rule in the target configuration, ensuring that the 57 | // corresponding ESLint plugin is included in the `jsPlugins` array. 58 | // 59 | // This will add the jsPlugin if it is not already present. 60 | export const enableJsPluginRule = ( 61 | targetConfig: OxlintConfigOrOverride, 62 | rule: string, 63 | ruleEntry: Linter.RuleEntry | undefined 64 | ): boolean => { 65 | const pluginName = extractPluginId(rule); 66 | 67 | if (pluginName === undefined) { 68 | return false; 69 | } 70 | 71 | if (ignorePlugins.has(pluginName)) { 72 | return false; 73 | } 74 | if (targetConfig.jsPlugins === undefined) { 75 | targetConfig.jsPlugins = []; 76 | } 77 | 78 | const eslintPluginName = guessEslintPluginName(pluginName); 79 | 80 | if (!targetConfig.jsPlugins.includes(eslintPluginName)) { 81 | targetConfig.jsPlugins.push(eslintPluginName); 82 | } 83 | 84 | targetConfig.rules = targetConfig.rules || {}; 85 | targetConfig.rules[rule] = ruleEntry; 86 | return true; 87 | }; 88 | -------------------------------------------------------------------------------- /integration_test/e2e/js-plugins-with-overrides.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import migrateConfig from '../../src/index.js'; 3 | 4 | import { defineConfig } from 'eslint/config'; 5 | import regexpPlugin from 'eslint-plugin-regexp'; 6 | 7 | // ESLint config to test that error rules with overrides to turn them off are migrated correctly. 8 | const firstEslintConfig = defineConfig([ 9 | { 10 | plugins: { regexp: regexpPlugin }, 11 | rules: { 12 | 'regexp/no-lazy-ends': ['error', { ignorePartial: false }], 13 | }, 14 | }, 15 | { 16 | plugins: { regexp: regexpPlugin }, 17 | files: ['**/*.js'], 18 | rules: { 19 | 'regexp/no-lazy-ends': 'off', 20 | }, 21 | }, 22 | ]); 23 | 24 | // ESLint config to test that disabled rules with overrides to turn them to error are migrated correctly. 25 | const secondEslintConfig = defineConfig([ 26 | { 27 | plugins: { regexp: regexpPlugin }, 28 | rules: { 29 | 'regexp/no-lazy-ends': 'off', 30 | }, 31 | }, 32 | { 33 | plugins: { regexp: regexpPlugin }, 34 | files: ['**/*.js'], 35 | rules: { 36 | 'regexp/no-lazy-ends': ['error', { ignorePartial: false }], 37 | }, 38 | }, 39 | ]); 40 | 41 | describe('JS Plugins with overrides', () => { 42 | test('should migrate first config correctly', async () => { 43 | const oxlintConfig = await migrateConfig(firstEslintConfig, undefined, { 44 | jsPlugins: true, 45 | }); 46 | 47 | // Should have the first rule set to error. 48 | expect(oxlintConfig.rules?.['regexp/no-lazy-ends']).toStrictEqual([ 49 | 'error', 50 | { ignorePartial: false }, 51 | ]); 52 | expect(oxlintConfig.overrides).toBeDefined(); 53 | expect(oxlintConfig.jsPlugins).toStrictEqual(['eslint-plugin-regexp']); 54 | 55 | // The override should still be present, should only have one. 56 | const overrides = oxlintConfig.overrides!; 57 | expect(overrides).toHaveLength(1); 58 | // jsPlugins is unnecessary in the override since it's already set in the base config. 59 | expect(overrides[0].jsPlugins).toBeUndefined(); 60 | 61 | // Should be set to off in the override. 62 | expect(overrides[0].rules?.['regexp/no-lazy-ends']).toBe('off'); 63 | }); 64 | 65 | test('should migrate second config correctly', async () => { 66 | const oxlintConfig = await migrateConfig(secondEslintConfig, undefined, { 67 | jsPlugins: true, 68 | }); 69 | 70 | // Should have the first rule unset. It was set to `off` in the base config, and could be safely removed. 71 | expect(oxlintConfig.rules?.['regexp/no-lazy-ends']).toBeUndefined(); 72 | expect(oxlintConfig.overrides).toBeDefined(); 73 | expect(oxlintConfig.jsPlugins).toBeUndefined(); 74 | 75 | // The override should still be present, should only have one. 76 | const overrides = oxlintConfig.overrides!; 77 | expect(overrides).toHaveLength(1); 78 | expect(overrides[0].jsPlugins).toStrictEqual(['eslint-plugin-regexp']); 79 | 80 | // Should be set to error in the override. 81 | expect(overrides[0].rules?.['regexp/no-lazy-ends']).toStrictEqual([ 82 | 'error', 83 | { ignorePartial: false }, 84 | ]); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /scripts/constants.ts: -------------------------------------------------------------------------------- 1 | // these are the mappings from the scope in the rules.rs to the eslint scope 2 | // only used for the scopes where the directory structure doesn't reflect the eslint scope 3 | // such as `typescript` vs `@typescript-eslint` or others. Eslint as a scope is an exception, 4 | // as eslint doesn't have a scope. 5 | // Basically the reverse of this: 6 | export const aliasPluginNames: Record = { 7 | // for scripts/generate and src/build-from-oxlint-config 8 | eslint: '', 9 | typescript: '@typescript-eslint', 10 | nextjs: '@next/next', 11 | 12 | // only for src/build-from-oxlint-config 13 | react_perf: 'react-perf', 14 | jsx_a11y: 'jsx-a11y', 15 | }; 16 | 17 | // Some vitest rules are re-implemented version of jest rules. 18 | // Since oxlint supports these rules under jest/*, we need to remap them. 19 | // remapping in source-code: 20 | export const viteTestCompatibleRules = [ 21 | 'consistent-test-it', 22 | 'expect-expect', 23 | 'max-expects', 24 | 'max-nested-describe', 25 | 'no-alias-methods', 26 | 'no-commented-out-tests', 27 | 'no-conditional-expect', 28 | 'no-conditional-in-test', 29 | 'no-disabled-tests', 30 | 'no-duplicate-hooks', 31 | 'no-focused-tests', 32 | 'no-hooks', 33 | 'no-identical-title', 34 | 'no-interpolation-in-snapshots', 35 | 'no-large-snapshots', 36 | 'no-mocks-import', 37 | 'no-restricted-jest-methods', 38 | 'no-restricted-matchers', 39 | 'no-standalone-expect', 40 | 'no-test-prefixes', 41 | 'no-test-return-statement', 42 | 'prefer-called-with', 43 | 'prefer-comparison-matcher', 44 | 'prefer-each', 45 | 'prefer-equality-matcher', 46 | 'prefer-expect-resolves', 47 | 'require-hook', 48 | 'prefer-hooks-in-order', 49 | 'prefer-hooks-on-top', 50 | 'prefer-lowercase-title', 51 | 'prefer-mock-promise-shorthand', 52 | 'prefer-spy-on', 53 | 'prefer-strict-equal', 54 | 'prefer-to-be', 55 | 'prefer-to-contain', 56 | 'prefer-to-have-length', 57 | 'prefer-todo', 58 | 'require-to-throw-message', 59 | 'require-top-level-describe', 60 | 'valid-describe-callback', 61 | 'valid-expect', 62 | ]; 63 | 64 | export const unicornRulesExtendEslintRules = ['no-negated-condition']; 65 | 66 | // All rules from `eslint-plugin-react-hooks` 67 | // Since oxlint supports these rules under react/*, we need to remap them. 68 | // See https://react.dev/reference/eslint-plugin-react-hooks#recommended for the full list 69 | // (React Compiler-related rules are sourced in an odd way, so there's no good source code location to find them all) 70 | export const reactHookRulesInsideReactScope = [ 71 | 'rules-of-hooks', 72 | 'exhaustive-deps', 73 | 74 | // Compiler-related rules 75 | 'component-hook-factories', 76 | 'config', 77 | 'error-boundaries', 78 | 'gating', 79 | 'globals', 80 | 'immutability', 81 | 'incompatible-library', 82 | 'preserve-manual-memoization', 83 | 'purity', 84 | 'refs', 85 | 'set-state-in-effect', 86 | 'set-state-in-render', 87 | 'static-components', 88 | 'unsupported-syntax', 89 | 'use-memo', 90 | ]; 91 | -------------------------------------------------------------------------------- /scripts/traverse-rules.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process'; 2 | import { 3 | aliasPluginNames, 4 | reactHookRulesInsideReactScope, 5 | unicornRulesExtendEslintRules, 6 | viteTestCompatibleRules, 7 | } from './constants.js'; 8 | import { typescriptRulesExtendEslintRules } from '../src/constants.js'; 9 | 10 | export type Rule = { 11 | value: string; 12 | scope: string; 13 | category: string; 14 | }; 15 | 16 | /** 17 | * Read the rules from oxlint command and returns an array of Rule-Objects 18 | */ 19 | function readRulesFromCommand(): Rule[] { 20 | // do not handle the exception 21 | const oxlintOutput = execSync(`npx oxlint --rules --format=json`, { 22 | encoding: 'utf8', 23 | stdio: 'pipe', 24 | }); 25 | 26 | // do not handle the exception 27 | return JSON.parse(oxlintOutput); 28 | } 29 | 30 | /** 31 | * Some rules are in a different scope than in eslint 32 | */ 33 | function fixScopeOfRule(rule: Rule): void { 34 | if ( 35 | rule.scope === 'react' && 36 | reactHookRulesInsideReactScope.includes(rule.value) 37 | ) { 38 | rule.scope = 'react_hooks'; 39 | } 40 | } 41 | 42 | /** 43 | * oxlint returns the value without a scope name 44 | */ 45 | function fixValueOfRule(rule: Rule): void { 46 | if (rule.scope === 'eslint') { 47 | return; 48 | } 49 | 50 | const scope = 51 | rule.scope in aliasPluginNames ? aliasPluginNames[rule.scope] : rule.scope; 52 | 53 | rule.value = `${scope}/${rule.value}`; 54 | } 55 | 56 | /** 57 | * some rules are reimplemented in another scope 58 | * remap them so we can disable all the reimplemented too 59 | */ 60 | function getAliasRules(rule: Rule): Rule | undefined { 61 | if ( 62 | rule.scope === 'eslint' && 63 | typescriptRulesExtendEslintRules.includes(rule.value) 64 | ) { 65 | return { 66 | value: `@typescript-eslint/${rule.value}`, 67 | scope: 'typescript', 68 | category: rule.category, 69 | }; 70 | } 71 | 72 | if (rule.scope === 'jest' && viteTestCompatibleRules.includes(rule.value)) { 73 | return { 74 | value: `vitest/${rule.value}`, 75 | scope: 'vitest', 76 | category: rule.category, 77 | }; 78 | } 79 | 80 | if (rule.scope === 'import') { 81 | return { 82 | value: `import-x/${rule.value}`, 83 | scope: 'import-x', 84 | category: rule.category, 85 | }; 86 | } 87 | 88 | if ( 89 | rule.scope === 'eslint' && 90 | unicornRulesExtendEslintRules.includes(rule.value) 91 | ) { 92 | return { 93 | value: `unicorn/${rule.value}`, 94 | scope: 'unicorn', 95 | category: rule.category, 96 | }; 97 | } 98 | } 99 | 100 | export function traverseRules(): Rule[] { 101 | // get all rules and filter the ignored one 102 | const rules = readRulesFromCommand().filter( 103 | (rule) => !['oxc'].includes(rule.scope) 104 | ); 105 | 106 | const aliasRules: Rule[] = []; 107 | 108 | for (const rule of rules) { 109 | const aliasRule = getAliasRules(rule); 110 | if (aliasRule) { 111 | aliasRules.push(aliasRule); 112 | } 113 | 114 | fixScopeOfRule(rule); 115 | fixValueOfRule(rule); 116 | } 117 | 118 | return [...rules, ...aliasRules]; 119 | } 120 | -------------------------------------------------------------------------------- /src/jsPlugins.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { enableJsPluginRule, isIgnoredPluginRule } from './jsPlugins.js'; 3 | import { OxlintConfigOrOverride } from './types.js'; 4 | 5 | describe('enableJsPluginRule', () => { 6 | const rules = [ 7 | { 8 | eslintRule: '@stylistic/indent', 9 | plugin: '@stylistic/eslint-plugin', 10 | oxlintRule: '@stylistic/indent', 11 | }, 12 | { 13 | eslintRule: '@stylistic/ts/member-delimiter-style', 14 | plugin: '@stylistic/eslint-plugin-ts', 15 | oxlintRule: '@stylistic/ts/member-delimiter-style', 16 | }, 17 | { 18 | eslintRule: 'tsdoc/syntax', 19 | plugin: 'eslint-plugin-tsdoc', 20 | oxlintRule: 'tsdoc/syntax', 21 | }, 22 | { 23 | eslintRule: 'mocha/no-pending-tests', 24 | plugin: 'eslint-plugin-mocha', 25 | oxlintRule: 'mocha/no-pending-tests', 26 | }, 27 | { 28 | eslintRule: 'perfectionist/sort-exports', 29 | plugin: 'eslint-plugin-perfectionist', 30 | oxlintRule: 'perfectionist/sort-exports', 31 | }, 32 | { 33 | eslintRule: '@eslint-community/eslint-comments/disable-enable-pair', 34 | plugin: '@eslint-community/eslint-plugin-eslint-comments', 35 | oxlintRule: '@eslint-community/eslint-comments/disable-enable-pair', 36 | }, 37 | ]; 38 | 39 | for (const { eslintRule, plugin, oxlintRule } of rules) { 40 | test(`should enable js plugin ${plugin} rule for ${eslintRule}`, () => { 41 | const targetConfig: OxlintConfigOrOverride = {}; 42 | 43 | const result = enableJsPluginRule(targetConfig, eslintRule, 'error'); 44 | 45 | expect(result).toBe(true); 46 | expect(targetConfig.jsPlugins).toContain(plugin); 47 | expect(targetConfig.rules?.[oxlintRule]).toBe('error'); 48 | }); 49 | } 50 | 51 | test('should return false for ignored plugins', () => { 52 | const targetConfig: OxlintConfigOrOverride = {}; 53 | const result = enableJsPluginRule( 54 | targetConfig, 55 | '@typescript-eslint/no-unused-vars', 56 | 'warn' 57 | ); 58 | 59 | expect(result).toBe(false); 60 | expect(targetConfig.jsPlugins).toBeUndefined(); 61 | expect(targetConfig.rules).toBeUndefined(); 62 | }); 63 | }); 64 | 65 | describe('isIgnoredPluginRule', () => { 66 | test('returns true for core ESLint rule (no plugin)', () => { 67 | expect(isIgnoredPluginRule('no-unused-vars')).toBe(true); 68 | expect(isIgnoredPluginRule('eqeqeq')).toBe(true); 69 | }); 70 | 71 | test('returns true for ignored plugin rules', () => { 72 | // @typescript-eslint is treated as ignored 73 | expect(isIgnoredPluginRule('@typescript-eslint/no-unused-vars')).toBe(true); 74 | // local plugin rules should be ignored (TODO: implement proper handling later) 75 | expect(isIgnoredPluginRule('local/some-rule')).toBe(true); 76 | }); 77 | 78 | test('returns false for non-ignored plugin rules', () => { 79 | expect(isIgnoredPluginRule('mocha/no-pending-tests')).toBe(false); 80 | expect(isIgnoredPluginRule('tsdoc/syntax')).toBe(false); 81 | expect( 82 | isIgnoredPluginRule( 83 | '@eslint-community/eslint-comments/disable-enable-pair' 84 | ) 85 | ).toBe(false); 86 | expect(isIgnoredPluginRule('@stylistic/indent')).toBe(false); 87 | expect(isIgnoredPluginRule('@stylistic/ts/member-delimiter-style')).toBe( 88 | false 89 | ); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @oxlint/migrate 2 | 3 | ![test](https://github.com/oxc-project/oxlint-migrate/actions/workflows/test.yml/badge.svg) 4 | [![NPM Version](https://img.shields.io/npm/v/%40oxlint%2Fmigrate)](https://www.npmjs.com/package/@oxlint/migrate) 5 | [![NPM Downloads](https://img.shields.io/npm/dm/%40oxlint%2Fmigrate)](https://www.npmjs.com/package/@oxlint/migrate) 6 | 7 | Generates a `.oxlintrc.json` from an existing eslint flat config. 8 | 9 | ## Usage 10 | 11 | ```shell 12 | npx @oxlint/migrate 13 | ``` 14 | 15 | When no config file provided, the script searches for the default eslint config filenames in the current directory. 16 | 17 | ### Options 18 | 19 | | Options | Description | 20 | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | 21 | | `--merge` | \* merge eslint configuration with an existing .oxlintrc.json configuration | 22 | | `--type-aware` | Include type aware rules, which are supported with `oxlint --type-aware` | 23 | | `--with-nursery` | Include oxlint rules which are currently under development | 24 | | `--js-plugins` | \*\* Include ESLint plugins via `jsPlugins` key. | 25 | | `--output-file ` | The oxlint configuration file where to eslint v9 rules will be written to, default: `.oxlintrc.json` | 26 | | `--replace-eslint-comments` | Search in the project files for eslint comments and replaces them with oxlint. Some eslint comments are not supported and will be reported. | 27 | 28 | \* WARNING: When some `categories` are enabled, this tools will enable more rules with the combination of `plugins`. 29 | Else we need to disable each rule `plugin/categories` combination, which is not covered by your eslint configuration. 30 | This behavior can change in the future. 31 | 32 | \*\* WARNING: Tries to guess the plugin name. Should work with most of the plugin names. 33 | Not every ESLint API is integrated with `oxlint`. 34 | Tested ESLint Plugins with `oxlint` can be found in this [Oxc Discussion](https://github.com/oxc-project/oxc/discussions/14862). 35 | 36 | ### User Flow 37 | 38 | - Upgrade `oxlint` and `@oxlint/migrate` to the same version. 39 | - Execute `npx @oxlint/migrate` 40 | - (Optional): Disable supported rules via [eslint-plugin-oxlint](https://github.com/oxc-project/eslint-plugin-oxlint) 41 | 42 | ### TypeScript ESLint Configuration Files 43 | 44 | For Deno and Bun, TypeScript configuration files, like `eslint.config.mts`, are natively supported. 45 | For Node.js, you must install [jiti](https://www.npmjs.com/package/jiti) as a dev dependency. 46 | 47 | ## Contributing 48 | 49 | ### Generate rules 50 | 51 | Generates the rules from installed oxlint version 52 | 53 | ```shell 54 | pnpm generate 55 | pnpm format 56 | ``` 57 | 58 | ### Unit + Integration Test 59 | 60 | ```shell 61 | pnpm vitest 62 | ``` 63 | 64 | ### Manual Testing 65 | 66 | ```shell 67 | pnpm manual-test 68 | ``` 69 | -------------------------------------------------------------------------------- /integration_test/__snapshots__/duplicate-overrides.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`duplicate-overrides > duplicate-overrides 1`] = ` 4 | { 5 | "config": { 6 | "$schema": "./node_modules/oxlint/configuration_schema.json", 7 | "categories": { 8 | "correctness": "off", 9 | }, 10 | "env": { 11 | "builtin": true, 12 | }, 13 | "overrides": [ 14 | { 15 | "files": [ 16 | "**/*.js", 17 | "**/*.cjs", 18 | "**/*.mjs", 19 | ], 20 | "rules": { 21 | "no-unused-vars": "error", 22 | }, 23 | }, 24 | { 25 | "files": [ 26 | "**/*.ts", 27 | "**/*.mts", 28 | "**/*.cts", 29 | ], 30 | "rules": { 31 | "arrow-body-style": "error", 32 | }, 33 | }, 34 | ], 35 | "plugins": [], 36 | }, 37 | "warnings": [], 38 | } 39 | `; 40 | 41 | exports[`duplicate-overrides --js-plugins > duplicate-overrides--js-plugins 1`] = ` 42 | { 43 | "config": { 44 | "$schema": "./node_modules/oxlint/configuration_schema.json", 45 | "categories": { 46 | "correctness": "off", 47 | }, 48 | "env": { 49 | "builtin": true, 50 | }, 51 | "overrides": [ 52 | { 53 | "files": [ 54 | "**/*.js", 55 | "**/*.cjs", 56 | "**/*.mjs", 57 | ], 58 | "rules": { 59 | "no-unused-vars": "error", 60 | }, 61 | }, 62 | { 63 | "files": [ 64 | "**/*.ts", 65 | "**/*.mts", 66 | "**/*.cts", 67 | ], 68 | "rules": { 69 | "arrow-body-style": "error", 70 | }, 71 | }, 72 | ], 73 | "plugins": [], 74 | }, 75 | "warnings": [], 76 | } 77 | `; 78 | 79 | exports[`duplicate-overrides --type-aware > duplicate-overrides--type-aware 1`] = ` 80 | { 81 | "config": { 82 | "$schema": "./node_modules/oxlint/configuration_schema.json", 83 | "categories": { 84 | "correctness": "off", 85 | }, 86 | "env": { 87 | "builtin": true, 88 | }, 89 | "overrides": [ 90 | { 91 | "files": [ 92 | "**/*.js", 93 | "**/*.cjs", 94 | "**/*.mjs", 95 | ], 96 | "rules": { 97 | "no-unused-vars": "error", 98 | }, 99 | }, 100 | { 101 | "files": [ 102 | "**/*.ts", 103 | "**/*.mts", 104 | "**/*.cts", 105 | ], 106 | "rules": { 107 | "arrow-body-style": "error", 108 | }, 109 | }, 110 | ], 111 | "plugins": [], 112 | }, 113 | "warnings": [], 114 | } 115 | `; 116 | 117 | exports[`duplicate-overrides merge > duplicate-overrides--merge 1`] = ` 118 | { 119 | "config": { 120 | "$schema": "./node_modules/oxlint/configuration_schema.json", 121 | "categories": { 122 | "correctness": "error", 123 | "perf": "error", 124 | }, 125 | "env": { 126 | "builtin": true, 127 | }, 128 | "overrides": [ 129 | { 130 | "files": [ 131 | "**/*.js", 132 | "**/*.cjs", 133 | "**/*.mjs", 134 | ], 135 | "rules": { 136 | "no-unused-vars": "error", 137 | }, 138 | }, 139 | { 140 | "files": [ 141 | "**/*.ts", 142 | "**/*.mts", 143 | "**/*.cts", 144 | ], 145 | "rules": { 146 | "arrow-body-style": "error", 147 | }, 148 | }, 149 | ], 150 | }, 151 | "warnings": [], 152 | } 153 | `; 154 | -------------------------------------------------------------------------------- /src/walker/comments/CheatSheet.md: -------------------------------------------------------------------------------- 1 | # ✅ ESLint Directive Cheat Sheet 2 | 3 | ## 🔧 Rule Configuration 4 | 5 | | Directive | Comment Type | Purpose | 6 | | ------------------------------------------------ | ------------ | -------------------------------------------------------------------------- | 7 | | `/* eslint rule-name: "off" */` | **Block** | Configure rules for the **entire file** or at the location of the comment. | 8 | | Example: | | | 9 | | `/* eslint eqeqeq: "off", no-console: "warn" */` | Block | Disables `eqeqeq`, warns for `console.log`. | 10 | 11 | --- 12 | 13 | ## 🌍 Global Variables 14 | 15 | | Directive | Comment Type | Purpose | 16 | | ------------------------- | ------------ | ---------------------------------------------------------- | 17 | | `/* global var1, var2 */` | **Block** | Declares globals so ESLint doesn’t mark them as undefined. | 18 | | Example: | | | 19 | | `/* global $, jQuery */` | Block | Tells ESLint that `$` and `jQuery` are available globally. | 20 | 21 | --- 22 | 23 | ## 🌐 Environment Declaration 24 | 25 | | Directive | Comment Type | Purpose | 26 | | ----------------------------- | ------------ | ---------------------------------------------------------------- | 27 | | `/* eslint-env env1, env2 */` | **Block** | Declares environments so ESLint knows which globals are allowed. | 28 | | Example: | | | 29 | | `/* eslint-env node, es6 */` | Block | Enables Node.js and ES6 environments. | 30 | 31 | --- 32 | 33 | ## 🚫 Disabling ESLint Rules 34 | 35 | | Directive | Comment Type | Affects | Notes | 36 | | ----------------------------------- | ------------ | ---------------------- | -------------------------------------------------------- | 37 | | `/* eslint-disable */` | **Block** | All following code | Disables **all rules** unless specific rules are listed. | 38 | | `/* eslint-disable rule1, rule2 */` | **Block** | All following code | Disables only listed rules. | 39 | | `/* eslint-enable */` | **Block** | Re-enables all rules | Paired with `eslint-disable`. | 40 | | `// eslint-disable-line rule1` | **Line** | That specific line | Use with specific rules or all rules. | 41 | | `// eslint-disable-next-line rule1` | **Line** | The **next line** only | Very handy for one-off exceptions. | 42 | 43 | --- 44 | 45 | ## 📤 Exported Variables 46 | 47 | | Directive | Comment Type | Purpose | 48 | | --------------------------- | ------------ | ------------------------------------------------------- | 49 | | `/* exported var1, var2 */` | **Block** | Declares that variables are meant to be used elsewhere. | 50 | | Example: | | | 51 | | `/* exported myFunction */` | Block | Avoids "defined but never used" warnings in scripts. | 52 | -------------------------------------------------------------------------------- /bin/oxlint-migrate.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | import { existsSync, renameSync, writeFileSync, readFileSync } from 'node:fs'; 5 | import path from 'node:path'; 6 | import { 7 | getAutodetectedEslintConfigName, 8 | loadESLintConfig, 9 | } from './config-loader.js'; 10 | import main from '../src/index.js'; 11 | import packageJson from '../package.json' with { type: 'json' }; 12 | import { Options } from '../src/types.js'; 13 | import { walkAndReplaceProjectFiles } from '../src/walker/index.js'; 14 | import { getAllProjectFiles } from './project-loader.js'; 15 | import { writeFile } from 'node:fs/promises'; 16 | import { preFixForJsPlugins } from '../src/js_plugin_fixes.js'; 17 | import { DefaultReporter } from '../src/reporter.js'; 18 | 19 | const cwd = process.cwd(); 20 | 21 | const getFileContent = (absoluteFilePath: string): string | undefined => { 22 | try { 23 | return readFileSync(absoluteFilePath, 'utf-8'); 24 | } catch { 25 | return undefined; 26 | } 27 | }; 28 | 29 | program 30 | .name('oxlint-migrate') 31 | .version(packageJson.version) 32 | .argument('[eslint-config]', 'The path to the eslint v9 config file') 33 | .option( 34 | '--output-file ', 35 | 'The oxlint configuration file where to eslint v9 rules will be written to', 36 | '.oxlintrc.json' 37 | ) 38 | .option( 39 | '--merge', 40 | 'Merge eslint configuration with an existing .oxlintrc.json configuration', 41 | false 42 | ) 43 | .option( 44 | '--with-nursery', 45 | 'Include oxlint rules which are currently under development', 46 | false 47 | ) 48 | .option( 49 | '--replace-eslint-comments', 50 | 'Search in the project files for eslint comments and replaces them with oxlint. Some eslint comments are not supported and will be reported.' 51 | ) 52 | .option( 53 | '--type-aware', 54 | 'Includes supported type-aware rules. Needs the same flag in `oxlint` to enable it.' 55 | ) 56 | .option( 57 | '--js-plugins', 58 | 'Tries to convert unsupported oxlint plugins with `jsPlugins`.' 59 | ) 60 | .action(async (filePath: string | undefined) => { 61 | const cliOptions = program.opts(); 62 | const oxlintFilePath = path.join(cwd, cliOptions.outputFile); 63 | const reporter = new DefaultReporter(); 64 | 65 | const options: Options = { 66 | reporter, 67 | merge: !!cliOptions.merge, 68 | withNursery: !!cliOptions.withNursery, 69 | typeAware: !!cliOptions.typeAware, 70 | jsPlugins: !!cliOptions.jsPlugins, 71 | }; 72 | 73 | if (cliOptions.replaceEslintComments) { 74 | await walkAndReplaceProjectFiles( 75 | await getAllProjectFiles(), 76 | (filePath: string) => getFileContent(filePath), 77 | (filePath: string, content: string) => 78 | writeFile(filePath, content, 'utf-8'), 79 | options 80 | ); 81 | 82 | // stop the program 83 | return; 84 | } 85 | 86 | if (filePath === undefined) { 87 | filePath = getAutodetectedEslintConfigName(cwd); 88 | } else { 89 | filePath = path.join(cwd, filePath); 90 | } 91 | 92 | if (filePath === undefined) { 93 | program.error(`could not autodetect eslint config file`); 94 | } 95 | 96 | const resetPreFix = await preFixForJsPlugins(); 97 | const eslintConfigs = await loadESLintConfig(filePath); 98 | resetPreFix(); 99 | 100 | let config; 101 | if (options.merge && existsSync(oxlintFilePath)) { 102 | // we expect that is a right config file 103 | config = JSON.parse( 104 | readFileSync(oxlintFilePath, { encoding: 'utf8', flag: 'r' }) 105 | ); 106 | } 107 | 108 | const oxlintConfig = 109 | 'default' in eslintConfigs 110 | ? await main(eslintConfigs.default, config, options) 111 | : await main(eslintConfigs, config, options); 112 | 113 | if (existsSync(oxlintFilePath)) { 114 | renameSync(oxlintFilePath, `${oxlintFilePath}.bak`); 115 | } 116 | 117 | writeFileSync(oxlintFilePath, JSON.stringify(oxlintConfig, null, 2)); 118 | 119 | for (const report of reporter.getReports()) { 120 | console.warn(report); 121 | } 122 | }); 123 | 124 | program.parse(); 125 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from 'eslint'; 2 | import { Options, OxlintConfig, OxlintConfigOverride } from './types.js'; 3 | import { 4 | detectEnvironmentByGlobals, 5 | transformEnvAndGlobals, 6 | } from './env_globals.js'; 7 | import { cleanUpOxlintConfig } from './cleanup.js'; 8 | import { transformIgnorePatterns } from './ignorePatterns.js'; 9 | import { 10 | detectNeededRulesPlugins, 11 | transformRuleEntry, 12 | } from './plugins_rules.js'; 13 | import { detectSameOverride } from './overrides.js'; 14 | import fixForJsPlugins from './js_plugin_fixes.js'; 15 | 16 | const buildConfig = ( 17 | configs: Linter.Config[], 18 | oxlintConfig?: OxlintConfig, 19 | options?: Options 20 | ): OxlintConfig => { 21 | if (oxlintConfig === undefined) { 22 | // when upgrading and no configuration is found, we use the default configuration from oxlint 23 | if (options?.merge) { 24 | oxlintConfig = { 25 | // disable all plugins and check later 26 | plugins: ['oxc', 'typescript', 'unicorn', 'react'], 27 | categories: { 28 | correctness: 'warn', 29 | }, 30 | }; 31 | } else { 32 | // when no merge we start from 0 rules 33 | oxlintConfig = { 34 | $schema: './node_modules/oxlint/configuration_schema.json', 35 | // disable all plugins and check later 36 | plugins: [], 37 | categories: { 38 | // ToDo: for merge set the default category manuel when it is not found 39 | // ToDo: later we can remove it again 40 | // default category 41 | correctness: 'off', 42 | }, 43 | }; 44 | } 45 | } 46 | 47 | // when merge check if $schema is not defined, 48 | // the default config has already defined it 49 | if (oxlintConfig.$schema === undefined && options?.merge) { 50 | oxlintConfig.$schema = './node_modules/oxlint/configuration_schema.json'; 51 | } 52 | 53 | // when upgrading check for env default 54 | // the default config has already defined it 55 | if (oxlintConfig.env?.builtin === undefined) { 56 | if (oxlintConfig.env === undefined) { 57 | oxlintConfig.env = {}; 58 | } 59 | oxlintConfig.env.builtin = true; 60 | } 61 | 62 | // when merge use the existing overrides, or else create an empty one 63 | const overrides: OxlintConfigOverride[] = options?.merge 64 | ? (oxlintConfig.overrides ?? []) 65 | : []; 66 | 67 | for (const config of configs) { 68 | // we are ignoring oxlint eslint plugin 69 | if (config.name?.startsWith('oxlint/')) { 70 | continue; 71 | } 72 | 73 | // target the base config or create an override config 74 | let targetConfig: OxlintConfig | OxlintConfigOverride; 75 | 76 | if (config.files === undefined) { 77 | targetConfig = oxlintConfig; 78 | } else { 79 | targetConfig = { 80 | files: (Array.isArray(config.files) 81 | ? config.files 82 | : [config.files]) as string[], 83 | }; 84 | const [push, result] = detectSameOverride(oxlintConfig, targetConfig); 85 | 86 | if (push) { 87 | overrides.push(result); 88 | } 89 | } 90 | 91 | transformIgnorePatterns(config, targetConfig, options); 92 | transformRuleEntry(config, targetConfig, options); 93 | transformEnvAndGlobals(config, targetConfig, options); 94 | 95 | // clean up overrides 96 | if ('files' in targetConfig) { 97 | detectNeededRulesPlugins(targetConfig); 98 | detectEnvironmentByGlobals(targetConfig); 99 | cleanUpOxlintConfig(targetConfig); 100 | } 101 | } 102 | 103 | oxlintConfig.overrides = overrides; 104 | 105 | detectNeededRulesPlugins(oxlintConfig); 106 | detectEnvironmentByGlobals(oxlintConfig); 107 | cleanUpOxlintConfig(oxlintConfig); 108 | 109 | return oxlintConfig; 110 | }; 111 | 112 | const main = async ( 113 | configs: 114 | | Linter.Config 115 | | Linter.Config[] 116 | | Promise 117 | | Promise, 118 | oxlintConfig?: OxlintConfig, 119 | options?: Options 120 | ): Promise => { 121 | const resolved = await Promise.resolve(fixForJsPlugins(configs)); 122 | const resolvedConfigs = Array.isArray(resolved) ? resolved : [resolved]; 123 | 124 | return buildConfig(resolvedConfigs, oxlintConfig, options); 125 | }; 126 | 127 | export default main; 128 | -------------------------------------------------------------------------------- /integration_test/__snapshots__/eslint-globals.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`eslint-globals > eslint-globals 1`] = ` 4 | { 5 | "config": { 6 | "$schema": "./node_modules/oxlint/configuration_schema.json", 7 | "categories": { 8 | "correctness": "off", 9 | }, 10 | "env": { 11 | "builtin": true, 12 | }, 13 | "overrides": [ 14 | { 15 | "env": { 16 | "browser": true, 17 | "es2026": true, 18 | "node": true, 19 | }, 20 | "files": [ 21 | "**/*.js", 22 | "**/*.cjs", 23 | "**/*.mjs", 24 | ], 25 | "rules": { 26 | "no-unused-vars": "error", 27 | }, 28 | }, 29 | { 30 | "env": { 31 | "browser": true, 32 | "serviceworker": true, 33 | "worker": true, 34 | }, 35 | "files": [ 36 | "**/*.ts", 37 | ], 38 | "rules": { 39 | "arrow-body-style": "error", 40 | }, 41 | }, 42 | ], 43 | "plugins": [], 44 | }, 45 | "warnings": [], 46 | } 47 | `; 48 | 49 | exports[`eslint-globals --js-plugins > eslint-globals--js-plugins 1`] = ` 50 | { 51 | "config": { 52 | "$schema": "./node_modules/oxlint/configuration_schema.json", 53 | "categories": { 54 | "correctness": "off", 55 | }, 56 | "env": { 57 | "builtin": true, 58 | }, 59 | "overrides": [ 60 | { 61 | "env": { 62 | "browser": true, 63 | "es2026": true, 64 | "node": true, 65 | }, 66 | "files": [ 67 | "**/*.js", 68 | "**/*.cjs", 69 | "**/*.mjs", 70 | ], 71 | "rules": { 72 | "no-unused-vars": "error", 73 | }, 74 | }, 75 | { 76 | "env": { 77 | "browser": true, 78 | "serviceworker": true, 79 | "worker": true, 80 | }, 81 | "files": [ 82 | "**/*.ts", 83 | ], 84 | "rules": { 85 | "arrow-body-style": "error", 86 | }, 87 | }, 88 | ], 89 | "plugins": [], 90 | }, 91 | "warnings": [], 92 | } 93 | `; 94 | 95 | exports[`eslint-globals --type-aware > eslint-globals--type-aware 1`] = ` 96 | { 97 | "config": { 98 | "$schema": "./node_modules/oxlint/configuration_schema.json", 99 | "categories": { 100 | "correctness": "off", 101 | }, 102 | "env": { 103 | "builtin": true, 104 | }, 105 | "overrides": [ 106 | { 107 | "env": { 108 | "browser": true, 109 | "es2026": true, 110 | "node": true, 111 | }, 112 | "files": [ 113 | "**/*.js", 114 | "**/*.cjs", 115 | "**/*.mjs", 116 | ], 117 | "rules": { 118 | "no-unused-vars": "error", 119 | }, 120 | }, 121 | { 122 | "env": { 123 | "browser": true, 124 | "serviceworker": true, 125 | "worker": true, 126 | }, 127 | "files": [ 128 | "**/*.ts", 129 | ], 130 | "rules": { 131 | "arrow-body-style": "error", 132 | }, 133 | }, 134 | ], 135 | "plugins": [], 136 | }, 137 | "warnings": [], 138 | } 139 | `; 140 | 141 | exports[`eslint-globals merge > eslint-globals--merge 1`] = ` 142 | { 143 | "config": { 144 | "$schema": "./node_modules/oxlint/configuration_schema.json", 145 | "categories": { 146 | "correctness": "error", 147 | "perf": "error", 148 | }, 149 | "env": { 150 | "builtin": true, 151 | }, 152 | "overrides": [ 153 | { 154 | "env": { 155 | "browser": true, 156 | "es2026": true, 157 | "node": true, 158 | }, 159 | "files": [ 160 | "**/*.js", 161 | "**/*.cjs", 162 | "**/*.mjs", 163 | ], 164 | "rules": { 165 | "no-unused-vars": "error", 166 | }, 167 | }, 168 | { 169 | "env": { 170 | "browser": true, 171 | "serviceworker": true, 172 | "worker": true, 173 | }, 174 | "files": [ 175 | "**/*.ts", 176 | ], 177 | "rules": { 178 | "arrow-body-style": "error", 179 | }, 180 | }, 181 | ], 182 | }, 183 | "warnings": [], 184 | } 185 | `; 186 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const rulesPrefixesForPlugins: Record = { 2 | import: 'import', 3 | 'import-x': 'import', 4 | jest: 'jest', 5 | jsdoc: 'jsdoc', 6 | 'jsx-a11y': 'jsx-a11y', 7 | '@next/next': 'nextjs', 8 | node: 'node', 9 | n: 'node', 10 | promise: 'promise', 11 | react: 'react', 12 | 'react-perf': 'react-perf', 13 | 'react-hooks': 'react', 14 | '@typescript-eslint': 'typescript', 15 | unicorn: 'unicorn', 16 | vitest: 'vitest', 17 | vue: 'vue', 18 | }; 19 | 20 | // Some typescript-eslint rules are re-implemented version of eslint rules. 21 | // Since oxlint supports these rules under eslint/* and it also supports TS, 22 | // we should override these to make implementation status up-to-date. 23 | // remapping in source-code: 24 | export const typescriptRulesExtendEslintRules = [ 25 | 'class-methods-use-this', 26 | 'default-param-last', 27 | 'init-declarations', 28 | 'max-params', 29 | 'no-array-constructor', 30 | 'no-dupe-class-members', 31 | 'no-empty-function', 32 | 'no-invalid-this', 33 | 'no-loop-func', 34 | 'no-loss-of-precision', 35 | 'no-magic-numbers', 36 | 'no-redeclare', 37 | 'no-restricted-imports', 38 | 'no-shadow', 39 | 'no-unused-expressions', 40 | 'no-unused-vars', 41 | 'no-use-before-define', 42 | 'no-useless-constructor', 43 | ]; 44 | 45 | // Some typescript-eslint rules requires `oxlint-tsgolint` with `--type-aware` flag 46 | // Only support these rules, when the same is flag passed to the migrate script. 47 | // Example: https://github.com/rolldown/rolldown/pull/5660 48 | // List copied from: 49 | // https://github.com/typescript-eslint/typescript-eslint/blob/7319bad3a5022be2adfbcb331451cfd85d1d786a/packages/eslint-plugin/src/configs/flat/disable-type-checked.ts 50 | export const typescriptTypeAwareRules = [ 51 | '@typescript-eslint/await-thenable', 52 | '@typescript-eslint/consistent-return', 53 | '@typescript-eslint/consistent-type-exports', 54 | '@typescript-eslint/dot-notation', 55 | '@typescript-eslint/naming-convention', 56 | '@typescript-eslint/no-array-delete', 57 | '@typescript-eslint/no-base-to-string', 58 | '@typescript-eslint/no-confusing-void-expression', 59 | '@typescript-eslint/no-deprecated', 60 | '@typescript-eslint/no-duplicate-type-constituents', 61 | '@typescript-eslint/no-floating-promises', 62 | '@typescript-eslint/no-for-in-array', 63 | '@typescript-eslint/no-implied-eval', 64 | '@typescript-eslint/no-meaningless-void-operator', 65 | '@typescript-eslint/no-misused-promises', 66 | '@typescript-eslint/no-misused-spread', 67 | '@typescript-eslint/no-mixed-enums', 68 | '@typescript-eslint/no-redundant-type-constituents', 69 | '@typescript-eslint/no-unnecessary-boolean-literal-compare', 70 | '@typescript-eslint/no-unnecessary-condition', 71 | '@typescript-eslint/no-unnecessary-qualifier', 72 | '@typescript-eslint/no-unnecessary-template-expression', 73 | '@typescript-eslint/no-unnecessary-type-arguments', 74 | '@typescript-eslint/no-unnecessary-type-assertion', 75 | '@typescript-eslint/no-unnecessary-type-conversion', 76 | '@typescript-eslint/no-unnecessary-type-parameters', 77 | '@typescript-eslint/no-unsafe-argument', 78 | '@typescript-eslint/no-unsafe-assignment', 79 | '@typescript-eslint/no-unsafe-call', 80 | '@typescript-eslint/no-unsafe-enum-comparison', 81 | '@typescript-eslint/no-unsafe-member-access', 82 | '@typescript-eslint/no-unsafe-return', 83 | '@typescript-eslint/no-unsafe-type-assertion', 84 | '@typescript-eslint/no-unsafe-unary-minus', 85 | '@typescript-eslint/non-nullable-type-assertion-style', 86 | '@typescript-eslint/only-throw-error', 87 | '@typescript-eslint/prefer-destructuring', 88 | '@typescript-eslint/prefer-find', 89 | '@typescript-eslint/prefer-includes', 90 | '@typescript-eslint/prefer-nullish-coalescing', 91 | '@typescript-eslint/prefer-optional-chain', 92 | '@typescript-eslint/prefer-promise-reject-errors', 93 | '@typescript-eslint/prefer-readonly', 94 | '@typescript-eslint/prefer-readonly-parameter-types', 95 | '@typescript-eslint/prefer-reduce-type-parameter', 96 | '@typescript-eslint/prefer-regexp-exec', 97 | '@typescript-eslint/prefer-return-this-type', 98 | '@typescript-eslint/prefer-string-starts-ends-with', 99 | '@typescript-eslint/promise-function-async', 100 | '@typescript-eslint/related-getter-setter-pairs', 101 | '@typescript-eslint/require-array-sort-compare', 102 | '@typescript-eslint/require-await', 103 | '@typescript-eslint/restrict-plus-operands', 104 | '@typescript-eslint/restrict-template-expressions', 105 | '@typescript-eslint/return-await', 106 | '@typescript-eslint/strict-boolean-expressions', 107 | '@typescript-eslint/switch-exhaustiveness-check', 108 | '@typescript-eslint/unbound-method', 109 | '@typescript-eslint/use-unknown-in-catch-callback-variable', 110 | ]; 111 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import main from './index.js'; 3 | import globals from 'globals'; 4 | 5 | describe('main', () => { 6 | test('basic', async () => { 7 | const result = await main([ 8 | { 9 | rules: { 10 | 'no-magic-numbers': 'error', 11 | }, 12 | }, 13 | ]); 14 | 15 | expect(result).toStrictEqual({ 16 | $schema: './node_modules/oxlint/configuration_schema.json', 17 | categories: { 18 | correctness: 'off', 19 | }, 20 | env: { 21 | builtin: true, 22 | }, 23 | plugins: [], 24 | rules: { 25 | 'no-magic-numbers': 'error', 26 | }, 27 | }); 28 | }); 29 | 30 | test('2 basic configs', async () => { 31 | const result = await main([ 32 | { 33 | rules: { 34 | 'no-magic-numbers': 'error', 35 | }, 36 | }, 37 | { 38 | rules: { 39 | 'no-loss-of-precision': 'error', 40 | }, 41 | }, 42 | ]); 43 | 44 | expect(result).toStrictEqual({ 45 | $schema: './node_modules/oxlint/configuration_schema.json', 46 | categories: { 47 | correctness: 'off', 48 | }, 49 | env: { 50 | builtin: true, 51 | }, 52 | plugins: [], 53 | rules: { 54 | 'no-magic-numbers': 'error', 55 | 'no-loss-of-precision': 'error', 56 | }, 57 | }); 58 | }); 59 | 60 | test('overlapping rules in configs', async () => { 61 | const result = await main([ 62 | { 63 | rules: { 64 | 'no-magic-numbers': 'warn', 65 | }, 66 | }, 67 | { 68 | rules: { 69 | 'no-magic-numbers': 'off', 70 | }, 71 | }, 72 | ]); 73 | 74 | expect(result).toStrictEqual({ 75 | $schema: './node_modules/oxlint/configuration_schema.json', 76 | categories: { 77 | correctness: 'off', 78 | }, 79 | env: { 80 | builtin: true, 81 | }, 82 | plugins: [], 83 | rules: {}, 84 | }); 85 | }); 86 | 87 | test('1 basic config, 1 file config', async () => { 88 | const result = await main([ 89 | { 90 | rules: { 91 | 'no-magic-numbers': 'error', 92 | }, 93 | }, 94 | { 95 | // ToDo: eslint-plugin-typescript will probably generate this when using their build method 96 | files: ['*.ts'], 97 | rules: { 98 | 'no-loss-of-precision': 'error', 99 | }, 100 | }, 101 | ]); 102 | 103 | expect(result).toStrictEqual({ 104 | $schema: './node_modules/oxlint/configuration_schema.json', 105 | categories: { 106 | correctness: 'off', 107 | }, 108 | env: { 109 | builtin: true, 110 | }, 111 | overrides: [ 112 | { 113 | files: ['*.ts'], 114 | rules: { 115 | 'no-loss-of-precision': 'error', 116 | }, 117 | }, 118 | ], 119 | plugins: [], 120 | rules: { 121 | 'no-magic-numbers': 'error', 122 | }, 123 | }); 124 | }); 125 | 126 | test('globals', async () => { 127 | const result = await main([ 128 | { 129 | languageOptions: { 130 | globals: { 131 | Foo: 'writable', 132 | Foo2: 'writeable', 133 | Bar: 'readable', 134 | Bar2: 'writeable', 135 | Baz: 'off', 136 | Bux: true, 137 | Bux2: false, 138 | }, 139 | }, 140 | }, 141 | ]); 142 | 143 | expect(result).toStrictEqual({ 144 | $schema: './node_modules/oxlint/configuration_schema.json', 145 | categories: { 146 | correctness: 'off', 147 | }, 148 | env: { 149 | builtin: true, 150 | }, 151 | globals: { 152 | Foo: 'writable', 153 | Foo2: 'writable', 154 | Bar: 'readonly', 155 | Bar2: 'writable', 156 | Baz: 'off', 157 | Bux: 'writable', 158 | Bux2: 'readonly', 159 | }, 160 | plugins: [], 161 | }); 162 | }); 163 | 164 | test('auto removes globals when env is detected', async () => { 165 | const result = await main([ 166 | { 167 | languageOptions: { 168 | ecmaVersion: 2022, 169 | globals: globals.es2022, 170 | }, 171 | }, 172 | ]); 173 | 174 | // ToDo: just detect the oldest one and skip all others 175 | // we can not be sure that es2022 is supported, when no ecmaVersion is provided 176 | // the match in globals package could possible detect es2021 support. 177 | expect(result).toStrictEqual({ 178 | $schema: './node_modules/oxlint/configuration_schema.json', 179 | categories: { 180 | correctness: 'off', 181 | }, 182 | env: { 183 | builtin: true, 184 | es2024: true, 185 | }, 186 | plugins: [], 187 | }); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /integration_test/projects/vuejs-core.eslint.config.js: -------------------------------------------------------------------------------- 1 | import importX from 'eslint-plugin-import-x'; 2 | import tseslint from 'typescript-eslint'; 3 | import vitest from '@vitest/eslint-plugin'; 4 | import { builtinModules } from 'node:module'; 5 | 6 | const DOMGlobals = ['window', 'document']; 7 | const NodeGlobals = ['module', 'require']; 8 | 9 | const banConstEnum = { 10 | selector: 'TSEnumDeclaration[const=true]', 11 | message: 12 | 'Please use non-const enums. This project automatically inlines enums.', 13 | }; 14 | 15 | export default tseslint.config( 16 | { 17 | files: ['**/*.js', '**/*.ts', '**/*.tsx'], 18 | extends: [tseslint.configs.base], 19 | plugins: { 20 | 'import-x': importX, 21 | }, 22 | rules: { 23 | 'no-debugger': 'error', 24 | 'no-console': ['error', { allow: ['warn', 'error', 'info'] }], 25 | // most of the codebase are expected to be env agnostic 26 | 'no-restricted-globals': ['error', ...DOMGlobals, ...NodeGlobals], 27 | 28 | 'no-restricted-syntax': [ 29 | 'error', 30 | banConstEnum, 31 | { 32 | selector: 'ObjectPattern > RestElement', 33 | message: 34 | 'Our output target is ES2016, and object rest spread results in ' + 35 | 'verbose helpers and should be avoided.', 36 | }, 37 | { 38 | selector: 'ObjectExpression > SpreadElement', 39 | message: 40 | 'esbuild transpiles object spread into very verbose inline helpers.\n' + 41 | 'Please use the `extend` helper from @vue/shared instead.', 42 | }, 43 | { 44 | selector: 'AwaitExpression', 45 | message: 46 | 'Our output target is ES2016, so async/await syntax should be avoided.', 47 | }, 48 | { 49 | selector: 'ChainExpression', 50 | message: 51 | 'Our output target is ES2016, and optional chaining results in ' + 52 | 'verbose helpers and should be avoided.', 53 | }, 54 | ], 55 | 'sort-imports': ['error', { ignoreDeclarationSort: true }], 56 | 57 | 'import-x/no-nodejs-modules': [ 58 | 'error', 59 | { allow: builtinModules.map((mod) => `node:${mod}`) }, 60 | ], 61 | // This rule enforces the preference for using '@ts-expect-error' comments in TypeScript 62 | // code to indicate intentional type errors, improving code clarity and maintainability. 63 | '@typescript-eslint/prefer-ts-expect-error': 'error', 64 | // Enforce the use of 'import type' for importing types 65 | '@typescript-eslint/consistent-type-imports': [ 66 | 'error', 67 | { 68 | fixStyle: 'inline-type-imports', 69 | disallowTypeAnnotations: false, 70 | }, 71 | ], 72 | // Enforce the use of top-level import type qualifier when an import only has specifiers with inline type qualifiers 73 | '@typescript-eslint/no-import-type-side-effects': 'error', 74 | }, 75 | }, 76 | 77 | // tests, no restrictions (runs in Node / Vitest with jsdom) 78 | { 79 | files: [ 80 | '**/__tests__/**', 81 | 'packages-private/dts-test/**', 82 | 'packages-private/dts-build-test/**', 83 | ], 84 | plugins: { vitest }, 85 | languageOptions: { 86 | globals: { 87 | ...vitest.environments.env.globals, 88 | }, 89 | }, 90 | rules: { 91 | 'no-console': 'off', 92 | 'no-restricted-globals': 'off', 93 | 'no-restricted-syntax': 'off', 94 | 'vitest/no-disabled-tests': 'error', 95 | 'vitest/no-focused-tests': 'error', 96 | }, 97 | }, 98 | 99 | // shared, may be used in any env 100 | { 101 | files: ['packages/shared/**', 'eslint.config.js'], 102 | rules: { 103 | 'no-restricted-globals': 'off', 104 | }, 105 | }, 106 | 107 | // Packages targeting DOM 108 | { 109 | files: ['packages/{vue,vue-compat,runtime-dom}/**'], 110 | rules: { 111 | 'no-restricted-globals': ['error', ...NodeGlobals], 112 | }, 113 | }, 114 | 115 | // Packages targeting Node 116 | { 117 | files: ['packages/{compiler-sfc,compiler-ssr,server-renderer}/**'], 118 | rules: { 119 | 'no-restricted-globals': ['error', ...DOMGlobals], 120 | 'no-restricted-syntax': ['error', banConstEnum], 121 | }, 122 | }, 123 | 124 | // Private package, browser only + no syntax restrictions 125 | { 126 | files: [ 127 | 'packages-private/template-explorer/**', 128 | 'packages-private/sfc-playground/**', 129 | ], 130 | rules: { 131 | 'no-restricted-globals': ['error', ...NodeGlobals], 132 | 'no-restricted-syntax': ['error', banConstEnum], 133 | 'no-console': 'off', 134 | }, 135 | }, 136 | 137 | // JavaScript files 138 | { 139 | files: ['*.js'], 140 | rules: { 141 | // We only do `no-unused-vars` checks for js files, TS files are checked by TypeScript itself. 142 | 'no-unused-vars': ['error', { vars: 'all', args: 'none' }], 143 | }, 144 | }, 145 | 146 | // Node scripts 147 | { 148 | files: [ 149 | 'eslint.config.js', 150 | 'rollup*.config.js', 151 | 'scripts/**', 152 | './*.{js,ts}', 153 | 'packages/*/*.js', 154 | 'packages/vue/*/*.js', 155 | ], 156 | rules: { 157 | 'no-restricted-globals': 'off', 158 | 'no-restricted-syntax': ['error', banConstEnum], 159 | 'no-console': 'off', 160 | }, 161 | }, 162 | 163 | // Import nodejs modules in compiler-sfc 164 | { 165 | files: ['packages/compiler-sfc/src/**'], 166 | rules: { 167 | 'import-x/no-nodejs-modules': ['error', { allow: builtinModules }], 168 | }, 169 | }, 170 | 171 | { 172 | ignores: [ 173 | '**/dist/', 174 | '**/temp/', 175 | '**/coverage/', 176 | '.idea/', 177 | 'explorations/', 178 | 'dts-build/packages', 179 | ], 180 | } 181 | ); 182 | -------------------------------------------------------------------------------- /src/cleanup.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { cleanUpOxlintConfig } from './cleanup.js'; 3 | import type { OxlintConfig } from './types.js'; 4 | 5 | describe('cleanUpOxlintConfig', () => { 6 | describe('overrides cleanup', () => { 7 | it('should remove empty overrides array', () => { 8 | const config: OxlintConfig = { 9 | overrides: [], 10 | }; 11 | cleanUpOxlintConfig(config); 12 | expect(config.overrides).toBeUndefined(); 13 | }); 14 | 15 | it('should remove overrides with only one key (files)', () => { 16 | const config: OxlintConfig = { 17 | overrides: [{ files: ['*.ts'] }], 18 | }; 19 | cleanUpOxlintConfig(config); 20 | expect(config.overrides).toBeUndefined(); 21 | }); 22 | 23 | it('should keep overrides with files and rules', () => { 24 | const config: OxlintConfig = { 25 | overrides: [ 26 | { 27 | files: ['*.ts'], 28 | rules: { 'no-console': 'error' }, 29 | }, 30 | ], 31 | }; 32 | cleanUpOxlintConfig(config); 33 | expect(config.overrides).toHaveLength(1); 34 | }); 35 | 36 | it('should merge consecutive identical overrides', () => { 37 | const config: OxlintConfig = { 38 | overrides: [ 39 | { 40 | files: ['*.ts', '*.tsx'], 41 | plugins: ['typescript'], 42 | }, 43 | { 44 | files: ['*.ts', '*.tsx'], 45 | plugins: ['typescript'], 46 | }, 47 | ], 48 | }; 49 | cleanUpOxlintConfig(config); 50 | expect(config.overrides).toHaveLength(1); 51 | }); 52 | 53 | it('should not merge non-consecutive identical overrides', () => { 54 | const config: OxlintConfig = { 55 | overrides: [ 56 | { 57 | files: ['*.ts'], 58 | rules: { 'no-console': 'error' }, 59 | }, 60 | { 61 | files: ['*.js'], 62 | rules: { 'no-var': 'error' }, 63 | }, 64 | { 65 | files: ['*.ts'], 66 | rules: { 'no-console': 'error' }, 67 | }, 68 | ], 69 | }; 70 | cleanUpOxlintConfig(config); 71 | expect(config.overrides).toHaveLength(3); 72 | }); 73 | }); 74 | 75 | describe('env cleanup', () => { 76 | it('should remove unsupported es3, es5, es2015 env keys', () => { 77 | const config: OxlintConfig = { 78 | env: { 79 | es3: true, 80 | es5: true, 81 | es2015: true, 82 | es2020: true, 83 | }, 84 | }; 85 | cleanUpOxlintConfig(config); 86 | expect(config.env?.es3).toBeUndefined(); 87 | expect(config.env?.es5).toBeUndefined(); 88 | expect(config.env?.es2015).toBeUndefined(); 89 | expect(config.env?.es2020).toBe(true); 90 | }); 91 | 92 | it('should remove older ES versions when newer one is present', () => { 93 | const config: OxlintConfig = { 94 | env: { 95 | es2020: true, 96 | es2021: true, 97 | es2022: true, 98 | }, 99 | }; 100 | cleanUpOxlintConfig(config); 101 | expect(config.env?.es2020).toBeUndefined(); 102 | expect(config.env?.es2021).toBeUndefined(); 103 | expect(config.env?.es2022).toBe(true); 104 | }); 105 | }); 106 | 107 | describe('globals cleanup', () => { 108 | it('should remove empty globals object', () => { 109 | const config: OxlintConfig = { 110 | globals: {}, 111 | }; 112 | cleanUpOxlintConfig(config); 113 | expect(config.globals).toBeUndefined(); 114 | }); 115 | 116 | it('should keep non-empty globals object', () => { 117 | const config: OxlintConfig = { 118 | globals: { 119 | myGlobal: 'readonly', 120 | }, 121 | }; 122 | cleanUpOxlintConfig(config); 123 | expect(config.globals).toBeDefined(); 124 | expect(config.globals?.myGlobal).toBe('readonly'); 125 | }); 126 | }); 127 | 128 | describe('duplicate overrides with differing files cleanup', () => { 129 | it('should merge consecutive overrides with differing files but identical other settings', () => { 130 | const config: OxlintConfig = { 131 | overrides: [ 132 | { 133 | files: ['*.ts'], 134 | plugins: ['typescript'], 135 | }, 136 | { 137 | files: ['*.tsx'], 138 | plugins: ['typescript'], 139 | }, 140 | ], 141 | }; 142 | cleanUpOxlintConfig(config); 143 | expect(config.overrides).toHaveLength(1); 144 | expect(config.overrides?.[0].files).toEqual(['*.ts', '*.tsx']); 145 | }); 146 | 147 | it('should merge consecutive overrides with differing files but identical other settings, including jsPlugins and categories', () => { 148 | const config: OxlintConfig = { 149 | overrides: [ 150 | { 151 | files: ['*.ts'], 152 | plugins: ['typescript'], 153 | jsPlugins: ['foobar'], 154 | categories: { correctness: 'warn' }, 155 | }, 156 | { 157 | files: ['*.tsx'], 158 | plugins: ['typescript'], 159 | jsPlugins: ['foobar'], 160 | categories: { correctness: 'warn' }, 161 | }, 162 | ], 163 | }; 164 | cleanUpOxlintConfig(config); 165 | expect(config.overrides).toHaveLength(1); 166 | expect(config.overrides?.[0].files).toEqual(['*.ts', '*.tsx']); 167 | expect(config.overrides?.[0].jsPlugins).toEqual(['foobar']); 168 | expect(config.overrides?.[0].categories).toEqual({ correctness: 'warn' }); 169 | }); 170 | 171 | it('should not merge consecutive overrides with differing non-file settings like env', () => { 172 | const config: OxlintConfig = { 173 | overrides: [ 174 | { 175 | files: ['*.ts'], 176 | env: { browser: true }, 177 | rules: { 'no-console': 'error' }, 178 | }, 179 | { 180 | files: ['*.tsx'], 181 | env: { node: true }, 182 | rules: { 'no-console': 'error' }, 183 | }, 184 | ], 185 | }; 186 | cleanUpOxlintConfig(config); 187 | expect(config.overrides).toHaveLength(2); 188 | }); 189 | 190 | // We don't currently handle merging for this, as it's not possible to easily 191 | // determine if the override in the middle is changing the ultimate behavior of the 192 | // configured lint rules. 193 | it('should not merge non-consecutive overrides with differing files', () => { 194 | const config: OxlintConfig = { 195 | overrides: [ 196 | { 197 | files: ['*.ts'], 198 | plugins: ['typescript'], 199 | }, 200 | { 201 | files: ['*.js'], 202 | plugins: ['javascript'], 203 | }, 204 | { 205 | files: ['*.tsx'], 206 | plugins: ['typescript'], 207 | }, 208 | ], 209 | }; 210 | cleanUpOxlintConfig(config); 211 | expect(config.overrides).toHaveLength(3); 212 | }); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /src/walker/partialSourceTextLoader.ts: -------------------------------------------------------------------------------- 1 | export type PartialSourceText = { 2 | sourceText: string; 3 | offset: number; 4 | lang?: 'js' | 'jsx' | 'ts' | 'tsx'; 5 | sourceType?: 'script' | 'module'; 6 | }; 7 | 8 | function extractLangAttribute( 9 | source: string 10 | ): PartialSourceText['lang'] | undefined { 11 | const langIndex = source.indexOf('lang'); 12 | if (langIndex === -1) return undefined; 13 | 14 | let cursor = langIndex + 4; 15 | 16 | // Skip whitespace after "lang" 17 | while (cursor < source.length && isWhitespace(source[cursor])) { 18 | cursor++; 19 | } 20 | 21 | // Check for '=' 22 | if (source[cursor] !== '=') return undefined; 23 | cursor++; 24 | 25 | // Skip whitespace after '=' 26 | while (cursor < source.length && isWhitespace(source[cursor])) { 27 | cursor++; 28 | } 29 | 30 | const quote = source[cursor]; 31 | if (quote !== '"' && quote !== "'") return undefined; 32 | cursor++; 33 | 34 | let value = ''; 35 | while (cursor < source.length && source[cursor] !== quote) { 36 | value += source[cursor++]; 37 | } 38 | 39 | if (value === 'js' || value === 'jsx' || value === 'ts' || value === 'tsx') { 40 | return value; 41 | } 42 | 43 | return undefined; 44 | } 45 | 46 | function extractScriptBlocks( 47 | sourceText: string, 48 | offset: number, 49 | maxBlocks: number, 50 | parseLangAttribute: boolean 51 | ): PartialSourceText[] { 52 | const results: PartialSourceText[] = []; 53 | 54 | while (offset < sourceText.length) { 55 | const idx = sourceText.indexOf('' && 63 | nextChar !== '\n' && 64 | nextChar !== '\t' 65 | ) { 66 | offset = idx + 7; 67 | continue; 68 | } 69 | 70 | // Parse to end of opening tag 114 | const contentStart = i; 115 | const closeTag = ''; 116 | const closeIdx = sourceText.indexOf(closeTag, contentStart); 117 | if (closeIdx === -1) break; 118 | 119 | const content = sourceText.slice(contentStart, closeIdx); 120 | 121 | results.push({ sourceText: content, offset: contentStart, lang }); 122 | 123 | if (results.length >= maxBlocks) { 124 | break; // Limit reached 125 | } 126 | 127 | offset = closeIdx + closeTag.length; 128 | } 129 | 130 | return results; 131 | } 132 | 133 | export default function partialSourceTextLoader( 134 | absoluteFilePath: string, 135 | fileContent: string 136 | ): PartialSourceText[] { 137 | if (absoluteFilePath.endsWith('.vue')) { 138 | // ToDo: only two script blocks are supported 139 | return partialVueSourceTextLoader(fileContent); 140 | } else if (absoluteFilePath.endsWith('.astro')) { 141 | return partialAstroSourceTextLoader(fileContent); 142 | } else if (absoluteFilePath.endsWith('.svelte')) { 143 | // ToDo: only two script blocks are supported 144 | return partialSvelteSourceTextLoader(fileContent); 145 | } 146 | 147 | return [ 148 | { 149 | sourceText: fileContent, 150 | offset: 0, 151 | }, 152 | ]; 153 | } 154 | 155 | function isWhitespace(char: string): boolean { 156 | return char === ' ' || char === '\t' || char === '\r'; 157 | } 158 | 159 | // Helper to find frontmatter delimiter (---) with optional leading whitespace on a line 160 | function findDelimiter(sourceText: string, startPos: number): number { 161 | let i = startPos; 162 | while (i < sourceText.length) { 163 | // Check if start of line (or start of file) 164 | if (i === 0 || sourceText[i - 1] === '\n') { 165 | // Skip whitespace before delimiter 166 | let j = i; 167 | while (j < sourceText.length && isWhitespace(sourceText[j])) j++; 168 | 169 | // Check if '---' starts here 170 | if ( 171 | sourceText[j] === '-' && 172 | sourceText[j + 1] === '-' && 173 | sourceText[j + 2] === '-' 174 | ) { 175 | // Check rest of line is whitespace until newline or EOF 176 | let k = j + 3; 177 | while (k < sourceText.length && sourceText[k] !== '\n') { 178 | if (!isWhitespace(sourceText[k])) break; 179 | k++; 180 | } 181 | if (k === sourceText.length || sourceText[k] === '\n') { 182 | return j; // delimiter found at position j 183 | } 184 | } 185 | } 186 | i++; 187 | } 188 | return -1; 189 | } 190 | 191 | export function partialVueSourceTextLoader( 192 | sourceText: string 193 | ): PartialSourceText[] { 194 | return extractScriptBlocks(sourceText, 0, 2, true); 195 | } 196 | 197 | export function partialSvelteSourceTextLoader( 198 | sourceText: string 199 | ): PartialSourceText[] { 200 | return extractScriptBlocks(sourceText, 0, 2, true); 201 | } 202 | 203 | export function partialAstroSourceTextLoader( 204 | sourceText: string 205 | ): PartialSourceText[] { 206 | const results: PartialSourceText[] = []; 207 | let pos = 0; 208 | 209 | // Find frontmatter start delimiter 210 | const frontmatterStartDelimiter = findDelimiter(sourceText, pos); 211 | 212 | if (frontmatterStartDelimiter !== -1) { 213 | const frontmatterContentStart = frontmatterStartDelimiter + 3; 214 | // Find frontmatter end delimiter after content start 215 | const frontmatterEndDelimiter = findDelimiter( 216 | sourceText, 217 | frontmatterContentStart 218 | ); 219 | 220 | if (frontmatterEndDelimiter !== -1) { 221 | // Extract content between delimiters *including leading whitespace and newlines* 222 | const content = sourceText.slice( 223 | frontmatterContentStart, 224 | frontmatterEndDelimiter 225 | ); 226 | results.push({ 227 | sourceText: content, 228 | offset: frontmatterContentStart, 229 | lang: 'ts' as const, 230 | sourceType: 'module' as const, 231 | }); 232 | pos = frontmatterEndDelimiter + 3; 233 | } 234 | } 235 | 236 | results.push( 237 | ...extractScriptBlocks(sourceText, pos, Number.MAX_SAFE_INTEGER, false).map( 238 | (sourceText) => { 239 | return Object.assign(sourceText, { 240 | lang: `ts` as const, 241 | sourceType: `module` as const, 242 | }); 243 | } 244 | ) 245 | ); 246 | 247 | return results; 248 | } 249 | -------------------------------------------------------------------------------- /src/walker/comments/replaceRuleDirectiveComment.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import replaceRuleDirectiveComment from './replaceRuleDirectiveComment.js'; 3 | import { correctnessRules, nurseryRules } from '../../generated/rules.js'; 4 | 5 | const eslintCommentsLines = ['eslint-disable-line', 'eslint-disable-next-line']; 6 | 7 | const eslintCommentsBlock = ['eslint-disable', 'eslint-enable']; 8 | 9 | describe('replaceRuleDirectiveComment', () => { 10 | describe('untouched comments', () => { 11 | it('should keep invalid eslint line comments', () => { 12 | const comments = [ 13 | 'hello world', 14 | 'invalid-comment', 15 | 'disable-what', 16 | 'disable-line-what', 17 | 'disable-next-line-what', 18 | ...eslintCommentsBlock, 19 | ]; 20 | 21 | for (const comment of comments) { 22 | expect(replaceRuleDirectiveComment(comment, 'Line', {})).toBe(comment); 23 | } 24 | }); 25 | 26 | it('should keep invalid eslint block comments', () => { 27 | const comments = [ 28 | 'hello world', 29 | 'invalid-comment', 30 | 'disable-what', 31 | 'disable-line-what', 32 | 'disable-next-line-what', 33 | ...eslintCommentsLines, 34 | ]; 35 | 36 | for (const comment of comments) { 37 | expect(replaceRuleDirectiveComment(comment, 'Block', {})).toBe(comment); 38 | } 39 | }); 40 | 41 | it('should ignore comment of the directive', () => { 42 | const blockComments = eslintCommentsBlock.map( 43 | (prefix) => `${prefix} ${correctnessRules[0]} -- eslint-disable` 44 | ); 45 | const lineComments = eslintCommentsLines.map( 46 | (prefix) => 47 | `${prefix} ${correctnessRules[0]} -- eslint-disable-next-line` 48 | ); 49 | 50 | for (const comment of blockComments) { 51 | const expectedComment = comment.substring(6); 52 | expect(replaceRuleDirectiveComment(comment, 'Block', {})).toBe( 53 | `oxlint${expectedComment}` 54 | ); 55 | } 56 | 57 | for (const comment of lineComments) { 58 | const expectedComment = comment.substring(6); 59 | expect(replaceRuleDirectiveComment(comment, 'Line', {})).toBe( 60 | `oxlint${expectedComment}` 61 | ); 62 | } 63 | }); 64 | 65 | it('should keep eslint comments which disable / enable all rules', () => { 66 | const comments = [ 67 | ...eslintCommentsBlock, 68 | ...eslintCommentsBlock.map((comment) => `${comment} `), 69 | 'disable -- no-debugger', // rule-name inside comment 70 | ]; 71 | 72 | for (const comment of comments) { 73 | expect(replaceRuleDirectiveComment(comment, 'Block', {})).toBe(comment); 74 | expect(replaceRuleDirectiveComment(comment, 'Line', {})).toBe(comment); 75 | } 76 | }); 77 | 78 | it('should keep eslint comments for unsupported rules', () => { 79 | const blockComments = eslintCommentsBlock.map( 80 | (prefix) => `${prefix} unknown-rule` 81 | ); 82 | const lineComments = eslintCommentsLines.map( 83 | (prefix) => `${prefix} unknown-rule` 84 | ); 85 | 86 | for (const comment of blockComments) { 87 | expect(replaceRuleDirectiveComment(comment, 'Block', {})).toBe(comment); 88 | } 89 | 90 | for (const comment of lineComments) { 91 | expect(replaceRuleDirectiveComment(comment, 'Line', {})).toBe(comment); 92 | } 93 | }); 94 | 95 | it('should keep eslint comments with multiple rules, where one of them is not supported', () => { 96 | const blockComments = eslintCommentsBlock.map( 97 | (prefix) => `${prefix} ${correctnessRules[0]}, unknown-rule` 98 | ); 99 | 100 | const lineComments = eslintCommentsLines.map( 101 | (prefix) => `${prefix} ${correctnessRules[0]}, unknown-rule` 102 | ); 103 | 104 | for (const comment of blockComments) { 105 | expect(replaceRuleDirectiveComment(comment, 'Block', {})).toBe(comment); 106 | } 107 | 108 | for (const comment of lineComments) { 109 | expect(replaceRuleDirectiveComment(comment, 'Line', {})).toBe(comment); 110 | } 111 | }); 112 | 113 | it('should keep eslint comments with multiple rules, where the comma between is missing', () => { 114 | const blockComments = eslintCommentsBlock.map( 115 | // the comma is missing here, it will not count as an valid comment 116 | // __________________________________________v 117 | (prefix) => `${prefix} ${correctnessRules[0]} ${correctnessRules[1]}` 118 | ); 119 | 120 | const lineComments = eslintCommentsLines.map( 121 | (prefix) => `${prefix} ${correctnessRules[0]} ${correctnessRules[1]}` 122 | ); 123 | 124 | for (const comment of blockComments) { 125 | expect(replaceRuleDirectiveComment(comment, 'Block', {})).toBe(comment); 126 | } 127 | 128 | for (const comment of lineComments) { 129 | expect(replaceRuleDirectiveComment(comment, 'Line', {})).toBe(comment); 130 | } 131 | }); 132 | }); 133 | 134 | describe('touched comments', () => { 135 | it('should replace single rule', () => { 136 | const blockComment = eslintCommentsBlock.map( 137 | (prefix) => `${prefix} ${correctnessRules[0]}` 138 | ); 139 | 140 | const lineComment = eslintCommentsLines.map( 141 | (prefix) => `${prefix} ${correctnessRules[0]}` 142 | ); 143 | 144 | for (const comment of blockComment) { 145 | const newComment = replaceRuleDirectiveComment(comment, 'Block', {}); 146 | expect(comment).toContain('eslint-'); 147 | expect(newComment).toBe(comment.replace('eslint', 'oxlint')); 148 | } 149 | 150 | for (const comment of lineComment) { 151 | const newComment = replaceRuleDirectiveComment(comment, 'Line', {}); 152 | expect(comment).toContain('eslint-'); 153 | expect(newComment).toBe(comment.replace('eslint', 'oxlint')); 154 | } 155 | }); 156 | 157 | it('should replace multiple rules', () => { 158 | const blockComments = eslintCommentsBlock.map( 159 | (prefix) => `${prefix} ${correctnessRules[0]}, ${correctnessRules[1]}` 160 | ); 161 | 162 | const lineComments = eslintCommentsLines.map( 163 | (prefix) => `${prefix} ${correctnessRules[0]}, ${correctnessRules[1]}` 164 | ); 165 | 166 | for (const comment of blockComments) { 167 | expect(replaceRuleDirectiveComment(comment, 'Block', {})).toBe( 168 | comment.replace('eslint', 'oxlint') 169 | ); 170 | } 171 | 172 | for (const comment of lineComments) { 173 | expect(replaceRuleDirectiveComment(comment, 'Line', {})).toBe( 174 | comment.replace('eslint', 'oxlint') 175 | ); 176 | } 177 | }); 178 | }); 179 | 180 | describe('withNurseryCheck', () => { 181 | it('should ignore nursery rules on default', () => { 182 | const blockComments = eslintCommentsBlock.map( 183 | (prefix) => `${prefix} ${nurseryRules[0]}` 184 | ); 185 | for (const comment of blockComments) { 186 | expect(replaceRuleDirectiveComment(comment, 'Block', {})).toBe(comment); 187 | } 188 | }); 189 | 190 | it('should respect nursery rules with `options.withNursery`', () => { 191 | const blockComments = eslintCommentsBlock.map( 192 | (prefix) => `${prefix} ${nurseryRules[0]}` 193 | ); 194 | 195 | for (const comment of blockComments) { 196 | expect( 197 | replaceRuleDirectiveComment(comment, 'Block', { 198 | withNursery: true, 199 | }) 200 | ).toBe(comment.replace('eslint', 'oxlint')); 201 | } 202 | }); 203 | }); 204 | }); 205 | -------------------------------------------------------------------------------- /src/walker/partialSourceTextLoader.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { 3 | partialAstroSourceTextLoader, 4 | partialSvelteSourceTextLoader, 5 | partialVueSourceTextLoader, 6 | } from './partialSourceTextLoader.js'; 7 | 8 | /** 9 | * Tests copied from oxc: 10 | * @link https://github.com/oxc-project/oxc/blob/dfe54b44ff9815e793bf61a5dd966a146998d81a/crates/oxc_linter/src/loader/partial_loader/vue.rs 11 | */ 12 | describe('partialVueSourceTextLoader', () => { 13 | it('should parse simple script tag', () => { 14 | const sourceText = ` 15 | 16 | `; 17 | const result = partialVueSourceTextLoader(sourceText); 18 | 19 | expect(result).toStrictEqual([ 20 | { 21 | lang: undefined, 22 | sourceText: 'debugger;', 23 | offset: 59, 24 | }, 25 | ]); 26 | }); 27 | 28 | it('should parse generics script tag', () => { 29 | const sourceText = ` 30 | 31 | `; 32 | const result = partialVueSourceTextLoader(sourceText); 33 | 34 | expect(result).toStrictEqual([ 35 | { 36 | lang: 'ts', 37 | sourceText: 'debugger;', 38 | offset: 118, 39 | }, 40 | ]); 41 | }); 42 | 43 | it('should parse script tag with ">" inside attribute', () => { 44 | const sourceText = ` 45 | 46 | `; 47 | const result = partialVueSourceTextLoader(sourceText); 48 | 49 | expect(result).toStrictEqual([ 50 | { 51 | lang: 'ts', 52 | sourceText: 'debugger;', 53 | offset: 90, 54 | }, 55 | ]); 56 | }); 57 | 58 | it('should return empty array when no script found', () => { 59 | const sourceText = ``; 60 | const result = partialVueSourceTextLoader(sourceText); 61 | 62 | expect(result).toStrictEqual([]); 63 | }); 64 | 65 | it('should ignore script-like elements in template', () => { 66 | const sourceText = ` 67 | 68 | `; 69 | const result = partialVueSourceTextLoader(sourceText); 70 | 71 | expect(result).toStrictEqual([ 72 | { 73 | lang: undefined, 74 | sourceText: 'debugger;', 75 | offset: 60, 76 | }, 77 | ]); 78 | }); 79 | 80 | it('should parse multiple script blocks', () => { 81 | const sourceText = ` 82 | 83 | 84 | `; 85 | const result = partialVueSourceTextLoader(sourceText); 86 | 87 | expect(result).toStrictEqual([ 88 | { 89 | lang: undefined, 90 | sourceText: ' export default {} ', 91 | offset: 60, 92 | }, 93 | { 94 | lang: undefined, 95 | sourceText: 'debugger;', 96 | offset: 107, 97 | }, 98 | ]); 99 | }); 100 | }); 101 | 102 | /** 103 | * Test copied from oxc: 104 | * @link https://github.com/oxc-project/oxc/blob/fea9df4228750a38a6a2c4cc418a42a35dfb6fb8/crates/oxc_linter/src/loader/partial_loader/svelte.rs 105 | */ 106 | describe('partialSvelteSourceTextLoader', () => { 107 | it('should parse simple script tag', () => { 108 | const sourceText = ` 109 | 112 |

Hello World

`; 113 | const result = partialSvelteSourceTextLoader(sourceText); 114 | expect(result).toStrictEqual([ 115 | { 116 | lang: undefined, 117 | sourceText: '\n console.log("hi");\n ', 118 | offset: 12, 119 | }, 120 | ]); 121 | }); 122 | 123 | it('should parse generics script tag', () => { 124 | const sourceText = ` 125 | 128 |

hello world

`; 129 | const result = partialSvelteSourceTextLoader(sourceText); 130 | 131 | expect(result).toStrictEqual([ 132 | { 133 | lang: 'ts', 134 | sourceText: '\n console.log("hi");\n ', 135 | offset: 72, 136 | }, 137 | ]); 138 | }); 139 | 140 | it('should parse both script tags (context module)', () => { 141 | const sourceText = ` 142 | 145 | 148 |

hello world

`; 149 | const result = partialSvelteSourceTextLoader(sourceText); 150 | expect(result).toStrictEqual([ 151 | { 152 | lang: undefined, 153 | sourceText: "\n export const foo = 'bar';\n ", 154 | offset: 30, 155 | }, 156 | { 157 | lang: undefined, 158 | sourceText: '\n console.log("hi");\n ', 159 | offset: 89, 160 | }, 161 | ]); 162 | }); 163 | }); 164 | 165 | /** 166 | * Test copied from oxc: 167 | * @link https://github.com/oxc-project/oxc/blob/fea9df4228750a38a6a2c4cc418a42a35dfb6fb8/crates/oxc_linter/src/loader/partial_loader/astro.rs 168 | */ 169 | describe('partialAstroSourceTextLoader', () => { 170 | it('should parse simple script tag', () => { 171 | const sourceText = ` 172 |

Welcome, world!

173 | `; 174 | const result = partialAstroSourceTextLoader(sourceText); 175 | expect(result).toStrictEqual([ 176 | { 177 | lang: 'ts', 178 | sourceType: 'module', 179 | sourceText: 'console.log("Hi");', 180 | offset: 42, 181 | }, 182 | ]); 183 | }); 184 | 185 | it('should parse frontmatter', () => { 186 | const sourceText = ` 187 | --- 188 | const { message = 'Welcome, world!' } = Astro.props; 189 | --- 190 | 191 |

Welcome, world!

192 | 193 | `; 196 | const result = partialAstroSourceTextLoader(sourceText); 197 | expect(result).toStrictEqual([ 198 | { 199 | lang: 'ts', 200 | sourceType: 'module', 201 | sourceText: 202 | "\n const { message = 'Welcome, world!' } = Astro.props;\n ", 203 | offset: 8, 204 | }, 205 | { 206 | lang: 'ts', 207 | sourceType: 'module', 208 | sourceText: '\n console.log("Hi");\n ', 209 | offset: 117, 210 | }, 211 | ]); 212 | }); 213 | 214 | it('should parse with inline script', () => { 215 | const sourceText = ` 216 |

Welcome, world!

217 | 218 | 219 | 220 | `; 223 | 224 | const result = partialAstroSourceTextLoader(sourceText); 225 | expect(result).toStrictEqual([ 226 | { 227 | lang: 'ts', 228 | sourceType: 'module', 229 | sourceText: '', 230 | offset: 94, 231 | }, 232 | { 233 | lang: 'ts', 234 | sourceType: 'module', 235 | sourceText: '\n console.log("Hi");\n ', 236 | offset: 117, 237 | }, 238 | ]); 239 | }); 240 | 241 | it('should parse with self closing script tag', () => { 242 | const sourceText = ` 243 |

Welcome, world!

244 | 245 | `; 250 | 251 | const result = partialAstroSourceTextLoader(sourceText); 252 | expect(result).toStrictEqual([ 253 | { 254 | lang: 'ts', 255 | sourceType: 'module', 256 | sourceText: '\n console.log("Hi");\n ', 257 | offset: 116, 258 | }, 259 | ]); 260 | }); 261 | }); 262 | -------------------------------------------------------------------------------- /src/cleanup.ts: -------------------------------------------------------------------------------- 1 | import { 2 | cleanUpSupersetEnvs, 3 | cleanUpUselessOverridesEnv, 4 | ES_VERSIONS, 5 | removeGlobalsWithAreCoveredByEnv, 6 | transformBoolGlobalToString, 7 | } from './env_globals.js'; 8 | import { 9 | cleanUpDisabledRootRules, 10 | cleanUpRulesWhichAreCoveredByCategory, 11 | cleanUpUselessOverridesPlugins, 12 | cleanUpUselessOverridesRules, 13 | replaceNodePluginName, 14 | replaceTypescriptAliasRules, 15 | } from './plugins_rules.js'; 16 | import { 17 | OxlintConfigOrOverride, 18 | OxlintConfig, 19 | OxlintConfigOverride, 20 | } from './types.js'; 21 | import { isEqualDeep } from './utilities.js'; 22 | 23 | const TS_ESLINT_DEFAULT_OVERRIDE: OxlintConfigOverride = { 24 | files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'], 25 | rules: { 26 | 'no-class-assign': 'off', 27 | 'no-const-assign': 'off', 28 | 'no-dupe-class-members': 'off', 29 | 'no-dupe-keys': 'off', 30 | 'no-func-assign': 'off', 31 | 'no-import-assign': 'off', 32 | 'no-new-native-nonconstructor': 'off', 33 | 'no-obj-calls': 'off', 34 | 'no-redeclare': 'off', 35 | 'no-setter-return': 'off', 36 | 'no-this-before-super': 'off', 37 | 'no-unsafe-negation': 'off', 38 | 'no-var': 'error', 39 | 'prefer-rest-params': 'error', 40 | 'prefer-spread': 'error', 41 | }, 42 | }; 43 | 44 | const cleanUpDefaultTypeScriptOverridesForEslint = ( 45 | config: OxlintConfig 46 | ): void => { 47 | if (config.overrides === undefined) { 48 | return; 49 | } 50 | 51 | for (const [index, override] of config.overrides.entries()) { 52 | if (isEqualDeep(override, TS_ESLINT_DEFAULT_OVERRIDE)) { 53 | delete config.overrides[index]; 54 | } 55 | } 56 | 57 | config.overrides = config.overrides.filter( 58 | (overrides) => Object.keys(overrides).length > 0 59 | ); 60 | 61 | if (Object.keys(config.overrides).length === 0) { 62 | delete config.overrides; 63 | } 64 | }; 65 | 66 | const cleanUpUselessOverridesEntries = (config: OxlintConfig): void => { 67 | cleanUpDefaultTypeScriptOverridesForEslint(config); 68 | cleanUpUselessOverridesRules(config); 69 | cleanUpUselessOverridesPlugins(config); 70 | cleanUpUselessOverridesEnv(config); 71 | cleanUpSupersetEnvs(config); 72 | 73 | if (config.overrides === undefined) { 74 | return; 75 | } 76 | 77 | for (const [overrideIndex, override] of config.overrides.entries()) { 78 | // If there's only one key left, it can be deleted. An override needs files+plugins or files+rules to make sense. 79 | if (Object.keys(override).length === 1) { 80 | delete config.overrides[overrideIndex]; 81 | } 82 | } 83 | 84 | config.overrides = config.overrides.filter( 85 | (overrides) => Object.keys(overrides).length > 0 86 | ); 87 | 88 | // Merge consecutive identical overrides to avoid redundancy 89 | mergeConsecutiveIdenticalOverrides(config); 90 | mergeConsecutiveOverridesWithDifferingFiles(config); 91 | 92 | if (config.overrides.length === 0) { 93 | delete config.overrides; 94 | } 95 | }; 96 | 97 | export const cleanUpOxlintConfig = (config: OxlintConfigOrOverride): void => { 98 | removeGlobalsWithAreCoveredByEnv(config); 99 | transformBoolGlobalToString(config); 100 | replaceTypescriptAliasRules(config); 101 | replaceNodePluginName(config); 102 | cleanUpRulesWhichAreCoveredByCategory(config); 103 | 104 | // no entries in globals, we can remove the globals key 105 | if ( 106 | config.globals !== undefined && 107 | Object.keys(config.globals).length === 0 108 | ) { 109 | delete config.globals; 110 | } 111 | 112 | if (config.env !== undefined) { 113 | // these are not supported by oxlint 114 | delete config.env.es3; 115 | delete config.env.es5; 116 | delete config.env.es2015; 117 | 118 | let detected = false; 119 | // remove older es versions, 120 | // because newer ones are always a superset of them 121 | for (const esVersion of [...ES_VERSIONS].reverse()) { 122 | if (detected) { 123 | delete config.env[`es${esVersion}`]; 124 | } else if (config.env[`es${esVersion}`] === true) { 125 | detected = true; 126 | } 127 | } 128 | } 129 | 130 | if (!('files' in config)) { 131 | cleanUpUselessOverridesEntries(config); 132 | cleanUpDisabledRootRules(config); 133 | } 134 | }; 135 | 136 | /** 137 | * Merges consecutive identical overrides in the config's overrides array 138 | * Merges only if the overrides are directly next to each other 139 | * (otherwise they could be overriden in between one another). 140 | * 141 | * Example: 142 | * 143 | * ```json 144 | * overrides: [ 145 | * { 146 | * "files": [ 147 | * "*.ts", 148 | * "*.tsx", 149 | * ], 150 | * "plugins": [ 151 | * "typescript", 152 | * ], 153 | * }, 154 | * { 155 | * "files": [ 156 | * "*.ts", 157 | * "*.tsx", 158 | * ], 159 | * "plugins": [ 160 | * "typescript", 161 | * ], 162 | * }, 163 | * ] 164 | * ``` 165 | */ 166 | function mergeConsecutiveIdenticalOverrides(config: OxlintConfig) { 167 | if (config.overrides === undefined) { 168 | return; 169 | } 170 | if (config.overrides.length <= 1) { 171 | return; 172 | } 173 | 174 | const mergedOverrides: OxlintConfigOverride[] = []; 175 | let i = 0; 176 | 177 | while (i < config.overrides.length) { 178 | const current = config.overrides[i]; 179 | 180 | // Check if the next override is identical to the current one 181 | if ( 182 | i + 1 < config.overrides.length && 183 | isEqualDeep(current, config.overrides[i + 1]) 184 | ) { 185 | // Skip duplicates - just add the first one 186 | mergedOverrides.push(current); 187 | // Skip all consecutive duplicates 188 | while ( 189 | i + 1 < config.overrides.length && 190 | isEqualDeep(current, config.overrides[i + 1]) 191 | ) { 192 | i++; 193 | } 194 | } else { 195 | mergedOverrides.push(current); 196 | } 197 | 198 | i++; 199 | } 200 | 201 | config.overrides = mergedOverrides; 202 | } 203 | 204 | /** 205 | * Merge consecutive overrides that have differing files but everything else is identical. 206 | * 207 | * ```json 208 | * "overrides": [ 209 | * { 210 | * "files": [ 211 | * "*.ts", 212 | * ], 213 | * "rules": { 214 | * "arrow-body-style": "error", 215 | * }, 216 | * }, 217 | * { 218 | * "files": [ 219 | * "*.mts", 220 | * "*.cts", 221 | * ], 222 | * "rules": { 223 | * "arrow-body-style": "error", 224 | * }, 225 | * }, 226 | * ], 227 | * ``` 228 | */ 229 | function mergeConsecutiveOverridesWithDifferingFiles(config: OxlintConfig) { 230 | if (config.overrides === undefined) { 231 | return; 232 | } 233 | if (config.overrides.length <= 1) { 234 | return; 235 | } 236 | 237 | const mergedOverrides: OxlintConfigOverride[] = []; 238 | let i = 0; 239 | 240 | while (i < config.overrides.length) { 241 | const current = config.overrides[i]; 242 | const currentFiles = current.files; 243 | const { files: _, ...currentWithoutFiles } = current; 244 | 245 | // Look ahead to find consecutive overrides with same properties (except files) 246 | let j = i + 1; 247 | const filesToMerge: string[] = [...currentFiles]; 248 | 249 | while (j < config.overrides.length) { 250 | const next = config.overrides[j]; 251 | const { files: __, ...nextWithoutFiles } = next; 252 | 253 | // Check if everything except files is identical 254 | if (isEqualDeep(currentWithoutFiles, nextWithoutFiles)) { 255 | // Merge the files 256 | filesToMerge.push(...next.files); 257 | j++; 258 | } else { 259 | break; 260 | } 261 | } 262 | 263 | // Create the merged override 264 | if (j > i + 1) { 265 | // We found overrides to merge 266 | // Deduplicate the files array 267 | const uniqueFiles = [...new Set(filesToMerge)]; 268 | mergedOverrides.push({ 269 | ...current, 270 | files: uniqueFiles, 271 | }); 272 | i = j; 273 | } else { 274 | // No merge, keep as is 275 | mergedOverrides.push(current); 276 | i++; 277 | } 278 | } 279 | 280 | config.overrides = mergedOverrides; 281 | } 282 | -------------------------------------------------------------------------------- /src/walker/replaceCommentsInFile.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import replaceCommentsInFile from './replaceCommentsInFile.js'; 3 | import { DefaultReporter } from '../reporter.js'; 4 | 5 | describe('replaceCommentsInFile', () => { 6 | const tsPath = '/tmp/fake-path.ts'; 7 | const vuePath = '/tmp/fake-path.vue'; 8 | const astroPath = '/tmp/fake-path.astro'; 9 | const sveltePath = '/tmp/fake-path.svelte'; 10 | 11 | it('should replace multiple line comments', () => { 12 | const sourceText = ` 13 | // eslint-disable no-debugger 14 | debugger; 15 | // eslint-disable no-console -- description 16 | console.log('hello world'); 17 | `; 18 | 19 | const newSourceText = replaceCommentsInFile(tsPath, sourceText, {}); 20 | 21 | expect(newSourceText).toBe(` 22 | // oxlint-disable no-debugger 23 | debugger; 24 | // oxlint-disable no-console -- description 25 | console.log('hello world'); 26 | `); 27 | }); 28 | 29 | it('should replace multiple block comments', () => { 30 | const sourceText = ` 31 | /* eslint-disable no-debugger */ 32 | debugger; 33 | /* eslint-disable no-console -- description */ 34 | console.log('hello world'); 35 | `; 36 | 37 | const newSourceText = replaceCommentsInFile(tsPath, sourceText, {}); 38 | 39 | expect(newSourceText).toBe(` 40 | /* oxlint-disable no-debugger */ 41 | debugger; 42 | /* oxlint-disable no-console -- description */ 43 | console.log('hello world'); 44 | `); 45 | }); 46 | 47 | it('should replace respect line breaks ', () => { 48 | const sourceText = ` 49 | /* 50 | eslint-disable no-debugger 51 | */ 52 | debugger; 53 | /* eslint-disable no-console -- description */ 54 | 55 | console.log('hello world'); 56 | `; 57 | 58 | const newSourceText = replaceCommentsInFile(tsPath, sourceText, {}); 59 | 60 | expect(newSourceText).toBe(` 61 | /* 62 | oxlint-disable no-debugger 63 | */ 64 | debugger; 65 | /* oxlint-disable no-console -- description */ 66 | 67 | console.log('hello world'); 68 | `); 69 | }); 70 | 71 | describe('unsupported eslint comments', () => { 72 | it('should report inline configuration comments', () => { 73 | const sourceText = ` 74 | /* eslint eqeqeq: "off" */ 75 | debugger; 76 | /* eslint eqeqeq: "off" -- description */ 77 | console.log('hello world'); 78 | `; 79 | const reporter = new DefaultReporter(); 80 | const newSourceText = replaceCommentsInFile(tsPath, sourceText, { 81 | reporter: reporter, 82 | }); 83 | expect(newSourceText).toBe(sourceText); 84 | expect(reporter.getReports()).toStrictEqual([ 85 | '/tmp/fake-path.ts, char offset 9: changing eslint rules with inline comment is not supported', 86 | '/tmp/fake-path.ts, char offset 62: changing eslint rules with inline comment is not supported', 87 | ]); 88 | }); 89 | 90 | it('should report inline global configuration', () => { 91 | const sourceText = ` 92 | /* global jQuery */ 93 | debugger; 94 | /* global jQuery -- description */ 95 | console.log('hello world'); 96 | `; 97 | const reporter = new DefaultReporter(); 98 | const newSourceText = replaceCommentsInFile(tsPath, sourceText, { 99 | reporter: reporter, 100 | }); 101 | expect(newSourceText).toBe(sourceText); 102 | expect(reporter.getReports()).toStrictEqual([ 103 | '/tmp/fake-path.ts, char offset 9: changing globals with inline comment is not supported', 104 | '/tmp/fake-path.ts, char offset 55: changing globals with inline comment is not supported', 105 | ]); 106 | }); 107 | 108 | it('should ignore standalone eslint comment', () => { 109 | const sourceText = ` 110 | /* eslint */ 111 | debugger; 112 | `; 113 | const reporter = new DefaultReporter(); 114 | const newSourceText = replaceCommentsInFile(tsPath, sourceText, { 115 | reporter: reporter, 116 | }); 117 | expect(newSourceText).toBe(sourceText); 118 | expect(reporter.getReports()).toStrictEqual([]); 119 | }); 120 | }); 121 | 122 | describe('special file extensions', () => { 123 | it('should handle .vue files', () => { 124 | const sourceText = ` 125 | 126 | 130 | 134 | `; 135 | const newSourceText = replaceCommentsInFile(vuePath, sourceText, {}); 136 | expect(newSourceText).toBe(` 137 | 138 | 142 | 146 | `); 147 | }); 148 | 149 | it('should handle typescript syntax in .vue files', () => { 150 | const sourceText = ` 151 | 156 | `; 157 | const newSourceText = replaceCommentsInFile(sveltePath, sourceText, {}); 158 | expect(newSourceText).toBe(` 159 | 164 | `); 165 | }); 166 | 167 | it('should handle .astro files', () => { 168 | const sourceText = ` 169 | --- 170 | /* eslint-disable no-debugger */ 171 | debugger; 172 | --- 173 | 177 |

Hello World!

178 | `; 179 | const newSourceText = replaceCommentsInFile(astroPath, sourceText, {}); 180 | expect(newSourceText).toBe(` 181 | --- 182 | /* oxlint-disable no-debugger */ 183 | debugger; 184 | --- 185 | 189 |

Hello World!

190 | `); 191 | }); 192 | 193 | it('should handle .svelte files', () => { 194 | const sourceText = ` 195 | 199 | 203 |
Hello Svelte
204 | `; 205 | const newSourceText = replaceCommentsInFile(sveltePath, sourceText, {}); 206 | expect(newSourceText).toBe(` 207 | 211 | 215 |
Hello Svelte
216 | `); 217 | }); 218 | 219 | it('should handle typescript syntax in .svelte files', () => { 220 | const sourceText = ` 221 | 227 | 231 |
Hello Svelte
232 | `; 233 | const newSourceText = replaceCommentsInFile(sveltePath, sourceText, {}); 234 | expect(newSourceText).toBe(` 235 | 241 | 245 |
Hello Svelte
246 | `); 247 | }); 248 | }); 249 | }); 250 | -------------------------------------------------------------------------------- /integration_test/__snapshots__/react-project.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`react-project > react-project 1`] = ` 4 | { 5 | "config": { 6 | "$schema": "./node_modules/oxlint/configuration_schema.json", 7 | "categories": { 8 | "correctness": "off", 9 | }, 10 | "env": { 11 | "builtin": true, 12 | }, 13 | "plugins": [ 14 | "react", 15 | "react-perf", 16 | ], 17 | "rules": { 18 | "react-hooks/exhaustive-deps": "warn", 19 | "react-hooks/rules-of-hooks": "error", 20 | "react-perf/jsx-no-new-array-as-prop": "error", 21 | "react-perf/jsx-no-new-function-as-prop": "error", 22 | "react-perf/jsx-no-new-object-as-prop": "error", 23 | "react/jsx-key": "error", 24 | "react/jsx-no-comment-textnodes": "error", 25 | "react/jsx-no-duplicate-props": "error", 26 | "react/jsx-no-target-blank": "error", 27 | "react/jsx-no-undef": "error", 28 | "react/no-children-prop": "error", 29 | "react/no-danger-with-children": "error", 30 | "react/no-direct-mutation-state": "error", 31 | "react/no-find-dom-node": "error", 32 | "react/no-is-mounted": "error", 33 | "react/no-render-return-value": "error", 34 | "react/no-string-refs": "error", 35 | "react/no-unescaped-entities": "error", 36 | "react/no-unknown-property": "error", 37 | }, 38 | }, 39 | "warnings": [ 40 | "unsupported rule: react/display-name", 41 | "unsupported rule: react/jsx-uses-vars", 42 | "unsupported rule: react/no-deprecated", 43 | "unsupported rule: react/prop-types", 44 | "unsupported rule, but available as a nursery rule: react/require-render-return", 45 | "unsupported rule: react-hooks/static-components", 46 | "unsupported rule: react-hooks/use-memo", 47 | "unsupported rule: react-hooks/void-use-memo", 48 | "unsupported rule: react-hooks/component-hook-factories", 49 | "unsupported rule: react-hooks/preserve-manual-memoization", 50 | "unsupported rule: react-hooks/incompatible-library", 51 | "unsupported rule: react-hooks/immutability", 52 | "unsupported rule: react-hooks/globals", 53 | "unsupported rule: react-hooks/refs", 54 | "unsupported rule: react-hooks/set-state-in-effect", 55 | "unsupported rule: react-hooks/error-boundaries", 56 | "unsupported rule: react-hooks/purity", 57 | "unsupported rule: react-hooks/set-state-in-render", 58 | "unsupported rule: react-hooks/unsupported-syntax", 59 | "unsupported rule: react-hooks/config", 60 | "unsupported rule: react-hooks/gating", 61 | ], 62 | } 63 | `; 64 | 65 | exports[`react-project --js-plugins > react-project--js-plugins 1`] = ` 66 | { 67 | "config": { 68 | "$schema": "./node_modules/oxlint/configuration_schema.json", 69 | "categories": { 70 | "correctness": "off", 71 | }, 72 | "env": { 73 | "builtin": true, 74 | }, 75 | "plugins": [ 76 | "react", 77 | "react-perf", 78 | ], 79 | "rules": { 80 | "react-hooks/exhaustive-deps": "warn", 81 | "react-hooks/rules-of-hooks": "error", 82 | "react-perf/jsx-no-new-array-as-prop": "error", 83 | "react-perf/jsx-no-new-function-as-prop": "error", 84 | "react-perf/jsx-no-new-object-as-prop": "error", 85 | "react/jsx-key": "error", 86 | "react/jsx-no-comment-textnodes": "error", 87 | "react/jsx-no-duplicate-props": "error", 88 | "react/jsx-no-target-blank": "error", 89 | "react/jsx-no-undef": "error", 90 | "react/no-children-prop": "error", 91 | "react/no-danger-with-children": "error", 92 | "react/no-direct-mutation-state": "error", 93 | "react/no-find-dom-node": "error", 94 | "react/no-is-mounted": "error", 95 | "react/no-render-return-value": "error", 96 | "react/no-string-refs": "error", 97 | "react/no-unescaped-entities": "error", 98 | "react/no-unknown-property": "error", 99 | }, 100 | }, 101 | "warnings": [ 102 | "unsupported rule: react/display-name", 103 | "unsupported rule: react/jsx-uses-vars", 104 | "unsupported rule: react/no-deprecated", 105 | "unsupported rule: react/prop-types", 106 | "unsupported rule, but available as a nursery rule: react/require-render-return", 107 | "unsupported rule: react-hooks/static-components", 108 | "unsupported rule: react-hooks/use-memo", 109 | "unsupported rule: react-hooks/void-use-memo", 110 | "unsupported rule: react-hooks/component-hook-factories", 111 | "unsupported rule: react-hooks/preserve-manual-memoization", 112 | "unsupported rule: react-hooks/incompatible-library", 113 | "unsupported rule: react-hooks/immutability", 114 | "unsupported rule: react-hooks/globals", 115 | "unsupported rule: react-hooks/refs", 116 | "unsupported rule: react-hooks/set-state-in-effect", 117 | "unsupported rule: react-hooks/error-boundaries", 118 | "unsupported rule: react-hooks/purity", 119 | "unsupported rule: react-hooks/set-state-in-render", 120 | "unsupported rule: react-hooks/unsupported-syntax", 121 | "unsupported rule: react-hooks/config", 122 | "unsupported rule: react-hooks/gating", 123 | ], 124 | } 125 | `; 126 | 127 | exports[`react-project --type-aware > react-project--type-aware 1`] = ` 128 | { 129 | "config": { 130 | "$schema": "./node_modules/oxlint/configuration_schema.json", 131 | "categories": { 132 | "correctness": "off", 133 | }, 134 | "env": { 135 | "builtin": true, 136 | }, 137 | "plugins": [ 138 | "react", 139 | "react-perf", 140 | ], 141 | "rules": { 142 | "react-hooks/exhaustive-deps": "warn", 143 | "react-hooks/rules-of-hooks": "error", 144 | "react-perf/jsx-no-new-array-as-prop": "error", 145 | "react-perf/jsx-no-new-function-as-prop": "error", 146 | "react-perf/jsx-no-new-object-as-prop": "error", 147 | "react/jsx-key": "error", 148 | "react/jsx-no-comment-textnodes": "error", 149 | "react/jsx-no-duplicate-props": "error", 150 | "react/jsx-no-target-blank": "error", 151 | "react/jsx-no-undef": "error", 152 | "react/no-children-prop": "error", 153 | "react/no-danger-with-children": "error", 154 | "react/no-direct-mutation-state": "error", 155 | "react/no-find-dom-node": "error", 156 | "react/no-is-mounted": "error", 157 | "react/no-render-return-value": "error", 158 | "react/no-string-refs": "error", 159 | "react/no-unescaped-entities": "error", 160 | "react/no-unknown-property": "error", 161 | }, 162 | }, 163 | "warnings": [ 164 | "unsupported rule: react/display-name", 165 | "unsupported rule: react/jsx-uses-vars", 166 | "unsupported rule: react/no-deprecated", 167 | "unsupported rule: react/prop-types", 168 | "unsupported rule, but available as a nursery rule: react/require-render-return", 169 | "unsupported rule: react-hooks/static-components", 170 | "unsupported rule: react-hooks/use-memo", 171 | "unsupported rule: react-hooks/void-use-memo", 172 | "unsupported rule: react-hooks/component-hook-factories", 173 | "unsupported rule: react-hooks/preserve-manual-memoization", 174 | "unsupported rule: react-hooks/incompatible-library", 175 | "unsupported rule: react-hooks/immutability", 176 | "unsupported rule: react-hooks/globals", 177 | "unsupported rule: react-hooks/refs", 178 | "unsupported rule: react-hooks/set-state-in-effect", 179 | "unsupported rule: react-hooks/error-boundaries", 180 | "unsupported rule: react-hooks/purity", 181 | "unsupported rule: react-hooks/set-state-in-render", 182 | "unsupported rule: react-hooks/unsupported-syntax", 183 | "unsupported rule: react-hooks/config", 184 | "unsupported rule: react-hooks/gating", 185 | ], 186 | } 187 | `; 188 | 189 | exports[`react-project merge > react-project--merge 1`] = ` 190 | { 191 | "config": { 192 | "$schema": "./node_modules/oxlint/configuration_schema.json", 193 | "categories": { 194 | "correctness": "error", 195 | "perf": "error", 196 | }, 197 | "env": { 198 | "builtin": true, 199 | }, 200 | "plugins": [ 201 | "react", 202 | "react-perf", 203 | ], 204 | "rules": { 205 | "react-hooks/exhaustive-deps": "warn", 206 | "react-hooks/rules-of-hooks": "error", 207 | "react/jsx-no-comment-textnodes": "error", 208 | "react/jsx-no-target-blank": "error", 209 | "react/no-unescaped-entities": "error", 210 | "react/no-unknown-property": "error", 211 | "react/react-in-jsx-scope": "error", 212 | }, 213 | }, 214 | "warnings": [ 215 | "unsupported rule: react/display-name", 216 | "unsupported rule: react/jsx-uses-vars", 217 | "unsupported rule: react/no-deprecated", 218 | "unsupported rule: react/prop-types", 219 | "unsupported rule, but available as a nursery rule: react/require-render-return", 220 | "unsupported rule: react-hooks/static-components", 221 | "unsupported rule: react-hooks/use-memo", 222 | "unsupported rule: react-hooks/void-use-memo", 223 | "unsupported rule: react-hooks/component-hook-factories", 224 | "unsupported rule: react-hooks/preserve-manual-memoization", 225 | "unsupported rule: react-hooks/incompatible-library", 226 | "unsupported rule: react-hooks/immutability", 227 | "unsupported rule: react-hooks/globals", 228 | "unsupported rule: react-hooks/refs", 229 | "unsupported rule: react-hooks/set-state-in-effect", 230 | "unsupported rule: react-hooks/error-boundaries", 231 | "unsupported rule: react-hooks/purity", 232 | "unsupported rule: react-hooks/set-state-in-render", 233 | "unsupported rule: react-hooks/unsupported-syntax", 234 | "unsupported rule: react-hooks/config", 235 | "unsupported rule: react-hooks/gating", 236 | ], 237 | } 238 | `; 239 | -------------------------------------------------------------------------------- /integration_test/projects/typescript.eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import eslint from '@eslint/js'; 3 | import * as regexpPlugin from 'eslint-plugin-regexp'; 4 | import globals from 'globals'; 5 | import path from 'path'; 6 | import tseslint from 'typescript-eslint'; 7 | import url from 'url'; 8 | 9 | const __filename = url.fileURLToPath(new URL(import.meta.url)); 10 | const __dirname = path.dirname(__filename); 11 | 12 | const ruleFiles = []; 13 | 14 | export default tseslint.config( 15 | { 16 | files: ['**/*.{ts,tsx,cts,mts,js,cjs,mjs}'], 17 | }, 18 | { 19 | ignores: [ 20 | '**/node_modules/**', 21 | 'built/**', 22 | 'tests/**', 23 | 'lib/**', 24 | 'src/lib/*.generated.d.ts', 25 | 'scripts/**/*.js', 26 | 'scripts/**/*.d.*', 27 | 'internal/**', 28 | 'coverage/**', 29 | ], 30 | }, 31 | eslint.configs.recommended, 32 | ...tseslint.configs.recommended, 33 | ...tseslint.configs.stylistic, 34 | regexpPlugin.configs['flat/recommended'], 35 | { 36 | plugins: { 37 | local: { 38 | rules: Object.fromEntries(ruleFiles), 39 | }, 40 | }, 41 | }, 42 | { 43 | languageOptions: { 44 | parserOptions: { 45 | warnOnUnsupportedTypeScriptVersion: false, 46 | }, 47 | globals: globals.node, 48 | }, 49 | }, 50 | { 51 | rules: { 52 | // eslint 53 | 'dot-notation': 'error', 54 | eqeqeq: 'error', 55 | 'no-caller': 'error', 56 | 'no-constant-condition': ['error', { checkLoops: false }], 57 | 'no-eval': 'error', 58 | 'no-extra-bind': 'error', 59 | 'no-new-func': 'error', 60 | 'no-new-wrappers': 'error', 61 | 'no-return-await': 'error', 62 | 'no-template-curly-in-string': 'error', 63 | 'no-throw-literal': 'error', 64 | 'no-undef-init': 'error', 65 | 'no-var': 'error', 66 | 'object-shorthand': 'error', 67 | 'prefer-const': 'error', 68 | 'prefer-object-spread': 'error', 69 | 'unicode-bom': ['error', 'never'], 70 | 71 | 'no-restricted-syntax': [ 72 | 'error', 73 | { 74 | selector: 'Literal[raw=null]', 75 | message: 'Avoid using null; use undefined instead.', 76 | }, 77 | { 78 | selector: 'TSNullKeyword', 79 | message: 'Avoid using null; use undefined instead.', 80 | }, 81 | ], 82 | 83 | // Enabled in eslint:recommended, but not applicable here 84 | 'no-extra-boolean-cast': 'off', 85 | 'no-case-declarations': 'off', 86 | 'no-cond-assign': 'off', 87 | 'no-control-regex': 'off', 88 | 'no-inner-declarations': 'off', 89 | 90 | // @typescript-eslint/eslint-plugin 91 | '@typescript-eslint/naming-convention': [ 92 | 'error', 93 | { 94 | selector: 'typeLike', 95 | format: ['PascalCase'], 96 | filter: { regex: '^(__String|[A-Za-z]+_[A-Za-z]+)$', match: false }, 97 | }, 98 | { 99 | selector: 'interface', 100 | format: ['PascalCase'], 101 | custom: { regex: '^I[A-Z]', match: false }, 102 | filter: { 103 | regex: '^I(Arguments|TextWriter|O([A-Z][a-z]+[A-Za-z]*)?)$', 104 | match: false, 105 | }, 106 | }, 107 | { 108 | selector: 'variable', 109 | format: ['camelCase', 'PascalCase', 'UPPER_CASE'], 110 | leadingUnderscore: 'allow', 111 | filter: { 112 | regex: '^(_{1,2}filename|_{1,2}dirname|_+|[A-Za-z]+_[A-Za-z]+)$', 113 | match: false, 114 | }, 115 | }, 116 | { 117 | selector: 'function', 118 | format: ['camelCase', 'PascalCase'], 119 | leadingUnderscore: 'allow', 120 | filter: { regex: '^[A-Za-z]+_[A-Za-z]+$', match: false }, 121 | }, 122 | { 123 | selector: 'parameter', 124 | format: ['camelCase'], 125 | leadingUnderscore: 'allow', 126 | filter: { regex: '^(_+|[A-Za-z]+_[A-Z][a-z]+)$', match: false }, 127 | }, 128 | { 129 | selector: 'method', 130 | format: ['camelCase', 'PascalCase'], 131 | leadingUnderscore: 'allow', 132 | filter: { regex: '^([0-9]+|[A-Za-z]+_[A-Za-z]+)$', match: false }, 133 | }, 134 | { 135 | selector: 'memberLike', 136 | format: ['camelCase'], 137 | leadingUnderscore: 'allow', 138 | filter: { regex: '^([0-9]+|[A-Za-z]+_[A-Za-z]+)$', match: false }, 139 | }, 140 | { 141 | selector: 'enumMember', 142 | format: ['camelCase', 'PascalCase'], 143 | leadingUnderscore: 'allow', 144 | filter: { regex: '^[A-Za-z]+_[A-Za-z]+$', match: false }, 145 | }, 146 | { selector: 'property', format: null }, 147 | ], 148 | 149 | '@typescript-eslint/unified-signatures': 'error', 150 | 'no-unused-expressions': 'off', 151 | '@typescript-eslint/no-unused-expressions': [ 152 | 'error', 153 | { allowTernary: true }, 154 | ], 155 | 156 | // Rules enabled in typescript-eslint configs that are not applicable here 157 | '@typescript-eslint/ban-ts-comment': 'off', 158 | '@typescript-eslint/class-literal-property-style': 'off', 159 | '@typescript-eslint/consistent-indexed-object-style': 'off', 160 | '@typescript-eslint/consistent-generic-constructors': 'off', 161 | '@typescript-eslint/no-duplicate-enum-values': 'off', 162 | '@typescript-eslint/no-empty-function': 'off', 163 | '@typescript-eslint/no-namespace': 'off', 164 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', 165 | '@typescript-eslint/no-var-requires': 'off', 166 | '@typescript-eslint/no-empty-interface': 'off', 167 | '@typescript-eslint/no-explicit-any': 'off', 168 | '@typescript-eslint/no-empty-object-type': 'off', // {} is a totally useful and valid type. 169 | '@typescript-eslint/no-require-imports': 'off', 170 | '@typescript-eslint/no-unused-vars': [ 171 | 'warn', 172 | { 173 | // Ignore: (solely underscores | starting with exactly one underscore) 174 | argsIgnorePattern: '^(_+$|_[^_])', 175 | varsIgnorePattern: '^(_+$|_[^_])', 176 | // Not setting an ignore pattern for caught errors; those can always be safely removed. 177 | }, 178 | ], 179 | '@typescript-eslint/no-inferrable-types': 'off', 180 | 181 | // Pending https://github.com/typescript-eslint/typescript-eslint/issues/4820 182 | '@typescript-eslint/prefer-optional-chain': 'off', 183 | 184 | // scripts/eslint/rules 185 | 'local/only-arrow-functions': [ 186 | 'error', 187 | { 188 | allowNamedFunctions: true, 189 | allowDeclarations: true, 190 | }, 191 | ], 192 | 'local/argument-trivia': 'error', 193 | 'local/no-in-operator': 'error', 194 | 'local/debug-assert': 'error', 195 | 'local/no-keywords': 'error', 196 | 'local/jsdoc-format': 'error', 197 | 'local/js-extensions': 'error', 198 | 'local/no-array-mutating-method-expressions': 'error', 199 | }, 200 | }, 201 | { 202 | files: ['**/*.mjs', '**/*.mts'], 203 | rules: { 204 | // These globals don't exist outside of CJS files. 205 | 'no-restricted-globals': [ 206 | 'error', 207 | { name: '__filename' }, 208 | { name: '__dirname' }, 209 | { name: 'require' }, 210 | { name: 'module' }, 211 | { name: 'exports' }, 212 | ], 213 | }, 214 | }, 215 | { 216 | files: ['src/**'], 217 | languageOptions: { 218 | parserOptions: { 219 | tsconfigRootDir: __dirname, 220 | project: './src/tsconfig-eslint.json', 221 | }, 222 | }, 223 | }, 224 | { 225 | files: ['scripts/**'], 226 | languageOptions: { 227 | parserOptions: { 228 | tsconfigRootDir: __dirname, 229 | project: './scripts/tsconfig.json', 230 | }, 231 | }, 232 | }, 233 | { 234 | files: ['src/**'], 235 | rules: { 236 | '@typescript-eslint/no-unnecessary-type-assertion': 'error', 237 | 'no-restricted-globals': [ 238 | 'error', 239 | { name: 'setTimeout' }, 240 | { name: 'clearTimeout' }, 241 | { name: 'setInterval' }, 242 | { name: 'clearInterval' }, 243 | { name: 'setImmediate' }, 244 | { name: 'clearImmediate' }, 245 | { name: 'performance' }, 246 | ], 247 | 'local/no-direct-import': 'error', 248 | }, 249 | }, 250 | { 251 | files: ['src/harness/**', 'src/testRunner/**'], 252 | rules: { 253 | 'no-restricted-globals': 'off', 254 | 'regexp/no-super-linear-backtracking': 'off', 255 | 'local/no-direct-import': 'off', 256 | }, 257 | }, 258 | { 259 | files: ['src/**/_namespaces/**'], 260 | rules: { 261 | 'local/no-direct-import': 'off', 262 | }, 263 | }, 264 | { 265 | files: ['src/lib/*.d.ts'], 266 | ...tseslint.configs.disableTypeChecked, 267 | }, 268 | { 269 | files: ['src/lib/*.d.ts'], 270 | languageOptions: { 271 | globals: {}, 272 | }, 273 | rules: { 274 | '@typescript-eslint/interface-name-prefix': 'off', 275 | '@typescript-eslint/prefer-function-type': 'off', 276 | '@typescript-eslint/unified-signatures': 'off', 277 | '@typescript-eslint/no-unsafe-function-type': 'off', 278 | '@typescript-eslint/no-wrapper-object-types': 'off', 279 | '@typescript-eslint/no-unused-vars': 'off', 280 | 281 | // scripts/eslint/rules 282 | 'local/no-keywords': 'off', 283 | 284 | // eslint 285 | 'no-var': 'off', 286 | 'no-restricted-globals': 'off', 287 | 'no-shadow-restricted-names': 'off', 288 | 'no-restricted-syntax': 'off', 289 | }, 290 | }, 291 | { 292 | files: ['src/lib/es2019.array.d.ts'], 293 | rules: { 294 | '@typescript-eslint/array-type': 'off', 295 | }, 296 | } 297 | ); 298 | -------------------------------------------------------------------------------- /src/env_globals.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { 3 | cleanUpSupersetEnvs, 4 | cleanUpUselessOverridesEnv, 5 | detectEnvironmentByGlobals, 6 | removeGlobalsWithAreCoveredByEnv, 7 | transformEnvAndGlobals, 8 | } from './env_globals.js'; 9 | import { OxlintConfig } from './types.js'; 10 | import globals from 'globals'; 11 | import type { Linter } from 'eslint'; 12 | 13 | describe('detectEnvironmentByGlobals', () => { 14 | test('detect es2024', () => { 15 | const config: OxlintConfig = { 16 | globals: globals.es2024, 17 | }; 18 | 19 | detectEnvironmentByGlobals(config); 20 | expect(config.env?.es2024).toBe(true); 21 | }); 22 | 23 | test('does not detect unsupported es version', () => { 24 | const config: OxlintConfig = { 25 | globals: globals.es3, 26 | }; 27 | 28 | detectEnvironmentByGlobals(config); 29 | expect(config.env).toBeUndefined(); 30 | }); 31 | 32 | test('detect browser env with >=97% match (missing a few keys)', () => { 33 | // Create a copy of browser globals and remove a few keys to simulate version differences 34 | const browserGlobals: Record = { 35 | ...globals.browser, 36 | }; 37 | const totalKeys = Object.keys(browserGlobals).length; 38 | const keysToRemove = Math.floor(totalKeys * 0.02); // Remove 2% of keys 39 | 40 | let removed = 0; 41 | for (const key in browserGlobals) { 42 | if (removed < keysToRemove) { 43 | delete browserGlobals[key]; 44 | removed++; 45 | } 46 | } 47 | 48 | const config: OxlintConfig = { 49 | globals: browserGlobals, 50 | }; 51 | 52 | detectEnvironmentByGlobals(config); 53 | expect(config.env?.browser).toBe(true); 54 | // ensure that browser is the only env detected 55 | expect(Object.keys(config.env || {}).length).toBe(1); 56 | }); 57 | 58 | test('does not detect env when match is <97%', () => { 59 | // Create a copy of browser globals and remove >3% of keys 60 | const browserGlobals: Record = { 61 | ...globals.browser, 62 | }; 63 | const totalKeys = Object.keys(browserGlobals).length; 64 | const keysToRemove = Math.floor(totalKeys * 0.04); // Remove 4% of keys 65 | 66 | let removed = 0; 67 | for (const key in browserGlobals) { 68 | if (removed < keysToRemove) { 69 | delete browserGlobals[key]; 70 | removed++; 71 | } 72 | } 73 | 74 | const config: OxlintConfig = { 75 | globals: browserGlobals, 76 | }; 77 | 78 | detectEnvironmentByGlobals(config); 79 | expect(config.env?.browser).toBeUndefined(); 80 | }); 81 | }); 82 | 83 | describe('removeGlobalsWithAreCoveredByEnv', () => { 84 | test('detect es2024', () => { 85 | const config: OxlintConfig = { 86 | env: { 87 | es2024: true, 88 | }, 89 | globals: globals.es2024, 90 | }; 91 | 92 | removeGlobalsWithAreCoveredByEnv(config); 93 | expect(config.globals).toBeUndefined(); 94 | }); 95 | }); 96 | 97 | describe('transformEnvAndGlobals', () => { 98 | test('transform languageOptions.ecmaVersion 2024 to env', () => { 99 | const eslintConfig: Linter.Config = { 100 | languageOptions: { 101 | ecmaVersion: 2024, 102 | }, 103 | }; 104 | 105 | const config: OxlintConfig = {}; 106 | transformEnvAndGlobals(eslintConfig, config); 107 | 108 | expect(config).toStrictEqual({ 109 | env: { 110 | es2024: true, 111 | }, 112 | }); 113 | }); 114 | 115 | test('transform latest languageOptions.ecmaVersion to 2026', () => { 116 | const eslintConfig: Linter.Config = { 117 | languageOptions: { 118 | ecmaVersion: 'latest', 119 | }, 120 | }; 121 | 122 | const config: OxlintConfig = {}; 123 | transformEnvAndGlobals(eslintConfig, config); 124 | 125 | expect(config).toStrictEqual({ 126 | env: { 127 | es2026: true, 128 | }, 129 | }); 130 | }); 131 | 132 | test('cleanUpUselessOverridesEnv', () => { 133 | const config: OxlintConfig = { 134 | env: { 135 | es2024: true, 136 | }, 137 | overrides: [ 138 | { 139 | files: [], 140 | env: { 141 | es2024: true, 142 | }, 143 | }, 144 | ], 145 | }; 146 | 147 | cleanUpUselessOverridesEnv(config); 148 | 149 | expect(config).toStrictEqual({ 150 | env: { 151 | es2024: true, 152 | }, 153 | overrides: [ 154 | { 155 | files: [], 156 | }, 157 | ], 158 | }); 159 | }); 160 | }); 161 | 162 | describe('cleanUpSupersetEnvs', () => { 163 | test('removes shared-node-browser when node is present', () => { 164 | const config: OxlintConfig = { 165 | env: { 166 | 'shared-node-browser': true, 167 | node: true, 168 | }, 169 | }; 170 | 171 | cleanUpSupersetEnvs(config); 172 | 173 | expect(config).toStrictEqual({ 174 | env: { 175 | node: true, 176 | }, 177 | }); 178 | }); 179 | 180 | test('does not removes shared-node-browser when node has a different value', () => { 181 | const config: OxlintConfig = { 182 | env: { 183 | 'shared-node-browser': true, 184 | node: false, 185 | }, 186 | }; 187 | 188 | cleanUpSupersetEnvs(config); 189 | 190 | expect(config).toStrictEqual({ 191 | env: { 192 | 'shared-node-browser': true, 193 | node: false, 194 | }, 195 | }); 196 | }); 197 | 198 | test('removes subset env from override when superset is in same override', () => { 199 | const config: OxlintConfig = { 200 | env: { 201 | es2024: true, 202 | }, 203 | overrides: [ 204 | { 205 | files: ['*.test.js'], 206 | env: { 207 | 'shared-node-browser': true, 208 | node: true, 209 | }, 210 | }, 211 | ], 212 | }; 213 | 214 | cleanUpSupersetEnvs(config); 215 | 216 | expect(config).toStrictEqual({ 217 | env: { 218 | es2024: true, 219 | }, 220 | overrides: [ 221 | { 222 | files: ['*.test.js'], 223 | env: { 224 | node: true, 225 | }, 226 | }, 227 | ], 228 | }); 229 | }); 230 | 231 | test('removes subset env from override when superset is in main config', () => { 232 | const config: OxlintConfig = { 233 | env: { 234 | node: true, 235 | }, 236 | overrides: [ 237 | { 238 | files: ['*.test.js'], 239 | env: { 240 | 'shared-node-browser': true, 241 | }, 242 | }, 243 | ], 244 | }; 245 | 246 | cleanUpSupersetEnvs(config); 247 | 248 | expect(config).toStrictEqual({ 249 | env: { 250 | node: true, 251 | }, 252 | overrides: [ 253 | { 254 | files: ['*.test.js'], 255 | }, 256 | ], 257 | }); 258 | }); 259 | 260 | test('keeps subset env in override when it differs from superset in main', () => { 261 | const config: OxlintConfig = { 262 | env: { 263 | node: false, 264 | }, 265 | overrides: [ 266 | { 267 | files: ['*.test.js'], 268 | env: { 269 | 'shared-node-browser': true, 270 | }, 271 | }, 272 | ], 273 | }; 274 | 275 | cleanUpSupersetEnvs(config); 276 | 277 | expect(config).toStrictEqual({ 278 | env: { 279 | node: false, 280 | }, 281 | overrides: [ 282 | { 283 | files: ['*.test.js'], 284 | env: { 285 | 'shared-node-browser': true, 286 | }, 287 | }, 288 | ], 289 | }); 290 | }); 291 | 292 | test('keeps subset env in override when superset is also in override with different value', () => { 293 | const config: OxlintConfig = { 294 | env: { 295 | es2024: true, 296 | }, 297 | overrides: [ 298 | { 299 | files: ['*.test.js'], 300 | env: { 301 | 'shared-node-browser': true, 302 | node: false, 303 | }, 304 | }, 305 | ], 306 | }; 307 | 308 | cleanUpSupersetEnvs(config); 309 | 310 | expect(config).toStrictEqual({ 311 | env: { 312 | es2024: true, 313 | }, 314 | overrides: [ 315 | { 316 | files: ['*.test.js'], 317 | env: { 318 | 'shared-node-browser': true, 319 | node: false, 320 | }, 321 | }, 322 | ], 323 | }); 324 | }); 325 | 326 | test('handles multiple overrides independently', () => { 327 | const config: OxlintConfig = { 328 | env: { 329 | node: true, 330 | }, 331 | overrides: [ 332 | { 333 | files: ['*.test.js'], 334 | env: { 335 | 'shared-node-browser': true, 336 | }, 337 | }, 338 | { 339 | files: ['*.spec.js'], 340 | env: { 341 | commonjs: true, 342 | node: true, 343 | }, 344 | }, 345 | ], 346 | }; 347 | 348 | cleanUpSupersetEnvs(config); 349 | 350 | expect(config).toStrictEqual({ 351 | env: { 352 | node: true, 353 | }, 354 | overrides: [ 355 | { 356 | files: ['*.test.js'], 357 | }, 358 | { 359 | files: ['*.spec.js'], 360 | env: { 361 | node: true, 362 | }, 363 | }, 364 | ], 365 | }); 366 | }); 367 | 368 | test('handles browser superset env in overrides', () => { 369 | const config: OxlintConfig = { 370 | env: { 371 | browser: true, 372 | }, 373 | overrides: [ 374 | { 375 | files: ['*.worker.js'], 376 | env: { 377 | 'shared-node-browser': true, 378 | }, 379 | }, 380 | ], 381 | }; 382 | 383 | cleanUpSupersetEnvs(config); 384 | 385 | expect(config).toStrictEqual({ 386 | env: { 387 | browser: true, 388 | }, 389 | overrides: [ 390 | { 391 | files: ['*.worker.js'], 392 | }, 393 | ], 394 | }); 395 | }); 396 | 397 | test('does not remove anything when no superset envs are present', () => { 398 | const config: OxlintConfig = { 399 | env: { 400 | es2024: true, 401 | }, 402 | overrides: [ 403 | { 404 | files: ['*.test.js'], 405 | env: { 406 | 'shared-node-browser': true, 407 | }, 408 | }, 409 | ], 410 | }; 411 | 412 | cleanUpSupersetEnvs(config); 413 | 414 | expect(config).toStrictEqual({ 415 | env: { 416 | es2024: true, 417 | }, 418 | overrides: [ 419 | { 420 | files: ['*.test.js'], 421 | env: { 422 | 'shared-node-browser': true, 423 | }, 424 | }, 425 | ], 426 | }); 427 | }); 428 | }); 429 | -------------------------------------------------------------------------------- /src/env_globals.ts: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import { Options, OxlintConfig, OxlintConfigOrOverride } from './types.js'; 3 | import type { Linter } from 'eslint'; 4 | 5 | // 6 | export const ES_VERSIONS = [ 7 | 6, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2025, 2026, 8 | ]; 9 | 10 | // 11 | const OTHER_SUPPORTED_ENVS = [ 12 | 'browser', 13 | 'node', 14 | 'shared-node-browser', 15 | 'worker', 16 | 'serviceworker', 17 | 18 | 'amd', 19 | 'applescript', 20 | 'astro', 21 | 'atomtest', 22 | 'commonjs', 23 | 'embertest', 24 | 'greasemonkey', 25 | 'jasmine', 26 | 'jest', 27 | 'jquery', 28 | 'meteor', 29 | 'mocha', 30 | 'mongo', 31 | 'nashorn', 32 | 'protractor', 33 | 'prototypejs', 34 | 'phantomjs', 35 | 'shelljs', 36 | 'svelte', 37 | 'webextensions', 38 | 'qunit', 39 | 'vitest', 40 | 'vue', 41 | ]; 42 | 43 | // these parsers are supported by oxlint and should not be reported 44 | const SUPPORTED_ESLINT_PARSERS = ['typescript-eslint/parser']; 45 | 46 | const normalizeGlobValue = (value: Linter.GlobalConf): boolean | undefined => { 47 | if (value === 'readable' || value === 'readonly' || value === false) { 48 | return false; 49 | } 50 | 51 | if (value === 'off') { 52 | return undefined; 53 | } 54 | 55 | return true; 56 | }; 57 | 58 | // In Eslint v9 there are no envs and all are build in with `globals` package 59 | // we look what environment is supported and remove all globals which fall under it 60 | export const removeGlobalsWithAreCoveredByEnv = ( 61 | config: OxlintConfigOrOverride 62 | ) => { 63 | if (config.globals === undefined || config.env === undefined) { 64 | return; 65 | } 66 | 67 | for (const [env, entries] of Object.entries(globals)) { 68 | if (config.env[env] === true) { 69 | for (const entry of Object.keys(entries)) { 70 | // @ts-expect-error -- filtering makes the key to any 71 | if (normalizeGlobValue(config.globals[entry]) === entries[entry]) { 72 | delete config.globals[entry]; 73 | } 74 | } 75 | } 76 | } 77 | 78 | if (Object.keys(config.globals).length === 0) { 79 | delete config.globals; 80 | } 81 | }; 82 | 83 | export const transformBoolGlobalToString = (config: OxlintConfigOrOverride) => { 84 | if (config.globals === undefined) { 85 | return; 86 | } 87 | 88 | for (const [entry, value] of Object.entries(config.globals)) { 89 | if (value === false || value === 'readable') { 90 | config.globals[entry] = 'readonly'; 91 | } else if (value === true || value === 'writeable') { 92 | config.globals[entry] = 'writable'; 93 | } 94 | } 95 | }; 96 | 97 | // Environments we want to apply a threshold match for, because they're quite large. 98 | const THRESHOLD_ENVS = ['browser', 'node', 'serviceworker', 'worker']; 99 | 100 | export const detectEnvironmentByGlobals = (config: OxlintConfigOrOverride) => { 101 | if (config.globals === undefined) { 102 | return; 103 | } 104 | 105 | for (const [env, entries] of Object.entries(globals)) { 106 | if (!env.startsWith('es') && !OTHER_SUPPORTED_ENVS.includes(env)) { 107 | continue; 108 | } 109 | 110 | // skip unsupported oxlint EcmaScript versions 111 | if ( 112 | env.startsWith('es') && 113 | !ES_VERSIONS.includes(parseInt(env.replace(/^es/, ''))) 114 | ) { 115 | continue; 116 | } 117 | 118 | let search = Object.keys(entries); 119 | 120 | let matches = search.filter( 121 | (entry) => 122 | // @ts-expect-error -- we already checked for undefined 123 | entry in config.globals && 124 | // @ts-expect-error -- filtering makes the key to any 125 | normalizeGlobValue(config.globals[entry]) === entries[entry] 126 | ); 127 | 128 | // For especially large globals, we allow a match if >=97% of keys match. 129 | // This lets us handle version differences in globals package where 130 | // there's a difference of just a few extra/removed keys. 131 | // Do not do any other envs, otherwise things like es2024 and es2026 132 | // would match each other. 133 | const useThreshold = THRESHOLD_ENVS.includes(env); 134 | 135 | const withinThreshold = 136 | useThreshold && matches.length / search.length >= 0.97; 137 | 138 | if ( 139 | withinThreshold || 140 | (!useThreshold && matches.length === search.length) 141 | ) { 142 | if (config.env === undefined) { 143 | config.env = {}; 144 | } 145 | config.env[env] = true; 146 | } 147 | } 148 | }; 149 | 150 | export const transformEnvAndGlobals = ( 151 | eslintConfig: Linter.Config, 152 | targetConfig: OxlintConfigOrOverride, 153 | options?: Options 154 | ): void => { 155 | if ( 156 | eslintConfig.languageOptions?.parser !== undefined && 157 | eslintConfig.languageOptions?.parser !== null && 158 | typeof eslintConfig.languageOptions.parser === 'object' && 159 | 'meta' in eslintConfig.languageOptions.parser && 160 | !(SUPPORTED_ESLINT_PARSERS as (string | undefined)[]).includes( 161 | // @ts-expect-error 162 | eslintConfig.languageOptions.parser.meta?.name 163 | ) 164 | ) { 165 | options?.reporter?.report( 166 | 'special parser detected: ' + 167 | // @ts-expect-error 168 | eslintConfig.languageOptions.parser.meta?.name 169 | ); 170 | } 171 | 172 | if ( 173 | eslintConfig.languageOptions?.globals !== undefined && 174 | eslintConfig.languageOptions?.globals !== null 175 | ) { 176 | if (targetConfig.globals === undefined) { 177 | targetConfig.globals = {}; 178 | } 179 | 180 | // when upgrading check if the global already exists and do not write 181 | if (options?.merge) { 182 | for (const [global, globalSetting] of Object.entries( 183 | eslintConfig.languageOptions.globals 184 | )) { 185 | if (!(global in targetConfig.globals)) { 186 | targetConfig.globals[global] = globalSetting; 187 | } 188 | } 189 | } else { 190 | // no merge, hard append 191 | Object.assign(targetConfig.globals, eslintConfig.languageOptions.globals); 192 | } 193 | } 194 | 195 | if (eslintConfig.languageOptions?.ecmaVersion !== undefined) { 196 | if (eslintConfig.languageOptions.ecmaVersion === 'latest') { 197 | if (targetConfig.env === undefined) { 198 | targetConfig.env = {}; 199 | } 200 | const latestVersion = `es${ES_VERSIONS[ES_VERSIONS.length - 1]}`; 201 | if (!(latestVersion in targetConfig.env)) { 202 | targetConfig.env[latestVersion] = true; 203 | } 204 | } else if ( 205 | typeof eslintConfig.languageOptions.ecmaVersion === 'number' && 206 | ES_VERSIONS.includes(eslintConfig.languageOptions.ecmaVersion) 207 | ) { 208 | if (targetConfig.env === undefined) { 209 | targetConfig.env = {}; 210 | } 211 | const targetVersion = `es${eslintConfig.languageOptions.ecmaVersion}`; 212 | if (!(targetVersion in targetConfig.env)) { 213 | targetConfig.env[targetVersion] = true; 214 | } 215 | } 216 | } 217 | }; 218 | 219 | export const cleanUpUselessOverridesEnv = (config: OxlintConfig): void => { 220 | if (config.env === undefined || config.overrides === undefined) { 221 | return; 222 | } 223 | 224 | for (const override of config.overrides) { 225 | if (override.env === undefined) { 226 | continue; 227 | } 228 | 229 | for (const [overrideEnv, overrideEnvConfig] of Object.entries( 230 | override.env 231 | )) { 232 | if ( 233 | overrideEnv in config.env && 234 | config.env[overrideEnv] === overrideEnvConfig 235 | ) { 236 | delete override.env[overrideEnv]; 237 | } 238 | } 239 | 240 | if (Object.keys(override.env).length === 0) { 241 | delete override.env; 242 | } 243 | } 244 | }; 245 | 246 | // These are envs where the key includes all of the globals from the values. 247 | // So for example, for shared-node-browser, if the user has either `node` or `browser` already in their `env`, we can remove `shared-node-browser`. 248 | const SUPERSET_ENVS: Record = { 249 | node: ['nodeBuiltin', 'shared-node-browser', 'commonjs'], 250 | browser: ['shared-node-browser'], 251 | }; 252 | 253 | /** 254 | * Cleans up superset environments in the config and its overrides. 255 | * If a superset environment is present, its subset environments are removed, e.g. all globals from `shared-node-browser` are also in `browser` and `node`. 256 | * 257 | * This also applies for overrides, where if a superset env is defined in the override or main config, 258 | * the subset envs can be removed from the override if the override has the same value as the superset. 259 | */ 260 | export const cleanUpSupersetEnvs = (config: OxlintConfig): void => { 261 | // Clean up main config env 262 | if (config.env !== undefined) { 263 | // If we have a superset env, remove its subsets 264 | for (const [supersetEnv, subsetEnvs] of Object.entries(SUPERSET_ENVS)) { 265 | if (!(supersetEnv in config.env)) { 266 | continue; 267 | } 268 | 269 | for (const subsetEnv of subsetEnvs) { 270 | if (config.env[subsetEnv] === config.env[supersetEnv]) { 271 | delete config.env[subsetEnv]; 272 | } 273 | } 274 | } 275 | } 276 | 277 | // Clean up overrides 278 | if (config.overrides !== undefined) { 279 | for (const override of config.overrides) { 280 | if (override.env === undefined) { 281 | continue; 282 | } 283 | 284 | for (const [supersetEnv, subsetEnvs] of Object.entries(SUPERSET_ENVS)) { 285 | // Check if the superset env is in the override 286 | const supersetInOverride = supersetEnv in override.env; 287 | const supersetInMain = 288 | config.env !== undefined && supersetEnv in config.env; 289 | 290 | for (const subsetEnv of subsetEnvs) { 291 | if (!(subsetEnv in override.env)) { 292 | continue; 293 | } 294 | 295 | // Case 1: Both superset and subset are in the override with the same value 296 | // We can safely remove the subset 297 | if ( 298 | supersetInOverride && 299 | override.env[subsetEnv] === override.env[supersetEnv] 300 | ) { 301 | delete override.env[subsetEnv]; 302 | continue; 303 | } 304 | 305 | // Case 2: Superset is in main config, subset is in override 306 | // If they have the same value, the subset is redundant 307 | if ( 308 | supersetInMain && 309 | !supersetInOverride && 310 | config.env![supersetEnv] === override.env[subsetEnv] 311 | ) { 312 | delete override.env[subsetEnv]; 313 | } 314 | } 315 | } 316 | 317 | // Clean up empty env object 318 | if (Object.keys(override.env).length === 0) { 319 | delete override.env; 320 | } 321 | } 322 | } 323 | }; 324 | -------------------------------------------------------------------------------- /integration_test/projects/puppeteer.eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // https://github.com/puppeteer/puppeteer/blob/784d5ad02aa475d6c9deedd658552d156e8c6a69/eslint.config.mjs 2 | /** 3 | * @license 4 | * Copyright 2024 Google Inc. 5 | * SPDX-License-Identifier: Apache-2.0 6 | */ 7 | // import puppeteerPlugin from '@puppeteer/eslint'; 8 | import stylisticPlugin from '@stylistic/eslint-plugin'; 9 | import { defineConfig, globalIgnores } from 'eslint/config'; 10 | import importPlugin from 'eslint-plugin-import'; 11 | import mocha from 'eslint-plugin-mocha'; 12 | import eslintPrettierPluginRecommended from 'eslint-plugin-prettier/recommended'; 13 | import tsdoc from 'eslint-plugin-tsdoc'; 14 | import globals from 'globals'; 15 | import typescriptEslint from 'typescript-eslint'; 16 | 17 | export default defineConfig([ 18 | globalIgnores([ 19 | '**/node_modules', 20 | '**/build/', 21 | '**/lib/', 22 | '**/bin/', 23 | '**/*.tsbuildinfo', 24 | '**/*.api.json', 25 | '**/*.tgz', 26 | '**/yarn.lock', 27 | '**/.docusaurus/', 28 | '**/.cache-loader', 29 | 'test/output-*/', 30 | '**/.dev_profile*', 31 | '**/coverage/', 32 | '**/generated/', 33 | '**/.eslintcache', 34 | '**/.cache/', 35 | '**/.vscode', 36 | '!.vscode/extensions.json', 37 | '!.vscode/*.template.json', 38 | '**/.devcontainer', 39 | '**/.DS_Store', 40 | '**/.env.local', 41 | '**/.env.development.local', 42 | '**/.env.test.local', 43 | '**/.env.production.local', 44 | '**/npm-debug.log*', 45 | '**/yarn-debug.log*', 46 | '**/yarn-error.log*', 47 | '**/.wireit', 48 | '**/assets/', 49 | '**/third_party/', 50 | 'packages/ng-schematics/sandbox/**/*', 51 | 'packages/ng-schematics/multi/**/*', 52 | 'packages/ng-schematics/src/**/files/', 53 | 'examples/puppeteer-in-browser/out/**/*', 54 | 'examples/puppeteer-in-browser/node_modules/**/*', 55 | 'examples/puppeteer-in-extension/out/**/*', 56 | 'examples/puppeteer-in-extension/node_modules/**/*', 57 | ]), 58 | eslintPrettierPluginRecommended, 59 | importPlugin.flatConfigs.typescript, 60 | { 61 | name: 'JavaScript rules', 62 | plugins: { 63 | mocha, 64 | '@typescript-eslint': typescriptEslint.plugin, 65 | '@stylistic': stylisticPlugin, 66 | // '@puppeteer': puppeteerPlugin, 67 | }, 68 | 69 | languageOptions: { 70 | ecmaVersion: 'latest', 71 | sourceType: 'module', 72 | 73 | globals: { 74 | ...globals.node, 75 | }, 76 | 77 | parser: typescriptEslint.parser, 78 | }, 79 | 80 | settings: { 81 | 'import/resolver': { 82 | typescript: true, 83 | }, 84 | }, 85 | 86 | rules: { 87 | curly: ['error', 'all'], 88 | 'arrow-body-style': ['error', 'always'], 89 | 'prettier/prettier': 'error', 90 | 91 | 'spaced-comment': [ 92 | 'error', 93 | 'always', 94 | { 95 | markers: ['*', '/'], 96 | }, 97 | ], 98 | 99 | eqeqeq: ['error'], 100 | 101 | 'accessor-pairs': [ 102 | 'error', 103 | { 104 | getWithoutSet: false, 105 | setWithoutGet: false, 106 | }, 107 | ], 108 | 109 | 'new-parens': 'error', 110 | 111 | 'prefer-const': 'error', 112 | 113 | 'max-len': [ 114 | 'error', 115 | { 116 | /* this setting doesn't impact things as we use Prettier to format 117 | * our code and hence dictate the line length. 118 | * Prettier aims for 80 but sometimes makes the decision to go just 119 | * over 80 chars as it decides that's better than wrapping. ESLint's 120 | * rule defaults to 80 but therefore conflicts with Prettier. So we 121 | * set it to something far higher than Prettier would allow to avoid 122 | * it causing issues and conflicting with Prettier. 123 | */ 124 | code: 200, 125 | comments: 90, 126 | ignoreTemplateLiterals: true, 127 | ignoreUrls: true, 128 | ignoreStrings: true, 129 | ignoreRegExpLiterals: true, 130 | }, 131 | ], 132 | 133 | 'no-var': 'error', 134 | 'no-with': 'error', 135 | 'no-multi-str': 'error', 136 | 'no-caller': 'error', 137 | 'no-implied-eval': 'error', 138 | 'no-labels': 'error', 139 | 'no-new-object': 'error', 140 | 'no-octal-escape': 'error', 141 | 'no-self-compare': 'error', 142 | 'no-shadow-restricted-names': 'error', 143 | 'no-cond-assign': 'error', 144 | 'no-debugger': 'error', 145 | 'no-dupe-keys': 'error', 146 | 'no-duplicate-case': 'error', 147 | 'no-empty-character-class': 'error', 148 | 'no-unreachable': 'error', 149 | 'no-unsafe-negation': 'error', 150 | radix: 'error', 151 | 'valid-typeof': 'error', 152 | 153 | 'no-unused-vars': [ 154 | 'error', 155 | { 156 | args: 'none', 157 | vars: 'local', 158 | varsIgnorePattern: 159 | '([fx]?describe|[fx]?it|beforeAll|beforeEach|afterAll|afterEach)', 160 | }, 161 | ], 162 | 163 | // Disabled as it now reports issues - https://eslint.org/docs/latest/rules/no-implicit-globals 164 | // 'no-implicit-globals': ['error'], 165 | 'require-yield': 'error', 166 | 'template-curly-spacing': ['error', 'never'], 167 | 'mocha/no-exclusive-tests': 'error', 168 | 169 | 'import/order': [ 170 | 'error', 171 | { 172 | 'newlines-between': 'always', 173 | 174 | alphabetize: { 175 | order: 'asc', 176 | caseInsensitive: true, 177 | }, 178 | }, 179 | ], 180 | 181 | 'import/no-cycle': [ 182 | 'error', 183 | { 184 | maxDepth: Infinity, 185 | }, 186 | ], 187 | 188 | 'import/enforce-node-protocol-usage': ['error', 'always'], 189 | 190 | '@stylistic/function-call-spacing': 'error', 191 | '@stylistic/semi': 'error', 192 | 193 | // Keeps comments formatted. 194 | '@puppeteer/prettier-comments': 'error', 195 | // Enforces consistent file extension 196 | '@puppeteer/extensions': 'error', 197 | // Enforces license headers on files 198 | '@puppeteer/check-license': 'error', 199 | }, 200 | }, 201 | ...[ 202 | typescriptEslint.configs.eslintRecommended, 203 | typescriptEslint.configs.recommended, 204 | typescriptEslint.configs.stylistic, 205 | ] 206 | .flat() 207 | // oxlint-disable-next-line oxc/no-map-spread 208 | .map((config) => { 209 | return { 210 | ...config, 211 | files: ['**/*.ts'], 212 | }; 213 | }), 214 | { 215 | name: 'TypeScript rules', 216 | files: ['**/*.ts'], 217 | 218 | plugins: { 219 | tsdoc, 220 | }, 221 | 222 | languageOptions: { 223 | ecmaVersion: 'latest', 224 | sourceType: 'module', 225 | 226 | parserOptions: { 227 | allowAutomaticSingleRunInference: true, 228 | project: './tsconfig.base.json', 229 | }, 230 | }, 231 | 232 | rules: { 233 | // Enforces clean up of used resources. 234 | '@puppeteer/use-using': 'error', 235 | 236 | '@typescript-eslint/array-type': [ 237 | 'error', 238 | { 239 | default: 'array-simple', 240 | }, 241 | ], 242 | 243 | 'no-unused-vars': 'off', 244 | 245 | '@typescript-eslint/no-unused-vars': [ 246 | 'error', 247 | { 248 | argsIgnorePattern: '^_', 249 | varsIgnorePattern: '^_', 250 | }, 251 | ], 252 | 253 | '@typescript-eslint/no-empty-function': 'off', 254 | '@typescript-eslint/no-use-before-define': 'off', 255 | // We have to use any on some types so the warning isn't valuable. 256 | '@typescript-eslint/no-explicit-any': 'off', 257 | // We don't require explicit return types on basic functions or 258 | // dummy functions in tests, for example 259 | '@typescript-eslint/explicit-function-return-type': 'off', 260 | // We allow non-null assertions if the value was asserted using `assert` API. 261 | '@typescript-eslint/no-non-null-assertion': 'off', 262 | '@typescript-eslint/no-unnecessary-template-expression': 'error', 263 | '@typescript-eslint/no-unsafe-function-type': 'error', 264 | '@typescript-eslint/no-wrapper-object-types': 'error', 265 | 266 | // By default this is a warning but we want it to error. 267 | '@typescript-eslint/explicit-module-boundary-types': 'error', 268 | 269 | 'no-restricted-syntax': [ 270 | 'error', 271 | { 272 | selector: "CallExpression[callee.name='require']", 273 | message: '`require` statements are not allowed. Use `import`.', 274 | }, 275 | ], 276 | 277 | '@typescript-eslint/no-floating-promises': [ 278 | 'error', 279 | { 280 | ignoreVoid: true, 281 | ignoreIIFE: true, 282 | }, 283 | ], 284 | 285 | '@typescript-eslint/prefer-ts-expect-error': 'error', 286 | // This is more performant; see https://v8.dev/blog/fast-async. 287 | '@typescript-eslint/return-await': ['error', 'always'], 288 | // This optimizes the dependency tracking for type-only files. 289 | '@typescript-eslint/consistent-type-imports': 'error', 290 | // So type-only exports get elided. 291 | '@typescript-eslint/consistent-type-exports': 'error', 292 | // Don't want to trigger unintended side-effects. 293 | '@typescript-eslint/no-import-type-side-effects': 'error', 294 | // Prefer interfaces over types for shape like. 295 | '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], 296 | }, 297 | }, 298 | { 299 | name: 'Puppeteer Core syntax', 300 | files: ['packages/puppeteer-core/src/**/*.ts'], 301 | rules: { 302 | 'no-restricted-syntax': [ 303 | 'error', 304 | { 305 | message: 'Use method `Deferred.race()` instead.', 306 | selector: 307 | 'MemberExpression[object.name="Promise"][property.name="race"]', 308 | }, 309 | { 310 | message: 311 | 'Deferred `valueOrThrow` should not be called in `Deferred.race()` pass deferred directly', 312 | selector: 313 | 'CallExpression[callee.object.name="Deferred"][callee.property.name="race"] > ArrayExpression > CallExpression[callee.property.name="valueOrThrow"]', 314 | }, 315 | ], 316 | 'no-restricted-imports': [ 317 | 'error', 318 | { 319 | patterns: ['*Events', '*.test.js'], 320 | paths: [], 321 | }, 322 | ], 323 | }, 324 | }, 325 | { 326 | name: 'Packages', 327 | files: [ 328 | 'packages/puppeteer-core/src/**/*.test.ts', 329 | 'tools/mocha-runner/src/test.ts', 330 | ], 331 | rules: { 332 | // With the Node.js test runner, `describe` and `it` are technically 333 | // promises, but we don't need to await them. 334 | '@typescript-eslint/no-floating-promises': 'off', 335 | }, 336 | }, 337 | { 338 | files: ['packages/**/*.ts'], 339 | 340 | rules: { 341 | 'tsdoc/syntax': 'error', 342 | }, 343 | }, 344 | { 345 | name: 'Mocha', 346 | files: ['test/**/*.ts'], 347 | rules: { 348 | 'no-restricted-imports': [ 349 | 'error', 350 | { 351 | /** 352 | * The mocha tests run on the compiled output in the /lib directory 353 | * so we should avoid importing from src. 354 | */ 355 | patterns: ['*src*'], 356 | }, 357 | ], 358 | }, 359 | }, 360 | { 361 | name: 'Mocha Tests', 362 | files: ['test/**/*.spec.ts'], 363 | 364 | rules: { 365 | '@typescript-eslint/no-unused-vars': [ 366 | 'error', 367 | { 368 | argsIgnorePattern: '^_', 369 | varsIgnorePattern: '^_', 370 | }, 371 | ], 372 | 373 | 'no-restricted-syntax': [ 374 | 'error', 375 | { 376 | message: 377 | 'Use helper command `launch` to make sure the browsers get cleaned', 378 | selector: 379 | 'MemberExpression[object.name="puppeteer"][property.name="launch"]', 380 | }, 381 | { 382 | message: 'Unexpected debugging mocha test.', 383 | selector: 384 | 'CallExpression[callee.object.name="it"] > MemberExpression > Identifier[name="deflake"], CallExpression[callee.object.name="it"] > MemberExpression > Identifier[name="deflakeOnly"]', 385 | }, 386 | { 387 | message: 'No `expect` in EventHandler. They will never throw errors', 388 | selector: 389 | 'CallExpression[callee.property.name="on"] BlockStatement > :not(TryStatement) > ExpressionStatement > CallExpression[callee.object.callee.name="expect"]', 390 | }, 391 | ], 392 | 393 | 'mocha/no-pending-tests': 'error', 394 | 'mocha/no-identical-title': 'error', 395 | '@puppeteer/no-quirks-mode-set-content': 'error', 396 | }, 397 | }, 398 | ]); 399 | --------------------------------------------------------------------------------