use memchr::memmem;
use crate::diagnostic::{LintDiagnostic, Severity};
use super::{ScriptLintResult, ScriptRule, ScriptRuleMeta};
static META: ScriptRuleMeta = ScriptRuleMeta {
name: "script/no-deep-destructure-in-props",
description: "Disallow deeply nested destructuring in defineProps",
default_severity: Severity::Warning,
};
pub struct NoDeepDestructureInProps {
pub max_depth: usize,
}
impl Default for NoDeepDestructureInProps {
fn default() -> Self {
Self { max_depth: 1 }
}
}
impl NoDeepDestructureInProps {
fn has_deep_nesting(pattern: &str, max_depth: usize) -> bool {
let mut depth: usize = 0;
let mut max_seen: usize = 0;
for c in pattern.chars() {
match c {
'{' => {
depth += 1;
max_seen = max_seen.max(depth);
}
'}' => {
depth = depth.saturating_sub(1);
}
_ => {}
}
}
max_seen > max_depth
}
fn extract_destructure_pattern(source: &str, define_props_pos: usize) -> Option<&str> {
let before = &source[..define_props_pos];
let eq_pos = before.rfind('=')?;
let decl_start = before[..eq_pos]
.rfind("const ")
.or_else(|| before[..eq_pos].rfind("let "))?;
let pattern_start = if before[decl_start..].starts_with("const ") {
decl_start + 6
} else {
decl_start + 4
};
let pattern = before[pattern_start..eq_pos].trim();
if pattern.starts_with('{') {
Some(pattern)
} else {
None
}
}
}
impl ScriptRule for NoDeepDestructureInProps {
fn meta(&self) -> &'static ScriptRuleMeta {
&META
}
fn check(&self, source: &str, offset: usize, result: &mut ScriptLintResult) {
let bytes = source.as_bytes();
if memmem::find(bytes, b"defineProps").is_none() {
return;
}
let finder = memmem::Finder::new(b"defineProps");
let mut search_start = 0;
while let Some(pos) = finder.find(&bytes[search_start..]) {
let abs_pos = search_start + pos;
search_start = abs_pos + 11;
if let Some(pattern) = Self::extract_destructure_pattern(source, abs_pos) {
if Self::has_deep_nesting(pattern, self.max_depth) {
let pattern_start = source[..abs_pos].rfind(pattern).unwrap_or(abs_pos);
result.add_diagnostic(
LintDiagnostic::warn(
META.name,
"Avoid deeply nested destructuring in defineProps",
(offset + pattern_start) as u32,
(offset + pattern_start + pattern.len()) as u32,
)
.with_help(
"Use simple destructuring and access nested properties via computed or direct prop access",
),
);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::NoDeepDestructureInProps;
use crate::rules::script::ScriptLinter;
fn create_linter() -> ScriptLinter {
let mut linter = ScriptLinter::new();
linter.add_rule(Box::new(NoDeepDestructureInProps::default()));
linter
}
#[test]
fn test_valid_simple_destructure() {
let linter = create_linter();
let result = linter.lint(
"const { name, count = 0 } = defineProps<{ name: string }>()",
0,
);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_valid_no_destructure() {
let linter = create_linter();
let result = linter.lint("const props = defineProps<{ name: string }>()", 0);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_invalid_deep_destructure() {
let linter = create_linter();
let result = linter.lint(
"const { user: { name } } = defineProps<{ user: User }>()",
0,
);
assert_eq!(result.warning_count, 1);
insta::assert_debug_snapshot!(result.diagnostics);
}
#[test]
fn test_invalid_very_deep_destructure() {
let linter = create_linter();
let result = linter.lint(
"const { config: { settings: { theme } } } = defineProps()",
0,
);
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_has_deep_nesting() {
assert!(!NoDeepDestructureInProps::has_deep_nesting("{ a, b }", 1));
assert!(!NoDeepDestructureInProps::has_deep_nesting(
"{ a, b = 1 }",
1
));
assert!(NoDeepDestructureInProps::has_deep_nesting(
"{ a: { b } }",
1
));
assert!(NoDeepDestructureInProps::has_deep_nesting(
"{ a: { b: { c } } }",
1
));
assert!(NoDeepDestructureInProps::has_deep_nesting(
"{ a: { b: { c } } }",
2
));
assert!(!NoDeepDestructureInProps::has_deep_nesting(
"{ a: { b } }",
2
));
}
}