use std::path::Path;
use oxc_ast::ast::{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 NoUseStateLazyInitMissing;
impl super::Rule for NoUseStateLazyInitMissing {
fn name(&self) -> &str {
"no_useState_lazy_init_missing"
}
fn run(&self, ctx: &RuleContext<'_>) -> Vec<Issue> {
let mut visitor = LazyInitVisitor {
issues: Vec::new(),
source_text: ctx.source_text,
file_path: ctx.file_path,
};
visitor.visit_program(ctx.program);
visitor.issues
}
}
struct LazyInitVisitor<'a> {
issues: Vec<Issue>,
source_text: &'a str,
file_path: &'a Path,
}
const EXPENSIVE_CALLS: &[&str] = &[
"JSON.parse",
"JSON.stringify",
"structuredClone",
"localStorage.getItem",
"sessionStorage.getItem",
"JSON",
];
impl<'a> Visit<'a> for LazyInitVisitor<'_> {
fn visit_call_expression(&mut self, expr: &CallExpression<'a>) {
if is_use_state(expr) {
if let Some(arg) = expr.arguments.first() {
if let Some(init_expr) = arg.as_expression() {
let already_lazy = matches!(
init_expr,
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
);
if !already_lazy {
if let Expression::CallExpression(inner_call) = init_expr {
let fn_name = callee_name(&inner_call.callee);
let is_expensive =
EXPENSIVE_CALLS.iter().any(|&e| fn_name.starts_with(e))
|| !inner_call.arguments.is_empty();
if is_expensive {
self.emit(&fn_name, inner_call.span);
}
}
}
}
}
}
walk::walk_call_expression(self, expr);
}
}
impl LazyInitVisitor<'_> {
fn emit(&mut self, fn_name: &str, span: Span) {
let (line, col) = offset_to_line_col(self.source_text, span.start);
self.issues.push(Issue {
rule: "no_useState_lazy_init_missing".to_string(),
message: format!(
"`useState({fn_name}(...))` evaluates `{fn_name}` on every render, \
but React only uses the initial value on mount. \
Use the lazy initializer form: `useState(() => {fn_name}(...))`"
),
file: self.file_path.to_path_buf(),
line,
column: col,
severity: Severity::Warning,
});
}
}
fn is_use_state(expr: &CallExpression<'_>) -> bool {
match &expr.callee {
Expression::Identifier(id) => id.name.as_str() == "useState",
Expression::StaticMemberExpression(m) => m.property.name.as_str() == "useState",
_ => false,
}
}
fn callee_name(callee: &Expression<'_>) -> String {
match callee {
Expression::Identifier(id) => id.name.to_string(),
Expression::StaticMemberExpression(m) => {
format!("{}.{}", callee_name(&m.object), m.property.name)
}
_ => "fn".to_string(),
}
}