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 NoUselessMemo;
impl super::Rule for NoUselessMemo {
fn name(&self) -> &str {
"no_useless_memo"
}
fn run(&self, ctx: &RuleContext<'_>) -> Vec<Issue> {
let mut visitor = UselessMemoVisitor {
issues: Vec::new(),
source_text: ctx.source_text,
file_path: ctx.file_path,
};
visitor.visit_program(ctx.program);
visitor.issues
}
}
struct UselessMemoVisitor<'a> {
issues: Vec<Issue>,
source_text: &'a str,
file_path: &'a Path,
}
impl<'a> Visit<'a> for UselessMemoVisitor<'_> {
fn visit_call_expression(&mut self, expr: &CallExpression<'a>) {
if let Some(hook_name) = memo_hook_name(expr) {
if expr.arguments.len() == 2 {
if let Some(Expression::ArrayExpression(arr)) = expr.arguments[1].as_expression() {
if arr.elements.is_empty() {
self.emit(hook_name, expr.span);
}
}
}
}
walk::walk_call_expression(self, expr);
}
}
impl UselessMemoVisitor<'_> {
fn emit(&mut self, hook: &str, span: Span) {
let (line, col) = offset_to_line_col(self.source_text, span.start);
let alt = if hook == "useMemo" {
"a module-level constant or variable"
} else {
"a module-level function"
};
self.issues.push(Issue {
rule: "no_useless_memo".to_string(),
message: format!(
"`{hook}` with empty `[]` deps never recomputes — equivalent to {alt}. \
Move the value outside the component to eliminate hook overhead."
),
file: self.file_path.to_path_buf(),
line,
column: col,
severity: Severity::Warning,
});
}
}
fn memo_hook_name<'a>(expr: &'a CallExpression<'_>) -> Option<&'a str> {
match &expr.callee {
Expression::Identifier(id) => match id.name.as_str() {
"useMemo" | "useCallback" => Some(id.name.as_str()),
_ => None,
},
Expression::StaticMemberExpression(m) => match m.property.name.as_str() {
"useMemo" | "useCallback" => Some(m.property.name.as_str()),
_ => None,
},
_ => None,
}
}