diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 344112dce1395..ccad7740e77d7 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -227,6 +227,7 @@ mod react { pub mod jsx_no_target_blank; pub mod jsx_no_undef; pub mod jsx_no_useless_fragment; + pub mod jsx_props_no_spread_multi; pub mod no_children_prop; pub mod no_danger; pub mod no_direct_mutation_state; @@ -733,6 +734,7 @@ oxc_macros::declare_all_lint_rules! { react::jsx_no_comment_textnodes, react::jsx_no_duplicate_props, react::jsx_no_useless_fragment, + react::jsx_props_no_spread_multi, react::jsx_no_undef, react::react_in_jsx_scope, react::no_children_prop, diff --git a/crates/oxc_linter/src/rules/react/jsx_props_no_spread_multi.rs b/crates/oxc_linter/src/rules/react/jsx_props_no_spread_multi.rs new file mode 100644 index 0000000000000..37b8b60a66e40 --- /dev/null +++ b/crates/oxc_linter/src/rules/react/jsx_props_no_spread_multi.rs @@ -0,0 +1,105 @@ +use rustc_hash::FxHashMap; + +use oxc_ast::{ast::JSXAttributeItem, AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{Atom, Span}; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn jsx_props_no_spread_multi_diagnostic(spans: Vec, prop_name: &str) -> OxcDiagnostic { + OxcDiagnostic::warn("Disallow JSX prop spreading the same identifier multiple times.") + .with_help(format!("Prop '{prop_name}' is spread multiple times.")) + .with_labels(spans) +} + +#[derive(Debug, Default, Clone)] +pub struct JsxPropsNoSpreadMulti; + +declare_oxc_lint!( + /// ### What it does + /// Enforces that any unique expression is only spread once. + /// + /// ### Why is this bad? + /// Generally spreading the same expression twice is an indicator of a mistake since any attribute between the spreads may be overridden when the intent was not to. + /// Even when that is not the case this will lead to unnecessary computations being performed. + /// + /// ### Example + /// ```jsx + /// // Bad + /// + /// + /// // Good + /// + /// + /// ``` + JsxPropsNoSpreadMulti, + correctness, + pending // TODO: add auto-fix to remove the first spread. Removing the second one would change program behavior. +); + +impl Rule for JsxPropsNoSpreadMulti { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::JSXOpeningElement(jsx_opening_el) = node.kind() { + let spread_attrs = + jsx_opening_el.attributes.iter().filter_map(JSXAttributeItem::as_spread); + + let mut identifier_names: FxHashMap<&Atom, Span> = FxHashMap::default(); + let mut duplicate_spreads: FxHashMap<&Atom, Vec> = FxHashMap::default(); + + for spread_attr in spread_attrs { + if let Some(identifier_name) = + spread_attr.argument.get_identifier_reference().map(|arg| &arg.name) + { + identifier_names + .entry(identifier_name) + .and_modify(|first_span| { + duplicate_spreads + .entry(identifier_name) + .or_insert_with(|| vec![*first_span]) + .push(spread_attr.span); + }) + .or_insert(spread_attr.span); + } + } + + for (identifier_name, spans) in duplicate_spreads { + ctx.diagnostic(jsx_props_no_spread_multi_diagnostic(spans, identifier_name)); + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + " + const a = {}; + + ", + " + const a = {}; + const b = {}; + + ", + ]; + + let fail = vec![ + " + const props = {}; + + ", + r#" + const props = {}; +
+ "#, + " + const props = {}; +
+ ", + ]; + + Tester::new(JsxPropsNoSpreadMulti::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/jsx_props_no_spread_multi.snap b/crates/oxc_linter/src/snapshots/jsx_props_no_spread_multi.snap new file mode 100644 index 0000000000000..5cbb80aae7ab2 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/jsx_props_no_spread_multi.snap @@ -0,0 +1,29 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-react(jsx-props-no-spread-multi): Disallow JSX prop spreading the same identifier multiple times. + ╭─[jsx_props_no_spread_multi.tsx:3:16] + 2 │ const props = {}; + 3 │ + · ────────── ────────── + 4 │ + ╰──── + help: Prop 'props' is spread multiple times. + + ⚠ eslint-plugin-react(jsx-props-no-spread-multi): Disallow JSX prop spreading the same identifier multiple times. + ╭─[jsx_props_no_spread_multi.tsx:3:16] + 2 │ const props = {}; + 3 │
+ · ────────── ────────── + 4 │ + ╰──── + help: Prop 'props' is spread multiple times. + + ⚠ eslint-plugin-react(jsx-props-no-spread-multi): Disallow JSX prop spreading the same identifier multiple times. + ╭─[jsx_props_no_spread_multi.tsx:3:16] + 2 │ const props = {}; + 3 │
+ · ────────── ────────── ────────── + 4 │ + ╰──── + help: Prop 'props' is spread multiple times.