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": {