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 =>
  1. {d.text}
  2. )}
; "#; 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 =>
  1. {d.text}
  2. )}
; + /// ``` + /// + /// ```jsx,expect_diagnostic + /// let Component = (props) => <>{props.data.map(d =>
  • {d.text}
  • )}; + /// ``` + /// + /// ```jsx,expect_diagnostic + /// let Component = (props) => ( + ///
      + /// {props.data.map((d) => ( + ///
    1. {d.text}
    2. + /// ))} + ///
    + /// ); + /// ``` + /// + /// ### Valid + /// + /// ```jsx + /// let Component = (props) =>
      {d =>
    1. {d.text}
    2. }
    ; + /// ``` + /// + /// ```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 =>
    1. {d.text}
    2. )}
    + 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 =>
    1. {d.text}
    2. )}
    ; + +let Component = (props) => <>{props.data.map(d =>
  • {d.text}
  • )}; + +let Component = (props) =>
      {props.data.map(d =>
    1. {d.text}
    2. )}
    ; + +function Component(props) { + return
      {props.data.map(d =>
    1. {d.text}
    2. )}
    ; +} + +function Component(props) { + return
      {props.data?.map(d =>
    1. {d.text}
    2. )}
    ; +} + +let Component = (props) =>
      {props.data.map(() =>
    1. )}
    ; + +let Component = (props) =>
      {props.data.map((...args) =>
    1. {args[0].text}
    2. )}
    ; \ 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 =>
    1. {d.text}
    2. )}
    ; + +let Component = (props) => <>{props.data.map(d =>
  • {d.text}
  • )}; + +let Component = (props) =>
      {props.data.map(d =>
    1. {d.text}
    2. )}
    ; + +function Component(props) { + return
      {props.data.map(d =>
    1. {d.text}
    2. )}
    ; +} + +function Component(props) { + return
      {props.data?.map(d =>
    1. {d.text}
    2. )}
    ; +} + +let Component = (props) =>
      {props.data.map(() =>
    1. )}
    ; + +let Component = (props) =>
      {props.data.map((...args) =>
    1. {args[0].text}
    2. )}
    ; +``` + +# 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 =>
    1. {d.text}
    2. )}
    ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 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 =>
    1. {d.text}
    2. )}
    ; + 2 │ + > 3 │ let Component = (props) => <>{props.data.map(d =>
  • {d.text}
  • )}; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 4 │ + 5 │ let Component = (props) =>
      {props.data.map(d =>
    1. {d.text}
    2. )}
    ; + + 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 =>
    1. {d.text}
    2. )}
    ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 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 =>
    1. {d.text}
    2. )}
    ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 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 =>
    1. {d.text}
    2. )}
    ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 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(() =>
    1. )}
    ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 16 │ + 17 │ let Component = (props) =>
      {props.data.map((...args) =>
    1. {args[0].text}
    2. )}
    ; + + 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(() =>
    1. )}
    ; + 16 │ + > 17 │ let Component = (props) =>
      {props.data.map((...args) =>
    1. {args[0].text}
    2. )}
    ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + 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 =>
    1. {d.text}
    2. }
    ; + +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 =>
    1. {d.text}
    2. }
    ; + +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": [