Skip to content

Commit 93ae1c7

Browse files

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed

crates/oxc_linter/src/rules.rs

+2
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ mod react {
227227
pub mod jsx_no_target_blank;
228228
pub mod jsx_no_undef;
229229
pub mod jsx_no_useless_fragment;
230+
pub mod jsx_props_no_spread_multi;
230231
pub mod no_children_prop;
231232
pub mod no_danger;
232233
pub mod no_direct_mutation_state;
@@ -733,6 +734,7 @@ oxc_macros::declare_all_lint_rules! {
733734
react::jsx_no_comment_textnodes,
734735
react::jsx_no_duplicate_props,
735736
react::jsx_no_useless_fragment,
737+
react::jsx_props_no_spread_multi,
736738
react::jsx_no_undef,
737739
react::react_in_jsx_scope,
738740
react::no_children_prop,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use rustc_hash::FxHashMap;
2+
3+
use oxc_ast::{ast::JSXAttributeItem, AstKind};
4+
use oxc_diagnostics::OxcDiagnostic;
5+
use oxc_macros::declare_oxc_lint;
6+
use oxc_span::{Atom, Span};
7+
8+
use crate::{context::LintContext, rule::Rule, AstNode};
9+
10+
fn jsx_props_no_spread_multi_diagnostic(spans: Vec<Span>, prop_name: &str) -> OxcDiagnostic {
11+
OxcDiagnostic::warn("Disallow JSX prop spreading the same identifier multiple times.")
12+
.with_help(format!("Prop '{prop_name}' is spread multiple times."))
13+
.with_labels(spans)
14+
}
15+
16+
#[derive(Debug, Default, Clone)]
17+
pub struct JsxPropsNoSpreadMulti;
18+
19+
declare_oxc_lint!(
20+
/// ### What it does
21+
/// Enforces that any unique expression is only spread once.
22+
///
23+
/// ### Why is this bad?
24+
/// 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.
25+
/// Even when that is not the case this will lead to unnecessary computations being performed.
26+
///
27+
/// ### Example
28+
/// ```jsx
29+
/// // Bad
30+
/// <App {...props} myAttr="1" {...props} />
31+
///
32+
/// // Good
33+
/// <App myAttr="1" {...props} />
34+
/// <App {...props} myAttr="1" />
35+
/// ```
36+
JsxPropsNoSpreadMulti,
37+
correctness,
38+
pending // TODO: add auto-fix to remove the first spread. Removing the second one would change program behavior.
39+
);
40+
41+
impl Rule for JsxPropsNoSpreadMulti {
42+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
43+
if let AstKind::JSXOpeningElement(jsx_opening_el) = node.kind() {
44+
let spread_attrs =
45+
jsx_opening_el.attributes.iter().filter_map(JSXAttributeItem::as_spread);
46+
47+
let mut identifier_names: FxHashMap<&Atom, Span> = FxHashMap::default();
48+
let mut duplicate_spreads: FxHashMap<&Atom, Vec<Span>> = FxHashMap::default();
49+
50+
for spread_attr in spread_attrs {
51+
if let Some(identifier_name) =
52+
spread_attr.argument.get_identifier_reference().map(|arg| &arg.name)
53+
{
54+
identifier_names
55+
.entry(identifier_name)
56+
.and_modify(|first_span| {
57+
duplicate_spreads
58+
.entry(identifier_name)
59+
.or_insert_with(|| vec![*first_span])
60+
.push(spread_attr.span);
61+
})
62+
.or_insert(spread_attr.span);
63+
}
64+
}
65+
66+
for (identifier_name, spans) in duplicate_spreads {
67+
ctx.diagnostic(jsx_props_no_spread_multi_diagnostic(spans, identifier_name));
68+
}
69+
}
70+
}
71+
}
72+
73+
#[test]
74+
fn test() {
75+
use crate::tester::Tester;
76+
77+
let pass = vec![
78+
"
79+
const a = {};
80+
<App {...a} />
81+
",
82+
"
83+
const a = {};
84+
const b = {};
85+
<App {...a} {...b} />
86+
",
87+
];
88+
89+
let fail = vec![
90+
"
91+
const props = {};
92+
<App {...props} {...props} />
93+
",
94+
r#"
95+
const props = {};
96+
<div {...props} a="a" {...props} />
97+
"#,
98+
"
99+
const props = {};
100+
<div {...props} {...props} {...props} />
101+
",
102+
];
103+
104+
Tester::new(JsxPropsNoSpreadMulti::NAME, pass, fail).test_and_snapshot();
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-react(jsx-props-no-spread-multi): Disallow JSX prop spreading the same identifier multiple times.
5+
╭─[jsx_props_no_spread_multi.tsx:3:16]
6+
2const props = {};
7+
3<App {...props} {...props} />
8+
· ────────── ──────────
9+
4
10+
╰────
11+
help: Prop 'props' is spread multiple times.
12+
13+
eslint-plugin-react(jsx-props-no-spread-multi): Disallow JSX prop spreading the same identifier multiple times.
14+
╭─[jsx_props_no_spread_multi.tsx:3:16]
15+
2const props = {};
16+
3<div {...props} a="a" {...props} />
17+
· ────────── ──────────
18+
4
19+
╰────
20+
help: Prop 'props' is spread multiple times.
21+
22+
eslint-plugin-react(jsx-props-no-spread-multi): Disallow JSX prop spreading the same identifier multiple times.
23+
╭─[jsx_props_no_spread_multi.tsx:3:16]
24+
2const props = {};
25+
3<div {...props} {...props} {...props} />
26+
· ────────── ────────── ──────────
27+
4
28+
╰────
29+
help: Prop 'props' is spread multiple times.

0 commit comments

Comments
 (0)