Skip to content

Commit c9dac23

Browse files
committed
feat(linter): add react/jsx-fragments rule
1 parent 6b054b6 commit c9dac23

File tree

3 files changed

+152
-0
lines changed

3 files changed

+152
-0
lines changed

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ mod react {
300300
pub mod jsx_boolean_value;
301301
pub mod jsx_curly_brace_presence;
302302
pub mod jsx_filename_extension;
303+
pub mod jsx_fragments;
303304
pub mod jsx_key;
304305
pub mod jsx_no_comment_textnodes;
305306
pub mod jsx_no_duplicate_props;
@@ -921,6 +922,7 @@ oxc_macros::declare_all_lint_rules! {
921922
react::forbid_elements,
922923
react::forward_ref_uses_ref,
923924
react::iframe_missing_sandbox,
925+
react::jsx_fragments,
924926
react::jsx_filename_extension,
925927
react::jsx_boolean_value,
926928
react::jsx_curly_brace_presence,
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use oxc_ast::{
2+
AstKind,
3+
ast::{JSXElementName, JSXMemberExpressionObject, JSXOpeningElement},
4+
};
5+
use oxc_diagnostics::OxcDiagnostic;
6+
use oxc_macros::declare_oxc_lint;
7+
use oxc_span::{GetSpan, Span};
8+
9+
use crate::{AstNode, context::LintContext, rule::Rule};
10+
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)
15+
}
16+
17+
#[derive(Debug, Default, Clone)]
18+
pub struct JsxFragments;
19+
20+
// See <https://github.com/oxc-project/oxc/issues/6050> for documentation details.
21+
declare_oxc_lint!(
22+
/// ### What it does
23+
///
24+
/// Enforces the shorthand form for React fragments
25+
///
26+
/// ### Why is this bad?
27+
///
28+
/// Shorthand form is much more succinct and readable than the fully qualified element name.
29+
///
30+
/// ### Examples
31+
///
32+
/// Examples of **incorrect** code for this rule:
33+
/// ```jsx
34+
/// <React.Fragment><Foo /></React.Fragment>
35+
/// ```
36+
///
37+
/// Examples of **correct** code for this rule:
38+
/// ```jsx
39+
/// <><Foo /></>
40+
/// ```
41+
///
42+
/// ```jsx
43+
/// <React.Fragment key="key"><Foo /></React.Fragment>
44+
/// ```
45+
JsxFragments,
46+
react,
47+
style,
48+
fix
49+
);
50+
51+
impl Rule for JsxFragments {
52+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
53+
match node.kind() {
54+
AstKind::JSXElement(jsx_elem) => {
55+
let Some(closing_element) = &jsx_elem.closing_element else {
56+
return;
57+
};
58+
if !is_jsx_fragment(&jsx_elem.opening_element)
59+
|| jsx_elem.opening_element.attributes.len() > 0
60+
{
61+
return;
62+
}
63+
ctx.diagnostic_with_fix(
64+
jsx_fragments_diagnostic(jsx_elem.opening_element.name.span()),
65+
|fixer| {
66+
let before_opening_tag = ctx.source_range(Span::new(
67+
jsx_elem.span().start,
68+
jsx_elem.opening_element.span().start,
69+
));
70+
let between_opening_tag_and_closing_tag = ctx.source_range(Span::new(
71+
jsx_elem.opening_element.span().end,
72+
closing_element.span().start,
73+
));
74+
let after_closing_tag = ctx.source_range(Span::new(
75+
closing_element.span().end,
76+
jsx_elem.span().end,
77+
));
78+
let mut replacement = String::new();
79+
replacement.push_str(&before_opening_tag);
80+
replacement.push_str("<>");
81+
replacement.push_str(&between_opening_tag_and_closing_tag);
82+
replacement.push_str("</>");
83+
replacement.push_str(&after_closing_tag);
84+
fixer.replace(jsx_elem.span(), replacement)
85+
},
86+
);
87+
}
88+
_ => {}
89+
}
90+
}
91+
92+
fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
93+
ctx.source_type().is_jsx()
94+
}
95+
}
96+
97+
fn is_jsx_fragment(elem: &JSXOpeningElement) -> bool {
98+
match &elem.name {
99+
JSXElementName::IdentifierReference(ident) => ident.name == "Fragment",
100+
JSXElementName::MemberExpression(mem_expr) => {
101+
if let JSXMemberExpressionObject::IdentifierReference(ident) = &mem_expr.object {
102+
ident.name == "React" && mem_expr.property.name == "Fragment"
103+
} else {
104+
false
105+
}
106+
}
107+
JSXElementName::NamespacedName(_)
108+
| JSXElementName::Identifier(_)
109+
| JSXElementName::ThisExpression(_) => false,
110+
}
111+
}
112+
113+
#[test]
114+
fn test() {
115+
use crate::tester::Tester;
116+
117+
let pass = vec![
118+
"<><Foo /></>",
119+
"<Fragment key=\"key\"><Foo /></Fragment>",
120+
"<React.Fragment key=\"key\"><Foo /></React.Fragment>",
121+
"<Fragment />",
122+
"<React.Fragment />",
123+
];
124+
125+
let fail = vec!["<Fragment><Foo /></Fragment>", "<React.Fragment><Foo /></React.Fragment>"];
126+
127+
let fix = vec![
128+
("<Fragment><Foo /></Fragment>", "<><Foo /></>"),
129+
("<React.Fragment><Foo /></React.Fragment>", "<><Foo /></>"),
130+
];
131+
Tester::new(JsxFragments::NAME, JsxFragments::PLUGIN, pass, fail)
132+
.expect_fix(fix)
133+
.test_and_snapshot();
134+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-react(jsx-fragments): Shorthand form for React fragments is preferred
5+
╭─[jsx_fragments.tsx:1:2]
6+
1 │ <Fragment><Foo /></Fragment>
7+
· ────────
8+
╰────
9+
help: Use <></> instead of <React.Fragment></React.Fragment>
10+
11+
eslint-plugin-react(jsx-fragments): Shorthand form for React fragments is preferred
12+
╭─[jsx_fragments.tsx:1:2]
13+
1 │ <React.Fragment><Foo /></React.Fragment>
14+
· ──────────────
15+
╰────
16+
help: Use <></> instead of <React.Fragment></React.Fragment>

0 commit comments

Comments
 (0)