feat(linter/react-perf): handle new objects and arrays in prop assignment patterns (#4396)

# 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 <Bar x={x} />
}
```
- feat: handle new objects/etc in binding patterns
```tsx
const Foo = ({ x = [] }) => {
  //           ^^^^^^ now reports this new array
  return <Bar x={x} />
}
```
-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 <Bar x={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).
```
- 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`
This commit is contained in:
DonIsaac 2024-07-23 01:52:59 +00:00
parent ac08de817e
commit 68efcd4000
10 changed files with 542 additions and 163 deletions

View file

@ -304,6 +304,11 @@ impl<'a> IdentifierReference<'a> {
reference_flag: ReferenceFlag::Read,
}
}
#[inline]
pub fn reference_id(&self) -> Option<ReferenceId> {
self.reference_id.get()
}
}
impl<'a> Hash for BindingIdentifier<'a> {

View file

@ -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 check_for_violation_on_expr(&self, expr: &Expression<'_>) -> Option<Span> {
check_expression(expr)
}
fn should_run(&self, ctx: &LintContext) -> bool {
ctx.source_type().is_jsx()
}
}
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<Span>)> {
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"<Item jsx={this.props.jsx || <SubItem />} />",
r"<Item jsx={this.props.jsx ? this.props.jsx : <SubItem />} />",
r"<Item jsx={this.props.jsx || (this.props.component ? this.props.component : <SubItem />)} />",
r"const Icon = <svg />; const Foo = () => (<IconButton icon={Icon} />)",
];
let fail = vec![
@ -98,6 +78,7 @@ fn test() {
r"const Foo = () => (<Item jsx={this.props.jsx || <SubItem />} />)",
r"const Foo = () => (<Item jsx={this.props.jsx ? this.props.jsx : <SubItem />} />)",
r"const Foo = () => (<Item jsx={this.props.jsx || (this.props.component ? this.props.component : <SubItem />)} />)",
r"const Foo = () => { const Icon = <svg />; return (<IconButton icon={Icon} />) }",
];
Tester::new(JsxNoJsxAsProp::NAME, pass, fail).with_react_perf_plugin(true).test_and_snapshot();

View file

@ -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 check_for_violation_on_expr(&self, expr: &Expression<'_>) -> Option<Span> {
check_expression(expr)
}
fn should_run(&self, ctx: &LintContext) -> bool {
ctx.source_type().is_jsx()
}
}
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<Span>)> {
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(&param.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<Span> {
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"<Item list={this.props.list} />",
r"const Foo = () => <Item list={this.props.list} />",
r"<Item list={[]} />",
r"<Item list={new Array()} />",
r"<Item list={Array()} />",
r"<Item list={this.props.list || []} />",
r"<Item list={this.props.list ? this.props.list : []} />",
r"<Item list={this.props.list || (this.props.arr ? this.props.arr : [])} />",
r"const Foo = () => <Item list={this.props.list} />",
r"const x = []; const Foo = () => <Item list={x} />",
r"const DEFAULT_X = []; const Foo = ({ x = DEFAULT_X }) => <Item list={x} />",
];
let fail = vec![
r"const Foo = () => (<Item list={[]} />)",
r"const Foo = () => (<Item list={new Array()} />)",
r"const Foo = () => (<Item list={Array()} />)",
r"const Foo = () => (<Item list={arr1.concat(arr2)} />)",
r"const Foo = () => (<Item list={arr1.filter(x => x > 0)} />)",
r"const Foo = () => (<Item list={arr1.map(x => x * x)} />)",
r"const Foo = () => (<Item list={this.props.list || []} />)",
r"const Foo = () => (<Item list={this.props.list ? this.props.list : []} />)",
r"const Foo = () => (<Item list={this.props.list || (this.props.arr ? this.props.arr : [])} />)",
r"const Foo = () => { let x = []; return <Item list={x} /> }",
r"const Foo = ({ x = [] }) => <Item list={x} />",
];
Tester::new(JsxNoNewArrayAsProp::NAME, pass, fail)

View file

@ -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 check_for_violation_on_expr(&self, expr: &Expression<'_>) -> Option<Span> {
check_expression(expr)
}
fn should_run(&self, ctx: &LintContext) -> bool {
ctx.source_type().is_jsx()
}
}
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<Span>)> {
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"<Item callback={this.props.callback ? this.props.callback : function() {}} />",
r"<Item prop={this.props.callback || this.props.callback ? this.props.callback : function(){}} />",
r"<Item prop={this.props.callback || (this.props.cb ? this.props.cb : function(){})} />",
r"
import { FC, useCallback } from 'react';
export const Foo: FC = props => {
const onClick = useCallback(
e => { props.onClick?.(e) },
[props.onClick]
);
return <button onClick={onClick} />
}",
r"
import React from 'react'
function onClick(e: React.MouseEvent) {
window.location.navigate(e.target.href)
}
export default function Foo() {
return <a onClick={onClick} />
}
",
];
let fail = vec![
@ -143,6 +147,35 @@ fn test() {
r"const Foo = () => (<Item callback={this.props.callback ? this.props.callback : function() {}} />)",
r"const Foo = () => (<Item prop={this.props.callback || this.props.callback ? this.props.callback : function(){}} />)",
r"const Foo = () => (<Item prop={this.props.callback || (this.props.cb ? this.props.cb : function(){})} />)",
r"
const Foo = ({ onClick }) => {
const _onClick = onClick.bind(this)
return <button onClick={_onClick} />
}",
r"
const Foo = () => {
function onClick(e) {
window.location.navigate(e.target.href)
}
return <a onClick={onClick} />
}
",
r"
const Foo = () => {
const onClick = (e) => {
window.location.navigate(e.target.href)
}
return <a onClick={onClick} />
}
",
r"
const Foo = () => {
const onClick = function (e) {
window.location.navigate(e.target.href)
}
return <a onClick={onClick} />
}
",
];
Tester::new(JsxNoNewFunctionAsProp::NAME, pass, fail)

View file

@ -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 check_for_violation_on_expr(&self, expr: &Expression<'_>) -> Option<Span> {
check_expression(expr)
}
fn should_run(&self, ctx: &LintContext) -> bool {
ctx.source_type().is_jsx()
}
}
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<Span>)> {
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(&param.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<Span> {
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"<Item config={staticConfig} />",
r"<Item config={{}} />",
r"<Item config={'foo'} />",
r"const Foo = () => <Item config={staticConfig} />",
r"const Foo = (props) => <Item {...props} />",
r"const Foo = (props) => <Item x={props.x} />",
r"const Foo = ({ x = 5 }) => <Item x={x} />",
r"const x = {}; const Foo = () => <Bar x={x} />",
r"const DEFAULT_X = {}; const Foo = ({ x = DEFAULT_X }) => <Bar x={x} />",
r"
import { FC, useMemo } from 'react';
import { Bar } from './bar';
export const Foo: FC = () => {
const x = useMemo(() => ({ foo: 'bar' }), []);
return <Bar prop={x} />
}
",
r"
import { FC, useMemo } from 'react';
import { Bar } from './bar';
export const Foo: FC = () => {
const x = useMemo(() => ({ foo: 'bar' }), []);
const y = x;
return <Bar prop={y} />
}
",
// new arr, not an obj
r"const Foo = () => <Item arr={[]} />",
];
let fail = vec![
r"const Foo = () => <Item config={{}} />",
r"const Foo = () => <Item config={Object.create(null)} />",
r"const Foo = ({ x }) => <Item config={Object.assign({}, x)} />",
r"const Foo = () => (<Item config={new Object()} />)",
r"const Foo = () => (<Item config={Object()} />)",
r"const Foo = () => (<div style={{display: 'none'}} />)",
r"const Foo = () => (<Item config={this.props.config || {}} />)",
r"const Foo = () => (<Item config={this.props.config ? this.props.config : {}} />)",
r"const Foo = () => (<Item config={this.props.config || (this.props.default ? this.props.default : {})} />)",
r"const Foo = () => { const x = {}; return <Bar x={x} /> }",
r"const Foo = ({ x = {} }) => <Item x={x} />",
];
Tester::new(JsxNoNewObjectAsProp::NAME, pass, fail)

View file

@ -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 = <svg />; return (<IconButton icon={Icon} />) }
· ──┬─ ───┬─── ──┬─
· │ │ ╰── 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).

View file

@ -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 = () => (<Item list={arr1.concat(arr2)} />)
· ─────────────────
╰────
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 = () => (<Item list={arr1.filter(x => 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 = () => (<Item list={arr1.map(x => 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 = () => (<Item list={this.props.list || []} />)
@ -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 <Item list={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).
⚠ 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 = [] }) => <Item list={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).

View file

@ -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 <button onClick={_onClick} />
· ────┬───
· ╰── 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 <a onClick={onClick} />
· ───┬───
· ╰── 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 <a onClick={onClick} />
· ───┬───
· ╰── 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 <a onClick={onClick} />
· ───┬───
· ╰── 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).

View file

@ -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 = () => <Item config={Object.create(null)} />
· ───────────────────
╰────
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 }) => <Item config={Object.assign({}, 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 = () => (<Item config={new Object()} />)
@ -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 <Bar x={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).
⚠ 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 = {} }) => <Item x={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).

View file

@ -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<Span>,
) -> 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<Span>;
/// 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<Span>)>;
}
impl<R> 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) (<div foo />)
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,
}
}