From 68efcd40001f5ab048a25ed03ca14199bd8450f3 Mon Sep 17 00:00:00 2001
From: DonIsaac <22823424+DonIsaac@users.noreply.github.com>
Date: Tue, 23 Jul 2024 01:52:59 +0000
Subject: [PATCH] feat(linter/react-perf): handle new objects and arrays in
prop assignment patterns (#4396)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# What This PR Does
Massively improves all `react-perf` rules
- feat: handle new objects/etc assigned to variables
```tsx
const Foo = () => {
const x = { foo: 'bar' } // <- now reports this new object
return
}
```
- feat: handle new objects/etc in binding patterns
```tsx
const Foo = ({ x = [] }) => {
// ^^^^^^ now reports this new array
return
}
```
-feat: nice and descriptive labels for new objects/etc assigned to intermediate variables
```
⚠ eslint-plugin-react-perf(jsx-no-new-object-as-prop): JSX attribute values should not contain objects created in the same scope.
╭─[jsx_no_new_object_as_prop.tsx:1:27]
1 │ const Foo = () => { const x = {}; return }
· ┬ ─┬ ┬
· │ │ ╰── And used here
· │ ╰── And assigned a new value here
· ╰── The prop was declared here
╰────
help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
```
- feat: consider `Object.assign()` and `Object.create()` as a new object
- feat: consider `arr.[map, filter, concat]` as a new array
- refactor: move shared implementation code to `ReactPerfRule` in `oxc_linter::utils::react_perf`
---
crates/oxc_ast/src/ast_impl/js.rs | 5 +
.../rules/react_perf/jsx_no_jsx_as_prop.rs | 57 ++----
.../react_perf/jsx_no_new_array_as_prop.rs | 85 ++++----
.../react_perf/jsx_no_new_function_as_prop.rs | 111 ++++++----
.../react_perf/jsx_no_new_object_as_prop.rs | 105 ++++++----
.../src/snapshots/jsx_no_jsx_as_prop.snap | 10 +
.../snapshots/jsx_no_new_array_as_prop.snap | 41 ++++
.../jsx_no_new_function_as_prop.snap | 58 ++++++
.../snapshots/jsx_no_new_object_as_prop.snap | 34 ++++
crates/oxc_linter/src/utils/react_perf.rs | 191 +++++++++++++++++-
10 files changed, 538 insertions(+), 159 deletions(-)
diff --git a/crates/oxc_ast/src/ast_impl/js.rs b/crates/oxc_ast/src/ast_impl/js.rs
index 5017ec8c13e84..25224e65ca9ce 100644
--- a/crates/oxc_ast/src/ast_impl/js.rs
+++ b/crates/oxc_ast/src/ast_impl/js.rs
@@ -304,6 +304,11 @@ impl<'a> IdentifierReference<'a> {
reference_flag: ReferenceFlag::Read,
}
}
+
+ #[inline]
+ pub fn reference_id(&self) -> Option {
+ self.reference_id.get()
+ }
}
impl<'a> Hash for BindingIdentifier<'a> {
diff --git a/crates/oxc_linter/src/rules/react_perf/jsx_no_jsx_as_prop.rs b/crates/oxc_linter/src/rules/react_perf/jsx_no_jsx_as_prop.rs
index 6eb43a7a4099b..968a4efc243b9 100644
--- a/crates/oxc_linter/src/rules/react_perf/jsx_no_jsx_as_prop.rs
+++ b/crates/oxc_linter/src/rules/react_perf/jsx_no_jsx_as_prop.rs
@@ -1,18 +1,8 @@
-use oxc_ast::{
- ast::{Expression, JSXAttributeValue, JSXElement},
- AstKind,
-};
-use oxc_diagnostics::OxcDiagnostic;
+use crate::utils::ReactPerfRule;
+use oxc_ast::{ast::Expression, AstKind};
use oxc_macros::declare_oxc_lint;
-use oxc_span::Span;
-
-use crate::{context::LintContext, rule::Rule, utils::get_prop_value, AstNode};
-
-fn jsx_no_jsx_as_prop_diagnostic(span0: Span) -> OxcDiagnostic {
- OxcDiagnostic::warn("JSX attribute values should not contain other JSX.")
- .with_help(r"simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).")
- .with_label(span0)
-}
+use oxc_semantic::SymbolId;
+use oxc_span::{GetSpan, Span};
#[derive(Debug, Default, Clone)]
pub struct JsxNoJsxAsProp;
@@ -36,34 +26,23 @@ declare_oxc_lint!(
perf
);
-impl Rule for JsxNoJsxAsProp {
- fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
- if node.scope_id() == ctx.scopes().root_scope_id() {
- return;
- }
- if let AstKind::JSXElement(jsx_elem) = node.kind() {
- check_jsx_element(jsx_elem, ctx);
- }
- }
+impl ReactPerfRule for JsxNoJsxAsProp {
+ const MESSAGE: &'static str = "JSX attribute values should not contain other JSX.";
- fn should_run(&self, ctx: &LintContext) -> bool {
- ctx.source_type().is_jsx()
+ fn check_for_violation_on_expr(&self, expr: &Expression<'_>) -> Option {
+ check_expression(expr)
}
-}
-fn check_jsx_element<'a>(jsx_elem: &JSXElement<'a>, ctx: &LintContext<'a>) {
- for item in &jsx_elem.opening_element.attributes {
- match get_prop_value(item) {
- None => return,
- Some(JSXAttributeValue::ExpressionContainer(container)) => {
- if let Some(expr) = container.expression.as_expression() {
- if let Some(span) = check_expression(expr) {
- ctx.diagnostic(jsx_no_jsx_as_prop_diagnostic(span));
- }
- }
- }
- _ => {}
+ fn check_for_violation_on_ast_kind(
+ &self,
+ kind: &AstKind<'_>,
+ _symbol_id: SymbolId,
+ ) -> Option<(/* decl */ Span, /* init */ Option)> {
+ let AstKind::VariableDeclarator(decl) = kind else {
+ return None;
};
+ let init_span = decl.init.as_ref().and_then(check_expression)?;
+ Some((decl.id.span(), Some(init_span)))
}
}
@@ -91,6 +70,7 @@ fn test() {
r" } />",
r" } />",
r" )} />",
+ r"const Icon = ; const Foo = () => ()",
];
let fail = vec![
@@ -98,6 +78,7 @@ fn test() {
r"const Foo = () => ( } />)",
r"const Foo = () => ( } />)",
r"const Foo = () => ( )} />)",
+ r"const Foo = () => { const Icon = ; return () }",
];
Tester::new(JsxNoJsxAsProp::NAME, pass, fail).with_react_perf_plugin(true).test_and_snapshot();
diff --git a/crates/oxc_linter/src/rules/react_perf/jsx_no_new_array_as_prop.rs b/crates/oxc_linter/src/rules/react_perf/jsx_no_new_array_as_prop.rs
index bccfc9e9a8a6d..2b1c57a31e479 100644
--- a/crates/oxc_linter/src/rules/react_perf/jsx_no_new_array_as_prop.rs
+++ b/crates/oxc_linter/src/rules/react_perf/jsx_no_new_array_as_prop.rs
@@ -1,24 +1,13 @@
-use oxc_ast::{
- ast::{Expression, JSXAttributeValue, JSXElement},
- AstKind,
-};
-use oxc_diagnostics::OxcDiagnostic;
+use oxc_ast::{ast::Expression, AstKind};
use oxc_macros::declare_oxc_lint;
-use oxc_span::Span;
+use oxc_semantic::SymbolId;
+use oxc_span::{GetSpan, Span};
use crate::{
- context::LintContext,
- rule::Rule,
- utils::{get_prop_value, is_constructor_matching_name},
- AstNode,
+ ast_util::is_method_call,
+ utils::{find_initialized_binding, is_constructor_matching_name, ReactPerfRule},
};
-fn jsx_no_new_array_as_prop_diagnostic(span0: Span) -> OxcDiagnostic {
- OxcDiagnostic::warn("JSX attribute values should not contain Arrays created in the same scope.")
- .with_help(r"simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).")
- .with_label(span0)
-}
-
#[derive(Debug, Default, Clone)]
pub struct JsxNoNewArrayAsProp;
@@ -44,34 +33,33 @@ declare_oxc_lint!(
perf
);
-impl Rule for JsxNoNewArrayAsProp {
- fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
- if node.scope_id() == ctx.scopes().root_scope_id() {
- return;
- }
- if let AstKind::JSXElement(jsx_elem) = node.kind() {
- check_jsx_element(jsx_elem, ctx);
- }
- }
+impl ReactPerfRule for JsxNoNewArrayAsProp {
+ const MESSAGE: &'static str =
+ "JSX attribute values should not contain Arrays created in the same scope.";
- fn should_run(&self, ctx: &LintContext) -> bool {
- ctx.source_type().is_jsx()
+ fn check_for_violation_on_expr(&self, expr: &Expression<'_>) -> Option {
+ check_expression(expr)
}
-}
-fn check_jsx_element<'a>(jsx_elem: &JSXElement<'a>, ctx: &LintContext<'a>) {
- for item in &jsx_elem.opening_element.attributes {
- match get_prop_value(item) {
- None => return,
- Some(JSXAttributeValue::ExpressionContainer(container)) => {
- if let Some(expr) = container.expression.as_expression() {
- if let Some(span) = check_expression(expr) {
- ctx.diagnostic(jsx_no_new_array_as_prop_diagnostic(span));
- }
+ fn check_for_violation_on_ast_kind(
+ &self,
+ kind: &AstKind<'_>,
+ symbol_id: SymbolId,
+ ) -> Option<(/* decl */ Span, /* init */ Option)> {
+ match kind {
+ AstKind::VariableDeclarator(decl) => {
+ if let Some(init_span) = decl.init.as_ref().and_then(check_expression) {
+ return Some((decl.id.span(), Some(init_span)));
}
+ None
+ }
+ AstKind::FormalParameter(param) => {
+ let (id, init) = find_initialized_binding(¶m.pattern, symbol_id)?;
+ let init_span = check_expression(init)?;
+ Some((id.span(), Some(init_span)))
}
- _ => {}
- };
+ _ => None,
+ }
}
}
@@ -79,7 +67,15 @@ fn check_expression(expr: &Expression) -> Option {
match expr.without_parenthesized() {
Expression::ArrayExpression(expr) => Some(expr.span),
Expression::CallExpression(expr) => {
- if is_constructor_matching_name(&expr.callee, "Array") {
+ if is_constructor_matching_name(&expr.callee, "Array")
+ || is_method_call(
+ expr.as_ref(),
+ None,
+ Some(&["concat", "map", "filter"]),
+ Some(1),
+ Some(1),
+ )
+ {
Some(expr.span)
} else {
None
@@ -108,22 +104,29 @@ fn test() {
let pass = vec![
r" ",
- r"const Foo = () => ",
r" ",
r" ",
r" ",
r" ",
r" ",
r" ",
+ r"const Foo = () => ",
+ r"const x = []; const Foo = () => ",
+ r"const DEFAULT_X = []; const Foo = ({ x = DEFAULT_X }) => ",
];
let fail = vec![
r"const Foo = () => ( )",
r"const Foo = () => ( )",
r"const Foo = () => ( )",
+ r"const Foo = () => ( )",
+ r"const Foo = () => (- x > 0)} />)",
+ r"const Foo = () => (
- x * x)} />)",
r"const Foo = () => (
)",
r"const Foo = () => ( )",
r"const Foo = () => ( )",
+ r"const Foo = () => { let x = []; return }",
+ r"const Foo = ({ x = [] }) => ",
];
Tester::new(JsxNoNewArrayAsProp::NAME, pass, fail)
diff --git a/crates/oxc_linter/src/rules/react_perf/jsx_no_new_function_as_prop.rs b/crates/oxc_linter/src/rules/react_perf/jsx_no_new_function_as_prop.rs
index ce86756e1fa63..2ccd8c755618b 100644
--- a/crates/oxc_linter/src/rules/react_perf/jsx_no_new_function_as_prop.rs
+++ b/crates/oxc_linter/src/rules/react_perf/jsx_no_new_function_as_prop.rs
@@ -1,23 +1,12 @@
use oxc_ast::{
- ast::{Expression, JSXAttributeValue, JSXElement, MemberExpression},
+ ast::{Expression, MemberExpression},
AstKind,
};
-use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
-use oxc_span::Span;
+use oxc_semantic::SymbolId;
+use oxc_span::{GetSpan, Span};
-use crate::{
- context::LintContext,
- rule::Rule,
- utils::{get_prop_value, is_constructor_matching_name},
- AstNode,
-};
-
-fn jsx_no_new_function_as_prop_diagnostic(span0: Span) -> OxcDiagnostic {
- OxcDiagnostic::warn("JSX attribute values should not contain functions created in the same scope.")
- .with_help(r"simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).")
- .with_label(span0)
-}
+use crate::utils::{is_constructor_matching_name, ReactPerfRule};
#[derive(Debug, Default, Clone)]
pub struct JsxNoNewFunctionAsProp;
@@ -39,34 +28,31 @@ declare_oxc_lint!(
perf
);
-impl Rule for JsxNoNewFunctionAsProp {
- fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
- if node.scope_id() == ctx.scopes().root_scope_id() {
- return;
- }
- if let AstKind::JSXElement(jsx_elem) = node.kind() {
- check_jsx_element(jsx_elem, ctx);
- }
- }
+impl ReactPerfRule for JsxNoNewFunctionAsProp {
+ const MESSAGE: &'static str =
+ "JSX attribute values should not contain functions created in the same scope.";
- fn should_run(&self, ctx: &LintContext) -> bool {
- ctx.source_type().is_jsx()
+ fn check_for_violation_on_expr(&self, expr: &Expression<'_>) -> Option {
+ check_expression(expr)
}
-}
-fn check_jsx_element<'a>(jsx_elem: &JSXElement<'a>, ctx: &LintContext<'a>) {
- for item in &jsx_elem.opening_element.attributes {
- match get_prop_value(item) {
- None => return,
- Some(JSXAttributeValue::ExpressionContainer(container)) => {
- if let Some(expr) = container.expression.as_expression() {
- if let Some(span) = check_expression(expr) {
- ctx.diagnostic(jsx_no_new_function_as_prop_diagnostic(span));
- }
- }
+ fn check_for_violation_on_ast_kind(
+ &self,
+ kind: &AstKind<'_>,
+ _symbol_id: SymbolId,
+ ) -> Option<(/* decl */ Span, /* init */ Option)> {
+ match kind {
+ AstKind::VariableDeclarator(decl)
+ if decl.init.as_ref().and_then(check_expression).is_some() =>
+ {
+ // don't report init span, b/c thats usually an arrow
+ // function expression which gets quite large. It also
+ // doesn't add any value.
+ Some((decl.id.span(), None))
}
- _ => {}
- };
+ AstKind::Function(f) => Some((f.id.as_ref().map_or(f.span, GetSpan::span), None)),
+ _ => None,
+ }
}
}
@@ -131,6 +117,24 @@ fn test() {
r" ",
r" ",
r" ",
+ r"
+ import { FC, useCallback } from 'react';
+ export const Foo: FC = props => {
+ const onClick = useCallback(
+ e => { props.onClick?.(e) },
+ [props.onClick]
+ );
+ return
+ }",
+ r"
+ import React from 'react'
+ function onClick(e: React.MouseEvent) {
+ window.location.navigate(e.target.href)
+ }
+ export default function Foo() {
+ return
+ }
+ ",
];
let fail = vec![
@@ -143,6 +147,35 @@ fn test() {
r"const Foo = () => ( )",
r"const Foo = () => ( )",
r"const Foo = () => ( )",
+ r"
+ const Foo = ({ onClick }) => {
+ const _onClick = onClick.bind(this)
+ return
+ }",
+ r"
+ const Foo = () => {
+ function onClick(e) {
+ window.location.navigate(e.target.href)
+ }
+ return
+ }
+ ",
+ r"
+ const Foo = () => {
+ const onClick = (e) => {
+ window.location.navigate(e.target.href)
+ }
+ return
+ }
+ ",
+ r"
+ const Foo = () => {
+ const onClick = function (e) {
+ window.location.navigate(e.target.href)
+ }
+ return
+ }
+ ",
];
Tester::new(JsxNoNewFunctionAsProp::NAME, pass, fail)
diff --git a/crates/oxc_linter/src/rules/react_perf/jsx_no_new_object_as_prop.rs b/crates/oxc_linter/src/rules/react_perf/jsx_no_new_object_as_prop.rs
index c470f9f91c90a..a0ee324171e5f 100644
--- a/crates/oxc_linter/src/rules/react_perf/jsx_no_new_object_as_prop.rs
+++ b/crates/oxc_linter/src/rules/react_perf/jsx_no_new_object_as_prop.rs
@@ -1,24 +1,13 @@
-use oxc_ast::{
- ast::{Expression, JSXAttributeValue, JSXElement},
- AstKind,
-};
-use oxc_diagnostics::OxcDiagnostic;
+use oxc_ast::{ast::Expression, AstKind};
use oxc_macros::declare_oxc_lint;
-use oxc_span::Span;
+use oxc_semantic::SymbolId;
+use oxc_span::{GetSpan, Span};
use crate::{
- context::LintContext,
- rule::Rule,
- utils::{get_prop_value, is_constructor_matching_name},
- AstNode,
+ ast_util::is_method_call,
+ utils::{find_initialized_binding, is_constructor_matching_name, ReactPerfRule},
};
-fn jsx_no_new_object_as_prop_diagnostic(span0: Span) -> OxcDiagnostic {
- OxcDiagnostic::warn("JSX attribute values should not contain objects created in the same scope.")
- .with_help(r"simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).")
- .with_label(span0)
-}
-
#[derive(Debug, Default, Clone)]
pub struct JsxNoNewObjectAsProp;
@@ -43,34 +32,33 @@ declare_oxc_lint!(
perf
);
-impl Rule for JsxNoNewObjectAsProp {
- fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
- if node.scope_id() == ctx.scopes().root_scope_id() {
- return;
- }
- if let AstKind::JSXElement(jsx_elem) = node.kind() {
- check_jsx_element(jsx_elem, ctx);
- }
- }
+impl ReactPerfRule for JsxNoNewObjectAsProp {
+ const MESSAGE: &'static str =
+ "JSX attribute values should not contain objects created in the same scope.";
- fn should_run(&self, ctx: &LintContext) -> bool {
- ctx.source_type().is_jsx()
+ fn check_for_violation_on_expr(&self, expr: &Expression<'_>) -> Option {
+ check_expression(expr)
}
-}
-fn check_jsx_element<'a>(jsx_elem: &JSXElement<'a>, ctx: &LintContext<'a>) {
- for item in &jsx_elem.opening_element.attributes {
- match get_prop_value(item) {
- None => return,
- Some(JSXAttributeValue::ExpressionContainer(container)) => {
- if let Some(expr) = container.expression.as_expression() {
- if let Some(span) = check_expression(expr) {
- ctx.diagnostic(jsx_no_new_object_as_prop_diagnostic(span));
- }
+ fn check_for_violation_on_ast_kind(
+ &self,
+ kind: &AstKind<'_>,
+ symbol_id: SymbolId,
+ ) -> Option<(/* decl */ Span, /* init */ Option)> {
+ match kind {
+ AstKind::VariableDeclarator(decl) => {
+ if let Some(init_span) = decl.init.as_ref().and_then(check_expression) {
+ return Some((decl.id.span(), Some(init_span)));
}
+ None
+ }
+ AstKind::FormalParameter(param) => {
+ let (id, init) = find_initialized_binding(¶m.pattern, symbol_id)?;
+ let init_span = check_expression(init)?;
+ Some((id.span(), Some(init_span)))
}
- _ => {}
- };
+ _ => None,
+ }
}
}
@@ -78,7 +66,15 @@ fn check_expression(expr: &Expression) -> Option {
match expr.without_parenthesized() {
Expression::ObjectExpression(expr) => Some(expr.span),
Expression::CallExpression(expr) => {
- if is_constructor_matching_name(&expr.callee, "Object") {
+ if is_constructor_matching_name(&expr.callee, "Object")
+ || is_method_call(
+ expr.as_ref(),
+ Some(&["Object"]),
+ Some(&["assign", "create"]),
+ None,
+ None,
+ )
+ {
Some(expr.span)
} else {
None
@@ -108,17 +104,46 @@ fn test() {
let pass = vec![
r" ",
r" ",
+ r" ",
r"const Foo = () => ",
+ r"const Foo = (props) => ",
+ r"const Foo = (props) => ",
+ r"const Foo = ({ x = 5 }) => ",
+ r"const x = {}; const Foo = () => ",
+ r"const DEFAULT_X = {}; const Foo = ({ x = DEFAULT_X }) => ",
+ r"
+ import { FC, useMemo } from 'react';
+ import { Bar } from './bar';
+ export const Foo: FC = () => {
+ const x = useMemo(() => ({ foo: 'bar' }), []);
+ return
+ }
+ ",
+ r"
+ import { FC, useMemo } from 'react';
+ import { Bar } from './bar';
+ export const Foo: FC = () => {
+ const x = useMemo(() => ({ foo: 'bar' }), []);
+ const y = x;
+ return
+ }
+ ",
+ // new arr, not an obj
+ r"const Foo = () => ",
];
let fail = vec![
r"const Foo = () => ",
+ r"const Foo = () => ",
+ r"const Foo = ({ x }) => ",
r"const Foo = () => ( )",
r"const Foo = () => ( )",
r"const Foo = () => ()",
r"const Foo = () => ( )",
r"const Foo = () => ( )",
r"const Foo = () => ( )",
+ r"const Foo = () => { const x = {}; return }",
+ r"const Foo = ({ x = {} }) => ",
];
Tester::new(JsxNoNewObjectAsProp::NAME, pass, fail)
diff --git a/crates/oxc_linter/src/snapshots/jsx_no_jsx_as_prop.snap b/crates/oxc_linter/src/snapshots/jsx_no_jsx_as_prop.snap
index 1efa1bd9509f4..7f25083157cea 100644
--- a/crates/oxc_linter/src/snapshots/jsx_no_jsx_as_prop.snap
+++ b/crates/oxc_linter/src/snapshots/jsx_no_jsx_as_prop.snap
@@ -28,3 +28,13 @@ source: crates/oxc_linter/src/tester.rs
· ───────────
╰────
help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
+ ⚠ eslint-plugin-react-perf(jsx-no-jsx-as-prop): JSX attribute values should not contain other JSX.
+ ╭─[jsx_no_jsx_as_prop.tsx:1:27]
+ 1 │ const Foo = () => { const Icon = ; return () }
+ · ──┬─ ───┬─── ──┬─
+ · │ │ ╰── And used here
+ · │ ╰── And assigned a new value here
+ · ╰── The prop was declared here
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
diff --git a/crates/oxc_linter/src/snapshots/jsx_no_new_array_as_prop.snap b/crates/oxc_linter/src/snapshots/jsx_no_new_array_as_prop.snap
index b1f691bca6e8f..3841422ccfb52 100644
--- a/crates/oxc_linter/src/snapshots/jsx_no_new_array_as_prop.snap
+++ b/crates/oxc_linter/src/snapshots/jsx_no_new_array_as_prop.snap
@@ -22,6 +22,27 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+ ⚠ eslint-plugin-react-perf(jsx-no-new-array-as-prop): JSX attribute values should not contain Arrays created in the same scope.
+ ╭─[jsx_no_new_array_as_prop.tsx:1:32]
+ 1 │ const Foo = () => ( )
+ · ─────────────────
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
+ ⚠ eslint-plugin-react-perf(jsx-no-new-array-as-prop): JSX attribute values should not contain Arrays created in the same scope.
+ ╭─[jsx_no_new_array_as_prop.tsx:1:32]
+ 1 │ const Foo = () => (- x > 0)} />)
+ · ───────────────────────
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
+ ⚠ eslint-plugin-react-perf(jsx-no-new-array-as-prop): JSX attribute values should not contain Arrays created in the same scope.
+ ╭─[jsx_no_new_array_as_prop.tsx:1:32]
+ 1 │ const Foo = () => (
- x * x)} />)
+ · ────────────────────
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
⚠ eslint-plugin-react-perf(jsx-no-new-array-as-prop): JSX attribute values should not contain Arrays created in the same scope.
╭─[jsx_no_new_array_as_prop.tsx:1:51]
1 │ const Foo = () => (
)
@@ -42,3 +63,23 @@ source: crates/oxc_linter/src/tester.rs
· ──
╰────
help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
+ ⚠ eslint-plugin-react-perf(jsx-no-new-array-as-prop): JSX attribute values should not contain Arrays created in the same scope.
+ ╭─[jsx_no_new_array_as_prop.tsx:1:25]
+ 1 │ const Foo = () => { let x = []; return }
+ · ┬ ─┬ ┬
+ · │ │ ╰── And used here
+ · │ ╰── And assigned a new value here
+ · ╰── The prop was declared here
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
+ ⚠ eslint-plugin-react-perf(jsx-no-new-array-as-prop): JSX attribute values should not contain Arrays created in the same scope.
+ ╭─[jsx_no_new_array_as_prop.tsx:1:16]
+ 1 │ const Foo = ({ x = [] }) =>
+ · ┬ ─┬ ┬
+ · │ │ ╰── And used here
+ · │ ╰── And assigned a new value here
+ · ╰── The prop was declared here
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
diff --git a/crates/oxc_linter/src/snapshots/jsx_no_new_function_as_prop.snap b/crates/oxc_linter/src/snapshots/jsx_no_new_function_as_prop.snap
index 42bb3b357a576..7ad4ce7042703 100644
--- a/crates/oxc_linter/src/snapshots/jsx_no_new_function_as_prop.snap
+++ b/crates/oxc_linter/src/snapshots/jsx_no_new_function_as_prop.snap
@@ -63,3 +63,61 @@ source: crates/oxc_linter/src/tester.rs
· ────────────
╰────
help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
+ ⚠ eslint-plugin-react-perf(jsx-no-new-function-as-prop): JSX attribute values should not contain functions created in the same scope.
+ ╭─[jsx_no_new_function_as_prop.tsx:3:19]
+ 2 │ const Foo = ({ onClick }) => {
+ 3 │ const _onClick = onClick.bind(this)
+ · ────┬───
+ · ╰── The prop was declared here
+ 4 │ return
+ · ────┬───
+ · ╰── And used here
+ 5 │ }
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
+ ⚠ eslint-plugin-react-perf(jsx-no-new-function-as-prop): JSX attribute values should not contain functions created in the same scope.
+ ╭─[jsx_no_new_function_as_prop.tsx:3:22]
+ 2 │ const Foo = () => {
+ 3 │ function onClick(e) {
+ · ───┬───
+ · ╰── The prop was declared here
+ 4 │ window.location.navigate(e.target.href)
+ 5 │ }
+ 6 │ return
+ · ───┬───
+ · ╰── And used here
+ 7 │ }
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
+ ⚠ eslint-plugin-react-perf(jsx-no-new-function-as-prop): JSX attribute values should not contain functions created in the same scope.
+ ╭─[jsx_no_new_function_as_prop.tsx:3:19]
+ 2 │ const Foo = () => {
+ 3 │ const onClick = (e) => {
+ · ───┬───
+ · ╰── The prop was declared here
+ 4 │ window.location.navigate(e.target.href)
+ 5 │ }
+ 6 │ return
+ · ───┬───
+ · ╰── And used here
+ 7 │ }
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
+ ⚠ eslint-plugin-react-perf(jsx-no-new-function-as-prop): JSX attribute values should not contain functions created in the same scope.
+ ╭─[jsx_no_new_function_as_prop.tsx:3:19]
+ 2 │ const Foo = () => {
+ 3 │ const onClick = function (e) {
+ · ───┬───
+ · ╰── The prop was declared here
+ 4 │ window.location.navigate(e.target.href)
+ 5 │ }
+ 6 │ return
+ · ───┬───
+ · ╰── And used here
+ 7 │ }
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
diff --git a/crates/oxc_linter/src/snapshots/jsx_no_new_object_as_prop.snap b/crates/oxc_linter/src/snapshots/jsx_no_new_object_as_prop.snap
index f1de1ecde019d..cbfe2e3f935b6 100644
--- a/crates/oxc_linter/src/snapshots/jsx_no_new_object_as_prop.snap
+++ b/crates/oxc_linter/src/snapshots/jsx_no_new_object_as_prop.snap
@@ -8,6 +8,20 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+ ⚠ eslint-plugin-react-perf(jsx-no-new-object-as-prop): JSX attribute values should not contain objects created in the same scope.
+ ╭─[jsx_no_new_object_as_prop.tsx:1:33]
+ 1 │ const Foo = () =>
+ · ───────────────────
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
+ ⚠ eslint-plugin-react-perf(jsx-no-new-object-as-prop): JSX attribute values should not contain objects created in the same scope.
+ ╭─[jsx_no_new_object_as_prop.tsx:1:38]
+ 1 │ const Foo = ({ x }) =>
+ · ────────────────────
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
⚠ eslint-plugin-react-perf(jsx-no-new-object-as-prop): JSX attribute values should not contain objects created in the same scope.
╭─[jsx_no_new_object_as_prop.tsx:1:34]
1 │ const Foo = () => ( )
@@ -49,3 +63,23 @@ source: crates/oxc_linter/src/tester.rs
· ──
╰────
help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
+ ⚠ eslint-plugin-react-perf(jsx-no-new-object-as-prop): JSX attribute values should not contain objects created in the same scope.
+ ╭─[jsx_no_new_object_as_prop.tsx:1:27]
+ 1 │ const Foo = () => { const x = {}; return }
+ · ┬ ─┬ ┬
+ · │ │ ╰── And used here
+ · │ ╰── And assigned a new value here
+ · ╰── The prop was declared here
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
+
+ ⚠ eslint-plugin-react-perf(jsx-no-new-object-as-prop): JSX attribute values should not contain objects created in the same scope.
+ ╭─[jsx_no_new_object_as_prop.tsx:1:16]
+ 1 │ const Foo = ({ x = {} }) =>
+ · ┬ ─┬ ┬
+ · │ │ ╰── And used here
+ · │ ╰── And assigned a new value here
+ · ╰── The prop was declared here
+ ╰────
+ help: simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).
diff --git a/crates/oxc_linter/src/utils/react_perf.rs b/crates/oxc_linter/src/utils/react_perf.rs
index 4f13ee46fb0ae..56b32fdc9d701 100644
--- a/crates/oxc_linter/src/utils/react_perf.rs
+++ b/crates/oxc_linter/src/utils/react_perf.rs
@@ -1,4 +1,127 @@
-use oxc_ast::ast::Expression;
+use std::fmt;
+
+use crate::{rule::Rule, AstNode, LintContext};
+use oxc_ast::{
+ ast::{
+ BindingIdentifier, BindingPattern, BindingPatternKind, Expression, JSXAttributeItem,
+ JSXAttributeValue,
+ },
+ AstKind,
+};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_semantic::SymbolId;
+use oxc_span::Span;
+
+fn react_perf_inline_diagnostic(message: &'static str, attr_span: Span) -> OxcDiagnostic {
+ OxcDiagnostic::warn(message)
+ .with_help(r"simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).")
+ .with_label(attr_span)
+}
+fn react_perf_reference_diagnostic(
+ message: &'static str,
+ attr_span: Span,
+ decl_span: Span,
+ init_span: Option,
+) -> OxcDiagnostic {
+ let mut diagnostic = OxcDiagnostic::warn(message)
+ .with_help(r"simplify props or memoize props in the parent component (https://react.dev/reference/react/memo#my-component-rerenders-when-a-prop-is-an-object-or-array).")
+ .with_label(
+ decl_span.label("The prop was declared here"),
+ );
+
+ if let Some(init_span) = init_span {
+ diagnostic = diagnostic.and_label(init_span.label("And assigned a new value here"));
+ }
+
+ diagnostic.and_label(attr_span.label("And used here"))
+}
+
+pub(crate) trait ReactPerfRule: Sized + Default + fmt::Debug {
+ const MESSAGE: &'static str;
+
+ /// Check if an [`Expression`] violates a react perf rule. If it does,
+ /// report the [`OxcDiagnostic`] and return `true`.
+ ///
+ /// [`OxcDiagnostic`]: oxc_diagnostics::OxcDiagnostic
+ fn check_for_violation_on_expr(&self, expr: &Expression<'_>) -> Option;
+ /// Check if a node of some [`AstKind`] violates a react perf rule. If it does,
+ /// report the [`OxcDiagnostic`] and return `true`.
+ ///
+ /// [`OxcDiagnostic`]: oxc_diagnostics::OxcDiagnostic
+ fn check_for_violation_on_ast_kind(
+ &self,
+ kind: &AstKind<'_>,
+ symbol_id: SymbolId,
+ ) -> Option<(/* decl */ Span, /* init */ Option)>;
+}
+
+impl Rule for R
+where
+ R: ReactPerfRule,
+{
+ fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+ // new objects/arrays/etc created at the root scope do not get
+ // re-created on each render and thus do not affect performance.
+ if node.scope_id() == ctx.scopes().root_scope_id() {
+ return;
+ }
+
+ // look for JSX attributes whose values are expressions (foo={bar}) (as opposed to
+ // spreads ({...foo}) or just boolean attributes) ()
+ let AstKind::JSXAttributeItem(JSXAttributeItem::Attribute(attr)) = node.kind() else {
+ return;
+ };
+ let Some(JSXAttributeValue::ExpressionContainer(container)) = attr.value.as_ref() else {
+ return;
+ };
+ let Some(expr) = container.expression.as_expression() else {
+ return;
+ };
+
+ // strip parenthesis and TS type casting expressions
+ let expr = expr.get_inner_expression();
+ // When expr is a violation, this fn will report the appropriate
+ // diagnostic and return true.
+ if let Some(attr_span) = self.check_for_violation_on_expr(expr) {
+ ctx.diagnostic(react_perf_inline_diagnostic(Self::MESSAGE, attr_span));
+ return;
+ }
+
+ // check for new objects/arrays/etc declared within the render function,
+ // which is effectively the same as passing a new object/array/etc
+ // directly as a prop.
+ let Expression::Identifier(ident) = expr else {
+ return;
+ };
+ let Some(symbol_id) =
+ ident.reference_id().and_then(|id| ctx.symbols().get_reference(id).symbol_id())
+ else {
+ return;
+ };
+ // Symbols declared at the root scope won't (or, at least, shouldn't) be
+ // re-assigned inside component render functions, so we can safely
+ // ignore them.
+ if ctx.symbols().get_scope_id(symbol_id) == ctx.scopes().root_scope_id() {
+ return;
+ }
+
+ let declaration_node = ctx.nodes().get_node(ctx.symbols().get_declaration(symbol_id));
+ if let Some((decl_span, init_span)) =
+ self.check_for_violation_on_ast_kind(&declaration_node.kind(), symbol_id)
+ {
+ ctx.diagnostic(react_perf_reference_diagnostic(
+ Self::MESSAGE,
+ ident.span,
+ decl_span,
+ init_span,
+ ));
+ }
+ }
+
+ fn should_run(&self, ctx: &LintContext) -> bool {
+ ctx.source_type().is_jsx()
+ }
+}
pub fn is_constructor_matching_name(callee: &Expression<'_>, name: &str) -> bool {
let Expression::Identifier(ident) = callee else {
@@ -6,3 +129,69 @@ pub fn is_constructor_matching_name(callee: &Expression<'_>, name: &str) -> bool
};
ident.name == name
}
+
+pub fn find_initialized_binding<'a, 'b>(
+ binding: &'b BindingPattern<'a>,
+ symbol_id: SymbolId,
+) -> Option<(&'b BindingIdentifier<'a>, &'b Expression<'a>)> {
+ match &binding.kind {
+ BindingPatternKind::AssignmentPattern(assignment) => {
+ match &assignment.left.kind {
+ BindingPatternKind::BindingIdentifier(id) => {
+ // look for `x = {}`, or recurse if lhs is a binding pattern
+ if id.symbol_id.get().is_some_and(|binding_id| binding_id == symbol_id) {
+ return Some((id.as_ref(), &assignment.right));
+ }
+ None
+ }
+ BindingPatternKind::ObjectPattern(obj) => {
+ for prop in &obj.properties {
+ let maybe_initialized_binding =
+ find_initialized_binding(&prop.value, symbol_id);
+ if maybe_initialized_binding.is_some() {
+ return maybe_initialized_binding;
+ }
+ }
+ None
+ }
+ BindingPatternKind::ArrayPattern(arr) => {
+ for el in &arr.elements {
+ let Some(el) = el else {
+ continue;
+ };
+ let maybe_initialized_binding = find_initialized_binding(el, symbol_id);
+ if maybe_initialized_binding.is_some() {
+ return maybe_initialized_binding;
+ }
+ }
+ None
+ }
+ // assignment patterns should not have an assignment pattern on
+ // the left.
+ BindingPatternKind::AssignmentPattern(_) => None,
+ }
+ }
+ BindingPatternKind::ObjectPattern(obj) => {
+ for prop in &obj.properties {
+ let maybe_initialized_binding = find_initialized_binding(&prop.value, symbol_id);
+ if maybe_initialized_binding.is_some() {
+ return maybe_initialized_binding;
+ }
+ }
+ None
+ }
+ BindingPatternKind::ArrayPattern(arr) => {
+ for el in &arr.elements {
+ let Some(el) = el else {
+ continue;
+ };
+ let maybe_initialized_binding = find_initialized_binding(el, symbol_id);
+ if maybe_initialized_binding.is_some() {
+ return maybe_initialized_binding;
+ }
+ }
+ None
+ }
+ BindingPatternKind::BindingIdentifier(_) => None,
+ }
+}