From daf1ea2ff5fde0141ab1db1b5d27cf3e8c112bba Mon Sep 17 00:00:00 2001 From: Naoki Ikeguchi Date: Mon, 15 Sep 2025 19:12:15 +0900 Subject: [PATCH 1/5] feat(lint): add `noReactForwardRef` rule --- .changeset/calm-stars-rhyme.md | 5 + .../migrate/eslint_any_rule_to_biome.rs | 12 + .../src/analyzer/linter/rules.rs | 103 +++-- .../src/categories.rs | 3 +- crates/biome_js_analyze/src/lint/nursery.rs | 3 +- .../src/lint/nursery/no_react_forward_ref.rs | 424 ++++++++++++++++++ .../invalid-named-import.jsx | 15 + .../invalid-named-import.jsx.snap | 152 +++++++ .../invalid-named-import.tsx | 25 ++ .../invalid-named-import.tsx.snap | 202 +++++++++ .../nursery/noReactForwardRef/invalid.jsx | 23 + .../noReactForwardRef/invalid.jsx.snap | 228 ++++++++++ .../nursery/noReactForwardRef/invalid.tsx | 25 ++ .../noReactForwardRef/invalid.tsx.snap | 202 +++++++++ .../specs/nursery/noReactForwardRef/valid.jsx | 9 + .../nursery/noReactForwardRef/valid.jsx.snap | 17 + crates/biome_js_factory/src/make.rs | 20 + crates/biome_rule_options/src/lib.rs | 1 + .../src/no_react_forward_ref.rs | 6 + .../@biomejs/backend-jsonrpc/src/workspace.ts | 25 +- .../@biomejs/biome/configuration_schema.json | 36 ++ 21 files changed, 1492 insertions(+), 44 deletions(-) create mode 100644 .changeset/calm-stars-rhyme.md create mode 100644 crates/biome_js_analyze/src/lint/nursery/no_react_forward_ref.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.tsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.tsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.tsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.tsx.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx.snap create mode 100644 crates/biome_rule_options/src/no_react_forward_ref.rs diff --git a/.changeset/calm-stars-rhyme.md b/.changeset/calm-stars-rhyme.md new file mode 100644 index 000000000000..4c923c62406f --- /dev/null +++ b/.changeset/calm-stars-rhyme.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Added a new lint rule [`noReactForwardRef`](https://biomejs.dev/linter/rules/no-react-forward-ref/), which detects usages of `forwardRef` that is no longer needed and deprecated in React 19. 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 6bea3353b945..d087993a6d67 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 @@ -9,6 +9,18 @@ pub(crate) fn migrate_eslint_any_rule( results: &mut eslint_to_biome::MigrationResults, ) -> bool { match eslint_name { + "@eslint-react/no-forward-ref" => { + if !options.include_nursery { + results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery); + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .unwrap_group_as_mut() + .no_react_forward_ref + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "@eslint-react/no-nested-component-definitions" => { let group = rules.correctness.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 32dd8dedb522..7b65d5eb88f1 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -234,6 +234,7 @@ pub enum RuleName { NoQuickfixBiome, NoQwikUseVisibleTask, NoReExportAll, + NoReactForwardRef, NoReactPropAssignments, NoReactSpecificProps, NoRedeclare, @@ -596,6 +597,7 @@ impl RuleName { Self::NoQuickfixBiome => "noQuickfixBiome", Self::NoQwikUseVisibleTask => "noQwikUseVisibleTask", Self::NoReExportAll => "noReExportAll", + Self::NoReactForwardRef => "noReactForwardRef", Self::NoReactPropAssignments => "noReactPropAssignments", Self::NoReactSpecificProps => "noReactSpecificProps", Self::NoRedeclare => "noRedeclare", @@ -954,6 +956,7 @@ impl RuleName { Self::NoQuickfixBiome => RuleGroup::Suspicious, Self::NoQwikUseVisibleTask => RuleGroup::Nursery, Self::NoReExportAll => RuleGroup::Performance, + Self::NoReactForwardRef => RuleGroup::Nursery, Self::NoReactPropAssignments => RuleGroup::Correctness, Self::NoReactSpecificProps => RuleGroup::Suspicious, Self::NoRedeclare => RuleGroup::Suspicious, @@ -1321,6 +1324,7 @@ impl std::str::FromStr for RuleName { "noQuickfixBiome" => Ok(Self::NoQuickfixBiome), "noQwikUseVisibleTask" => Ok(Self::NoQwikUseVisibleTask), "noReExportAll" => Ok(Self::NoReExportAll), + "noReactForwardRef" => Ok(Self::NoReactForwardRef), "noReactPropAssignments" => Ok(Self::NoReactPropAssignments), "noReactSpecificProps" => Ok(Self::NoReactSpecificProps), "noRedeclare" => Ok(Self::NoRedeclare), @@ -4614,7 +4618,7 @@ impl From for Correctness { #[cfg_attr(feature = "schema", derive(JsonSchema))] #[serde(rename_all = "camelCase", default, deny_unknown_fields)] #[doc = r" A list of rules that belong to this group"] -pub struct Nursery { # [doc = r" Enables the recommended rules for this group"] # [serde (skip_serializing_if = "Option::is_none")] pub recommended : Option < bool > , # [doc = "Prevent the listing of duplicate dependencies. The rule supports the following dependency groups: \"bundledDependencies\", \"bundleDependencies\", \"dependencies\", \"devDependencies\", \"overrides\", \"optionalDependencies\", and \"peerDependencies\"."] # [serde (skip_serializing_if = "Option::is_none")] pub no_duplicate_dependencies : Option < RuleConfiguration < biome_rule_options :: no_duplicate_dependencies :: NoDuplicateDependenciesOptions >> , # [doc = "Require Promise-like statements to be handled appropriately."] # [serde (skip_serializing_if = "Option::is_none")] pub no_floating_promises : Option < RuleFixConfiguration < biome_rule_options :: no_floating_promises :: NoFloatingPromisesOptions >> , # [doc = "Prevent import cycles."] # [serde (skip_serializing_if = "Option::is_none")] pub no_import_cycles : Option < RuleConfiguration < biome_rule_options :: no_import_cycles :: NoImportCyclesOptions >> , # [doc = "Disallow string literals inside JSX elements."] # [serde (skip_serializing_if = "Option::is_none")] pub no_jsx_literals : Option < RuleConfiguration < biome_rule_options :: no_jsx_literals :: NoJsxLiteralsOptions >> , # [doc = "Disallow Promises to be used in places where they are almost certainly a mistake."] # [serde (skip_serializing_if = "Option::is_none")] pub no_misused_promises : Option < RuleFixConfiguration < biome_rule_options :: no_misused_promises :: NoMisusedPromisesOptions >> , # [doc = "Prevent client components from being async functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_next_async_client_component : Option < RuleConfiguration < biome_rule_options :: no_next_async_client_component :: NoNextAsyncClientComponentOptions >> , # [doc = "Disallow non-null assertions after optional chaining expressions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_non_null_asserted_optional_chain : Option < RuleConfiguration < biome_rule_options :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChainOptions >> , # [doc = "Disallow useVisibleTask$() functions in Qwik components."] # [serde (skip_serializing_if = "Option::is_none")] pub no_qwik_use_visible_task : Option < RuleConfiguration < biome_rule_options :: no_qwik_use_visible_task :: NoQwikUseVisibleTaskOptions >> , # [doc = "Disallow usage of sensitive data such as API keys and tokens."] # [serde (skip_serializing_if = "Option::is_none")] pub no_secrets : Option < RuleConfiguration < biome_rule_options :: no_secrets :: NoSecretsOptions >> , # [doc = "Disallow variable declarations from shadowing variables declared in the outer scope."] # [serde (skip_serializing_if = "Option::is_none")] pub no_shadow : Option < RuleConfiguration < biome_rule_options :: no_shadow :: NoShadowOptions >> , # [doc = "Disallow unnecessary type-based conditions that can be statically determined as redundant."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unnecessary_conditions : Option < RuleConfiguration < biome_rule_options :: no_unnecessary_conditions :: NoUnnecessaryConditionsOptions >> , # [doc = "Warn when importing non-existing exports."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unresolved_imports : Option < RuleConfiguration < biome_rule_options :: no_unresolved_imports :: NoUnresolvedImportsOptions >> , # [doc = "Disallow unused catch bindings."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_catch_binding : Option < RuleFixConfiguration < biome_rule_options :: no_useless_catch_binding :: NoUselessCatchBindingOptions >> , # [doc = "Disallow the use of useless undefined."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_undefined : Option < RuleFixConfiguration < biome_rule_options :: no_useless_undefined :: NoUselessUndefinedOptions >> , # [doc = "Enforce that Vue component data options are declared as functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_data_object_declaration : Option < RuleFixConfiguration < biome_rule_options :: no_vue_data_object_declaration :: NoVueDataObjectDeclarationOptions >> , # [doc = "Disallow reserved keys in Vue component data and computed properties."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_keys :: NoVueReservedKeysOptions >> , # [doc = "Disallow reserved names to be used as props."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_props : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_props :: NoVueReservedPropsOptions >> , # [doc = "Enforces href attribute for \\ elements."] # [serde (skip_serializing_if = "Option::is_none")] pub use_anchor_href : Option < RuleConfiguration < biome_rule_options :: use_anchor_href :: UseAnchorHrefOptions >> , # [doc = "Enforce consistent arrow function bodies."] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_arrow_return : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_arrow_return :: UseConsistentArrowReturnOptions >> , # [doc = "Enforce type definitions to consistently use either interface or type."] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_type_definitions : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_type_definitions :: UseConsistentTypeDefinitionsOptions >> , # [doc = "Require switch-case statements to be exhaustive."] # [serde (skip_serializing_if = "Option::is_none")] pub use_exhaustive_switch_cases : Option < RuleFixConfiguration < biome_rule_options :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCasesOptions >> , # [doc = "Enforce types in functions, methods, variables, and parameters."] # [serde (skip_serializing_if = "Option::is_none")] pub use_explicit_type : Option < RuleConfiguration < biome_rule_options :: use_explicit_type :: UseExplicitTypeOptions >> , # [doc = "Enforces that \\ elements have both width and height attributes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_image_size : Option < RuleConfiguration < biome_rule_options :: use_image_size :: UseImageSizeOptions >> , # [doc = "Enforce a maximum number of parameters in function definitions."] # [serde (skip_serializing_if = "Option::is_none")] pub use_max_params : Option < RuleConfiguration < biome_rule_options :: use_max_params :: UseMaxParamsOptions >> , # [doc = "Prefer using the class prop as a classlist over the classnames helper."] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_classlist : Option < RuleConfiguration < biome_rule_options :: use_qwik_classlist :: UseQwikClasslistOptions >> , # [doc = "Enforce that components are defined as functions and never as classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_react_function_components : Option < RuleConfiguration < biome_rule_options :: use_react_function_components :: UseReactFunctionComponentsOptions >> , # [doc = "Enforce the sorting of CSS utility classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_sorted_classes : Option < RuleFixConfiguration < biome_rule_options :: use_sorted_classes :: UseSortedClassesOptions >> , # [doc = "Enforce multi-word component names in Vue components."] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_multi_word_component_names : Option < RuleConfiguration < biome_rule_options :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNamesOptions >> } +pub struct Nursery { # [doc = r" Enables the recommended rules for this group"] # [serde (skip_serializing_if = "Option::is_none")] pub recommended : Option < bool > , # [doc = "Prevent the listing of duplicate dependencies. The rule supports the following dependency groups: \"bundledDependencies\", \"bundleDependencies\", \"dependencies\", \"devDependencies\", \"overrides\", \"optionalDependencies\", and \"peerDependencies\"."] # [serde (skip_serializing_if = "Option::is_none")] pub no_duplicate_dependencies : Option < RuleConfiguration < biome_rule_options :: no_duplicate_dependencies :: NoDuplicateDependenciesOptions >> , # [doc = "Require Promise-like statements to be handled appropriately."] # [serde (skip_serializing_if = "Option::is_none")] pub no_floating_promises : Option < RuleFixConfiguration < biome_rule_options :: no_floating_promises :: NoFloatingPromisesOptions >> , # [doc = "Prevent import cycles."] # [serde (skip_serializing_if = "Option::is_none")] pub no_import_cycles : Option < RuleConfiguration < biome_rule_options :: no_import_cycles :: NoImportCyclesOptions >> , # [doc = "Disallow string literals inside JSX elements."] # [serde (skip_serializing_if = "Option::is_none")] pub no_jsx_literals : Option < RuleConfiguration < biome_rule_options :: no_jsx_literals :: NoJsxLiteralsOptions >> , # [doc = "Disallow Promises to be used in places where they are almost certainly a mistake."] # [serde (skip_serializing_if = "Option::is_none")] pub no_misused_promises : Option < RuleFixConfiguration < biome_rule_options :: no_misused_promises :: NoMisusedPromisesOptions >> , # [doc = "Prevent client components from being async functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_next_async_client_component : Option < RuleConfiguration < biome_rule_options :: no_next_async_client_component :: NoNextAsyncClientComponentOptions >> , # [doc = "Disallow non-null assertions after optional chaining expressions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_non_null_asserted_optional_chain : Option < RuleConfiguration < biome_rule_options :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChainOptions >> , # [doc = "Disallow useVisibleTask$() functions in Qwik components."] # [serde (skip_serializing_if = "Option::is_none")] pub no_qwik_use_visible_task : Option < RuleConfiguration < biome_rule_options :: no_qwik_use_visible_task :: NoQwikUseVisibleTaskOptions >> , # [doc = "Replaces usages of forwardRef with passing ref as a prop."] # [serde (skip_serializing_if = "Option::is_none")] pub no_react_forward_ref : Option < RuleFixConfiguration < biome_rule_options :: no_react_forward_ref :: NoReactForwardRefOptions >> , # [doc = "Disallow usage of sensitive data such as API keys and tokens."] # [serde (skip_serializing_if = "Option::is_none")] pub no_secrets : Option < RuleConfiguration < biome_rule_options :: no_secrets :: NoSecretsOptions >> , # [doc = "Disallow variable declarations from shadowing variables declared in the outer scope."] # [serde (skip_serializing_if = "Option::is_none")] pub no_shadow : Option < RuleConfiguration < biome_rule_options :: no_shadow :: NoShadowOptions >> , # [doc = "Disallow unnecessary type-based conditions that can be statically determined as redundant."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unnecessary_conditions : Option < RuleConfiguration < biome_rule_options :: no_unnecessary_conditions :: NoUnnecessaryConditionsOptions >> , # [doc = "Warn when importing non-existing exports."] # [serde (skip_serializing_if = "Option::is_none")] pub no_unresolved_imports : Option < RuleConfiguration < biome_rule_options :: no_unresolved_imports :: NoUnresolvedImportsOptions >> , # [doc = "Disallow unused catch bindings."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_catch_binding : Option < RuleFixConfiguration < biome_rule_options :: no_useless_catch_binding :: NoUselessCatchBindingOptions >> , # [doc = "Disallow the use of useless undefined."] # [serde (skip_serializing_if = "Option::is_none")] pub no_useless_undefined : Option < RuleFixConfiguration < biome_rule_options :: no_useless_undefined :: NoUselessUndefinedOptions >> , # [doc = "Enforce that Vue component data options are declared as functions."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_data_object_declaration : Option < RuleFixConfiguration < biome_rule_options :: no_vue_data_object_declaration :: NoVueDataObjectDeclarationOptions >> , # [doc = "Disallow reserved keys in Vue component data and computed properties."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_keys :: NoVueReservedKeysOptions >> , # [doc = "Disallow reserved names to be used as props."] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_props : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_props :: NoVueReservedPropsOptions >> , # [doc = "Enforces href attribute for \\ elements."] # [serde (skip_serializing_if = "Option::is_none")] pub use_anchor_href : Option < RuleConfiguration < biome_rule_options :: use_anchor_href :: UseAnchorHrefOptions >> , # [doc = "Enforce consistent arrow function bodies."] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_arrow_return : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_arrow_return :: UseConsistentArrowReturnOptions >> , # [doc = "Enforce type definitions to consistently use either interface or type."] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_type_definitions : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_type_definitions :: UseConsistentTypeDefinitionsOptions >> , # [doc = "Require switch-case statements to be exhaustive."] # [serde (skip_serializing_if = "Option::is_none")] pub use_exhaustive_switch_cases : Option < RuleFixConfiguration < biome_rule_options :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCasesOptions >> , # [doc = "Enforce types in functions, methods, variables, and parameters."] # [serde (skip_serializing_if = "Option::is_none")] pub use_explicit_type : Option < RuleConfiguration < biome_rule_options :: use_explicit_type :: UseExplicitTypeOptions >> , # [doc = "Enforces that \\ elements have both width and height attributes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_image_size : Option < RuleConfiguration < biome_rule_options :: use_image_size :: UseImageSizeOptions >> , # [doc = "Enforce a maximum number of parameters in function definitions."] # [serde (skip_serializing_if = "Option::is_none")] pub use_max_params : Option < RuleConfiguration < biome_rule_options :: use_max_params :: UseMaxParamsOptions >> , # [doc = "Prefer using the class prop as a classlist over the classnames helper."] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_classlist : Option < RuleConfiguration < biome_rule_options :: use_qwik_classlist :: UseQwikClasslistOptions >> , # [doc = "Enforce that components are defined as functions and never as classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_react_function_components : Option < RuleConfiguration < biome_rule_options :: use_react_function_components :: UseReactFunctionComponentsOptions >> , # [doc = "Enforce the sorting of CSS utility classes."] # [serde (skip_serializing_if = "Option::is_none")] pub use_sorted_classes : Option < RuleFixConfiguration < biome_rule_options :: use_sorted_classes :: UseSortedClassesOptions >> , # [doc = "Enforce multi-word component names in Vue components."] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_multi_word_component_names : Option < RuleConfiguration < biome_rule_options :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNamesOptions >> } impl Nursery { const GROUP_NAME: &'static str = "nursery"; pub(crate) const GROUP_RULES: &'static [&'static str] = &[ @@ -4626,6 +4630,7 @@ impl Nursery { "noNextAsyncClientComponent", "noNonNullAssertedOptionalChain", "noQwikUseVisibleTask", + "noReactForwardRef", "noSecrets", "noShadow", "noUnnecessaryConditions", @@ -4678,6 +4683,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]), ]; } impl RuleGroupExt for Nursery { @@ -4729,106 +4735,111 @@ impl RuleGroupExt for Nursery { { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } - if let Some(rule) = self.no_secrets.as_ref() + if let Some(rule) = self.no_react_forward_ref.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } - if let Some(rule) = self.no_shadow.as_ref() + if let Some(rule) = self.no_secrets.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } - if let Some(rule) = self.no_unnecessary_conditions.as_ref() + if let Some(rule) = self.no_shadow.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } - if let Some(rule) = self.no_unresolved_imports.as_ref() + if let Some(rule) = self.no_unnecessary_conditions.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } - if let Some(rule) = self.no_useless_catch_binding.as_ref() + if let Some(rule) = self.no_unresolved_imports.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } - if let Some(rule) = self.no_useless_undefined.as_ref() + if let Some(rule) = self.no_useless_catch_binding.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } - if let Some(rule) = self.no_vue_data_object_declaration.as_ref() + if let Some(rule) = self.no_useless_undefined.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } - if let Some(rule) = self.no_vue_reserved_keys.as_ref() + if let Some(rule) = self.no_vue_data_object_declaration.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } - if let Some(rule) = self.no_vue_reserved_props.as_ref() + if let Some(rule) = self.no_vue_reserved_keys.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } - if let Some(rule) = self.use_anchor_href.as_ref() + if let Some(rule) = self.no_vue_reserved_props.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } - if let Some(rule) = self.use_consistent_arrow_return.as_ref() + if let Some(rule) = self.use_anchor_href.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } - if let Some(rule) = self.use_consistent_type_definitions.as_ref() + if let Some(rule) = self.use_consistent_arrow_return.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } - if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() + if let Some(rule) = self.use_consistent_type_definitions.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } - if let Some(rule) = self.use_explicit_type.as_ref() + if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } - if let Some(rule) = self.use_image_size.as_ref() + if let Some(rule) = self.use_explicit_type.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } - if let Some(rule) = self.use_max_params.as_ref() + if let Some(rule) = self.use_image_size.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } - if let Some(rule) = self.use_qwik_classlist.as_ref() + if let Some(rule) = self.use_max_params.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } - if let Some(rule) = self.use_react_function_components.as_ref() + if let Some(rule) = self.use_qwik_classlist.as_ref() && 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 let Some(rule) = self.use_react_function_components.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } - if let Some(rule) = self.use_vue_multi_word_component_names.as_ref() + if let Some(rule) = self.use_sorted_classes.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } + if let Some(rule) = self.use_vue_multi_word_component_names.as_ref() + && rule.is_enabled() + { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); + } index_set } fn get_disabled_rules(&self) -> FxHashSet> { @@ -4873,106 +4884,111 @@ impl RuleGroupExt for Nursery { { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } - if let Some(rule) = self.no_secrets.as_ref() + if let Some(rule) = self.no_react_forward_ref.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } - if let Some(rule) = self.no_shadow.as_ref() + if let Some(rule) = self.no_secrets.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } - if let Some(rule) = self.no_unnecessary_conditions.as_ref() + if let Some(rule) = self.no_shadow.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } - if let Some(rule) = self.no_unresolved_imports.as_ref() + if let Some(rule) = self.no_unnecessary_conditions.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } - if let Some(rule) = self.no_useless_catch_binding.as_ref() + if let Some(rule) = self.no_unresolved_imports.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } - if let Some(rule) = self.no_useless_undefined.as_ref() + if let Some(rule) = self.no_useless_catch_binding.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } - if let Some(rule) = self.no_vue_data_object_declaration.as_ref() + if let Some(rule) = self.no_useless_undefined.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } - if let Some(rule) = self.no_vue_reserved_keys.as_ref() + if let Some(rule) = self.no_vue_data_object_declaration.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } - if let Some(rule) = self.no_vue_reserved_props.as_ref() + if let Some(rule) = self.no_vue_reserved_keys.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } - if let Some(rule) = self.use_anchor_href.as_ref() + if let Some(rule) = self.no_vue_reserved_props.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } - if let Some(rule) = self.use_consistent_arrow_return.as_ref() + if let Some(rule) = self.use_anchor_href.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } - if let Some(rule) = self.use_consistent_type_definitions.as_ref() + if let Some(rule) = self.use_consistent_arrow_return.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } - if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() + if let Some(rule) = self.use_consistent_type_definitions.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } - if let Some(rule) = self.use_explicit_type.as_ref() + if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } - if let Some(rule) = self.use_image_size.as_ref() + if let Some(rule) = self.use_explicit_type.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } - if let Some(rule) = self.use_max_params.as_ref() + if let Some(rule) = self.use_image_size.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } - if let Some(rule) = self.use_qwik_classlist.as_ref() + if let Some(rule) = self.use_max_params.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } - if let Some(rule) = self.use_react_function_components.as_ref() + if let Some(rule) = self.use_qwik_classlist.as_ref() && 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 let Some(rule) = self.use_react_function_components.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[26])); } - if let Some(rule) = self.use_vue_multi_word_component_names.as_ref() + if let Some(rule) = self.use_sorted_classes.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } + if let Some(rule) = self.use_vue_multi_word_component_names.as_ref() + && rule.is_disabled() + { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -5035,6 +5051,10 @@ impl RuleGroupExt for Nursery { .no_qwik_use_visible_task .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noReactForwardRef" => self + .no_react_forward_ref + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noSecrets" => self .no_secrets .as_ref() @@ -5131,6 +5151,7 @@ impl From for Nursery { no_next_async_client_component: Some(value.into()), no_non_null_asserted_optional_chain: Some(value.into()), no_qwik_use_visible_task: Some(value.into()), + no_react_forward_ref: Some(value.into()), no_secrets: Some(value.into()), no_shadow: Some(value.into()), no_unnecessary_conditions: Some(value.into()), diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 24ebd30ccbc0..f51054e8f98a 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -162,6 +162,7 @@ define_categories! { "lint/correctness/useValidTypeof": "https://biomejs.dev/linter/rules/use-valid-typeof", "lint/correctness/useYield": "https://biomejs.dev/linter/rules/use-yield", "lint/nursery/noColorInvalidHex": "https://biomejs.dev/linter/rules/no-color-invalid-hex", + "lint/nursery/noDuplicateDependencies": "https://biomejs.dev/linter/rules/no-duplicate-dependencies", "lint/nursery/noFloatingPromises": "https://biomejs.dev/linter/rules/no-floating-promises", "lint/nursery/noImplicitCoercion": "https://biomejs.dev/linter/rules/no-implicit-coercion", "lint/nursery/noImportCycles": "https://biomejs.dev/linter/rules/no-import-cycles", @@ -171,6 +172,7 @@ define_categories! { "lint/nursery/noNextAsyncClientComponent": "https://biomejs.dev/linter/rules/no-next-async-client-component", "lint/nursery/noNonNullAssertedOptionalChain": "https://biomejs.dev/linter/rules/no-non-null-asserted-optional-chain", "lint/nursery/noQwikUseVisibleTask": "https://biomejs.dev/linter/rules/no-qwik-use-visible-task", + "lint/nursery/noReactForwardRef": "https://biomejs.dev/linter/rules/no-react-forward-ref", "lint/nursery/noSecrets": "https://biomejs.dev/linter/rules/no-secrets", "lint/nursery/noShadow": "https://biomejs.dev/linter/rules/no-shadow", "lint/nursery/noUnnecessaryConditions": "https://biomejs.dev/linter/rules/no-unnecessary-conditions", @@ -312,7 +314,6 @@ define_categories! { "lint/suspicious/noDuplicateCase": "https://biomejs.dev/linter/rules/no-duplicate-case", "lint/suspicious/noDuplicateClassMembers": "https://biomejs.dev/linter/rules/no-duplicate-class-members", "lint/suspicious/noDuplicateCustomProperties": "https://biomejs.dev/linter/rules/no-duplicate-custom-properties", - "lint/nursery/noDuplicateDependencies": "https://biomejs.dev/linter/rules/no-duplicate-dependencies", "lint/suspicious/noDuplicateElseIf": "https://biomejs.dev/linter/rules/no-duplicate-else-if", "lint/suspicious/noDuplicateFields": "https://biomejs.dev/linter/rules/no-duplicate-fields", "lint/suspicious/noDuplicateFontNames": "https://biomejs.dev/linter/rules/no-font-family-duplicate-names", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index a0e7396fa7e2..7ebe1752eb25 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -10,6 +10,7 @@ pub mod no_misused_promises; pub mod no_next_async_client_component; pub mod no_non_null_asserted_optional_chain; pub mod no_qwik_use_visible_task; +pub mod no_react_forward_ref; pub mod no_secrets; pub mod no_shadow; pub mod no_unnecessary_conditions; @@ -30,4 +31,4 @@ pub mod use_qwik_classlist; pub mod use_react_function_components; pub mod use_sorted_classes; pub mod use_vue_multi_word_component_names; -declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } } +declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } } diff --git a/crates/biome_js_analyze/src/lint/nursery/no_react_forward_ref.rs b/crates/biome_js_analyze/src/lint/nursery/no_react_forward_ref.rs new file mode 100644 index 000000000000..d1733f92d8d7 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_react_forward_ref.rs @@ -0,0 +1,424 @@ +use biome_analyze::{ + FixKind, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule, +}; +use biome_console::markup; +use biome_diagnostics::Severity; +use biome_js_factory::make; +use biome_js_syntax::{ + AnyJsArrowFunctionParameters, AnyJsBinding, AnyJsBindingPattern, AnyJsCallArgument, + AnyJsExpression, AnyJsFormalParameter, AnyJsObjectBindingPatternMember, AnyJsParameter, + AnyTsType, AnyTsTypeMember, JsCallExpression, JsIdentifierBinding, JsParameters, T, + TsPropertySignatureTypeMember, TsReferenceType, +}; +use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt}; +use biome_rule_options::no_react_forward_ref::NoReactForwardRefOptions; + +use crate::JsRuleAction; +use crate::react::{ReactLibrary, is_global_react_import, is_react_call_api}; +use crate::services::semantic::Semantic; + +declare_lint_rule! { + /// Replaces usages of `forwardRef` with passing `ref` as a prop. + /// + /// In React 19, `forwardRef` is no longer necessary. Pass `ref` as a prop instead. + /// `forwardRef` will be deprecated in a future release. + /// See [the official blog post](https://react.dev/blog/2024/12/05/react-19#ref-as-a-prop) for details. + /// + /// This rule should be disabled if you are working with React 18 or earlier. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```jsx,expect_diagnostic + /// import { forwardRef } from "react"; + /// + /// const MyInput = forwardRef(function MyInput(props, ref) { + /// return ; + /// }); + /// ``` + /// + /// ```jsx,expect_diagnostic + /// import { forwardRef } from "react"; + /// + /// const MyInput = forwardRef((props, ref) => { + /// return ; + /// }); + /// ``` + /// + /// ### Valid + /// + /// ```jsx + /// function MyInput({ ref, ...props }) { + /// return ; + /// } + /// ``` + /// + /// ```jsx + /// const MyInput = ({ ref, ...props }) => { + /// return ; + /// } + /// ``` + /// + pub NoReactForwardRef { + version: "next", + name: "noReactForwardRef", + language: "js", + severity: Severity::Warning, + domains: &[RuleDomain::React], + sources: &[RuleSource::EslintReactXyz("no-forward-ref").same()], + recommended: false, + fix_kind: FixKind::Unsafe, + } +} + +impl Rule for NoReactForwardRef { + type Query = Semantic; + type State = (); + type Signals = Option; + type Options = NoReactForwardRefOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + let model = ctx.model(); + let callee = node.callee().ok()?; + + is_react_call_api(&callee, model, ReactLibrary::React, "forwardRef").then_some(()) + } + + fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { + let node = ctx.query(); + + Some( + RuleDiagnostic::new( + rule_category!(), + node.range(), + markup! { + "Replaces usages of ""forwardRef"" with passing ""ref"" as a prop." + }, + ) + .note(markup! { + "In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead." + }) + .note(markup! { + "Consider disabling this rule if you are working with React 18 or earlier." + }), + ) + } + + fn action(ctx: &RuleContext, _state: &Self::State) -> Option { + let node = ctx.query(); + let model = ctx.model(); + let mut mutation = ctx.root().begin(); + + let AnyJsCallArgument::AnyJsExpression(function) = + node.arguments().ok()?.args().first()?.ok()? + else { + return None; + }; + + let type_args = match node.type_arguments() { + Some(p) => { + let mut iter = p.ts_type_argument_list().into_iter(); + let ref_type = iter.next().and_then(|node| node.ok()); + let props_type = iter.next().and_then(|node| node.ok()); + + (ref_type, props_type) + } + _ => (None, None), + }; + + let global_react_import = model + .all_bindings() + .filter_map(|binding| JsIdentifierBinding::cast_ref(binding.syntax())) + .find(|binding| is_global_react_import(binding, ReactLibrary::React)); + + let new_function: AnyJsExpression = match function.clone() { + AnyJsExpression::JsArrowFunctionExpression(f) => { + let AnyJsArrowFunctionParameters::JsParameters(params) = f.parameters().ok()? + else { + return None; + }; + + f.with_parameters(fix_parameters(params, type_args, global_react_import)?.into()) + .into() + } + AnyJsExpression::JsFunctionExpression(f) => { + let params = f.parameters().ok()?; + + f.with_parameters(fix_parameters(params, type_args, global_react_import)?) + .into() + } + _ => return None, + }; + + mutation.replace_node(node.clone().into(), new_function); + + Some(JsRuleAction::new( + ctx.metadata().action_category(ctx.category(), ctx.group()), + ctx.metadata().applicability(), + markup! { "Remove the ""forwardRef()"" call and receive the ""ref"" as a prop." }, + mutation, + )) + } +} + +fn fix_parameters( + params: JsParameters, + type_args: (Option, Option), + global_react_import: Option, +) -> Option { + let mut iter = params.items().into_iter(); + let AnyJsParameter::AnyJsFormalParameter(AnyJsFormalParameter::JsFormalParameter(props)) = + iter.next()?.ok()? + else { + return None; + }; + + let mut new_props = props.clone(); + + // If the forwarded ref is used in the function, add it into the first parameter + if let Some(AnyJsParameter::AnyJsFormalParameter(AnyJsFormalParameter::JsFormalParameter( + r#ref, + ))) = iter.next().and_then(|node| node.ok()) + { + let mut new_ref = match r#ref.binding().ok()? { + // (props, ref) => ({ ref, ...props }) + AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding(b)) + if b.name_token().ok()?.text_trimmed() == "ref" => + { + make::js_object_binding_pattern_shorthand_property(b.into()) + .build() + .into() + } + // (props, myRef) => ({ ref: myRef, ...props }) + // (props, { current }) => ({ ref: { current }, ...props }) + _ => make::js_object_binding_pattern_property( + make::js_literal_member_name(make::ident("ref")).into(), + make::token_with_trailing_space(T![:]), + r#ref.binding().ok()?, + ) + .build() + .into(), + }; + + match props.binding().ok()? { + // (props, ref) => ({ ref, ...props }) + AnyJsBindingPattern::AnyJsBinding(AnyJsBinding::JsIdentifierBinding(binding)) => { + let properties = make::js_object_binding_pattern_property_list( + [ + new_ref, + make::js_object_binding_pattern_rest(make::token(T![...]), binding.into()) + .into(), + ], + [make::token_with_trailing_space(T![,])], + ); + + new_props = new_props.with_binding( + make::js_object_binding_pattern( + make::token_with_trailing_space(T!['{']), + properties, + make::token_with_leading_space(T!['}']), + ) + .into(), + ); + } + + // ({ foo, bar }, ref) => ({ foo, bar, ref }) + AnyJsBindingPattern::JsObjectBindingPattern(binding) => { + let rest = + binding + .properties() + .into_iter() + .find_map(|member| match member.ok()? { + AnyJsObjectBindingPatternMember::JsObjectBindingPatternRest(rest) => { + Some(rest) + } + _ => None, + }); + + let mut properties = binding + .properties() + .into_iter() + .filter_map(|member| member.ok()) + .filter(|member| { + !matches!( + member, + AnyJsObjectBindingPatternMember::JsObjectBindingPatternRest(_) + ) + }) + .collect::>(); + + // Transfer the trailing trivia of the last property to the new `ref` property. + if rest.is_none() + && let Some(p) = properties.last_mut() + && let Some(trivia) = p.syntax().last_trailing_trivia() + { + new_ref = new_ref.with_trailing_trivia_pieces(trivia.pieces())?; + *p = p.clone().with_trailing_trivia_pieces([])?; + } + + // Add the `ref` property just before the rest property. + properties.push(new_ref); + + if let Some(rest) = rest { + properties.push(rest.into()); + } + + let mut separators = binding + .properties() + .separators() + .filter_map(|sep| sep.ok()) + .collect::>(); + + separators.push( + separators + .last() + .cloned() + .unwrap_or_else(|| make::token_with_trailing_space(T![,])), + ); + + new_props = new_props.with_binding( + binding + .with_properties(make::js_object_binding_pattern_property_list( + properties, separators, + )) + .into(), + ) + } + + // Not a valid function component? Nothing to do. + _ => return None, + } + } + + // Add a type annotation to the function based on the type arguments provided for forwardRef. + if let (Some(ref_type), Some(props_type)) = type_args + && props.type_annotation().is_none() + { + let new_props_type: AnyTsType = match props_type { + // If the props is annotated with an object type, try to add a new property for the ref. + AnyTsType::TsObjectType(ty) => { + let mut members = ty.members().into_iter().collect::>(); + let mut trailing_trivia = None; + + if let Some(AnyTsTypeMember::TsPropertySignatureTypeMember(mut member)) = + members.pop() + { + // Detach the trailing trivia of the last property to be moved later. + trailing_trivia = member.syntax().last_trailing_trivia(); + member = member.with_trailing_trivia_pieces([])?; + + if member.separator_token().is_none() { + member = member.with_separator_token_token(Some( + make::token_with_trailing_space(T![,]), + )); + } + + members.push(member.into()); + } + + let mut new_member = make_ref_property_member(ref_type, global_react_import)?; + if let Some(trivia) = trailing_trivia { + // Attach the removed trailing trivia above. + new_member = new_member.with_trailing_trivia_pieces(trivia.pieces())?; + } + + members.push(new_member.into()); + + ty.with_members(make::ts_type_member_list(members)).into() + } + + // Otherwise, create an intersection type with the props type and the ref type. + ty => make::ts_intersection_type(make::ts_intersection_type_element_list( + [ + ty, + make::ts_object_type( + make::token_with_trailing_space(T!['{']), + make::ts_type_member_list([make_ref_property_member( + ref_type, + global_react_import, + )? + .into()]), + make::token_with_leading_space(T!['}']), + ) + .into(), + ], + [make::token_decorated_with_space(T![&])], + )) + .build() + .into(), + }; + + new_props = new_props.with_type_annotation(Some(make::ts_type_annotation( + make::token_with_trailing_space(T![:]), + new_props_type, + ))); + } + + Some(params.with_items(make::js_parameter_list( + [AnyJsParameter::AnyJsFormalParameter(new_props.into())], + [], + ))) +} + +/// Make a property type member for the `ref` property with a type annotation. +fn make_ref_property_member( + ty: AnyTsType, + global_react_import: Option, +) -> Option { + Some( + make::ts_property_signature_type_member( + make::js_literal_member_name(make::ident("ref")).into(), + ) + .with_optional_token(make::token(T![?])) + .with_type_annotation(make::ts_type_annotation( + make::token_with_trailing_space(T![:]), + make_ref_object_type(ty, global_react_import)?.into(), + )) + .build(), + ) +} + +/// Make a `React.RefObject` type where `T` is the specified type. +fn make_ref_object_type( + ty: AnyTsType, + global_react_import: Option, +) -> Option { + // If `React` is imported globally, prefer using it. + let reference_type = if let Some(binding) = global_react_import { + let react = binding + .name_token() + .ok()? + .with_leading_trivia_pieces([]) + .with_trailing_trivia_pieces([]); + + make::ts_qualified_name( + make::js_reference_identifier(react).into(), + make::token(T![.]), + make::js_name(make::ident("RefObject")), + ) + .into() + } else { + // TODO: Automatically adding the symbol to the import would be nice + make::js_reference_identifier(make::ident("RefObject")).into() + }; + + Some( + make::ts_reference_type(reference_type) + .with_type_arguments(make::ts_type_arguments( + make::token(T![<]), + make::ts_type_argument_list( + [make::ts_union_type(make::ts_union_type_variant_list( + [ty, make::ts_null_literal_type(make::token(T![null])).into()], + [make::token_decorated_with_space(T![|])], + )) + .build() + .into()], + [], + ), + make::token(T![>]), + )) + .build(), + ) +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx new file mode 100644 index 000000000000..63ec7790ad53 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx @@ -0,0 +1,15 @@ +import { forwardRef } from "react"; + +const Component1 = forwardRef((props, ref) => { + return null; +}); + +const Component2 = forwardRef((props, ref) => null); + +const Component3 = forwardRef(function (props, ref) { + return null; +}); + +const Component4 = forwardRef(function Component(props, ref) { + return null; +}); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx.snap new file mode 100644 index 000000000000..2edac0f383ea --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx.snap @@ -0,0 +1,152 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid-named-import.jsx +--- +# Input +```jsx +import { forwardRef } from "react"; + +const Component1 = forwardRef((props, ref) => { + return null; +}); + +const Component2 = forwardRef((props, ref) => null); + +const Component3 = forwardRef(function (props, ref) { + return null; +}); + +const Component4 = forwardRef(function Component(props, ref) { + return null; +}); + +``` + +# Diagnostics +``` +invalid-named-import.jsx:3:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 1 │ import { forwardRef } from "react"; + 2 │ + > 3 │ const Component1 = forwardRef((props, ref) => { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 4 │ return null; + > 5 │ }); + │ ^^ + 6 │ + 7 │ const Component2 = forwardRef((props, ref) => null); + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 1 1 │ import { forwardRef } from "react"; + 2 2 │ + 3 │ - const·Component1·=·forwardRef((props,·ref)·=>·{ + 3 │ + const·Component1·=·({·ref,·...props·})·=>·{ + 4 4 │ return null; + 5 │ - }); + 5 │ + }; + 6 6 │ + 7 7 │ const Component2 = forwardRef((props, ref) => null); + + +``` + +``` +invalid-named-import.jsx:7:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 5 │ }); + 6 │ + > 7 │ const Component2 = forwardRef((props, ref) => null); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ + 9 │ const Component3 = forwardRef(function (props, ref) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 5 5 │ }); + 6 6 │ + 7 │ - const·Component2·=·forwardRef((props,·ref)·=>·null); + 7 │ + const·Component2·=·({·ref,·...props·})·=>·null; + 8 8 │ + 9 9 │ const Component3 = forwardRef(function (props, ref) { + + +``` + +``` +invalid-named-import.jsx:9:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 7 │ const Component2 = forwardRef((props, ref) => null); + 8 │ + > 9 │ const Component3 = forwardRef(function (props, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 10 │ return null; + > 11 │ }); + │ ^^ + 12 │ + 13 │ const Component4 = forwardRef(function Component(props, ref) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 7 7 │ const Component2 = forwardRef((props, ref) => null); + 8 8 │ + 9 │ - const·Component3·=·forwardRef(function·(props,·ref)·{ + 9 │ + const·Component3·=·function·({·ref,·...props·})·{ + 10 10 │ return null; + 11 │ - }); + 11 │ + }; + 12 12 │ + 13 13 │ const Component4 = forwardRef(function Component(props, ref) { + + +``` + +``` +invalid-named-import.jsx:13:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 11 │ }); + 12 │ + > 13 │ const Component4 = forwardRef(function Component(props, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 14 │ return null; + > 15 │ }); + │ ^^ + 16 │ + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 11 11 │ }); + 12 12 │ + 13 │ - const·Component4·=·forwardRef(function·Component(props,·ref)·{ + 13 │ + const·Component4·=·function·Component({·ref,·...props·})·{ + 14 14 │ return null; + 15 │ - }); + 15 │ + }; + 16 16 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.tsx b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.tsx new file mode 100644 index 000000000000..c818a619c007 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.tsx @@ -0,0 +1,25 @@ +import { forwardRef } from "react"; + +interface ComponentProps { + foo: string; +} + +const Component1 = forwardRef(function Component(props, ref) { + return
; +}); + +const Component2 = forwardRef(function Component(props, ref) { + return
{props.foo}
; +}); + +const Component3 = forwardRef(function Component({ foo }, ref) { + return
{foo}
; +}); + +const Component4 = forwardRef(function Component({ foo }, r) { + return
{foo}
; +}); + +const Component5 = forwardRef(function Component({ foo, ...rest }, r) { + return
{foo}
; +}); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.tsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.tsx.snap new file mode 100644 index 000000000000..d688e2521ac3 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.tsx.snap @@ -0,0 +1,202 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid-named-import.tsx +--- +# Input +```tsx +import { forwardRef } from "react"; + +interface ComponentProps { + foo: string; +} + +const Component1 = forwardRef(function Component(props, ref) { + return
; +}); + +const Component2 = forwardRef(function Component(props, ref) { + return
{props.foo}
; +}); + +const Component3 = forwardRef(function Component({ foo }, ref) { + return
{foo}
; +}); + +const Component4 = forwardRef(function Component({ foo }, r) { + return
{foo}
; +}); + +const Component5 = forwardRef(function Component({ foo, ...rest }, r) { + return
{foo}
; +}); + +``` + +# Diagnostics +``` +invalid-named-import.tsx:7:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 5 │ } + 6 │ + > 7 │ const Component1 = forwardRef(function Component(props, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 8 │ return
; + > 9 │ }); + │ ^^ + 10 │ + 11 │ const Component2 = forwardRef(function Component(props, ref) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 5 5 │ } + 6 6 │ + 7 │ - const·Component1·=·forwardRef(function·Component(props,·ref)·{ + 7 │ + const·Component1·=·function·Component({·ref,·...props·}:·ComponentProps·&·{·ref?:·RefObject·})·{ + 8 8 │ return
; + 9 │ - }); + 9 │ + }; + 10 10 │ + 11 11 │ const Component2 = forwardRef(function Component(props, ref) { + + +``` + +``` +invalid-named-import.tsx:11:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 9 │ }); + 10 │ + > 11 │ const Component2 = forwardRef(function Component(props, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 12 │ return
{props.foo}
; + > 13 │ }); + │ ^^ + 14 │ + 15 │ const Component3 = forwardRef(function Component({ foo }, ref) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 9 9 │ }); + 10 10 │ + 11 │ - const·Component2·=·forwardRef(function·Component(props,·ref)·{ + 11 │ + const·Component2·=·function·Component({·ref,·...props·}:·{·foo:·string,·ref?:·RefObject·})·{ + 12 12 │ return
{props.foo}
; + 13 │ - }); + 13 │ + }; + 14 14 │ + 15 15 │ const Component3 = forwardRef(function Component({ foo }, ref) { + + +``` + +``` +invalid-named-import.tsx:15:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 13 │ }); + 14 │ + > 15 │ const Component3 = forwardRef(function Component({ foo }, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 16 │ return
{foo}
; + > 17 │ }); + │ ^^ + 18 │ + 19 │ const Component4 = forwardRef(function Component({ foo }, r) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 13 13 │ }); + 14 14 │ + 15 │ - const·Component3·=·forwardRef(function·Component({·foo·},·ref)·{ + 15 │ + const·Component3·=·function·Component({·foo,·ref·}:·{·foo:·string,·ref?:·RefObject·})·{ + 16 16 │ return
{foo}
; + 17 │ - }); + 17 │ + }; + 18 18 │ + 19 19 │ const Component4 = forwardRef(function Component({ foo }, r) { + + +``` + +``` +invalid-named-import.tsx:19:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 17 │ }); + 18 │ + > 19 │ const Component4 = forwardRef(function Component({ foo }, r) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 20 │ return
{foo}
; + > 21 │ }); + │ ^^ + 22 │ + 23 │ const Component5 = forwardRef(function Component({ foo, ...rest }, r) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 17 17 │ }); + 18 18 │ + 19 │ - const·Component4·=·forwardRef(function·Component({·foo·},·r)·{ + 19 │ + const·Component4·=·function·Component({·foo,·ref:·r·}:·{·foo:·string,·ref?:·RefObject·})·{ + 20 20 │ return
{foo}
; + 21 │ - }); + 21 │ + }; + 22 22 │ + 23 23 │ const Component5 = forwardRef(function Component({ foo, ...rest }, r) { + + +``` + +``` +invalid-named-import.tsx:23:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 21 │ }); + 22 │ + > 23 │ const Component5 = forwardRef(function Component({ foo, ...rest }, r) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 24 │ return
{foo}
; + > 25 │ }); + │ ^^ + 26 │ + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 21 21 │ }); + 22 22 │ + 23 │ - const·Component5·=·forwardRef(function·Component({·foo,·...rest·},·r)·{ + 23 │ + const·Component5·=·function·Component({·foo,·ref:·r,·...rest·}:·{·foo:·string,·bar:·number,·ref?:·RefObject·})·{ + 24 24 │ return
{foo}
; + 25 │ - }); + 25 │ + }; + 26 26 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx new file mode 100644 index 000000000000..ad12375cdb74 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx @@ -0,0 +1,23 @@ +import * as React from "react"; + +const Component1 = React.forwardRef((props, ref) => { + return null; +}); + +const Component2 = React.forwardRef((props, ref) => null); + +const Component3 = React.forwardRef(function (props, ref) { + return null; +}); + +const Component4 = React.forwardRef(function Component(props) { + return null; +}); + +const Component5 = React.forwardRef(function Component(props, ref) { + return
; +}); + +const Component6 = React.forwardRef(function Component({ foo, bar }, ref) { + return
; +}); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx.snap new file mode 100644 index 000000000000..c5c6ace22e7e --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx.snap @@ -0,0 +1,228 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.jsx +--- +# Input +```jsx +import * as React from "react"; + +const Component1 = React.forwardRef((props, ref) => { + return null; +}); + +const Component2 = React.forwardRef((props, ref) => null); + +const Component3 = React.forwardRef(function (props, ref) { + return null; +}); + +const Component4 = React.forwardRef(function Component(props) { + return null; +}); + +const Component5 = React.forwardRef(function Component(props, ref) { + return
; +}); + +const Component6 = React.forwardRef(function Component({ foo, bar }, ref) { + return
; +}); + +``` + +# Diagnostics +``` +invalid.jsx:3:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 1 │ import * as React from "react"; + 2 │ + > 3 │ const Component1 = React.forwardRef((props, ref) => { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 4 │ return null; + > 5 │ }); + │ ^^ + 6 │ + 7 │ const Component2 = React.forwardRef((props, ref) => null); + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 1 1 │ import * as React from "react"; + 2 2 │ + 3 │ - const·Component1·=·React.forwardRef((props,·ref)·=>·{ + 3 │ + const·Component1·=·({·ref,·...props·})·=>·{ + 4 4 │ return null; + 5 │ - }); + 5 │ + }; + 6 6 │ + 7 7 │ const Component2 = React.forwardRef((props, ref) => null); + + +``` + +``` +invalid.jsx:7:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 5 │ }); + 6 │ + > 7 │ const Component2 = React.forwardRef((props, ref) => null); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ + 9 │ const Component3 = React.forwardRef(function (props, ref) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 5 5 │ }); + 6 6 │ + 7 │ - const·Component2·=·React.forwardRef((props,·ref)·=>·null); + 7 │ + const·Component2·=·({·ref,·...props·})·=>·null; + 8 8 │ + 9 9 │ const Component3 = React.forwardRef(function (props, ref) { + + +``` + +``` +invalid.jsx:9:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 7 │ const Component2 = React.forwardRef((props, ref) => null); + 8 │ + > 9 │ const Component3 = React.forwardRef(function (props, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 10 │ return null; + > 11 │ }); + │ ^^ + 12 │ + 13 │ const Component4 = React.forwardRef(function Component(props) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 7 7 │ const Component2 = React.forwardRef((props, ref) => null); + 8 8 │ + 9 │ - const·Component3·=·React.forwardRef(function·(props,·ref)·{ + 9 │ + const·Component3·=·function·({·ref,·...props·})·{ + 10 10 │ return null; + 11 │ - }); + 11 │ + }; + 12 12 │ + 13 13 │ const Component4 = React.forwardRef(function Component(props) { + + +``` + +``` +invalid.jsx:13:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 11 │ }); + 12 │ + > 13 │ const Component4 = React.forwardRef(function Component(props) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 14 │ return null; + > 15 │ }); + │ ^^ + 16 │ + 17 │ const Component5 = React.forwardRef(function Component(props, ref) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 11 11 │ }); + 12 12 │ + 13 │ - const·Component4·=·React.forwardRef(function·Component(props)·{ + 13 │ + const·Component4·=·function·Component(props)·{ + 14 14 │ return null; + 15 │ - }); + 15 │ + }; + 16 16 │ + 17 17 │ const Component5 = React.forwardRef(function Component(props, ref) { + + +``` + +``` +invalid.jsx:17:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 15 │ }); + 16 │ + > 17 │ const Component5 = React.forwardRef(function Component(props, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 18 │ return
; + > 19 │ }); + │ ^^ + 20 │ + 21 │ const Component6 = React.forwardRef(function Component({ foo, bar }, ref) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 15 15 │ }); + 16 16 │ + 17 │ - const·Component5·=·React.forwardRef(function·Component(props,·ref)·{ + 17 │ + const·Component5·=·function·Component({·ref,·...props·})·{ + 18 18 │ return
; + 19 │ - }); + 19 │ + }; + 20 20 │ + 21 21 │ const Component6 = React.forwardRef(function Component({ foo, bar }, ref) { + + +``` + +``` +invalid.jsx:21:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 19 │ }); + 20 │ + > 21 │ const Component6 = React.forwardRef(function Component({ foo, bar }, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 22 │ return
; + > 23 │ }); + │ ^^ + 24 │ + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 19 19 │ }); + 20 20 │ + 21 │ - const·Component6·=·React.forwardRef(function·Component({·foo,·bar·},·ref)·{ + 21 │ + const·Component6·=·function·Component({·foo,·bar,·ref·})·{ + 22 22 │ return
; + 23 │ - }); + 23 │ + }; + 24 24 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.tsx b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.tsx new file mode 100644 index 000000000000..03d1cba21eb5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; + +interface ComponentProps { + foo: string; +} + +const Component1 = React.forwardRef(function Component(props, ref) { + return
; +}); + +const Component2 = React.forwardRef(function Component(props, ref) { + return
{props.foo}
; +}); + +const Component3 = React.forwardRef(function Component({ foo }, ref) { + return
{foo}
; +}); + +const Component4 = React.forwardRef(function Component({ foo }, r) { + return
{foo}
; +}); + +const Component5 = React.forwardRef(function Component({ foo, ...rest }, r) { + return
{foo}
; +}); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.tsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.tsx.snap new file mode 100644 index 000000000000..319fbe959437 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.tsx.snap @@ -0,0 +1,202 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.tsx +--- +# Input +```tsx +import * as React from "react"; + +interface ComponentProps { + foo: string; +} + +const Component1 = React.forwardRef(function Component(props, ref) { + return
; +}); + +const Component2 = React.forwardRef(function Component(props, ref) { + return
{props.foo}
; +}); + +const Component3 = React.forwardRef(function Component({ foo }, ref) { + return
{foo}
; +}); + +const Component4 = React.forwardRef(function Component({ foo }, r) { + return
{foo}
; +}); + +const Component5 = React.forwardRef(function Component({ foo, ...rest }, r) { + return
{foo}
; +}); + +``` + +# Diagnostics +``` +invalid.tsx:7:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 5 │ } + 6 │ + > 7 │ const Component1 = React.forwardRef(function Component(props, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 8 │ return
; + > 9 │ }); + │ ^^ + 10 │ + 11 │ const Component2 = React.forwardRef(function Component(props, ref) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 5 5 │ } + 6 6 │ + 7 │ - const·Component1·=·React.forwardRef(function·Component(props,·ref)·{ + 7 │ + const·Component1·=·function·Component({·ref,·...props·}:·ComponentProps·&·{·ref?:·React.RefObject·})·{ + 8 8 │ return
; + 9 │ - }); + 9 │ + }; + 10 10 │ + 11 11 │ const Component2 = React.forwardRef(function Component(props, ref) { + + +``` + +``` +invalid.tsx:11:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 9 │ }); + 10 │ + > 11 │ const Component2 = React.forwardRef(function Component(props, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 12 │ return
{props.foo}
; + > 13 │ }); + │ ^^ + 14 │ + 15 │ const Component3 = React.forwardRef(function Component({ foo }, ref) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 9 9 │ }); + 10 10 │ + 11 │ - const·Component2·=·React.forwardRef(function·Component(props,·ref)·{ + 11 │ + const·Component2·=·function·Component({·ref,·...props·}:·{·foo:·string,·ref?:·React.RefObject·})·{ + 12 12 │ return
{props.foo}
; + 13 │ - }); + 13 │ + }; + 14 14 │ + 15 15 │ const Component3 = React.forwardRef(function Component({ foo }, ref) { + + +``` + +``` +invalid.tsx:15:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 13 │ }); + 14 │ + > 15 │ const Component3 = React.forwardRef(function Component({ foo }, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 16 │ return
{foo}
; + > 17 │ }); + │ ^^ + 18 │ + 19 │ const Component4 = React.forwardRef(function Component({ foo }, r) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 13 13 │ }); + 14 14 │ + 15 │ - const·Component3·=·React.forwardRef(function·Component({·foo·},·ref)·{ + 15 │ + const·Component3·=·function·Component({·foo,·ref·}:·{·foo:·string,·ref?:·React.RefObject·})·{ + 16 16 │ return
{foo}
; + 17 │ - }); + 17 │ + }; + 18 18 │ + 19 19 │ const Component4 = React.forwardRef(function Component({ foo }, r) { + + +``` + +``` +invalid.tsx:19:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 17 │ }); + 18 │ + > 19 │ const Component4 = React.forwardRef(function Component({ foo }, r) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 20 │ return
{foo}
; + > 21 │ }); + │ ^^ + 22 │ + 23 │ const Component5 = React.forwardRef(function Component({ foo, ...rest }, r) { + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 17 17 │ }); + 18 18 │ + 19 │ - const·Component4·=·React.forwardRef(function·Component({·foo·},·r)·{ + 19 │ + const·Component4·=·function·Component({·foo,·ref:·r·}:·{·foo:·string,·ref?:·React.RefObject·})·{ + 20 20 │ return
{foo}
; + 21 │ - }); + 21 │ + }; + 22 22 │ + 23 23 │ const Component5 = React.forwardRef(function Component({ foo, ...rest }, r) { + + +``` + +``` +invalid.tsx:23:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 21 │ }); + 22 │ + > 23 │ const Component5 = React.forwardRef(function Component({ foo, ...rest }, r) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 24 │ return
{foo}
; + > 25 │ }); + │ ^^ + 26 │ + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 21 21 │ }); + 22 22 │ + 23 │ - const·Component5·=·React.forwardRef(function·Component({·foo,·...rest·},·r)·{ + 23 │ + const·Component5·=·function·Component({·foo,·ref:·r,·...rest·}:·{·foo:·string,·bar:·number,·ref?:·React.RefObject·})·{ + 24 24 │ return
{foo}
; + 25 │ - }); + 25 │ + }; + 26 26 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx new file mode 100644 index 000000000000..cb3b2efec9c8 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx @@ -0,0 +1,9 @@ +/* should not generate diagnostics */ + +const Component1 = ({ ref }) => { + return null; +}; + +const Component2 = ({ ref, ...props }) => { + return null; +}; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx.snap new file mode 100644 index 000000000000..4cd3e76b3d70 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx.snap @@ -0,0 +1,17 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.jsx +--- +# Input +```jsx +/* should not generate diagnostics */ + +const Component1 = ({ ref }) => { + return null; +}; + +const Component2 = ({ ref, ...props }) => { + return null; +}; + +``` diff --git a/crates/biome_js_factory/src/make.rs b/crates/biome_js_factory/src/make.rs index bd5f72a22a02..8975fefd30bb 100644 --- a/crates/biome_js_factory/src/make.rs +++ b/crates/biome_js_factory/src/make.rs @@ -101,6 +101,26 @@ pub fn token_decorated_with_space(kind: JsSyntaxKind) -> JsSyntaxToken { } } +/// Create a new token with the specified syntax kind, and a whitespace trivia +/// piece on the leading position +pub fn token_with_leading_space(kind: JsSyntaxKind) -> JsSyntaxToken { + if let Some(text) = kind.to_string() { + JsSyntaxToken::new_detached(kind, &format!(" {text}"), [TriviaPiece::whitespace(1)], []) + } else { + panic!("token kind {kind:?} cannot be transformed to text") + } +} + +/// Create a new token with the specified syntax kind, and a whitespace trivia +/// piece on the trailing position +pub fn token_with_trailing_space(kind: JsSyntaxKind) -> JsSyntaxToken { + if let Some(text) = kind.to_string() { + JsSyntaxToken::new_detached(kind, &format!("{text} "), [], [TriviaPiece::whitespace(1)]) + } else { + panic!("token kind {kind:?} cannot be transformed to text") + } +} + /// EOF token pub fn eof() -> JsSyntaxToken { JsSyntaxToken::new_detached(JsSyntaxKind::EOF, "", [], []) diff --git a/crates/biome_rule_options/src/lib.rs b/crates/biome_rule_options/src/lib.rs index 92a6d3d4665c..e92b6d7a6520 100644 --- a/crates/biome_rule_options/src/lib.rs +++ b/crates/biome_rule_options/src/lib.rs @@ -147,6 +147,7 @@ pub mod no_prototype_builtins; pub mod no_quickfix_biome; pub mod no_qwik_use_visible_task; pub mod no_re_export_all; +pub mod no_react_forward_ref; pub mod no_react_prop_assignments; pub mod no_react_specific_props; pub mod no_redeclare; diff --git a/crates/biome_rule_options/src/no_react_forward_ref.rs b/crates/biome_rule_options/src/no_react_forward_ref.rs new file mode 100644 index 000000000000..770ef34fa264 --- /dev/null +++ b/crates/biome_rule_options/src/no_react_forward_ref.rs @@ -0,0 +1,6 @@ +use biome_deserialize_macros::Deserializable; +use serde::{Deserialize, Serialize}; +#[derive(Default, Clone, Debug, Deserialize, Deserializable, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, default)] +pub struct NoReactForwardRefOptions {} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 2fd40ac7e0d2..7b708cf50aac 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1657,6 +1657,10 @@ export interface Nursery { * Disallow useVisibleTask$() functions in Qwik components. */ noQwikUseVisibleTask?: RuleConfiguration_for_NoQwikUseVisibleTaskOptions; + /** + * Replaces usages of forwardRef with passing ref as a prop. + */ + noReactForwardRef?: RuleFixConfiguration_for_NoReactForwardRefOptions; /** * Disallow usage of sensitive data such as API keys and tokens. */ @@ -3002,6 +3006,9 @@ export type RuleConfiguration_for_NoNonNullAssertedOptionalChainOptions = export type RuleConfiguration_for_NoQwikUseVisibleTaskOptions = | RulePlainConfiguration | RuleWithOptions_for_NoQwikUseVisibleTaskOptions; +export type RuleFixConfiguration_for_NoReactForwardRefOptions = + | RulePlainConfiguration + | RuleWithFixOptions_for_NoReactForwardRefOptions; export type RuleConfiguration_for_NoSecretsOptions = | RulePlainConfiguration | RuleWithOptions_for_NoSecretsOptions; @@ -5435,6 +5442,20 @@ export interface RuleWithOptions_for_NoQwikUseVisibleTaskOptions { */ options: NoQwikUseVisibleTaskOptions; } +export interface RuleWithFixOptions_for_NoReactForwardRefOptions { + /** + * The kind of the code actions emitted by the rule + */ + fix?: FixKind; + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: NoReactForwardRefOptions; +} export interface RuleWithOptions_for_NoSecretsOptions { /** * The severity of the emitted diagnostics by the rule @@ -8084,6 +8105,7 @@ export interface NoMisusedPromisesOptions {} export interface NoNextAsyncClientComponentOptions {} export interface NoNonNullAssertedOptionalChainOptions {} export interface NoQwikUseVisibleTaskOptions {} +export interface NoReactForwardRefOptions {} export interface NoSecretsOptions { /** * Set entropy threshold (default is 41). @@ -8813,6 +8835,7 @@ export type Category = | "lint/correctness/useValidTypeof" | "lint/correctness/useYield" | "lint/nursery/noColorInvalidHex" + | "lint/nursery/noDuplicateDependencies" | "lint/nursery/noFloatingPromises" | "lint/nursery/noImplicitCoercion" | "lint/nursery/noImportCycles" @@ -8822,6 +8845,7 @@ export type Category = | "lint/nursery/noNextAsyncClientComponent" | "lint/nursery/noNonNullAssertedOptionalChain" | "lint/nursery/noQwikUseVisibleTask" + | "lint/nursery/noReactForwardRef" | "lint/nursery/noSecrets" | "lint/nursery/noShadow" | "lint/nursery/noUnnecessaryConditions" @@ -8963,7 +8987,6 @@ export type Category = | "lint/suspicious/noDuplicateCase" | "lint/suspicious/noDuplicateClassMembers" | "lint/suspicious/noDuplicateCustomProperties" - | "lint/nursery/noDuplicateDependencies" | "lint/suspicious/noDuplicateElseIf" | "lint/suspicious/noDuplicateFields" | "lint/suspicious/noDuplicateFontNames" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 7e921b77be12..342b5489d7af 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -3989,6 +3989,16 @@ ] }, "NoReExportAllOptions": { "type": "object", "additionalProperties": false }, + "NoReactForwardRefConfiguration": { + "anyOf": [ + { "$ref": "#/definitions/RulePlainConfiguration" }, + { "$ref": "#/definitions/RuleWithNoReactForwardRefOptions" } + ] + }, + "NoReactForwardRefOptions": { + "type": "object", + "additionalProperties": false + }, "NoReactPropAssignmentsConfiguration": { "anyOf": [ { "$ref": "#/definitions/RulePlainConfiguration" }, @@ -5007,6 +5017,13 @@ { "type": "null" } ] }, + "noReactForwardRef": { + "description": "Replaces usages of forwardRef with passing ref as a prop.", + "anyOf": [ + { "$ref": "#/definitions/NoReactForwardRefConfiguration" }, + { "type": "null" } + ] + }, "noSecrets": { "description": "Disallow usage of sensitive data such as API keys and tokens.", "anyOf": [ @@ -8194,6 +8211,25 @@ }, "additionalProperties": false }, + "RuleWithNoReactForwardRefOptions": { + "type": "object", + "required": ["level"], + "properties": { + "fix": { + "description": "The kind of the code actions emitted by the rule", + "anyOf": [{ "$ref": "#/definitions/FixKind" }, { "type": "null" }] + }, + "level": { + "description": "The severity of the emitted diagnostics by the rule", + "allOf": [{ "$ref": "#/definitions/RulePlainConfiguration" }] + }, + "options": { + "description": "Rule's options", + "allOf": [{ "$ref": "#/definitions/NoReactForwardRefOptions" }] + } + }, + "additionalProperties": false + }, "RuleWithNoReactPropAssignmentsOptions": { "type": "object", "required": ["level"], From d75d879aa22eafef9f0e42137ba61f11895c6c62 Mon Sep 17 00:00:00 2001 From: Naoki Ikeguchi Date: Thu, 18 Sep 2025 01:37:10 +0900 Subject: [PATCH 2/5] test(lint): more test cases --- .../invalid-named-import.jsx | 6 ++- .../invalid-named-import.jsx.snap | 44 +++++++++++++++++-- .../nursery/noReactForwardRef/invalid.jsx | 4 ++ .../noReactForwardRef/invalid.jsx.snap | 38 ++++++++++++++++ .../specs/nursery/noReactForwardRef/valid.jsx | 8 ++++ .../nursery/noReactForwardRef/valid.jsx.snap | 8 ++++ 6 files changed, 104 insertions(+), 4 deletions(-) diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx index 63ec7790ad53..ad3a302daa69 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx @@ -1,4 +1,4 @@ -import { forwardRef } from "react"; +import { forwardRef, memo } from "react"; const Component1 = forwardRef((props, ref) => { return null; @@ -13,3 +13,7 @@ const Component3 = forwardRef(function (props, ref) { const Component4 = forwardRef(function Component(props, ref) { return null; }); + +const Component5 = memo(forwardRef(function Component(props, ref) { + return
; +})); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx.snap index 2edac0f383ea..35ecec04a08b 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid-named-import.jsx.snap @@ -4,7 +4,7 @@ expression: invalid-named-import.jsx --- # Input ```jsx -import { forwardRef } from "react"; +import { forwardRef, memo } from "react"; const Component1 = forwardRef((props, ref) => { return null; @@ -20,6 +20,10 @@ const Component4 = forwardRef(function Component(props, ref) { return null; }); +const Component5 = memo(forwardRef(function Component(props, ref) { + return
; +})); + ``` # Diagnostics @@ -28,7 +32,7 @@ invalid-named-import.jsx:3:20 lint/nursery/noReactForwardRef FIXABLE ━━━ ! Replaces usages of forwardRef with passing ref as a prop. - 1 │ import { forwardRef } from "react"; + 1 │ import { forwardRef, memo } from "react"; 2 │ > 3 │ const Component1 = forwardRef((props, ref) => { │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -44,7 +48,7 @@ invalid-named-import.jsx:3:20 lint/nursery/noReactForwardRef FIXABLE ━━━ i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. - 1 1 │ import { forwardRef } from "react"; + 1 1 │ import { forwardRef, memo } from "react"; 2 2 │ 3 │ - const·Component1·=·forwardRef((props,·ref)·=>·{ 3 │ + const·Component1·=·({·ref,·...props·})·=>·{ @@ -132,6 +136,7 @@ invalid-named-import.jsx:13:20 lint/nursery/noReactForwardRef FIXABLE ━━ > 15 │ }); │ ^^ 16 │ + 17 │ const Component5 = memo(forwardRef(function Component(props, ref) { i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. @@ -147,6 +152,39 @@ invalid-named-import.jsx:13:20 lint/nursery/noReactForwardRef FIXABLE ━━ 15 │ - }); 15 │ + }; 16 16 │ + 17 17 │ const Component5 = memo(forwardRef(function Component(props, ref) { + + +``` + +``` +invalid-named-import.jsx:17:25 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 15 │ }); + 16 │ + > 17 │ const Component5 = memo(forwardRef(function Component(props, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 18 │ return
; + > 19 │ })); + │ ^^ + 20 │ + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 15 15 │ }); + 16 16 │ + 17 │ - const·Component5·=·memo(forwardRef(function·Component(props,·ref)·{ + 17 │ + const·Component5·=·memo(function·Component({·ref,·...props·})·{ + 18 18 │ return
; + 19 │ - })); + 19 │ + }); + 20 20 │ ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx index ad12375cdb74..423a58924495 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx @@ -21,3 +21,7 @@ const Component5 = React.forwardRef(function Component(props, ref) { const Component6 = React.forwardRef(function Component({ foo, bar }, ref) { return
; }); + +const Component7 = React.memo(React.forwardRef(function Component(props, ref) { + return
; +})); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx.snap index c5c6ace22e7e..1e1364394295 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/invalid.jsx.snap @@ -28,6 +28,10 @@ const Component6 = React.forwardRef(function Component({ foo, bar }, ref) { return
; }); +const Component7 = React.memo(React.forwardRef(function Component(props, ref) { + return
; +})); + ``` # Diagnostics @@ -208,6 +212,7 @@ invalid.jsx:21:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━ > 23 │ }); │ ^^ 24 │ + 25 │ const Component7 = React.memo(React.forwardRef(function Component(props, ref) { i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. @@ -223,6 +228,39 @@ invalid.jsx:21:20 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━ 23 │ - }); 23 │ + }; 24 24 │ + 25 25 │ const Component7 = React.memo(React.forwardRef(function Component(props, ref) { + + +``` + +``` +invalid.jsx:25:31 lint/nursery/noReactForwardRef FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Replaces usages of forwardRef with passing ref as a prop. + + 23 │ }); + 24 │ + > 25 │ const Component7 = React.memo(React.forwardRef(function Component(props, ref) { + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + > 26 │ return
; + > 27 │ })); + │ ^^ + 28 │ + + i In React 19, 'forwardRef' is no longer necessary. Pass 'ref' as a prop instead. + + i Consider disabling this rule if you are working with React 18 or earlier. + + i Unsafe fix: Remove the forwardRef() call and receive the ref as a prop. + + 23 23 │ }); + 24 24 │ + 25 │ - const·Component7·=·React.memo(React.forwardRef(function·Component(props,·ref)·{ + 25 │ + const·Component7·=·React.memo(function·Component({·ref,·...props·})·{ + 26 26 │ return
; + 27 │ - })); + 27 │ + }); + 28 28 │ ``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx index cb3b2efec9c8..47420e42b0c0 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx @@ -7,3 +7,11 @@ const Component1 = ({ ref }) => { const Component2 = ({ ref, ...props }) => { return null; }; + +const Component3 = (props) => { + return null; +}; + +const Component4 = ({ foo, bar }) => { + return null; +}; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx.snap index 4cd3e76b3d70..bf5519f2b987 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noReactForwardRef/valid.jsx.snap @@ -14,4 +14,12 @@ const Component2 = ({ ref, ...props }) => { return null; }; +const Component3 = (props) => { + return null; +}; + +const Component4 = ({ foo, bar }) => { + return null; +}; + ``` From 83e0afefacae224dcebc29acb8ccb3410fcfdbbb Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:47:57 +0000 Subject: [PATCH 3/5] [autofix.ci] apply automated fixes --- .../@biomejs/backend-jsonrpc/src/workspace.ts | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index ae18a6f7a52d..2382dfc9b4d0 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -155,7 +155,7 @@ Examples where this may be useful: // But it's probably better to ignore a specific dependency. // For instance, one that happens to be particularly slow to // scan: "RedisCommander.d.ts", ], } } ``` -Please be aware that rules relying on the module graph or type inference information may be negatively affected if dependencies of your project aren't (fully) scanned. +Please be aware that rules relying on the module graph or type inference information may be negatively affected if dependencies of your project aren't (fully) scanned. */ experimentalScannerIgnores?: string[]; /** @@ -219,7 +219,7 @@ export interface FormatterConfiguration { /** * Use any `.editorconfig` files to configure the formatter. Configuration in `biome.json` will override `.editorconfig` configuration. -Default: `true`. +Default: `true`. */ useEditorconfig?: Bool; } @@ -282,7 +282,7 @@ export interface JsConfiguration { /** * A list of global bindings that should be ignored by the analyzers -If defined here, they should not emit diagnostics. +If defined here, they should not emit diagnostics. */ globals?: string[]; /** @@ -359,7 +359,7 @@ export interface VcsConfiguration { /** * The folder where Biome should check for VCS files. By default, Biome will use the same folder where `biome.json` was found. -If Biome can't find the configuration, it will attempt to use the current working directory. If no current working directory can't be found, Biome won't use the VCS integration, and a diagnostic will be emitted +If Biome can't find the configuration, it will attempt to use the current working directory. If no current working directory can't be found, Biome won't use the VCS integration, and a diagnostic will be emitted */ root?: string; /** @@ -452,7 +452,7 @@ export type LineEnding = "lf" | "crlf" | "cr"; /** * Validated value for the `line_width` formatter options -The allowed range of values is 1..=320 +The allowed range of values is 1..=320 */ export type LineWidth = number; /** @@ -696,13 +696,13 @@ export interface JsParserConfiguration { /** * When enabled, files like `.js`/`.mjs`/`.cjs` may contain JSX syntax. -Defaults to `true`. +Defaults to `true`. */ jsxEverywhere?: Bool; /** * It enables the experimental and unsafe parsing of parameter decorators -These decorators belong to an old proposal, and they are subject to change. +These decorators belong to an old proposal, and they are subject to change. */ unsafeParameterDecoratorsEnabled?: Bool; } @@ -868,7 +868,7 @@ export type QuoteStyle = "double" | "single"; /** * Whether to indent the content of `