diff --git a/.changeset/twelve-plants-marry.md b/.changeset/twelve-plants-marry.md
new file mode 100644
index 000000000000..e0367664ea96
--- /dev/null
+++ b/.changeset/twelve-plants-marry.md
@@ -0,0 +1,5 @@
+---
+"@biomejs/biome": minor
+---
+
+Add the new rule `useForComponent`, which enforce using Solid's `` component for mapping an array to JSX elements.
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 4c511541b679..6536c4e915d6 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
@@ -2112,6 +2112,21 @@ pub(crate) fn migrate_eslint_any_rule(
.get_or_insert(Default::default());
rule.set_level(rule.level().max(rule_severity.into()));
}
+ "solidjs/perfer-for" => {
+ if !options.include_inspired {
+ results.has_inspired_rules = true;
+ return false;
+ }
+ if !options.include_nursery {
+ return false;
+ }
+ let group = rules.nursery.get_or_insert_with(Default::default);
+ let rule = group
+ .unwrap_group_as_mut()
+ .use_for_component
+ .get_or_insert(Default::default());
+ rule.set_level(rule.level().max(rule_severity.into()));
+ }
"sonarjs/cognitive-complexity" => {
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 ea86a9c8b2a8..ca6e77678952 100644
--- a/crates/biome_configuration/src/analyzer/linter/rules.rs
+++ b/crates/biome_configuration/src/analyzer/linter/rules.rs
@@ -3221,6 +3221,9 @@ pub struct Nursery {
#[doc = "Require that all exports are declared after all non-export statements."]
#[serde(skip_serializing_if = "Option::is_none")]
pub use_exports_last: Option>,
+ #[doc = "Enforce using Solid's \\ component for mapping an array to JSX elements."]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub use_for_component: Option>,
#[doc = "Enforces the use of a recommended display strategy with Google Fonts."]
#[serde(skip_serializing_if = "Option::is_none")]
pub use_google_font_display:
@@ -3318,6 +3321,7 @@ impl Nursery {
"useDeprecatedReason",
"useExplicitType",
"useExportsLast",
+ "useForComponent",
"useGoogleFontDisplay",
"useGoogleFontPreconnect",
"useGuardForIn",
@@ -3345,9 +3349,9 @@ impl Nursery {
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52]),
- RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58]),
- RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60]),
- RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[62]),
+ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59]),
+ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61]),
+ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[63]),
];
const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]),
@@ -3415,6 +3419,7 @@ impl Nursery {
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[62]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[63]),
RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[64]),
+ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[65]),
];
}
impl RuleGroupExt for Nursery {
@@ -3701,56 +3706,61 @@ impl RuleGroupExt for Nursery {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54]));
}
}
- if let Some(rule) = self.use_google_font_display.as_ref() {
+ if let Some(rule) = self.use_for_component.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55]));
}
}
- if let Some(rule) = self.use_google_font_preconnect.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[56]));
}
}
- if let Some(rule) = self.use_guard_for_in.as_ref() {
+ if let Some(rule) = self.use_google_font_preconnect.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57]));
}
}
- if let Some(rule) = self.use_named_operation.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[58]));
}
}
- if let Some(rule) = self.use_naming_convention.as_ref() {
+ if let Some(rule) = self.use_named_operation.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59]));
}
}
- if let Some(rule) = self.use_parse_int_radix.as_ref() {
+ if let Some(rule) = self.use_naming_convention.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60]));
}
}
- if let Some(rule) = self.use_sorted_classes.as_ref() {
+ if let Some(rule) = self.use_parse_int_radix.as_ref() {
if rule.is_enabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61]));
}
}
- 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[62]));
}
}
- 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[63]));
}
}
- 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[64]));
}
}
+ 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[65]));
+ }
+ }
index_set
}
fn get_disabled_rules(&self) -> FxHashSet> {
@@ -4030,56 +4040,61 @@ impl RuleGroupExt for Nursery {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54]));
}
}
- if let Some(rule) = self.use_google_font_display.as_ref() {
+ if let Some(rule) = self.use_for_component.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55]));
}
}
- if let Some(rule) = self.use_google_font_preconnect.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[56]));
}
}
- if let Some(rule) = self.use_guard_for_in.as_ref() {
+ if let Some(rule) = self.use_google_font_preconnect.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57]));
}
}
- if let Some(rule) = self.use_named_operation.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[58]));
}
}
- if let Some(rule) = self.use_naming_convention.as_ref() {
+ if let Some(rule) = self.use_named_operation.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59]));
}
}
- if let Some(rule) = self.use_parse_int_radix.as_ref() {
+ if let Some(rule) = self.use_naming_convention.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60]));
}
}
- if let Some(rule) = self.use_sorted_classes.as_ref() {
+ if let Some(rule) = self.use_parse_int_radix.as_ref() {
if rule.is_disabled() {
index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61]));
}
}
- 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[62]));
}
}
- 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[63]));
}
}
- 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[64]));
}
}
+ 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[65]));
+ }
+ }
index_set
}
#[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"]
@@ -4330,6 +4345,10 @@ impl RuleGroupExt for Nursery {
.use_exports_last
.as_ref()
.map(|conf| (conf.level(), conf.get_options())),
+ "useForComponent" => self
+ .use_for_component
+ .as_ref()
+ .map(|conf| (conf.level(), conf.get_options())),
"useGoogleFontDisplay" => self
.use_google_font_display
.as_ref()
diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs
index 5eda78606a2d..940baefefb77 100644
--- a/crates/biome_diagnostics_categories/src/categories.rs
+++ b/crates/biome_diagnostics_categories/src/categories.rs
@@ -140,6 +140,7 @@ define_categories! {
"lint/nursery/noConsole": "https://biomejs.dev/linter/rules/no-console",
"lint/nursery/noConstantBinaryExpression": "https://biomejs.dev/linter/rules/no-constant-binary-expression",
"lint/nursery/noDescendingSpecificity": "https://biomejs.dev/linter/rules/no-descending-specificity",
+ "lint/nursery/noDestructuredProps": "https://biomejs.dev/linter/rules/no-destructured-props",
"lint/nursery/noDocumentCookie": "https://biomejs.dev/linter/rules/no-document-cookie",
"lint/nursery/noDocumentImportInPage": "https://biomejs.dev/linter/rules/no-document-import-in-page",
"lint/nursery/noDoneCallback": "https://biomejs.dev/linter/rules/no-done-callback",
@@ -170,7 +171,6 @@ define_categories! {
"lint/nursery/noPackagePrivateImports": "https://biomejs.dev/linter/rules/no-package-private-imports",
"lint/nursery/noProcessEnv": "https://biomejs.dev/linter/rules/no-process-env",
"lint/nursery/noProcessGlobal": "https://biomejs.dev/linter/rules/no-process-global",
- "lint/nursery/noDestructuredProps": "https://biomejs.dev/linter/rules/no-destructured-props",
"lint/nursery/noReactSpecificProps": "https://biomejs.dev/linter/rules/no-react-specific-props",
"lint/nursery/noRestrictedImports": "https://biomejs.dev/linter/rules/no-restricted-imports",
"lint/nursery/noRestrictedTypes": "https://biomejs.dev/linter/rules/no-restricted-types",
@@ -219,6 +219,7 @@ define_categories! {
"lint/nursery/useNamedOperation": "https://biomejs.dev/linter/rules/use-named-operation",
"lint/nursery/useNamingConvention": "https://biomejs.dev/linter/rules/use-naming-convention",
"lint/nursery/useParseIntRadix": "https://biomejs.dev/linter/rules/use-parse-int-radix",
+ "lint/nursery/useForComponent": "https://biomejs.dev/linter/rules/use-for-component",
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
"lint/nursery/useSortedProperties": "https://biomejs.dev/linter/rules/use-sorted-properties",
"lint/nursery/useStrictMode": "https://biomejs.dev/linter/rules/use-strict-mode",
diff --git a/crates/biome_js_analyze/src/lib.rs b/crates/biome_js_analyze/src/lib.rs
index 43d70dc65a71..841b2deed473 100644
--- a/crates/biome_js_analyze/src/lib.rs
+++ b/crates/biome_js_analyze/src/lib.rs
@@ -202,15 +202,14 @@ mod tests {
#[test]
fn quick_test() {
const SOURCE: &str = r#"
-let Component = ({ prop1, prop2 }: Props) => ;
-
+let Component = (props) => {props.data.map(d => - {d.text}
)}
;
"#;
let parsed = parse(SOURCE, JsFileSource::tsx(), JsParserOptions::default());
let mut error_ranges: Vec = Vec::new();
let options = AnalyzerOptions::default();
- let rule_filter = RuleFilter::Rule("nursery", "noDestructuredProps");
+ let rule_filter = RuleFilter::Rule("nursery", "useForComponent");
let mut dependencies = Dependencies::default();
dependencies.add("buffer", "latest");
diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs
index 1bf09ac09222..15df01489523 100644
--- a/crates/biome_js_analyze/src/lint/nursery.rs
+++ b/crates/biome_js_analyze/src/lint/nursery.rs
@@ -47,6 +47,7 @@ pub mod use_consistent_member_accessibility;
pub mod use_consistent_object_definition;
pub mod use_explicit_type;
pub mod use_exports_last;
+pub mod use_for_component;
pub mod use_google_font_display;
pub mod use_google_font_preconnect;
pub mod use_guard_for_in;
@@ -55,4 +56,4 @@ pub mod use_sorted_classes;
pub mod use_strict_mode;
pub mod use_trim_start_end;
pub mod use_valid_autocomplete;
-declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_common_js :: NoCommonJs , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_document_cookie :: NoDocumentCookie , self :: no_document_import_in_page :: NoDocumentImportInPage , self :: no_duplicate_else_if :: NoDuplicateElseIf , self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess , self :: no_enum :: NoEnum , self :: no_exported_imports :: NoExportedImports , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_head_element :: NoHeadElement , self :: no_head_import_in_document :: NoHeadImportInDocument , self :: no_img_element :: NoImgElement , self :: no_import_cycles :: NoImportCycles , self :: no_irregular_whitespace :: NoIrregularWhitespace , self :: no_nested_ternary :: NoNestedTernary , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_octal_escape :: NoOctalEscape , self :: no_package_private_imports :: NoPackagePrivateImports , self :: no_process_env :: NoProcessEnv , self :: no_process_global :: NoProcessGlobal , self :: no_restricted_imports :: NoRestrictedImports , self :: no_restricted_types :: NoRestrictedTypes , self :: no_secrets :: NoSecrets , self :: no_static_element_interactions :: NoStaticElementInteractions , self :: no_substr :: NoSubstr , self :: no_template_curly_in_string :: NoTemplateCurlyInString , self :: no_ts_ignore :: NoTsIgnore , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_escape_in_regex :: NoUselessEscapeInRegex , self :: no_useless_string_raw :: NoUselessStringRaw , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole , self :: use_at_index :: UseAtIndex , self :: use_collapsed_if :: UseCollapsedIf , self :: use_component_export_only_modules :: UseComponentExportOnlyModules , self :: use_consistent_curly_braces :: UseConsistentCurlyBraces , self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_google_font_display :: UseGoogleFontDisplay , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_guard_for_in :: UseGuardForIn , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_sorted_classes :: UseSortedClasses , self :: use_strict_mode :: UseStrictMode , self :: use_trim_start_end :: UseTrimStartEnd , self :: use_valid_autocomplete :: UseValidAutocomplete ,] } }
+declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_common_js :: NoCommonJs , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_document_cookie :: NoDocumentCookie , self :: no_document_import_in_page :: NoDocumentImportInPage , self :: no_duplicate_else_if :: NoDuplicateElseIf , self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess , self :: no_enum :: NoEnum , self :: no_exported_imports :: NoExportedImports , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_head_element :: NoHeadElement , self :: no_head_import_in_document :: NoHeadImportInDocument , self :: no_img_element :: NoImgElement , self :: no_import_cycles :: NoImportCycles , self :: no_irregular_whitespace :: NoIrregularWhitespace , self :: no_nested_ternary :: NoNestedTernary , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_octal_escape :: NoOctalEscape , self :: no_package_private_imports :: NoPackagePrivateImports , self :: no_process_env :: NoProcessEnv , self :: no_process_global :: NoProcessGlobal , self :: no_restricted_imports :: NoRestrictedImports , self :: no_restricted_types :: NoRestrictedTypes , self :: no_secrets :: NoSecrets , self :: no_static_element_interactions :: NoStaticElementInteractions , self :: no_substr :: NoSubstr , self :: no_template_curly_in_string :: NoTemplateCurlyInString , self :: no_ts_ignore :: NoTsIgnore , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_escape_in_regex :: NoUselessEscapeInRegex , self :: no_useless_string_raw :: NoUselessStringRaw , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole , self :: use_at_index :: UseAtIndex , self :: use_collapsed_if :: UseCollapsedIf , self :: use_component_export_only_modules :: UseComponentExportOnlyModules , self :: use_consistent_curly_braces :: UseConsistentCurlyBraces , self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_display :: UseGoogleFontDisplay , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_guard_for_in :: UseGuardForIn , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_sorted_classes :: UseSortedClasses , self :: use_strict_mode :: UseStrictMode , self :: use_trim_start_end :: UseTrimStartEnd , self :: use_valid_autocomplete :: UseValidAutocomplete ,] } }
diff --git a/crates/biome_js_analyze/src/lint/nursery/use_for_component.rs b/crates/biome_js_analyze/src/lint/nursery/use_for_component.rs
new file mode 100644
index 000000000000..0e01fa836b3f
--- /dev/null
+++ b/crates/biome_js_analyze/src/lint/nursery/use_for_component.rs
@@ -0,0 +1,115 @@
+use biome_analyze::{
+ context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource,
+ RuleSourceKind,
+};
+use biome_console::markup;
+use biome_js_syntax::{AnyJsMemberExpression, JsCallExpression, JsSyntaxKind, JsxExpressionChild};
+use biome_rowan::{AstNode, AstSeparatedList, SyntaxNodeOptionExt};
+
+declare_lint_rule! {
+ /// Enforce using Solid's `` component for mapping an array to JSX elements.
+ ///
+ /// In Solid, `` component for efficiently rendering lists. Array#map causes DOM elements to be recreated.
+ ///
+ /// For details on `` Component, see the [Solid docs about Components](https://docs.solidjs.com/reference/components/for).
+ ///
+ /// ## Examples
+ ///
+ /// ### Invalid
+ ///
+ /// ```jsx,expect_diagnostic
+ /// let Component = (props) => {props.data.map(d => - {d.text}
)}
;
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// let Component = (props) => <>{props.data.map(d => {d.text})}>;
+ /// ```
+ ///
+ /// ```jsx,expect_diagnostic
+ /// let Component = (props) => (
+ ///
+ /// {props.data.map((d) => (
+ /// - {d.text}
+ /// ))}
+ ///
+ /// );
+ /// ```
+ ///
+ /// ### Valid
+ ///
+ /// ```jsx
+ /// let Component = (props) => {d => - {d.text}
}
;
+ /// ```
+ ///
+ /// ```jsx
+ /// let abc = x.map(y => y + z);
+ /// ```
+ ///
+ /// ```jsx
+ /// let Component = (props) => {
+ /// let abc = x.map(y => y + z);
+ /// return Hello, world!
;
+ /// }
+ /// ```
+ ///
+ pub UseForComponent {
+ version: "next",
+ name: "useForComponent",
+ language: "js",
+ domains: &[RuleDomain::Solid],
+ recommended: false,
+ sources: &[RuleSource::EslintSolid("perfer-for")],
+ source_kind: RuleSourceKind::Inspired,
+ }
+}
+
+impl Rule for UseForComponent {
+ type Query = Ast;
+ type State = ();
+ type Signals = Option;
+ type Options = ();
+
+ fn run(ctx: &RuleContext) -> Self::Signals {
+ let node = ctx.query();
+
+ if let Some(parent) = node.parent::() {
+ // Only judge the expression child under JSX_CHILD_LIST
+ // all jsxexpression with case can be covered here like:
+ // {props.data.map(d => - {d.text}
)}
+ if !matches!(
+ parent.syntax().parent().kind(),
+ Some(JsSyntaxKind::JSX_CHILD_LIST)
+ ) {
+ return None;
+ }
+
+ // check for Array.prototype.map in JSX
+ let member_expression =
+ AnyJsMemberExpression::cast(node.callee().ok()?.omit_parentheses().into_syntax())?;
+ let args = node.arguments().ok()?.args();
+
+ if args.len() != 1 || member_expression.member_name()?.text() != "map" {
+ return None;
+ }
+
+ return Some(());
+ }
+
+ None
+ }
+
+ fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option {
+ let node = ctx.query();
+
+ Some(RuleDiagnostic::new(
+ rule_category!(),
+ node.syntax().text_trimmed_range(),
+ markup! {
+ "Array.prototype.map will cause DOM elements to be recreated, it is not recommended to use it in Solid here."
+ },
+ ).note(markup! {
+ "Use Solid's `` component for efficiently rendering lists. See \
+ ""Solid docs"" for more details."
+ }))
+ }
+}
diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs
index ba59a90fe681..be8bab7f7191 100644
--- a/crates/biome_js_analyze/src/options.rs
+++ b/crates/biome_js_analyze/src/options.rs
@@ -347,6 +347,8 @@ pub type UseExportsLast =
pub type UseFilenamingConvention = < lint :: style :: use_filenaming_convention :: UseFilenamingConvention as biome_analyze :: Rule > :: Options ;
pub type UseFlatMap = ::Options;
pub type UseFocusableInteractive = < lint :: a11y :: use_focusable_interactive :: UseFocusableInteractive as biome_analyze :: Rule > :: Options ;
+pub type UseForComponent =
+ ::Options;
pub type UseForOf = ::Options;
pub type UseFragmentSyntax =
::Options;
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useForComponent/invalid.tsx b/crates/biome_js_analyze/tests/specs/nursery/useForComponent/invalid.tsx
new file mode 100644
index 000000000000..262aa14fcb44
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useForComponent/invalid.tsx
@@ -0,0 +1,17 @@
+let Component = (props) => {props.data.map(d => - {d.text}
)}
;
+
+let Component = (props) => <>{props.data.map(d => {d.text})}>;
+
+let Component = (props) => {props.data.map(d => - {d.text}
)}
;
+
+function Component(props) {
+ return {props.data.map(d => - {d.text}
)}
;
+}
+
+function Component(props) {
+ return {props.data?.map(d => - {d.text}
)}
;
+}
+
+let Component = (props) => {props.data.map(() => )}
;
+
+let Component = (props) => {props.data.map((...args) => - {args[0].text}
)}
;
\ No newline at end of file
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useForComponent/invalid.tsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useForComponent/invalid.tsx.snap
new file mode 100644
index 000000000000..74da420ec2a8
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useForComponent/invalid.tsx.snap
@@ -0,0 +1,139 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: invalid.tsx
+snapshot_kind: text
+---
+# Input
+```tsx
+let Component = (props) => {props.data.map(d => - {d.text}
)}
;
+
+let Component = (props) => <>{props.data.map(d => {d.text})}>;
+
+let Component = (props) => {props.data.map(d => - {d.text}
)}
;
+
+function Component(props) {
+ return {props.data.map(d => - {d.text}
)}
;
+}
+
+function Component(props) {
+ return {props.data?.map(d => - {d.text}
)}
;
+}
+
+let Component = (props) => {props.data.map(() => )}
;
+
+let Component = (props) => {props.data.map((...args) => - {args[0].text}
)}
;
+```
+
+# Diagnostics
+```
+invalid.tsx:1:33 lint/nursery/useForComponent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Array.prototype.map will cause DOM elements to be recreated, it is not recommended to use it in Solid here.
+
+ > 1 │ let Component = (props) => {props.data.map(d => - {d.text}
)}
;
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 2 │
+ 3 │ let Component = (props) => <>{props.data.map(d => {d.text})}>;
+
+ i Use Solid's `` component for efficiently rendering lists. See Solid docs for more details.
+
+
+```
+
+```
+invalid.tsx:3:31 lint/nursery/useForComponent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Array.prototype.map will cause DOM elements to be recreated, it is not recommended to use it in Solid here.
+
+ 1 │ let Component = (props) => {props.data.map(d => - {d.text}
)}
;
+ 2 │
+ > 3 │ let Component = (props) => <>{props.data.map(d => {d.text})}>;
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 4 │
+ 5 │ let Component = (props) => {props.data.map(d => - {d.text}
)}
;
+
+ i Use Solid's `` component for efficiently rendering lists. See Solid docs for more details.
+
+
+```
+
+```
+invalid.tsx:5:33 lint/nursery/useForComponent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Array.prototype.map will cause DOM elements to be recreated, it is not recommended to use it in Solid here.
+
+ 3 │ let Component = (props) => <>{props.data.map(d => {d.text})}>;
+ 4 │
+ > 5 │ let Component = (props) => {props.data.map(d => - {d.text}
)}
;
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 6 │
+ 7 │ function Component(props) {
+
+ i Use Solid's `` component for efficiently rendering lists. See Solid docs for more details.
+
+
+```
+
+```
+invalid.tsx:8:15 lint/nursery/useForComponent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Array.prototype.map will cause DOM elements to be recreated, it is not recommended to use it in Solid here.
+
+ 7 │ function Component(props) {
+ > 8 │ return {props.data.map(d => - {d.text}
)}
;
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 9 │ }
+ 10 │
+
+ i Use Solid's `` component for efficiently rendering lists. See Solid docs for more details.
+
+
+```
+
+```
+invalid.tsx:12:15 lint/nursery/useForComponent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Array.prototype.map will cause DOM elements to be recreated, it is not recommended to use it in Solid here.
+
+ 11 │ function Component(props) {
+ > 12 │ return {props.data?.map(d => - {d.text}
)}
;
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 13 │ }
+ 14 │
+
+ i Use Solid's `` component for efficiently rendering lists. See Solid docs for more details.
+
+
+```
+
+```
+invalid.tsx:15:33 lint/nursery/useForComponent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Array.prototype.map will cause DOM elements to be recreated, it is not recommended to use it in Solid here.
+
+ 13 │ }
+ 14 │
+ > 15 │ let Component = (props) => {props.data.map(() => )}
;
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ 16 │
+ 17 │ let Component = (props) => {props.data.map((...args) => - {args[0].text}
)}
;
+
+ i Use Solid's `` component for efficiently rendering lists. See Solid docs for more details.
+
+
+```
+
+```
+invalid.tsx:17:33 lint/nursery/useForComponent ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ i Array.prototype.map will cause DOM elements to be recreated, it is not recommended to use it in Solid here.
+
+ 15 │ let Component = (props) => {props.data.map(() => )}
;
+ 16 │
+ > 17 │ let Component = (props) => {props.data.map((...args) => - {args[0].text}
)}
;
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ i Use Solid's `` component for efficiently rendering lists. See Solid docs for more details.
+
+
+```
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useForComponent/valid.tsx b/crates/biome_js_analyze/tests/specs/nursery/useForComponent/valid.tsx
new file mode 100644
index 000000000000..1ba43a993057
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useForComponent/valid.tsx
@@ -0,0 +1,8 @@
+let Component = (props) => {d => - {d.text}
}
;
+
+let abc = x.map(y => y + z);
+
+let Component = (props) => {
+ let abc = x.map(y => y + z);
+ return Hello, world!
;
+}
\ No newline at end of file
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useForComponent/valid.tsx.snap b/crates/biome_js_analyze/tests/specs/nursery/useForComponent/valid.tsx.snap
new file mode 100644
index 000000000000..6f6fedb75e61
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useForComponent/valid.tsx.snap
@@ -0,0 +1,16 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: valid.tsx
+snapshot_kind: text
+---
+# Input
+```tsx
+let Component = (props) => {d => - {d.text}
}
;
+
+let abc = x.map(y => y + z);
+
+let Component = (props) => {
+ let abc = x.map(y => y + z);
+ return Hello, world!
;
+}
+```
diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
index 52d31f4008a9..81c57f5b9242 100644
--- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts
+++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
@@ -1667,6 +1667,10 @@ export interface Nursery {
* Require that all exports are declared after all non-export statements.
*/
useExportsLast?: RuleConfiguration_for_Null;
+ /**
+ * Enforce using Solid's \ component for mapping an array to JSX elements.
+ */
+ useForComponent?: RuleConfiguration_for_Null;
/**
* Enforces the use of a recommended display strategy with Google Fonts.
*/
@@ -3159,6 +3163,7 @@ export type Category =
| "lint/nursery/noConsole"
| "lint/nursery/noConstantBinaryExpression"
| "lint/nursery/noDescendingSpecificity"
+ | "lint/nursery/noDestructuredProps"
| "lint/nursery/noDocumentCookie"
| "lint/nursery/noDocumentImportInPage"
| "lint/nursery/noDoneCallback"
@@ -3189,7 +3194,6 @@ export type Category =
| "lint/nursery/noPackagePrivateImports"
| "lint/nursery/noProcessEnv"
| "lint/nursery/noProcessGlobal"
- | "lint/nursery/noDestructuredProps"
| "lint/nursery/noReactSpecificProps"
| "lint/nursery/noRestrictedImports"
| "lint/nursery/noRestrictedTypes"
@@ -3238,6 +3242,7 @@ export type Category =
| "lint/nursery/useNamedOperation"
| "lint/nursery/useNamingConvention"
| "lint/nursery/useParseIntRadix"
+ | "lint/nursery/useForComponent"
| "lint/nursery/useSortedClasses"
| "lint/nursery/useSortedProperties"
| "lint/nursery/useStrictMode"
diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json
index c2c99ea70096..98b5afe4ac3c 100644
--- a/packages/@biomejs/biome/configuration_schema.json
+++ b/packages/@biomejs/biome/configuration_schema.json
@@ -2847,6 +2847,13 @@
{ "type": "null" }
]
},
+ "useForComponent": {
+ "description": "Enforce using Solid's \\ component for mapping an array to JSX elements.",
+ "anyOf": [
+ { "$ref": "#/definitions/RuleConfiguration" },
+ { "type": "null" }
+ ]
+ },
"useGoogleFontDisplay": {
"description": "Enforces the use of a recommended display strategy with Google Fonts.",
"anyOf": [