diff --git a/.changeset/calm-shrimps-study.md b/.changeset/calm-shrimps-study.md new file mode 100644 index 000000000000..ccd073d89c3e --- /dev/null +++ b/.changeset/calm-shrimps-study.md @@ -0,0 +1,14 @@ +--- +'@biomejs/biome': patch +--- + +Added the new rule [`noLeakedRender`](https://biomejs.dev/linter/rules/no-leaked-render). This rule helps prevent potential leaks when rendering components that use binary expressions or ternaries. + +For example, the following code triggers the rule because the component would render `0`: + +```jsx +const Component = () => { + const count = 0; + return
{count && Count: {count}}
; +} +``` 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 7adae101bb09..f41752efd2fc 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 @@ -2525,6 +2525,22 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "react/no-leaked-render" => { + if !options.include_inspired { + results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Inspired); + return false; + } + 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_leaked_render + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "react/no-unknown-property" => { if !options.include_nursery { results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery); diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index e9bae05f7acc..fbe818e13a7e 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -207,6 +207,7 @@ pub enum RuleName { NoJsxLiterals, NoLabelVar, NoLabelWithoutControl, + NoLeakedRender, NoMagicNumbers, NoMisleadingCharacterClass, NoMisleadingInstantiator, @@ -596,6 +597,7 @@ impl RuleName { Self::NoJsxLiterals => "noJsxLiterals", Self::NoLabelVar => "noLabelVar", Self::NoLabelWithoutControl => "noLabelWithoutControl", + Self::NoLeakedRender => "noLeakedRender", Self::NoMagicNumbers => "noMagicNumbers", Self::NoMisleadingCharacterClass => "noMisleadingCharacterClass", Self::NoMisleadingInstantiator => "noMisleadingInstantiator", @@ -985,6 +987,7 @@ impl RuleName { Self::NoJsxLiterals => RuleGroup::Nursery, Self::NoLabelVar => RuleGroup::Suspicious, Self::NoLabelWithoutControl => RuleGroup::A11y, + Self::NoLeakedRender => RuleGroup::Nursery, Self::NoMagicNumbers => RuleGroup::Style, Self::NoMisleadingCharacterClass => RuleGroup::Suspicious, Self::NoMisleadingInstantiator => RuleGroup::Suspicious, @@ -1379,6 +1382,7 @@ impl std::str::FromStr for RuleName { "noJsxLiterals" => Ok(Self::NoJsxLiterals), "noLabelVar" => Ok(Self::NoLabelVar), "noLabelWithoutControl" => Ok(Self::NoLabelWithoutControl), + "noLeakedRender" => Ok(Self::NoLeakedRender), "noMagicNumbers" => Ok(Self::NoMagicNumbers), "noMisleadingCharacterClass" => Ok(Self::NoMisleadingCharacterClass), "noMisleadingInstantiator" => Ok(Self::NoMisleadingInstantiator), @@ -4780,7 +4784,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 = "Disallow continue statements.\nSee https://biomejs.dev/linter/rules/no-continue"] # [serde (skip_serializing_if = "Option::is_none")] pub no_continue : Option < RuleConfiguration < biome_rule_options :: no_continue :: NoContinueOptions >> , # [doc = "Restrict imports of deprecated exports.\nSee https://biomejs.dev/linter/rules/no-deprecated-imports"] # [serde (skip_serializing_if = "Option::is_none")] pub no_deprecated_imports : Option < RuleConfiguration < biome_rule_options :: no_deprecated_imports :: NoDeprecatedImportsOptions >> , # [doc = "Prevent the listing of duplicate dependencies. The rule supports the following dependency groups: \"bundledDependencies\", \"bundleDependencies\", \"dependencies\", \"devDependencies\", \"overrides\", \"optionalDependencies\", and \"peerDependencies\".\nSee https://biomejs.dev/linter/rules/no-duplicate-dependencies"] # [serde (skip_serializing_if = "Option::is_none")] pub no_duplicate_dependencies : Option < RuleConfiguration < biome_rule_options :: no_duplicate_dependencies :: NoDuplicateDependenciesOptions >> , # [doc = "Disallow empty sources.\nSee https://biomejs.dev/linter/rules/no-empty-source"] # [serde (skip_serializing_if = "Option::is_none")] pub no_empty_source : Option < RuleConfiguration < biome_rule_options :: no_empty_source :: NoEmptySourceOptions >> , # [doc = "Require Promise-like statements to be handled appropriately.\nSee https://biomejs.dev/linter/rules/no-floating-promises"] # [serde (skip_serializing_if = "Option::is_none")] pub no_floating_promises : Option < RuleFixConfiguration < biome_rule_options :: no_floating_promises :: NoFloatingPromisesOptions >> , # [doc = "Disallow iterating using a for-in loop.\nSee https://biomejs.dev/linter/rules/no-for-in"] # [serde (skip_serializing_if = "Option::is_none")] pub no_for_in : Option < RuleConfiguration < biome_rule_options :: no_for_in :: NoForInOptions >> , # [doc = "Prevent import cycles.\nSee https://biomejs.dev/linter/rules/no-import-cycles"] # [serde (skip_serializing_if = "Option::is_none")] pub no_import_cycles : Option < RuleConfiguration < biome_rule_options :: no_import_cycles :: NoImportCyclesOptions >> , # [doc = "Disallows the usage of the unary operators ++ and --.\nSee https://biomejs.dev/linter/rules/no-increment-decrement"] # [serde (skip_serializing_if = "Option::is_none")] pub no_increment_decrement : Option < RuleConfiguration < biome_rule_options :: no_increment_decrement :: NoIncrementDecrementOptions >> , # [doc = "Disallow string literals inside JSX elements.\nSee https://biomejs.dev/linter/rules/no-jsx-literals"] # [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.\nSee https://biomejs.dev/linter/rules/no-misused-promises"] # [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.\nSee https://biomejs.dev/linter/rules/no-next-async-client-component"] # [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 function parameters that are only used in recursive calls.\nSee https://biomejs.dev/linter/rules/no-parameters-only-used-in-recursion"] # [serde (skip_serializing_if = "Option::is_none")] pub no_parameters_only_used_in_recursion : Option < RuleFixConfiguration < biome_rule_options :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursionOptions >> , # [doc = "Replaces usages of forwardRef with passing ref as a prop.\nSee https://biomejs.dev/linter/rules/no-react-forward-ref"] # [serde (skip_serializing_if = "Option::is_none")] pub no_react_forward_ref : Option < RuleFixConfiguration < biome_rule_options :: no_react_forward_ref :: NoReactForwardRefOptions >> , # [doc = "Disallow variable declarations from shadowing variables declared in the outer scope.\nSee https://biomejs.dev/linter/rules/no-shadow"] # [serde (skip_serializing_if = "Option::is_none")] pub no_shadow : Option < RuleConfiguration < biome_rule_options :: no_shadow :: NoShadowOptions >> , # [doc = "Prevent the usage of synchronous scripts.\nSee https://biomejs.dev/linter/rules/no-sync-scripts"] # [serde (skip_serializing_if = "Option::is_none")] pub no_sync_scripts : Option < RuleConfiguration < biome_rule_options :: no_sync_scripts :: NoSyncScriptsOptions >> , # [doc = "Disallow ternary operators.\nSee https://biomejs.dev/linter/rules/no-ternary"] # [serde (skip_serializing_if = "Option::is_none")] pub no_ternary : Option < RuleConfiguration < biome_rule_options :: no_ternary :: NoTernaryOptions >> , # [doc = "Disallow unknown DOM properties.\nSee https://biomejs.dev/linter/rules/no-unknown-attribute"] # [serde (skip_serializing_if = "Option::is_none")] pub no_unknown_attribute : Option < RuleConfiguration < biome_rule_options :: no_unknown_attribute :: NoUnknownAttributeOptions >> , # [doc = "Disallow unnecessary type-based conditions that can be statically determined as redundant.\nSee https://biomejs.dev/linter/rules/no-unnecessary-conditions"] # [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.\nSee https://biomejs.dev/linter/rules/no-unresolved-imports"] # [serde (skip_serializing_if = "Option::is_none")] pub no_unresolved_imports : Option < RuleConfiguration < biome_rule_options :: no_unresolved_imports :: NoUnresolvedImportsOptions >> , # [doc = "Disallow expression statements that are neither a function call nor an assignment.\nSee https://biomejs.dev/linter/rules/no-unused-expressions"] # [serde (skip_serializing_if = "Option::is_none")] pub no_unused_expressions : Option < RuleConfiguration < biome_rule_options :: no_unused_expressions :: NoUnusedExpressionsOptions >> , # [doc = "Disallow unused catch bindings.\nSee https://biomejs.dev/linter/rules/no-useless-catch-binding"] # [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.\nSee https://biomejs.dev/linter/rules/no-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.\nSee https://biomejs.dev/linter/rules/no-vue-data-object-declaration"] # [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 duplicate keys in Vue component data, methods, computed properties, and other options.\nSee https://biomejs.dev/linter/rules/no-vue-duplicate-keys"] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_duplicate_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_duplicate_keys :: NoVueDuplicateKeysOptions >> , # [doc = "Disallow reserved keys in Vue component data and computed properties.\nSee https://biomejs.dev/linter/rules/no-vue-reserved-keys"] # [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.\nSee https://biomejs.dev/linter/rules/no-vue-reserved-props"] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_props : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_props :: NoVueReservedPropsOptions >> , # [doc = "Disallow using v-if and v-for directives on the same element.\nSee https://biomejs.dev/linter/rules/no-vue-v-if-with-v-for"] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_v_if_with_v_for : Option < RuleConfiguration < biome_rule_options :: no_vue_v_if_with_v_for :: NoVueVIfWithVForOptions >> , # [doc = "Require Array#sort and Array#toSorted calls to always provide a compareFunction.\nSee https://biomejs.dev/linter/rules/use-array-sort-compare"] # [serde (skip_serializing_if = "Option::is_none")] pub use_array_sort_compare : Option < RuleConfiguration < biome_rule_options :: use_array_sort_compare :: UseArraySortCompareOptions >> , # [doc = "Enforce consistent arrow function bodies.\nSee https://biomejs.dev/linter/rules/use-consistent-arrow-return"] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_arrow_return : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_arrow_return :: UseConsistentArrowReturnOptions >> , # [doc = "Require all descriptions to follow the same style (either block or inline) to maintain consistency and improve readability across the schema.\nSee https://biomejs.dev/linter/rules/use-consistent-graphql-descriptions"] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_graphql_descriptions : Option < RuleConfiguration < biome_rule_options :: use_consistent_graphql_descriptions :: UseConsistentGraphqlDescriptionsOptions >> , # [doc = "Require the @deprecated directive to specify a deletion date.\nSee https://biomejs.dev/linter/rules/use-deprecated-date"] # [serde (skip_serializing_if = "Option::is_none")] pub use_deprecated_date : Option < RuleConfiguration < biome_rule_options :: use_deprecated_date :: UseDeprecatedDateOptions >> , # [doc = "Require switch-case statements to be exhaustive.\nSee https://biomejs.dev/linter/rules/use-exhaustive-switch-cases"] # [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.\nSee https://biomejs.dev/linter/rules/use-explicit-type"] # [serde (skip_serializing_if = "Option::is_none")] pub use_explicit_type : Option < RuleConfiguration < biome_rule_options :: use_explicit_type :: UseExplicitTypeOptions >> , # [doc = "Enforce the use of Array.prototype.find() over Array.prototype.filter() followed by [0] when looking for a single result.\nSee https://biomejs.dev/linter/rules/use-find"] # [serde (skip_serializing_if = "Option::is_none")] pub use_find : Option < RuleConfiguration < biome_rule_options :: use_find :: UseFindOptions >> , # [doc = "Enforce a maximum number of parameters in function definitions.\nSee https://biomejs.dev/linter/rules/use-max-params"] # [serde (skip_serializing_if = "Option::is_none")] pub use_max_params : Option < RuleConfiguration < biome_rule_options :: use_max_params :: UseMaxParamsOptions >> , # [doc = "Disallow use* hooks outside of component$ or other use* hooks in Qwik applications.\nSee https://biomejs.dev/linter/rules/use-qwik-method-usage"] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_method_usage : Option < RuleConfiguration < biome_rule_options :: use_qwik_method_usage :: UseQwikMethodUsageOptions >> , # [doc = "Disallow unserializable expressions in Qwik dollar ($) scopes.\nSee https://biomejs.dev/linter/rules/use-qwik-valid-lexical-scope"] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_valid_lexical_scope : Option < RuleConfiguration < biome_rule_options :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScopeOptions >> , # [doc = "Enforce the sorting of CSS utility classes.\nSee https://biomejs.dev/linter/rules/use-sorted-classes"] # [serde (skip_serializing_if = "Option::is_none")] pub use_sorted_classes : Option < RuleFixConfiguration < biome_rule_options :: use_sorted_classes :: UseSortedClassesOptions >> , # [doc = "Enforce the use of the spread operator over .apply().\nSee https://biomejs.dev/linter/rules/use-spread"] # [serde (skip_serializing_if = "Option::is_none")] pub use_spread : Option < RuleFixConfiguration < biome_rule_options :: use_spread :: UseSpreadOptions >> , # [doc = "Enforce unique operation names across a GraphQL document.\nSee https://biomejs.dev/linter/rules/use-unique-graphql-operation-name"] # [serde (skip_serializing_if = "Option::is_none")] pub use_unique_graphql_operation_name : Option < RuleConfiguration < biome_rule_options :: use_unique_graphql_operation_name :: UseUniqueGraphqlOperationNameOptions >> , # [doc = "Enforce specific order of Vue compiler macros.\nSee https://biomejs.dev/linter/rules/use-vue-define-macros-order"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_define_macros_order : Option < RuleFixConfiguration < biome_rule_options :: use_vue_define_macros_order :: UseVueDefineMacrosOrderOptions >> , # [doc = "Enforce hyphenated (kebab-case) attribute names in Vue templates.\nSee https://biomejs.dev/linter/rules/use-vue-hyphenated-attributes"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_hyphenated_attributes : Option < RuleFixConfiguration < biome_rule_options :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributesOptions >> , # [doc = "Enforce multi-word component names in Vue components.\nSee https://biomejs.dev/linter/rules/use-vue-multi-word-component-names"] # [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 >> , # [doc = "Forbids v-bind directives with missing arguments or invalid modifiers.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-bind"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_bind : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_bind :: UseVueValidVBindOptions >> , # [doc = "Enforce valid usage of v-else.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-else"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_else : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_else :: UseVueValidVElseOptions >> , # [doc = "Enforce valid v-else-if directives.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-else-if"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_else_if : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_else_if :: UseVueValidVElseIfOptions >> , # [doc = "Enforce valid v-html directives.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-html"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_html : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_html :: UseVueValidVHtmlOptions >> , # [doc = "Enforces valid v-if usage for Vue templates.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-if"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_if : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_if :: UseVueValidVIfOptions >> , # [doc = "Enforce valid v-on directives with proper arguments, modifiers, and handlers.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-on"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_on : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_on :: UseVueValidVOnOptions >> , # [doc = "Enforce valid v-text Vue directives.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-text"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_text : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_text :: UseVueValidVTextOptions >> } +pub struct Nursery { # [doc = r" Enables the recommended rules for this group"] # [serde (skip_serializing_if = "Option::is_none")] pub recommended : Option < bool > , # [doc = "Disallow continue statements.\nSee https://biomejs.dev/linter/rules/no-continue"] # [serde (skip_serializing_if = "Option::is_none")] pub no_continue : Option < RuleConfiguration < biome_rule_options :: no_continue :: NoContinueOptions >> , # [doc = "Restrict imports of deprecated exports.\nSee https://biomejs.dev/linter/rules/no-deprecated-imports"] # [serde (skip_serializing_if = "Option::is_none")] pub no_deprecated_imports : Option < RuleConfiguration < biome_rule_options :: no_deprecated_imports :: NoDeprecatedImportsOptions >> , # [doc = "Prevent the listing of duplicate dependencies. The rule supports the following dependency groups: \"bundledDependencies\", \"bundleDependencies\", \"dependencies\", \"devDependencies\", \"overrides\", \"optionalDependencies\", and \"peerDependencies\".\nSee https://biomejs.dev/linter/rules/no-duplicate-dependencies"] # [serde (skip_serializing_if = "Option::is_none")] pub no_duplicate_dependencies : Option < RuleConfiguration < biome_rule_options :: no_duplicate_dependencies :: NoDuplicateDependenciesOptions >> , # [doc = "Disallow empty sources.\nSee https://biomejs.dev/linter/rules/no-empty-source"] # [serde (skip_serializing_if = "Option::is_none")] pub no_empty_source : Option < RuleConfiguration < biome_rule_options :: no_empty_source :: NoEmptySourceOptions >> , # [doc = "Require Promise-like statements to be handled appropriately.\nSee https://biomejs.dev/linter/rules/no-floating-promises"] # [serde (skip_serializing_if = "Option::is_none")] pub no_floating_promises : Option < RuleFixConfiguration < biome_rule_options :: no_floating_promises :: NoFloatingPromisesOptions >> , # [doc = "Disallow iterating using a for-in loop.\nSee https://biomejs.dev/linter/rules/no-for-in"] # [serde (skip_serializing_if = "Option::is_none")] pub no_for_in : Option < RuleConfiguration < biome_rule_options :: no_for_in :: NoForInOptions >> , # [doc = "Prevent import cycles.\nSee https://biomejs.dev/linter/rules/no-import-cycles"] # [serde (skip_serializing_if = "Option::is_none")] pub no_import_cycles : Option < RuleConfiguration < biome_rule_options :: no_import_cycles :: NoImportCyclesOptions >> , # [doc = "Disallows the usage of the unary operators ++ and --.\nSee https://biomejs.dev/linter/rules/no-increment-decrement"] # [serde (skip_serializing_if = "Option::is_none")] pub no_increment_decrement : Option < RuleConfiguration < biome_rule_options :: no_increment_decrement :: NoIncrementDecrementOptions >> , # [doc = "Disallow string literals inside JSX elements.\nSee https://biomejs.dev/linter/rules/no-jsx-literals"] # [serde (skip_serializing_if = "Option::is_none")] pub no_jsx_literals : Option < RuleConfiguration < biome_rule_options :: no_jsx_literals :: NoJsxLiteralsOptions >> , # [doc = "Prevent problematic leaked values from being rendered.\nSee https://biomejs.dev/linter/rules/no-leaked-render"] # [serde (skip_serializing_if = "Option::is_none")] pub no_leaked_render : Option < RuleConfiguration < biome_rule_options :: no_leaked_render :: NoLeakedRenderOptions >> , # [doc = "Disallow Promises to be used in places where they are almost certainly a mistake.\nSee https://biomejs.dev/linter/rules/no-misused-promises"] # [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.\nSee https://biomejs.dev/linter/rules/no-next-async-client-component"] # [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 function parameters that are only used in recursive calls.\nSee https://biomejs.dev/linter/rules/no-parameters-only-used-in-recursion"] # [serde (skip_serializing_if = "Option::is_none")] pub no_parameters_only_used_in_recursion : Option < RuleFixConfiguration < biome_rule_options :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursionOptions >> , # [doc = "Replaces usages of forwardRef with passing ref as a prop.\nSee https://biomejs.dev/linter/rules/no-react-forward-ref"] # [serde (skip_serializing_if = "Option::is_none")] pub no_react_forward_ref : Option < RuleFixConfiguration < biome_rule_options :: no_react_forward_ref :: NoReactForwardRefOptions >> , # [doc = "Disallow variable declarations from shadowing variables declared in the outer scope.\nSee https://biomejs.dev/linter/rules/no-shadow"] # [serde (skip_serializing_if = "Option::is_none")] pub no_shadow : Option < RuleConfiguration < biome_rule_options :: no_shadow :: NoShadowOptions >> , # [doc = "Prevent the usage of synchronous scripts.\nSee https://biomejs.dev/linter/rules/no-sync-scripts"] # [serde (skip_serializing_if = "Option::is_none")] pub no_sync_scripts : Option < RuleConfiguration < biome_rule_options :: no_sync_scripts :: NoSyncScriptsOptions >> , # [doc = "Disallow ternary operators.\nSee https://biomejs.dev/linter/rules/no-ternary"] # [serde (skip_serializing_if = "Option::is_none")] pub no_ternary : Option < RuleConfiguration < biome_rule_options :: no_ternary :: NoTernaryOptions >> , # [doc = "Disallow unknown DOM properties.\nSee https://biomejs.dev/linter/rules/no-unknown-attribute"] # [serde (skip_serializing_if = "Option::is_none")] pub no_unknown_attribute : Option < RuleConfiguration < biome_rule_options :: no_unknown_attribute :: NoUnknownAttributeOptions >> , # [doc = "Disallow unnecessary type-based conditions that can be statically determined as redundant.\nSee https://biomejs.dev/linter/rules/no-unnecessary-conditions"] # [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.\nSee https://biomejs.dev/linter/rules/no-unresolved-imports"] # [serde (skip_serializing_if = "Option::is_none")] pub no_unresolved_imports : Option < RuleConfiguration < biome_rule_options :: no_unresolved_imports :: NoUnresolvedImportsOptions >> , # [doc = "Disallow expression statements that are neither a function call nor an assignment.\nSee https://biomejs.dev/linter/rules/no-unused-expressions"] # [serde (skip_serializing_if = "Option::is_none")] pub no_unused_expressions : Option < RuleConfiguration < biome_rule_options :: no_unused_expressions :: NoUnusedExpressionsOptions >> , # [doc = "Disallow unused catch bindings.\nSee https://biomejs.dev/linter/rules/no-useless-catch-binding"] # [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.\nSee https://biomejs.dev/linter/rules/no-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.\nSee https://biomejs.dev/linter/rules/no-vue-data-object-declaration"] # [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 duplicate keys in Vue component data, methods, computed properties, and other options.\nSee https://biomejs.dev/linter/rules/no-vue-duplicate-keys"] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_duplicate_keys : Option < RuleConfiguration < biome_rule_options :: no_vue_duplicate_keys :: NoVueDuplicateKeysOptions >> , # [doc = "Disallow reserved keys in Vue component data and computed properties.\nSee https://biomejs.dev/linter/rules/no-vue-reserved-keys"] # [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.\nSee https://biomejs.dev/linter/rules/no-vue-reserved-props"] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_reserved_props : Option < RuleConfiguration < biome_rule_options :: no_vue_reserved_props :: NoVueReservedPropsOptions >> , # [doc = "Disallow using v-if and v-for directives on the same element.\nSee https://biomejs.dev/linter/rules/no-vue-v-if-with-v-for"] # [serde (skip_serializing_if = "Option::is_none")] pub no_vue_v_if_with_v_for : Option < RuleConfiguration < biome_rule_options :: no_vue_v_if_with_v_for :: NoVueVIfWithVForOptions >> , # [doc = "Require Array#sort and Array#toSorted calls to always provide a compareFunction.\nSee https://biomejs.dev/linter/rules/use-array-sort-compare"] # [serde (skip_serializing_if = "Option::is_none")] pub use_array_sort_compare : Option < RuleConfiguration < biome_rule_options :: use_array_sort_compare :: UseArraySortCompareOptions >> , # [doc = "Enforce consistent arrow function bodies.\nSee https://biomejs.dev/linter/rules/use-consistent-arrow-return"] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_arrow_return : Option < RuleFixConfiguration < biome_rule_options :: use_consistent_arrow_return :: UseConsistentArrowReturnOptions >> , # [doc = "Require all descriptions to follow the same style (either block or inline) to maintain consistency and improve readability across the schema.\nSee https://biomejs.dev/linter/rules/use-consistent-graphql-descriptions"] # [serde (skip_serializing_if = "Option::is_none")] pub use_consistent_graphql_descriptions : Option < RuleConfiguration < biome_rule_options :: use_consistent_graphql_descriptions :: UseConsistentGraphqlDescriptionsOptions >> , # [doc = "Require the @deprecated directive to specify a deletion date.\nSee https://biomejs.dev/linter/rules/use-deprecated-date"] # [serde (skip_serializing_if = "Option::is_none")] pub use_deprecated_date : Option < RuleConfiguration < biome_rule_options :: use_deprecated_date :: UseDeprecatedDateOptions >> , # [doc = "Require switch-case statements to be exhaustive.\nSee https://biomejs.dev/linter/rules/use-exhaustive-switch-cases"] # [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.\nSee https://biomejs.dev/linter/rules/use-explicit-type"] # [serde (skip_serializing_if = "Option::is_none")] pub use_explicit_type : Option < RuleConfiguration < biome_rule_options :: use_explicit_type :: UseExplicitTypeOptions >> , # [doc = "Enforce the use of Array.prototype.find() over Array.prototype.filter() followed by [0] when looking for a single result.\nSee https://biomejs.dev/linter/rules/use-find"] # [serde (skip_serializing_if = "Option::is_none")] pub use_find : Option < RuleConfiguration < biome_rule_options :: use_find :: UseFindOptions >> , # [doc = "Enforce a maximum number of parameters in function definitions.\nSee https://biomejs.dev/linter/rules/use-max-params"] # [serde (skip_serializing_if = "Option::is_none")] pub use_max_params : Option < RuleConfiguration < biome_rule_options :: use_max_params :: UseMaxParamsOptions >> , # [doc = "Disallow use* hooks outside of component$ or other use* hooks in Qwik applications.\nSee https://biomejs.dev/linter/rules/use-qwik-method-usage"] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_method_usage : Option < RuleConfiguration < biome_rule_options :: use_qwik_method_usage :: UseQwikMethodUsageOptions >> , # [doc = "Disallow unserializable expressions in Qwik dollar ($) scopes.\nSee https://biomejs.dev/linter/rules/use-qwik-valid-lexical-scope"] # [serde (skip_serializing_if = "Option::is_none")] pub use_qwik_valid_lexical_scope : Option < RuleConfiguration < biome_rule_options :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScopeOptions >> , # [doc = "Enforce the sorting of CSS utility classes.\nSee https://biomejs.dev/linter/rules/use-sorted-classes"] # [serde (skip_serializing_if = "Option::is_none")] pub use_sorted_classes : Option < RuleFixConfiguration < biome_rule_options :: use_sorted_classes :: UseSortedClassesOptions >> , # [doc = "Enforce the use of the spread operator over .apply().\nSee https://biomejs.dev/linter/rules/use-spread"] # [serde (skip_serializing_if = "Option::is_none")] pub use_spread : Option < RuleFixConfiguration < biome_rule_options :: use_spread :: UseSpreadOptions >> , # [doc = "Enforce unique operation names across a GraphQL document.\nSee https://biomejs.dev/linter/rules/use-unique-graphql-operation-name"] # [serde (skip_serializing_if = "Option::is_none")] pub use_unique_graphql_operation_name : Option < RuleConfiguration < biome_rule_options :: use_unique_graphql_operation_name :: UseUniqueGraphqlOperationNameOptions >> , # [doc = "Enforce specific order of Vue compiler macros.\nSee https://biomejs.dev/linter/rules/use-vue-define-macros-order"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_define_macros_order : Option < RuleFixConfiguration < biome_rule_options :: use_vue_define_macros_order :: UseVueDefineMacrosOrderOptions >> , # [doc = "Enforce hyphenated (kebab-case) attribute names in Vue templates.\nSee https://biomejs.dev/linter/rules/use-vue-hyphenated-attributes"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_hyphenated_attributes : Option < RuleFixConfiguration < biome_rule_options :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributesOptions >> , # [doc = "Enforce multi-word component names in Vue components.\nSee https://biomejs.dev/linter/rules/use-vue-multi-word-component-names"] # [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 >> , # [doc = "Forbids v-bind directives with missing arguments or invalid modifiers.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-bind"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_bind : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_bind :: UseVueValidVBindOptions >> , # [doc = "Enforce valid usage of v-else.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-else"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_else : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_else :: UseVueValidVElseOptions >> , # [doc = "Enforce valid v-else-if directives.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-else-if"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_else_if : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_else_if :: UseVueValidVElseIfOptions >> , # [doc = "Enforce valid v-html directives.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-html"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_html : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_html :: UseVueValidVHtmlOptions >> , # [doc = "Enforces valid v-if usage for Vue templates.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-if"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_if : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_if :: UseVueValidVIfOptions >> , # [doc = "Enforce valid v-on directives with proper arguments, modifiers, and handlers.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-on"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_on : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_on :: UseVueValidVOnOptions >> , # [doc = "Enforce valid v-text Vue directives.\nSee https://biomejs.dev/linter/rules/use-vue-valid-v-text"] # [serde (skip_serializing_if = "Option::is_none")] pub use_vue_valid_v_text : Option < RuleConfiguration < biome_rule_options :: use_vue_valid_v_text :: UseVueValidVTextOptions >> } impl Nursery { const GROUP_NAME: &'static str = "nursery"; pub(crate) const GROUP_RULES: &'static [&'static str] = &[ @@ -4793,6 +4797,7 @@ impl Nursery { "noImportCycles", "noIncrementDecrement", "noJsxLiterals", + "noLeakedRender", "noMisusedPromises", "noNextAsyncClientComponent", "noParametersOnlyUsedInRecursion", @@ -4836,7 +4841,7 @@ impl Nursery { "useVueValidVText", ]; const RECOMMENDED_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = - &[RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])]; + &[RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), @@ -4888,6 +4893,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]), ]; } impl RuleGroupExt for Nursery { @@ -4944,211 +4950,216 @@ impl RuleGroupExt for Nursery { { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } - if let Some(rule) = self.no_misused_promises.as_ref() + if let Some(rule) = self.no_leaked_render.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } - if let Some(rule) = self.no_next_async_client_component.as_ref() + if let Some(rule) = self.no_misused_promises.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } - if let Some(rule) = self.no_parameters_only_used_in_recursion.as_ref() + if let Some(rule) = self.no_next_async_client_component.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } - if let Some(rule) = self.no_react_forward_ref.as_ref() + if let Some(rule) = self.no_parameters_only_used_in_recursion.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } - if let Some(rule) = self.no_shadow.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[13])); } - if let Some(rule) = self.no_sync_scripts.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[14])); } - if let Some(rule) = self.no_ternary.as_ref() + if let Some(rule) = self.no_sync_scripts.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } - if let Some(rule) = self.no_unknown_attribute.as_ref() + if let Some(rule) = self.no_ternary.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } - if let Some(rule) = self.no_unnecessary_conditions.as_ref() + if let Some(rule) = self.no_unknown_attribute.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } - 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[18])); } - if let Some(rule) = self.no_unused_expressions.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[19])); } - if let Some(rule) = self.no_useless_catch_binding.as_ref() + if let Some(rule) = self.no_unused_expressions.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } - 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[21])); } - 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[22])); } - if let Some(rule) = self.no_vue_duplicate_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[23])); } - if let Some(rule) = self.no_vue_reserved_keys.as_ref() + if let Some(rule) = self.no_vue_duplicate_keys.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } - 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[25])); } - if let Some(rule) = self.no_vue_v_if_with_v_for.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[26])); } - if let Some(rule) = self.use_array_sort_compare.as_ref() + if let Some(rule) = self.no_vue_v_if_with_v_for.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } - if let Some(rule) = self.use_consistent_arrow_return.as_ref() + if let Some(rule) = self.use_array_sort_compare.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } - if let Some(rule) = self.use_consistent_graphql_descriptions.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[29])); } - if let Some(rule) = self.use_deprecated_date.as_ref() + if let Some(rule) = self.use_consistent_graphql_descriptions.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } - if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() + if let Some(rule) = self.use_deprecated_date.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } - 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[32])); } - if let Some(rule) = self.use_find.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[33])); } - if let Some(rule) = self.use_max_params.as_ref() + if let Some(rule) = self.use_find.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } - if let Some(rule) = self.use_qwik_method_usage.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[35])); } - if let Some(rule) = self.use_qwik_valid_lexical_scope.as_ref() + if let Some(rule) = self.use_qwik_method_usage.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } - if let Some(rule) = self.use_sorted_classes.as_ref() + if let Some(rule) = self.use_qwik_valid_lexical_scope.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } - if let Some(rule) = self.use_spread.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[38])); } - if let Some(rule) = self.use_unique_graphql_operation_name.as_ref() + if let Some(rule) = self.use_spread.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } - if let Some(rule) = self.use_vue_define_macros_order.as_ref() + if let Some(rule) = self.use_unique_graphql_operation_name.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } - if let Some(rule) = self.use_vue_hyphenated_attributes.as_ref() + if let Some(rule) = self.use_vue_define_macros_order.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } - if let Some(rule) = self.use_vue_multi_word_component_names.as_ref() + if let Some(rule) = self.use_vue_hyphenated_attributes.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } - if let Some(rule) = self.use_vue_valid_v_bind.as_ref() + 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[43])); } - if let Some(rule) = self.use_vue_valid_v_else.as_ref() + if let Some(rule) = self.use_vue_valid_v_bind.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); } - if let Some(rule) = self.use_vue_valid_v_else_if.as_ref() + if let Some(rule) = self.use_vue_valid_v_else.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45])); } - if let Some(rule) = self.use_vue_valid_v_html.as_ref() + if let Some(rule) = self.use_vue_valid_v_else_if.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46])); } - if let Some(rule) = self.use_vue_valid_v_if.as_ref() + if let Some(rule) = self.use_vue_valid_v_html.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47])); } - if let Some(rule) = self.use_vue_valid_v_on.as_ref() + if let Some(rule) = self.use_vue_valid_v_if.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48])); } - if let Some(rule) = self.use_vue_valid_v_text.as_ref() + if let Some(rule) = self.use_vue_valid_v_on.as_ref() && rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49])); } + if let Some(rule) = self.use_vue_valid_v_text.as_ref() + && rule.is_enabled() + { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50])); + } index_set } fn get_disabled_rules(&self) -> FxHashSet> { @@ -5198,211 +5209,216 @@ impl RuleGroupExt for Nursery { { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } - if let Some(rule) = self.no_misused_promises.as_ref() + if let Some(rule) = self.no_leaked_render.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } - if let Some(rule) = self.no_next_async_client_component.as_ref() + if let Some(rule) = self.no_misused_promises.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } - if let Some(rule) = self.no_parameters_only_used_in_recursion.as_ref() + if let Some(rule) = self.no_next_async_client_component.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } - if let Some(rule) = self.no_react_forward_ref.as_ref() + if let Some(rule) = self.no_parameters_only_used_in_recursion.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } - if let Some(rule) = self.no_shadow.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[13])); } - if let Some(rule) = self.no_sync_scripts.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[14])); } - if let Some(rule) = self.no_ternary.as_ref() + if let Some(rule) = self.no_sync_scripts.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } - if let Some(rule) = self.no_unknown_attribute.as_ref() + if let Some(rule) = self.no_ternary.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } - if let Some(rule) = self.no_unnecessary_conditions.as_ref() + if let Some(rule) = self.no_unknown_attribute.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } - 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[18])); } - if let Some(rule) = self.no_unused_expressions.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[19])); } - if let Some(rule) = self.no_useless_catch_binding.as_ref() + if let Some(rule) = self.no_unused_expressions.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } - 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[21])); } - 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[22])); } - if let Some(rule) = self.no_vue_duplicate_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[23])); } - if let Some(rule) = self.no_vue_reserved_keys.as_ref() + if let Some(rule) = self.no_vue_duplicate_keys.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } - 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[25])); } - if let Some(rule) = self.no_vue_v_if_with_v_for.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[26])); } - if let Some(rule) = self.use_array_sort_compare.as_ref() + if let Some(rule) = self.no_vue_v_if_with_v_for.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27])); } - if let Some(rule) = self.use_consistent_arrow_return.as_ref() + if let Some(rule) = self.use_array_sort_compare.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } - if let Some(rule) = self.use_consistent_graphql_descriptions.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[29])); } - if let Some(rule) = self.use_deprecated_date.as_ref() + if let Some(rule) = self.use_consistent_graphql_descriptions.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } - if let Some(rule) = self.use_exhaustive_switch_cases.as_ref() + if let Some(rule) = self.use_deprecated_date.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } - 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[32])); } - if let Some(rule) = self.use_find.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[33])); } - if let Some(rule) = self.use_max_params.as_ref() + if let Some(rule) = self.use_find.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } - if let Some(rule) = self.use_qwik_method_usage.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[35])); } - if let Some(rule) = self.use_qwik_valid_lexical_scope.as_ref() + if let Some(rule) = self.use_qwik_method_usage.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } - if let Some(rule) = self.use_sorted_classes.as_ref() + if let Some(rule) = self.use_qwik_valid_lexical_scope.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } - if let Some(rule) = self.use_spread.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[38])); } - if let Some(rule) = self.use_unique_graphql_operation_name.as_ref() + if let Some(rule) = self.use_spread.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } - if let Some(rule) = self.use_vue_define_macros_order.as_ref() + if let Some(rule) = self.use_unique_graphql_operation_name.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40])); } - if let Some(rule) = self.use_vue_hyphenated_attributes.as_ref() + if let Some(rule) = self.use_vue_define_macros_order.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } - if let Some(rule) = self.use_vue_multi_word_component_names.as_ref() + if let Some(rule) = self.use_vue_hyphenated_attributes.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } - if let Some(rule) = self.use_vue_valid_v_bind.as_ref() + 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[43])); } - if let Some(rule) = self.use_vue_valid_v_else.as_ref() + if let Some(rule) = self.use_vue_valid_v_bind.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44])); } - if let Some(rule) = self.use_vue_valid_v_else_if.as_ref() + if let Some(rule) = self.use_vue_valid_v_else.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45])); } - if let Some(rule) = self.use_vue_valid_v_html.as_ref() + if let Some(rule) = self.use_vue_valid_v_else_if.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[46])); } - if let Some(rule) = self.use_vue_valid_v_if.as_ref() + if let Some(rule) = self.use_vue_valid_v_html.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[47])); } - if let Some(rule) = self.use_vue_valid_v_on.as_ref() + if let Some(rule) = self.use_vue_valid_v_if.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[48])); } - if let Some(rule) = self.use_vue_valid_v_text.as_ref() + if let Some(rule) = self.use_vue_valid_v_on.as_ref() && rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[49])); } + if let Some(rule) = self.use_vue_valid_v_text.as_ref() + && rule.is_disabled() + { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50])); + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -5469,6 +5485,10 @@ impl RuleGroupExt for Nursery { .no_jsx_literals .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noLeakedRender" => self + .no_leaked_render + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noMisusedPromises" => self .no_misused_promises .as_ref() @@ -5650,6 +5670,7 @@ impl From for Nursery { no_import_cycles: Some(value.into()), no_increment_decrement: Some(value.into()), no_jsx_literals: Some(value.into()), + no_leaked_render: Some(value.into()), no_misused_promises: Some(value.into()), no_next_async_client_component: Some(value.into()), no_parameters_only_used_in_recursion: Some(value.into()), diff --git a/crates/biome_configuration/src/generated/domain_selector.rs b/crates/biome_configuration/src/generated/domain_selector.rs index 4e505bcb93bd..fe6882e6843a 100644 --- a/crates/biome_configuration/src/generated/domain_selector.rs +++ b/crates/biome_configuration/src/generated/domain_selector.rs @@ -55,6 +55,7 @@ static REACT_FILTERS: LazyLock>> = LazyLock::new(|| { RuleFilter::Rule("correctness", "useHookAtTopLevel"), RuleFilter::Rule("correctness", "useJsxKeyInIterable"), RuleFilter::Rule("correctness", "useUniqueElementIds"), + RuleFilter::Rule("nursery", "noLeakedRender"), RuleFilter::Rule("nursery", "noReactForwardRef"), RuleFilter::Rule("nursery", "noSyncScripts"), RuleFilter::Rule("nursery", "noUnknownAttribute"), diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index aa73b1e52e2b..7ccd16fabddd 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -175,6 +175,7 @@ define_categories! { "lint/nursery/noImportCycles": "https://biomejs.dev/linter/rules/no-import-cycles", "lint/nursery/noIncrementDecrement": "https://biomejs.dev/linter/rules/no-increment-decrement", "lint/nursery/noJsxLiterals": "https://biomejs.dev/linter/rules/no-jsx-literals", + "lint/nursery/noLeakedRender": "https://biomejs.dev/linter/rules/no-leaked-render", "lint/nursery/noMissingGenericFamilyKeyword": "https://biomejs.dev/linter/rules/no-missing-generic-family-keyword", "lint/nursery/noMisusedPromises": "https://biomejs.dev/linter/rules/no-misused-promises", "lint/nursery/noNextAsyncClientComponent": "https://biomejs.dev/linter/rules/no-next-async-client-component", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 99fdc4399fdd..48cf0300b35e 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -11,6 +11,7 @@ pub mod no_for_in; pub mod no_import_cycles; pub mod no_increment_decrement; pub mod no_jsx_literals; +pub mod no_leaked_render; pub mod no_misused_promises; pub mod no_next_async_client_component; pub mod no_parameters_only_used_in_recursion; @@ -40,4 +41,4 @@ pub mod use_sorted_classes; pub mod use_spread; pub mod use_vue_define_macros_order; pub mod use_vue_multi_word_component_names; -declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } } +declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } } diff --git a/crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs b/crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs new file mode 100644 index 000000000000..1312ef0fe5c6 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs @@ -0,0 +1,276 @@ +use biome_analyze::{ + Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule, +}; +use biome_console::markup; +use biome_js_syntax::{ + AnyJsExpression, JsConditionalExpression, JsLogicalExpression, JsLogicalOperator, JsSyntaxNode, + JsxExpressionAttributeValue, JsxExpressionChild, JsxTagExpression, + binding_ext::AnyJsBindingDeclaration, +}; +use biome_rowan::{AstNode, declare_node_union}; +use biome_rule_options::no_leaked_render::NoLeakedRenderOptions; + +use crate::services::semantic::Semantic; + +declare_lint_rule! { + /// Prevent problematic leaked values from being rendered. + /// + /// This rule prevents values that might cause unintentionally rendered values + /// or rendering crashes in React JSX. When using conditional rendering with the + /// logical AND operator (`&&`), if the left-hand side evaluates to a falsy value like + /// `0`, `NaN`, or any empty string, these values will be rendered instead of rendering nothing. + /// + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```jsx,expect_diagnostic + /// const Component = () => { + /// const count = 0; + /// return
{count && Count: {count}}
; + /// } + /// ``` + /// + /// ```jsx,expect_diagnostic + /// const Component = () => { + /// const items = []; + /// return
{items.length && }
; + /// } + /// ``` + /// + /// ```jsx,expect_diagnostic + /// const Component = () => { + /// const user = null; + /// return
{user && }
; + /// } + /// ``` + /// + /// + /// ### Valid + /// + /// ```jsx + /// const Component = () => { + /// const count = 0; + /// return
{count > 0 && Count: {count}}
; + /// } + /// ``` + /// + /// ```jsx + /// const Component = () => { + /// const items = []; + /// return
{!!items.length && }
; + /// } + /// ``` + /// + /// ```jsx + /// const Component = () => { + /// const user = null; + /// return
{user ? : null}
; + /// } + /// ``` + /// + /// ```jsx + /// const Component = () => { + /// const condition = false; + /// return
{condition ? : }
; + /// } + /// ``` + /// + /// ```jsx + /// const Component = () => { + /// const isReady = true; + /// return
{isReady && }
; + /// } + /// ``` + + pub NoLeakedRender{ + version: "next", + name: "noLeakedRender", + language: "jsx", + domains: &[RuleDomain::React], + sources: &[ + RuleSource::EslintReact("no-leaked-render").inspired(), + ], + recommended: false, + } +} + +impl Rule for NoLeakedRender { + type Query = Semantic; + type State = bool; + type Signals = Option; + type Options = NoLeakedRenderOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + let query = ctx.query(); + let model = ctx.model(); + + if !is_inside_jsx_expression(query.syntax()) { + return None; + } + + match query { + NoLeakedRenderQuery::JsLogicalExpression(exp) => { + let op = exp.operator().ok()?; + + if op != JsLogicalOperator::LogicalAnd { + return None; + } + let left = exp.left().ok()?; + + let is_left_hand_side_safe = matches!( + left, + AnyJsExpression::JsUnaryExpression(_) + | AnyJsExpression::JsCallExpression(_) + | AnyJsExpression::JsBinaryExpression(_) + ); + + if is_left_hand_side_safe { + return None; + } + + let mut is_nested_left_hand_side_safe = false; + + let mut stack = vec![left.clone()]; + + // Traverse the expression tree iteratively using a stack + // This allows us to check nested expressions without recursion + while let Some(current) = stack.pop() { + match current { + AnyJsExpression::JsLogicalExpression(expr) => { + let left = expr.left().ok()?.omit_parentheses(); + let right = expr.right().ok()?.omit_parentheses(); + stack.push(left); + stack.push(right); + } + AnyJsExpression::JsParenthesizedExpression(expr) => { + stack.push(expr.expression().ok()?.omit_parentheses()); + } + // If we find expressions that coerce to boolean (unary, call, binary), + // then the entire expression is considered safe + AnyJsExpression::JsUnaryExpression(_) + | AnyJsExpression::JsCallExpression(_) + | AnyJsExpression::JsBinaryExpression(_) => { + is_nested_left_hand_side_safe = true; + break; + } + _ => {} + } + } + + if is_nested_left_hand_side_safe { + return None; + } + + if let AnyJsExpression::JsIdentifierExpression(ident) = &left { + let name = ident.name().ok()?; + + // Use the semantic model to resolve the variable binding and check + // if it's initialized with a boolean literal. This allows us to + // handle cases like: + // let isOpen = false; // This is safe + // return
{isOpen && }
; // This should pass + if let Some(binding) = model.binding(&name) + && binding + .tree() + .declaration() + .and_then(|declaration| { + if let AnyJsBindingDeclaration::JsVariableDeclarator(declarator) = + declaration + { + Some(declarator) + } else { + None + } + }) + .and_then(|declarator| declarator.initializer()) + .and_then(|initializer| initializer.expression().ok()) + .and_then(|expr| { + if let AnyJsExpression::AnyJsLiteralExpression(literal) = expr { + Some(literal) + } else { + None + } + }) + .and_then(|literal| literal.value_token().ok()) + .is_some_and(|token| matches!(token.text_trimmed(), "true" | "false")) + { + return None; + } + } + + let is_literal = matches!(left, AnyJsExpression::AnyJsLiteralExpression(_)); + if is_literal && left.to_trimmed_text().is_empty() { + return None; + } + + Some(true) + } + NoLeakedRenderQuery::JsConditionalExpression(expr) => { + let alternate = expr.alternate().ok()?; + let is_alternate_identifier = + matches!(alternate, AnyJsExpression::JsIdentifierExpression(_)); + let is_jsx_element_alt = matches!(alternate, AnyJsExpression::JsxTagExpression(_)); + if !is_alternate_identifier || is_jsx_element_alt { + return None; + } + + Some(true) + } + } + } + + fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { + let node = ctx.query(); + + match node { + NoLeakedRenderQuery::JsLogicalExpression(_) => { + Some( + RuleDiagnostic::new( + rule_category!(), + node.range(), + markup! { + "Potential leaked value that might cause unintended rendering." + }, + ) + .note(markup! { + "JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output." + }) + .note(markup! { + "Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression." + }) + ) + } + NoLeakedRenderQuery::JsConditionalExpression(_) => { + Some( + RuleDiagnostic::new( + rule_category!(), + node.range(), + markup! { + "Potential leaked value that might cause unintended rendering." + }, + ) + .note(markup! { + "This happens when you use ternary operators in JSX with alternate values that could be variables." + }) + .note(markup! { + "Replace with a safe alternate value like an empty string , null or another JSX element." + }) + ) + } + } + } +} + +declare_node_union! { + pub NoLeakedRenderQuery = JsLogicalExpression | JsConditionalExpression +} + +fn is_inside_jsx_expression(node: &JsSyntaxNode) -> bool { + node.ancestors().any(|ancestor| { + JsxExpressionChild::can_cast(ancestor.kind()) + || JsxExpressionAttributeValue::can_cast(ancestor.kind()) + || JsxTagExpression::can_cast(ancestor.kind()) + }) +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx new file mode 100644 index 000000000000..acca87e420b2 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx @@ -0,0 +1,83 @@ +// should generate diagnostics + +const Example1 = () => { + return ( + <> + {0 && } + {'' && } + {NaN && } + + ); +}; + +const Component1 = ({ count, title }) => { + return
{count && title}
; +}; + +const Component2 = ({ count }) => { + return
{count && There are {count} results}
; +}; + +const Component3 = ({ elements }) => { + return
{elements.length && }
; +}; + +const Component4 = ({ nestedCollection }) => { + return ( +
{nestedCollection.elements.length && }
+ ); +}; + +const Component5 = ({ elements }) => { + return
{elements[0] && }
; +}; + +const Component6 = ({ numberA, numberB }) => { + return
{(numberA || numberB) && {numberA + numberB}}
; +}; + +const MyComponent1 = () => { + return ( + <> + {someCondition && ( +
+

hello

+
+ )} + + ); +}; + +const MyComponent2 = () => { + return <>{someCondition && }; +}; + +const MyComponent3 = () => { + return
{maybeObject && (isFoo ? : )}
; +}; + +const MyComponent4 = () => { + return ; +}; + +const MyComponent5 = () => { + return ; +}; + +const isOpen1 = 0; +const Component7 = () => { + return 0} />; +}; + +const Component8 = ({ count, title }) => { + return
{(((((count))))) && ((title))}
; +}; + +const Component9 = ({ data }) => { + return
{(((((data)))) && (((((data.value))))))}
; +}; + +const Component = ({ value }) => { + return
{(((value))) && }
; +}; + diff --git a/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx.snap new file mode 100644 index 000000000000..c07711cd5ef3 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx.snap @@ -0,0 +1,445 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.jsx +--- +# Input +```jsx +// should generate diagnostics + +const Example1 = () => { + return ( + <> + {0 && } + {'' && } + {NaN && } + + ); +}; + +const Component1 = ({ count, title }) => { + return
{count && title}
; +}; + +const Component2 = ({ count }) => { + return
{count && There are {count} results}
; +}; + +const Component3 = ({ elements }) => { + return
{elements.length && }
; +}; + +const Component4 = ({ nestedCollection }) => { + return ( +
{nestedCollection.elements.length && }
+ ); +}; + +const Component5 = ({ elements }) => { + return
{elements[0] && }
; +}; + +const Component6 = ({ numberA, numberB }) => { + return
{(numberA || numberB) && {numberA + numberB}}
; +}; + +const MyComponent1 = () => { + return ( + <> + {someCondition && ( +
+

hello

+
+ )} + + ); +}; + +const MyComponent2 = () => { + return <>{someCondition && }; +}; + +const MyComponent3 = () => { + return
{maybeObject && (isFoo ? : )}
; +}; + +const MyComponent4 = () => { + return ; +}; + +const MyComponent5 = () => { + return ; +}; + +const isOpen1 = 0; +const Component7 = () => { + return 0} />; +}; + +const Component8 = ({ count, title }) => { + return
{(((((count))))) && ((title))}
; +}; + +const Component9 = ({ data }) => { + return
{(((((data)))) && (((((data.value))))))}
; +}; + +const Component = ({ value }) => { + return
{(((value))) && }
; +}; + + +``` + +# Diagnostics +``` +invalid.jsx:6:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 4 │ return ( + 5 │ <> + > 6 │ {0 && } + │ ^^^^^^^^^^^^^^^^^^ + 7 │ {'' && } + 8 │ {NaN && } + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:7:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 5 │ <> + 6 │ {0 && } + > 7 │ {'' && } + │ ^^^^^^^^^^^^^^^^^^^ + 8 │ {NaN && } + 9 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:8:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 6 │ {0 && } + 7 │ {'' && } + > 8 │ {NaN && } + │ ^^^^^^^^^^^^^^^^^^^^ + 9 │ + 10 │ ); + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:14:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 13 │ const Component1 = ({ count, title }) => { + > 14 │ return
{count && title}
; + │ ^^^^^^^^^^^^^^ + 15 │ }; + 16 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:18:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 17 │ const Component2 = ({ count }) => { + > 18 │ return
{count && There are {count} results}
; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 19 │ }; + 20 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:22:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 21 │ const Component3 = ({ elements }) => { + > 22 │ return
{elements.length && }
; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 23 │ }; + 24 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:27:9 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 25 │ const Component4 = ({ nestedCollection }) => { + 26 │ return ( + > 27 │
{nestedCollection.elements.length && }
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 28 │ ); + 29 │ }; + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:32:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 31 │ const Component5 = ({ elements }) => { + > 32 │ return
{elements[0] && }
; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 33 │ }; + 34 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:36:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 35 │ const Component6 = ({ numberA, numberB }) => { + > 36 │ return
{(numberA || numberB) && {numberA + numberB}}
; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 37 │ }; + 38 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:42:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 40 │ return ( + 41 │ <> + > 42 │ {someCondition && ( + │ ^^^^^^^^^^^^^^^^^^ + > 43 │
+ > 44 │

hello

+ > 45 │
+ > 46 │ )} + │ ^ + 47 │ + 48 │ ); + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:52:12 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 51 │ const MyComponent2 = () => { + > 52 │ return <>{someCondition && }; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 53 │ }; + 54 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:56:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 55 │ const MyComponent3 = () => { + > 56 │ return
{maybeObject && (isFoo ? : )}
; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 57 │ }; + 58 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:60:29 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 59 │ const MyComponent4 = () => { + > 60 │ return ; + │ ^^^^^^^^^^^^^^^^^^^^^ + 61 │ }; + 62 │ + + i This happens when you use ternary operators in JSX with alternate values that could be variables. + + i Replace with a safe alternate value like an empty string , null or another JSX element. + + +``` + +``` +invalid.jsx:64:29 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 63 │ const MyComponent5 = () => { + > 64 │ return ; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 65 │ }; + 66 │ + + i This happens when you use ternary operators in JSX with alternate values that could be variables. + + i Replace with a safe alternate value like an empty string , null or another JSX element. + + +``` + +``` +invalid.jsx:64:29 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 63 │ const MyComponent5 = () => { + > 64 │ return ; + │ ^^^^^^^^^^^^^^^^^^^^^^^ + 65 │ }; + 66 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:69:24 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 67 │ const isOpen1 = 0; + 68 │ const Component7 = () => { + > 69 │ return 0} />; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 70 │ }; + 71 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:73:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 72 │ const Component8 = ({ count, title }) => { + > 73 │ return
{(((((count))))) && ((title))}
; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 74 │ }; + 75 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:77:16 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 76 │ const Component9 = ({ data }) => { + > 77 │ return
{(((((data)))) && (((((data.value))))))}
; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 78 │ }; + 79 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` + +``` +invalid.jsx:81:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 80 │ const Component = ({ value }) => { + > 81 │ return
{(((value))) && }
; + │ ^^^^^^^^^^^^^^^^^^^^^^^ + 82 │ }; + 83 │ + + i JavaScript's && operator returns the left value when it's falsy (e.g., 0, NaN, ''). React will render that value, causing unexpected UI output. + + i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/valid.jsx new file mode 100644 index 000000000000..8f8c6fbf5eb9 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/valid.jsx @@ -0,0 +1,74 @@ +// /* should not generate diagnostics */ +const Component1 = () => { + return
{customTitle || defaultTitle}
; +}; + +const Component2 = ({ elements }) => { + return
{elements}
; +}; + +const Component3 = ({ elements }) => { + return
There are {elements.length} elements
; +}; + +const Component4 = ({ elements, count }) => { + return
{!count && 'No results found'}
; +}; + +const Component5 = ({ elements }) => { + return
{!!elements.length && }
; +}; + +const Component6 = ({ elements }) => { + return
{Boolean(elements.length) && }
; +}; + +const Component7 = ({ elements }) => { + return
{elements.length > 0 && }
; +}; + +const Component8 = ({ elements }) => { + return
{elements.length ? : null}
; +}; + +const Component9 = ({ elements, count }) => { + return
{count ? : null}
; +}; + +const Component10 = ({ elements, count }) => { + return
{count ? : }
; +}; + +const Component11 = ({ elements, count }) => { + return
{!!count && }
; +}; + +const Component12 = ({ elements, count }) => { + return ( +
+
{direction ? (direction === 'down' ? '▼' : '▲') : ''}
+
{containerName.length > 0 ? 'Loading several stuff' : 'Loading'}
+
+ ); +}; + +const Component13 = ({ direction }) => { + return ( +
+
{!!direction && direction === 'down' && '▼'}
+
{direction === 'down' && !!direction && '▼'}
+
{direction === 'down' || (!!direction && '▼')}
+
{(!display || display === DISPLAY.WELCOME) && foo}
+
+ ); +}; + +const isOpen1 = true; +const Component14 = () => { + return 0} />; +}; + +const isOpen2 = false; +const Component15 = () => { + return 0} />; +}; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/valid.jsx.snap b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/valid.jsx.snap new file mode 100644 index 000000000000..52b3aa64bf6e --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/valid.jsx.snap @@ -0,0 +1,82 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.jsx +--- +# Input +```jsx +// /* should not generate diagnostics */ +const Component1 = () => { + return
{customTitle || defaultTitle}
; +}; + +const Component2 = ({ elements }) => { + return
{elements}
; +}; + +const Component3 = ({ elements }) => { + return
There are {elements.length} elements
; +}; + +const Component4 = ({ elements, count }) => { + return
{!count && 'No results found'}
; +}; + +const Component5 = ({ elements }) => { + return
{!!elements.length && }
; +}; + +const Component6 = ({ elements }) => { + return
{Boolean(elements.length) && }
; +}; + +const Component7 = ({ elements }) => { + return
{elements.length > 0 && }
; +}; + +const Component8 = ({ elements }) => { + return
{elements.length ? : null}
; +}; + +const Component9 = ({ elements, count }) => { + return
{count ? : null}
; +}; + +const Component10 = ({ elements, count }) => { + return
{count ? : }
; +}; + +const Component11 = ({ elements, count }) => { + return
{!!count && }
; +}; + +const Component12 = ({ elements, count }) => { + return ( +
+
{direction ? (direction === 'down' ? '▼' : '▲') : ''}
+
{containerName.length > 0 ? 'Loading several stuff' : 'Loading'}
+
+ ); +}; + +const Component13 = ({ direction }) => { + return ( +
+
{!!direction && direction === 'down' && '▼'}
+
{direction === 'down' && !!direction && '▼'}
+
{direction === 'down' || (!!direction && '▼')}
+
{(!display || display === DISPLAY.WELCOME) && foo}
+
+ ); +}; + +const isOpen1 = true; +const Component14 = () => { + return 0} />; +}; + +const isOpen2 = false; +const Component15 = () => { + return 0} />; +}; + +``` diff --git a/crates/biome_rule_options/src/lib.rs b/crates/biome_rule_options/src/lib.rs index 572e16ede6d1..1763a1792c53 100644 --- a/crates/biome_rule_options/src/lib.rs +++ b/crates/biome_rule_options/src/lib.rs @@ -120,6 +120,7 @@ pub mod no_irregular_whitespace; pub mod no_jsx_literals; pub mod no_label_var; pub mod no_label_without_control; +pub mod no_leaked_render; pub mod no_magic_numbers; pub mod no_misleading_character_class; pub mod no_misleading_instantiator; diff --git a/crates/biome_rule_options/src/no_leaked_render.rs b/crates/biome_rule_options/src/no_leaked_render.rs new file mode 100644 index 000000000000..137d41616df8 --- /dev/null +++ b/crates/biome_rule_options/src/no_leaked_render.rs @@ -0,0 +1,9 @@ +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))] +pub struct NoLeakedRenderOptions {} + +impl biome_deserialize::Merge for NoLeakedRenderOptions { + fn merge_with(&mut self, _other: Self) {} +} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 52bd18cca3a6..a698d8eff23d 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1901,6 +1901,11 @@ See https://biomejs.dev/linter/rules/no-jsx-literals */ noJsxLiterals?: NoJsxLiteralsConfiguration; /** + * Prevent problematic leaked values from being rendered. +See https://biomejs.dev/linter/rules/no-leaked-render + */ + noLeakedRender?: NoLeakedRenderConfiguration; + /** * Disallow Promises to be used in places where they are almost certainly a mistake. See https://biomejs.dev/linter/rules/no-misused-promises */ @@ -3560,6 +3565,9 @@ export type NoIncrementDecrementConfiguration = export type NoJsxLiteralsConfiguration = | RulePlainConfiguration | RuleWithNoJsxLiteralsOptions; +export type NoLeakedRenderConfiguration = + | RulePlainConfiguration + | RuleWithNoLeakedRenderOptions; export type NoMisusedPromisesConfiguration = | RulePlainConfiguration | RuleWithNoMisusedPromisesOptions; @@ -4947,6 +4955,10 @@ export interface RuleWithNoJsxLiteralsOptions { level: RulePlainConfiguration; options?: NoJsxLiteralsOptions; } +export interface RuleWithNoLeakedRenderOptions { + level: RulePlainConfiguration; + options?: NoLeakedRenderOptions; +} export interface RuleWithNoMisusedPromisesOptions { fix?: FixKind; level: RulePlainConfiguration; @@ -6258,6 +6270,7 @@ export interface NoJsxLiteralsOptions { */ noStrings?: boolean; } +export type NoLeakedRenderOptions = {}; export type NoMisusedPromisesOptions = {}; export type NoNextAsyncClientComponentOptions = {}; export type NoParametersOnlyUsedInRecursionOptions = {}; @@ -7088,6 +7101,7 @@ export type Category = | "lint/nursery/noImportCycles" | "lint/nursery/noIncrementDecrement" | "lint/nursery/noJsxLiterals" + | "lint/nursery/noLeakedRender" | "lint/nursery/noMissingGenericFamilyKeyword" | "lint/nursery/noMisusedPromises" | "lint/nursery/noNextAsyncClientComponent" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 4d81747d236c..d11ad2dd84f0 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -3666,6 +3666,13 @@ }, "additionalProperties": false }, + "NoLeakedRenderConfiguration": { + "oneOf": [ + { "$ref": "#/$defs/RulePlainConfiguration" }, + { "$ref": "#/$defs/RuleWithNoLeakedRenderOptions" } + ] + }, + "NoLeakedRenderOptions": { "type": "object" }, "NoMagicNumbersConfiguration": { "oneOf": [ { "$ref": "#/$defs/RulePlainConfiguration" }, @@ -5107,6 +5114,13 @@ { "type": "null" } ] }, + "noLeakedRender": { + "description": "Prevent problematic leaked values from being rendered.\nSee https://biomejs.dev/linter/rules/no-leaked-render", + "anyOf": [ + { "$ref": "#/$defs/NoLeakedRenderConfiguration" }, + { "type": "null" } + ] + }, "noMisusedPromises": { "description": "Disallow Promises to be used in places where they are almost certainly a mistake.\nSee https://biomejs.dev/linter/rules/no-misused-promises", "anyOf": [ @@ -7015,6 +7029,15 @@ "additionalProperties": false, "required": ["level"] }, + "RuleWithNoLeakedRenderOptions": { + "type": "object", + "properties": { + "level": { "$ref": "#/$defs/RulePlainConfiguration" }, + "options": { "$ref": "#/$defs/NoLeakedRenderOptions" } + }, + "additionalProperties": false, + "required": ["level"] + }, "RuleWithNoMagicNumbersOptions": { "type": "object", "properties": {