diff --git a/CHANGELOG.md b/CHANGELOG.md index 76a2bd8f21b3..29a0fa75c31d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -273,6 +273,8 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b - Implement [#2043](https://github.com/biomejs/biome/issues/2043): The React rule [`useExhaustiveDependencies`](https://biomejs.dev/linter/rules/use-exhaustive-dependencies/) is now also compatible with Preact hooks imported from `preact/hooks` or `preact/compat`. Contributed by @arendjr +- Add rule [noFlatMapIdentity](https://biomejs.dev/linter/rules/no-flat-map-identity) to disallow unnecessary callback use on `flatMap`. Contributed by @isnakode + - Add rule [noConstantMathMinMaxClamp](https://biomejs.dev/linter/rules/no-constant-math-min-max-clamp), which disallows using `Math.min` and `Math.max` to clamp a value where the result itself is constant. Contributed by @mgomulak #### Enhancements diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index 9713dd05b31b..8e9e65ea7283 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2620,6 +2620,9 @@ pub struct Nursery { #[doc = "Disallow using export or module.exports in files containing tests"] #[serde(skip_serializing_if = "Option::is_none")] pub no_exports_in_test: Option>, + #[doc = "Disallow to use unnecessary callback on flatMap."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_flat_map_identity: Option>, #[doc = "Disallow focused tests."] #[serde(skip_serializing_if = "Option::is_none")] pub no_focused_tests: Option>, @@ -2679,7 +2682,7 @@ impl DeserializableValidator for Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 26] = [ + pub(crate) const GROUP_RULES: [&'static str; 27] = [ "noBarrelFile", "noColorInvalidHex", "noConsole", @@ -2692,6 +2695,7 @@ impl Nursery { "noEvolvingAny", "noExcessiveNestedTestSuites", "noExportsInTest", + "noFlatMapIdentity", "noFocusedTests", "noMisplacedAssertion", "noNamespaceImport", @@ -2707,7 +2711,7 @@ impl Nursery { "useNodeAssertStrict", "useSortedClasses", ]; - const RECOMMENDED_RULES: [&'static str; 11] = [ + const RECOMMENDED_RULES: [&'static str; 12] = [ "noDoneCallback", "noDuplicateElseIf", "noDuplicateFontNames", @@ -2716,11 +2720,12 @@ impl Nursery { "noEvolvingAny", "noExcessiveNestedTestSuites", "noExportsInTest", + "noFlatMapIdentity", "noFocusedTests", "noSuspiciousSemicolonInJsx", "noUselessTernary", ]; - const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 11] = [ + const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 12] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]), @@ -2730,10 +2735,11 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), ]; - const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 26] = [ + const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 27] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -2760,6 +2766,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -2836,76 +2843,81 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_focused_tests.as_ref() { + if let Some(rule) = self.no_flat_map_identity.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.no_misplaced_assertion.as_ref() { + if let Some(rule) = self.no_focused_tests.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.no_namespace_import.as_ref() { + if let Some(rule) = self.no_misplaced_assertion.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.no_nodejs_modules.as_ref() { + if let Some(rule) = self.no_namespace_import.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.no_re_export_all.as_ref() { + if let Some(rule) = self.no_nodejs_modules.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_restricted_imports.as_ref() { + if let Some(rule) = self.no_re_export_all.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_skipped_tests.as_ref() { + if let Some(rule) = self.no_restricted_imports.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_suspicious_semicolon_in_jsx.as_ref() { + if let Some(rule) = self.no_skipped_tests.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_undeclared_dependencies.as_ref() { + if let Some(rule) = self.no_suspicious_semicolon_in_jsx.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_useless_ternary.as_ref() { + if let Some(rule) = self.no_undeclared_dependencies.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.no_useless_ternary.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_jsx_key_in_iterable.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[23])); } } - if let Some(rule) = self.use_node_assert_strict.as_ref() { + if let Some(rule) = self.use_jsx_key_in_iterable.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_node_assert_strict.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } + 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[26])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -2970,76 +2982,81 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_focused_tests.as_ref() { + if let Some(rule) = self.no_flat_map_identity.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.no_misplaced_assertion.as_ref() { + if let Some(rule) = self.no_focused_tests.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.no_namespace_import.as_ref() { + if let Some(rule) = self.no_misplaced_assertion.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.no_nodejs_modules.as_ref() { + if let Some(rule) = self.no_namespace_import.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.no_re_export_all.as_ref() { + if let Some(rule) = self.no_nodejs_modules.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_restricted_imports.as_ref() { + if let Some(rule) = self.no_re_export_all.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_skipped_tests.as_ref() { + if let Some(rule) = self.no_restricted_imports.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_suspicious_semicolon_in_jsx.as_ref() { + if let Some(rule) = self.no_skipped_tests.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_undeclared_dependencies.as_ref() { + if let Some(rule) = self.no_suspicious_semicolon_in_jsx.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.no_useless_ternary.as_ref() { + if let Some(rule) = self.no_undeclared_dependencies.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.no_useless_ternary.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_jsx_key_in_iterable.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[23])); } } - if let Some(rule) = self.use_node_assert_strict.as_ref() { + if let Some(rule) = self.use_jsx_key_in_iterable.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_node_assert_strict.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); } } + 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[26])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3050,10 +3067,10 @@ impl Nursery { pub(crate) fn is_recommended_rule(rule_name: &str) -> bool { Self::RECOMMENDED_RULES.contains(&rule_name) } - pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 11] { + pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 12] { Self::RECOMMENDED_RULES_AS_FILTERS } - pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 26] { + pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 27] { Self::ALL_RULES_AS_FILTERS } #[doc = r" Select preset rules"] @@ -3124,6 +3141,10 @@ impl Nursery { .no_exports_in_test .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noFlatMapIdentity" => self + .no_flat_map_identity + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noFocusedTests" => self .no_focused_tests .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 0bcf3812e308..de593a0273f3 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -119,6 +119,7 @@ define_categories! { "lint/nursery/noEvolvingAny": "https://biomejs.dev/linter/rules/no-evolving-any", "lint/nursery/noExcessiveNestedTestSuites": "https://biomejs.dev/linter/rules/no-excessive-nested-test-suites", "lint/nursery/noExportsInTest": "https://biomejs.dev/linter/rules/no-exports-in-test", + "lint/nursery/noFlatMapIdentity": "https://biomejs.dev/linter/rules/no-flat-map-identity", "lint/nursery/noFocusedTests": "https://biomejs.dev/linter/rules/no-focused-tests", "lint/nursery/noDuplicateFontNames": "https://biomejs.dev/linter/rules/no-font-family-duplicate-names", "lint/nursery/noMisplacedAssertion": "https://biomejs.dev/linter/rules/no-misplaced-assertion", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index c27632815635..02a5219a0ad1 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -11,6 +11,7 @@ pub mod no_duplicate_test_hooks; pub mod no_evolving_any; pub mod no_excessive_nested_test_suites; pub mod no_exports_in_test; +pub mod no_flat_map_identity; pub mod no_focused_tests; pub mod no_misplaced_assertion; pub mod no_namespace_import; @@ -39,6 +40,7 @@ declare_group! { self :: no_evolving_any :: NoEvolvingAny , self :: no_excessive_nested_test_suites :: NoExcessiveNestedTestSuites , self :: no_exports_in_test :: NoExportsInTest , + self :: no_flat_map_identity :: NoFlatMapIdentity , self :: no_focused_tests :: NoFocusedTests , self :: no_misplaced_assertion :: NoMisplacedAssertion , self :: no_namespace_import :: NoNamespaceImport , diff --git a/crates/biome_js_analyze/src/lint/nursery/no_flat_map_identity.rs b/crates/biome_js_analyze/src/lint/nursery/no_flat_map_identity.rs new file mode 100644 index 000000000000..e817ff2dba8d --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_flat_map_identity.rs @@ -0,0 +1,182 @@ +use biome_analyze::{ + context::RuleContext, declare_rule, ActionCategory, Ast, FixKind, Rule, RuleDiagnostic, + RuleSource, +}; +use biome_console::markup; +use biome_diagnostics::Applicability; +use biome_js_factory::make::{ident, js_call_argument_list, js_call_arguments, js_name, token}; +use biome_js_syntax::{ + AnyJsExpression, AnyJsFunctionBody, AnyJsMemberExpression, AnyJsName, AnyJsStatement, + JsCallExpression, JsSyntaxKind, +}; +use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt}; + +use crate::JsRuleAction; + +declare_rule! { + /// Disallow to use unnecessary callback on `flatMap`. + /// + /// To achieve the same result (flattening an array) more concisely and efficiently, you should use `flat` instead. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// array.flatMap((arr) => arr); + /// ``` + /// + /// ```js,expect_diagnostic + /// array.flatMap((arr) => {return arr}); + /// ``` + /// + /// ### Valid + /// + /// ```js + /// array.flatMap((arr) => arr * 2); + /// ``` + /// + pub NoFlatMapIdentity { + version: "next", + name: "noFlatMapIdentity", + recommended: true, + sources: &[RuleSource::Clippy("flat_map_identity")], + fix_kind: FixKind::Safe, + } +} + +impl Rule for NoFlatMapIdentity { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let flat_map_call = ctx.query(); + + let flat_map_expression = + AnyJsMemberExpression::cast_ref(flat_map_call.callee().ok()?.syntax())?; + + if flat_map_expression.object().is_err() { + return None; + } + + if flat_map_expression.member_name()?.text() != "flatMap" { + return None; + } + + let arguments = flat_map_call.arguments().ok()?.args(); + + if let Some(arg) = arguments.first() { + let arg = arg.ok()?; + let (function_param, function_body) = match arg.as_any_js_expression()? { + AnyJsExpression::JsArrowFunctionExpression(arg) => { + let parameter: String = match arg.parameters().ok()? { + biome_js_syntax::AnyJsArrowFunctionParameters::AnyJsBinding(p) => { + p.text().trim_matches(&['(', ')']).to_owned() + } + biome_js_syntax::AnyJsArrowFunctionParameters::JsParameters(p) => { + if p.items().len() == 1 { + if let Some(param) = p.items().into_iter().next() { + param.ok()?.text() + } else { + return None; + } + } else { + return None; + } + } + }; + + let function_body: String = match arg.body().ok()? { + AnyJsFunctionBody::AnyJsExpression(body) => body.omit_parentheses().text(), + AnyJsFunctionBody::JsFunctionBody(body) => { + let mut statement = body.statements().into_iter(); + match statement.next() { + Some(AnyJsStatement::JsReturnStatement(body)) => { + let Some(AnyJsExpression::JsIdentifierExpression( + return_statement, + )) = body.argument() + else { + return None; + }; + return_statement.name().ok()?.text() + } + _ => return None, + } + } + }; + (parameter, function_body) + } + AnyJsExpression::JsFunctionExpression(arg) => { + let function_parameter = arg.parameters().ok()?.text(); + let function_parameter = + function_parameter.trim_matches(&['(', ')']).to_owned(); + + let mut statement = arg.body().ok()?.statements().into_iter(); + if let Some(AnyJsStatement::JsReturnStatement(body)) = statement.next() { + let Some(AnyJsExpression::JsIdentifierExpression(return_statement)) = + body.argument() + else { + return None; + }; + (function_parameter, return_statement.name().ok()?.text()) + } else { + return None; + } + } + _ => return None, + }; + + if function_param == function_body { + return Some(()); + } + } + None + } + + fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { + let node = ctx.query(); + Some( + RuleDiagnostic::new( + rule_category!(), + node.range(), + markup! { + "Avoid unnecessary callback in ""flatMap"" call." + }, + ) + .note(markup! {"You can just use ""flat"" to flatten the array."}), + ) + } + fn action(ctx: &RuleContext, _state: &Self::State) -> Option { + let node = ctx.query(); + let mut mutation = ctx.root().begin(); + + let empty_argument = js_call_arguments( + token(JsSyntaxKind::L_PAREN), + js_call_argument_list(vec![], vec![]), + token(JsSyntaxKind::R_PAREN), + ); + + let Ok(AnyJsExpression::JsStaticMemberExpression(flat_expression)) = node.callee() else { + return None; + }; + + let flat_member = js_name(ident("flat")); + let flat_call = flat_expression.with_member(AnyJsName::JsName(flat_member)); + + mutation.replace_node( + node.clone(), + node.clone() + .with_arguments(empty_argument) + .with_callee(AnyJsExpression::JsStaticMemberExpression(flat_call)), + ); + + Some(JsRuleAction { + mutation, + message: markup! {"Replace unnecessary ""flatMap"" call to ""flat"" instead."}.to_owned(), + category: ActionCategory::QuickFix, + applicability: Applicability::Always, + }) + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index bd00edea37ed..5e33dff57ec9 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -90,6 +90,8 @@ pub type NoExtraBooleanCast = ::Options; pub type NoExtraNonNullAssertion = < lint :: suspicious :: no_extra_non_null_assertion :: NoExtraNonNullAssertion as biome_analyze :: Rule > :: Options ; pub type NoFallthroughSwitchClause = < lint :: suspicious :: no_fallthrough_switch_clause :: NoFallthroughSwitchClause as biome_analyze :: Rule > :: Options ; +pub type NoFlatMapIdentity = + ::Options; pub type NoFocusedTests = ::Options; pub type NoForEach = ::Options; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFlatMapIdentity/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noFlatMapIdentity/invalid.js new file mode 100644 index 000000000000..3646527edd29 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFlatMapIdentity/invalid.js @@ -0,0 +1,11 @@ +array.flatMap(function f(arr) { return arr }); + +array.flatMap(function (arr) { return arr }); + +array.flatMap((arr) => arr) + +array.flatMap((arr) => {return arr}) + +array.flatMap(arr => arr) + +array.flatMap(arr => {return arr}) \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFlatMapIdentity/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noFlatMapIdentity/invalid.js.snap new file mode 100644 index 000000000000..a86b30ad6e96 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFlatMapIdentity/invalid.js.snap @@ -0,0 +1,167 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```jsx +array.flatMap(function f(arr) { return arr }); + +array.flatMap(function (arr) { return arr }); + +array.flatMap((arr) => arr) + +array.flatMap((arr) => {return arr}) + +array.flatMap(arr => arr) + +array.flatMap(arr => {return arr}) +``` + +# Diagnostics +``` +invalid.js:1:1 lint/nursery/noFlatMapIdentity FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Avoid unnecessary callback in flatMap call. + + > 1 │ array.flatMap(function f(arr) { return arr }); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 2 │ + 3 │ array.flatMap(function (arr) { return arr }); + + i You can just use flat to flatten the array. + + i Safe fix: Replace unnecessary flatMap call to flat instead. + + 1 │ - array.flatMap(function·f(arr)·{·return·arr·}); + 1 │ + array.flat(); + 2 2 │ + 3 3 │ array.flatMap(function (arr) { return arr }); + + +``` + +``` +invalid.js:3:1 lint/nursery/noFlatMapIdentity FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Avoid unnecessary callback in flatMap call. + + 1 │ array.flatMap(function f(arr) { return arr }); + 2 │ + > 3 │ array.flatMap(function (arr) { return arr }); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 4 │ + 5 │ array.flatMap((arr) => arr) + + i You can just use flat to flatten the array. + + i Safe fix: Replace unnecessary flatMap call to flat instead. + + 1 1 │ array.flatMap(function f(arr) { return arr }); + 2 2 │ + 3 │ - array.flatMap(function·(arr)·{·return·arr·}); + 3 │ + array.flat(); + 4 4 │ + 5 5 │ array.flatMap((arr) => arr) + + +``` + +``` +invalid.js:5:1 lint/nursery/noFlatMapIdentity FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Avoid unnecessary callback in flatMap call. + + 3 │ array.flatMap(function (arr) { return arr }); + 4 │ + > 5 │ array.flatMap((arr) => arr) + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 │ + 7 │ array.flatMap((arr) => {return arr}) + + i You can just use flat to flatten the array. + + i Safe fix: Replace unnecessary flatMap call to flat instead. + + 3 3 │ array.flatMap(function (arr) { return arr }); + 4 4 │ + 5 │ - array.flatMap((arr)·=>·arr) + 5 │ + array.flat() + 6 6 │ + 7 7 │ array.flatMap((arr) => {return arr}) + + +``` + +``` +invalid.js:7:1 lint/nursery/noFlatMapIdentity FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Avoid unnecessary callback in flatMap call. + + 5 │ array.flatMap((arr) => arr) + 6 │ + > 7 │ array.flatMap((arr) => {return arr}) + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ + 9 │ array.flatMap(arr => arr) + + i You can just use flat to flatten the array. + + i Safe fix: Replace unnecessary flatMap call to flat instead. + + 5 5 │ array.flatMap((arr) => arr) + 6 6 │ + 7 │ - array.flatMap((arr)·=>·{return·arr}) + 7 │ + array.flat() + 8 8 │ + 9 9 │ array.flatMap(arr => arr) + + +``` + +``` +invalid.js:9:1 lint/nursery/noFlatMapIdentity FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Avoid unnecessary callback in flatMap call. + + 7 │ array.flatMap((arr) => {return arr}) + 8 │ + > 9 │ array.flatMap(arr => arr) + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + 10 │ + 11 │ array.flatMap(arr => {return arr}) + + i You can just use flat to flatten the array. + + i Safe fix: Replace unnecessary flatMap call to flat instead. + + 7 7 │ array.flatMap((arr) => {return arr}) + 8 8 │ + 9 │ - array.flatMap(arr·=>·arr) + 9 │ + array.flat() + 10 10 │ + 11 11 │ array.flatMap(arr => {return arr}) + + +``` + +``` +invalid.js:11:1 lint/nursery/noFlatMapIdentity FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Avoid unnecessary callback in flatMap call. + + 9 │ array.flatMap(arr => arr) + 10 │ + > 11 │ array.flatMap(arr => {return arr}) + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i You can just use flat to flatten the array. + + i Safe fix: Replace unnecessary flatMap call to flat instead. + + 9 9 │ array.flatMap(arr => arr) + 10 10 │ + 11 │ - array.flatMap(arr·=>·{return·arr}) + 11 │ + array.flat() + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFlatMapIdentity/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noFlatMapIdentity/valid.js new file mode 100644 index 000000000000..c8086a8188fb --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFlatMapIdentity/valid.js @@ -0,0 +1,9 @@ +array.flatMap((arr) => arr * 2); + +array.flatMap(arr => arr * 2); + +flatMap((x) => x); + +flatMap(x => x); + +arr.flatMap((x, y) => (x, y)) \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFlatMapIdentity/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noFlatMapIdentity/valid.js.snap new file mode 100644 index 000000000000..00937a0116ab --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFlatMapIdentity/valid.js.snap @@ -0,0 +1,16 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```jsx +array.flatMap((arr) => arr * 2); + +array.flatMap(arr => arr * 2); + +flatMap((x) => x); + +flatMap(x => x); + +arr.flatMap((x, y) => (x, y)) +``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index b62ed514e6c9..80b9f8200ca9 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -944,6 +944,10 @@ export interface Nursery { * Disallow using export or module.exports in files containing tests */ noExportsInTest?: RuleConfiguration_for_Null; + /** + * Disallow to use unnecessary callback on flatMap. + */ + noFlatMapIdentity?: RuleConfiguration_for_Null; /** * Disallow focused tests. */ @@ -1927,6 +1931,7 @@ export type Category = | "lint/nursery/noEvolvingAny" | "lint/nursery/noExcessiveNestedTestSuites" | "lint/nursery/noExportsInTest" + | "lint/nursery/noFlatMapIdentity" | "lint/nursery/noFocusedTests" | "lint/nursery/noDuplicateFontNames" | "lint/nursery/noMisplacedAssertion" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 668e3f630d48..002cadb5aae1 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1484,6 +1484,13 @@ { "type": "null" } ] }, + "noFlatMapIdentity": { + "description": "Disallow to use unnecessary callback on flatMap.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noFocusedTests": { "description": "Disallow focused tests.", "anyOf": [ diff --git a/website/src/components/generated/NumberOfRules.astro b/website/src/components/generated/NumberOfRules.astro index ca8f1fff2aa8..dba9a190f7e8 100644 --- a/website/src/components/generated/NumberOfRules.astro +++ b/website/src/components/generated/NumberOfRules.astro @@ -1,2 +1,2 @@ -

Biome's linter has a total of 215 rules

\ No newline at end of file +

Biome's linter has a total of 216 rules

\ No newline at end of file diff --git a/website/src/content/docs/internals/changelog.md b/website/src/content/docs/internals/changelog.md index 073cb84e0240..a26c109313ac 100644 --- a/website/src/content/docs/internals/changelog.md +++ b/website/src/content/docs/internals/changelog.md @@ -279,6 +279,8 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b - Implement [#2043](https://github.com/biomejs/biome/issues/2043): The React rule [`useExhaustiveDependencies`](https://biomejs.dev/linter/rules/use-exhaustive-dependencies/) is now also compatible with Preact hooks imported from `preact/hooks` or `preact/compat`. Contributed by @arendjr +- Add rule [noFlatMapIdentity](https://biomejs.dev/linter/rules/no-flat-map-identity) to disallow unnecessary callback use on `flatMap`. Contributed by @isnakode + - Add rule [noConstantMathMinMaxClamp](https://biomejs.dev/linter/rules/no-constant-math-min-max-clamp), which disallows using `Math.min` and `Math.max` to clamp a value where the result itself is constant. Contributed by @mgomulak #### Enhancements diff --git a/website/src/content/docs/linter/rules/index.mdx b/website/src/content/docs/linter/rules/index.mdx index b454ca80fd72..15f7f0530c79 100644 --- a/website/src/content/docs/linter/rules/index.mdx +++ b/website/src/content/docs/linter/rules/index.mdx @@ -263,6 +263,7 @@ Rules that belong to this group are not subject to semantic versionany type through reassignments. | | | [noExcessiveNestedTestSuites](/linter/rules/no-excessive-nested-test-suites) | This rule enforces a maximum depth to nested describe() in test files. | | | [noExportsInTest](/linter/rules/no-exports-in-test) | Disallow using export or module.exports in files containing tests | | +| [noFlatMapIdentity](/linter/rules/no-flat-map-identity) | Disallow to use unnecessary callback on flatMap. | 🔧 | | [noFocusedTests](/linter/rules/no-focused-tests) | Disallow focused tests. | ⚠️ | | [noMisplacedAssertion](/linter/rules/no-misplaced-assertion) | Checks that the assertion function, for example expect, is placed inside an it() function call. | | | [noNamespaceImport](/linter/rules/no-namespace-import) | Disallow the use of namespace imports. | | diff --git a/website/src/content/docs/linter/rules/no-flat-map-identity.md b/website/src/content/docs/linter/rules/no-flat-map-identity.md new file mode 100644 index 000000000000..36f7f5bb9342 --- /dev/null +++ b/website/src/content/docs/linter/rules/no-flat-map-identity.md @@ -0,0 +1,78 @@ +--- +title: noFlatMapIdentity (not released) +--- + +**Diagnostic Category: `lint/nursery/noFlatMapIdentity`** + +:::danger +This rule hasn't been released yet. +::: + +:::caution +This rule is part of the [nursery](/linter/rules/#nursery) group. +::: + +Source: flat_map_identity + +Disallow to use unnecessary callback on `flatMap`. + +To achieve the same result (flattening an array) more concisely and efficiently, you should use `flat` instead. + +## Examples + +### Invalid + +```jsx +array.flatMap((arr) => arr); +``` + +

nursery/noFlatMapIdentity.jsx:1:1 lint/nursery/noFlatMapIdentity  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Avoid unnecessary callback in flatMap call.
+  
+  > 1 │ array.flatMap((arr) => arr);
+   ^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    2 │ 
+  
+   You can just use flat to flatten the array.
+  
+   Safe fix: Replace unnecessary flatMap call to flat instead.
+  
+    1  - array.flatMap((arr)·=>·arr);
+      1+ array.flat();
+    2 2  
+  
+
+ +```jsx +array.flatMap((arr) => {return arr}); +``` + +
nursery/noFlatMapIdentity.jsx:1:1 lint/nursery/noFlatMapIdentity  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━
+
+   Avoid unnecessary callback in flatMap call.
+  
+  > 1 │ array.flatMap((arr) => {return arr});
+   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    2 │ 
+  
+   You can just use flat to flatten the array.
+  
+   Safe fix: Replace unnecessary flatMap call to flat instead.
+  
+    1  - array.flatMap((arr)·=>·{return·arr});
+      1+ array.flat();
+    2 2  
+  
+
+ +### Valid + +```jsx +array.flatMap((arr) => arr * 2); +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options)