Skip to content

Commit 5aa1e66

Browse files
committed
refactor(lint): fix compilation errors in vue reactivity rule
1 parent 6e1ca5d commit 5aa1e66

17 files changed

+247
-279
lines changed

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

Lines changed: 141 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ use crate::services::semantic::Semantic;
22
use biome_analyze::{Rule, RuleDiagnostic, context::RuleContext, declare_lint_rule};
33
use biome_console::markup;
44
use biome_js_syntax::{
5-
AnyJsExpression, AnyJsStatement, JsExportDefaultExpressionClause, JsMethodObjectMember,
6-
JsObjectExpression, JsVariableDeclaration,
5+
AnyJsAssignmentPattern, AnyJsExpression, AnyJsStatement, JsAssignmentExpression,
6+
JsCallExpression, JsExportDefaultExpressionClause, JsMethodObjectMember, JsObjectExpression,
7+
JsVariableDeclaration,
78
};
89
use biome_rowan::{AstNode, AstNodeList, AstSeparatedList, TextRange};
910
use biome_rule_options::no_setup_props_reactivity_loss::NoSetupPropsReactivityLossOptions;
@@ -86,8 +87,11 @@ declare_lint_rule! {
8687
}
8788
}
8889

90+
/// Represents the different types of reactivity loss violations that can occur.
8991
pub enum RuleState {
92+
/// Props destructured directly in setup function parameters.
9093
DestructuredParameter(TextRange),
94+
/// Props destructured in variable assignments within the setup function body.
9195
DestructuredAssignment(TextRange),
9296
}
9397

