use std::path::Path;
use oxc_ast::ast::{
ConditionalExpression, Expression, JSXAttributeItem, JSXAttributeName, JSXAttributeValue,
JSXOpeningElement, LogicalExpression, ParenthesizedExpression,
};
use oxc_ast_visit::{walk, Visit};
use oxc_span::Span;
use crate::{
rules::{Issue, RuleContext, Severity},
utils::offset_to_line_col,
};
pub struct UnstableProps;
impl super::Rule for UnstableProps {
fn name(&self) -> &str {
"unstable_props"
}
fn run(&self, ctx: &RuleContext<'_>) -> Vec<Issue> {
let mut visitor = UnstablePropsVisitor {
issues: Vec::new(),
source_text: ctx.source_text,
file_path: ctx.file_path,
};
visitor.visit_program(ctx.program);
visitor.issues
}
}
struct UnstablePropsVisitor<'a> {
issues: Vec<Issue>,
source_text: &'a str,
file_path: &'a Path,
}
impl<'a> Visit<'a> for UnstablePropsVisitor<'_> {
fn visit_jsx_opening_element(&mut self, elem: &JSXOpeningElement<'a>) {
for attr_item in &elem.attributes {
match attr_item {
JSXAttributeItem::Attribute(attr) => {
let Some(JSXAttributeValue::ExpressionContainer(container)) = &attr.value
else {
continue;
};
let Some(expr) = container.expression.as_expression() else {
continue;
};
let prop_name = extract_prop_name(&attr.name);
if is_memo_wrapped(expr) {
continue;
}
self.scan_for_unstable(expr, &prop_name);
}
JSXAttributeItem::SpreadAttribute(_) => {}
}
}
walk::walk_jsx_opening_element(self, elem);
}
}
impl UnstablePropsVisitor<'_> {
fn scan_for_unstable(&mut self, expr: &Expression<'_>, prop_name: &str) {
match expr {
Expression::ObjectExpression(obj) => {
self.emit(UnstableKind::Object, prop_name, obj.span);
}
Expression::ArrayExpression(arr) => {
self.emit(UnstableKind::Array, prop_name, arr.span);
}
Expression::ConditionalExpression(cond) => {
self.scan_conditional(cond, prop_name);
}
Expression::LogicalExpression(logical) => {
self.scan_logical(logical, prop_name);
}
Expression::ParenthesizedExpression(paren) => {
self.scan_parenthesized(paren, prop_name);
}
_ => {}
}
}
fn scan_conditional(&mut self, cond: &ConditionalExpression<'_>, prop_name: &str) {
self.scan_for_unstable(&cond.consequent, prop_name);
self.scan_for_unstable(&cond.alternate, prop_name);
}
fn scan_logical(&mut self, logical: &LogicalExpression<'_>, prop_name: &str) {
self.scan_for_unstable(&logical.left, prop_name);
self.scan_for_unstable(&logical.right, prop_name);
}
fn scan_parenthesized(&mut self, paren: &ParenthesizedExpression<'_>, prop_name: &str) {
self.scan_for_unstable(&paren.expression, prop_name);
}
fn emit(&mut self, kind: UnstableKind, prop_name: &str, span: Span) {
let (line, col) = offset_to_line_col(self.source_text, span.start);
let suggestion = build_suggestion(kind, prop_name);
let kind_desc = match kind {
UnstableKind::Object => "Object literal",
UnstableKind::Array => "Array literal",
};
self.issues.push(Issue {
rule: "unstable_props".to_string(),
message: format!(
"{kind_desc} in '{prop_name}' prop creates a new reference on every render. {suggestion}"
),
file: self.file_path.to_path_buf(),
line,
column: col,
severity: Severity::Medium,
source: crate::rules::IssueSource::ReactPerfAnalyzer,
category: crate::rules::IssueCategory::Performance,
});
}
}
#[derive(Copy, Clone)]
enum UnstableKind {
Object,
Array,
}
fn extract_prop_name(name: &JSXAttributeName<'_>) -> String {
match name {
JSXAttributeName::Identifier(id) => id.name.to_string(),
JSXAttributeName::NamespacedName(ns) => {
format!("{}:{}", ns.namespace.name, ns.name.name)
}
}
}
fn is_memo_wrapped(expr: &Expression<'_>) -> bool {
if let Expression::CallExpression(call) = expr {
return is_use_memo_callee(&call.callee);
}
false
}
fn is_use_memo_callee(callee: &Expression<'_>) -> bool {
match callee {
Expression::Identifier(id) => id.name.as_str() == "useMemo",
Expression::StaticMemberExpression(member) => member.property.name.as_str() == "useMemo",
_ => false,
}
}
fn build_suggestion(kind: UnstableKind, prop_name: &str) -> String {
let is_style_prop = matches!(prop_name, "style" | "sx" | "css" | "theme" | "wrapperStyle");
let pascal_name = capitalize(prop_name);
match kind {
UnstableKind::Object if is_style_prop => {
format!(
"Extract to a module-level constant or wrap with useMemo: \
`const {pascal_name} = useMemo(() => ({{ ... }}), [deps])`"
)
}
UnstableKind::Object => {
format!(
"Extract to a stable variable outside the component or wrap with useMemo: \
`const {pascal_name} = useMemo(() => ({{ ... }}), [deps])`"
)
}
UnstableKind::Array => {
format!(
"Extract to a module-level constant or wrap with useMemo: \
`const {pascal_name} = useMemo(() => [...], [deps])`"
)
}
}
}
fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
}
}