Skip to content

Commit b4706d8

Browse files
committed
fix(noLeakedRender): fixed the issue with diagnostics for some components
1 parent a244fc4 commit b4706d8

File tree

3 files changed

+194
-24
lines changed

3 files changed

+194
-24
lines changed

crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use biome_analyze::{Rule, RuleDiagnostic, context::RuleContext, declare_lint_rule};
1+
use biome_analyze::{
2+
Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
24
use biome_console::markup;
35
use biome_js_semantic::{Binding, SemanticModel};
46
use biome_js_syntax::{
@@ -89,12 +91,43 @@ declare_lint_rule! {
8991
/// const isReady = true;
9092
/// return <div>{isReady && <Content />}</div>;
9193
/// }
94+
///
95+
/// ### `validStrategies`
96+
///
97+
/// An array containing "coerce", "ternary", or both (default: ["ternary", "coerce"])
98+
/// Decide which strategies are considered valid to prevent leaked renders (at least 1 is required).
99+
/// The "coerce" option will transform the conditional of the JSX expression to a boolean.
100+
/// The "ternary" option transforms the binary expression into a ternay expression return `null`
101+
/// for falsy values.
102+
///
103+
/// ```json
104+
/// {
105+
/// "noLeakedRender": {
106+
/// "options": {
107+
/// "validStrategies": ["coerce", "ternary"]
108+
/// }
109+
/// }
110+
/// }
111+
///
112+
/// {
113+
/// "noLeakedRender": {
114+
/// "options": {
115+
/// "validStrategies": ["coerce"]
116+
/// }
117+
/// }
118+
/// }
119+
/// ```
120+
92121
/// ```
93122
94123
pub NoLeakedRender{
95124
version: "next",
96125
name: "noLeakedRender",
97-
language: "js",
126+
language: "jsx",
127+
domains: &[RuleDomain::React],
128+
sources: &[
129+
RuleSource::EslintReact("no-leaked-render").same(),
130+
],
98131
recommended: false,
99132
}
100133
}
@@ -103,11 +136,7 @@ const COERCE_STRATEGY: &str = "coerce";
103136
const TERNARY_STRATEGY: &str = "ternary";
104137
const TERNARY_INVALID_ALTERNATE_VALUES: &[&str] = &["null", "undefined", "false"];
105138

106-
const DEFAULT_VALID_STRATEGIES: &[&str] = &[TERNARY_STRATEGY, COERCE_STRATEGY];
107-
108-
pub enum NoLeakedRenderState {
109-
NoPotentialLeakedRender,
110-
}
139+
const DEFAULT_VALID_STRATEGIES: &[&str] = &[COERCE_STRATEGY, TERNARY_STRATEGY];
111140

112141
fn get_variable_from_context(
113142
model: &SemanticModel,
@@ -132,7 +161,7 @@ declare_node_union! {
132161

133162
impl Rule for NoLeakedRender {
134163
type Query = Semantic<Query>;
135-
type State = NoLeakedRenderState;
164+
type State = bool;
136165
type Signals = Option<Self::State>;
137166
type Options = NoLeakedRenderOptions;
138167

@@ -179,25 +208,27 @@ impl Rule for NoLeakedRender {
179208
if let AnyJsExpression::JsIdentifierExpression(ident) = &left {
180209
let name = ident.name().ok()?;
181210

182-
let binding = get_variable_from_context(
211+
if let Some(binding) = get_variable_from_context(
183212
model,
184213
left_node,
185214
name.to_trimmed_text().trim(),
186-
)?;
187-
188-
let declaration = binding.tree().declaration()?;
215+
) {
216+
let declaration = binding.tree().declaration()?;
189217

190-
if let AnyJsBindingDeclaration::JsVariableDeclarator(declarator) =
191-
declaration
192-
{
193-
let initializer = declarator.initializer()?;
194-
let initializer = initializer.expression().ok()?;
218+
if let AnyJsBindingDeclaration::JsVariableDeclarator(declarator) =
219+
declaration
220+
{
221+
let initializer = declarator.initializer()?;
222+
let initializer = initializer.expression().ok()?;
195223

196-
if let AnyJsExpression::AnyJsLiteralExpression(literal) = initializer {
197-
let literal = literal.value_token().ok()?;
224+
if let AnyJsExpression::AnyJsLiteralExpression(literal) =
225+
initializer
226+
{
227+
let literal = literal.value_token().ok()?;
198228

199-
if matches!(literal.text_trimmed(), "true" | "false") {
200-
return None;
229+
if matches!(literal.text_trimmed(), "true" | "false") {
230+
return None;
231+
}
201232
}
202233
}
203234
}
@@ -209,7 +240,7 @@ impl Rule for NoLeakedRender {
209240
return None;
210241
}
211242

212-
return Some(NoLeakedRenderState::NoPotentialLeakedRender);
243+
return Some(true);
213244
}
214245
Query::JsConditionalExpression(expr) => {
215246
if valid_strategies
@@ -223,13 +254,13 @@ impl Rule for NoLeakedRender {
223254
.iter()
224255
.any(|&s| alternate.to_trimmed_text() == s);
225256

226-
let is_jsx_element_alt = matches!(alternate, AnyJsExpression::sxTagExpression(_));
257+
let is_jsx_element_alt = matches!(alternate, AnyJsExpression::JsxTagExpression(_));
227258

228259
if !is_problematic_alternate || is_jsx_element_alt {
229260
return None;
230261
}
231262

232-
return Some(NoLeakedRenderState::NoPotentialLeakedRender);
263+
return Some(true);
233264
}
234265
}
235266
}

crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/coerce/invalid.jsx.snap

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,84 @@ invalid.jsx:47:15 lint/nursery/noLeakedRender ━━━━━━━━━━━
305305
i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression.
306306
307307
308+
```
309+
310+
```
311+
invalid.jsx:51:15 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
312+
313+
i Potential leaked value that might cause unintended rendering.
314+
315+
50 │ const MyComponent2 = () => {
316+
> 51return <div>{maybeObject && (isFoo ? <Aaa /> : <Bbb />)}</div>;
317+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
318+
52};
319+
53 │
320+
321+
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.
322+
323+
i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression.
324+
325+
326+
```
327+
328+
```
329+
invalid.jsx:59:29 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
330+
331+
i Potential leaked value that might cause unintended rendering.
332+
333+
58 │ const MyComponent4 = () => {
334+
> 59return <Something checked={cond && isIndeterminate ? false : isChecked} />;
335+
^^^^^^^^^^^^^^^^^^^^^^^
336+
60};
337+
61 │
338+
339+
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.
340+
341+
i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression.
342+
343+
344+
```
345+
346+
```
347+
invalid.jsx:65:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
348+
349+
i Potential leaked value that might cause unintended rendering.
350+
351+
63 │ return (
352+
64 │ <>
353+
> 65 │ {someCondition && (
354+
^^^^^^^^^^^^^^^^^^
355+
> 66 │ <div>
356+
> 67 │ <p>hello</p>
357+
> 68 │ </div>
358+
> 69 │ )}
359+
│ ^
360+
70 │ </>
361+
71 │ );
362+
363+
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.
364+
365+
i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression.
366+
367+
368+
```
369+
370+
```
371+
invalid.jsx:75:12 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
372+
373+
i Potential leaked value that might cause unintended rendering.
374+
375+
74 │ const MyComponent6 = () => {
376+
> 75return <>{someCondition && <SomeComponent prop1={val1} prop2={val2} />}</>;
377+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
378+
76};
379+
77 │
380+
381+
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.
382+
383+
i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression.
384+
385+
308386
```
309387

310388
```

crates/biome_js_analyze/tests/specs/nursery/noLeakedRender/invalid.jsx.snap

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,25 @@ invalid.jsx:7:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━
9797
i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression.
9898
9999
100+
```
101+
102+
```
103+
invalid.jsx:8:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
104+
105+
i Potential leaked value that might cause unintended rendering.
106+
107+
6 │ {0 && <Something />}
108+
7 │ {'' && <Something />}
109+
> 8 │ {NaN && <Something />}
110+
│ ^^^^^^^^^^^^^^^^^^^^
111+
9 │ </>
112+
10 │ );
113+
114+
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.
115+
116+
i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression.
117+
118+
100119
```
101120

102121
```
@@ -207,3 +226,45 @@ invalid.jsx:36:15 lint/nursery/noLeakedRender ━━━━━━━━━━━
207226
208227
209228
```
229+
230+
```
231+
invalid.jsx:42:5 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
232+
233+
i Potential leaked value that might cause unintended rendering.
234+
235+
40 │ return (
236+
41 │ <>
237+
> 42 │ {someCondition && (
238+
^^^^^^^^^^^^^^^^^^
239+
> 43 │ <div>
240+
> 44 │ <p>hello</p>
241+
> 45 │ </div>
242+
> 46 │ )}
243+
│ ^
244+
47 │ </>
245+
48 │ );
246+
247+
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.
248+
249+
i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression.
250+
251+
252+
```
253+
254+
```
255+
invalid.jsx:52:12 lint/nursery/noLeakedRender ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
256+
257+
i Potential leaked value that might cause unintended rendering.
258+
259+
51 │ const MyComponent2 = () => {
260+
> 52return <>{someCondition && <SomeComponent prop1={val1} prop2={val2} />}</>;
261+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
262+
53};
263+
54 │
264+
265+
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.
266+
267+
i Make sure the condition is explicitly boolean.Use !!value, value > 0, or a ternary expression.
268+
269+
270+
```

0 commit comments

Comments
 (0)