@@ -97,6 +101,11 @@ impl Rule for NoSetupPropsReactivityLoss {
97101
type Signals = Vec<Self::State>;
98102
type Options = NoSetupPropsReactivityLossOptions;
99103

104+
/// Analyzes Vue component exports to detect props reactivity loss patterns.
105+
///
106+
/// This method examines the default export of a Vue component, specifically
107+
/// looking for setup methods that might cause props to lose reactivity through
108+
/// destructuring or improper usage patterns.
100109
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
101110
let export_clause = ctx.query();
102111
let mut signals = Vec::new();
@@ -111,6 +120,10 @@ impl Rule for NoSetupPropsReactivityLoss {
111120
signals
112121
}
113122

123+
/// Creates diagnostic messages for detected reactivity loss violations.
124+
///
125+
/// Generates appropriate error messages and helpful notes based on the type
126+
/// of violation detected (parameter destructuring vs assignment destructuring).
114127
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
115128
match state {
116129
RuleState::DestructuredParameter(range) => Some(
@@ -141,6 +154,10 @@ impl Rule for NoSetupPropsReactivityLoss {
141154
}
142155
}
143156

157+
/// Searches for a 'setup' method within a Vue component object expression.
158+
///
159+
/// Iterates through the object members to find a method with the name 'setup',
160+
/// which is the Composition API setup function in Vue components.
144161
fn find_setup_method(obj_expr: &JsObjectExpression) -> Option<JsMethodObjectMember> {
145162
obj_expr.members().into_iter().flatten().find_map(|member| {
146163
let method = member.as_js_method_object_member()?;
@@ -151,6 +168,10 @@ fn find_setup_method(obj_expr: &JsObjectExpression) -> Option<JsMethodObjectMemb
151168
})
152169
}
153170

171+
/// Analyzes a setup method for props reactivity loss patterns.
172+
///
173+
/// Examines both the method parameters (for direct destructuring) and the method body
174+
/// (for destructuring assignments) to detect violations of Vue's reactivity rules.
154175
fn check_setup_method(setup_method: &JsMethodObjectMember, signals: &mut Vec<RuleState>) {
155176
let params = setup_method.parameters();
156177

@@ -160,8 +181,7 @@ fn check_setup_method(setup_method: &JsMethodObjectMember, signals: &mut Vec<Rul
160181
.as_any_js_formal_parameter()
161182
.and_then(|fp| fp.as_js_formal_parameter())
162183
&& let Ok(binding) = formal_param.binding()
163-
&& (binding.as_js_object_binding_pattern().is_some()
164-
|| binding.as_js_array_binding_pattern().is_some())
184+
&& binding.as_js_object_binding_pattern().is_some()
165185
{
166186
signals.push(RuleState::DestructuredParameter(binding.range()));
167187
}
@@ -171,6 +191,10 @@ fn check_setup_method(setup_method: &JsMethodObjectMember, signals: &mut Vec<Rul
171191
}
172192
}
173193

194+
/// Examines statements within the setup function body for props destructuring.
195+
///
196+
/// Focuses on variable declarations that might destructure the props parameter,
197+
/// which would cause reactivity loss in Vue's Composition API.
174198
fn check_setup_body_statements(
175199
statements: impl Iterator<Item = AnyJsStatement>,
176200
signals: &mut Vec<RuleState>,
@@ -193,20 +217,64 @@ fn check_setup_body_statements(
193217
})
194218
.map(|n| n.text().to_string());
195219

196-
statements
197-
.filter_map(|stmt| match stmt {
198-
AnyJsStatement::JsVariableStatement(var_stmt) => var_stmt.declaration().ok(),
199-
_ => None,
220+
statements.for_each(|stmt| match stmt {
221+
AnyJsStatement::JsVariableStatement(var_stmt) => {
222+
if let Ok(declaration) = var_stmt.declaration() {
223+
check_variable_declaration_for_props_destructuring(
224+
&declaration,
225+
signals,
226+
&props_param_name,
227+
);
228+
}
229+
}
230+
AnyJsStatement::JsExpressionStatement(expr_stmt) => {
231+
if let Ok(expr) = expr_stmt.expression() {
232+
// Handle direct assignment expressions
233+
if let AnyJsExpression::JsAssignmentExpression(assign_expr) = &expr {
234+
check_assignment_expression_for_props_destructuring(
235+
assign_expr,
236+
signals,
237+
&props_param_name,
238+
);
239+
}
240+
// Handle parenthesized assignment expressions
241+
else if let AnyJsExpression::JsParenthesizedExpression(paren_expr) = &expr
242+
&& let Ok(AnyJsExpression::JsAssignmentExpression(assign_expr)) =
243+
paren_expr.expression()
244+
{
245+
check_assignment_expression_for_props_destructuring(
246+
&assign_expr,
247+
signals,
248+
&props_param_name,
249+
);
250+
}
251+
}
252+
}
253+
_ => {}
254+
});
255+
}
256+
257+
/// Checks if a call expression is a Vue reactive helper function.
258+
///
259+
/// Returns true if the call is to toRefs, toRef, reactive, or ref functions,
260+
/// which preserve props reactivity when destructuring.
261+
fn is_reactive_helper_call(call_expr: &JsCallExpression) -> bool {
262+
call_expr
263+
.callee()
264+
.ok()
265+
.and_then(|callee| callee.as_js_identifier_expression().cloned())
266+
.and_then(|id_expr| id_expr.name().ok())
267+
.and_then(|name| name.value_token().ok())
268+
.is_some_and(|token| {
269+
let text = token.text_trimmed();
270+
matches!(text, "toRefs" | "toRef" | "reactive" | "ref")
200271
})
201-
.for_each(|declaration| {
202-
check_variable_declaration_for_props_destructuring(
203-
&declaration,
204-
signals,
205-
&props_param_name,
206-
)
207-
});
208272
}
209273

274+
/// Checks if a variable declaration destructures the props parameter.
275+
///
276+
/// Analyzes variable declarators to determine if they use destructuring patterns
277+
/// on the props parameter, while allowing reactive helpers like toRefs, toRef, etc.
210278
fn check_variable_declaration_for_props_destructuring(
211279
declaration: &JsVariableDeclaration,
212280
signals: &mut Vec<RuleState>,
@@ -217,10 +285,10 @@ fn check_variable_declaration_for_props_destructuring(
217285
};
218286

219287
for declarator in declaration.declarators().into_iter().flatten() {
220-
let has_destructuring_pattern = declarator.id().ok().is_some_and(|id| {
221-
id.as_js_object_binding_pattern().is_some()
222-
|| id.as_js_array_binding_pattern().is_some()
223-
});
288+
let has_destructuring_pattern = declarator
289+
.id()
290+
.ok()
291+
.is_some_and(|id| id.as_js_object_binding_pattern().is_some());
224292

225293
if !has_destructuring_pattern {
226294
continue;
@@ -240,18 +308,7 @@ fn check_variable_declaration_for_props_destructuring(
240308
.and_then(|name| name.value_token().ok())
241309
.is_some_and(|token| token.text_trimmed() == props_name),
242310
AnyJsExpression::JsCallExpression(call_expr) => {
243-
let is_reactive_helper = call_expr
244-
.callee()
245-
.ok()
246-
.and_then(|callee| callee.as_js_identifier_expression().cloned())
247-
.and_then(|id_expr| id_expr.name().ok())
248-
.and_then(|name| name.value_token().ok())
249-
.is_some_and(|token| {
250-
let text = token.text_trimmed();
251-
matches!(text, "toRefs" | "toRef" | "reactive" | "ref")
252-
});
253-
254-
!is_reactive_helper
311+
!is_reactive_helper_call(&call_expr)
255312
&& call_expr.arguments().ok().is_some_and(|args| {
256313
args.args().into_iter().flatten().any(|arg| {
257314
matches!(
@@ -274,3 +331,56 @@ fn check_variable_declaration_for_props_destructuring(
274331
}
275332
}
276333
}
334+
335+
/// Checks assignment expressions for props destructuring patterns.
336+
///
337+
/// Examines assignment expressions to detect patterns like `({ count } = props)`
338+
/// that would cause reactivity loss in Vue's Composition API.
339+
fn check_assignment_expression_for_props_destructuring(
340+
assign_expr: &JsAssignmentExpression,
341+
signals: &mut Vec<RuleState>,
342+
props_param_name: &Option<String>,
343+
) {
344+
let Some(props_name) = props_param_name.as_ref() else {
345+
return;
346+
};
347+
348+
// Check if the left side is a destructuring pattern
349+
if let Ok(left) = assign_expr.left()
350+
&& let AnyJsAssignmentPattern::JsObjectAssignmentPattern(object_assignment_pattern) = left
351+
{
352+
// Check if the right side is the props parameter
353+
if let Ok(right) = assign_expr.right() {
354+
let should_report = match right {
355+
AnyJsExpression::JsIdentifierExpression(id_expr) => id_expr
356+
.name()
357+
.ok()
358+
.and_then(|name| name.value_token().ok())
359+
.is_some_and(|token| token.text_trimmed() == props_name),
360+
AnyJsExpression::JsCallExpression(call_expr) => {
361+
!is_reactive_helper_call(&call_expr)
362+
&& call_expr.arguments().ok().is_some_and(|args| {
363+
args.args().into_iter().flatten().any(|arg| {
364+
matches!(
365+
arg.as_any_js_expression(),
366+
Some(AnyJsExpression::JsIdentifierExpression(id_expr))
367+
if id_expr
368+
.name()
369+
.ok()
370+
.and_then(|name| name.value_token().ok())
371+
.is_some_and(|token| token.text_trimmed() == props_name)
372+
)
373+
})
374+
})
375+
}
376+
_ => false,
377+
};
378+
379+
if should_report {
380+
signals.push(RuleState::DestructuredAssignment(
381+
object_assignment_pattern.range(),
382+
));
383+
}
384+
}
385+
}
386+
}

crates/biome_js_analyze/tests/specs/nursery/noSetupPropsReactivityLoss/array_destructuring.js

Lines changed: 0 additions & 6 deletions
This file was deleted.

crates/biome_js_analyze/tests/specs/nursery/noSetupPropsReactivityLoss/array_destructuring.js.snap

Lines changed: 0 additions & 32 deletions
This file was deleted.

crates/biome_js_analyze/tests/specs/nursery/noSetupPropsReactivityLoss/destructuring_assignment.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Destructuring assignment
2+
/* should generate diagnostics */
23
export default {
34
setup(props) {
45
const { count } = props

crates/biome_js_analyze/tests/specs/nursery/noSetupPropsReactivityLoss/destructuring_assignment.js.snap

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ expression: destructuring_assignment.js
55
# Input
66
```js
77
// Destructuring assignment
8+
/* should generate diagnostics */
89
export default {
910
setup(props) {
1011
const { count } = props
@@ -16,16 +17,16 @@ export default {
1617

1718
# Diagnostics
1819
```
19-
destructuring_assignment.js:4:11 lint/nursery/noSetupPropsReactivityLoss ━━━━━━━━━━━━━━━━━━━━━━━━━━━
20+
destructuring_assignment.js:5:11 lint/nursery/noSetupPropsReactivityLoss ━━━━━━━━━━━━━━━━━━━━━━━━━━━
2021
2122
i Destructuring props in the root scope of setup causes reactivity loss.
2223
23-
2 │ export default {
24-
3setup(props) {
25-
> 4 │ const { count } = props
24+
3 │ export default {
25+
4setup(props) {
26+
> 5 │ const { count } = props
2627
^^^^^^^^^^^^^^^^^
27-
5return () => h('div', count)
28-
6}
28+
6return () => h('div', count)
29+
7}
2930
3031
i Use the props object directly or destructure inside callbacks/render functions.
3132

crates/biome_js_analyze/tests/specs/nursery/noSetupPropsReactivityLoss/destructuring_params.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Destructuring in setup parameters
2+
/* should generate diagnostics */
23
export default {
34
setup({ count }) {
45
return () => h('div', count)

crates/biome_js_analyze/tests/specs/nursery/noSetupPropsReactivityLoss/destructuring_params.js.snap

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ expression: destructuring_params.js
55
# Input
66
```js
77
// Destructuring in setup parameters
8+
/* should generate diagnostics */
89
export default {
910
setup({ count }) {
1011
return () => h('div', count)
@@ -15,16 +16,16 @@ export default {
1516

1617
# Diagnostics
1718
```
18-
destructuring_params.js:3:9 lint/nursery/noSetupPropsReactivityLoss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19+
destructuring_params.js:4:9 lint/nursery/noSetupPropsReactivityLoss ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1920
2021
i Destructuring props in setup function parameters causes reactivity loss.
2122
22-
1 │ // Destructuring in setup parameters
23-
2 │ export default {
24-
> 3setup({ count }) {
23+
2 │ /* should generate diagnostics */
24+
3 │ export default {
25+
> 4setup({ count }) {
2526
│ ^^^^^^^^^
26-
4 │ return () => h('div', count)
27-
5 │ }
27+
5 │ return () => h('div', count)
28+
6 │ }
2829
2930
i Use the props object directly to maintain reactivity: `props.propertyName`
3031

0 commit comments

Comments
 (0)