use std::path::Path;
use oxc_ast::ast::{
Expression, JSXAttributeItem, JSXAttributeName, JSXAttributeValue, JSXElementName,
JSXOpeningElement,
};
use oxc_ast_visit::{walk, Visit};
use oxc_span::Span;
use crate::{
rules::{Issue, RuleContext, Severity},
utils::offset_to_line_col,
};
pub struct NoNewContextValue;
impl super::Rule for NoNewContextValue {
fn name(&self) -> &str {
"no_new_context_value"
}
fn run(&self, ctx: &RuleContext<'_>) -> Vec<Issue> {
let mut visitor = ContextValueVisitor {
issues: Vec::new(),
source_text: ctx.source_text,
file_path: ctx.file_path,
};
visitor.visit_program(ctx.program);
visitor.issues
}
}
struct ContextValueVisitor<'a> {
issues: Vec<Issue>,
source_text: &'a str,
file_path: &'a Path,
}
impl<'a> Visit<'a> for ContextValueVisitor<'_> {
fn visit_jsx_opening_element(&mut self, elem: &JSXOpeningElement<'a>) {
if is_context_provider(&elem.name) {
for attr_item in &elem.attributes {
if let JSXAttributeItem::Attribute(attr) = attr_item {
if extract_prop_name(&attr.name) != "value" {
continue;
}
let Some(JSXAttributeValue::ExpressionContainer(container)) = &attr.value
else {
continue;
};
let Some(expr) = container.expression.as_expression() else {
continue;
};
if is_memo_wrapped(expr) {
continue;
}
self.scan_value(expr);
}
}
}
walk::walk_jsx_opening_element(self, elem);
}
}
impl ContextValueVisitor<'_> {
fn scan_value(&mut self, expr: &Expression<'_>) {
match expr {
Expression::ObjectExpression(obj) => {
self.emit(
"object literal",
"Extract to a stable variable or wrap with useMemo: \
`const value = useMemo(() => ({ ... }), [deps])`",
obj.span,
);
}
Expression::ArrayExpression(arr) => {
self.emit(
"array literal",
"Extract to a stable variable or wrap with useMemo: \
`const value = useMemo(() => [...], [deps])`",
arr.span,
);
}
Expression::ArrowFunctionExpression(arrow) => {
self.emit(
"arrow function",
"Wrap with useCallback: \
`const value = useCallback(() => { ... }, [deps])`",
arrow.span,
);
}
Expression::FunctionExpression(func) => {
self.emit(
"function expression",
"Wrap with useCallback: \
`const value = useCallback(function() { ... }, [deps])`",
func.span,
);
}
Expression::ConditionalExpression(cond) => {
self.scan_value(&cond.consequent);
self.scan_value(&cond.alternate);
}
Expression::LogicalExpression(logical) => {
self.scan_value(&logical.left);
self.scan_value(&logical.right);
}
Expression::ParenthesizedExpression(paren) => {
self.scan_value(&paren.expression);
}
_ => {}
}
}
fn emit(&mut self, kind: &str, suggestion: &str, span: Span) {
let (line, col) = offset_to_line_col(self.source_text, span.start);
self.issues.push(Issue {
rule: "no_new_context_value".to_string(),
message: format!(
"Context Provider 'value' receives a new {kind} on every render — \
all consumers will re-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,
});
}
}
fn is_context_provider(name: &JSXElementName<'_>) -> bool {
match name {
JSXElementName::MemberExpression(member) => member.property.name.as_str() == "Provider",
_ => false,
}
}
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 {
match &call.callee {
Expression::Identifier(id) => return id.name.as_str() == "useMemo",
Expression::StaticMemberExpression(member) => {
return member.property.name.as_str() == "useMemo";
}
_ => {}
}
}
false
}