Skip to content

Commit 93a4521

Browse files
committed
fix(linter/jsx-fragments): add mode option
1 parent c9dac23 commit 93a4521

File tree

2 files changed

+120
-18
lines changed

2 files changed

+120
-18
lines changed

crates/oxc_linter/src/rules/react/jsx_fragments.rs

Lines changed: 113 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,59 @@ use oxc_ast::{
55
use oxc_diagnostics::OxcDiagnostic;
66
use oxc_macros::declare_oxc_lint;
77
use oxc_span::{GetSpan, Span};
8+
use serde_json::Value;
89

910
use crate::{AstNode, context::LintContext, rule::Rule};
1011

11-
fn jsx_fragments_diagnostic(span: Span) -> OxcDiagnostic {
12-
OxcDiagnostic::warn("Shorthand form for React fragments is preferred")
13-
.with_help("Use <></> instead of <React.Fragment></React.Fragment>")
14-
.with_label(span)
12+
fn jsx_fragments_diagnostic(span: Span, mode: FragmentMode) -> OxcDiagnostic {
13+
let msg = if mode == FragmentMode::Element {
14+
"Standard form for React fragments is preferred"
15+
} else {
16+
"Shorthand form for React fragments is preferred"
17+
};
18+
let help = if mode == FragmentMode::Element {
19+
"Use <React.Fragment></React.Fragment> instead of <></>"
20+
} else {
21+
"Use <></> instead of <React.Fragment></React.Fragment>"
22+
};
23+
OxcDiagnostic::warn(msg).with_help(help).with_label(span)
1524
}
1625

1726
#[derive(Debug, Default, Clone)]
18-
pub struct JsxFragments;
27+
pub struct JsxFragments {
28+
mode: FragmentMode,
29+
}
30+
31+
#[derive(Debug, Default, Clone, PartialEq, Copy)]
32+
pub enum FragmentMode {
33+
#[default]
34+
Syntax,
35+
Element,
36+
}
37+
38+
impl From<&str> for FragmentMode {
39+
fn from(value: &str) -> Self {
40+
if value == "element" { Self::Element } else { Self::Syntax }
41+
}
42+
}
1943

2044
// See <https://github.com/oxc-project/oxc/issues/6050> for documentation details.
2145
declare_oxc_lint!(
2246
/// ### What it does
2347
///
24-
/// Enforces the shorthand form for React fragments
48+
/// Enforces the shorthand or standard form for React Fragments.
2549
///
2650
/// ### Why is this bad?
2751
///
28-
/// Shorthand form is much more succinct and readable than the fully qualified element name.
52+
/// Makes code using fragments more consistent one way or the other.
2953
///
30-
/// ### Examples
54+
/// ### Options
55+
///
56+
/// `{ "mode": "syntax" | "element" }`
57+
///
58+
/// #### `syntax` mode
59+
/// This is the default mode. It will enforce the shorthand syntax for React fragments, with one exception.
60+
/// Keys or attributes are not supported by the shorthand syntax, so the rule will not warn on standard-form fragments that use those.
3161
///
3262
/// Examples of **incorrect** code for this rule:
3363
/// ```jsx
@@ -42,16 +72,44 @@ declare_oxc_lint!(
4272
/// ```jsx
4373
/// <React.Fragment key="key"><Foo /></React.Fragment>
4474
/// ```
75+
///
76+
/// #### `element` mode
77+
/// This mode enforces the standard form for React fragments.
78+
///
79+
/// Examples of **incorrect** code for this rule:
80+
/// ```jsx
81+
/// <><Foo /></>
82+
/// ```
83+
///
84+
/// Examples of **correct** code for this rule:
85+
/// ```jsx
86+
/// <React.Fragment><Foo /></React.Fragment>
87+
/// ```
88+
///
89+
/// ```jsx
90+
/// <React.Fragment key="key"><Foo /></React.Fragment>
91+
/// ```
4592
JsxFragments,
4693
react,
4794
style,
4895
fix
4996
);
5097

5198
impl Rule for JsxFragments {
99+
fn from_configuration(value: Value) -> Self {
100+
let obj = value.get(0);
101+
Self {
102+
mode: obj
103+
.and_then(|v| v.get("mode"))
104+
.and_then(Value::as_str)
105+
.map(FragmentMode::from)
106+
.unwrap_or_default(),
107+
}
108+
}
109+
52110
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
53111
match node.kind() {
54-
AstKind::JSXElement(jsx_elem) => {
112+
AstKind::JSXElement(jsx_elem) if self.mode == FragmentMode::Syntax => {
55113
let Some(closing_element) = &jsx_elem.closing_element else {
56114
return;
57115
};
@@ -61,7 +119,7 @@ impl Rule for JsxFragments {
61119
return;
62120
}
63121
ctx.diagnostic_with_fix(
64-
jsx_fragments_diagnostic(jsx_elem.opening_element.name.span()),
122+
jsx_fragments_diagnostic(jsx_elem.opening_element.name.span(), self.mode),
65123
|fixer| {
66124
let before_opening_tag = ctx.source_range(Span::new(
67125
jsx_elem.span().start,
@@ -85,6 +143,32 @@ impl Rule for JsxFragments {
85143
},
86144
);
87145
}
146+
AstKind::JSXFragment(jsx_frag) if self.mode == FragmentMode::Element => {
147+
ctx.diagnostic_with_fix(
148+
jsx_fragments_diagnostic(jsx_frag.opening_fragment.span(), self.mode),
149+
|fixer| {
150+
let before_opening_tag = ctx.source_range(Span::new(
151+
jsx_frag.span().start,
152+
jsx_frag.opening_fragment.span().start,
153+
));
154+
let between_opening_tag_and_closing_tag = ctx.source_range(Span::new(
155+
jsx_frag.opening_fragment.span().end,
156+
jsx_frag.closing_fragment.span().start,
157+
));
158+
let after_closing_tag = ctx.source_range(Span::new(
159+
jsx_frag.closing_fragment.span().end,
160+
jsx_frag.span().end,
161+
));
162+
let mut replacement = String::new();
163+
replacement.push_str(&before_opening_tag);
164+
replacement.push_str("<React.Fragment>");
165+
replacement.push_str(&between_opening_tag_and_closing_tag);
166+
replacement.push_str("</React.Fragment>");
167+
replacement.push_str(&after_closing_tag);
168+
fixer.replace(jsx_frag.span(), replacement)
169+
},
170+
);
171+
}
88172
_ => {}
89173
}
90174
}
@@ -113,20 +197,31 @@ fn is_jsx_fragment(elem: &JSXOpeningElement) -> bool {
113197
#[test]
114198
fn test() {
115199
use crate::tester::Tester;
200+
use serde_json::json;
116201

117202
let pass = vec![
118-
"<><Foo /></>",
119-
"<Fragment key=\"key\"><Foo /></Fragment>",
120-
"<React.Fragment key=\"key\"><Foo /></React.Fragment>",
121-
"<Fragment />",
122-
"<React.Fragment />",
203+
("<><Foo /></>", None),
204+
(r#"<Fragment key="key"><Foo /></Fragment>"#, None),
205+
(r#"<React.Fragment key="key"><Foo /></React.Fragment>"#, None),
206+
("<Fragment />", None),
207+
("<React.Fragment />", None),
208+
("<React.Fragment><Foo /></React.Fragment>", Some(json!([{"mode": "element"}]))),
123209
];
124210

125-
let fail = vec!["<Fragment><Foo /></Fragment>", "<React.Fragment><Foo /></React.Fragment>"];
211+
let fail = vec![
212+
("<Fragment><Foo /></Fragment>", None),
213+
("<React.Fragment><Foo /></React.Fragment>", None),
214+
("<><Foo /></>", Some(json!([{"mode": "element"}]))),
215+
];
126216

127217
let fix = vec![
128-
("<Fragment><Foo /></Fragment>", "<><Foo /></>"),
129-
("<React.Fragment><Foo /></React.Fragment>", "<><Foo /></>"),
218+
("<Fragment><Foo /></Fragment>", "<><Foo /></>", None),
219+
("<React.Fragment><Foo /></React.Fragment>", "<><Foo /></>", None),
220+
(
221+
"<><Foo /></>",
222+
"<React.Fragment><Foo /></React.Fragment>",
223+
Some(json!([{"mode": "element"}])),
224+
),
130225
];
131226
Tester::new(JsxFragments::NAME, JsxFragments::PLUGIN, pass, fail)
132227
.expect_fix(fix)

crates/oxc_linter/src/snapshots/react_jsx_fragments.snap

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,10 @@ source: crates/oxc_linter/src/tester.rs
1414
· ──────────────
1515
╰────
1616
help: Use <></> instead of <React.Fragment></React.Fragment>
17+
18+
eslint-plugin-react(jsx-fragments): Standard form for React fragments is preferred
19+
╭─[jsx_fragments.tsx:1:1]
20+
1<><Foo /></>
21+
· ──
22+
╰────
23+
help: Use <React.Fragment></React.Fragment> instead of <></>

0 commit comments

Comments
 (0)