diff --git a/.changeset/smooth-sites-sip.md b/.changeset/smooth-sites-sip.md new file mode 100644 index 000000000000..7703256415dc --- /dev/null +++ b/.changeset/smooth-sites-sip.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Improved the summary provided by `biome migrate eslint` to be clearer on why rules were not migrated. Biome now specifies a reason when a rule is not migrated, such as being incompatible with the formatter or not implemented yet. This helps users make more informed decisions when migrating their ESLint configurations to Biome. diff --git a/crates/biome_analyze/src/rule.rs b/crates/biome_analyze/src/rule.rs index d51040741a6e..3734878e8d1a 100644 --- a/crates/biome_analyze/src/rule.rs +++ b/crates/biome_analyze/src/rule.rs @@ -93,94 +93,88 @@ impl TryFrom for Applicability { } } -#[derive(Debug, Clone, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub enum RuleSource { +pub enum RuleSource<'a> { /// Rules from [Rust Clippy](https://rust-lang.github.io/rust-clippy/master/index.html) - Clippy(&'static str), + Clippy(&'a str), /// Rules from [Deno Lint](https://github.com/denoland/deno_lint) - DenoLint(&'static str), + DenoLint(&'a str), /// Rules from [Eslint](https://eslint.org/) - Eslint(&'static str), + Eslint(&'a str), /// Rules from [Eslint Plugin Barrel Files](https://github.com/thepassle/eslint-plugin-barrel-files) - EslintBarrelFiles(&'static str), + EslintBarrelFiles(&'a str), /// Rules from [GraphQL-ESLint](https://github.com/graphql-hive/graphql-eslint) - EslintGraphql(&'static str), + EslintGraphql(&'a str), /// Rules from [Eslint Plugin Import](https://github.com/import-js/eslint-plugin-import) - EslintImport(&'static str), + EslintImport(&'a str), /// Rules from [Eslint Plugin Import Access](https://github.com/uhyo/eslint-plugin-import-access) - EslintImportAccess(&'static str), + EslintImportAccess(&'a str), /// Rules from [Eslint Plugin Jest](https://github.com/jest-community/eslint-plugin-jest) - EslintJest(&'static str), + EslintJest(&'a str), /// Rules from [Eslint Plugin JSDOc](https://github.com/gajus/eslint-plugin-jsdoc) - EslintJsDoc(&'static str), + EslintJsDoc(&'a str), /// Rules from [Eslint Plugin JSX A11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) - EslintJsxA11y(&'static str), + EslintJsxA11y(&'a str), /// Rules from [Eslint Plugin Mysticatea](https://github.com/mysticatea/eslint-plugin) - EslintMysticatea(&'static str), + EslintMysticatea(&'a str), /// Rules from [Eslint Plugin N](https://github.com/eslint-community/eslint-plugin-n) - EslintN(&'static str), + EslintN(&'a str), /// Rules from [Eslint Plugin Next](https://github.com/vercel/next.js/tree/canary/packages/eslint-plugin-next) - EslintNext(&'static str), + EslintNext(&'a str), /// Rules from [Eslint Plugin No Secrets](https://github.com/nickdeis/eslint-plugin-no-secrets) - EslintNoSecrets(&'static str), + EslintNoSecrets(&'a str), /// Rules from [Eslint Plugin Package.json](https://github.com/JoshuaKGoldberg/eslint-plugin-package-json) - EslintPackageJson(&'static str), + EslintPackageJson(&'a str), /// Rules from [Eslint Plugin Package.json Dependencies](https://github.com/idan-at/eslint-plugin-package-json-dependencies) - EslintPackageJsonDependencies(&'static str), + EslintPackageJsonDependencies(&'a str), /// Rules from [Eslint Plugin Perfectionist](https://perfectionist.dev/) - EslintPerfectionist(&'static str), + EslintPerfectionist(&'a str), /// Rules from [Eslint Plugin Qwik](https://github.com/QwikDev/qwik) - EslintQwik(&'static str), + EslintQwik(&'a str), /// Rules from [Eslint Plugin React](https://github.com/jsx-eslint/eslint-plugin-react) - EslintReact(&'static str), + EslintReact(&'a str), /// Rules from [Eslint Plugin React Hooks](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/README.md) - EslintReactHooks(&'static str), + EslintReactHooks(&'a str), /// Rules from [Eslint Plugin React Prefer Function Component](https://github.com/tatethurston/eslint-plugin-react-prefer-function-component) - EslintReactPreferFunctionComponent(&'static str), + EslintReactPreferFunctionComponent(&'a str), /// Rules from [Eslint Plugin React Refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) - EslintReactRefresh(&'static str), + EslintReactRefresh(&'a str), /// Rules from [eslint-react.xyz](https://eslint-react.xyz/) - EslintReactX(&'static str), + EslintReactX(&'a str), /// Rules from [eslint-react.xyz](https://eslint-react.xyz/) - EslintReactXyz(&'static str), + EslintReactXyz(&'a str), /// Rules from [Eslint Plugin Regexp](https://github.com/ota-meshi/eslint-plugin-regexp) - EslintRegexp(&'static str), + EslintRegexp(&'a str), /// Rules from [Eslint Plugin Solid](https://github.com/solidjs-community/eslint-plugin-solid) - EslintSolid(&'static str), + EslintSolid(&'a str), /// Rules from [Eslint Plugin Sonar](https://github.com/SonarSource/eslint-plugin-sonarjs) - EslintSonarJs(&'static str), + EslintSonarJs(&'a str), /// Rules from [Eslint Plugin Stylistic](https://eslint.style) - EslintStylistic(&'static str), + EslintStylistic(&'a str), /// Rules from [Eslint Plugin Typescript](https://typescript-eslint.io) - EslintTypeScript(&'static str), + EslintTypeScript(&'a str), /// Rules from [Eslint Plugin Unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) - EslintUnicorn(&'static str), + EslintUnicorn(&'a str), /// Rules from [Eslint Plugin Unused Imports](https://github.com/sweepline/eslint-plugin-unused-imports) - EslintUnusedImports(&'static str), + EslintUnusedImports(&'a str), /// Rules from [Eslint Plugin Vitest](https://github.com/vitest-dev/eslint-plugin-vitest) - EslintVitest(&'static str), + EslintVitest(&'a str), /// Rules from [Eslint Plugin Vue.js](https://eslint.vuejs.org/) - EslintVueJs(&'static str), + EslintVueJs(&'a str), /// Rules from [graphql-schema-linter](https://github.com/cjoudrey/graphql-schema-linter) - GraphqlSchemaLinter(&'static str), + GraphqlSchemaLinter(&'a str), /// Rules from [Stylelint](https://github.com/stylelint/stylelint) - Stylelint(&'static str), + Stylelint(&'a str), /// Rules from [Eslint Plugin Turbo](https://github.com/vercel/turborepo/tree/main/packages/eslint-plugin-turbo) - EslintTurbo(&'static str), + EslintTurbo(&'a str), /// Rules from [html-eslint](https://html-eslint.org/) - HtmlEslint(&'static str), + HtmlEslint(&'a str), } -impl PartialEq for RuleSource { - fn eq(&self, other: &Self) -> bool { - std::mem::discriminant(self) == std::mem::discriminant(other) - } -} - -impl std::fmt::Display for RuleSource { +impl<'a> std::fmt::Display for RuleSource<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Clippy(_) => write!(f, "Clippy"), @@ -228,46 +222,77 @@ impl std::fmt::Display for RuleSource { } } -impl PartialOrd for RuleSource { +impl<'a> PartialOrd for RuleSource<'a> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for RuleSource { +impl<'a> Ord for RuleSource<'a> { fn cmp(&self, other: &Self) -> Ordering { - if let (Self::Eslint(self_rule), Self::Eslint(other_rule)) = (self, other) { - self_rule.cmp(other_rule) - } else if self.is_eslint() { - Ordering::Greater - } else if other.is_eslint() { - Ordering::Less - } else { - let self_rule = self.as_rule_name(); - let other_rule = other.as_rule_name(); - self_rule.cmp(other_rule) - } + self.sort_key().cmp(&other.sort_key()) } } -impl RuleSource { - /// The rule has the same logic as the declared rule. - pub const fn same(self) -> RuleSourceWithKind { - RuleSourceWithKind { - kind: RuleSourceKind::SameLogic, - source: self, +impl<'a> RuleSource<'a> { + /// Returns the variant index for sorting and comparison across lifetimes. + pub const fn variant_index(&self) -> u16 { + match self { + Self::Clippy(_) => 0, + Self::DenoLint(_) => 1, + Self::Eslint(_) => 2, + Self::EslintBarrelFiles(_) => 3, + Self::EslintGraphql(_) => 4, + Self::EslintImport(_) => 5, + Self::EslintImportAccess(_) => 6, + Self::EslintJest(_) => 7, + Self::EslintJsDoc(_) => 8, + Self::EslintJsxA11y(_) => 9, + Self::EslintMysticatea(_) => 10, + Self::EslintN(_) => 11, + Self::EslintNext(_) => 12, + Self::EslintNoSecrets(_) => 13, + Self::EslintPackageJson(_) => 14, + Self::EslintPackageJsonDependencies(_) => 15, + Self::EslintPerfectionist(_) => 16, + Self::EslintQwik(_) => 17, + Self::EslintReact(_) => 18, + Self::EslintReactHooks(_) => 19, + Self::EslintReactPreferFunctionComponent(_) => 20, + Self::EslintReactRefresh(_) => 21, + Self::EslintReactX(_) => 22, + Self::EslintReactXyz(_) => 23, + Self::EslintRegexp(_) => 24, + Self::EslintSolid(_) => 25, + Self::EslintSonarJs(_) => 26, + Self::EslintStylistic(_) => 27, + Self::EslintTypeScript(_) => 28, + Self::EslintUnicorn(_) => 29, + Self::EslintUnusedImports(_) => 30, + Self::EslintVitest(_) => 31, + Self::EslintVueJs(_) => 32, + Self::GraphqlSchemaLinter(_) => 33, + Self::Stylelint(_) => 34, + Self::EslintTurbo(_) => 35, + Self::HtmlEslint(_) => 36, } } - /// The rule has been a source of inspiration for the declared rule. - pub const fn inspired(self) -> RuleSourceWithKind { - RuleSourceWithKind { - kind: RuleSourceKind::Inspired, - source: self, + const fn sort_key(&self) -> (u16, &'a str) { + (self.variant_index(), self.as_rule_name()) + } + + /// Compares this `RuleSource` with another that may have a different lifetime. + /// This is useful for comparing runtime-created `RuleSource<'_>` values with + /// `RuleSource<'static>` values in const arrays. + pub fn cmp_any<'b>(&self, other: &RuleSource<'b>) -> Ordering { + match self.variant_index().cmp(&other.variant_index()) { + Ordering::Equal => self.as_rule_name().cmp(other.as_rule_name()), + ord => ord, } } - pub fn as_rule_name(&self) -> &'static str { + pub const fn as_rule_name(&self) -> &'a str { match self { Self::Clippy(rule_name) | Self::DenoLint(rule_name) @@ -309,49 +334,53 @@ impl RuleSource { } } - pub fn to_namespaced_rule_name(&self) -> String { + pub const fn namespace(&self) -> &'static str { match self { - Self::Clippy(rule_name) - | Self::DenoLint(rule_name) - | Self::Eslint(rule_name) - | Self::GraphqlSchemaLinter(rule_name) - | Self::Stylelint(rule_name) => (*rule_name).to_string(), - Self::EslintBarrelFiles(rule_name) => format!("barrel-files/{rule_name}"), - Self::EslintGraphql(rule_name) => format!("@graphql-eslint/{rule_name}"), - Self::EslintImport(rule_name) => format!("import/{rule_name}"), - Self::EslintImportAccess(rule_name) => format!("import-access/{rule_name}"), - Self::EslintJest(rule_name) => format!("jest/{rule_name}"), - Self::EslintJsDoc(rule_name) => format!("jsdoc/{rule_name}"), - Self::EslintJsxA11y(rule_name) => format!("jsx-a11y/{rule_name}"), - Self::EslintMysticatea(rule_name) => format!("@mysticatea/{rule_name}"), - Self::EslintN(rule_name) => format!("n/{rule_name}"), - Self::EslintNext(rule_name) => format!("@next/next/{rule_name}"), - Self::EslintNoSecrets(rule_name) => format!("no-secrets/{rule_name}"), - Self::EslintPackageJson(rule_name) => format!("package-json/{rule_name}"), - Self::EslintPackageJsonDependencies(rule_name) => { - format!("package-json-dependencies/{rule_name}") - } - Self::EslintPerfectionist(rule_name) => format!("perfectionist/{rule_name}"), - Self::EslintQwik(rule_name) => format!("qwik/{rule_name}"), - Self::EslintReact(rule_name) => format!("react/{rule_name}"), - Self::EslintReactHooks(rule_name) => format!("react-hooks/{rule_name}"), - Self::EslintReactPreferFunctionComponent(rule_name) => { - format!("react-prefer-function-component/{rule_name}") - } - Self::EslintReactRefresh(rule_name) => format!("react-refresh/{rule_name}"), - Self::EslintReactX(rule_name) => format!("react-x/{rule_name}"), - Self::EslintReactXyz(rule_name) => format!("@eslint-react/{rule_name}"), - Self::EslintRegexp(rule_name) => format!("regexp/{rule_name}"), - Self::EslintSolid(rule_name) => format!("solid/{rule_name}"), - Self::EslintSonarJs(rule_name) => format!("sonarjs/{rule_name}"), - Self::EslintStylistic(rule_name) => format!("@stylistic/{rule_name}"), - Self::EslintTypeScript(rule_name) => format!("@typescript-eslint/{rule_name}"), - Self::EslintUnicorn(rule_name) => format!("unicorn/{rule_name}"), - Self::EslintUnusedImports(rule_name) => format!("unused-imports/{rule_name}"), - Self::EslintVitest(rule_name) => format!("vitest/{rule_name}"), - Self::EslintVueJs(rule_name) => format!("vue/{rule_name}"), - Self::EslintTurbo(rule_name) => format!("turbo/{rule_name}"), - Self::HtmlEslint(rule_name) => format!("@html-eslint/{rule_name}"), + Self::Clippy(_) + | Self::DenoLint(_) + | Self::Eslint(_) + | Self::GraphqlSchemaLinter(_) + | Self::Stylelint(_) => "", + Self::EslintBarrelFiles(_) => "barrel-files", + Self::EslintGraphql(_) => "@graphql-eslint", + Self::EslintImport(_) => "import", + Self::EslintImportAccess(_) => "import-access", + Self::EslintJest(_) => "jest", + Self::EslintJsDoc(_) => "jsdoc", + Self::EslintJsxA11y(_) => "jsx-a11y", + Self::EslintMysticatea(_) => "@mysticatea", + Self::EslintN(_) => "n", + Self::EslintNext(_) => "@next/next", + Self::EslintNoSecrets(_) => "no-secrets", + Self::EslintPackageJson(_) => "package-json", + Self::EslintPackageJsonDependencies(_) => "package-json-dependencies", + Self::EslintPerfectionist(_) => "perfectionist", + Self::EslintQwik(_) => "qwik", + Self::EslintReact(_) => "react", + Self::EslintReactHooks(_) => "react-hooks", + Self::EslintReactPreferFunctionComponent(_) => "react-prefer-function-component", + Self::EslintReactRefresh(_) => "react-refresh", + Self::EslintReactX(_) => "react-x", + Self::EslintReactXyz(_) => "@eslint-react", + Self::EslintRegexp(_) => "regexp", + Self::EslintSolid(_) => "solid", + Self::EslintSonarJs(_) => "sonarjs", + Self::EslintStylistic(_) => "@stylistic", + Self::EslintTypeScript(_) => "@typescript-eslint", + Self::EslintUnicorn(_) => "unicorn", + Self::EslintUnusedImports(_) => "unused-imports", + Self::EslintVitest(_) => "vitest", + Self::EslintVueJs(_) => "vue", + Self::EslintTurbo(_) => "turbo", + Self::HtmlEslint(_) => "@html-eslint", + } + } + + pub fn to_namespaced_rule_name(&self) -> String { + if self.namespace().is_empty() { + self.as_rule_name().to_string() + } else { + format!("{}/{}", self.namespace(), self.as_rule_name()) } } @@ -397,7 +426,7 @@ impl RuleSource { } } - pub fn as_url_and_rule_name(&self) -> (String, &'static str) { + pub fn as_url_and_rule_name(&self) -> (String, &'a str) { (self.to_rule_url(), self.as_rule_name()) } @@ -423,6 +452,24 @@ impl RuleSource { } } +impl RuleSource<'static> { + /// The rule has the same logic as the declared rule. + pub const fn same(self) -> RuleSourceWithKind { + RuleSourceWithKind { + kind: RuleSourceKind::SameLogic, + source: self, + } + } + + /// The rule has been a source of inspiration for the declared rule. + pub const fn inspired(self) -> RuleSourceWithKind { + RuleSourceWithKind { + kind: RuleSourceKind::Inspired, + source: self, + } + } +} + #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] @@ -441,7 +488,7 @@ pub enum RuleSourceKind { #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct RuleSourceWithKind { pub kind: RuleSourceKind, - pub source: RuleSource, + pub source: RuleSource<'static>, } impl RuleSourceKind { diff --git a/crates/biome_cli/src/execute/migrate.rs b/crates/biome_cli/src/execute/migrate.rs index 86613898b8da..a54e2e20b46b 100644 --- a/crates/biome_cli/src/execute/migrate.rs +++ b/crates/biome_cli/src/execute/migrate.rs @@ -36,6 +36,7 @@ mod eslint_unicorn; mod ignorefile; mod node; mod prettier; +mod unsupported_rules; pub(crate) struct MigratePayload<'a> { pub(crate) session: CliSession<'a>, diff --git a/crates/biome_cli/src/execute/migrate/eslint_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_to_biome.rs index 89a1000641c9..9bddc9f3b49f 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_to_biome.rs @@ -1,8 +1,12 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; + +use crate::execute::migrate::unsupported_rules::UNSUPPORTED_RULES; use super::{eslint_any_rule_to_biome::migrate_eslint_any_rule, eslint_eslint, eslint_typescript}; +use biome_analyze::RuleSource; use biome_configuration::analyzer::SeverityOrGroup; use biome_configuration::{self as biome_config}; +use biome_console::fmt::Display; use biome_console::markup; use biome_deserialize::Merge; use biome_diagnostics::Location; @@ -25,85 +29,48 @@ pub(crate) struct MigrationOptions { pub(crate) include_nursery: bool, } -/// Sorted ESlint stylistic rules. -/// The array is sorted to allow binary search. -const ESLINT_STYLISTIC_RULES: &[&str] = &[ - "array-bracket-newline", - "array-bracket-spacing", - "array-element-newline", - "arrow-body-style", - "arrow-parens", - "arrow-spacing", - "block-spacing", - "brace-style", - "capitalized-comments", - "comma-dangle", - "comma-spacing", - "comma-style", - "computed-property-spacing", - "dot-location", - "eol-last", - "func-call-spacing", - "function-call-argument-newline", - "function-paren-newline", - "generator-star-spacing", - "implicit-arrow-linebreak", - "indent", - "indent-legacy", - "jsx-quotes", - "key-spacing", - "keyword-spacing", - "line-comment-position", - "linebreak-style", - "lines-around-comment", - "lines-around-directive", - "lines-between-class-members", - "max-len", - "max-statements-per-line", - "multiline-comment-style", - "multiline-ternary", - "new-parens", - "newline-after-var", - "newline-before-return", - "newline-per-chained-call", - "no-confusing-arrow", - "no-extra-parens", - "no-extra-semi", - "no-floating-decimal", - "no-mixed-operators", - "no-mixed-spaces-and-tabs", - "no-multiple-empty-lines", - "no-spaced-func", - "no-tabs", - "no-trailing-spaces", - "no-whitespace-before-property", - "nonblock-statement-body-position", - "object-curly-newline", - "object-curly-spacing", - "object-property-newline", - "one-var-declaration-per-line", - "operator-linebreak", - "padded-blocks", - "padding-line-between-statements", - "quote-props", - "quotes", - "rest-spread-spacing", - "semi", - "semi-spacing", - "semi-style", - "space-before-blocks", - "space-before-function-paren", - "space-in-parens", - "space-infix-ops", - "space-unary-ops", - "spaced-comment", - "switch-colon-spacing", - "template-curly-spacing", - "template-tag-spacing", - "wrap-iife", - "wrap-regex", - "yield-star-spacing", -]; +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub(crate) struct UnsupportedRule(pub RuleSource<'static>, pub UnsupportedRuleReason); + +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub(crate) enum UnsupportedRuleReason { + /// The rule is stylistic and is fundamentally incompatible with the formatter, and there's no formatter option to adjust its behavior. + /// + /// This is for rules that enforce formatting that are at odds with Biome's formatting decisions. + Stylistic, + /// The formatter completely covers the functionality that the rule is meant to enforce (assuming default rule options). + /// + /// The rule is therefore redundant when using the formatter, and losing the rule does not reduce code quality. + FormatterCovers, + /// The functionality is covered by a Biome formatter option. + FormatterOption(&'static str), + /// The rule belongs to a known source, but it is not yet implemented in Biome. + KnownSourceNotImplemented, + /// The rule belongs to an unknown source, and is therefore not implemented in Biome. + UnknownSource, +} + +impl Display for UnsupportedRuleReason { + fn fmt(&self, fmt: &mut biome_console::fmt::Formatter) -> std::io::Result<()> { + match self { + Self::Stylistic => { + fmt.write_markup(markup! { "Stylistic, incompatible with formatter." }) + } + Self::FormatterCovers => { + fmt.write_markup(markup! { "Redundant, completely covered by Biome's formatter." }) + } + Self::FormatterOption(option) => fmt.write_markup( + markup! { "Covered by Biome's "{option}" formatter option." }, + ), + Self::KnownSourceNotImplemented => { + fmt.write_markup(markup! { "Known source, not yet implemented." }) + } + Self::UnknownSource => fmt.write_markup(markup! { + "These rules originate from an eslint plugin or other tool that Biome doesn't know about." + }), + } + } +} #[derive(Debug, Default)] pub(crate) struct MigrationResults { @@ -115,9 +82,7 @@ pub(crate) struct MigrationResults { pub(crate) inspired: BTreeSet, pub(crate) nursery: BTreeSet, pub(crate) migrated: BTreeSet, - /// Stylistic rules that are not supported on purpose. - pub(crate) stylistic: BTreeSet, - pub(crate) unsupported: BTreeSet, + pub(crate) unsupported: BTreeMap, } impl MigrationResults { pub(crate) fn add(&mut self, sourced_rule: &str, status: RuleMigrationResult) { @@ -133,26 +98,14 @@ impl MigrationResults { self.nursery.insert(sourced); } RuleMigrationResult::Unsupported => { - if sourced.rule_name.starts_with("@stylistic/") - || (sourced.plugin_name.is_none() - && ESLINT_STYLISTIC_RULES - .binary_search(&sourced.rule_name.as_ref()) - .is_ok()) - { - self.stylistic.insert(sourced); - } else { - self.unsupported.insert(sourced); - }; + let reason = unsupported_rule_reason(&sourced); + self.unsupported.insert(sourced, reason); } } } pub(crate) fn rule_count(&self) -> usize { - self.migrated.len() - + self.inspired.len() - + self.nursery.len() - + self.stylistic.len() - + self.unsupported.len() + self.migrated.len() + self.inspired.len() + self.nursery.len() + self.unsupported.len() } } impl biome_diagnostics::Diagnostic for MigrationResults { @@ -175,15 +128,50 @@ impl biome_diagnostics::Diagnostic for MigrationResults { fn message(&self, fmt: &mut biome_console::fmt::Formatter<'_>) -> std::io::Result<()> { let count = self.rule_count(); if count != 0 { - let migrated_count = self.migrated.len() - + if self.write { - 0 - } else { - self.inspired.len() + self.nursery.len() - }; - let migrated_percent = migrated_count * 100 / count; - let verb = if self.write { "have been" } else { "can be" }; - fmt.write_markup(markup! { {migrated_percent}"% ("{migrated_count}"/"{count}") of the rules "{verb}" migrated." }) + let formatter_covers_count = self + .unsupported + .iter() + .filter(|(_, reason)| { + matches!( + reason, + UnsupportedRuleReason::FormatterCovers + | UnsupportedRuleReason::FormatterOption(_) + ) + }) + .count(); + + let directly_covered_count = self.migrated.len(); + let inspired_count = self.inspired.len(); + let nursery_count = self.nursery.len(); + + let total_migratable_count = directly_covered_count + inspired_count + nursery_count; + let total_covered_count = total_migratable_count + formatter_covers_count; + let total_covered_percent = total_covered_count * 100 / count; + let directly_covered_percent = directly_covered_count * 100 / count; + + fmt.write_markup(markup! { {count}" ESLint rules found\n" })?; + if formatter_covers_count > 0 { + fmt.write_markup(markup! { "- "{formatter_covers_count}" are obsolete"" because of Biome's formatter\n" })?; + } + + if self.write { + fmt.write_markup(markup! { "- "{directly_covered_count}" have been migrated"" to Biome's rules\n" })?; + } else { + fmt.write_markup(markup! { "- "{directly_covered_count}" can be migrated"" to Biome's rules (run with --write to migrate)\n" })?; + if inspired_count > 0 { + fmt.write_markup(markup! { " - ""+"{inspired_count}" with --include-inspired\n" })?; + } + if nursery_count > 0 { + fmt.write_markup(markup! { " - ""+"{nursery_count}" with --include-nursery (experimental rules)\n" })?; + } + } + + fmt.write_markup(markup! { + "- "{total_covered_percent}"% ("{total_covered_count}")"" of your ESLint rules are fully covered by Biome\n" + })?; + fmt.write_markup(markup! { + " - "{directly_covered_percent}"% ("{directly_covered_count}") via direct migration to Biome rules\n" + }) } else { fmt.write_markup(markup! { "No rules to migrate." }) } @@ -218,7 +206,7 @@ impl biome_diagnostics::Diagnostic for MigrationResults { .collect(); visitor.record_list(list.as_slice())?; } - if !self.inspired.is_empty() { + if !self.nursery.is_empty() { visitor.record_log( biome_diagnostics::LogCategory::Info, &markup! { "Rules that can be migrated to a nursery rule using ""--include-nursery"":" }, @@ -230,29 +218,95 @@ impl biome_diagnostics::Diagnostic for MigrationResults { .collect(); visitor.record_list(list.as_slice())?; } - if !self.stylistic.is_empty() { - visitor.record_log( - biome_diagnostics::LogCategory::Info, - &markup! { "Stylistic rules that the formatter may support (manual migration required):" }, - )?; - let list: Vec<_> = self - .stylistic - .iter() - .map(|item| item as &dyn biome_console::fmt::Display) - .collect(); - visitor.record_list(list.as_slice())?; - } if !self.unsupported.is_empty() { + let mut stylistic = Vec::new(); + let mut formatter_covers = Vec::new(); + let mut formatter_option = Vec::new(); + let mut known_source_not_implemented = Vec::new(); + let mut unknown_source = Vec::new(); + + for (rule, reason) in &self.unsupported { + match reason { + UnsupportedRuleReason::Stylistic => stylistic.push(rule), + UnsupportedRuleReason::FormatterCovers => formatter_covers.push(rule), + UnsupportedRuleReason::FormatterOption(_) => { + formatter_option.push((rule, reason)) + } + UnsupportedRuleReason::KnownSourceNotImplemented => { + known_source_not_implemented.push(rule); + } + UnsupportedRuleReason::UnknownSource => unknown_source.push(rule), + } + } + visitor.record_log( biome_diagnostics::LogCategory::Info, - &markup! { "Unsupported rules:" }, + &markup! { "Unsupported rules ("{stylistic.len()}" incompatible with formatter, "{formatter_covers.len()}" made obsolete by the formatter, "{formatter_option.len()}" covered by a formatter option, "{known_source_not_implemented.len()}" not yet implemented, "{unknown_source.len()}" unknown source):" }, )?; - let list: Vec<_> = self - .unsupported - .iter() - .map(|item| item as &dyn biome_console::fmt::Display) - .collect(); - visitor.record_list(list.as_slice())?; + + if !stylistic.is_empty() { + visitor.record_log( + biome_diagnostics::LogCategory::Info, + &markup! { "These rules enforce code styles that are incompatible with the formatter in some way:" }, + )?; + let list: Vec<_> = stylistic + .iter() + .map(|item| *item as &dyn biome_console::fmt::Display) + .collect(); + visitor.record_list(list.as_slice())?; + } + + if !formatter_covers.is_empty() { + visitor.record_log( + biome_diagnostics::LogCategory::Info, + &markup! { "These rules enforce behavior completely covered by the formatter (so you don't lose the functionality):" }, + )?; + let list: Vec<_> = formatter_covers + .iter() + .map(|item| *item as &dyn biome_console::fmt::Display) + .collect(); + visitor.record_list(list.as_slice())?; + } + + if !formatter_option.is_empty() { + visitor.record_log( + biome_diagnostics::LogCategory::Info, + &markup! { "These rules are covered by formatter options, but they require manual migration:" }, + )?; + let list: Vec<_> = formatter_option + .iter() + .map(|(rule, reason)| UnsupportedRuleDisplay { rule, reason }) + .collect(); + let list: Vec<_> = list + .iter() + .map(|item| item as &dyn biome_console::fmt::Display) + .collect(); + visitor.record_list(list.as_slice())?; + } + + if !known_source_not_implemented.is_empty() { + visitor.record_log( + biome_diagnostics::LogCategory::Info, + &markup! { "These rules have not yet been implemented:" }, + )?; + let list: Vec<_> = known_source_not_implemented + .iter() + .map(|item| *item as &dyn biome_console::fmt::Display) + .collect(); + visitor.record_list(list.as_slice())?; + } + + if !unknown_source.is_empty() { + visitor.record_log( + biome_diagnostics::LogCategory::Info, + &markup! { "These rules originate from an eslint plugin or other tool that Biome doesn't know about:" }, + )?; + let list: Vec<_> = unknown_source + .iter() + .map(|item| *item as &dyn biome_console::fmt::Display) + .collect(); + visitor.record_list(list.as_slice())?; + } } Ok(()) } @@ -300,12 +354,86 @@ impl std::fmt::Display for EslintRuleName { } } } + +impl<'a> TryFrom<&'a EslintRuleName> for RuleSource<'a> { + type Error = &'static str; + + fn try_from(value: &'a EslintRuleName) -> Result { + let EslintRuleName { + plugin_name, + rule_name, + } = value; + let constructor: fn(&'a str) -> RuleSource<'a> = match plugin_name.as_deref() { + None => RuleSource::Eslint, + Some("barrel-files") => RuleSource::EslintBarrelFiles, + Some("@graphql-eslint") => RuleSource::EslintGraphql, + Some("import") => RuleSource::EslintImport, + Some("import-access") => RuleSource::EslintImportAccess, + Some("jest") => RuleSource::EslintJest, + Some("jsdoc") => RuleSource::EslintJsDoc, + Some("jsx-a11y") => RuleSource::EslintJsxA11y, + Some("@mysticatea") => RuleSource::EslintMysticatea, + Some("n") => RuleSource::EslintN, + Some("@next/next") => RuleSource::EslintNext, + Some("no-secrets") => RuleSource::EslintNoSecrets, + Some("package-json") => RuleSource::EslintPackageJson, + Some("package-json-dependencies") => RuleSource::EslintPackageJsonDependencies, + Some("perfectionist") => RuleSource::EslintPerfectionist, + Some("qwik") => RuleSource::EslintQwik, + Some("react") => RuleSource::EslintReact, + Some("react-hooks") => RuleSource::EslintReactHooks, + Some("react-prefer-function-component") => { + RuleSource::EslintReactPreferFunctionComponent + } + Some("react-refresh") => RuleSource::EslintReactRefresh, + Some("react-x") => RuleSource::EslintReactX, + Some("@eslint-react") => RuleSource::EslintReactXyz, + Some("regexp") => RuleSource::EslintRegexp, + Some("solid") => RuleSource::EslintSolid, + Some("sonarjs") => RuleSource::EslintSonarJs, + Some("@stylistic") => RuleSource::EslintStylistic, + Some("@typescript-eslint") => RuleSource::EslintTypeScript, + Some("unicorn") => RuleSource::EslintUnicorn, + Some("unused-imports") => RuleSource::EslintUnusedImports, + Some("vitest" | "@vitest") => RuleSource::EslintVitest, + Some("vue") => RuleSource::EslintVueJs, + Some("turbo") => RuleSource::EslintTurbo, + Some("@html-eslint") => RuleSource::HtmlEslint, + Some(_) => return Err("Unknown ESLint rule source"), + }; + + Ok(constructor(rule_name)) + } +} + +fn unsupported_rule_reason(rule_name: &EslintRuleName) -> UnsupportedRuleReason { + let Ok(sourced_rule) = RuleSource::try_from(rule_name) else { + return UnsupportedRuleReason::UnknownSource; + }; + + if let Ok(index) = UNSUPPORTED_RULES.binary_search_by(|rule| rule.0.cmp_any(&sourced_rule)) { + return UNSUPPORTED_RULES[index].1.clone(); + } + UnsupportedRuleReason::KnownSourceNotImplemented +} + impl biome_console::fmt::Display for EslintRuleName { fn fmt(&self, fmt: &mut biome_console::fmt::Formatter) -> std::io::Result<()> { fmt.write_fmt(format_args!("{self}")) } } +struct UnsupportedRuleDisplay<'a> { + rule: &'a EslintRuleName, + reason: &'a UnsupportedRuleReason, +} + +impl biome_console::fmt::Display for UnsupportedRuleDisplay<'_> { + fn fmt(&self, fmt: &mut biome_console::fmt::Formatter) -> std::io::Result<()> { + fmt.write_markup(markup! { {self.rule}" - "{self.reason} }) + } +} + impl eslint_eslint::AnyConfigData { pub(crate) fn into_biome_config( self, @@ -662,11 +790,6 @@ mod tests { use eslint_eslint::*; use std::borrow::Cow; - #[test] - fn test_eslint_stylistic_rules_order() { - assert!(ESLINT_STYLISTIC_RULES.is_sorted()); - } - #[test] fn flat_config_single_config_object() { let flat_config = FlatConfigData(vec![FlatConfigObject { @@ -777,4 +900,12 @@ mod tests { )) ); } + + #[test] + fn sanity_check_unsupported_rule_lookup() { + assert_eq!( + unsupported_rule_reason(&EslintRuleName::from_str("eol-last")), + UnsupportedRuleReason::FormatterCovers + ); + } } diff --git a/crates/biome_cli/src/execute/migrate/unsupported_rules.rs b/crates/biome_cli/src/execute/migrate/unsupported_rules.rs new file mode 100644 index 000000000000..6d5ceef39836 --- /dev/null +++ b/crates/biome_cli/src/execute/migrate/unsupported_rules.rs @@ -0,0 +1,368 @@ +//! Metadata about unsupported lint rules. + +use biome_analyze::RuleSource::*; + +use crate::execute::migrate::eslint_to_biome::UnsupportedRule; +use crate::execute::migrate::eslint_to_biome::UnsupportedRuleReason::*; + +// Sorted ESLint unsupported rules. +/// The array is sorted to allow binary search. +pub const UNSUPPORTED_RULES: &[UnsupportedRule] = &[ + UnsupportedRule(Eslint("array-bracket-newline"), FormatterCovers), + UnsupportedRule(Eslint("array-bracket-spacing"), FormatterCovers), + UnsupportedRule(Eslint("array-element-newline"), FormatterCovers), + UnsupportedRule(Eslint("arrow-parens"), FormatterOption("arrowParentheses")), + UnsupportedRule(Eslint("arrow-spacing"), Stylistic), + UnsupportedRule(Eslint("block-spacing"), FormatterCovers), + UnsupportedRule(Eslint("brace-style"), Stylistic), + UnsupportedRule(Eslint("capitalized-comments"), Stylistic), + UnsupportedRule(Eslint("comma-dangle"), Stylistic), + UnsupportedRule(Eslint("comma-spacing"), FormatterCovers), + UnsupportedRule(Eslint("comma-style"), Stylistic), + UnsupportedRule(Eslint("computed-property-spacing"), Stylistic), + UnsupportedRule(Eslint("dot-location"), Stylistic), + UnsupportedRule(Eslint("eol-last"), FormatterCovers), + UnsupportedRule(Eslint("func-call-spacing"), Stylistic), + UnsupportedRule(Eslint("function-call-argument-newline"), FormatterCovers), + UnsupportedRule(Eslint("function-paren-newline"), FormatterCovers), + UnsupportedRule(Eslint("generator-star"), FormatterCovers), + UnsupportedRule(Eslint("generator-star-spacing"), FormatterCovers), + UnsupportedRule(Eslint("implicit-arrow-linebreak"), Stylistic), + UnsupportedRule(Eslint("indent"), FormatterOption("indentWidth")), + UnsupportedRule(Eslint("indent-legacy"), FormatterOption("indentWidth")), + UnsupportedRule(Eslint("jsx-quotes"), FormatterOption("jsxQuoteStyle")), + UnsupportedRule(Eslint("key-spacing"), FormatterCovers), + UnsupportedRule(Eslint("keyword-spacing"), FormatterCovers), + UnsupportedRule(Eslint("line-comment-position"), Stylistic), + UnsupportedRule(Eslint("linebreak-style"), FormatterOption("lineEnding")), + UnsupportedRule(Eslint("lines-around-comment"), Stylistic), + UnsupportedRule(Eslint("lines-around-directive"), Stylistic), + UnsupportedRule(Eslint("lines-between-class-members"), Stylistic), + UnsupportedRule(Eslint("max-len"), FormatterOption("lineWidth")), + UnsupportedRule(Eslint("max-statements-per-line"), FormatterCovers), + UnsupportedRule(Eslint("multiline-comment-style"), Stylistic), + UnsupportedRule(Eslint("multiline-ternary"), Stylistic), + UnsupportedRule(Eslint("new-parens"), FormatterCovers), + UnsupportedRule(Eslint("newline-after-var"), Stylistic), + UnsupportedRule(Eslint("newline-before-return"), Stylistic), + UnsupportedRule(Eslint("newline-per-chained-call"), FormatterCovers), + UnsupportedRule(Eslint("no-confusing-arrow"), FormatterCovers), + UnsupportedRule(Eslint("no-extra-parens"), FormatterCovers), + UnsupportedRule(Eslint("no-extra-semi"), FormatterCovers), + UnsupportedRule(Eslint("no-floating-decimal"), FormatterCovers), + UnsupportedRule(Eslint("no-mixed-operators"), Stylistic), + UnsupportedRule( + Eslint("no-mixed-spaces-and-tabs"), + FormatterOption("indentStyle"), + ), + UnsupportedRule(Eslint("no-multi-spaces"), FormatterCovers), + UnsupportedRule(Eslint("no-multiple-empty-lines"), FormatterCovers), + UnsupportedRule(Eslint("no-space-before-semi"), FormatterCovers), + UnsupportedRule(Eslint("no-spaced-func"), Stylistic), + UnsupportedRule(Eslint("no-tabs"), FormatterOption("indentStyle")), + UnsupportedRule(Eslint("no-trailing-spaces"), FormatterCovers), + UnsupportedRule(Eslint("no-whitespace-before-property"), Stylistic), + UnsupportedRule(Eslint("nonblock-statement-body-position"), Stylistic), + UnsupportedRule(Eslint("object-curly-newline"), FormatterCovers), + UnsupportedRule(Eslint("object-curly-spacing"), FormatterCovers), + UnsupportedRule(Eslint("object-property-newline"), FormatterCovers), + UnsupportedRule(Eslint("one-var-declaration-per-line"), Stylistic), + UnsupportedRule( + Eslint("operator-linebreak"), + FormatterOption("operatorLinebreak"), + ), + UnsupportedRule(Eslint("padded-blocks"), Stylistic), + UnsupportedRule(Eslint("padding-line-between-statements"), Stylistic), + UnsupportedRule(Eslint("quote-props"), Stylistic), + UnsupportedRule(Eslint("quotes"), FormatterOption("quoteStyle")), + UnsupportedRule(Eslint("rest-spread-spacing"), FormatterCovers), + UnsupportedRule(Eslint("semi"), FormatterOption("semicolons")), + UnsupportedRule(Eslint("semi-spacing"), FormatterCovers), + UnsupportedRule(Eslint("semi-style"), FormatterCovers), + UnsupportedRule(Eslint("space-after-function-name"), Stylistic), + UnsupportedRule(Eslint("space-after-keywords"), FormatterCovers), + UnsupportedRule(Eslint("space-before-blocks"), FormatterCovers), + UnsupportedRule(Eslint("space-before-function-paren"), Stylistic), + UnsupportedRule(Eslint("space-before-function-parentheses"), Stylistic), + UnsupportedRule(Eslint("space-before-keywords"), FormatterCovers), + UnsupportedRule(Eslint("space-in-brackets"), Stylistic), + UnsupportedRule(Eslint("space-in-parens"), FormatterCovers), + UnsupportedRule(Eslint("space-infix-ops"), FormatterCovers), + UnsupportedRule(Eslint("space-return-throw-case"), FormatterCovers), + UnsupportedRule(Eslint("space-unary-ops"), FormatterCovers), + UnsupportedRule(Eslint("space-unary-word-ops"), FormatterCovers), + UnsupportedRule(Eslint("spaced-comment"), Stylistic), + UnsupportedRule(Eslint("switch-colon-spacing"), FormatterCovers), + UnsupportedRule(Eslint("template-curly-spacing"), FormatterCovers), + UnsupportedRule(Eslint("template-tag-spacing"), FormatterCovers), + UnsupportedRule(Eslint("wrap-iife"), Stylistic), + UnsupportedRule(Eslint("wrap-regex"), Stylistic), + UnsupportedRule(Eslint("yield-star-spacing"), FormatterCovers), + UnsupportedRule(EslintJest("padding-around-after-all-blocks"), Stylistic), + UnsupportedRule(EslintJest("padding-around-after-each-blocks"), Stylistic), + UnsupportedRule(EslintJest("padding-around-all"), Stylistic), + UnsupportedRule(EslintJest("padding-around-before-all-blocks"), Stylistic), + UnsupportedRule(EslintJest("padding-around-before-each-blocks"), Stylistic), + UnsupportedRule(EslintJest("padding-around-describe-blocks"), Stylistic), + UnsupportedRule(EslintJest("padding-around-expect-groups"), Stylistic), + UnsupportedRule(EslintJest("padding-around-test-blocks"), Stylistic), + UnsupportedRule(EslintReact("jsx-child-element-spacing"), FormatterCovers), + UnsupportedRule(EslintReact("jsx-closing-bracket-location"), FormatterCovers), + UnsupportedRule(EslintReact("jsx-closing-tag-location"), FormatterCovers), + UnsupportedRule(EslintReact("jsx-curly-newline"), FormatterCovers), + UnsupportedRule(EslintReact("jsx-curly-spacing"), FormatterCovers), + UnsupportedRule(EslintReact("jsx-equals-spacing"), FormatterCovers), + UnsupportedRule(EslintReact("jsx-first-prop-new-line"), Stylistic), + UnsupportedRule(EslintReact("jsx-indent"), FormatterOption("indentStyle")), + UnsupportedRule(EslintReact("jsx-indent-props"), Stylistic), + UnsupportedRule(EslintReact("jsx-max-props-per-line"), Stylistic), + UnsupportedRule(EslintReact("jsx-newline"), FormatterCovers), + UnsupportedRule(EslintReact("jsx-one-expression-per-line"), Stylistic), + UnsupportedRule(EslintReact("jsx-props-no-multi-spaces"), FormatterCovers), + UnsupportedRule(EslintReact("jsx-space-before-closing"), FormatterCovers), + UnsupportedRule(EslintReact("jsx-tag-spacing"), FormatterCovers), + UnsupportedRule(EslintReact("jsx-wrap-multilines"), Stylistic), + UnsupportedRule(EslintStylistic("array-bracket-newline"), FormatterCovers), + UnsupportedRule(EslintStylistic("array-bracket-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("array-element-newline"), FormatterCovers), + UnsupportedRule( + EslintStylistic("arrow-parens"), + FormatterOption("arrowParentheses"), + ), + UnsupportedRule(EslintStylistic("arrow-spacing"), Stylistic), + UnsupportedRule(EslintStylistic("block-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("brace-style"), Stylistic), + UnsupportedRule(EslintStylistic("comma-dangle"), Stylistic), + UnsupportedRule(EslintStylistic("comma-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("comma-style"), Stylistic), + UnsupportedRule(EslintStylistic("computed-property-spacing"), Stylistic), + UnsupportedRule(EslintStylistic("dot-location"), Stylistic), + UnsupportedRule(EslintStylistic("eol-last"), FormatterCovers), + UnsupportedRule(EslintStylistic("func-call-spacing"), Stylistic), + UnsupportedRule( + EslintStylistic("function-call-argument-newline"), + FormatterCovers, + ), + UnsupportedRule(EslintStylistic("function-paren-newline"), FormatterCovers), + UnsupportedRule(EslintStylistic("generator-star-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("implicit-arrow-linebreak"), Stylistic), + UnsupportedRule(EslintStylistic("indent"), FormatterOption("indentWidth")), + UnsupportedRule( + EslintStylistic("indent-binary-ops"), + FormatterOption("indentWidth"), + ), + UnsupportedRule( + EslintStylistic("jsx-child-element-spacing"), + FormatterCovers, + ), + UnsupportedRule( + EslintStylistic("jsx-closing-bracket-location"), + FormatterCovers, + ), + UnsupportedRule(EslintStylistic("jsx-closing-tag-location"), FormatterCovers), + UnsupportedRule(EslintStylistic("jsx-curly-newline"), FormatterCovers), + UnsupportedRule(EslintStylistic("jsx-curly-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("jsx-equals-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("jsx-first-prop-new-line"), Stylistic), + UnsupportedRule(EslintStylistic("jsx-function-call-newline"), Stylistic), + UnsupportedRule(EslintStylistic("jsx-indent-props"), Stylistic), + UnsupportedRule(EslintStylistic("jsx-max-props-per-line"), Stylistic), + UnsupportedRule(EslintStylistic("jsx-newline"), FormatterCovers), + UnsupportedRule(EslintStylistic("jsx-one-expression-per-line"), Stylistic), + UnsupportedRule( + EslintStylistic("jsx-quotes"), + FormatterOption("jsxQuoteStyle"), + ), + UnsupportedRule(EslintStylistic("jsx-tag-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("jsx-wrap-multilines"), FormatterCovers), + UnsupportedRule(EslintStylistic("key-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("keyword-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("line-comment-position"), Stylistic), + UnsupportedRule( + EslintStylistic("linebreak-style"), + FormatterOption("lineEnding"), + ), + UnsupportedRule(EslintStylistic("lines-around-comment"), Stylistic), + UnsupportedRule(EslintStylistic("lines-between-class-members"), Stylistic), + UnsupportedRule(EslintStylistic("list-style"), FormatterCovers), + UnsupportedRule(EslintStylistic("max-len"), FormatterOption("lineWidth")), + UnsupportedRule(EslintStylistic("max-statements-per-line"), FormatterCovers), + UnsupportedRule(EslintStylistic("member-delimiter-style"), FormatterCovers), + UnsupportedRule(EslintStylistic("multiline-comment-style"), Stylistic), + UnsupportedRule(EslintStylistic("multiline-ternary"), Stylistic), + UnsupportedRule(EslintStylistic("new-parens"), FormatterCovers), + UnsupportedRule(EslintStylistic("newline-per-chained-call"), FormatterCovers), + UnsupportedRule(EslintStylistic("no-confusing-arrow"), FormatterCovers), + UnsupportedRule(EslintStylistic("no-extra-parens"), FormatterCovers), + UnsupportedRule(EslintStylistic("no-extra-semi"), FormatterCovers), + UnsupportedRule(EslintStylistic("no-floating-decimal"), FormatterCovers), + UnsupportedRule(EslintStylistic("no-mixed-operators"), Stylistic), + UnsupportedRule(EslintStylistic("no-multi-spaces"), FormatterCovers), + UnsupportedRule(EslintStylistic("no-multiple-empty-lines"), FormatterCovers), + UnsupportedRule(EslintStylistic("no-spaced-func"), Stylistic), + UnsupportedRule(EslintStylistic("no-tabs"), FormatterOption("indentStyle")), + UnsupportedRule(EslintStylistic("no-trailing-spaces"), FormatterCovers), + UnsupportedRule(EslintStylistic("no-whitespace-before-property"), Stylistic), + UnsupportedRule( + EslintStylistic("nonblock-statement-body-position"), + Stylistic, + ), + UnsupportedRule(EslintStylistic("object-curly-newline"), FormatterCovers), + UnsupportedRule(EslintStylistic("object-curly-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("object-property-newline"), FormatterCovers), + UnsupportedRule(EslintStylistic("one-var-declaration-per-line"), Stylistic), + UnsupportedRule( + EslintStylistic("operator-linebreak"), + FormatterOption("operatorLinebreak"), + ), + UnsupportedRule(EslintStylistic("padded-blocks"), Stylistic), + UnsupportedRule( + EslintStylistic("padding-line-between-statements"), + Stylistic, + ), + UnsupportedRule(EslintStylistic("quote-props"), Stylistic), + UnsupportedRule(EslintStylistic("quotes"), FormatterOption("quoteStyle")), + UnsupportedRule(EslintStylistic("rest-spread-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("space-after-keywords"), FormatterCovers), + UnsupportedRule(EslintStylistic("space-before-blocks"), FormatterCovers), + UnsupportedRule(EslintStylistic("space-before-function-paren"), Stylistic), + UnsupportedRule( + EslintStylistic("space-before-function-parentheses"), + Stylistic, + ), + UnsupportedRule(EslintStylistic("space-before-keywords"), FormatterCovers), + UnsupportedRule(EslintStylistic("space-in-brackets"), Stylistic), + UnsupportedRule(EslintStylistic("space-in-parens"), FormatterCovers), + UnsupportedRule(EslintStylistic("space-infix-ops"), FormatterCovers), + UnsupportedRule(EslintStylistic("space-return-throw-case"), FormatterCovers), + UnsupportedRule(EslintStylistic("space-unary-ops"), FormatterCovers), + UnsupportedRule(EslintStylistic("space-unary-word-ops"), FormatterCovers), + UnsupportedRule(EslintStylistic("spaced-comment"), Stylistic), + UnsupportedRule(EslintStylistic("switch-colon-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("template-curly-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("template-tag-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("type-annotation-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("type-generic-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("type-named-tuple-spacing"), FormatterCovers), + UnsupportedRule(EslintStylistic("wrap-iife"), Stylistic), + UnsupportedRule(EslintStylistic("wrap-regex"), Stylistic), + UnsupportedRule(EslintStylistic("yield-star-spacing"), FormatterCovers), + UnsupportedRule(EslintTypeScript("brace-style"), Stylistic), + UnsupportedRule(EslintTypeScript("comma-dangle"), Stylistic), + UnsupportedRule(EslintTypeScript("comma-spacing"), FormatterCovers), + UnsupportedRule(EslintTypeScript("indent"), FormatterOption("indentWidth")), + UnsupportedRule(EslintTypeScript("no-extra-parens"), FormatterCovers), + UnsupportedRule(EslintTypeScript("no-extra-semi"), FormatterCovers), + UnsupportedRule(EslintTypeScript("object-curly-spacing"), FormatterCovers), + UnsupportedRule(EslintTypeScript("quotes"), FormatterOption("quoteStyle")), + UnsupportedRule(EslintTypeScript("semi"), FormatterOption("semicolons")), + UnsupportedRule(EslintTypeScript("space-before-blocks"), FormatterCovers), + UnsupportedRule(EslintTypeScript("space-before-function-paren"), Stylistic), + UnsupportedRule(EslintTypeScript("space-infix-ops"), FormatterCovers), + UnsupportedRule(EslintVueJs("array-bracket-newline"), FormatterCovers), + UnsupportedRule(EslintVueJs("array-bracket-spacing"), FormatterCovers), + UnsupportedRule(EslintVueJs("array-element-newline"), FormatterCovers), + UnsupportedRule(EslintVueJs("arrow-spacing"), Stylistic), + UnsupportedRule(EslintVueJs("block-spacing"), FormatterCovers), + UnsupportedRule(EslintVueJs("block-tag-newline"), Stylistic), + UnsupportedRule(EslintVueJs("brace-style"), Stylistic), + UnsupportedRule(EslintVueJs("comma-dangle"), Stylistic), + UnsupportedRule(EslintVueJs("comma-spacing"), FormatterCovers), + UnsupportedRule(EslintVueJs("comma-style"), Stylistic), + UnsupportedRule(EslintVueJs("dot-location"), Stylistic), + UnsupportedRule(EslintVueJs("first-attribute-linebreak"), Stylistic), + UnsupportedRule(EslintVueJs("html-closing-bracket-newline"), Stylistic), + UnsupportedRule(EslintVueJs("html-closing-bracket-spacing"), Stylistic), + UnsupportedRule(EslintVueJs("html-comment-content-newline"), Stylistic), + UnsupportedRule(EslintVueJs("html-comment-content-spacing"), Stylistic), + UnsupportedRule(EslintVueJs("html-comment-indent"), Stylistic), + UnsupportedRule(EslintVueJs("html-indent"), FormatterCovers), + UnsupportedRule(EslintVueJs("html-quotes"), Stylistic), + UnsupportedRule( + EslintVueJs("html-self-closing"), + FormatterOption("selfCloseVoidElements"), + ), + UnsupportedRule(EslintVueJs("key-spacing"), FormatterCovers), + UnsupportedRule(EslintVueJs("keyword-spacing"), FormatterCovers), + UnsupportedRule(EslintVueJs("max-attributes-per-line"), FormatterCovers), + UnsupportedRule(EslintVueJs("max-len"), FormatterOption("lineWidth")), + UnsupportedRule( + EslintVueJs("multiline-html-element-content-newline"), + Stylistic, + ), + UnsupportedRule(EslintVueJs("multiline-ternary"), Stylistic), + UnsupportedRule(EslintVueJs("mustache-interpolation-spacing"), Stylistic), + UnsupportedRule( + EslintVueJs("new-line-between-multi-line-property"), + Stylistic, + ), + UnsupportedRule(EslintVueJs("no-extra-parens"), FormatterCovers), + UnsupportedRule(EslintVueJs("no-multi-spaces"), FormatterCovers), + UnsupportedRule(EslintVueJs("no-parsing-error"), FormatterCovers), + UnsupportedRule( + EslintVueJs("no-spaces-around-equal-signs-in-attribute"), + FormatterCovers, + ), + UnsupportedRule(EslintVueJs("object-curly-newline"), FormatterCovers), + UnsupportedRule(EslintVueJs("object-curly-spacing"), FormatterCovers), + UnsupportedRule(EslintVueJs("object-property-newline"), FormatterCovers), + UnsupportedRule( + EslintVueJs("operator-linebreak"), + FormatterOption("operatorLinebreak"), + ), + UnsupportedRule(EslintVueJs("quote-props"), Stylistic), + UnsupportedRule(EslintVueJs("quotes"), FormatterOption("quoteStyle")), + UnsupportedRule( + EslintVueJs("script-indent"), + FormatterOption("indentScriptAndStyle"), + ), + UnsupportedRule( + EslintVueJs("singleline-html-element-content-newline"), + Stylistic, + ), + UnsupportedRule(EslintVueJs("space-in-parens"), FormatterCovers), + UnsupportedRule(EslintVueJs("space-infix-ops"), FormatterCovers), + UnsupportedRule(EslintVueJs("space-unary-ops"), FormatterCovers), + UnsupportedRule(EslintVueJs("template-curly-spacing"), FormatterCovers), +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_eslint_unsupported_rules_order() { + let is_sorted = UNSUPPORTED_RULES + .windows(2) + .all(|pair| pair[0].0 <= pair[1].0); + + if !is_sorted { + let mut sorted_rules: Vec<_> = UNSUPPORTED_RULES.iter().collect(); + sorted_rules.sort_by_key(|rule| rule.0); + panic!( + "UNSUPPORTED_RULES is not sorted. Expected order:\n{:?}", + sorted_rules + ); + } + } + + #[test] + fn test_eslint_unsupported_rules_no_duplicates() { + let mut seen = std::collections::HashSet::new(); + let mut duplicates = Vec::new(); + + for rule in UNSUPPORTED_RULES { + let rule_source = &rule.0; + if !seen.insert(rule_source) { + duplicates.push(rule_source); + } + } + + if !duplicates.is_empty() { + panic!( + "UNSUPPORTED_RULES contains duplicate rule sources:\n{:?}", + duplicates + ); + } + } +} diff --git a/crates/biome_cli/tests/commands/migrate_eslint.rs b/crates/biome_cli/tests/commands/migrate_eslint.rs index 6699f07127e8..a07610d8f28b 100644 --- a/crates/biome_cli/tests/commands/migrate_eslint.rs +++ b/crates/biome_cli/tests/commands/migrate_eslint.rs @@ -712,3 +712,34 @@ fn migrate_merge_with_overrides() { result, )); } + +#[test] +fn migrate_rules_covered_by_formatter() { + let biomejson = r#"{ "linter": { "enabled": true } }"#; + let eslintrc = r#"{ + "rules": { + "eol-last": "error", + "indent": ["error", 2], + }, + }"#; + + let fs = MemoryFileSystem::default(); + fs.insert(Utf8Path::new("biome.json").into(), biomejson.as_bytes()); + fs.insert(Utf8Path::new(".eslintrc").into(), eslintrc.as_bytes()); + + let mut console = BufferConsole::default(); + let (fs, result) = run_cli( + fs, + &mut console, + Args::from(["migrate", "eslint"].as_slice()), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "migrate_rules_covered_by_formatter", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslint_config_packagejson.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslint_config_packagejson.snap index a3e73c3269f2..0ef7fc68a59b 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslint_config_packagejson.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslint_config_packagejson.snap @@ -49,7 +49,11 @@ biome.json migrate ━━━━━━━━━━━━━━━━━━━━ ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (1/1) of the rules can be migrated. + i 1 ESLint rules found + - 1 can be migrated to Biome's rules (run with --write to migrate) + - 100% (1) of your ESLint rules are fully covered by Biome + - 100% (1) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintignore.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintignore.snap index faaf7e660bbe..4b3a451d45ee 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintignore.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintignore.snap @@ -58,7 +58,11 @@ biome.json migrate ━━━━━━━━━━━━━━━━━━━━ ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (1/1) of the rules can be migrated. + i 1 ESLint rules found + - 1 can be migrated to Biome's rules (run with --write to migrate) + - 100% (1) of your ESLint rules are fully covered by Biome + - 100% (1) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintignore_and_ignore_patterns.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintignore_and_ignore_patterns.snap index 14f1c30e5401..7fd04febea0d 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintignore_and_ignore_patterns.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintignore_and_ignore_patterns.snap @@ -56,7 +56,11 @@ biome.json migrate ━━━━━━━━━━━━━━━━━━━━ ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (1/1) of the rules can be migrated. + i 1 ESLint rules found + - 1 can be migrated to Biome's rules (run with --write to migrate) + - 100% (1) of your ESLint rules are fully covered by Biome + - 100% (1) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintignore_negated_patterns.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintignore_negated_patterns.snap index dcf18e6ae302..a2baa5de50f1 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintignore_negated_patterns.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintignore_negated_patterns.snap @@ -49,7 +49,11 @@ biome.json migrate ━━━━━━━━━━━━━━━━━━━━ ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (1/1) of the rules can be migrated. + i 1 ESLint rules found + - 1 can be migrated to Biome's rules (run with --write to migrate) + - 100% (1) of your ESLint rules are fully covered by Biome + - 100% (1) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrc.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrc.snap index e13e229233ed..c4e7b744cd62 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrc.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrc.snap @@ -77,7 +77,11 @@ biome.json migrate ━━━━━━━━━━━━━━━━━━━━ ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (6/6) of the rules can be migrated. + i 6 ESLint rules found + - 6 can be migrated to Biome's rules (run with --write to migrate) + - 100% (6) of your ESLint rules are fully covered by Biome + - 100% (6) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson.snap index f450d3667e4b..1bc1be79576a 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson.snap @@ -77,7 +77,11 @@ biome.json migrate ━━━━━━━━━━━━━━━━━━━━ ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (6/6) of the rules can be migrated. + i 6 ESLint rules found + - 6 can be migrated to Biome's rules (run with --write to migrate) + - 100% (6) of your ESLint rules are fully covered by Biome + - 100% (6) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_exclude_inspired.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_exclude_inspired.snap index 4dd81dafb4d6..9a05979e8dd2 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_exclude_inspired.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_exclude_inspired.snap @@ -31,14 +31,17 @@ biome.json migrate ━━━━━━━━━━━━━━━━━━━━ ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (1/1) of the rules can be migrated. + i 1 ESLint rules found + - 0 can be migrated to Biome's rules (run with --write to migrate) + - +1 with --include-inspired + - 100% (1) of your ESLint rules are fully covered by Biome + - 0% (0) via direct migration to Biome rules + i Rules that can be migrated to an inspired rule using --include-inspired: - no-else-return - i Rules that can be migrated to a nursery rule using --include-nursery: - ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_extended_rules.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_extended_rules.snap index 729aa674d75c..f740fdc200e3 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_extended_rules.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_extended_rules.snap @@ -47,7 +47,11 @@ biome.json migrate ━━━━━━━━━━━━━━━━━━━━ ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (4/4) of the rules can be migrated. + i 4 ESLint rules found + - 4 can be migrated to Biome's rules (run with --write to migrate) + - 100% (4) of your ESLint rules are fully covered by Biome + - 100% (4) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_fix.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_fix.snap index c03a92cfc391..4129ba4693e0 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_fix.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_fix.snap @@ -65,7 +65,11 @@ expression: redactor(content) ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (6/6) of the rules have been migrated. + i 6 ESLint rules found + - 6 have been migrated to Biome's rules + - 100% (6) of your ESLint rules are fully covered by Biome + - 100% (6) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_include_inspired.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_include_inspired.snap index fb7a3842e49e..4379ecdd4adf 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_include_inspired.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_include_inspired.snap @@ -35,7 +35,11 @@ biome.json migrate ━━━━━━━━━━━━━━━━━━━━ ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (1/1) of the rules can be migrated. + i 1 ESLint rules found + - 1 can be migrated to Biome's rules (run with --write to migrate) + - 100% (1) of your ESLint rules are fully covered by Biome + - 100% (1) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_override_existing_config.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_override_existing_config.snap index 236fe4b5cd74..b40c7a149362 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_override_existing_config.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_override_existing_config.snap @@ -42,7 +42,11 @@ biome.json migrate ━━━━━━━━━━━━━━━━━━━━ ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (1/1) of the rules can be migrated. + i 1 ESLint rules found + - 1 can be migrated to Biome's rules (run with --write to migrate) + - 100% (1) of your ESLint rules are fully covered by Biome + - 100% (1) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_rule_options.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_rule_options.snap index 53cbed0b25ca..1ced98db1c13 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_rule_options.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_rule_options.snap @@ -227,7 +227,11 @@ biome.json migrate ━━━━━━━━━━━━━━━━━━━━ ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (6/6) of the rules can be migrated. + i 6 ESLint rules found + - 6 can be migrated to Biome's rules (run with --write to migrate) + - 100% (6) of your ESLint rules are fully covered by Biome + - 100% (6) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_write.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_write.snap index c03a92cfc391..4129ba4693e0 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_write.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_eslintrcjson_write.snap @@ -65,7 +65,11 @@ expression: redactor(content) ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (6/6) of the rules have been migrated. + i 6 ESLint rules found + - 6 have been migrated to Biome's rules + - 100% (6) of your ESLint rules are fully covered by Biome + - 100% (6) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_merge_with_overrides.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_merge_with_overrides.snap index 3495680a4503..f4c2aea919d3 100644 --- a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_merge_with_overrides.snap +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_merge_with_overrides.snap @@ -74,7 +74,11 @@ biome.json migrate ━━━━━━━━━━━━━━━━━━━━ ```block migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - i 100% (1/1) of the rules can be migrated. + i 1 ESLint rules found + - 1 can be migrated to Biome's rules (run with --write to migrate) + - 100% (1) of your ESLint rules are fully covered by Biome + - 100% (1) via direct migration to Biome rules + ``` diff --git a/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_rules_covered_by_formatter.snap b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_rules_covered_by_formatter.snap new file mode 100644 index 000000000000..d8fd6b95794a --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_commands_migrate_eslint/migrate_rules_covered_by_formatter.snap @@ -0,0 +1,71 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +expression: redactor(content) +--- +## `biome.json` + +```json +{ "linter": { "enabled": true } } +``` + +## `.eslintrc` + +```eslintrc +{ + "rules": { + "eol-last": "error", + "indent": ["error", 2], + }, + } +``` + +# Emitted Messages + +```block +biome.json migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Configuration file can be updated. + + 1 │ - {·"linter":·{·"enabled":·true·}·} + 1 │ + {·"linter":·{·"enabled":·true,·"rules":·{·"recommended":·false·}·}·} + 2 │ + + + +``` + +```block +migrate ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i 2 ESLint rules found + - 2 are obsolete because of Biome's formatter + - 0 can be migrated to Biome's rules (run with --write to migrate) + - 100% (2) of your ESLint rules are fully covered by Biome + - 0% (0) via direct migration to Biome rules + + + i Unsupported rules (0 incompatible with formatter, 1 made obsolete by the formatter, 1 covered by a formatter option, 0 not yet implemented, 0 unknown source): + + i These rules enforce behavior completely covered by the formatter (so you don't lose the functionality): + + - eol-last + + i These rules are covered by formatter options, but they require manual migration: + + - indent - Covered by Biome's indentWidth formatter option. + + +``` + +```block +configuration ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Migration results: + + - biome.json: configuration needs migration. + + i Use --write to apply the changes. + + $ biome migrate --write + + +```