Skip to main content

react_auditor/rules/react/
effect_deps_complete.rs

1use oxc_ast::ast::Program;
2use oxc_ast_visit::Visit;
3use oxc_semantic::Semantic;
4
5use crate::rules::{Rule, RuleFinding, RuleMeta, Severity};
6
7pub struct EffectDepsComplete;
8
9const RULE_META: RuleMeta = RuleMeta {
10    id: "no-missing-deps",
11    default_severity: Severity::Warning,
12    category: "react",
13    description: "useEffect/useMemo/useCallback should have a dependency array",
14};
15
16fn is_hook_with_deps(name: &str) -> bool {
17    matches!(
18        name,
19        "useEffect" | "useMemo" | "useCallback" | "useLayoutEffect" | "useInsertionEffect"
20    )
21}
22
23impl Rule for EffectDepsComplete {
24    fn meta(&self) -> &RuleMeta {
25        &RULE_META
26    }
27
28    fn run(&self, program: &Program, _semantic: &Semantic, source_text: &str) -> Vec<RuleFinding> {
29        let mut collector = DepsCollector {
30            findings: Vec::new(),
31            source: source_text,
32        };
33        collector.visit_program(program);
34        collector.findings
35    }
36}
37
38struct DepsCollector<'a> {
39    findings: Vec<RuleFinding>,
40    source: &'a str,
41}
42
43impl<'a> DepsCollector<'a> {
44    fn add_finding(&mut self, start: usize, msg: String) {
45        let line = self.source[..start].lines().count().max(1);
46        let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
47        self.findings.push(RuleFinding {
48            line,
49            column: col + 1,
50            message: msg,
51        });
52    }
53}
54
55impl<'a> Visit<'a> for DepsCollector<'a> {
56    fn visit_call_expression(&mut self, expr: &oxc_ast::ast::CallExpression<'a>) {
57        let name = if let oxc_ast::ast::Expression::Identifier(ident) = &expr.callee {
58            ident.name.as_str()
59        } else {
60            return;
61        };
62
63        if !is_hook_with_deps(name) {
64            return;
65        }
66
67        if expr.arguments.len() < 2 {
68            self.add_finding(
69                expr.span.start as usize,
70                format!("`{name}` is missing a dependency array"),
71            );
72            return;
73        }
74
75        if let Some(oxc_ast::ast::Argument::ArrayExpression(arr)) = expr.arguments.last()
76            && arr.elements.is_empty()
77            && (name == "useEffect" || name == "useLayoutEffect" || name == "useInsertionEffect")
78        {
79            self.add_finding(
80                expr.span.start as usize,
81                format!("`{name}` has empty deps array — likely a bug"),
82            );
83        }
84    }
85}