use std::path::Path;
use oxc_ast::ast::{Expression, JSXAttributeItem, JSXAttributeValue, JSXOpeningElement};
use oxc_ast_visit::{walk, Visit};
use oxc_span::Span;
use crate::{
rules::{Issue, RuleContext, Severity},
utils::offset_to_line_col,
};
pub struct NoMathRandomInRender;
impl super::Rule for NoMathRandomInRender {
fn name(&self) -> &str {
"no_math_random_in_render"
}
fn run(&self, ctx: &RuleContext<'_>) -> Vec<Issue> {
let mut visitor = MathRandomVisitor {
issues: Vec::new(),
source_text: ctx.source_text,
file_path: ctx.file_path,
};
visitor.visit_program(ctx.program);
visitor.issues
}
}
struct MathRandomVisitor<'a> {
issues: Vec<Issue>,
source_text: &'a str,
file_path: &'a Path,
}
impl<'a> Visit<'a> for MathRandomVisitor<'_> {
fn visit_jsx_opening_element(&mut self, elem: &JSXOpeningElement<'a>) {
for attr_item in &elem.attributes {
if let JSXAttributeItem::Attribute(attr) = attr_item {
if let Some(JSXAttributeValue::ExpressionContainer(container)) = &attr.value {
if let Some(expr) = container.expression.as_expression() {
self.scan_expr(expr);
}
}
}
}
walk::walk_jsx_opening_element(self, elem);
}
}
impl MathRandomVisitor<'_> {
fn scan_expr(&mut self, expr: &Expression<'_>) {
match expr {
Expression::CallExpression(call) => {
if let Some(fn_name) = nondeterministic_call(&call.callee) {
self.emit(fn_name, call.span);
}
}
Expression::ConditionalExpression(cond) => {
self.scan_expr(&cond.consequent);
self.scan_expr(&cond.alternate);
}
Expression::LogicalExpression(logical) => {
self.scan_expr(&logical.left);
self.scan_expr(&logical.right);
}
Expression::ParenthesizedExpression(paren) => {
self.scan_expr(&paren.expression);
}
_ => {}
}
}
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_math_random_in_render".to_string(),
message: format!(
"`{fn_name}()` in a JSX prop returns a different value on every render, \
guaranteeing the child re-renders every time. \
Generate once with useMemo or useState: \
`const value = useMemo(() => {fn_name}(), [])`"
),
file: self.file_path.to_path_buf(),
line,
column: col,
severity: Severity::Medium,
source: crate::rules::IssueSource::ReactPerfAnalyzer,
category: crate::rules::IssueCategory::Performance,
});
}
}
fn nondeterministic_call(callee: &Expression<'_>) -> Option<&'static str> {
if let Expression::StaticMemberExpression(m) = callee {
if let Expression::Identifier(obj) = &m.object {
let obj_name = obj.name.as_str();
let prop_name = m.property.name.as_str();
return match (obj_name, prop_name) {
("Math", "random") => Some("Math.random"),
("Date", "now") => Some("Date.now"),
_ => None,
};
}
}
None
}