From 3ec841f309334346192b53807a066d4e67c01c9f Mon Sep 17 00:00:00 2001 From: Sysix Date: Thu, 19 Dec 2024 17:20:44 +0100 Subject: [PATCH 1/4] build: use `oxlint --rules --format=json` for building rules --- .github/workflows/bump_oxlint.yml | 3 +- .github/workflows/generate.yml | 3 - package.json | 1 - scripts/constants.ts | 21 --- scripts/generate.ts | 8 +- scripts/sparse-clone.ts | 95 ----------- scripts/traverse-rules.test.ts | 237 -------------------------- scripts/traverse-rules.ts | 263 ++++------------------------- src/generated/rules-by-category.ts | 35 +--- src/generated/rules-by-scope.ts | 45 ++--- 10 files changed, 51 insertions(+), 660 deletions(-) delete mode 100644 scripts/sparse-clone.ts delete mode 100644 scripts/traverse-rules.test.ts diff --git a/.github/workflows/bump_oxlint.yml b/.github/workflows/bump_oxlint.yml index 6d3df6a..9d24ce5 100644 --- a/.github/workflows/bump_oxlint.yml +++ b/.github/workflows/bump_oxlint.yml @@ -25,8 +25,7 @@ jobs: OXLINT_VERSION: ${{ inputs.version }} run: | pnpm install oxlint@${OXLINT_VERSION} - pnpm run clone ${OXLINT_VERSION} - pnpm run generate # Generate rules from source code + pnpm run generate # Generate rules pnpm run format # run prettier over it - name: Test and update snapshot diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml index ca10366..d03024f 100644 --- a/.github/workflows/generate.yml +++ b/.github/workflows/generate.yml @@ -28,9 +28,6 @@ jobs: - uses: ./.github/actions/pnpm - - name: Clone oxc_linter project - run: pnpm run clone - - name: Remove current generated code run: rm -r ./src/generated/ diff --git a/package.json b/package.json index 4f6cb3c..605f3b1 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "scripts": { "prepare": "husky", "generate": "node --import @oxc-node/core/register ./scripts/generate.ts", - "clone": "node --import @oxc-node/core/register ./scripts/sparse-clone.ts", "build": "vite build", "lint": "npx oxlint --tsconfig=tsconfig.json && npx eslint --flag unstable_ts_config", "format": "npx prettier --write .", diff --git a/scripts/constants.ts b/scripts/constants.ts index 8dd0572..368e165 100644 --- a/scripts/constants.ts +++ b/scripts/constants.ts @@ -1,26 +1,5 @@ -import path from 'node:path'; -import { aliasPluginNames } from '../src/constants.js'; - -const __dirname = new URL('.', import.meta.url).pathname; - -export const TARGET_DIRECTORY = path.resolve(__dirname, '..', '.oxc_sparse'); -export const VERSION_PREFIX = 'oxlint_v'; -export const SPARSE_CLONE_DIRECTORY = 'crates/oxc_linter/src'; - // these are the rules that don't have a direct equivalent in the eslint rules export const ignoreScope = new Set(['oxc', 'deepscan', 'security']); // these are the rules that are not fully implemented in oxc export const ignoreCategories = new Set(['nursery']); - -export function convertScope(scope: string) { - return Reflect.has(aliasPluginNames, scope) - ? aliasPluginNames[scope as 'eslint'] - : scope.replace('_', '-'); -} - -export function prefixScope(scope: string) { - const _scope = convertScope(scope); - - return _scope ? `${_scope}/` : ''; -} diff --git a/scripts/generate.ts b/scripts/generate.ts index 25914cd..4afd85f 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -6,13 +6,7 @@ import { traverseRules } from './traverse-rules.js'; const __dirname = new URL('.', import.meta.url).pathname; -const { successResultArray, failureResultArray } = await traverseRules(); - -if (failureResultArray.length > 0) { - console.error( - `Failed to generate rules for the following rules ${JSON.stringify(failureResultArray)}` - ); -} +const successResultArray = traverseRules(); const rulesGenerator = new RulesGenerator(successResultArray); const configGenerator = new ConfigGenerator(successResultArray); diff --git a/scripts/sparse-clone.ts b/scripts/sparse-clone.ts deleted file mode 100644 index 03c7201..0000000 --- a/scripts/sparse-clone.ts +++ /dev/null @@ -1,95 +0,0 @@ -import shell from 'shelljs'; -import fs from 'node:fs'; -import { - TARGET_DIRECTORY, - SPARSE_CLONE_DIRECTORY, - VERSION_PREFIX, -} from './constants.js'; -import packageJson from '../package.json' with { type: 'json' }; - -/** - * Run this file in CLI like `pnpm run clone` - * It clones the oxc_linter git project into your local file. - * - * You can run it with a version argument like `pnpm run clone 0.10.0`. - * When no argument is provided, the current package.json version is used. - * - */ - -const checkoutVersion = process.argv[2] ?? packageJson.version; - -// Function to initialize or reconfigure sparse-checkout -function configureSparseCheckout(cloneDirectory: string) { - console.log('Configuring sparse-checkout...'); - // Initialize sparse-checkout if not already initialized - if ( - !fs.existsSync('.oxc_sparse/.git/info/sparse-checkout') && - shell.exec('git sparse-checkout init --cone').code !== 0 - ) { - shell.echo('Error: Failed to initialize sparse-checkout'); - shell.exit(1); - } - - // Set the directory to be checked out - if (shell.exec(`git sparse-checkout set ${cloneDirectory}`).code !== 0) { - shell.echo('Error: Failed to configure sparse-checkout'); - shell.exit(1); - } -} - -function checkoutVersionTag(version: string) { - const tag = `${VERSION_PREFIX}${version}`; - - // Checkout the specified directory - if (shell.exec(`git checkout ${tag}`, { silent: true }).code !== 0) { - shell.echo('Error: Git checkout failed'); - shell.exit(1); - } - - console.log(`Successfully checkout git tag ${tag}`); -} - -// Function to clone or update a repository -function cloneOrUpdateRepo( - repositoryUrl: string, - targetDirectory: string, - cloneDirectory: string, - version: string -) { - // Check if the target directory exists and is a Git repository - if ( - fs.existsSync(targetDirectory) && - fs.existsSync(`${targetDirectory}/.git`) - ) { - console.log(`Repository exists, updating ${targetDirectory}...`); - - shell.cd(targetDirectory); - - configureSparseCheckout(cloneDirectory); - checkoutVersionTag(version); - } else { - console.log(`Cloning new repository into ${targetDirectory}...`); - - // Clone the repository without checking out files - if ( - shell.exec( - `git clone --filter=blob:none --no-checkout ${repositoryUrl} ${targetDirectory}` - ).code !== 0 - ) { - shell.echo('Error: Git clone failed'); - shell.exit(1); - } - - shell.cd(targetDirectory); - - configureSparseCheckout(cloneDirectory); - checkoutVersionTag(version); - } -} - -cloneOrUpdateRepo( - 'https://github.com/oxc-project/oxc.git', - TARGET_DIRECTORY, - SPARSE_CLONE_DIRECTORY, - checkoutVersion -); diff --git a/scripts/traverse-rules.test.ts b/scripts/traverse-rules.test.ts deleted file mode 100644 index 850108c..0000000 --- a/scripts/traverse-rules.test.ts +++ /dev/null @@ -1,237 +0,0 @@ -import type { Rule } from './traverse-rules.js'; -import { - getFileNameWithoutExtension, - getFolderNameUnderRules, -} from './traverse-rules.js'; -import { suite, expect, test, vi, afterEach, beforeEach } from 'vitest'; -import { readFilesRecursively } from './traverse-rules.js'; -import { type fs, vol } from 'memfs'; -import dedent from 'dedent'; - -suite('getFileNameWithoutExtension', () => { - test('getFileNameWithoutExtension returns correct file name without extension', () => { - const filePath = '/path/to/file.rs'; - const currentDirectory = '/path/to'; - const expectedFileName = 'file'; - - const result = getFileNameWithoutExtension(filePath, currentDirectory); - - expect(result).toEqual(expectedFileName); - }); - - test("getFileNameWithoutExtension returns current directory name when file name is 'mod.rs'", () => { - const filePath = '/path/to/mod.rs'; - const currentDirectory = '/path/to'; - const expectedFileName = 'to'; - - const result = getFileNameWithoutExtension(filePath, currentDirectory); - - expect(result).toEqual(expectedFileName); - }); -}); - -suite('getFolderNameUnderRules', () => { - test("getFolderNameUnderRules returns empty string when 'rules' directory not found", () => { - const filePath = '/path/to/file.ts'; - const expectedFolderName = ''; - - const result = getFolderNameUnderRules(filePath); - - expect(result).toEqual(expectedFolderName); - }); - - test("getFolderNameUnderRules returns folder name directly under 'rules'", () => { - const filePath = '/path/to/rules/folder/file.ts'; - const expectedFolderName = 'folder'; - - const result = getFolderNameUnderRules(filePath); - - expect(result).toEqual(expectedFolderName); - }); - - test("getFolderNameUnderRules returns remaining path if there's no additional '/'", () => { - const filePath = '/path/to/rules/file.ts'; - const expectedFolderName = 'file.ts'; - - const result = getFolderNameUnderRules(filePath); - - expect(result).toEqual(expectedFolderName); - }); -}); - -suite('readFilesRecursively', () => { - beforeEach(() => { - vi.mock('node:fs', async () => { - const memfs: { fs: typeof fs } = await vi.importActual('memfs'); - return { - promises: memfs.fs.promises, - }; - }); - }); - - afterEach(() => { - vol.reset(); - vi.restoreAllMocks(); - }); - - test('readFilesRecursively recursively reads files and directories', async () => { - // Prepare test data - const successResultArray: Rule[] = []; - const skippedResultArray: Rule[] = []; - const failureResultArray: Rule[] = []; - - vol.fromJSON({ - 'crates/src/rules/eslint/rulename-with-mod/mod.rs': 'content', - 'crates/src/rules/typescript/rulename-without-mod.rs': 'content', - 'crates/src/rules/oxc/rulename-which-will-be-skipped.rs': 'content', - }); - - // Call the function - await readFilesRecursively( - '.', - successResultArray, - skippedResultArray, - failureResultArray - ); - - expect(successResultArray.length).toEqual(0); - expect(skippedResultArray.length).toEqual(1); - expect(failureResultArray.length).toEqual(2); - - expect(successResultArray).toEqual([]); - expect(skippedResultArray).toEqual([ - { - category: 'unknown', - scope: 'oxc', - value: 'oxc/rulename-which-will-be-skipped', - }, - ]); - expect(failureResultArray).toContainEqual({ - category: 'unknown', - error: 'No match block for `declare_oxc_lint`', - scope: 'eslint', - value: 'rulename-with-mod', - }); - expect(failureResultArray).toContainEqual({ - category: 'unknown', - error: 'No match block for `declare_oxc_lint`', - scope: 'typescript', - value: '@typescript-eslint/rulename-without-mod', - }); - }); - - test('readFilesRecursively returns parsed rules correctly', async () => { - // Prepare test data - const successResultArray: Rule[] = []; - const skippedResultArray: Rule[] = []; - const failureResultArray: Rule[] = []; - - const ruleNameWithModuleContent = dedent(`declare_oxc_lint!( - /// Some Block Content - /// ) extra parenthesis to make sure it doesn't catch - DefaultCaseLast, - style - )`); - - const ruleNameWithoutModuleContent = dedent(`declare_oxc_lint!( - /// ### What it does - /// Disallow calling some global objects as functions - NoObjCalls, - correctness - )`); - - const ruleWithFixabilityModuleContent = dedent(`declare_oxc_lint!( - /// Some Block Content - /// ) extra parenthesis to make sure it doesn't catch - DefaultCaseLast, - correctness, - fix - )`); - - vol.fromJSON({ - 'crates/src/rules/eslint/rulename-with-mod/mod.rs': - ruleNameWithModuleContent, - 'crates/src/rules/typescript/rulename-without-mod.rs': - ruleNameWithoutModuleContent, - 'crates/src/rules/unicorn/rule-with-fixability.rs': - ruleWithFixabilityModuleContent, - }); - - // Call the function - await readFilesRecursively('.', successResultArray, [], []); - expect(successResultArray).toContainEqual({ - category: 'style', - scope: 'eslint', - value: 'rulename-with-mod', - }); - expect(successResultArray).toContainEqual({ - category: 'correctness', - scope: 'typescript', - value: '@typescript-eslint/rulename-without-mod', - }); - expect(successResultArray).toContainEqual({ - category: 'correctness', - scope: 'unicorn', - value: 'unicorn/rule-with-fixability', - }); - - expect(skippedResultArray).toEqual([]); - expect(failureResultArray).toEqual([]); - }); - - test('readFilesRecursively returns non-parsed rules correctly', async () => { - // Prepare test data - const successResultArray: Rule[] = []; - const skippedResultArray: Rule[] = []; - const failureResultArray: Rule[] = []; - - const badContent = 'bad content'; - - vol.fromJSON({ - 'crates/src/rules/eslint/rulename-that-will-be-skipped-because-no-match-block.rs': - badContent, - 'crates/src/rules/oxc/rulename-that-will-be-skipped-because-bad-content-or-scope.rs': - badContent, - 'crates/src/rules/oxc/rulename-that-will-be-skipped-because-skip-scope.rs': - badContent, - 'crates/src/rules/typescript/rulename-that-will-error.rs': badContent, - }); - - // Call the function - await readFilesRecursively( - '.', - successResultArray, - skippedResultArray, - failureResultArray - ); - - expect(successResultArray).toEqual([]); - expect(skippedResultArray).toEqual([ - { - category: 'unknown', - scope: 'oxc', - value: 'oxc/rulename-that-will-be-skipped-because-bad-content-or-scope', - }, - { - category: 'unknown', - scope: 'oxc', - value: 'oxc/rulename-that-will-be-skipped-because-skip-scope', - }, - ]); - - expect(failureResultArray).toEqual([ - { - category: 'unknown', - error: 'No match block for `declare_oxc_lint`', - scope: 'eslint', - value: 'rulename-that-will-be-skipped-because-no-match-block', - }, - { - category: 'unknown', - error: 'No match block for `declare_oxc_lint`', - scope: 'typescript', - value: '@typescript-eslint/rulename-that-will-error', - }, - ]); - }); -}); diff --git a/scripts/traverse-rules.ts b/scripts/traverse-rules.ts index 5ecffaa..da16e22 100644 --- a/scripts/traverse-rules.ts +++ b/scripts/traverse-rules.ts @@ -1,254 +1,59 @@ -import { promises } from 'node:fs'; -import path from 'node:path'; -import { - ignoreCategories, - ignoreScope, - prefixScope, - SPARSE_CLONE_DIRECTORY, - TARGET_DIRECTORY, -} from './constants.js'; +import { execSync } from 'node:child_process'; +import { ignoreCategories, ignoreScope } from './constants.js'; import { + aliasPluginNames, reactHookRulesInsideReactScope, - typescriptRulesExtendEslintRules, - viteTestCompatibleRules, } from '../src/constants.js'; -// Recursive function to read files in a directory, this currently assumes that the directory -// structure is semi-consistent within the oxc_linter crate -export async function readFilesRecursively( - directory: string, - successResultArray: Rule[], - skippedResultArray: Rule[], - failureResultArray: Rule[] -): Promise { - const entries = await promises.readdir(directory, { withFileTypes: true }); - - // Check if the current directory contains a 'mod.rs' file - // eslint-disable-next-line unicorn/prevent-abbreviations - const containsModRs = entries.some( - (entry) => entry.isFile() && entry.name === 'mod.rs' - ); - - await Promise.all( - entries.map(async (entry) => { - const entryPath = path.join(directory, entry.name); - if (entry.isDirectory()) { - await readFilesRecursively( - entryPath, - successResultArray, - skippedResultArray, - failureResultArray - ); // Recursive call for directories - } else if ( - entry.isFile() && - (!containsModRs || entry.name === 'mod.rs') - ) { - await processFile( - entryPath, - directory, - successResultArray, - skippedResultArray, - failureResultArray - ); // Process each file - } - }) - ); -} - export interface Rule { value: string; scope: string; category: string; - error?: string; } -// Function to process each file and extract the desired word -async function processFile( - filePath: string, - currentDirectory: string, - successResultArray: Rule[], - skippedResultArray: Rule[], - failureResultArray: Rule[] -): Promise { - const content = await promises.readFile(filePath, 'utf8'); - - // 'ok' way to get the scope, depends on the directory structure - let scope = getFolderNameUnderRules(filePath); - const shouldIgnoreRule = ignoreScope.has(scope); +function readFilesFromCommand(): Rule[] { + // do not handle the exception + const oxlintOutput = execSync(`npx oxlint --rules --format=json`, { + encoding: 'utf8', + stdio: 'pipe', + }); - // when the file is called `mod.rs` we want to use the parent directory name as the rule name - // Note that this is fairly brittle, as relying on the directory structure can be risky - const ruleNameWithoutScope = getFileNameWithoutExtension( - filePath, - currentDirectory - ).replaceAll('_', '-'); + // do not handle the exception + return JSON.parse(oxlintOutput); +} - // All rules from `eslint-plugin-react-hooks` - // Since oxlint supports these rules under react/*, we need to remap them. +function fixScopeOfRule(rule: Rule): void { if ( - scope === 'react' && - reactHookRulesInsideReactScope.includes(ruleNameWithoutScope) + rule.scope === 'react' && + reactHookRulesInsideReactScope.includes(rule.value) ) { - scope = 'react_hooks'; - } - - const effectiveRuleName = - `${prefixScope(scope)}${ruleNameWithoutScope}`.replaceAll('_', '-'); - - // add the rule to the skipped array and continue to see if there's a match regardless - if (shouldIgnoreRule) { - skippedResultArray.push({ - value: effectiveRuleName, - scope: scope, - category: 'unknown', - }); - - return; - } - - // Remove comments to prevent them from affecting the regex - const cleanContent = content.replaceAll(/^\s*\/\/.*$/gm, ''); - - // find the correct macro block where `);` or `}` is the end of the block - // ensure that the `);` or `}` is on its own line, with no characters before it - const blockRegex = /declare_oxc_lint!\s*([({]([\S\s]*?)\s*[)}]\s*;?)/gm; - - const match = blockRegex.exec(cleanContent); - - if (match === null) { - failureResultArray.push({ - value: effectiveRuleName, - scope: scope, - category: 'unknown', - error: 'No match block for `declare_oxc_lint`', - }); - return; - } - - const block = match[2]; - - // Remove comments to prevent them from affecting the regex - const cleanBlock = block.replaceAll(/\/\/.*$|\/\*[\S\s]*?\*\//gm, '').trim(); - - // Extract the keyword, skipping the optional fixability metadata, - // and correctly handling optional trailing characters - // since trailing commas are optional in Rust and the last keyword may not have one - const keywordRegex = /,\s*(\w+)\s*,?\s*(?:(\w+)\s*,?\s*)?$/; - const keywordMatch = keywordRegex.exec(cleanBlock); - - if (keywordMatch === null) { - failureResultArray.push({ - value: effectiveRuleName, - scope: `unknown: ${scope}`, - category: 'unknown', - error: 'Could not extract keyword from macro block', - }); - return; - } - - if (ignoreCategories.has(keywordMatch[1])) { - skippedResultArray.push({ - value: effectiveRuleName, - scope: scope, - category: keywordMatch[1], - }); - return; - } - - successResultArray.push({ - value: effectiveRuleName, - scope: scope, - category: keywordMatch[1], - }); - - // special case for eslint and typescript alias rules - if (scope === 'eslint') { - const ruleName = effectiveRuleName.replace(/^.*\//, ''); - - if (typescriptRulesExtendEslintRules.includes(ruleName)) { - successResultArray.push({ - value: `@typescript-eslint/${ruleName}`, - scope: 'typescript', - category: keywordMatch[1], - }); - } - - // special case for jest and vitest alias rules - } else if (scope === 'jest') { - const ruleName = effectiveRuleName.replace(/^.*\//, ''); - - if (viteTestCompatibleRules.includes(ruleName)) { - successResultArray.push({ - value: `vitest/${ruleName}`, - scope: 'vitest', - category: keywordMatch[1], - }); - } + rule.scope = 'react_hooks'; } } -export function getFolderNameUnderRules(filePath: string) { - const sourceIndex = filePath.indexOf('/rules/'); - if (sourceIndex === -1) { - return ''; // 'rules' directory not found - } - - // Extract the substring starting after '/src/' - const subPath = filePath.slice(Math.max(0, sourceIndex + 7)); +function fixValueOfRule(rule: Rule): void { + if (rule.scope !== 'eslint') { + const scope = + rule.scope in aliasPluginNames + ? aliasPluginNames[rule.scope] + : rule.scope; - // Find the next '/' to isolate the folder name directly under 'src' - const nextSlashIndex = subPath.indexOf('/'); - if (nextSlashIndex === -1) { - return subPath; // Return the remaining path if there's no additional '/' + rule.value = `${scope}/${rule.value}`; } - - return subPath.slice(0, Math.max(0, nextSlashIndex)); } -export function getFileNameWithoutExtension( - filePath: string, - currentDirectory: string -) { - return path.basename(filePath) === 'mod.rs' - ? path.basename(currentDirectory) - : path.basename(filePath, path.extname(filePath)); -} - -export async function traverseRules(): Promise<{ - successResultArray: Rule[]; - skippedResultArray: Rule[]; - failureResultArray: Rule[]; -}> { - const successResultArray: Rule[] = []; - const skippedResultArray: Rule[] = []; - const failureResultArray: Rule[] = []; - - const startDirectory = path.join( - TARGET_DIRECTORY, - SPARSE_CLONE_DIRECTORY, - 'rules' +export function traverseRules(): Rule[] { + // get all rules and filter the ignored one + const rules = readFilesFromCommand().filter( + (rule) => + !ignoreCategories.has(rule.category) && !ignoreScope.has(rule.scope) ); - await readFilesRecursively( - startDirectory, - successResultArray, - skippedResultArray, - failureResultArray - ); - - successResultArray.sort((aRule, bRule) => { - const scopeCompare = aRule.scope.localeCompare(bRule.scope); - - if (scopeCompare !== 0) { - return scopeCompare; - } - - return aRule.value.localeCompare(bRule.value); - }); - - console.log( - `>> Parsed ${successResultArray.length} rules, skipped ${skippedResultArray.length} and encountered ${failureResultArray.length} failures\n` - ); + // fix value mapping + for (const rule of rules) { + fixScopeOfRule(rule); + fixValueOfRule(rule); + } - return { successResultArray, skippedResultArray, failureResultArray }; + return rules; } diff --git a/src/generated/rules-by-category.ts b/src/generated/rules-by-category.ts index 3e95913..5b9b027 100644 --- a/src/generated/rules-by-category.ts +++ b/src/generated/rules-by-category.ts @@ -5,6 +5,7 @@ const pedanticRules = { eqeqeq: 'off', 'max-classes-per-file': 'off', 'max-lines': 'off', + 'no-object-constructor': 'off', 'no-array-constructor': 'off', 'no-case-declarations': 'off', 'no-constructor-return': 'off', @@ -12,7 +13,6 @@ const pedanticRules = { 'no-fallthrough': 'off', 'no-inner-declarations': 'off', 'no-new-wrappers': 'off', - 'no-object-constructor': 'off', 'no-prototype-builtins': 'off', 'no-redeclare': 'off', 'no-self-compare': 'off', @@ -36,8 +36,6 @@ const pedanticRules = { 'react-hooks/rules-of-hooks': 'off', '@typescript-eslint/ban-ts-comment': 'off', '@typescript-eslint/ban-types': 'off', - '@typescript-eslint/no-array-constructor': 'off', - '@typescript-eslint/no-redeclare': 'off', '@typescript-eslint/no-unsafe-function-type': 'off', '@typescript-eslint/prefer-enum-initializers': 'off', '@typescript-eslint/prefer-ts-expect-error': 'off', @@ -78,7 +76,6 @@ const pedanticRules = { 'unicorn/prefer-string-slice': 'off', 'unicorn/prefer-type-error': 'off', 'unicorn/require-number-to-fixed-digits-argument': 'off', - 'vitest/no-conditional-in-test': 'off', } as const; const restrictionRules = { @@ -87,8 +84,8 @@ const restrictionRules = { 'no-bitwise': 'off', 'no-console': 'off', 'no-div-regex': 'off', - 'no-empty': 'off', 'no-empty-function': 'off', + 'no-empty': 'off', 'no-eq-null': 'off', 'no-eval': 'off', 'no-iterator': 'off', @@ -119,7 +116,6 @@ const restrictionRules = { 'react/no-unknown-property': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-dynamic-delete': 'off', - '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-empty-object-type': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-import-type-side-effects': 'off', @@ -127,7 +123,6 @@ const restrictionRules = { '@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-require-imports': 'off', - '@typescript-eslint/no-unused-expressions': 'off', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/prefer-literal-enum-member': 'off', 'unicorn/no-abusive-eslint-disable': 'off', @@ -149,8 +144,9 @@ const styleRules = { 'func-names': 'off', 'guard-for-in': 'off', 'max-params': 'off', - 'no-continue': 'off', + 'no-restricted-imports': 'off', 'no-duplicate-imports': 'off', + 'no-continue': 'off', 'no-label-var': 'off', 'no-magic-numbers': 'off', 'no-multi-str': 'off', @@ -167,7 +163,6 @@ const styleRules = { 'sort-keys': 'off', yoda: 'off', 'import/first': 'off', - 'import/no-named-default': 'off', 'import/no-namespace': 'off', 'jest/consistent-test-it': 'off', 'jest/max-expects': 'off', @@ -221,11 +216,8 @@ const styleRules = { '@typescript-eslint/consistent-generic-constructors': 'off', '@typescript-eslint/consistent-indexed-object-style': 'off', '@typescript-eslint/consistent-type-definitions': 'off', - '@typescript-eslint/default-param-last': 'off', - '@typescript-eslint/max-params': 'off', - '@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-magic-numbers': 'off', + '@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/prefer-for-of': 'off', '@typescript-eslint/prefer-function-type': 'off', '@typescript-eslint/prefer-namespace-keyword': 'off', @@ -256,14 +248,8 @@ const styleRules = { 'unicorn/switch-case-braces': 'off', 'unicorn/text-encoding-identifier-case': 'off', 'unicorn/throw-new-error': 'off', - 'vitest/consistent-test-it': 'off', - 'vitest/no-alias-methods': 'off', - 'vitest/no-identical-title': 'off', 'vitest/no-import-node-test': 'off', - 'vitest/no-restricted-jest-methods': 'off', - 'vitest/no-test-prefixes': 'off', 'vitest/prefer-each': 'off', - 'vitest/prefer-hooks-in-order': 'off', 'vitest/prefer-to-be-falsy': 'off', 'vitest/prefer-to-be-object': 'off', 'vitest/prefer-to-be-truthy': 'off', @@ -403,15 +389,12 @@ const correctnessRules = { 'react/no-render-return-value': 'off', 'react/no-string-refs': 'off', 'react/void-dom-elements-no-children': 'off', - '@typescript-eslint/no-dupe-class-members': 'off', '@typescript-eslint/no-duplicate-enum-values': 'off', '@typescript-eslint/no-extra-non-null-assertion': 'off', - '@typescript-eslint/no-loss-of-precision': 'off', '@typescript-eslint/no-misused-new': 'off', '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', '@typescript-eslint/no-this-alias': 'off', '@typescript-eslint/no-unsafe-declaration-merging': 'off', - '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/no-useless-empty-export': 'off', '@typescript-eslint/no-wrapper-object-types': 'off', '@typescript-eslint/prefer-as-const': 'off', @@ -429,14 +412,8 @@ const correctnessRules = { 'unicorn/no-useless-spread': 'off', 'unicorn/prefer-set-size': 'off', 'unicorn/prefer-string-starts-ends-with': 'off', - 'vitest/expect-expect': 'off', - 'vitest/no-conditional-expect': 'off', 'vitest/no-conditional-tests': 'off', - 'vitest/no-disabled-tests': 'off', - 'vitest/no-focused-tests': 'off', 'vitest/require-local-test-context-for-concurrent-snapshots': 'off', - 'vitest/valid-describe-callback': 'off', - 'vitest/valid-expect': 'off', } as const; const perfRules = { @@ -469,10 +446,8 @@ const suspiciousRules = { '@typescript-eslint/no-confusing-non-null-assertion': 'off', '@typescript-eslint/no-extraneous-class': 'off', '@typescript-eslint/no-unnecessary-type-constraint': 'off', - '@typescript-eslint/no-useless-constructor': 'off', 'unicorn/consistent-function-scoping': 'off', 'unicorn/prefer-add-event-listener': 'off', - 'vitest/no-commented-out-tests': 'off', } as const; export { diff --git a/src/generated/rules-by-scope.ts b/src/generated/rules-by-scope.ts index 3c751d8..53ed456 100644 --- a/src/generated/rules-by-scope.ts +++ b/src/generated/rules-by-scope.ts @@ -12,6 +12,9 @@ const eslintRules = { 'max-classes-per-file': 'off', 'max-lines': 'off', 'max-params': 'off', + 'no-restricted-imports': 'off', + 'no-object-constructor': 'off', + 'no-duplicate-imports': 'off', 'no-alert': 'off', 'no-array-constructor': 'off', 'no-async-promise-executor': 'off', @@ -36,13 +39,12 @@ const eslintRules = { 'no-dupe-else-if': 'off', 'no-dupe-keys': 'off', 'no-duplicate-case': 'off', - 'no-duplicate-imports': 'off', 'no-else-return': 'off', - 'no-empty': 'off', 'no-empty-character-class': 'off', 'no-empty-function': 'off', 'no-empty-pattern': 'off', 'no-empty-static-block': 'off', + 'no-empty': 'off', 'no-eq-null': 'off', 'no-eval': 'off', 'no-ex-assign': 'off', @@ -60,13 +62,12 @@ const eslintRules = { 'no-loss-of-precision': 'off', 'no-magic-numbers': 'off', 'no-multi-str': 'off', - 'no-new': 'off', 'no-new-func': 'off', 'no-new-native-nonconstructor': 'off', 'no-new-wrappers': 'off', + 'no-new': 'off', 'no-nonoctal-decimal-escape': 'off', 'no-obj-calls': 'off', - 'no-object-constructor': 'off', 'no-plusplus': 'off', 'no-proto': 'off', 'no-prototype-builtins': 'off', @@ -121,6 +122,7 @@ const eslintRules = { const importRules = { 'import/default': 'off', 'import/first': 'off', + 'import/no-namespace': 'off', 'import/max-dependencies': 'off', 'import/named': 'off', 'import/namespace': 'off', @@ -132,8 +134,6 @@ const importRules = { 'import/no-dynamic-require': 'off', 'import/no-named-as-default': 'off', 'import/no-named-as-default-member': 'off', - 'import/no-named-default': 'off', - 'import/no-namespace': 'off', 'import/no-self-import': 'off', 'import/no-webpack-loader-syntax': 'off', 'import/unambiguous': 'off', @@ -213,7 +213,6 @@ const jsdocRules = { const jsxA11yRules = { 'jsx-a11y/alt-text': 'off', - 'jsx-a11y/anchor-ambiguous-text': 'off', 'jsx-a11y/anchor-has-content': 'off', 'jsx-a11y/anchor-is-valid': 'off', 'jsx-a11y/aria-activedescendant-has-tabindex': 'off', @@ -240,6 +239,7 @@ const jsxA11yRules = { 'jsx-a11y/role-supports-aria-props': 'off', 'jsx-a11y/scope': 'off', 'jsx-a11y/tabindex-no-positive': 'off', + 'jsx-a11y/anchor-ambiguous-text': 'off', } as const; const nextjsRules = { @@ -273,9 +273,9 @@ const nodeRules = { const promiseRules = { 'promise/avoid-new': 'off', 'promise/catch-or-return': 'off', + 'promise/no-promise-in-callback': 'off', 'promise/no-callback-in-promise': 'off', 'promise/no-new-statics': 'off', - 'promise/no-promise-in-callback': 'off', 'promise/param-names': 'off', 'promise/prefer-await-to-callbacks': 'off', 'promise/prefer-await-to-then': 'off', @@ -299,8 +299,8 @@ const reactRules = { 'react/jsx-props-no-spread-multi': 'off', 'react/no-array-index-key': 'off', 'react/no-children-prop': 'off', - 'react/no-danger': 'off', 'react/no-danger-with-children': 'off', + 'react/no-danger': 'off', 'react/no-direct-mutation-state': 'off', 'react/no-find-dom-node': 'off', 'react/no-is-mounted': 'off', @@ -336,38 +336,27 @@ const typescriptRules = { '@typescript-eslint/consistent-generic-constructors': 'off', '@typescript-eslint/consistent-indexed-object-style': 'off', '@typescript-eslint/consistent-type-definitions': 'off', - '@typescript-eslint/default-param-last': 'off', '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/max-params': 'off', - '@typescript-eslint/no-array-constructor': 'off', + '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-confusing-non-null-assertion': 'off', - '@typescript-eslint/no-dupe-class-members': 'off', '@typescript-eslint/no-duplicate-enum-values': 'off', '@typescript-eslint/no-dynamic-delete': 'off', - '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/no-empty-object-type': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-extra-non-null-assertion': 'off', '@typescript-eslint/no-extraneous-class': 'off', '@typescript-eslint/no-import-type-side-effects': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-loss-of-precision': 'off', - '@typescript-eslint/no-magic-numbers': 'off', '@typescript-eslint/no-misused-new': 'off', '@typescript-eslint/no-namespace': 'off', '@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'off', '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-redeclare': 'off', '@typescript-eslint/no-require-imports': 'off', '@typescript-eslint/no-this-alias': 'off', '@typescript-eslint/no-unnecessary-type-constraint': 'off', '@typescript-eslint/no-unsafe-declaration-merging': 'off', '@typescript-eslint/no-unsafe-function-type': 'off', - '@typescript-eslint/no-unused-expressions': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/no-useless-constructor': 'off', '@typescript-eslint/no-useless-empty-export': 'off', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-wrapper-object-types': 'off', @@ -476,27 +465,13 @@ const unicornRules = { } as const; const vitestRules = { - 'vitest/consistent-test-it': 'off', - 'vitest/expect-expect': 'off', - 'vitest/no-alias-methods': 'off', - 'vitest/no-commented-out-tests': 'off', - 'vitest/no-conditional-expect': 'off', - 'vitest/no-conditional-in-test': 'off', 'vitest/no-conditional-tests': 'off', - 'vitest/no-disabled-tests': 'off', - 'vitest/no-focused-tests': 'off', - 'vitest/no-identical-title': 'off', 'vitest/no-import-node-test': 'off', - 'vitest/no-restricted-jest-methods': 'off', - 'vitest/no-test-prefixes': 'off', 'vitest/prefer-each': 'off', - 'vitest/prefer-hooks-in-order': 'off', 'vitest/prefer-to-be-falsy': 'off', 'vitest/prefer-to-be-object': 'off', 'vitest/prefer-to-be-truthy': 'off', 'vitest/require-local-test-context-for-concurrent-snapshots': 'off', - 'vitest/valid-describe-callback': 'off', - 'vitest/valid-expect': 'off', } as const; export { From 47d8469b4fe5688f8969541dad0ec9d8e08065e5 Mon Sep 17 00:00:00 2001 From: Sysix Date: Thu, 19 Dec 2024 17:26:03 +0100 Subject: [PATCH 2/4] build: use oxlint --rules --format=json for building rules --- scripts/traverse-rules.ts | 14 +++++++------- src/generated/rules-by-category.ts | 2 +- src/generated/rules-by-scope.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/traverse-rules.ts b/scripts/traverse-rules.ts index da16e22..fb26e90 100644 --- a/scripts/traverse-rules.ts +++ b/scripts/traverse-rules.ts @@ -32,14 +32,14 @@ function fixScopeOfRule(rule: Rule): void { } function fixValueOfRule(rule: Rule): void { - if (rule.scope !== 'eslint') { - const scope = - rule.scope in aliasPluginNames - ? aliasPluginNames[rule.scope] - : rule.scope; - - rule.value = `${scope}/${rule.value}`; + if (rule.scope === 'eslint') { + return; } + + const scope = + rule.scope in aliasPluginNames ? aliasPluginNames[rule.scope] : rule.scope; + + rule.value = `${scope}/${rule.value}`; } export function traverseRules(): Rule[] { diff --git a/src/generated/rules-by-category.ts b/src/generated/rules-by-category.ts index 5b9b027..57e8eb4 100644 --- a/src/generated/rules-by-category.ts +++ b/src/generated/rules-by-category.ts @@ -144,7 +144,6 @@ const styleRules = { 'func-names': 'off', 'guard-for-in': 'off', 'max-params': 'off', - 'no-restricted-imports': 'off', 'no-duplicate-imports': 'off', 'no-continue': 'off', 'no-label-var': 'off', @@ -163,6 +162,7 @@ const styleRules = { 'sort-keys': 'off', yoda: 'off', 'import/first': 'off', + 'import/no-named-default': 'off', 'import/no-namespace': 'off', 'jest/consistent-test-it': 'off', 'jest/max-expects': 'off', diff --git a/src/generated/rules-by-scope.ts b/src/generated/rules-by-scope.ts index 53ed456..626a7a8 100644 --- a/src/generated/rules-by-scope.ts +++ b/src/generated/rules-by-scope.ts @@ -12,7 +12,6 @@ const eslintRules = { 'max-classes-per-file': 'off', 'max-lines': 'off', 'max-params': 'off', - 'no-restricted-imports': 'off', 'no-object-constructor': 'off', 'no-duplicate-imports': 'off', 'no-alert': 'off', @@ -122,6 +121,7 @@ const eslintRules = { const importRules = { 'import/default': 'off', 'import/first': 'off', + 'import/no-named-default': 'off', 'import/no-namespace': 'off', 'import/max-dependencies': 'off', 'import/named': 'off', From 5dbb4dd439096b9d893db417b6ccbde10cebb04f Mon Sep 17 00:00:00 2001 From: Sysix Date: Thu, 19 Dec 2024 17:45:24 +0100 Subject: [PATCH 3/4] build: use oxlint --rules --format=json for building rules --- scripts/traverse-rules.ts | 32 +++++++++++++++++++++++++++++- src/generated/rules-by-category.ts | 25 +++++++++++++++++++++++ src/generated/rules-by-scope.ts | 25 +++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/scripts/traverse-rules.ts b/scripts/traverse-rules.ts index fb26e90..d0b9fda 100644 --- a/scripts/traverse-rules.ts +++ b/scripts/traverse-rules.ts @@ -3,6 +3,8 @@ import { ignoreCategories, ignoreScope } from './constants.js'; import { aliasPluginNames, reactHookRulesInsideReactScope, + typescriptRulesExtendEslintRules, + viteTestCompatibleRules, } from '../src/constants.js'; export interface Rule { @@ -42,6 +44,27 @@ function fixValueOfRule(rule: Rule): void { rule.value = `${scope}/${rule.value}`; } +function getAliasRules(rule: Rule): Rule | undefined { + if ( + rule.scope === 'eslint' && + typescriptRulesExtendEslintRules.includes(rule.value) + ) { + return { + value: `@typescript-eslint/${rule.value}`, + scope: 'typescript', + category: rule.category, + }; + } + + if (rule.scope === 'jest' && viteTestCompatibleRules.includes(rule.value)) { + return { + value: `vitest/${rule.value}`, + scope: 'vitest', + category: rule.category, + }; + } +} + export function traverseRules(): Rule[] { // get all rules and filter the ignored one const rules = readFilesFromCommand().filter( @@ -49,11 +72,18 @@ export function traverseRules(): Rule[] { !ignoreCategories.has(rule.category) && !ignoreScope.has(rule.scope) ); + const aliasRules: Rule[] = []; + // fix value mapping for (const rule of rules) { + const aliasRule = getAliasRules(rule); + if (aliasRule) { + aliasRules.push(aliasRule); + } + fixScopeOfRule(rule); fixValueOfRule(rule); } - return rules; + return [...rules, ...aliasRules]; } diff --git a/src/generated/rules-by-category.ts b/src/generated/rules-by-category.ts index 57e8eb4..5372be7 100644 --- a/src/generated/rules-by-category.ts +++ b/src/generated/rules-by-category.ts @@ -76,6 +76,9 @@ const pedanticRules = { 'unicorn/prefer-string-slice': 'off', 'unicorn/prefer-type-error': 'off', 'unicorn/require-number-to-fixed-digits-argument': 'off', + '@typescript-eslint/no-array-constructor': 'off', + '@typescript-eslint/no-redeclare': 'off', + 'vitest/no-conditional-in-test': 'off', } as const; const restrictionRules = { @@ -136,6 +139,8 @@ const restrictionRules = { 'unicorn/prefer-modern-math-apis': 'off', 'unicorn/prefer-node-protocol': 'off', 'unicorn/prefer-number-properties': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-unused-expressions': 'off', } as const; const styleRules = { @@ -253,6 +258,15 @@ const styleRules = { 'vitest/prefer-to-be-falsy': 'off', 'vitest/prefer-to-be-object': 'off', 'vitest/prefer-to-be-truthy': 'off', + '@typescript-eslint/default-param-last': 'off', + '@typescript-eslint/max-params': 'off', + '@typescript-eslint/no-magic-numbers': 'off', + 'vitest/consistent-test-it': 'off', + 'vitest/no-alias-methods': 'off', + 'vitest/no-identical-title': 'off', + 'vitest/no-restricted-jest-methods': 'off', + 'vitest/no-test-prefixes': 'off', + 'vitest/prefer-hooks-in-order': 'off', } as const; const correctnessRules = { @@ -414,6 +428,15 @@ const correctnessRules = { 'unicorn/prefer-string-starts-ends-with': 'off', 'vitest/no-conditional-tests': 'off', 'vitest/require-local-test-context-for-concurrent-snapshots': 'off', + '@typescript-eslint/no-dupe-class-members': 'off', + '@typescript-eslint/no-loss-of-precision': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'vitest/expect-expect': 'off', + 'vitest/no-conditional-expect': 'off', + 'vitest/no-disabled-tests': 'off', + 'vitest/no-focused-tests': 'off', + 'vitest/valid-describe-callback': 'off', + 'vitest/valid-expect': 'off', } as const; const perfRules = { @@ -448,6 +471,8 @@ const suspiciousRules = { '@typescript-eslint/no-unnecessary-type-constraint': 'off', 'unicorn/consistent-function-scoping': 'off', 'unicorn/prefer-add-event-listener': 'off', + '@typescript-eslint/no-useless-constructor': 'off', + 'vitest/no-commented-out-tests': 'off', } as const; export { diff --git a/src/generated/rules-by-scope.ts b/src/generated/rules-by-scope.ts index 626a7a8..d1b44fe 100644 --- a/src/generated/rules-by-scope.ts +++ b/src/generated/rules-by-scope.ts @@ -368,6 +368,17 @@ const typescriptRules = { '@typescript-eslint/prefer-namespace-keyword': 'off', '@typescript-eslint/prefer-ts-expect-error': 'off', '@typescript-eslint/triple-slash-reference': 'off', + '@typescript-eslint/default-param-last': 'off', + '@typescript-eslint/max-params': 'off', + '@typescript-eslint/no-array-constructor': 'off', + '@typescript-eslint/no-dupe-class-members': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-loss-of-precision': 'off', + '@typescript-eslint/no-magic-numbers': 'off', + '@typescript-eslint/no-redeclare': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-useless-constructor': 'off', } as const; const unicornRules = { @@ -472,6 +483,20 @@ const vitestRules = { 'vitest/prefer-to-be-object': 'off', 'vitest/prefer-to-be-truthy': 'off', 'vitest/require-local-test-context-for-concurrent-snapshots': 'off', + 'vitest/consistent-test-it': 'off', + 'vitest/expect-expect': 'off', + 'vitest/no-alias-methods': 'off', + 'vitest/no-commented-out-tests': 'off', + 'vitest/no-conditional-expect': 'off', + 'vitest/no-conditional-in-test': 'off', + 'vitest/no-disabled-tests': 'off', + 'vitest/no-focused-tests': 'off', + 'vitest/no-identical-title': 'off', + 'vitest/no-restricted-jest-methods': 'off', + 'vitest/no-test-prefixes': 'off', + 'vitest/prefer-hooks-in-order': 'off', + 'vitest/valid-describe-callback': 'off', + 'vitest/valid-expect': 'off', } as const; export { From 73d06c73959ee475d99bc8abaa99595602e33560 Mon Sep 17 00:00:00 2001 From: Sysix Date: Thu, 19 Dec 2024 18:02:06 +0100 Subject: [PATCH 4/4] build: use oxlint --rules --format=json for building rules --- scripts/traverse-rules.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/scripts/traverse-rules.ts b/scripts/traverse-rules.ts index d0b9fda..ffae7c4 100644 --- a/scripts/traverse-rules.ts +++ b/scripts/traverse-rules.ts @@ -7,13 +7,16 @@ import { viteTestCompatibleRules, } from '../src/constants.js'; -export interface Rule { +export type Rule = { value: string; scope: string; category: string; -} +}; -function readFilesFromCommand(): Rule[] { +/** + * Read the rules from oxlint command and returns an array of Rule-Objects + */ +function readRulesFromCommand(): Rule[] { // do not handle the exception const oxlintOutput = execSync(`npx oxlint --rules --format=json`, { encoding: 'utf8', @@ -24,6 +27,9 @@ function readFilesFromCommand(): Rule[] { return JSON.parse(oxlintOutput); } +/** + * Some rules are in a different scope then in eslint + */ function fixScopeOfRule(rule: Rule): void { if ( rule.scope === 'react' && @@ -33,6 +39,9 @@ function fixScopeOfRule(rule: Rule): void { } } +/** + * oxlint returns the value without a scope name + */ function fixValueOfRule(rule: Rule): void { if (rule.scope === 'eslint') { return; @@ -44,6 +53,10 @@ function fixValueOfRule(rule: Rule): void { rule.value = `${scope}/${rule.value}`; } +/** + * some rules are reimplemented in another scope + * remap them so we can disable all the reimplemented too + */ function getAliasRules(rule: Rule): Rule | undefined { if ( rule.scope === 'eslint' && @@ -67,14 +80,13 @@ function getAliasRules(rule: Rule): Rule | undefined { export function traverseRules(): Rule[] { // get all rules and filter the ignored one - const rules = readFilesFromCommand().filter( + const rules = readRulesFromCommand().filter( (rule) => !ignoreCategories.has(rule.category) && !ignoreScope.has(rule.scope) ); const aliasRules: Rule[] = []; - // fix value mapping for (const rule of rules) { const aliasRule = getAliasRules(rule); if (aliasRule) {