diff --git a/CHANGELOG.md b/CHANGELOG.md index e71e565..b59b628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 1.9.3 + +- Added a few minor ESLint rules to be disabled. +- Now using Biome's JSON Metadata to generate this package's rules to be disabled. Many thanks, [ematipico](https://github.com/ematipico)! + ## 1.8.3 - Fix issue with ESLint's Flat Config ([#7](https://github.com/SrBrahma/eslint-config-biome/issues/7)). diff --git a/README.md b/README.md index d0289d5..4f79b6c 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,7 @@ npm install -D eslint-config-biome # or your preferred package manager ;) - Since v1.4.0, this package includes [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier), so formatting rules are also disabled as Biome is equivalent to Prettier. [Attribution](ATTRIBUTION.md). -- This package had its origin [in this discussion](https://github.com/biomejs/biome/discussions/3#discussioncomment-7876363). Thanks [DaniGuardiola -](https://github.com/DaniGuardiola) for [your *related initial crawler code*](https://github.com/biomejs/biome/discussions/3#discussioncomment-7910787)! +- This package had its origin [in this discussion](https://github.com/biomejs/biome/discussions/3#discussioncomment-7876363). ## 📰 [Changelog](CHANGELOG.md) diff --git a/index.js b/index.js index aa884d2..c96e2a9 100644 --- a/index.js +++ b/index.js @@ -26,6 +26,7 @@ module.exports = { "no-delete-var": "off", "no-dupe-args": "off", "no-dupe-class-members": "off", + "no-dupe-else-if": "off", "no-dupe-keys": "off", "no-duplicate-case": "off", "no-else-return": "off", @@ -60,7 +61,7 @@ module.exports = { "no-sequences": "off", "no-setter-return": "off", "no-shadow-restricted-names": "off", - "no-sparse-array": "off", + "no-sparse-arrays": "off", "no-this-before-super": "off", "no-unneeded-ternary": "off", "no-unreachable": "off", @@ -72,6 +73,7 @@ module.exports = { "no-useless-catch": "off", "no-useless-computed-key": "off", "no-useless-constructor": "off", + "no-useless-escape": "off", "no-useless-rename": "off", "no-var": "off", "no-with": "off", @@ -86,12 +88,13 @@ module.exports = { "require-yield": "off", "use-isnan": "off", "valid-typeof": "off", - "@mysticatea/eslint-plugin/no-this-in-static": "off", + "@eslint-community/eslint-plugin-mysticatea/no-this-in-static": "off", "@typescript-eslint/ban-types": "off", "@typescript-eslint/consistent-type-exports": "off", "@typescript-eslint/consistent-type-imports": "off", "@typescript-eslint/default-param-last": "off", "@typescript-eslint/dot-notation": "off", + "@typescript-eslint/explicit-member-accessibility": "off", "@typescript-eslint/no-dupe-class-members": "off", "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-explicit-any": "off", @@ -118,6 +121,7 @@ module.exports = { "@typescript-eslint/prefer-literal-enum-member": "off", "@typescript-eslint/prefer-namespace-keyword": "off", "@typescript-eslint/prefer-optional-chain": "off", + "eslint-plugin-mysticatea/no-this-in-static": "off", "jest/max-nested-describe": "off", "jest/no-duplicate-hooks": "off", "jest/no-export": "off", @@ -148,7 +152,9 @@ module.exports = { "jsx-a11y/no-noninteractive-element-to-interactive-role": "off", "jsx-a11y/no-noninteractive-tabindex": "off", "jsx-a11y/no-redundant-roles": "off", + "jsx-a11y/prefer-tag-over-role": "off", "jsx-a11y/role-has-required-aria-props": "off", + "jsx-a11y/role-supports-aria-props": "off", "jsx-a11y/scope": "off", "jsx-a11y/tabindex-no-positive": "off", "react/button-has-type": "off", @@ -164,6 +170,7 @@ module.exports = { "react/void-dom-elements-no-children": "off", "react-hooks/exhaustive-deps": "off", "simple-import-sort/imports": "off", + "sonarjs/prefer-while": "off", "stylistic/jsx-self-closing-comp": "off", "unicorn/new-for-builtins": "off", "unicorn/no-array-for-each": "off", diff --git a/package.json b/package.json index 1dd05ec..4c1aa2a 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "eslint-config-biome", - "version": "1.8.4", + "version": "1.9.3", "description": "Disables ESLint rules that have a recommended and equivalent Biome rule", "main": "index.js", "scripts": { + "dev": "bun --watch run scripts/index.ts", "format": "biome check --apply-unsafe . && eslint --fix . package.json", "format:check": "biome check . && eslint . package.json", "pre-commit": "bun format && bun typecheck && bun test && git add -A", @@ -11,8 +12,7 @@ "start": "bun i && bun run scripts/index.ts", "test:watch": "bun test --watch", "tsw": "tsc --watch --noEmit", - "typecheck": "tsc --noEmit", - "watch": "bun --watch run scripts/index.ts" + "typecheck": "tsc --noEmit" }, "files": [ "index.js", @@ -33,13 +33,11 @@ ], "repository": "SrBrahma/eslint-config-biome", "devDependencies": { - "@biomejs/biome": "^1.8.3", + "@biomejs/biome": "^1.9.3", "@sindresorhus/tsconfig": "^5.1.1", - "@types/bun": "^1.1.8", - "@types/jsdom": "^21.1.7", + "@types/bun": "^1.1.11", "eslint-config-gev": "4.5.1", - "husky": "^9.1.5", - "jsdom": "^24.1.3", - "typescript": "^5.5.4" + "husky": "^9.1.6", + "typescript": "^5.6.3" } } diff --git a/scripts/consts.ts b/scripts/consts.ts index 60124b7..f73b72c 100644 --- a/scripts/consts.ts +++ b/scripts/consts.ts @@ -6,6 +6,10 @@ export const filenames = { } export const rootPath = path.resolve(__dirname, "..") +/** + * Some ESLint rules are already taken care by Biome but it's not specified from the used sources of information that they do. + * So we manually disable them here. + */ export const extraRulesToDisable = [ "simple-import-sort/imports", // Not being added for some reason, TODO check it and fix it @@ -14,6 +18,12 @@ export const extraRulesToDisable = [ "no-delete-var", "no-return-assign", "no-useless-computed-key", - "unicorn/no-static-only-class", "unicorn/no-typeof-undefined", + "@typescript-eslint/no-useless-template-literals", + // There are two packages for mysticatea. One is already handled by the code, this handles the other package. + "eslint-plugin-mysticatea/no-this-in-static", + // https://biomejs.dev/linter/rules/use-import-type/ + "@typescript-eslint/no-import-type-side-effects", + // https://biomejs.dev/linter/rules/no-useless-type-constraint/ + "@typescript-eslint/no-unnecessary-type-arguments", ] diff --git a/scripts/fetchFromDocs.ts b/scripts/fetchFromDocs.ts deleted file mode 100644 index eb7fb76..0000000 --- a/scripts/fetchFromDocs.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { JSDOM } from "jsdom" - -export const fetchRecommendedBiomeRulesFromDocs = async (): Promise< - Array -> => { - const response = await fetch("https://biomejs.dev/linter/rules/") - const html = await response.text() - - const dom = new JSDOM(html) - const document = dom.window.document - - const rows = document.querySelectorAll("table tr") - - const recommendedRules = [...rows] - .map((row) => { - const cells = row.querySelectorAll("td") - const rowTexts = [...cells].map((cell) => cell.textContent?.trim() ?? "") - - return rowTexts - }) - .filter((rowTexts) => rowTexts[2]?.includes("✅")) - .map((recommendedRowTexts) => recommendedRowTexts[0] ?? "") - - return recommendedRules -} - -type Equivalency = { eslint: string; biome: string } - -const pluginBlacklist = ["Clippy"] - -const fetchEquivalentRulesFromDocs = async (): Promise> => { - const response = await fetch("https://biomejs.dev/linter/rules-sources/") - const html = await response.text() - - const dom = new JSDOM(html) - const document = dom.window.document - - const tables = document.querySelectorAll("table") - - const rowsTexts: Array> = [...tables].flatMap((table) => { - const tableHeaders = Array.from(table.querySelectorAll("th")).map( - (th) => th.textContent?.trim() ?? "", - ) - - const pluginName = tableHeaders[0] - ?.split(" ")[0] - ?.replace("eslint-plugin-", "") - ?.replace("typescript-eslint", "@typescript-eslint") - - if (!pluginName) throw new Error("Invalid plugin") - - if (pluginBlacklist.includes(pluginName)) return [[]] - - const prefix = ["Clippy", "ESLint"].includes(pluginName) - ? "" - : `${pluginName}/` - - const rows = [...table.querySelectorAll("tr")].filter((tr) => - tr.querySelector("td"), - ) - - const rowsTexts = rows.map((row) => { - const rowTexts = [...row.querySelectorAll("td, td")].map( - (cell) => cell.textContent?.trim() ?? "", - ) - - rowTexts[0] = prefix + rowTexts[0] - // Sometimes they have `${biomeRuleName} (inspired)`. We get just the first word. - rowTexts[1] = rowTexts[1]?.split(" ")[0] ?? "" - - return rowTexts - }) - - return rowsTexts - }) - - const equivalentRules: Array = rowsTexts.flatMap((rowTexts) => ({ - eslint: rowTexts[0] ?? "", - biome: rowTexts[1] ?? "", - })) - - return equivalentRules -} - -export const getEquivalentRulesFromDocs = async (): Promise> => { - const recommendedBiomeRules = await fetchRecommendedBiomeRulesFromDocs() - const equivalentRules = await fetchEquivalentRulesFromDocs() - - return recommendedBiomeRules - .map( - (biomeRule) => - equivalentRules.find( - (equivalentRule) => equivalentRule.biome === biomeRule, - )?.eslint, - ) - .filter(Boolean) as Array -} diff --git a/scripts/fetchFromGithub.ts b/scripts/fetchFromGithub.ts deleted file mode 100644 index 3c02173..0000000 --- a/scripts/fetchFromGithub.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { JSDOM } from "jsdom" - -const getTdString = (row: Element, column: number) => - ( - row.querySelector(`td:nth-child(${column})`) as HTMLTableCellElement - ).textContent?.trim() - -type Plugin = { id: string; prefix: string } - -/** Returns the ESLint rules for the equivalent Biome's rules that are recommended */ -const getEslintEquivalentRulesForPlugin = ( - plugin: Plugin, - document: Document, -): Array => { - const table = ( - document.querySelector(`#user-content-${plugin.id}`)?.parentNode as Element - ).nextElementSibling as HTMLTableElement | null - - if (!table) throw new Error(`Missing table for plugin ${plugin.id}`) - - const rows = Array.from(table.querySelectorAll("tbody > tr")) - - const eslintRules: Array = [] - - rows.forEach((row) => { - const eslintRule = getTdString(row, 1) - const biomeRule = getTdString(row, 3) - const isBiomeRecommended = getTdString(row, 4)?.includes("✅") - - if (eslintRule && biomeRule && isBiomeRecommended) { - eslintRules.push(plugin.prefix + eslintRule) - } - }) - - return eslintRules -} - -export const getEslintEquivalentRulesFromGithub = async (): Promise< - Array -> => { - const plugins: Array<{ id: string; prefix: string }> = [ - { id: "eslint", prefix: "" }, - { id: "typescript-eslint", prefix: "@typescript-eslint/" }, - { id: "eslint-plugin-jest", prefix: "jest/" }, - { id: "eslint-plugin-jsx-a11y", prefix: "jsx-a11y/" }, - { id: "eslint-plugin-react", prefix: "react/" }, - { id: "eslint-plugin-react-hooks", prefix: "react-hooks/" }, - { id: "eslint-plugin-unicorn", prefix: "unicorn/" }, - ] - - const response = await fetch("https://github.com/biomejs/biome/discussions/3") - const text = await response.text() - const dom = new JSDOM(text) - const document = dom.window.document - - return plugins.flatMap((plugin) => - getEslintEquivalentRulesForPlugin(plugin, document), - ) -} diff --git a/scripts/index.ts b/scripts/index.ts index 537c0aa..645ca86 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -1,18 +1,13 @@ /** Attribution for eslint-config-prettier is available at the ATTRIBUTION.md and in the eslint-config-prettier.js. */ import { extraRulesToDisable, filenames } from "./consts" -import { getEquivalentRulesFromDocs } from "./fetchFromDocs" -import { getEslintEquivalentRulesFromGithub } from "./fetchFromGithub.js" +import { getRulesFromJsonMetadata } from "./metadata.js" import { createPrettierFile } from "./prettier" import { getJsBaseRules, getTsExtensionsForRules } from "./tsExtensions" import { sortRules, writeMainFile } from "./utils" const main = async () => { - const rules = [ - ...(await getEquivalentRulesFromDocs()), - ...extraRulesToDisable, - ...(await getEslintEquivalentRulesFromGithub()), - ] + const rules = [...(await getRulesFromJsonMetadata()), ...extraRulesToDisable] const rulesWithTsExtends = [ ...rules, @@ -21,8 +16,9 @@ const main = async () => { ] const rulesNoDuplicates = [...new Set(rulesWithTsExtends)] + const sortedRules = sortRules(rulesNoDuplicates) - writeMainFile(sortRules(rulesNoDuplicates)) + writeMainFile(sortedRules) await createPrettierFile() console.log(`Generated ${filenames.index} & ${filenames.prettier}!`) diff --git a/scripts/metadata.ts b/scripts/metadata.ts new file mode 100644 index 0000000..46992c5 --- /dev/null +++ b/scripts/metadata.ts @@ -0,0 +1,169 @@ +/** + * For future references: + * + * Origin: https://github.com/SrBrahma/eslint-config-biome/issues/5#issuecomment-2399547365 + * Rules Metadata: https://biomejs.dev/metadata/rules.json + * Json Schema: https://biomejs.dev/metadata/schema.json + * Tool to TS type, then requires manual changes: https://transform.tools/json-schema-to-typescript + * Helper: https://10015.io/tools/json-tree-viewer + */ + +type FixKind = "none" | "safe" | "unsafe" + +type RuleSourceKind = "sameLogic" | "inspired" + +type Languages = "css" | "js" | "json" | "jsx" | "ts" + +const ruleSourceToPrefix: Record = { + eslint: "", + eslintGraphql: "graphql/", + eslintImport: "import/", + eslintImportAccess: "import-access/", + eslintJest: "jest/", + eslintJsxA11y: "jsx-a11y/", + eslintReact: "react/", + eslintReactHooks: "react-hooks/", + eslintReactRefresh: "react-refresh/", + eslintSolid: "solid/", + eslintSonarJs: "sonarjs/", + eslintStylistic: "stylistic/", + eslintTypeScript: "@typescript-eslint/", + eslintUnicorn: "unicorn/", + eslintUnusedImports: "unused-imports/", + eslintMysticatea: "@eslint-community/eslint-plugin-mysticatea/", + eslintBarrelFiles: "barrel-files/", + eslintN: "n/", + eslintNext: "@next/eslint-plugin-next/", + eslintNoSecrets: "no-secrets/", +} + +const skippedRulesSources = new Set() + +const usedRulesSources = new Set() + +const getEslintRulePrefix = ( + ruleSource: string, + ruleName: string, +): string | undefined => { + const rulePrefix = ruleSourceToPrefix[ruleSource] + + if (rulePrefix === undefined) { + skippedRulesSources.add(ruleSource) + + return undefined + } + + usedRulesSources.add(ruleSource) + + return `${rulePrefix}${ruleName}` +} + +export type RuleMetadata = { + /** + * It marks if a rule is deprecated, and if so a reason has to be provided. + */ + deprecated?: boolean + docs?: string + /** + * The kind of fix + */ + fixKind?: FixKind + /** + * The rule's documentation URL + */ + link?: string + /** + * The name of this rule, displayed in the diagnostics it emits + */ + name?: string + /** + * Whether a rule is recommended or not + */ + recommended?: boolean + /** + * The source kind of the rule + */ + sourceKind?: RuleSourceKind | null + /** + * The source metadata of the rule + */ + sources?: Array> + /** + * The version when the rule was implemented + */ + version?: string +} + +export type RulesGroup = { + languages: Record< + Languages, + { + [ruleSubSet: string]: { + [biomeRule: string]: RuleMetadata + } + } + > + numberOrRules?: number +} + +export type RulesMetadata = Record + +const getAllRules = async (): Promise> => { + const response = await fetch("https://biomejs.dev/metadata/rules.json") + const metadata = (await response.json()) as RulesMetadata + + const rules: Array = [] + + Object.values(metadata).forEach((ruleGroup) => { + Object.values(ruleGroup.languages).forEach((language) => { + Object.values(language).forEach((ruleSubSet) => { + Object.values(ruleSubSet).forEach((rule) => { + rules.push(rule) + }) + }) + }) + }) + + return rules +} + +export const getRulesFromJsonMetadata = async (): Promise> => { + const allRules = await getAllRules() + + const filteredRules = allRules.filter( + (rule) => rule.recommended && rule.sources?.length, + ) + + const rulesToDisable: Array = [] + + filteredRules.forEach((rule) => { + rule.sources?.forEach((ruleSource) => { + if (Object.keys(ruleSource).length > 1) + throw new Error("Rule source has more than one key!") + const key = Object.keys(ruleSource)[0] + + if (!key) throw new Error("Rule source has no key!") + + const val = ruleSource[key] + + if (!val) throw new Error("Rule source has no value!") + + const ruleToDisable = getEslintRulePrefix(key, val) + + if (ruleToDisable) rulesToDisable.push(ruleToDisable) + }) + }) + + console.warn( + "Skipped the following rule sources:", + [...skippedRulesSources.values()].sort(), + "Expected: clippy, stylelint.", + ) + + console.info( + "Used the following rule sources:", + [...usedRulesSources.values()].sort(), + ) + + return rulesToDisable +} diff --git a/scripts/tsExtensions.ts b/scripts/tsExtensions.ts index ab46f3a..93218fb 100644 --- a/scripts/tsExtensions.ts +++ b/scripts/tsExtensions.ts @@ -5,9 +5,9 @@ import { rootPath } from "./consts" export const getTsExtensionsForRules = ( rules: Array, ): Array => { - const glob = new Bun.Glob("*.js") + const jsGlob = new Bun.Glob("*.js") const tsExtensionRules = [ - ...glob.scanSync( + ...jsGlob.scanSync( path.resolve( rootPath, "node_modules/@typescript-eslint/eslint-plugin/dist/rules", @@ -27,9 +27,9 @@ export const getTsExtensionsForRules = ( } /** The Biome docs has some rules just under the Typescript table, but we also need to disable the JS base rules. */ export const getJsBaseRules = (allRules: Array): Array => { - const glob = new Bun.Glob("*.js") + const jsGlob = new Bun.Glob("*.js") const jsRules = [ - ...glob.scanSync(path.resolve(rootPath, "node_modules/eslint/lib/rules")), + ...jsGlob.scanSync(path.resolve(rootPath, "node_modules/eslint/lib/rules")), ] .map((s) => s.replace(".js", "")) .toSorted() // Sort to avoid diffs