feat(rome_js_analyze): new rule `useFlatMap`
MODIFY
crates/rome_diagnostics_categories/src/categories.rs
crates/rome_diagnostics_categories/src/categories.rs
@@ -59,6 +59,7 @@ define_dategories! {
"lint/nursery/useValidAnchor": "https://rome.tools/docs/lint/rules/useValidAnchor",
"lint/nursery/useKeyWithClickEvents": "https://rome.tools/docs/lint/rules/useKeyWithClickEvents",
"lint/nursery/noRestrictedGlobals": "https://rome.tools/docs/lint/rules/noRestrictedGlobals",
+ "lint/nursery/useFlatMap": "https://rome.tools/docs/lint/rules/useFlatMap",
"lint/nursery/noPositiveTabindex": "https://rome.tools/docs/lint/rules/noPositiveTabindex",
"lint/nursery/noConstAssign": "https://rome.tools/docs/lint/rules/noConstAssign",
"lint/nursery/noExplicitAny": "https://rome.tools/docs/lint/rules/noExplicitAny",

MODIFY
crates/rome_js_analyze/src/analyzers/nursery.rs
crates/rome_js_analyze/src/analyzers/nursery.rs
@@ -6,7 +6,8 @@ mod no_new_symbol;
mod no_unreachable;
mod use_anchor_content;
mod use_blank_target;
+mod use_flat_map;
mod use_key_with_click_events;
mod use_optional_chain;
mod use_valid_anchor;
-declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_auto_focus :: NoAutoFocus , self :: no_new_symbol :: NoNewSymbol , self :: no_unreachable :: NoUnreachable , self :: use_anchor_content :: UseAnchorContent , self :: use_blank_target :: UseBlankTarget , self :: use_key_with_click_events :: UseKeyWithClickEvents , self :: use_optional_chain :: UseOptionalChain , self :: use_valid_anchor :: UseValidAnchor ,] } }
+declare_group! { pub (crate) Nursery { name : "nursery" , rules : [self :: no_auto_focus :: NoAutoFocus , self :: no_new_symbol :: NoNewSymbol , self :: no_unreachable :: NoUnreachable , self :: use_anchor_content :: UseAnchorContent , self :: use_blank_target :: UseBlankTarget , self :: use_flat_map :: UseFlatMap , self :: use_key_with_click_events :: UseKeyWithClickEvents , self :: use_optional_chain :: UseOptionalChain , self :: use_valid_anchor :: UseValidAnchor ,] } }

