Skip to content

Commit 6d79c67

Browse files
authored
refactor(patina): inspect prop destructuring with ast (#2727)
1 parent d8acdb8 commit 6d79c67

1 file changed

Lines changed: 150 additions & 106 deletions

File tree

crates/vize_patina/src/rules/script/no_deep_destructure_in_props.rs

Lines changed: 150 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@
2727
//! const userName = computed(() => props.user.name)
2828
//! ```
2929
30-
use memchr::memmem;
31-
3230
use 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

3435
use 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
4348
pub 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

Comments
 (0)