react_auditor/rules/react/
effect_deps_complete.rs1use 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}