diff --git a/crates/rome_diagnostics_categories/src/categories.rs b/crates/rome_diagnostics_categories/src/categories.rs index c7266c5c895f..17af1636686a 100644 --- a/crates/rome_diagnostics_categories/src/categories.rs +++ b/crates/rome_diagnostics_categories/src/categories.rs @@ -103,6 +103,7 @@ define_categories! { "lint/nursery/useAriaPropTypes": "https://biomejs.dev/linter/rules/use-aria-prop-types", "lint/nursery/useArrowFunction": "https://biomejs.dev/linter/rules/use-arrow-function", "lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/lint/rules/use-biome-suppression-comment", + "lint/nursery/useCollapsedElseIf": "https://biomejs.dev/lint/rules/use-collapsed-else-if", "lint/nursery/useExhaustiveDependencies": "https://biomejs.dev/linter/rules/use-exhaustive-dependencies", "lint/nursery/useGetterReturn": "https://biomejs.dev/linter/rules/use-getter-return", "lint/nursery/useGroupedTypeImport": "https://biomejs.dev/linter/rules/use-grouped-type-import", diff --git a/crates/rome_js_analyze/src/analyzers/nursery.rs b/crates/rome_js_analyze/src/analyzers/nursery.rs index 338087fd4ae4..dff4e2987ac7 100644 --- a/crates/rome_js_analyze/src/analyzers/nursery.rs +++ b/crates/rome_js_analyze/src/analyzers/nursery.rs @@ -12,6 +12,7 @@ pub(crate) mod no_static_only_class; pub(crate) mod no_useless_empty_export; pub(crate) mod no_void; pub(crate) mod use_arrow_function; +pub(crate) mod use_collapsed_else_if; pub(crate) mod use_getter_return; pub(crate) mod use_grouped_type_import; pub(crate) mod use_import_restrictions; @@ -31,6 +32,7 @@ declare_group! { self :: no_useless_empty_export :: NoUselessEmptyExport , self :: no_void :: NoVoid , self :: use_arrow_function :: UseArrowFunction , + self :: use_collapsed_else_if :: UseCollapsedElseIf , self :: use_getter_return :: UseGetterReturn , self :: use_grouped_type_import :: UseGroupedTypeImport , self :: use_import_restrictions :: UseImportRestrictions , diff --git a/crates/rome_js_analyze/src/analyzers/nursery/use_collapsed_else_if.rs b/crates/rome_js_analyze/src/analyzers/nursery/use_collapsed_else_if.rs new file mode 100644 index 000000000000..cb991f997c7f --- /dev/null +++ b/crates/rome_js_analyze/src/analyzers/nursery/use_collapsed_else_if.rs @@ -0,0 +1,154 @@ +use crate::JsRuleAction; +use rome_analyze::{context::RuleContext, declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic}; +use rome_console::markup; +use rome_diagnostics::Applicability; +use rome_js_syntax::{AnyJsStatement, JsBlockStatement, JsElseClause, JsIfStatement}; +use rome_rowan::{AstNode, AstNodeList, BatchMutationExt}; + +declare_rule! { + /// Enforce using `else if` instead of nested `if` in `else` clauses. + /// + /// If an `if` statement is the only statement in the `else` block, it is often clearer to use an `else if` form. + /// + /// Source: https://eslint.org/docs/latest/rules/no-lonely-if + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// if (condition) { + /// // ... + /// } else { + /// if (anotherCondition) { + /// // ... + /// } + /// } + /// ``` + /// + /// ```js,expect_diagnostic + /// if (condition) { + /// // ... + /// } else { + /// if (anotherCondition) { + /// // ... + /// } else { + /// // ... + /// } + /// } + /// ``` + /// + /// ### Valid + /// + /// ```js + /// if (condition) { + /// // ... + /// } else if (anotherCondition) { + /// // ... + /// } + /// ``` + /// + /// ```js + /// if (condition) { + /// // ... + /// } else if (anotherCondition) { + /// // ... + /// } else { + /// // ... + /// } + /// ``` + /// + /// ```js + /// if (condition) { + /// // ... + /// } else { + /// if (anotherCondition) { + /// // ... + /// } + /// doSomething(); + /// } + /// ``` + /// + pub(crate) UseCollapsedElseIf { + version: "next", + name: "useCollapsedElseIf", + recommended: false, + } +} + +pub(crate) struct RuleState { + block_statement: JsBlockStatement, + if_statement: JsIfStatement, +} + +impl Rule for UseCollapsedElseIf { + type Query = Ast; + type State = RuleState; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let else_clause = ctx.query(); + let alternate = else_clause.alternate().ok()?; + let AnyJsStatement::JsBlockStatement(block_statement) = alternate else { + return None; + }; + let statements = block_statement.statements(); + if statements.len() != 1 { + return None; + } + if let AnyJsStatement::JsIfStatement(if_statement) = statements.first()? { + Some(RuleState { + block_statement, + if_statement, + }) + } else { + None + } + } + + fn diagnostic(_: &RuleContext, state: &Self::State) -> Option { + Some(RuleDiagnostic::new( + rule_category!(), + state.if_statement.syntax().text_range(), + markup! { + "This ""if"" statement can be collapsed into an ""else if"" statement." + }, + )) + } + + fn action(ctx: &RuleContext, state: &Self::State) -> Option { + let RuleState { + block_statement, + if_statement, + } = state; + + let has_comments = block_statement + .l_curly_token() + .ok()? + .has_trailing_comments() + || if_statement.syntax().has_leading_comments() + || if_statement.syntax().has_trailing_comments() + || block_statement.r_curly_token().ok()?.has_leading_comments(); + + let applicability = if has_comments { + Applicability::MaybeIncorrect + } else { + Applicability::Always + }; + + let mut mutation = ctx.root().begin(); + mutation.replace_node( + AnyJsStatement::from(block_statement.clone()), + if_statement.clone().into(), + ); + + Some(JsRuleAction { + category: ActionCategory::QuickFix, + applicability, + message: markup! { "Use collapsed ""else if"" instead." } + .to_owned(), + mutation, + }) + } +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/useCollapsedElseIf/invalid.js b/crates/rome_js_analyze/tests/specs/nursery/useCollapsedElseIf/invalid.js new file mode 100644 index 000000000000..bd9f21aeaf9e --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useCollapsedElseIf/invalid.js @@ -0,0 +1,101 @@ +/** + * Safe fixes: + */ + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } +} + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } else { + // ... + } +} + +/** + * Suggested fixes: + */ + +if (condition) { + // ... +} else { // Comment + if (anotherCondition) { + // ... + } +} + +if (condition) { + // ... +} else { + // Comment + if (anotherCondition) { + // ... + } +} + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } // Comment +} + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } + // Comment +} + +if (condition) { + // ... +} else { // Comment + if (anotherCondition) { + // ... + } else { + // ... + } +} + +if (condition) { + // ... +} else { + // Comment + if (anotherCondition) { + // ... + } else { + // ... + } +} + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } else { + // ... + } // Comment +} + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } else { + // ... + } + // Comment +} diff --git a/crates/rome_js_analyze/tests/specs/nursery/useCollapsedElseIf/invalid.js.snap b/crates/rome_js_analyze/tests/specs/nursery/useCollapsedElseIf/invalid.js.snap new file mode 100644 index 000000000000..46306a750032 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useCollapsedElseIf/invalid.js.snap @@ -0,0 +1,465 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```js +/** + * Safe fixes: + */ + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } +} + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } else { + // ... + } +} + +/** + * Suggested fixes: + */ + +if (condition) { + // ... +} else { // Comment + if (anotherCondition) { + // ... + } +} + +if (condition) { + // ... +} else { + // Comment + if (anotherCondition) { + // ... + } +} + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } // Comment +} + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } + // Comment +} + +if (condition) { + // ... +} else { // Comment + if (anotherCondition) { + // ... + } else { + // ... + } +} + +if (condition) { + // ... +} else { + // Comment + if (anotherCondition) { + // ... + } else { + // ... + } +} + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } else { + // ... + } // Comment +} + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } else { + // ... + } + // Comment +} + +``` + +# Diagnostics +``` +invalid.js:7:9 lint/nursery/useCollapsedElseIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into an else if statement. + + 5 │ if (condition) { + 6 │ // ... + > 7 │ } else { + │ + > 8 │ if (anotherCondition) { + > 9 │ // ... + > 10 │ } + │ ^ + 11 │ } + 12 │ + + i Safe fix: Use collapsed else if instead. + + 5 5 │ if (condition) { + 6 6 │ // ... + 7 │ - }·else·{ + 8 │ - → if·(anotherCondition)·{ + 7 │ + }·else·if·(anotherCondition)·{ + 9 8 │ // ... + 10 │ - → } + 11 │ - } + 9 │ + → } + 12 10 │ + 13 11 │ if (condition) { + + +``` + +``` +invalid.js:15:9 lint/nursery/useCollapsedElseIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into an else if statement. + + 13 │ if (condition) { + 14 │ // ... + > 15 │ } else { + │ + > 16 │ if (anotherCondition) { + > 17 │ // ... + > 18 │ } else { + > 19 │ // ... + > 20 │ } + │ ^ + 21 │ } + 22 │ + + i Safe fix: Use collapsed else if instead. + + 13 13 │ if (condition) { + 14 14 │ // ... + 15 │ - }·else·{ + 16 │ - → if·(anotherCondition)·{ + 15 │ + }·else·if·(anotherCondition)·{ + 17 16 │ // ... + 18 17 │ } else { + 19 18 │ // ... + 20 │ - → } + 21 │ - } + 19 │ + → } + 22 20 │ + 23 21 │ /** + + +``` + +``` +invalid.js:29:20 lint/nursery/useCollapsedElseIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into an else if statement. + + 27 │ if (condition) { + 28 │ // ... + > 29 │ } else { // Comment + │ + > 30 │ if (anotherCondition) { + > 31 │ // ... + > 32 │ } + │ ^ + 33 │ } + 34 │ + + i Suggested fix: Use collapsed else if instead. + + 27 27 │ if (condition) { + 28 28 │ // ... + 29 │ - }·else·{·//·Comment + 30 │ - → if·(anotherCondition)·{ + 29 │ + }·else·if·(anotherCondition)·{ + 31 30 │ // ... + 32 │ - → } + 33 │ - } + 31 │ + → } + 34 32 │ + 35 33 │ if (condition) { + + +``` + +``` +invalid.js:37:9 lint/nursery/useCollapsedElseIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into an else if statement. + + 35 │ if (condition) { + 36 │ // ... + > 37 │ } else { + │ + > 38 │ // Comment + > 39 │ if (anotherCondition) { + > 40 │ // ... + > 41 │ } + │ ^ + 42 │ } + 43 │ + + i Suggested fix: Use collapsed else if instead. + + 35 35 │ if (condition) { + 36 36 │ // ... + 37 │ - }·else·{ + 38 │ - → //·Comment + 39 │ - → if·(anotherCondition)·{ + 37 │ + }·else·if·(anotherCondition)·{ + 40 38 │ // ... + 41 │ - → } + 42 │ - } + 39 │ + → } + 43 40 │ + 44 41 │ if (condition) { + + +``` + +``` +invalid.js:46:9 lint/nursery/useCollapsedElseIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into an else if statement. + + 44 │ if (condition) { + 45 │ // ... + > 46 │ } else { + │ + > 47 │ if (anotherCondition) { + > 48 │ // ... + > 49 │ } // Comment + │ ^^^^^^^^^^^^ + 50 │ } + 51 │ + + i Suggested fix: Use collapsed else if instead. + + 44 44 │ if (condition) { + 45 45 │ // ... + 46 │ - }·else·{ + 47 │ - → if·(anotherCondition)·{ + 46 │ + }·else·if·(anotherCondition)·{ + 48 47 │ // ... + 49 │ - → }·//·Comment + 50 │ - } + 48 │ + → } + 51 49 │ + 52 50 │ if (condition) { + + +``` + +``` +invalid.js:54:9 lint/nursery/useCollapsedElseIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into an else if statement. + + 52 │ if (condition) { + 53 │ // ... + > 54 │ } else { + │ + > 55 │ if (anotherCondition) { + > 56 │ // ... + > 57 │ } + │ ^ + 58 │ // Comment + 59 │ } + + i Suggested fix: Use collapsed else if instead. + + 52 52 │ if (condition) { + 53 53 │ // ... + 54 │ - }·else·{ + 55 │ - → if·(anotherCondition)·{ + 54 │ + }·else·if·(anotherCondition)·{ + 56 55 │ // ... + 57 │ - → } + 58 │ - → //·Comment + 59 │ - } + 56 │ + → } + 60 57 │ + 61 58 │ if (condition) { + + +``` + +``` +invalid.js:63:20 lint/nursery/useCollapsedElseIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into an else if statement. + + 61 │ if (condition) { + 62 │ // ... + > 63 │ } else { // Comment + │ + > 64 │ if (anotherCondition) { + > 65 │ // ... + > 66 │ } else { + > 67 │ // ... + > 68 │ } + │ ^ + 69 │ } + 70 │ + + i Suggested fix: Use collapsed else if instead. + + 61 61 │ if (condition) { + 62 62 │ // ... + 63 │ - }·else·{·//·Comment + 64 │ - → if·(anotherCondition)·{ + 63 │ + }·else·if·(anotherCondition)·{ + 65 64 │ // ... + 66 65 │ } else { + 67 66 │ // ... + 68 │ - → } + 69 │ - } + 67 │ + → } + 70 68 │ + 71 69 │ if (condition) { + + +``` + +``` +invalid.js:73:9 lint/nursery/useCollapsedElseIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into an else if statement. + + 71 │ if (condition) { + 72 │ // ... + > 73 │ } else { + │ + > 74 │ // Comment + ... + > 78 │ // ... + > 79 │ } + │ ^ + 80 │ } + 81 │ + + i Suggested fix: Use collapsed else if instead. + + 71 71 │ if (condition) { + 72 72 │ // ... + 73 │ - }·else·{ + 74 │ - → //·Comment + 75 │ - → if·(anotherCondition)·{ + 73 │ + }·else·if·(anotherCondition)·{ + 76 74 │ // ... + 77 75 │ } else { + 78 76 │ // ... + 79 │ - → } + 80 │ - } + 77 │ + → } + 81 78 │ + 82 79 │ if (condition) { + + +``` + +``` +invalid.js:84:9 lint/nursery/useCollapsedElseIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into an else if statement. + + 82 │ if (condition) { + 83 │ // ... + > 84 │ } else { + │ + > 85 │ if (anotherCondition) { + > 86 │ // ... + > 87 │ } else { + > 88 │ // ... + > 89 │ } // Comment + │ ^^^^^^^^^^^^ + 90 │ } + 91 │ + + i Suggested fix: Use collapsed else if instead. + + 82 82 │ if (condition) { + 83 83 │ // ... + 84 │ - }·else·{ + 85 │ - → if·(anotherCondition)·{ + 84 │ + }·else·if·(anotherCondition)·{ + 86 85 │ // ... + 87 86 │ } else { + 88 87 │ // ... + 89 │ - → }·//·Comment + 90 │ - } + 88 │ + → } + 91 89 │ + 92 90 │ if (condition) { + + +``` + +``` +invalid.js:94:9 lint/nursery/useCollapsedElseIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into an else if statement. + + 92 │ if (condition) { + 93 │ // ... + > 94 │ } else { + │ + > 95 │ if (anotherCondition) { + > 96 │ // ... + > 97 │ } else { + > 98 │ // ... + > 99 │ } + │ ^ + 100 │ // Comment + 101 │ } + + i Suggested fix: Use collapsed else if instead. + + 92 92 │ if (condition) { + 93 93 │ // ... + 94 │ - }·else·{ + 95 │ - → if·(anotherCondition)·{ + 94 │ + }·else·if·(anotherCondition)·{ + 96 95 │ // ... + 97 96 │ } else { + 98 97 │ // ... + 99 │ - → } + 100 │ - → //·Comment + 101 │ - } + 98 │ + → } + 102 99 │ + + +``` + + diff --git a/crates/rome_js_analyze/tests/specs/nursery/useCollapsedElseIf/valid.js b/crates/rome_js_analyze/tests/specs/nursery/useCollapsedElseIf/valid.js new file mode 100644 index 000000000000..4a6c6d5a0695 --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useCollapsedElseIf/valid.js @@ -0,0 +1,22 @@ +if (condition) { + // ... +} else if (anotherCondition) { + // ... +} + +if (condition) { + // ... +} else if (anotherCondition) { + // ... +} else { + // ... +} + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } + doSomething(); +} \ No newline at end of file diff --git a/crates/rome_js_analyze/tests/specs/nursery/useCollapsedElseIf/valid.js.snap b/crates/rome_js_analyze/tests/specs/nursery/useCollapsedElseIf/valid.js.snap new file mode 100644 index 000000000000..40b1df03a0be --- /dev/null +++ b/crates/rome_js_analyze/tests/specs/nursery/useCollapsedElseIf/valid.js.snap @@ -0,0 +1,31 @@ +--- +source: crates/rome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```js +if (condition) { + // ... +} else if (anotherCondition) { + // ... +} + +if (condition) { + // ... +} else if (anotherCondition) { + // ... +} else { + // ... +} + +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } + doSomething(); +} +``` + + diff --git a/crates/rome_service/src/configuration/linter/rules.rs b/crates/rome_service/src/configuration/linter/rules.rs index 14d235e1a331..d4f3cb691f50 100644 --- a/crates/rome_service/src/configuration/linter/rules.rs +++ b/crates/rome_service/src/configuration/linter/rules.rs @@ -2021,6 +2021,10 @@ pub struct Nursery { #[bpaf(long("use-arrow-function"), argument("on|off|warn"), optional, hide)] #[serde(skip_serializing_if = "Option::is_none")] pub use_arrow_function: Option, + #[doc = "Enforce using else if instead of nested if in else clauses."] + #[bpaf(long("use-collapsed-else-if"), argument("on|off|warn"), optional, hide)] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_collapsed_else_if: Option, #[doc = "Enforce all dependencies are correctly specified."] #[bpaf( long("use-exhaustive-dependencies"), @@ -2076,7 +2080,7 @@ pub struct Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 30] = [ + pub(crate) const GROUP_RULES: [&'static str; 31] = [ "noAccumulatingSpread", "noAriaUnsupportedElements", "noBannedTypes", @@ -2099,6 +2103,7 @@ impl Nursery { "noVoid", "useAriaPropTypes", "useArrowFunction", + "useCollapsedElseIf", "useExhaustiveDependencies", "useGetterReturn", "useGroupedTypeImport", @@ -2146,13 +2151,13 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), ]; - const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 30] = [ + const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 31] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -2183,6 +2188,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended(&self) -> bool { matches!(self.recommended, Some(true)) } @@ -2303,46 +2309,51 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { + if let Some(rule) = self.use_collapsed_else_if.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_getter_return.as_ref() { + if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.use_grouped_type_import.as_ref() { + if let Some(rule) = self.use_getter_return.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.use_hook_at_top_level.as_ref() { + if let Some(rule) = self.use_grouped_type_import.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_hook_at_top_level.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_is_array.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_literal_enum_members.as_ref() { + if let Some(rule) = self.use_is_array.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_naming_convention.as_ref() { + if let Some(rule) = self.use_literal_enum_members.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } + if let Some(rule) = self.use_naming_convention.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -2457,46 +2468,51 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { + if let Some(rule) = self.use_collapsed_else_if.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_getter_return.as_ref() { + if let Some(rule) = self.use_exhaustive_dependencies.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.use_grouped_type_import.as_ref() { + if let Some(rule) = self.use_getter_return.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.use_hook_at_top_level.as_ref() { + if let Some(rule) = self.use_grouped_type_import.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_hook_at_top_level.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } } - if let Some(rule) = self.use_is_array.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } } - if let Some(rule) = self.use_literal_enum_members.as_ref() { + if let Some(rule) = self.use_is_array.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_naming_convention.as_ref() { + if let Some(rule) = self.use_literal_enum_members.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } + if let Some(rule) = self.use_naming_convention.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -2508,7 +2524,7 @@ impl Nursery { pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 20] { Self::RECOMMENDED_RULES_AS_FILTERS } - pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 30] { Self::ALL_RULES_AS_FILTERS } + pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 31] { Self::ALL_RULES_AS_FILTERS } #[doc = r" Select preset rules"] pub(crate) fn collect_preset_rules( &self, @@ -2551,6 +2567,7 @@ impl Nursery { "noVoid" => self.no_void.as_ref(), "useAriaPropTypes" => self.use_aria_prop_types.as_ref(), "useArrowFunction" => self.use_arrow_function.as_ref(), + "useCollapsedElseIf" => self.use_collapsed_else_if.as_ref(), "useExhaustiveDependencies" => self.use_exhaustive_dependencies.as_ref(), "useGetterReturn" => self.use_getter_return.as_ref(), "useGroupedTypeImport" => self.use_grouped_type_import.as_ref(), diff --git a/crates/rome_service/src/configuration/parse/json/rules.rs b/crates/rome_service/src/configuration/parse/json/rules.rs index 6be5e38f1eb8..dfd657a7d3a4 100644 --- a/crates/rome_service/src/configuration/parse/json/rules.rs +++ b/crates/rome_service/src/configuration/parse/json/rules.rs @@ -1786,6 +1786,7 @@ impl VisitNode for Nursery { "noVoid", "useAriaPropTypes", "useArrowFunction", + "useCollapsedElseIf", "useExhaustiveDependencies", "useGetterReturn", "useGroupedTypeImport", @@ -2319,6 +2320,29 @@ impl VisitNode for Nursery { )); } }, + "useCollapsedElseIf" => match value { + AnyJsonValue::JsonStringValue(_) => { + let mut configuration = RuleConfiguration::default(); + self.map_to_known_string(&value, name_text, &mut configuration, diagnostics)?; + self.use_collapsed_else_if = Some(configuration); + } + AnyJsonValue::JsonObjectValue(_) => { + let mut rule_configuration = RuleConfiguration::default(); + rule_configuration.map_rule_configuration( + &value, + name_text, + "useCollapsedElseIf", + diagnostics, + )?; + self.use_collapsed_else_if = Some(rule_configuration); + } + _ => { + diagnostics.push(DeserializationDiagnostic::new_incorrect_type( + "object or string", + value.range(), + )); + } + }, "useExhaustiveDependencies" => match value { AnyJsonValue::JsonStringValue(_) => { let mut configuration = RuleConfiguration::default(); diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json index 0fbe20f0529c..ef0abcb0511b 100644 --- a/editors/vscode/configuration_schema.json +++ b/editors/vscode/configuration_schema.json @@ -989,6 +989,13 @@ { "type": "null" } ] }, + "useCollapsedElseIf": { + "description": "Enforce using else if instead of nested if in else clauses.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useExhaustiveDependencies": { "description": "Enforce all dependencies are correctly specified.", "anyOf": [ diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 5694737d2347..5134f6b11f5d 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -659,6 +659,10 @@ export interface Nursery { * Use arrow functions over function expressions. */ useArrowFunction?: RuleConfiguration; + /** + * Enforce using else if instead of nested if in else clauses. + */ + useCollapsedElseIf?: RuleConfiguration; /** * Enforce all dependencies are correctly specified. */ @@ -1233,6 +1237,7 @@ export type Category = | "lint/nursery/useAriaPropTypes" | "lint/nursery/useArrowFunction" | "lint/nursery/useBiomeSuppressionComment" + | "lint/nursery/useCollapsedElseIf" | "lint/nursery/useExhaustiveDependencies" | "lint/nursery/useGetterReturn" | "lint/nursery/useGroupedTypeImport" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 0fbe20f0529c..ef0abcb0511b 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -989,6 +989,13 @@ { "type": "null" } ] }, + "useCollapsedElseIf": { + "description": "Enforce using else if instead of nested if in else clauses.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useExhaustiveDependencies": { "description": "Enforce all dependencies are correctly specified.", "anyOf": [ diff --git a/website/src/components/generated/NumberOfRules.astro b/website/src/components/generated/NumberOfRules.astro index 93f856432bf4..525d6a91a00b 100644 --- a/website/src/components/generated/NumberOfRules.astro +++ b/website/src/components/generated/NumberOfRules.astro @@ -1,2 +1,2 @@ -

Biome's linter has a total of 156 rules

\ No newline at end of file +

Biome's linter has a total of 157 rules

\ No newline at end of file diff --git a/website/src/content/docs/linter/rules/index.mdx b/website/src/content/docs/linter/rules/index.mdx index bcea1dad8740..897e7ac70d6d 100644 --- a/website/src/content/docs/linter/rules/index.mdx +++ b/website/src/content/docs/linter/rules/index.mdx @@ -350,6 +350,8 @@ Disallow the use of void operators, which is not a familiar operato Enforce that ARIA state and property values are valid. ### [useArrowFunction](/linter/rules/use-arrow-function) Use arrow functions over function expressions. +### [useCollapsedElseIf](/linter/rules/use-collapsed-else-if) +Enforce using else if instead of nested if in else clauses. ### [useExhaustiveDependencies](/linter/rules/use-exhaustive-dependencies) Enforce all dependencies are correctly specified. ### [useGetterReturn](/linter/rules/use-getter-return) diff --git a/website/src/content/docs/linter/rules/use-collapsed-else-if.md b/website/src/content/docs/linter/rules/use-collapsed-else-if.md new file mode 100644 index 000000000000..85b58f051c85 --- /dev/null +++ b/website/src/content/docs/linter/rules/use-collapsed-else-if.md @@ -0,0 +1,136 @@ +--- +title: useCollapsedElseIf (since vnext) +--- + + +Enforce using `else if` instead of nested `if` in `else` clauses. + +If an `if` statement is the only statement in the `else` block, it is often clearer to use an `else if` form. + +Source: https://eslint.org/docs/latest/rules/no-lonely-if + +## Examples + +### Invalid + +```jsx +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } +} +``` + +

nursery/useCollapsedElseIf.js:3:9 lint/nursery/useCollapsedElseIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━
+
+   This if statement can be collapsed into an else if statement.
+  
+    1 │ if (condition) {
+    2 │     // ...
+  > 3 │ } else {
+           
+  > 4 │     if (anotherCondition) {
+  > 5 │         // ...
+  > 6 │     }
+       ^
+    7 │ }
+    8 │ 
+  
+   Safe fix: Use collapsed else if instead.
+  
+    1 1  if (condition) {
+    2 2      // ...
+    3  - }·else·{
+    4  - ····if·(anotherCondition)·{
+      3+ }·else·if·(anotherCondition)·{
+    5 4          // ...
+    6  - ····}
+    7  - }
+      5+ ····}
+    8 6  
+  
+
+ +```jsx +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } else { + // ... + } +} +``` + +
nursery/useCollapsedElseIf.js:3:9 lint/nursery/useCollapsedElseIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━
+
+   This if statement can be collapsed into an else if statement.
+  
+     1 │ if (condition) {
+     2 │     // ...
+   > 3 │ } else {
+            
+   > 4 │     if (anotherCondition) {
+   > 5 │         // ...
+   > 6 │     } else {
+   > 7 │         // ...
+   > 8 │     }
+        ^
+     9 │ }
+    10 │ 
+  
+   Safe fix: Use collapsed else if instead.
+  
+     1 1  if (condition) {
+     2 2      // ...
+     3  - }·else·{
+     4  - ····if·(anotherCondition)·{
+       3+ }·else·if·(anotherCondition)·{
+     5 4          // ...
+     6 5      } else {
+     7 6          // ...
+     8  - ····}
+     9  - }
+       7+ ····}
+    10 8  
+  
+
+ +### Valid + +```jsx +if (condition) { + // ... +} else if (anotherCondition) { + // ... +} +``` + +```jsx +if (condition) { + // ... +} else if (anotherCondition) { + // ... +} else { + // ... +} +``` + +```jsx +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } + doSomething(); +} +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options)