ADD
crates/rome_js_analyze/src/analyzers/nursery/use_flat_map.rs
+use crate::JsRuleAction;
+use rome_analyze::context::RuleContext;
+use rome_analyze::{declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic};
+use rome_console::markup;
+use rome_diagnostics::Applicability;
+use rome_js_factory::make::{ident, js_name};
+use rome_js_syntax::{JsAnyExpression, JsAnyName, JsCallExpression};
+use rome_rowan::{AstNode, AstSeparatedList, BatchMutationExt};
+
+declare_rule! {
+ /// Promotes the use of `.flatMap()` when `map().flat()` are used together.
+ ///
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```js,expect_diagnostic
+ /// const array = ["split", "the text", "into words"];
+ /// array.map(sentence => sentence.split(' ')).flat();
+ /// ```
+ ///
+ /// ```js,expect_diagnostic
+ /// const array = ["split", "the text", "into words"];
+ /// array.map(sentence => sentence.split(' ')).flat(1);
+ /// ```
+ ///
+ /// ### Valid
+ ///
+ /// ```js
+ /// const array = ["split", "the text", "into words"];
+ /// array.map(sentence => sentence.split(' ')).flat(2);
+ /// ```
+ ///
+ pub(crate) UseFlatMap {
+ version: "10.0.0",
+ name: "useFlatMap",
+ recommended: false,
+ }
+}
+
+impl Rule for UseFlatMap {
+ type Query = Ast<JsCallExpression>;
+ type State = JsCallExpression;
+ type Signals = Option<Self::State>;
+ type Options = ();
+
+ fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+ let node = ctx.query();
+ let arguments = node.arguments().ok()?.args();
+ // Probably not a `flat` call.
+ if arguments.len() > 1 {
+ return None;
+ }
+ if let Some(first_argument) = arguments.first() {
+ let first_argument = first_argument.ok()?;
+ let first_argument = first_argument
+ .as_js_any_expression()?
+ .as_js_any_literal_expression()?
+ .as_js_number_literal_expression()?;
+
+ if first_argument.value_token().ok()?.text_trimmed() != "1" {
+ return None;
+ }
+ }
+ let static_member_expression = node.callee().ok()?;
+ let static_member_expression = static_member_expression.as_js_static_member_expression()?;
+ let flat_member_name = static_member_expression
+ .member()
+ .ok()?
+ .as_js_name()?
+ .value_token()
+ .ok()?;
+ if flat_member_name.text_trimmed() == "flat" {
+ let call_expression = static_member_expression.object().ok()?;
+ let call_expression = call_expression.as_js_call_expression()?;
+ let map_call_arguments = call_expression.arguments().ok()?.args();
+
+ // probably not a `.map` all coming from an array
+ if map_call_arguments.len() > 2 || map_call_arguments.len() < 1 {
+ return None;
+ }
+
+ let map_static_member_expression = call_expression.callee().ok()?;
+ let map_static_member_expression =
+ map_static_member_expression.as_js_static_member_expression()?;
+ let map_member_name = map_static_member_expression
+ .member()
+ .ok()?
+ .as_js_name()?
+ .value_token()
+ .ok()?;
+
+ if map_member_name.text_trimmed() == "map" {
+ return Some(call_expression.clone());
+ }
+ }
+
+ None
+ }
+
+ fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
+ let node = ctx.query();
+
+ Some(RuleDiagnostic::new(
+ rule_category!(),
+ node.syntax().text_trimmed_range(),
+ markup! {
+ "The call chain "<Emphasis>".map().flat()"</Emphasis>" can be replaced with a single "<Emphasis>".flatMap()"</Emphasis>" call."
+ },
+ ))
+ }
+
+ fn action(ctx: &RuleContext<Self>, flat_call: &Self::State) -> Option<JsRuleAction> {
+ let node = ctx.query();
+ let mut mutation = ctx.root().begin();
+
+ let flat_call = flat_call.clone();
+ let old_static_member_expression = flat_call.callee().ok()?;
+ let old_static_member_expression =
+ old_static_member_expression.as_js_static_member_expression()?;
+ let member = js_name(ident("flatMap"));
+
+ let flat_map_member_expression = old_static_member_expression
+ .clone()
+ .with_member(JsAnyName::JsName(member));
+
+ let flat_map_call = flat_call.with_callee(JsAnyExpression::JsStaticMemberExpression(
+ flat_map_member_expression,
+ ));
+
+ mutation.replace_node(node.clone(), flat_map_call);
+
+ Some(JsRuleAction {
+ mutation,
+ message: markup! {"Replace the chain with "<Emphasis>".flatMap()"</Emphasis>"."}
+ .to_owned(),
+ category: ActionCategory::QuickFix,
+ applicability: Applicability::Always,
+ })
+ }
+}

