use std::path::Path;
use oxc_ast::ast::{ArrayExpressionElement, CallExpression, Expression};
use oxc_ast_visit::{walk, Visit};
use oxc_span::Span;
use crate::{
rules::{Issue, RuleContext, Severity},
utils::offset_to_line_col,
};
pub struct NoUnstableHookDeps;
impl super::Rule for NoUnstableHookDeps {
fn name(&self) -> &str {
"no_unstable_hook_deps"
}
fn run(&self, ctx: &RuleContext<'_>) -> Vec<Issue> {
let mut visitor = HookDepsVisitor {
issues: Vec::new(),
source_text: ctx.source_text,
file_path: ctx.file_path,
};
visitor.visit_program(ctx.program);
visitor.issues
}
}
struct HookDepsVisitor<'a> {
issues: Vec<Issue>,
source_text: &'a str,
file_path: &'a Path,
}
const DEP_HOOKS: &[&str] = &[
"useEffect",
"useMemo",
"useCallback",
"useLayoutEffect",
"useInsertionEffect",
];
impl<'a> Visit<'a> for HookDepsVisitor<'_> {
fn visit_call_expression(&mut self, expr: &CallExpression<'a>) {
if let Some(hook_name) = hook_name(expr) {
if let Some(last_arg) = expr.arguments.last() {
if let Some(Expression::ArrayExpression(deps_array)) = last_arg.as_expression() {
for element in &deps_array.elements {
self.check_dep(hook_name, element);
}
}
}
}
walk::walk_call_expression(self, expr);
}
}
impl HookDepsVisitor<'_> {
fn check_dep(&mut self, hook_name: &str, element: &ArrayExpressionElement<'_>) {
match element {
ArrayExpressionElement::ObjectExpression(obj) => {
self.emit(
hook_name,
"object literal `{}`",
"Replace with individual stable primitive values or refs.",
obj.span,
);
}
ArrayExpressionElement::ArrayExpression(arr) => {
self.emit(
hook_name,
"nested array `[]`",
"Nested arrays in deps are always a new reference. Use individual stable values instead.",
arr.span,
);
}
ArrayExpressionElement::ArrowFunctionExpression(arrow) => {
self.emit(
hook_name,
"arrow function",
"Functions in deps create a new reference every render. Wrap with useCallback or move outside the component.",
arrow.span,
);
}
ArrayExpressionElement::FunctionExpression(func) => {
self.emit(
hook_name,
"function expression",
"Functions in deps create a new reference every render. Wrap with useCallback or move outside the component.",
func.span,
);
}
_ => {}
}
}
fn emit(&mut self, hook: &str, kind: &str, suggestion: &str, span: Span) {
let (line, col) = offset_to_line_col(self.source_text, span.start);
self.issues.push(Issue {
rule: "no_unstable_hook_deps".to_string(),
message: format!(
"`{hook}` dependency contains a {kind} — new reference on every render causes \
the hook to run every render. {suggestion}"
),
file: self.file_path.to_path_buf(),
line,
column: col,
severity: Severity::Warning,
});
}
}
fn hook_name<'a>(expr: &'a CallExpression<'_>) -> Option<&'a str> {
match &expr.callee {
Expression::Identifier(id) => {
let name = id.name.as_str();
if DEP_HOOKS.contains(&name) {
Some(name)
} else {
None
}
}
Expression::StaticMemberExpression(member) => {
let name = member.property.name.as_str();
if DEP_HOOKS.contains(&name) {
Some(name)
} else {
None
}
}
_ => None,
}
}