2727//! const userName = computed(() => props.user.name)
2828//! ```
2929
30- use memchr:: memmem;
31-
3230use crate :: diagnostic:: { LintDiagnostic , Severity } ;
31+ use oxc_ast:: ast:: { BindingPattern , CallExpression , Expression , Program , VariableDeclarator } ;
32+ use oxc_ast_visit:: { Visit , walk:: walk_variable_declarator} ;
33+ use oxc_span:: Span ;
3334
3435use super :: { ScriptLintResult , ScriptRule , ScriptRuleMeta } ;
3536
@@ -39,6 +40,10 @@ static META: ScriptRuleMeta = ScriptRuleMeta {
3940 default_severity : Severity :: Warning ,
4041} ;
4142
43+ const MESSAGE : & str = "Avoid deeply nested destructuring in defineProps" ;
44+ const HELP : & str =
45+ "Use simple destructuring and access nested properties via computed or direct prop access" ;
46+
4247/// Disallow deep destructuring in defineProps
4348pub struct NoDeepDestructureInProps {
4449 /// Maximum allowed nesting depth (default: 1)
@@ -51,103 +56,126 @@ impl Default for NoDeepDestructureInProps {
5156 }
5257}
5358
54- impl NoDeepDestructureInProps {
55- /// Check if a destructuring pattern has nested objects beyond max_depth
56- fn has_deep_nesting ( pattern : & str , max_depth : usize ) -> bool {
57- let mut depth: usize = 0 ;
58- let mut max_seen: usize = 0 ;
59-
60- for c in pattern. chars ( ) {
61- match c {
62- '{' => {
63- depth += 1 ;
64- max_seen = max_seen. max ( depth) ;
65- }
66- '}' => {
67- depth = depth. saturating_sub ( 1 ) ;
68- }
69- _ => { }
70- }
71- }
72-
73- // max_seen > max_depth means we have deeper nesting than allowed
74- // For max_depth = 1, we allow { a, b } but not { a: { b } }
75- max_seen > max_depth
59+ impl ScriptRule for NoDeepDestructureInProps {
60+ fn meta ( & self ) -> & ' static ScriptRuleMeta {
61+ & META
7662 }
7763
78- /// Extract the destructuring pattern from a defineProps call
79- fn extract_destructure_pattern ( source : & str , define_props_pos : usize ) -> Option < & str > {
80- // Look backwards from defineProps to find the pattern
81- let before = & source[ ..define_props_pos] ;
82-
83- // Find the last '=' before defineProps
84- let eq_pos = before. rfind ( '=' ) ?;
85-
86- // Find 'const' or 'let' before that
87- let decl_start = before[ ..eq_pos]
88- . rfind ( "const " )
89- . or_else ( || before[ ..eq_pos] . rfind ( "let " ) ) ?;
64+ #[ inline]
65+ fn uses_ast ( & self ) -> bool {
66+ true
67+ }
9068
91- // Extract the pattern between const/let and =
92- let pattern_start = if before[ decl_start..] . starts_with ( "const " ) {
93- decl_start + 6
94- } else {
95- decl_start + 4
69+ #[ inline]
70+ fn check_program < ' a > (
71+ & self ,
72+ program : & ' a Program < ' a > ,
73+ _source : & str ,
74+ offset : usize ,
75+ result : & mut ScriptLintResult ,
76+ ) {
77+ let mut visitor = NoDeepDestructureInPropsVisitor {
78+ max_depth : self . max_depth ,
79+ offset,
80+ result,
9681 } ;
82+ visitor. visit_program ( program) ;
83+ }
84+ }
9785
98- let pattern = before[ pattern_start..eq_pos] . trim ( ) ;
86+ struct NoDeepDestructureInPropsVisitor < ' result > {
87+ max_depth : usize ,
88+ offset : usize ,
89+ result : & ' result mut ScriptLintResult ,
90+ }
9991
100- // Only interested in destructuring patterns (starts with {)
101- if pattern. starts_with ( '{' ) {
102- Some ( pattern)
103- } else {
104- None
92+ impl < ' a > Visit < ' a > for NoDeepDestructureInPropsVisitor < ' _ > {
93+ fn visit_variable_declarator ( & mut self , it : & VariableDeclarator < ' a > ) {
94+ if let Some ( init) = & it. init
95+ && is_define_props_call ( init)
96+ && let Some ( span) = deep_object_pattern_span ( & it. id , self . max_depth )
97+ {
98+ let start = self . offset as u32 + span. start ;
99+ let end = self . offset as u32 + span. end ;
100+ self . result . add_diagnostic (
101+ LintDiagnostic :: warn ( META . name , MESSAGE , start, end) . with_help ( HELP ) ,
102+ ) ;
105103 }
104+ walk_variable_declarator ( self , it) ;
106105 }
107106}
108107
109- impl ScriptRule for NoDeepDestructureInProps {
110- fn meta ( & self ) -> & ' static ScriptRuleMeta {
111- & META
108+ fn deep_object_pattern_span ( pattern : & BindingPattern < ' _ > , max_depth : usize ) -> Option < Span > {
109+ match pattern {
110+ BindingPattern :: ObjectPattern ( object) => {
111+ ( object_pattern_depth ( pattern, 0 ) > max_depth) . then_some ( object. span )
112+ }
113+ _ => None ,
112114 }
115+ }
113116
114- fn check ( & self , source : & str , offset : usize , result : & mut ScriptLintResult ) {
115- let bytes = source. as_bytes ( ) ;
116-
117- // Fast bailout: check if defineProps is used
118- if memmem:: find ( bytes, b"defineProps" ) . is_none ( ) {
119- return ;
117+ fn object_pattern_depth ( pattern : & BindingPattern < ' _ > , current_depth : usize ) -> usize {
118+ match pattern {
119+ BindingPattern :: ObjectPattern ( object) => {
120+ let depth = current_depth + 1 ;
121+ let property_depth = object
122+ . properties
123+ . iter ( )
124+ . map ( |property| object_pattern_depth ( & property. value , depth) )
125+ . max ( )
126+ . unwrap_or ( depth) ;
127+ let rest_depth = object
128+ . rest
129+ . as_ref ( )
130+ . map ( |rest| object_pattern_depth ( & rest. argument , depth) )
131+ . unwrap_or ( depth) ;
132+ property_depth. max ( rest_depth)
120133 }
121-
122- // Find all occurrences of defineProps
123- let finder = memmem:: Finder :: new ( b"defineProps" ) ;
124- let mut search_start = 0 ;
125-
126- while let Some ( pos) = finder. find ( & bytes[ search_start..] ) {
127- let abs_pos = search_start + pos;
128- search_start = abs_pos + 11 ;
129-
130- // Extract the destructuring pattern
131- if let Some ( pattern) = Self :: extract_destructure_pattern ( source, abs_pos)
132- && Self :: has_deep_nesting ( pattern, self . max_depth )
133- {
134- // Find pattern position
135- let pattern_start = source[ ..abs_pos] . rfind ( pattern) . unwrap_or ( abs_pos) ;
136-
137- result. add_diagnostic (
138- LintDiagnostic :: warn (
139- META . name ,
140- "Avoid deeply nested destructuring in defineProps" ,
141- ( offset + pattern_start) as u32 ,
142- ( offset + pattern_start + pattern. len ( ) ) as u32 ,
143- )
144- . with_help (
145- "Use simple destructuring and access nested properties via computed or direct prop access" ,
146- ) ,
147- ) ;
148- }
134+ BindingPattern :: ArrayPattern ( array) => {
135+ let element_depth = array
136+ . elements
137+ . iter ( )
138+ . flatten ( )
139+ . map ( |element| object_pattern_depth ( element, current_depth) )
140+ . max ( )
141+ . unwrap_or ( current_depth) ;
142+ let rest_depth = array
143+ . rest
144+ . as_ref ( )
145+ . map ( |rest| object_pattern_depth ( & rest. argument , current_depth) )
146+ . unwrap_or ( current_depth) ;
147+ element_depth. max ( rest_depth)
148+ }
149+ BindingPattern :: AssignmentPattern ( assignment) => {
150+ object_pattern_depth ( & assignment. left , current_depth)
149151 }
152+ BindingPattern :: BindingIdentifier ( _) => current_depth,
153+ }
154+ }
155+
156+ fn is_define_props_call ( expression : & Expression < ' _ > ) -> bool {
157+ let Expression :: CallExpression ( call) = expression else {
158+ return false ;
159+ } ;
160+ if call_is_named ( call, "defineProps" ) {
161+ return true ;
150162 }
163+ if call_is_named ( call, "withDefaults" )
164+ && let Some ( first) = call
165+ . arguments
166+ . first ( )
167+ . and_then ( |argument| argument. as_expression ( ) )
168+ {
169+ return is_define_props_call ( first) ;
170+ }
171+ false
172+ }
173+
174+ fn call_is_named ( call : & CallExpression < ' _ > , name : & str ) -> bool {
175+ matches ! (
176+ & call. callee,
177+ Expression :: Identifier ( identifier) if identifier. name. as_str( ) == name
178+ )
151179}
152180
153181#[ cfg( test) ]
@@ -200,27 +228,43 @@ mod tests {
200228 }
201229
202230 #[ test]
203- fn test_has_deep_nesting ( ) {
204- assert ! ( !NoDeepDestructureInProps :: has_deep_nesting( "{ a, b }" , 1 ) ) ;
205- assert ! ( !NoDeepDestructureInProps :: has_deep_nesting(
206- "{ a, b = 1 }" ,
207- 1
208- ) ) ;
209- assert ! ( NoDeepDestructureInProps :: has_deep_nesting(
210- "{ a: { b } }" ,
211- 1
212- ) ) ;
213- assert ! ( NoDeepDestructureInProps :: has_deep_nesting(
214- "{ a: { b: { c } } }" ,
215- 1
216- ) ) ;
217- assert ! ( NoDeepDestructureInProps :: has_deep_nesting(
218- "{ a: { b: { c } } }" ,
219- 2
220- ) ) ;
221- assert ! ( !NoDeepDestructureInProps :: has_deep_nesting(
222- "{ a: { b } }" ,
223- 2
224- ) ) ;
231+ fn test_invalid_deep_destructure_with_defaults ( ) {
232+ let linter = create_linter ( ) ;
233+ let result = linter. lint (
234+ "const { user: { name } } = withDefaults(defineProps<{ user: User }>(), {})" ,
235+ 0 ,
236+ ) ;
237+ assert_eq ! ( result. warning_count, 1 ) ;
238+ }
239+
240+ #[ test]
241+ fn test_valid_with_higher_max_depth ( ) {
242+ let mut linter = ScriptLinter :: new ( ) ;
243+ linter. add_rule ( Box :: new ( NoDeepDestructureInProps { max_depth : 2 } ) ) ;
244+ let result = linter. lint (
245+ "const { user: { name } } = defineProps<{ user: User }>()" ,
246+ 0 ,
247+ ) ;
248+ assert_eq ! ( result. warning_count, 0 ) ;
249+ }
250+
251+ #[ test]
252+ fn test_define_props_string_not_matched ( ) {
253+ let linter = create_linter ( ) ;
254+ let result = linter. lint (
255+ r#"const text = "const { user: { name } } = defineProps()""# ,
256+ 0 ,
257+ ) ;
258+ assert_eq ! ( result. warning_count, 0 ) ;
259+ }
260+
261+ #[ test]
262+ fn test_unrelated_deep_destructure_not_matched ( ) {
263+ let linter = create_linter ( ) ;
264+ let result = linter. lint (
265+ "const { user: { name } } = createProps<{ user: User }>()" ,
266+ 0 ,
267+ ) ;
268+ assert_eq ! ( result. warning_count, 0 ) ;
225269 }
226270}
0 commit comments