mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 20:28:58 +00:00
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:
parent
ac08de817e
commit
68efcd4000
10 changed files with 542 additions and 163 deletions
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(¶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<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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(¶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<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)
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue