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 a53c03ac2953..bdf8fc2808df 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 @@ -1578,6 +1578,16 @@ pub(crate) fn migrate_eslint_any_rule( let rule = group.no_then_property.get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "unicorn/no-useless-length-check" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .no_useless_length_check + .get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "unicorn/no-useless-switch-case" => { let group = rules.complexity.get_or_insert_with(Default::default); let rule = group diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index d8dd18c0be50..1b5c0597f8d7 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3377,6 +3377,10 @@ pub struct Nursery { #[serde(skip_serializing_if = "Option::is_none")] pub no_useless_escape_in_regex: Option>, + #[doc = "Disallow unnecessary length checks within logical expressions."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_useless_length_check: + Option>, #[doc = "Disallow unnecessary String.raw function in template string literals without any escape sequence."] #[serde(skip_serializing_if = "Option::is_none")] pub no_useless_string_raw: @@ -3494,6 +3498,7 @@ impl Nursery { "noUnknownPseudoElement", "noUnknownTypeSelector", "noUselessEscapeInRegex", + "noUselessLengthCheck", "noUselessStringRaw", "noUselessUndefined", "noValueAtRule", @@ -3541,10 +3546,10 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]), 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[33]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3595,6 +3600,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3756,101 +3762,106 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.no_useless_string_raw.as_ref() { + if let Some(rule) = self.no_useless_length_check.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.no_useless_undefined.as_ref() { + if let Some(rule) = self.no_useless_string_raw.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.no_value_at_rule.as_ref() { + if let Some(rule) = self.no_useless_undefined.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { + if let Some(rule) = self.no_value_at_rule.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() { + if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_at_index.as_ref() { + if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_collapsed_if.as_ref() { + if let Some(rule) = self.use_at_index.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - 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[36])); } } - 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[37])); } } - 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[38])); } } - 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[39])); } } - 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[40])); } } - if let Some(rule) = self.use_google_font_display.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[41])); } } - if let Some(rule) = self.use_guard_for_in.as_ref() { + if let Some(rule) = self.use_google_font_display.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } - 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[43])); } } - 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[44])); } } - 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[45])); } } - 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[46])); } } - 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[47])); } } + 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[48])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -4000,101 +4011,106 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.no_useless_string_raw.as_ref() { + if let Some(rule) = self.no_useless_length_check.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.no_useless_undefined.as_ref() { + if let Some(rule) = self.no_useless_string_raw.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.no_value_at_rule.as_ref() { + if let Some(rule) = self.no_useless_undefined.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { + if let Some(rule) = self.no_value_at_rule.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() { + if let Some(rule) = self.use_adjacent_overload_signatures.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_at_index.as_ref() { + if let Some(rule) = self.use_aria_props_supported_by_role.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_collapsed_if.as_ref() { + if let Some(rule) = self.use_at_index.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - 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[36])); } } - 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[37])); } } - 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[38])); } } - 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[39])); } } - 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[40])); } } - if let Some(rule) = self.use_google_font_display.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[41])); } } - if let Some(rule) = self.use_guard_for_in.as_ref() { + if let Some(rule) = self.use_google_font_display.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } - 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[43])); } } - 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[44])); } } - 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[45])); } } - 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[46])); } } - 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[47])); } } + 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[48])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -4247,6 +4263,10 @@ impl Nursery { .no_useless_escape_in_regex .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noUselessLengthCheck" => self + .no_useless_length_check + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noUselessStringRaw" => self .no_useless_string_raw .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index c5b9fa49e2d5..2204e0ab7d2f 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -182,6 +182,7 @@ define_categories! { "lint/nursery/noUnmatchableAnbSelector": "https://biomejs.dev/linter/rules/no-unmatchable-anb-selector", "lint/nursery/noUnusedFunctionParameters": "https://biomejs.dev/linter/rules/no-unused-function-parameters", "lint/nursery/noUselessEscapeInRegex": "https://biomejs.dev/linter/rules/no-useless-escape-in-regex", + "lint/nursery/noUselessLengthCheck": "https://biomejs.dev/linter/rules/no-useless-length-check", "lint/nursery/noUselessStringRaw": "https://biomejs.dev/linter/rules/no-useless-string-raw", "lint/nursery/noUselessUndefined": "https://biomejs.dev/linter/rules/no-useless-undefined", "lint/nursery/noValueAtRule": "https://biomejs.dev/linter/rules/no-value-at-rule", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 61c1d46a5b78..c4e183ba098f 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -23,6 +23,7 @@ pub mod no_static_element_interactions; pub mod no_substr; pub mod no_template_curly_in_string; pub mod no_useless_escape_in_regex; +pub mod no_useless_length_check; pub mod no_useless_string_raw; pub mod no_useless_undefined; pub mod use_adjacent_overload_signatures; @@ -66,6 +67,7 @@ declare_lint_group! { self :: no_substr :: NoSubstr , self :: no_template_curly_in_string :: NoTemplateCurlyInString , self :: no_useless_escape_in_regex :: NoUselessEscapeInRegex , + self :: no_useless_length_check :: NoUselessLengthCheck , self :: no_useless_string_raw :: NoUselessStringRaw , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , diff --git a/crates/biome_js_analyze/src/lint/nursery/no_useless_length_check.rs b/crates/biome_js_analyze/src/lint/nursery/no_useless_length_check.rs new file mode 100644 index 000000000000..fbca629f8fd5 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_useless_length_check.rs @@ -0,0 +1,391 @@ +use std::collections::{HashMap, HashSet}; + +use biome_analyze::{ + context::RuleContext, declare_lint_rule, ActionCategory, Ast, FixKind, Rule, RuleDiagnostic, + RuleSource, +}; +use biome_console::markup; +use biome_js_syntax::{ + AnyJsExpression, AnyJsMemberExpression, JsBinaryExpression, JsBinaryOperator, + JsLogicalExpression, JsParenthesizedExpression, JsSyntaxKind, T, +}; +use biome_rowan::{AstNode, BatchMutationExt, TextRange}; + +use crate::JsRuleAction; + +declare_lint_rule! { + /// Disallow unnecessary length checks within logical expressions. + /// + /// When using the function [`Array#some()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some), it returns `false` when an array is empty; hence, there isn't need to check if the array is not empty. + /// + /// When using the function [`Array#every()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every), it returns `true` when an array is empty; hence, there isn't need to check if the array is empty. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// if (array.length === 0 || array.every(Boolean)); + /// ``` + /// + /// ```js,expect_diagnostic + /// if (array.length !== 0 && array.some(Boolean)); + /// ``` + /// + /// ```js,expect_diagnostic + /// if (array.length > 0 && array.some(Boolean)); + /// ``` + /// + /// ```js,expect_diagnostic + /// const isAllTrulyOrEmpty = array.length === 0 || array.every(Boolean); + /// ``` + /// + /// ### Valid + /// + /// ```js + /// if (array.every(Boolean)); + /// ``` + /// + /// ```js + /// if (array.some(Boolean)); + /// ``` + /// + /// ```js + /// const isAllTrulyOrEmpty = array.every(Boolean); + /// ``` + /// + /// ```js + /// const isAllTrulyOrEmpty = array.every(Boolean); + /// ``` + /// + /// ```js + /// if (array.length === 0 || anotherCheck() || array.every(Boolean)); + /// ``` + /// + /// ```js + /// const isNonEmptyAllTrulyArray = array.length > 0 && array.every(Boolean); + /// ``` + /// + /// ```js + /// const isEmptyArrayOrAllTruly = array.length === 0 || array.some(Boolean); + /// ``` + /// + pub NoUselessLengthCheck { + version: "next", + name: "noUselessLengthCheck", + language: "js", + recommended: false, + sources: &[RuleSource::EslintUnicorn("no-useless-length-check")], + fix_kind: FixKind::Unsafe, + } +} + +#[derive(Clone, Debug)] +pub enum FunctionKind { + /// `Array.some()` was used + Some, + /// `Array.every()` was used + Every, +} + +/// Whether the node is a descendant of a logical expression. +fn is_logical_exp_descendant(node: &AnyJsExpression, operator: JsSyntaxKind) -> bool { + let Some(parent) = node.syntax().parent() else { + return false; + }; + parent + .ancestors() + .find_map(|ancestor| { + if let Some(logical_exp) = JsLogicalExpression::cast_ref(&ancestor) { + return logical_exp + .operator_token() + .ok() + .map(|token| token.kind() == operator) + .or(Some(false)); + } + (!JsParenthesizedExpression::can_cast(ancestor.kind())).then_some(false) + }) + .unwrap_or(false) +} + +/// Extract the expressions that perform length comparisons corresponding to the errors you want to check. +/// # Examples +/// ## `foo.every()` +/// `foo.length === 0` -> `Some(foo)` +/// `foo.length !== 0` -> `None` +/// ## `foo.some()` +/// `foo.length !== 0` -> `Some(foo)` +/// `foo.length >= 1` -> `Some(foo)` +/// `foo.length === 0` -> `None` +fn get_comparing_length_exp( + binary_exp: &JsBinaryExpression, + function_kind: &FunctionKind, +) -> Option { + let operator = binary_exp.operator().ok()?; + // Check only when the number appears on the right side according to the original rules. + // We assume that you have already complied with useExplicitLengthCheck + let compare_exp = binary_exp.left().ok()?; + let value_exp = binary_exp.right().ok()?; + + let member_exp = compare_exp.as_js_static_member_expression()?; + let target = member_exp.object().ok()?; + let member = member_exp.member().ok()?; + if member.syntax().text_trimmed() != "length" || member_exp.is_optional_chain() { + return None; + } + let literal = value_exp.as_any_js_literal_expression()?; + let literal = literal.as_js_number_literal_expression()?; + match function_kind { + FunctionKind::Every => { + // .length === 0 + (literal.syntax().text_trimmed() == "0" + && (operator == JsBinaryOperator::StrictEquality + || operator == JsBinaryOperator::LessThan)) + .then_some(target) + } + FunctionKind::Some => { + // .length !== 0 + (literal.syntax().text_trimmed() == "0" + && (operator == JsBinaryOperator::StrictInequality + || operator == JsBinaryOperator::GreaterThan) + || literal.syntax().text_trimmed() == "1" + && operator == JsBinaryOperator::GreaterThanOrEqual) + .then_some(target) + } + } +} + +#[derive(Clone)] +/// A struct that manages the form before and after replacement. +pub struct FixablePoint { + /// The node before the replacement. + prev_node: JsLogicalExpression, + /// The node after the replacement. + next_node: AnyJsExpression, + /// Error occurrence location. + range: TextRange, +} + +/// Search for logical expressions and list expressions that compare to 0 and Array APIs (`.some()`, `.every()`). +/// `any_exp` is the expression to be searched. +/// `fixable_point` is the form before and after replacement. +/// `function_kind` is the kind of function used in the logical expression. +/// `comparing_zeros` is a HashMap that holds the names of arrays compared with zero and their corresponding expressions. +/// `array_tokens_used_api` is a HashSet that holds the names of arrays using `some` or `every` corresponding to `function_kind`. +fn search_logical_exp( + any_exp: &AnyJsExpression, + fixable_point: Option, + function_kind: &FunctionKind, + comparing_zeros: &mut HashMap>, + array_tokens_used_api: &mut HashSet, +) -> Option<()> { + match any_exp { + // || or && + AnyJsExpression::JsLogicalExpression(logical_exp) => { + let operator = match function_kind { + FunctionKind::Every => T![||], + FunctionKind::Some => T![&&], + }; + if logical_exp.operator_token().ok()?.kind() != operator { + return None; + }; + let left = logical_exp.left().ok()?; + let left_fixable_point = FixablePoint { + prev_node: logical_exp.clone(), + next_node: logical_exp.right().ok()?, + range: left.range(), + }; + search_logical_exp( + &left, + Some(left_fixable_point), + function_kind, + comparing_zeros, + array_tokens_used_api, + )?; + + let right = logical_exp.right().ok()?; + let right_fixable_point = FixablePoint { + prev_node: logical_exp.clone(), + next_node: logical_exp.left().ok()?, + range: right.range(), + }; + search_logical_exp( + &right, + Some(right_fixable_point), + function_kind, + comparing_zeros, + array_tokens_used_api, + ) + } + // a === 0 ext. + AnyJsExpression::JsBinaryExpression(binary_exp) => { + let comparing_zero = get_comparing_length_exp(binary_exp, function_kind)?; + let AnyJsExpression::JsIdentifierExpression(array_token) = comparing_zero else { + return None; + }; + let key = array_token.text(); + if let Some(comparing_zero_list) = comparing_zeros.get_mut(&key) { + comparing_zero_list.push(fixable_point?); + } else { + comparing_zeros.insert(key, vec![fixable_point?]); + } + Some(()) + } + // .some() or .every() etc. + AnyJsExpression::JsCallExpression(task_exp) => { + if task_exp.is_optional_chain() { + return None; + } + let task_member_exp = + AnyJsMemberExpression::cast(task_exp.callee().ok()?.into_syntax())?; + let task_target = task_member_exp.object().ok()?; + let task_target_token = task_target.as_js_identifier_expression()?; + + let task_member_name_node = task_member_exp.member_name()?; + let task_member_name = task_member_name_node.text(); + match function_kind { + FunctionKind::Every => { + if task_member_name == "every" { + array_tokens_used_api.insert(task_target_token.text()); + Some(()) + } else { + None + } + } + FunctionKind::Some => { + if task_member_name == "some" { + array_tokens_used_api.insert(task_target_token.text()); + Some(()) + } else { + None + } + } + } + } + // ( foo ) + AnyJsExpression::JsParenthesizedExpression(parent_exp) => search_logical_exp( + &parent_exp.expression().ok()?, + fixable_point, + function_kind, + comparing_zeros, + array_tokens_used_api, + ), + AnyJsExpression::JsIdentifierExpression(_) => Some(()), + _ => None, + } +} + +fn get_parenthesized_parent(exp: AnyJsExpression) -> AnyJsExpression { + let Some(parent) = exp.syntax().parent() else { + return exp; + }; + let Some(parent) = JsParenthesizedExpression::cast(parent) else { + return exp; + }; + get_parenthesized_parent(AnyJsExpression::from(parent)) +} + +pub struct NoUselessLengthCheckState { + /// The kind of function used in the logical expression. + function_kind: FunctionKind, + /// The form before and after replacement. + fixable_point: FixablePoint, +} + +impl Rule for NoUselessLengthCheck { + type Query = Ast; + type State = NoUselessLengthCheckState; + type Signals = Vec; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + + let mut fixable_list = Vec::new(); + + let Some(operator) = node.operator_token().ok() else { + return fixable_list; + }; + // node must not be a child of a logical expression + if is_logical_exp_descendant(&AnyJsExpression::from(node.clone()), operator.kind()) { + return fixable_list; + } + + for function_kind in [FunctionKind::Every, FunctionKind::Some] { + let mut comparing_zeros = HashMap::new(); + let mut array_tokens_used_api = HashSet::new(); + let search_result = search_logical_exp( + &AnyJsExpression::from(node.clone()), + None, + &function_kind, + &mut comparing_zeros, + &mut array_tokens_used_api, + ); + if search_result.is_some() { + for array_token in array_tokens_used_api { + if let Some(fixable_points) = comparing_zeros.get(&array_token) { + for fixable_point in fixable_points { + fixable_list.push(NoUselessLengthCheckState { + function_kind: function_kind.clone(), + fixable_point: fixable_point.clone(), + }); + } + } + } + } + } + fixable_list + } + + fn diagnostic( + _ctx: &RuleContext, + NoUselessLengthCheckState { + function_kind, + fixable_point, + }: &Self::State, + ) -> Option { + Some( + RuleDiagnostic::new( + rule_category!(), + fixable_point.range, + markup! { + "This length check is unnecessary." + }, + ) + .note( + match function_kind { + FunctionKind::Every => markup! { + "The empty check is useless as ""`Array#every()`"" returns ""`true`"" for an empty array." + }, + FunctionKind::Some => markup! { + "The non-empty check is useless as ""`Array#some()`"" returns ""`false`"" for an empty array." + }, + } + ), + ) + } + + fn action( + ctx: &RuleContext, + NoUselessLengthCheckState { fixable_point, .. }: &Self::State, + ) -> Option { + let mut mutation = ctx.root().begin(); + let FixablePoint { + prev_node, + next_node, + .. + } = fixable_point; + + mutation.replace_node( + get_parenthesized_parent(AnyJsExpression::from(prev_node.clone())), + next_node.clone().omit_parentheses(), + ); + + Some(JsRuleAction::new( + ActionCategory::QuickFix, + ctx.metadata().applicability(), + markup! { "Remove the length check" }.to_owned(), + mutation, + )) + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index 39938886cd81..8c63d2fb4f30 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -254,6 +254,8 @@ pub type NoUselessFragments = ::Options; pub type NoUselessLabel = ::Options; +pub type NoUselessLengthCheck = + ::Options; pub type NoUselessLoneBlockStatements = < lint :: complexity :: no_useless_lone_block_statements :: NoUselessLoneBlockStatements as biome_analyze :: Rule > :: Options ; pub type NoUselessRename = ::Options; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessLengthCheck/invalid.jsonc b/crates/biome_js_analyze/tests/specs/nursery/noUselessLengthCheck/invalid.jsonc new file mode 100644 index 000000000000..2a8b89247646 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessLengthCheck/invalid.jsonc @@ -0,0 +1,30 @@ +[ + "if (array.length === 0 || array.every(Boolean));", + "if (array.length !== 0 && array.some(Boolean));", + "if (array.length > 0 && array.some(Boolean));", + "if (((((array.length === 0 || array.every(Boolean))))));", + "const isAllTrulyOrEmpty = array.length === 0 || array.every(Boolean);", + "array.length === 0 || array.every(Boolean)", + "array.length > 0 && array.some(Boolean)", + "array.length !== 0 && array.some(Boolean)", + "array.length >= 1 && array.some(Boolean)", + "if ((( array.length > 0 )) && array.some(Boolean));", + "(array.length === 0 || array.every(Boolean)) || foo", + "foo || (array.length === 0 || array.every(Boolean))", + "(array.length > 0 && array.some(Boolean)) && foo", + "foo && (array.length > 0 && array.some(Boolean))", + "array.every(Boolean) || array.length === 0", + "array.some(Boolean) && array.length !== 0", + "array.some(Boolean) && array.length > 0", + "foo && array.length > 0 && array.some(Boolean)", + "foo || array.length === 0 || array.every(Boolean)", + "(foo || array.length === 0) || array.every(Boolean)", + "array.length === 0 || (array.every(Boolean) || foo)", + "array.length === 0 || (((array.every(Boolean) || foo)))", + "(foo && array.length > 0) && array.some(Boolean)", + "array.length > 0 && (array.some(Boolean) && foo)", + "array.every(Boolean) || array.length === 0 || array.every(Boolean)", + "array.length === 0 || array.every(Boolean) || array.length === 0", + "(array1.length === 0 || array1.every(Boolean)) || (array2.length === 0 || array2.every(Boolean))", + "(array1.length === 0 || array1.every(Boolean)) && (array2.length === 0 || array2.every(Boolean))" +] \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessLengthCheck/invalid.jsonc.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessLengthCheck/invalid.jsonc.snap new file mode 100644 index 000000000000..89096c535530 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessLengthCheck/invalid.jsonc.snap @@ -0,0 +1,699 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: invalid.jsonc +--- +# Input +```cjs +if (array.length === 0 || array.every(Boolean)); +``` + +# Diagnostics +``` +invalid.jsonc:1:5 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ if (array.length === 0 || array.every(Boolean)); + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ if·(array.length·===·0·||·array.every(Boolean)); + │ ---------------------- + +``` + +# Input +```cjs +if (array.length !== 0 && array.some(Boolean)); +``` + +# Diagnostics +``` +invalid.jsonc:1:5 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ if (array.length !== 0 && array.some(Boolean)); + │ ^^^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ if·(array.length·!==·0·&&·array.some(Boolean)); + │ ---------------------- + +``` + +# Input +```cjs +if (array.length > 0 && array.some(Boolean)); +``` + +# Diagnostics +``` +invalid.jsonc:1:5 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ if (array.length > 0 && array.some(Boolean)); + │ ^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ if·(array.length·>·0·&&·array.some(Boolean)); + │ -------------------- + +``` + +# Input +```cjs +if (((((array.length === 0 || array.every(Boolean)))))); +``` + +# Diagnostics +``` +invalid.jsonc:1:9 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ if (((((array.length === 0 || array.every(Boolean)))))); + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ if·(((((array.length·===·0·||·array.every(Boolean)))))); + │ -------------------------- ---- + +``` + +# Input +```cjs +const isAllTrulyOrEmpty = array.length === 0 || array.every(Boolean); +``` + +# Diagnostics +``` +invalid.jsonc:1:27 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ const isAllTrulyOrEmpty = array.length === 0 || array.every(Boolean); + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ const·isAllTrulyOrEmpty·=·array.length·===·0·||·array.every(Boolean); + │ ---------------------- + +``` + +# Input +```cjs +array.length === 0 || array.every(Boolean) +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.length === 0 || array.every(Boolean) + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.length·===·0·||·array.every(Boolean) + │ ---------------------- + +``` + +# Input +```cjs +array.length > 0 && array.some(Boolean) +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.length > 0 && array.some(Boolean) + │ ^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.length·>·0·&&·array.some(Boolean) + │ -------------------- + +``` + +# Input +```cjs +array.length !== 0 && array.some(Boolean) +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.length !== 0 && array.some(Boolean) + │ ^^^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.length·!==·0·&&·array.some(Boolean) + │ ---------------------- + +``` + +# Input +```cjs +array.length >= 1 && array.some(Boolean) +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.length >= 1 && array.some(Boolean) + │ ^^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.length·>=·1·&&·array.some(Boolean) + │ --------------------- + +``` + +# Input +```cjs +if ((( array.length > 0 )) && array.some(Boolean)); +``` + +# Diagnostics +``` +invalid.jsonc:1:5 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ if ((( array.length > 0 )) && array.some(Boolean)); + │ ^^^^^^^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ if·(((·array.length·>·0·))·&&·array.some(Boolean)); + │ -------------------------- + +``` + +# Input +```cjs +(array.length === 0 || array.every(Boolean)) || foo +``` + +# Diagnostics +``` +invalid.jsonc:1:2 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ (array.length === 0 || array.every(Boolean)) || foo + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ (array.length·===·0·||·array.every(Boolean))·||·foo + │ ----------------------- - + +``` + +# Input +```cjs +foo || (array.length === 0 || array.every(Boolean)) +``` + +# Diagnostics +``` +invalid.jsonc:1:9 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ foo || (array.length === 0 || array.every(Boolean)) + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ foo·||·(array.length·===·0·||·array.every(Boolean)) + │ ----------------------- - + +``` + +# Input +```cjs +(array.length > 0 && array.some(Boolean)) && foo +``` + +# Diagnostics +``` +invalid.jsonc:1:2 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ (array.length > 0 && array.some(Boolean)) && foo + │ ^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ (array.length·>·0·&&·array.some(Boolean))·&&·foo + │ --------------------- - + +``` + +# Input +```cjs +foo && (array.length > 0 && array.some(Boolean)) +``` + +# Diagnostics +``` +invalid.jsonc:1:9 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ foo && (array.length > 0 && array.some(Boolean)) + │ ^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ foo·&&·(array.length·>·0·&&·array.some(Boolean)) + │ --------------------- - + +``` + +# Input +```cjs +array.every(Boolean) || array.length === 0 +``` + +# Diagnostics +``` +invalid.jsonc:1:25 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.every(Boolean) || array.length === 0 + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.every(Boolean)·||·array.length·===·0 + │ ---------------------- + +``` + +# Input +```cjs +array.some(Boolean) && array.length !== 0 +``` + +# Diagnostics +``` +invalid.jsonc:1:24 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.some(Boolean) && array.length !== 0 + │ ^^^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.some(Boolean)·&&·array.length·!==·0 + │ ---------------------- + +``` + +# Input +```cjs +array.some(Boolean) && array.length > 0 +``` + +# Diagnostics +``` +invalid.jsonc:1:24 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.some(Boolean) && array.length > 0 + │ ^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.some(Boolean)·&&·array.length·>·0 + │ -------------------- + +``` + +# Input +```cjs +foo && array.length > 0 && array.some(Boolean) +``` + +# Diagnostics +``` +invalid.jsonc:1:8 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ foo && array.length > 0 && array.some(Boolean) + │ ^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ foo·&&·array.length·>·0·&&·array.some(Boolean) + │ -------------------- + +``` + +# Input +```cjs +foo || array.length === 0 || array.every(Boolean) +``` + +# Diagnostics +``` +invalid.jsonc:1:8 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ foo || array.length === 0 || array.every(Boolean) + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ foo·||·array.length·===·0·||·array.every(Boolean) + │ ---------------------- + +``` + +# Input +```cjs +(foo || array.length === 0) || array.every(Boolean) +``` + +# Diagnostics +``` +invalid.jsonc:1:9 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ (foo || array.length === 0) || array.every(Boolean) + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ (foo·||·array.length·===·0)·||·array.every(Boolean) + │ - ----------------------- + +``` + +# Input +```cjs +array.length === 0 || (array.every(Boolean) || foo) +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.length === 0 || (array.every(Boolean) || foo) + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.length·===·0·||·(array.every(Boolean)·||·foo) + │ ----------------------- - + +``` + +# Input +```cjs +array.length === 0 || (((array.every(Boolean) || foo))) +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.length === 0 || (((array.every(Boolean) || foo))) + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.length·===·0·||·(((array.every(Boolean)·||·foo))) + │ ------------------------- --- + +``` + +# Input +```cjs +(foo && array.length > 0) && array.some(Boolean) +``` + +# Diagnostics +``` +invalid.jsonc:1:9 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ (foo && array.length > 0) && array.some(Boolean) + │ ^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ (foo·&&·array.length·>·0)·&&·array.some(Boolean) + │ - --------------------- + +``` + +# Input +```cjs +array.length > 0 && (array.some(Boolean) && foo) +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.length > 0 && (array.some(Boolean) && foo) + │ ^^^^^^^^^^^^^^^^ + + i The non-empty check is useless as `Array#some()` returns `false` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.length·>·0·&&·(array.some(Boolean)·&&·foo) + │ --------------------- - + +``` + +# Input +```cjs +array.every(Boolean) || array.length === 0 || array.every(Boolean) +``` + +# Diagnostics +``` +invalid.jsonc:1:25 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.every(Boolean) || array.length === 0 || array.every(Boolean) + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.every(Boolean)·||·array.length·===·0·||·array.every(Boolean) + │ ---------------------- + +``` + +# Input +```cjs +array.length === 0 || array.every(Boolean) || array.length === 0 +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.length === 0 || array.every(Boolean) || array.length === 0 + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.length·===·0·||·array.every(Boolean)·||·array.length·===·0 + │ ---------------------- + +``` + +``` +invalid.jsonc:1:47 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ array.length === 0 || array.every(Boolean) || array.length === 0 + │ ^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ array.length·===·0·||·array.every(Boolean)·||·array.length·===·0 + │ ---------------------- + +``` + +# Input +```cjs +(array1.length === 0 || array1.every(Boolean)) || (array2.length === 0 || array2.every(Boolean)) +``` + +# Diagnostics +``` +invalid.jsonc:1:2 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ (array1.length === 0 || array1.every(Boolean)) || (array2.length === 0 || array2.every(Boolean)) + │ ^^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ (array1.length·===·0·||·array1.every(Boolean))·||·(array2.length·===·0·||·array2.every(Boolean)) + │ - ----------------------- - + +``` + +``` +invalid.jsonc:1:52 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ (array1.length === 0 || array1.every(Boolean)) || (array2.length === 0 || array2.every(Boolean)) + │ ^^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ (array1.length·===·0·||·array1.every(Boolean))·||·(array2.length·===·0·||·array2.every(Boolean)) + │ - ----------------------- - + +``` + +# Input +```cjs +(array1.length === 0 || array1.every(Boolean)) && (array2.length === 0 || array2.every(Boolean)) +``` + +# Diagnostics +``` +invalid.jsonc:1:2 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ (array1.length === 0 || array1.every(Boolean)) && (array2.length === 0 || array2.every(Boolean)) + │ ^^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ (array1.length·===·0·||·array1.every(Boolean))·&&·(array2.length·===·0·||·array2.every(Boolean)) + │ - ----------------------- - + +``` + +``` +invalid.jsonc:1:52 lint/nursery/noUselessLengthCheck FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This length check is unnecessary. + + > 1 │ (array1.length === 0 || array1.every(Boolean)) && (array2.length === 0 || array2.every(Boolean)) + │ ^^^^^^^^^^^^^^^^^^^ + + i The empty check is useless as `Array#every()` returns `true` for an empty array. + + i Unsafe fix: Remove the length check + + 1 │ (array1.length·===·0·||·array1.every(Boolean))·&&·(array2.length·===·0·||·array2.every(Boolean)) + │ - ----------------------- - + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessLengthCheck/valid.jsonc b/crates/biome_js_analyze/tests/specs/nursery/noUselessLengthCheck/valid.jsonc new file mode 100644 index 000000000000..1bb14b421c7f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessLengthCheck/valid.jsonc @@ -0,0 +1,35 @@ +[ + "if (array.every(Boolean));", + "if (array.some(Boolean));", + "array.length === 5 && array.some(Boolean)", + "const isAllTrulyOrEmpty = array.every(Boolean);", + "if (array.length === 0 || anotherCheck() || array.every(Boolean));", + "const isNonEmptyAllTrulyArray = array.length > 0 && array.every(Boolean);", + "const isEmptyArrayOrAllTruly = array.length === 0 || array.some(Boolean);", + "array.length === 0 ?? array.every(Boolean)", + "array.length === 0 && array.every(Boolean)", + "(array.length === 0) + (array.every(Boolean))", + "array.length === 1 || array.every(Boolean)", + "array.length === '0' || array.every(Boolean)", + "array.length === 0. || array.every(Boolean)", + "array.length === 0x0 || array.every(Boolean)", + "array.length !== 0 || array.every(Boolean)", + "array.length == 0 || array.every(Boolean)", + "0 === array.length || array.every(Boolean)", + "array?.length === 0 || array.every(Boolean)", + "array.notLength === 0 || array.every(Boolean)", + "array[length] === 0 || array.every(Boolean)", + "array.length === 0 || array.every?.(Boolean)", + "array.length === 0 || array?.every(Boolean)", + "array.length === 0 || array.every", + "array.length === 0 || array[every](Boolean)", + "array1.length === 0 || array2.every(Boolean)", + "(foo && array.length === 0) || array.every(Boolean) && foo", + "array.length === 0 || (array.every(Boolean) && foo)", + "(foo || array.length > 0) && array.some(Boolean)", + "array.length > 0 && (array.some(Boolean) || foo)", + "array.length > 0 && array.some(Boolean) && array.other(Boolean)", + "array.length > 0 && array.some(Boolean) && array.every(Boolean)", + "array.length === 0", + "array.every(Boolean)" +] \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessLengthCheck/valid.jsonc.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessLengthCheck/valid.jsonc.snap new file mode 100644 index 000000000000..ddad05a6581f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessLengthCheck/valid.jsonc.snap @@ -0,0 +1,169 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: valid.jsonc +--- +# Input +```cjs +if (array.every(Boolean)); +``` + +# Input +```cjs +if (array.some(Boolean)); +``` + +# Input +```cjs +array.length === 5 && array.some(Boolean) +``` + +# Input +```cjs +const isAllTrulyOrEmpty = array.every(Boolean); +``` + +# Input +```cjs +if (array.length === 0 || anotherCheck() || array.every(Boolean)); +``` + +# Input +```cjs +const isNonEmptyAllTrulyArray = array.length > 0 && array.every(Boolean); +``` + +# Input +```cjs +const isEmptyArrayOrAllTruly = array.length === 0 || array.some(Boolean); +``` + +# Input +```cjs +array.length === 0 ?? array.every(Boolean) +``` + +# Input +```cjs +array.length === 0 && array.every(Boolean) +``` + +# Input +```cjs +(array.length === 0) + (array.every(Boolean)) +``` + +# Input +```cjs +array.length === 1 || array.every(Boolean) +``` + +# Input +```cjs +array.length === '0' || array.every(Boolean) +``` + +# Input +```cjs +array.length === 0. || array.every(Boolean) +``` + +# Input +```cjs +array.length === 0x0 || array.every(Boolean) +``` + +# Input +```cjs +array.length !== 0 || array.every(Boolean) +``` + +# Input +```cjs +array.length == 0 || array.every(Boolean) +``` + +# Input +```cjs +0 === array.length || array.every(Boolean) +``` + +# Input +```cjs +array?.length === 0 || array.every(Boolean) +``` + +# Input +```cjs +array.notLength === 0 || array.every(Boolean) +``` + +# Input +```cjs +array[length] === 0 || array.every(Boolean) +``` + +# Input +```cjs +array.length === 0 || array.every?.(Boolean) +``` + +# Input +```cjs +array.length === 0 || array?.every(Boolean) +``` + +# Input +```cjs +array.length === 0 || array.every +``` + +# Input +```cjs +array.length === 0 || array[every](Boolean) +``` + +# Input +```cjs +array1.length === 0 || array2.every(Boolean) +``` + +# Input +```cjs +(foo && array.length === 0) || array.every(Boolean) && foo +``` + +# Input +```cjs +array.length === 0 || (array.every(Boolean) && foo) +``` + +# Input +```cjs +(foo || array.length > 0) && array.some(Boolean) +``` + +# Input +```cjs +array.length > 0 && (array.some(Boolean) || foo) +``` + +# Input +```cjs +array.length > 0 && array.some(Boolean) && array.other(Boolean) +``` + +# Input +```cjs +array.length > 0 && array.some(Boolean) && array.every(Boolean) +``` + +# Input +```cjs +array.length === 0 +``` + +# Input +```cjs +array.every(Boolean) +``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index b8ab54b2a6fc..8f82a9dc58e1 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1338,6 +1338,10 @@ export interface Nursery { * Disallow unnecessary escape sequence in regular expression literals. */ noUselessEscapeInRegex?: RuleFixConfiguration_for_Null; + /** + * Disallow unnecessary length checks within logical expressions. + */ + noUselessLengthCheck?: RuleFixConfiguration_for_Null; /** * Disallow unnecessary String.raw function in template string literals without any escape sequence. */ @@ -2993,6 +2997,7 @@ export type Category = | "lint/nursery/noUnmatchableAnbSelector" | "lint/nursery/noUnusedFunctionParameters" | "lint/nursery/noUselessEscapeInRegex" + | "lint/nursery/noUselessLengthCheck" | "lint/nursery/noUselessStringRaw" | "lint/nursery/noUselessUndefined" | "lint/nursery/noValueAtRule" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index f88cbb844c48..5e33e31830fc 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2296,6 +2296,13 @@ { "type": "null" } ] }, + "noUselessLengthCheck": { + "description": "Disallow unnecessary length checks within logical expressions.", + "anyOf": [ + { "$ref": "#/definitions/RuleFixConfiguration" }, + { "type": "null" } + ] + }, "noUselessStringRaw": { "description": "Disallow unnecessary String.raw function in template string literals without any escape sequence.", "anyOf": [