MODIFY
crates/rome_js_analyze/src/lib.rs
crates/rome_js_analyze/src/lib.rs
@@ -128,10 +128,7 @@ mod tests {
#[ignore]
#[test]
fn quick_test() {
- const SOURCE: &str = r#"function f(event, error) {
- console.log(event);
- console.log(error);
-}
+ const SOURCE: &str = r#"[0, [12]].map(Number).flat()
"#;
let parsed = parse(SOURCE, FileId::zero(), SourceType::jsx());

ADD
crates/rome_js_analyze/tests/specs/nursery/useFlatMap.js
+// invalid
+[0, [12]].map(Number).flat();
+[0, [12], [[16]]].map(Number).flat(1);
+[0, [12], [[16]]].map((element) => {}).flat(1);
+[0, [12], [[16]]].map((element, index) => {}).flat(1);
+[0, [12], [[16]]].map((element, index, array) => {}).flat(1);
+// valid
+[0, [12], [[16]]].map(Number).flat(2);
+array.flat().map(Number);
+array.map(Number).flat(customFn);
+MyObject.map(one, two, tree).flat();
+MyObject.map().flat(1, "test")
\ No newline at end of file

MODIFY
crates/rome_service/src/configuration/linter/rules.rs
crates/rome_service/src/configuration/linter/rules.rs
@@ -406,6 +406,7 @@ struct NurserySchema {
use_blank_target: Option<RuleConfiguration>,
use_button_type: Option<RuleConfiguration>,
use_camel_case: Option<RuleConfiguration>,
+ use_flat_map: Option<RuleConfiguration>,
use_fragment_syntax: Option<RuleConfiguration>,
use_key_with_click_events: Option<RuleConfiguration>,
use_key_with_mouse_events: Option<RuleConfiguration>,
@@ -414,7 +415,7 @@ struct NurserySchema {
}
impl Nursery {
const CATEGORY_NAME: &'static str = "nursery";
- pub(crate) const CATEGORY_RULES: [&'static str; 25] = [
+ pub(crate) const CATEGORY_RULES: [&'static str; 26] = [
"noArrayIndexKey",
"noAutofocus",
"noChildrenProp",
@@ -435,6 +436,7 @@ impl Nursery {
"useBlankTarget",
"useButtonType",
"useCamelCase",
+ "useFlatMap",
"useFragmentSyntax",
"useKeyWithClickEvents",
"useKeyWithMouseEvents",

MODIFY
editors/vscode/configuration_schema.json
editors/vscode/configuration_schema.json
}
]
},
+ "useFlatMap": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/RuleConfiguration"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
"useFragmentSyntax": {
"anyOf": [
{

MODIFY
npm/backend-jsonrpc/src/workspace.ts
npm/backend-jsonrpc/src/workspace.ts
@@ -179,6 +179,7 @@ export interface Nursery {
useBlankTarget?: RuleConfiguration;
useButtonType?: RuleConfiguration;
useCamelCase?: RuleConfiguration;
+ useFlatMap?: RuleConfiguration;
useFragmentSyntax?: RuleConfiguration;
useKeyWithClickEvents?: RuleConfiguration;
useKeyWithMouseEvents?: RuleConfiguration;
@@ -323,6 +324,7 @@ export type Category =
| "lint/nursery/useValidAnchor"
| "lint/nursery/useKeyWithClickEvents"
| "lint/nursery/noRestrictedGlobals"
+ | "lint/nursery/useFlatMap"
| "lint/nursery/noPositiveTabindex"
| "lint/nursery/noConstAssign"
| "lint/nursery/noExplicitAny"

MODIFY
website/src/docs/lint/rules/index.md
website/src/docs/lint/rules/index.md
@@ -395,6 +395,13 @@ Enforces the usage of the attribute <code>type</code> for the element <code>butt
Enforce camel case naming convention.
</div>
<div class="rule">
+<h3 data-toc-exclude id="useFlatMap">
+ <a href="/docs/lint/rules/useFlatMap">useFlatMap (since v10.0.0)</a>
+ <a class="header-anchor" href="#useFlatMap"></a>
+</h3>
+Promotes the use of <code>.flatMap()</code> when <code>map().flat()</code> are used together.
+</div>
+<div class="rule">
<h3 data-toc-exclude id="useFragmentSyntax">
<a href="/docs/lint/rules/useFragmentSyntax">useFragmentSyntax (since v0.10.0)</a>
<a class="header-anchor" href="#useFragmentSyntax"></a>

feat(rome_js_analyze): new rule `useFlatMap` (#3384)