@@ -2,8 +2,9 @@ use crate::services::semantic::Semantic;
22use biome_analyze:: { Rule , RuleDiagnostic , context:: RuleContext , declare_lint_rule} ;
33use biome_console:: markup;
44use biome_js_syntax:: {
5- AnyJsExpression , AnyJsStatement , JsExportDefaultExpressionClause , JsMethodObjectMember ,
6- JsObjectExpression , JsVariableDeclaration ,
5+ AnyJsAssignmentPattern , AnyJsExpression , AnyJsStatement , JsAssignmentExpression ,
6+ JsCallExpression , JsExportDefaultExpressionClause , JsMethodObjectMember , JsObjectExpression ,
7+ JsVariableDeclaration ,
78} ;
89use biome_rowan:: { AstNode , AstNodeList , AstSeparatedList , TextRange } ;
910use 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.
8991pub 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.
144161fn 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.
154175fn 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.
174198fn 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.
210278fn 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+ }
0 commit comments