diff --git a/.changeset/curvy-worlds-invent.md b/.changeset/curvy-worlds-invent.md new file mode 100644 index 000000000000..4ffae7f0177f --- /dev/null +++ b/.changeset/curvy-worlds-invent.md @@ -0,0 +1,30 @@ +--- +'@biomejs/biome': patch +--- +Fixed [#8491](https://github.com/biomejs/biome/issues/8491): Resolved false positive errors for safe boolean expressions. There are still pending fixes. Head to [#8491 (comment)](https://github.com/biomejs/biome/issues/8491#issuecomment-3669243551) for more details + +This new change will check for safe boolean expressions in variable declarations. + +For example, + +Valid: + +```jsx +let isOne = 1; +let isPositiveNumber = number > 0; + +return
{isOne && "One" } { isPositiveNumber && "Is positive" }
+``` + +Invalid: + +```jsx +let emptyStr = ''; +let isZero = 0; + +return ( +
+ {emptyStr && 'Empty String'} {isZero && 'Number is zero'}{' '} +
+); +``` 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 index 7acaaf49db20..e621e63e5083 100644 --- a/crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs +++ b/crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs @@ -2,10 +2,11 @@ use biome_analyze::{ Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule, }; use biome_console::markup; +use biome_js_semantic::SemanticModel; use biome_js_syntax::{ - AnyJsExpression, JsConditionalExpression, JsLogicalExpression, JsLogicalOperator, JsSyntaxNode, - JsxExpressionChild, JsxTagExpression, binding_ext::AnyJsBindingDeclaration, - jsx_ext::AnyJsxElement, + AnyJsExpression, AnyJsLiteralExpression, JsConditionalExpression, JsLogicalExpression, + JsLogicalOperator, JsReferenceIdentifier, JsSyntaxNode, JsUnaryOperator, JsxExpressionChild, + JsxTagExpression, binding_ext::AnyJsBindingDeclaration, jsx_ext::AnyJsxElement, }; use biome_rowan::{AstNode, declare_node_union}; use biome_rule_options::no_leaked_render::NoLeakedRenderOptions; @@ -98,7 +99,7 @@ declare_lint_rule! { impl Rule for NoLeakedRender { type Query = Semantic; - type State = bool; + type State = (); type Signals = Option; type Options = NoLeakedRenderOptions; @@ -119,14 +120,7 @@ impl Rule for NoLeakedRender { } 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 { + if is_left_hand_side_safe(&left)? { return None; } @@ -149,13 +143,12 @@ impl Rule for NoLeakedRender { } // 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; + expr => { + if is_left_hand_side_safe(&expr)? { + is_nested_left_hand_side_safe = true; + break; + } } - _ => {} } } @@ -165,47 +158,32 @@ impl Rule for NoLeakedRender { 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 + // if it's initialized with safe expressions. + // Safe: let isOpen = false; let isError = errorCount > 0; let isEqual = a === b; + // Unsafe: let emptyStr = ''; + + if let Some(variable) = find_variable(model, &name) { + match variable { + AnyJsExpression::AnyJsLiteralExpression(expr) => { + if is_unsafe_literal(&expr)? { + return Some(()); } - }) - .and_then(|literal| literal.value_token().ok()) - .is_some_and(|token| matches!(token.text_trimmed(), "true" | "false")) - { - return None; + } + AnyJsExpression::JsUnaryExpression(_) + | AnyJsExpression::JsBinaryExpression(_) + | AnyJsExpression::JsCallExpression(_) => return None, + _ => {} + } } } - - let is_literal = matches!(left, AnyJsExpression::AnyJsLiteralExpression(_)); - if is_literal && left.to_trimmed_text().is_empty() { - return None; + if let AnyJsExpression::AnyJsLiteralExpression(literal) = left + && is_unsafe_literal(&literal)? + { + return Some(()); } - Some(true) + Some(()) } NoLeakedRenderQuery::JsConditionalExpression(expr) => { let alternate = expr.alternate().ok()?; @@ -216,7 +194,7 @@ impl Rule for NoLeakedRender { return None; } - Some(true) + Some(()) } } } @@ -276,3 +254,73 @@ fn is_inside_jsx_expression(node: &JsSyntaxNode) -> Option { || AnyJsxElement::can_cast(parent.kind()), ) } + +fn find_variable(model: &SemanticModel, name: &JsReferenceIdentifier) -> Option { + let binding = model.binding(name)?; + let declaration = binding.tree().declaration()?; + let AnyJsBindingDeclaration::JsVariableDeclarator(declarator) = declaration else { + return None; + }; + + let expr = declarator.initializer()?.expression().ok()?; + Some(expr) +} + +fn is_unsafe_literal(literal: &AnyJsLiteralExpression) -> Option { + match literal { + AnyJsLiteralExpression::JsStringLiteralExpression(str) => { + if str.inner_string_text().ok()?.text().is_empty() { + return Some(true); + } + None + } + AnyJsLiteralExpression::JsNullLiteralExpression(_) => Some(true), + AnyJsLiteralExpression::JsNumberLiteralExpression(num) => { + let value = num.value_token().ok()?; + + if is_numeric_zero(value.text_trimmed()) { + return Some(true); + } + + None + } + + AnyJsLiteralExpression::JsBigintLiteralExpression(bnum) => { + let value = bnum.value_token().ok()?; + let literal = value.text_trimmed(); + let num = literal.strip_suffix("n").unwrap_or(literal); + + if is_numeric_zero(num) { + return Some(true); + } + None + } + _ => None, + } +} + +fn is_numeric_zero(num: &str) -> bool { + if let Ok(num) = num.parse::() { + return num == 0; + } + + if let Ok(num) = num.parse::() { + return num == 0.0; + } + + false +} + +fn is_left_hand_side_safe(expr: &AnyJsExpression) -> Option { + match expr { + AnyJsExpression::JsCallExpression(_) | AnyJsExpression::JsBinaryExpression(_) => Some(true), + AnyJsExpression::JsUnaryExpression(unary) => { + if unary.operator().ok()? == JsUnaryOperator::Minus { + Some(false) + } else { + Some(true) + } + } + _ => Some(false), + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx index e9d248104325..31e23a07b6c3 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx +++ b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx @@ -4,6 +4,11 @@ const Example1 = () => { return ( <> {0 && } + {0.0 && } + {-0 && } + {-0.0 && } + {0n && } + {-0n && } {'' && } {NaN && } @@ -67,3 +72,15 @@ const MyComponent5 = ({ data }) => { const MyComponent6 = ({ value }) => { return
{(((value))) && }
; }; + +const isNaN = NaN; +const isZero = 0; +const emptyStr = ''; +const MyComponent7 = () => { + return <> +
{isNan && 'NaN'}
+
{isZero && 'Zero'}
+
{emptyStr && "Empty String"}
+ +} + 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 index e8428ddc47a3..5b1a32b7f3fc 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx.snap @@ -10,6 +10,11 @@ const Example1 = () => { return ( <> {0 && } + {0.0 && } + {-0 && } + {-0.0 && } + {0n && } + {-0n && } {'' && } {NaN && } @@ -74,6 +79,18 @@ const MyComponent6 = ({ value }) => { return
{(((value))) && }
; }; +const isNaN = NaN; +const isZero = 0; +const emptyStr = ''; +const MyComponent7 = () => { + return <> +
{isNan && 'NaN'}
+
{isZero && 'Zero'}
+
{emptyStr && "Empty String"}
+ +} + + ``` # Diagnostics @@ -86,8 +103,8 @@ invalid.jsx:6:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━ 5 │ <> > 6 │ {0 && } │ ^^^^^^^^^^^^^^^^^^ - 7 │ {'' && } - 8 │ {NaN && } + 7 │ {0.0 && } + 8 │ {-0 && } 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. @@ -105,10 +122,10 @@ invalid.jsx:7:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━ 5 │ <> 6 │ {0 && } - > 7 │ {'' && } - │ ^^^^^^^^^^^^^^^^^^^ - 8 │ {NaN && } - 9 │ + > 7 │ {0.0 && } + │ ^^^^^^^^^^^^^^^^^^^^ + 8 │ {-0 && } + 9 │ {-0.0 && } 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. @@ -125,11 +142,116 @@ invalid.jsx:8:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━ i Potential leaked value that might cause unintended rendering. 6 │ {0 && } - 7 │ {'' && } - > 8 │ {NaN && } + 7 │ {0.0 && } + > 8 │ {-0 && } + │ ^^^^^^^^^^^^^^^^^^^ + 9 │ {-0.0 && } + 10 │ {0n && } + + 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. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +invalid.jsx:9:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 7 │ {0.0 && } + 8 │ {-0 && } + > 9 │ {-0.0 && } + │ ^^^^^^^^^^^^^^^^^^^^^ + 10 │ {0n && } + 11 │ {-0n && } + + 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. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +invalid.jsx:10:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 8 │ {-0 && } + 9 │ {-0.0 && } + > 10 │ {0n && } + │ ^^^^^^^^^^^^^^^^^^^ + 11 │ {-0n && } + 12 │ {'' && } + + 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. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +invalid.jsx:11:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 9 │ {-0.0 && } + 10 │ {0n && } + > 11 │ {-0n && } + │ ^^^^^^^^^^^^^^^^^^^^ + 12 │ {'' && } + 13 │ {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. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +invalid.jsx:12:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 10 │ {0n && } + 11 │ {-0n && } + > 12 │ {'' && } + │ ^^^^^^^^^^^^^^^^^^^ + 13 │ {NaN && } + 14 │ + + 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. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +invalid.jsx:13:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 11 │ {-0n && } + 12 │ {'' && } + > 13 │ {NaN && } │ ^^^^^^^^^^^^^^^^^^^^ - 9 │ - 10 │ ); + 14 │ + 15 │ ); 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. @@ -141,15 +263,15 @@ invalid.jsx:8:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━ ``` ``` -invalid.jsx:14:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:19:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i Potential leaked value that might cause unintended rendering. - 13 │ const Component1 = ({ count, title }) => { - > 14 │ return
{count && title}
; + 18 │ const Component1 = ({ count, title }) => { + > 19 │ return
{count && title}
; │ ^^^^^^^^^^^^^^ - 15 │ }; - 16 │ + 20 │ }; + 21 │ 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. @@ -161,15 +283,15 @@ invalid.jsx:14:15 lint/nursery/noLeakedRender ━━━━━━━━━━━ ``` ``` -invalid.jsx:18:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:23:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i Potential leaked value that might cause unintended rendering. - 17 │ const Component2 = ({ count }) => { - > 18 │ return
{count && There are {count} results}
; + 22 │ const Component2 = ({ count }) => { + > 23 │ return
{count && There are {count} results}
; │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 19 │ }; - 20 │ + 24 │ }; + 25 │ 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. @@ -181,15 +303,15 @@ invalid.jsx:18:15 lint/nursery/noLeakedRender ━━━━━━━━━━━ ``` ``` -invalid.jsx:22:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:27:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i Potential leaked value that might cause unintended rendering. - 21 │ const Component3 = ({ elements }) => { - > 22 │ return
{elements.length && }
; + 26 │ const Component3 = ({ elements }) => { + > 27 │ return
{elements.length && }
; │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 23 │ }; - 24 │ + 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. @@ -201,16 +323,16 @@ invalid.jsx:22:15 lint/nursery/noLeakedRender ━━━━━━━━━━━ ``` ``` -invalid.jsx:27:9 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:32:9 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i Potential leaked value that might cause unintended rendering. - 25 │ const Component4 = ({ nestedCollection }) => { - 26 │ return ( - > 27 │
{nestedCollection.elements.length && }
+ 30 │ const Component4 = ({ nestedCollection }) => { + 31 │ return ( + > 32 │
{nestedCollection.elements.length && }
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 28 │ ); - 29 │ }; + 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. @@ -222,15 +344,15 @@ invalid.jsx:27:9 lint/nursery/noLeakedRender ━━━━━━━━━━━ ``` ``` -invalid.jsx:32:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:37:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i Potential leaked value that might cause unintended rendering. - 31 │ const Component5 = ({ elements }) => { - > 32 │ return
{elements[0] && }
; + 36 │ const Component5 = ({ elements }) => { + > 37 │ return
{elements[0] && }
; │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 33 │ }; - 34 │ + 38 │ }; + 39 │ 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. @@ -242,15 +364,15 @@ invalid.jsx:32:15 lint/nursery/noLeakedRender ━━━━━━━━━━━ ``` ``` -invalid.jsx:36:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:41:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i Potential leaked value that might cause unintended rendering. - 35 │ const Component6 = ({ numberA, numberB }) => { - > 36 │ return
{(numberA || numberB) && {numberA + numberB}}
; + 40 │ const Component6 = ({ numberA, numberB }) => { + > 41 │ return
{(numberA || numberB) && {numberA + numberB}}
; │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 37 │ }; - 38 │ + 42 │ }; + 43 │ 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. @@ -262,21 +384,21 @@ invalid.jsx:36:15 lint/nursery/noLeakedRender ━━━━━━━━━━━ ``` ``` -invalid.jsx:42:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:47:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i Potential leaked value that might cause unintended rendering. - 40 │ return ( - 41 │ <> - > 42 │ {someCondition && ( + 45 │ return ( + 46 │ <> + > 47 │ {someCondition && ( │ ^^^^^^^^^^^^^^^^^^ - > 43 │
- > 44 │

hello

- > 45 │
- > 46 │ )} + > 48 │
+ > 49 │

hello

+ > 50 │
+ > 51 │ )} │ ^ - 47 │ - 48 │ ); + 52 │ + 53 │ ); 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. @@ -288,15 +410,15 @@ invalid.jsx:42:5 lint/nursery/noLeakedRender ━━━━━━━━━━━ ``` ``` -invalid.jsx:52:12 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:57:12 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i Potential leaked value that might cause unintended rendering. - 51 │ const MyComponent2 = () => { - > 52 │ return <>{someCondition && }; + 56 │ const MyComponent2 = () => { + > 57 │ return <>{someCondition && }; │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 53 │ }; - 54 │ + 58 │ }; + 59 │ 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. @@ -308,15 +430,15 @@ invalid.jsx:52:12 lint/nursery/noLeakedRender ━━━━━━━━━━━ ``` ``` -invalid.jsx:56:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:61:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i Potential leaked value that might cause unintended rendering. - 55 │ const MyComponent3 = () => { - > 56 │ return
{maybeObject && (isFoo ? : )}
; + 60 │ const MyComponent3 = () => { + > 61 │ return
{maybeObject && (isFoo ? : )}
; │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 57 │ }; - 58 │ + 62 │ }; + 63 │ 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. @@ -328,15 +450,15 @@ invalid.jsx:56:15 lint/nursery/noLeakedRender ━━━━━━━━━━━ ``` ``` -invalid.jsx:60:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:65:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i Potential leaked value that might cause unintended rendering. - 59 │ const MyComponent4 = ({ count, title }) => { - > 60 │ return
{(((((count))))) && ((title))}
; + 64 │ const MyComponent4 = ({ count, title }) => { + > 65 │ return
{(((((count))))) && ((title))}
; │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 61 │ }; - 62 │ + 66 │ }; + 67 │ 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. @@ -348,15 +470,78 @@ invalid.jsx:60:15 lint/nursery/noLeakedRender ━━━━━━━━━━━ ``` ``` -invalid.jsx:68:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +invalid.jsx:73:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ i Potential leaked value that might cause unintended rendering. - 67 │ const MyComponent6 = ({ value }) => { - > 68 │ return
{(((value))) && }
; + 72 │ const MyComponent6 = ({ value }) => { + > 73 │ return
{(((value))) && }
; │ ^^^^^^^^^^^^^^^^^^^^^^^ - 69 │ }; - 70 │ + 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. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +invalid.jsx:81:10 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 79 │ const MyComponent7 = () => { + 80 │ return <> + > 81 │
{isNan && 'NaN'}
+ │ ^^^^^^^^^^^^^^ + 82 │
{isZero && 'Zero'}
+ 83 │
{emptyStr && "Empty String"}
+ + 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. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +invalid.jsx:82:10 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 80 │ return <> + 81 │
{isNan && 'NaN'}
+ > 82 │
{isZero && 'Zero'}
+ │ ^^^^^^^^^^^^^^^^ + 83 │
{emptyStr && "Empty String"}
+ 84 │ + + 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. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` + +``` +invalid.jsx:83:9 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Potential leaked value that might cause unintended rendering. + + 81 │
{isNan && 'NaN'}
+ 82 │
{isZero && 'Zero'}
+ > 83 │
{emptyStr && "Empty String"}
+ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ + 84 │ + 85 │ } 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. diff --git a/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/valid.jsx b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/valid.jsx index 8f8c6fbf5eb9..a79f80defe4e 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/valid.jsx +++ b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/valid.jsx @@ -72,3 +72,23 @@ const isOpen2 = false; const Component15 = () => { return 0} />; }; + +const errorCount = 1; +const Component16 = () => { + return
{errorCount && 'Error'}
; +}; + +const isErr = errorCount > 0; +const Component17 = () => { + return
{isErr && 'Error'}
; +}; + +const someCondition = !!x; +const Component18 = () => { + return
{someCondition && 'hello'}
; +}; + +const isEqual = a == b; +const Component19 = () => { + return
{isEqual && 'Equal'}
; +}; 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 index 52b3aa64bf6e..bd429338edfc 100644 --- a/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/valid.jsx.snap +++ b/crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/valid.jsx.snap @@ -79,4 +79,24 @@ const Component15 = () => { return 0} />; }; +const errorCount = 1; +const Component16 = () => { + return
{errorCount && 'Error'}
; +}; + +const isErr = errorCount > 0; +const Component17 = () => { + return
{isErr && 'Error'}
; +}; + +const someCondition = !!x; +const Component18 = () => { + return
{someCondition && 'hello'}
; +}; + +const isEqual = a == b; +const Component19 = () => { + return
{isEqual && 'Equal'}
; +}; + ```