From bbb93b98b492fc1a21015ba8e1a604e7dc758ffc Mon Sep 17 00:00:00 2001 From: Naoki Ikeguchi Date: Wed, 16 Oct 2024 12:34:35 +0900 Subject: [PATCH] feat(lint): useCollapsedIf JS lint rule (#4179) Signed-off-by: Naoki Ikeguchi --- .../migrate/eslint_any_rule_to_biome.rs | 8 + .../src/analyzer/linter/rules.rs | 67 +- .../src/categories.rs | 1 + crates/biome_js_analyze/src/lint/nursery.rs | 2 + .../src/lint/nursery/use_collapsed_if.rs | 198 ++++ crates/biome_js_analyze/src/options.rs | 2 + .../specs/nursery/useCollapsedIf/invalid.js | 156 +++ .../nursery/useCollapsedIf/invalid.js.snap | 1024 +++++++++++++++++ .../specs/nursery/useCollapsedIf/valid.js | 40 + .../nursery/useCollapsedIf/valid.js.snap | 48 + .../@biomejs/backend-jsonrpc/src/workspace.ts | 5 + .../@biomejs/biome/configuration_schema.json | 7 + 12 files changed, 1534 insertions(+), 24 deletions(-) create mode 100644 crates/biome_js_analyze/src/lint/nursery/use_collapsed_if.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js.snap diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs index 439041656c45..b5c5dd51781f 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs @@ -1543,6 +1543,14 @@ pub(crate) fn migrate_eslint_any_rule( let rule = group.use_is_array.get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "unicorn/no-lonely-if" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group.use_collapsed_if.get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "unicorn/no-static-only-class" => { let group = rules.complexity.get_or_insert_with(Default::default); let rule = group.no_static_only_class.get_or_insert(Default::default()); diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index f57c2e14820a..76de7ff26e0a 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3395,6 +3395,9 @@ pub struct Nursery { #[doc = "Use at() instead of integer index access."] #[serde(skip_serializing_if = "Option::is_none")] pub use_at_index: Option>, + #[doc = "Enforce using single if instead of nested if clauses."] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_collapsed_if: Option>, #[doc = "Enforce declaring components only within modules that export React Components exclusively."] #[serde(skip_serializing_if = "Option::is_none")] pub use_component_export_only_modules: @@ -3488,6 +3491,7 @@ impl Nursery { "useAdjacentOverloadSignatures", "useAriaPropsSupportedByRole", "useAtIndex", + "useCollapsedIf", "useComponentExportOnlyModules", "useConsistentCurlyBraces", "useConsistentMemberAccessibility", @@ -3528,9 +3532,9 @@ 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[32]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3578,6 +3582,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3764,61 +3769,66 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_component_export_only_modules.as_ref() { + if let Some(rule) = self.use_collapsed_if.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_consistent_curly_braces.as_ref() { + if let Some(rule) = self.use_component_export_only_modules.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { + if let Some(rule) = self.use_consistent_curly_braces.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.use_deprecated_reason.as_ref() { + if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.use_explicit_type.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.use_guard_for_in.as_ref() { + if let Some(rule) = self.use_explicit_type.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_guard_for_in.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - if let Some(rule) = self.use_sorted_classes.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[41])); } } - if let Some(rule) = self.use_strict_mode.as_ref() { + if let Some(rule) = self.use_sorted_classes.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } - if let Some(rule) = self.use_trim_start_end.as_ref() { + if let Some(rule) = self.use_strict_mode.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_trim_start_end.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); } } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -3993,61 +4003,66 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_component_export_only_modules.as_ref() { + if let Some(rule) = self.use_collapsed_if.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_consistent_curly_braces.as_ref() { + if let Some(rule) = self.use_component_export_only_modules.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { + if let Some(rule) = self.use_consistent_curly_braces.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.use_deprecated_reason.as_ref() { + if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.use_explicit_type.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.use_guard_for_in.as_ref() { + if let Some(rule) = self.use_explicit_type.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_guard_for_in.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } } - if let Some(rule) = self.use_sorted_classes.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[41])); } } - if let Some(rule) = self.use_strict_mode.as_ref() { + if let Some(rule) = self.use_sorted_classes.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } - if let Some(rule) = self.use_trim_start_end.as_ref() { + if let Some(rule) = self.use_strict_mode.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_trim_start_end.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); } } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -4220,6 +4235,10 @@ impl Nursery { .use_at_index .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "useCollapsedIf" => self + .use_collapsed_if + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "useComponentExportOnlyModules" => self .use_component_export_only_modules .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 4d03ac67332e..7a5f6e5341dc 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -188,6 +188,7 @@ define_categories! { "lint/nursery/useAriaPropsSupportedByRole": "https://biomejs.dev/linter/rules/use-aria-props-supported-by-role", "lint/nursery/useAtIndex": "https://biomejs.dev/linter/rules/use-at-index", "lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment", + "lint/nursery/useCollapsedIf": "https://biomejs.dev/linter/rules/use-collapsed-if", "lint/nursery/useComponentExportOnlyModules": "https://biomejs.dev/linter/rules/use-components-only-module", "lint/nursery/useConsistentCurlyBraces": "https://biomejs.dev/linter/rules/use-consistent-curly-braces", "lint/nursery/useConsistentMemberAccessibility": "https://biomejs.dev/linter/rules/use-consistent-member-accessibility", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 4a3341d01aca..4660799ace74 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -27,6 +27,7 @@ pub mod no_useless_string_raw; pub mod use_adjacent_overload_signatures; pub mod use_aria_props_supported_by_role; pub mod use_at_index; +pub mod use_collapsed_if; pub mod use_component_export_only_modules; pub mod use_consistent_curly_braces; pub mod use_consistent_member_accessibility; @@ -67,6 +68,7 @@ declare_lint_group! { self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole , self :: use_at_index :: UseAtIndex , + self :: use_collapsed_if :: UseCollapsedIf , self :: use_component_export_only_modules :: UseComponentExportOnlyModules , self :: use_consistent_curly_braces :: UseConsistentCurlyBraces , self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility , diff --git a/crates/biome_js_analyze/src/lint/nursery/use_collapsed_if.rs b/crates/biome_js_analyze/src/lint/nursery/use_collapsed_if.rs new file mode 100644 index 000000000000..ced96dd5292e --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/use_collapsed_if.rs @@ -0,0 +1,198 @@ +use biome_analyze::{ + context::RuleContext, declare_lint_rule, ActionCategory, Ast, FixKind, Rule, RuleDiagnostic, + RuleSource, +}; +use biome_console::markup; +use biome_js_factory::make::{js_logical_expression, parenthesized, token}; +use biome_js_syntax::parentheses::NeedsParentheses; +use biome_js_syntax::{AnyJsStatement, JsIfStatement, T}; +use biome_rowan::{AstNode, AstNodeList, BatchMutationExt}; + +use crate::JsRuleAction; + +declare_lint_rule! { + /// Enforce using single `if` instead of nested `if` clauses. + /// + /// If an `if (b)` statement is the only statement in an `if (a)` block, it is often clearer to use an `if (a && b)` form. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// if (condition) { + /// if (anotherCondition) { + /// // ... + /// } + /// } + /// ``` + /// + /// ```js,expect_diagnostic + /// if (condition) { + /// // Comment + /// if (anotherCondition) { + /// // ... + /// } + /// } + /// ``` + /// + /// ### Valid + /// + /// ```js + /// if (condition && anotherCondition) { + /// // ... + /// } + /// ``` + /// + /// ```js + /// if (condition) { + /// if (anotherCondition) { + /// // ... + /// } + /// doSomething(); + /// } + /// ``` + /// + /// ```js + /// if (condition) { + /// if (anotherCondition) { + /// // ... + /// } else { + /// // ... + /// } + /// } + /// ``` + /// + pub UseCollapsedIf { + version: "next", + name: "useCollapsedIf", + language: "js", + sources: &[ + RuleSource::EslintUnicorn("no-lonely-if"), + RuleSource::Clippy("collapsible_if") + ], + recommended: false, + fix_kind: FixKind::Safe, + } +} + +pub struct RuleState { + parent_if_statement: JsIfStatement, + child_if_statement: JsIfStatement, +} + +impl Rule for UseCollapsedIf { + type Query = Ast; + type State = RuleState; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let if_stmt = ctx.query(); + let consequent = if_stmt.consequent().ok()?; + + let child_if_statement = match consequent { + // If `consequent` is a `JsBlockStatement` and the block contains only one + // `JsIfStatement`, the child `if` statement should be merged. + AnyJsStatement::JsBlockStatement(parent_block_statement) => { + let statements = parent_block_statement.statements(); + if statements.len() != 1 { + return None; + } + + let AnyJsStatement::JsIfStatement(child_if_statement) = statements.first()? else { + return None; + }; + + Some(child_if_statement) + } + // If `consequent` is a `JsIfStatement` without any block, it should be merged. + AnyJsStatement::JsIfStatement(child_if_statement) => Some(child_if_statement), + _ => None, + }?; + + // It cannot be merged if the child `if` statement has any else clause(s). + if child_if_statement.else_clause().is_some() { + return None; + } + + Some(RuleState { + parent_if_statement: if_stmt.clone(), + child_if_statement, + }) + } + + fn diagnostic(_ctx: &RuleContext, state: &Self::State) -> Option { + Some(RuleDiagnostic::new( + rule_category!(), + state.child_if_statement.syntax().text_range(), + markup! { + "This ""if"" statement can be collapsed into another ""if"" statement." + }, + )) + } + + fn action(ctx: &RuleContext, state: &Self::State) -> Option { + let RuleState { + parent_if_statement, + child_if_statement, + } = state; + + let parent_consequent = parent_if_statement.consequent().ok()?; + let parent_test = parent_if_statement.test().ok()?; + let child_consequent = child_if_statement.consequent().ok()?; + let child_test = child_if_statement.test().ok()?; + + let parent_has_comments = match &parent_consequent { + AnyJsStatement::JsBlockStatement(block_stmt) => { + block_stmt.l_curly_token().ok()?.has_trailing_comments() + || block_stmt.r_curly_token().ok()?.has_leading_comments() + } + _ => false, + }; + + let has_comments = parent_has_comments + || child_if_statement.syntax().has_comments_direct() + || child_if_statement + .r_paren_token() + .ok()? + .has_trailing_comments(); + if has_comments { + return None; + } + + let mut expr = + js_logical_expression(parent_test.clone(), token(T![&&]), child_test.clone()); + + // Parenthesize arms of the `&&` expression if needed + let left = expr.left().ok()?; + if left.needs_parentheses() { + expr = expr.with_left(parenthesized(left).into()); + } + + let right = expr.right().ok()?; + if right.needs_parentheses() { + expr = expr.with_right(parenthesized(right).into()); + } + + // If the inner `if` statement has no block and the statement does not end with semicolon, + // it cannot be fixed automatically because that will break the ASI rule. + if !matches!(&child_consequent, AnyJsStatement::JsBlockStatement(_)) { + let last_token = child_consequent.syntax().last_token()?; + if last_token.kind() != T![;] { + return None; + } + } + + let mut mutation = ctx.root().begin(); + mutation.replace_node(parent_test, expr.into()); + mutation.replace_node(parent_consequent, child_consequent); + + Some(JsRuleAction::new( + ActionCategory::QuickFix, + ctx.metadata().applicability(), + markup! { "Use collapsed ""if"" instead." }.to_owned(), + mutation, + )) + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index 93ab2a833591..fc0bd18366de 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -302,6 +302,8 @@ pub type UseButtonType = ::Options; pub type UseCollapsedElseIf = ::Options; +pub type UseCollapsedIf = + ::Options; pub type UseComponentExportOnlyModules = < lint :: nursery :: use_component_export_only_modules :: UseComponentExportOnlyModules as biome_analyze :: Rule > :: Options ; pub type UseConsistentArrayType = < lint :: style :: use_consistent_array_type :: UseConsistentArrayType as biome_analyze :: Rule > :: Options ; pub type UseConsistentBuiltinInstantiation = < lint :: style :: use_consistent_builtin_instantiation :: UseConsistentBuiltinInstantiation as biome_analyze :: Rule > :: Options ; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js new file mode 100644 index 000000000000..055684561cb1 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js @@ -0,0 +1,156 @@ +/** + * Safe fixes: + */ + +if (condition) { + if (anotherCondition) { + // ... + } +} + +if (condition) { + if (anotherCondition) { + // ... + } +} else { + // ... +} + +if (condition) // Comment + if (anotherCondition) + doSomething(); + +// Inner one is `JsBlockStatement` +if (condition) if (anotherCondition) { + // ... +} + +// Outer one is `JsBlockStatement` +if (condition) { + if (anotherCondition) doSomething(); +} + +// No `JsBlockStatement` +if (condition) if (anotherCondition) doSomething(); + +// `JsEmptyStatement` +if (condition) if (anotherCondition); + +// Nested +if (a) { + if (b) { + // ... + } +} else if (c) { + if (d) { + // ... + } +} + +// Need parenthesis +function* foo() { + if (a || b) + if (a ?? b) + if (a ? b : c) + if (a = b) + if (a += b) + if (a -= b) + if (a &&= b) + if (yield a) + if (a, b); +} + +// Should not add parenthesis +async function foo() { + if (a) + if (await a) + if (a.b) + if (a && b); +} + +// Don't case parenthesis in outer test +if (((a || b))) if (((c || d))); + +// Semicolon +if (a) + if (b) foo() + ;[].forEach(bar) + +if (a) { + if (b) foo() +} +;[].forEach(bar) + +/** + * Suggested fixes: + */ + +if (condition) { // Comment + if (anotherCondition) { + // ... + } +} + +if (condition) { + // Comment + if (anotherCondition) { + // ... + } +} + +if (condition) { + if (anotherCondition) { + // ... + } // Comment +} + +if (condition) { + if (anotherCondition) { + // ... + } + // Comment +} + +if (condition) { // Comment + if (anotherCondition) { + // ... + } +} else { + // ... +} + +if (condition) { + // Comment + if (anotherCondition) { + // ... + } +} else { + // ... +} + +if (condition) { + if (anotherCondition) { + // ... + } // Comment +} else { + // ... +} + +if (condition) { + if (anotherCondition) { + // ... + } + // Comment +} else { + // ... +} + +if (condition) + if (anotherCondition) // Comment + doSomething(); + +// Semicolon +if (a) { + if (b) foo() +} +[].forEach(bar) diff --git a/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js.snap new file mode 100644 index 000000000000..0e89ef746207 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js.snap @@ -0,0 +1,1024 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```jsx +/** + * Safe fixes: + */ + +if (condition) { + if (anotherCondition) { + // ... + } +} + +if (condition) { + if (anotherCondition) { + // ... + } +} else { + // ... +} + +if (condition) // Comment + if (anotherCondition) + doSomething(); + +// Inner one is `JsBlockStatement` +if (condition) if (anotherCondition) { + // ... +} + +// Outer one is `JsBlockStatement` +if (condition) { + if (anotherCondition) doSomething(); +} + +// No `JsBlockStatement` +if (condition) if (anotherCondition) doSomething(); + +// `JsEmptyStatement` +if (condition) if (anotherCondition); + +// Nested +if (a) { + if (b) { + // ... + } +} else if (c) { + if (d) { + // ... + } +} + +// Need parenthesis +function* foo() { + if (a || b) + if (a ?? b) + if (a ? b : c) + if (a = b) + if (a += b) + if (a -= b) + if (a &&= b) + if (yield a) + if (a, b); +} + +// Should not add parenthesis +async function foo() { + if (a) + if (await a) + if (a.b) + if (a && b); +} + +// Don't case parenthesis in outer test +if (((a || b))) if (((c || d))); + +// Semicolon +if (a) + if (b) foo() + ;[].forEach(bar) + +if (a) { + if (b) foo() +} +;[].forEach(bar) + +/** + * Suggested fixes: + */ + +if (condition) { // Comment + if (anotherCondition) { + // ... + } +} + +if (condition) { + // Comment + if (anotherCondition) { + // ... + } +} + +if (condition) { + if (anotherCondition) { + // ... + } // Comment +} + +if (condition) { + if (anotherCondition) { + // ... + } + // Comment +} + +if (condition) { // Comment + if (anotherCondition) { + // ... + } +} else { + // ... +} + +if (condition) { + // Comment + if (anotherCondition) { + // ... + } +} else { + // ... +} + +if (condition) { + if (anotherCondition) { + // ... + } // Comment +} else { + // ... +} + +if (condition) { + if (anotherCondition) { + // ... + } + // Comment +} else { + // ... +} + +if (condition) + if (anotherCondition) // Comment + doSomething(); + +// Semicolon +if (a) { + if (b) foo() +} +[].forEach(bar) + +``` + +# Diagnostics +``` +invalid.js:5:17 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 3 │ */ + 4 │ + > 5 │ if (condition) { + │ + > 6 │ if (anotherCondition) { + > 7 │ // ... + > 8 │ } + │ ^ + 9 │ } + 10 │ + + i Safe fix: Use collapsed if instead. + + 3 3 │ */ + 4 4 │ + 5 │ - if·(condition)·{ + 6 │ - → if·(anotherCondition)·{ + 7 │ - → → //·... + 8 │ - → } + 9 │ - } + 5 │ + if·(condition&&anotherCondition)·{ + 6 │ + → → //·... + 7 │ + → } + 10 8 │ + 11 9 │ if (condition) { + + +``` + +``` +invalid.js:11:17 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 9 │ } + 10 │ + > 11 │ if (condition) { + │ + > 12 │ if (anotherCondition) { + > 13 │ // ... + > 14 │ } + │ ^ + 15 │ } else { + 16 │ // ... + + i Safe fix: Use collapsed if instead. + + 9 9 │ } + 10 10 │ + 11 │ - if·(condition)·{ + 12 │ - → if·(anotherCondition)·{ + 13 │ - → → //·... + 14 │ - → } + 15 │ - }·else·{ + 11 │ + if·(condition&&anotherCondition)·{ + 12 │ + → → //·... + 13 │ + → }·else·{ + 16 14 │ // ... + 17 15 │ } + + +``` + +``` +invalid.js:19:26 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 17 │ } + 18 │ + > 19 │ if (condition) // Comment + │ + > 20 │ if (anotherCondition) + > 21 │ doSomething(); + │ ^^^^^^^^^^^^^^ + 22 │ + 23 │ // Inner one is `JsBlockStatement` + + i Safe fix: Use collapsed if instead. + + 17 17 │ } + 18 18 │ + 19 │ - if·(condition)·//·Comment + 20 │ - → if·(anotherCondition) + 21 │ - → → doSomething(); + 19 │ + if·(condition&&anotherCondition)·//·Comment + 20 │ + → doSomething(); + 22 21 │ + 23 22 │ // Inner one is `JsBlockStatement` + + +``` + +``` +invalid.js:24:16 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 23 │ // Inner one is `JsBlockStatement` + > 24 │ if (condition) if (anotherCondition) { + │ ^^^^^^^^^^^^^^^^^^^^^^^ + > 25 │ // ... + > 26 │ } + │ ^ + 27 │ + 28 │ // Outer one is `JsBlockStatement` + + i Safe fix: Use collapsed if instead. + + 22 22 │ + 23 23 │ // Inner one is `JsBlockStatement` + 24 │ - if·(condition)·if·(anotherCondition)·{ + 24 │ + if·(condition&&anotherCondition)·{ + 25 25 │ // ... + 26 26 │ } + + +``` + +``` +invalid.js:29:17 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 28 │ // Outer one is `JsBlockStatement` + > 29 │ if (condition) { + │ + > 30 │ if (anotherCondition) doSomething(); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 31 │ } + 32 │ + + i Safe fix: Use collapsed if instead. + + 27 27 │ + 28 28 │ // Outer one is `JsBlockStatement` + 29 │ - if·(condition)·{ + 30 │ - → if·(anotherCondition)·doSomething(); + 31 │ - } + 29 │ + if·(condition&&anotherCondition)·doSomething(); + 32 30 │ + 33 31 │ // No `JsBlockStatement` + + +``` + +``` +invalid.js:34:16 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 33 │ // No `JsBlockStatement` + > 34 │ if (condition) if (anotherCondition) doSomething(); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 35 │ + 36 │ // `JsEmptyStatement` + + i Safe fix: Use collapsed if instead. + + 32 32 │ + 33 33 │ // No `JsBlockStatement` + 34 │ - if·(condition)·if·(anotherCondition)·doSomething(); + 34 │ + if·(condition&&anotherCondition)·doSomething(); + 35 35 │ + 36 36 │ // `JsEmptyStatement` + + +``` + +``` +invalid.js:37:16 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 36 │ // `JsEmptyStatement` + > 37 │ if (condition) if (anotherCondition); + │ ^^^^^^^^^^^^^^^^^^^^^^ + 38 │ + 39 │ // Nested + + i Safe fix: Use collapsed if instead. + + 35 35 │ + 36 36 │ // `JsEmptyStatement` + 37 │ - if·(condition)·if·(anotherCondition); + 37 │ + if·(condition&&anotherCondition)·; + 38 38 │ + 39 39 │ // Nested + + +``` + +``` +invalid.js:40:9 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 39 │ // Nested + > 40 │ if (a) { + │ + > 41 │ if (b) { + > 42 │ // ... + > 43 │ } + │ ^ + 44 │ } else if (c) { + 45 │ if (d) { + + i Safe fix: Use collapsed if instead. + + 38 38 │ + 39 39 │ // Nested + 40 │ - if·(a)·{ + 41 │ - → if·(b)·{ + 42 │ - → → //·... + 43 │ - → } + 44 │ - }·else·if·(c)·{ + 40 │ + if·(a&&b)·{ + 41 │ + → → //·... + 42 │ + → }·else·if·(c)·{ + 45 43 │ if (d) { + 46 44 │ // ... + + +``` + +``` +invalid.js:44:16 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 42 │ // ... + 43 │ } + > 44 │ } else if (c) { + │ + > 45 │ if (d) { + > 46 │ // ... + > 47 │ } + │ ^ + 48 │ } + 49 │ + + i Safe fix: Use collapsed if instead. + + 42 42 │ // ... + 43 43 │ } + 44 │ - }·else·if·(c)·{ + 45 │ - → if·(d)·{ + 46 │ - → → //·... + 47 │ - → } + 48 │ - } + 44 │ + }·else·if·(c&&d)·{ + 45 │ + → → //·... + 46 │ + → } + 49 47 │ + 50 48 │ // Need parenthesis + + +``` + +``` +invalid.js:52:13 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 50 │ // Need parenthesis + 51 │ function* foo() { + > 52 │ if (a || b) + │ + > 53 │ if (a ?? b) + ... + > 59 │ if (yield a) + > 60 │ if (a, b); + │ ^^^^^^^^^^ + 61 │ } + 62 │ + + i Safe fix: Use collapsed if instead. + + 50 50 │ // Need parenthesis + 51 51 │ function* foo() { + 52 │ - → if·(a·||·b) + 53 │ - → → if·(a·??·b) + 54 │ - → → → if·(a·?·b·:·c) + 52 │ + → if·((a·||·b)&&(a·??·b)) + 53 │ + → → if·(a·?·b·:·c) + 55 54 │ if (a = b) + 56 55 │ if (a += b) + + +``` + +``` +invalid.js:53:14 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 51 │ function* foo() { + 52 │ if (a || b) + > 53 │ if (a ?? b) + │ + > 54 │ if (a ? b : c) + ... + > 59 │ if (yield a) + > 60 │ if (a, b); + │ ^^^^^^^^^^ + 61 │ } + 62 │ + + i Safe fix: Use collapsed if instead. + + 51 51 │ function* foo() { + 52 52 │ if (a || b) + 53 │ - → → if·(a·??·b) + 54 │ - → → → if·(a·?·b·:·c) + 55 │ - → → → → if·(a·=·b) + 53 │ + → → if·((a·??·b)&&(a·?·b·:·c)) + 54 │ + → → → if·(a·=·b) + 56 55 │ if (a += b) + 57 56 │ if (a -= b) + + +``` + +``` +invalid.js:54:18 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 52 │ if (a || b) + 53 │ if (a ?? b) + > 54 │ if (a ? b : c) + │ + > 55 │ if (a = b) + ... + > 59 │ if (yield a) + > 60 │ if (a, b); + │ ^^^^^^^^^^ + 61 │ } + 62 │ + + i Safe fix: Use collapsed if instead. + + 52 52 │ if (a || b) + 53 53 │ if (a ?? b) + 54 │ - → → → if·(a·?·b·:·c) + 55 │ - → → → → if·(a·=·b) + 56 │ - → → → → → if·(a·+=·b) + 54 │ + → → → if·((a·?·b·:·c)&&(a·=·b)) + 55 │ + → → → → if·(a·+=·b) + 57 56 │ if (a -= b) + 58 57 │ if (a &&= b) + + +``` + +``` +invalid.js:55:15 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 53 │ if (a ?? b) + 54 │ if (a ? b : c) + > 55 │ if (a = b) + │ + > 56 │ if (a += b) + > 57 │ if (a -= b) + > 58 │ if (a &&= b) + > 59 │ if (yield a) + > 60 │ if (a, b); + │ ^^^^^^^^^^ + 61 │ } + 62 │ + + i Safe fix: Use collapsed if instead. + + 53 53 │ if (a ?? b) + 54 54 │ if (a ? b : c) + 55 │ - → → → → if·(a·=·b) + 56 │ - → → → → → if·(a·+=·b) + 57 │ - → → → → → → if·(a·-=·b) + 55 │ + → → → → if·((a·=·b)&&(a·+=·b)) + 56 │ + → → → → → if·(a·-=·b) + 58 57 │ if (a &&= b) + 59 58 │ if (yield a) + + +``` + +``` +invalid.js:56:17 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 54 │ if (a ? b : c) + 55 │ if (a = b) + > 56 │ if (a += b) + │ + > 57 │ if (a -= b) + > 58 │ if (a &&= b) + > 59 │ if (yield a) + > 60 │ if (a, b); + │ ^^^^^^^^^^ + 61 │ } + 62 │ + + i Safe fix: Use collapsed if instead. + + 54 54 │ if (a ? b : c) + 55 55 │ if (a = b) + 56 │ - → → → → → if·(a·+=·b) + 57 │ - → → → → → → if·(a·-=·b) + 58 │ - → → → → → → → if·(a·&&=·b) + 56 │ + → → → → → if·((a·+=·b)&&(a·-=·b)) + 57 │ + → → → → → → if·(a·&&=·b) + 59 58 │ if (yield a) + 60 59 │ if (a, b); + + +``` + +``` +invalid.js:57:18 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 55 │ if (a = b) + 56 │ if (a += b) + > 57 │ if (a -= b) + │ + > 58 │ if (a &&= b) + > 59 │ if (yield a) + > 60 │ if (a, b); + │ ^^^^^^^^^^ + 61 │ } + 62 │ + + i Safe fix: Use collapsed if instead. + + 55 55 │ if (a = b) + 56 56 │ if (a += b) + 57 │ - → → → → → → if·(a·-=·b) + 58 │ - → → → → → → → if·(a·&&=·b) + 59 │ - → → → → → → → → if·(yield·a) + 57 │ + → → → → → → if·((a·-=·b)&&(a·&&=·b)) + 58 │ + → → → → → → → if·(yield·a) + 60 59 │ if (a, b); + 61 60 │ } + + +``` + +``` +invalid.js:58:20 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 56 │ if (a += b) + 57 │ if (a -= b) + > 58 │ if (a &&= b) + │ + > 59 │ if (yield a) + > 60 │ if (a, b); + │ ^^^^^^^^^^ + 61 │ } + 62 │ + + i Safe fix: Use collapsed if instead. + + 56 56 │ if (a += b) + 57 57 │ if (a -= b) + 58 │ - → → → → → → → if·(a·&&=·b) + 59 │ - → → → → → → → → if·(yield·a) + 60 │ - → → → → → → → → → if·(a,·b); + 58 │ + → → → → → → → if·((a·&&=·b)&&(yield·a)) + 59 │ + → → → → → → → → if·(a,·b); + 61 60 │ } + 62 61 │ + + +``` + +``` +invalid.js:59:21 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 57 │ if (a -= b) + 58 │ if (a &&= b) + > 59 │ if (yield a) + │ + > 60 │ if (a, b); + │ ^^^^^^^^^^ + 61 │ } + 62 │ + + i Safe fix: Use collapsed if instead. + + 57 57 │ if (a -= b) + 58 58 │ if (a &&= b) + 59 │ - → → → → → → → → if·(yield·a) + 60 │ - → → → → → → → → → if·(a,·b); + 59 │ + → → → → → → → → if·((yield·a)&&(a,·b)) + 60 │ + → → → → → → → → → ; + 61 61 │ } + 62 62 │ + + +``` + +``` +invalid.js:65:8 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 63 │ // Should not add parenthesis + 64 │ async function foo() { + > 65 │ if (a) + │ + > 66 │ if (await a) + > 67 │ if (a.b) + > 68 │ if (a && b); + │ ^^^^^^^^^^^^ + 69 │ } + 70 │ + + i Safe fix: Use collapsed if instead. + + 63 63 │ // Should not add parenthesis + 64 64 │ async function foo() { + 65 │ - → if·(a) + 66 │ - → → if·(await·a) + 67 │ - → → → if·(a.b) + 65 │ + → if·(a&&(await·a)) + 66 │ + → → if·(a.b) + 68 67 │ if (a && b); + 69 68 │ } + + +``` + +``` +invalid.js:66:15 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 64 │ async function foo() { + 65 │ if (a) + > 66 │ if (await a) + │ + > 67 │ if (a.b) + > 68 │ if (a && b); + │ ^^^^^^^^^^^^ + 69 │ } + 70 │ + + i Safe fix: Use collapsed if instead. + + 64 64 │ async function foo() { + 65 65 │ if (a) + 66 │ - → → if·(await·a) + 67 │ - → → → if·(a.b) + 68 │ - → → → → if·(a·&&·b); + 66 │ + → → if·((await·a)&&a.b) + 67 │ + → → → if·(a·&&·b); + 69 68 │ } + 70 69 │ + + +``` + +``` +invalid.js:67:12 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 65 │ if (a) + 66 │ if (await a) + > 67 │ if (a.b) + │ + > 68 │ if (a && b); + │ ^^^^^^^^^^^^ + 69 │ } + 70 │ + + i Safe fix: Use collapsed if instead. + + 65 65 │ if (a) + 66 66 │ if (await a) + 67 │ - → → → if·(a.b) + 68 │ - → → → → if·(a·&&·b); + 67 │ + → → → if·(a.b&&a·&&·b) + 68 │ + → → → → ; + 69 69 │ } + 70 70 │ + + +``` + +``` +invalid.js:72:17 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 71 │ // Don't case parenthesis in outer test + > 72 │ if (((a || b))) if (((c || d))); + │ ^^^^^^^^^^^^^^^^ + 73 │ + 74 │ // Semicolon + + i Safe fix: Use collapsed if instead. + + 70 70 │ + 71 71 │ // Don't case parenthesis in outer test + 72 │ - if·(((a·||·b)))·if·(((c·||·d))); + 72 │ + if·(((a·||·b))&&((c·||·d)))·; + 73 73 │ + 74 74 │ // Semicolon + + +``` + +``` +invalid.js:75:7 lint/nursery/useCollapsedIf FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 74 │ // Semicolon + > 75 │ if (a) + │ + > 76 │ if (b) foo() + > 77 │ ;[].forEach(bar) + │ ^ + 78 │ + 79 │ if (a) { + + i Safe fix: Use collapsed if instead. + + 73 73 │ + 74 74 │ // Semicolon + 75 │ - if·(a) + 76 │ - → if·(b)·foo() + 75 │ + if·(a&&b) + 76 │ + → foo() + 77 77 │ ;[].forEach(bar) + 78 78 │ + + +``` + +``` +invalid.js:79:9 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 77 │ ;[].forEach(bar) + 78 │ + > 79 │ if (a) { + │ + > 80 │ if (b) foo() + │ ^^^^^^^^^^^^ + 81 │ } + 82 │ ;[].forEach(bar) + + +``` + +``` +invalid.js:88:28 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 86 │ */ + 87 │ + > 88 │ if (condition) { // Comment + │ + > 89 │ if (anotherCondition) { + > 90 │ // ... + > 91 │ } + │ ^ + 92 │ } + 93 │ + + +``` + +``` +invalid.js:94:17 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 92 │ } + 93 │ + > 94 │ if (condition) { + │ + > 95 │ // Comment + > 96 │ if (anotherCondition) { + > 97 │ // ... + > 98 │ } + │ ^ + 99 │ } + 100 │ + + +``` + +``` +invalid.js:101:17 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 99 │ } + 100 │ + > 101 │ if (condition) { + │ + > 102 │ if (anotherCondition) { + > 103 │ // ... + > 104 │ } // Comment + │ ^^^^^^^^^^^^ + 105 │ } + 106 │ + + +``` + +``` +invalid.js:107:17 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 105 │ } + 106 │ + > 107 │ if (condition) { + │ + > 108 │ if (anotherCondition) { + > 109 │ // ... + > 110 │ } + │ ^ + 111 │ // Comment + 112 │ } + + +``` + +``` +invalid.js:114:28 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 112 │ } + 113 │ + > 114 │ if (condition) { // Comment + │ + > 115 │ if (anotherCondition) { + > 116 │ // ... + > 117 │ } + │ ^ + 118 │ } else { + 119 │ // ... + + +``` + +``` +invalid.js:122:17 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 120 │ } + 121 │ + > 122 │ if (condition) { + │ + > 123 │ // Comment + > 124 │ if (anotherCondition) { + > 125 │ // ... + > 126 │ } + │ ^ + 127 │ } else { + 128 │ // ... + + +``` + +``` +invalid.js:131:17 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 129 │ } + 130 │ + > 131 │ if (condition) { + │ + > 132 │ if (anotherCondition) { + > 133 │ // ... + > 134 │ } // Comment + │ ^^^^^^^^^^^^ + 135 │ } else { + 136 │ // ... + + +``` + +``` +invalid.js:139:17 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 137 │ } + 138 │ + > 139 │ if (condition) { + │ + > 140 │ if (anotherCondition) { + > 141 │ // ... + > 142 │ } + │ ^ + 143 │ // Comment + 144 │ } else { + + +``` + +``` +invalid.js:148:15 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 146 │ } + 147 │ + > 148 │ if (condition) + │ + > 149 │ if (anotherCondition) // Comment + > 150 │ doSomething(); + │ ^^^^^^^^^^^^^^ + 151 │ + 152 │ // Semicolon + + +``` + +``` +invalid.js:153:9 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This if statement can be collapsed into another if statement. + + 152 │ // Semicolon + > 153 │ if (a) { + │ + > 154 │ if (b) foo() + │ ^^^^^^^^^^^^ + 155 │ } + 156 │ [].forEach(bar) + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js new file mode 100644 index 000000000000..f1fc08fc0488 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js @@ -0,0 +1,40 @@ +if (condition && anotherCondition) { + // ... +} + +if (condition) { + if (anotherCondition) { + // ... + } + doSomething(); +} + +if (condition) { + if (anotherCondition) { + // ... + } else { + // ... + } +} + +if (condition) { + if (anotherCondition) { + // ... + } + doSomething(); +} else { + // ... +} + +if (condition) { + anotherCondition ? c() : d() +} + +// Covered by `useCollapsedElseIf` +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js.snap new file mode 100644 index 000000000000..83648186b0f1 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js.snap @@ -0,0 +1,48 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```jsx +if (condition && anotherCondition) { + // ... +} + +if (condition) { + if (anotherCondition) { + // ... + } + doSomething(); +} + +if (condition) { + if (anotherCondition) { + // ... + } else { + // ... + } +} + +if (condition) { + if (anotherCondition) { + // ... + } + doSomething(); +} else { + // ... +} + +if (condition) { + anotherCondition ? c() : d() +} + +// Covered by `useCollapsedElseIf` +if (condition) { + // ... +} else { + if (anotherCondition) { + // ... + } +} + +``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index a38557656a37..a5030b84d2d0 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1358,6 +1358,10 @@ export interface Nursery { * Use at() instead of integer index access. */ useAtIndex?: RuleFixConfiguration_for_Null; + /** + * Enforce using single if instead of nested if clauses. + */ + useCollapsedIf?: RuleFixConfiguration_for_Null; /** * Enforce declaring components only within modules that export React Components exclusively. */ @@ -2956,6 +2960,7 @@ export type Category = | "lint/nursery/useAriaPropsSupportedByRole" | "lint/nursery/useAtIndex" | "lint/nursery/useBiomeSuppressionComment" + | "lint/nursery/useCollapsedIf" | "lint/nursery/useComponentExportOnlyModules" | "lint/nursery/useConsistentCurlyBraces" | "lint/nursery/useConsistentMemberAccessibility" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 82f908c7d177..419e88edbaa9 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2327,6 +2327,13 @@ { "type": "null" } ] }, + "useCollapsedIf": { + "description": "Enforce using single if instead of nested if clauses.", + "anyOf": [ + { "$ref": "#/definitions/RuleFixConfiguration" }, + { "type": "null" } + ] + }, "useComponentExportOnlyModules": { "description": "Enforce declaring components only within modules that export React Components exclusively.", "anyOf": [