From a58c76acb44ef509329c7f54c8b1d92ff8820e18 Mon Sep 17 00:00:00 2001
From: connorshea <2977353+connorshea@users.noreply.github.com>
Date: Fri, 5 Dec 2025 15:47:04 +0000
Subject: [PATCH] fix(linter): Fix the `react/jsx-fragments` rule config to
take a string argument (#16175)
The [original rule](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-fragments.md) has a config option in the format of `["error", "element"]` or `["error", "syntax"]`. We required a config object like `["error", { "mode": "element" }]`, which is technically incompatible for existing configs that get migrated to oxlint.
This fixes that in the generated docs, and also makes it work correctly in the oxlint config. We retain compatibility with the object syntax in case anyone has set up oxlint with it already, and I've added tests to ensure both formats of the config option work.
Generated docs now:
```md
## Configuration
This rule accepts one of the following string values:
### `"syntax"`
This is the default mode. It will enforce the shorthand syntax for React fragments, with one exception.
Keys or attributes are not supported by the shorthand syntax, so the rule will not warn on standard-form fragments that use those.
Examples of **incorrect** code for this rule:
\```jsx
\```
Examples of **correct** code for this rule:
\```jsx
<>>
\```
\```jsx
\```
### `"element"`
This mode enforces the standard form for React fragments.
Examples of **incorrect** code for this rule:
\```jsx
<>>
\```
Examples of **correct** code for this rule:
\```jsx
\```
\```jsx
\```
```
---
.../src/rules/react/jsx_fragments.rs | 93 +++++++++++--------
.../src/snapshots/react_jsx_fragments.snap | 19 ++--
2 files changed, 67 insertions(+), 45 deletions(-)
diff --git a/crates/oxc_linter/src/rules/react/jsx_fragments.rs b/crates/oxc_linter/src/rules/react/jsx_fragments.rs
index 9eadeac0ef20b..ae20b2cf8113b 100644
--- a/crates/oxc_linter/src/rules/react/jsx_fragments.rs
+++ b/crates/oxc_linter/src/rules/react/jsx_fragments.rs
@@ -6,27 +6,52 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
-use crate::{AstNode, context::LintContext, rule::Rule, utils::is_jsx_fragment};
+use crate::{
+ AstNode,
+ context::LintContext,
+ rule::{DefaultRuleConfig, Rule},
+ utils::is_jsx_fragment,
+};
fn jsx_fragments_diagnostic(span: Span, mode: FragmentMode) -> OxcDiagnostic {
let msg = if mode == FragmentMode::Element {
- "Standard form for React fragments is preferred"
+ "Standard form for React fragments is preferred."
} else {
- "Shorthand form for React fragments is preferred"
+ "Shorthand form for React fragments is preferred."
};
let help = if mode == FragmentMode::Element {
- "Use instead of <>>"
+ "Use `` instead of `<>>`."
} else {
- "Use <>> instead of "
+ "Use `<>>` instead of ``."
};
OxcDiagnostic::warn(msg).with_help(help).with_label(span)
}
-#[derive(Debug, Default, Clone, JsonSchema, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase", default)]
-pub struct JsxFragments {
- /// `syntax` mode:
- ///
+#[derive(Debug, Clone, JsonSchema, Deserialize)]
+#[serde(untagged)]
+pub enum JsxFragments {
+ Mode(FragmentMode),
+ Object { mode: FragmentMode },
+}
+
+impl Default for JsxFragments {
+ fn default() -> Self {
+ JsxFragments::Mode(FragmentMode::Syntax)
+ }
+}
+
+impl JsxFragments {
+ fn mode(&self) -> FragmentMode {
+ match self {
+ JsxFragments::Mode(m) => *m,
+ JsxFragments::Object { mode } => *mode,
+ }
+ }
+}
+
+#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, JsonSchema, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum FragmentMode {
/// This is the default mode. It will enforce the shorthand syntax for React fragments, with one exception.
/// Keys or attributes are not supported by the shorthand syntax, so the rule will not warn on standard-form fragments that use those.
///
@@ -43,8 +68,8 @@ pub struct JsxFragments {
/// ```jsx
///
/// ```
- ///
- /// `element` mode:
+ #[default]
+ Syntax,
/// This mode enforces the standard form for React fragments.
///
/// Examples of **incorrect** code for this rule:
@@ -60,23 +85,9 @@ pub struct JsxFragments {
/// ```jsx
///
/// ```
- mode: FragmentMode,
-}
-
-#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, JsonSchema, Deserialize, Serialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum FragmentMode {
- #[default]
- Syntax,
Element,
}
-impl From<&str> for FragmentMode {
- fn from(value: &str) -> Self {
- if value == "element" { Self::Element } else { Self::Syntax }
- }
-}
-
declare_oxc_lint!(
/// ### What it does
///
@@ -89,24 +100,21 @@ declare_oxc_lint!(
react,
style,
fix,
- config = JsxFragments,
+ config = FragmentMode,
);
impl Rule for JsxFragments {
+ // Generally we should prefer the string-only syntax for compatibility with the original ESLint rule,
+ // but we originally implemented the rule with only the object syntax, so we support both now.
fn from_configuration(value: Value) -> Self {
- let obj = value.get(0);
- Self {
- mode: obj
- .and_then(|v| v.get("mode"))
- .and_then(Value::as_str)
- .map(FragmentMode::from)
- .unwrap_or_default(),
- }
+ serde_json::from_value::>(value)
+ .unwrap_or_default()
+ .into_inner()
}
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
match node.kind() {
- AstKind::JSXElement(jsx_elem) if self.mode == FragmentMode::Syntax => {
+ AstKind::JSXElement(jsx_elem) if self.mode() == FragmentMode::Syntax => {
let Some(closing_element) = &jsx_elem.closing_element else {
return;
};
@@ -116,7 +124,7 @@ impl Rule for JsxFragments {
return;
}
ctx.diagnostic_with_fix(
- jsx_fragments_diagnostic(jsx_elem.opening_element.name.span(), self.mode),
+ jsx_fragments_diagnostic(jsx_elem.opening_element.name.span(), self.mode()),
|fixer| {
let before_opening_tag = ctx.source_range(Span::new(
jsx_elem.span().start,
@@ -140,9 +148,9 @@ impl Rule for JsxFragments {
},
);
}
- AstKind::JSXFragment(jsx_frag) if self.mode == FragmentMode::Element => {
+ AstKind::JSXFragment(jsx_frag) if self.mode() == FragmentMode::Element => {
ctx.diagnostic_with_fix(
- jsx_fragments_diagnostic(jsx_frag.opening_fragment.span(), self.mode),
+ jsx_fragments_diagnostic(jsx_frag.opening_fragment.span(), self.mode()),
|fixer| {
let before_opening_tag = ctx.source_range(Span::new(
jsx_frag.span().start,
@@ -186,24 +194,31 @@ fn test() {
(r#""#, None),
("", None),
("", None),
+ // Configuration can be done via a string directly, or an object with the `mode` field.
+ ("<>>", Some(json!(["syntax"]))),
+ ("<>>", Some(json!([{"mode": "syntax"}]))),
+ ("", Some(json!(["element"]))),
("", Some(json!([{"mode": "element"}]))),
];
let fail = vec![
("", None),
("", None),
+ ("<>>", Some(json!(["element"]))),
("<>>", Some(json!([{"mode": "element"}]))),
];
let fix = vec![
("", "<>>", None),
("", "<>>", None),
+ ("<>>", "", Some(json!(["element"]))),
(
"<>>",
"",
Some(json!([{"mode": "element"}])),
),
];
+
Tester::new(JsxFragments::NAME, JsxFragments::PLUGIN, pass, fail)
.expect_fix(fix)
.test_and_snapshot();
diff --git a/crates/oxc_linter/src/snapshots/react_jsx_fragments.snap b/crates/oxc_linter/src/snapshots/react_jsx_fragments.snap
index 30dc51f0877d5..d693f8a83f6d5 100644
--- a/crates/oxc_linter/src/snapshots/react_jsx_fragments.snap
+++ b/crates/oxc_linter/src/snapshots/react_jsx_fragments.snap
@@ -1,23 +1,30 @@
---
source: crates/oxc_linter/src/tester.rs
---
- ⚠ eslint-plugin-react(jsx-fragments): Shorthand form for React fragments is preferred
+ ⚠ eslint-plugin-react(jsx-fragments): Shorthand form for React fragments is preferred.
╭─[jsx_fragments.tsx:1:2]
1 │
· ────────
╰────
- help: Use <>> instead of
+ help: Use `<>>` instead of ``.
- ⚠ eslint-plugin-react(jsx-fragments): Shorthand form for React fragments is preferred
+ ⚠ eslint-plugin-react(jsx-fragments): Shorthand form for React fragments is preferred.
╭─[jsx_fragments.tsx:1:2]
1 │
· ──────────────
╰────
- help: Use <>> instead of
+ help: Use `<>>` instead of ``.
- ⚠ eslint-plugin-react(jsx-fragments): Standard form for React fragments is preferred
+ ⚠ eslint-plugin-react(jsx-fragments): Standard form for React fragments is preferred.
╭─[jsx_fragments.tsx:1:1]
1 │ <>>
· ──
╰────
- help: Use instead of <>>
+ help: Use `` instead of `<>>`.
+
+ ⚠ eslint-plugin-react(jsx-fragments): Standard form for React fragments is preferred.
+ ╭─[jsx_fragments.tsx:1:1]
+ 1 │ <>>
+ · ──
+ ╰────
+ help: Use `` instead of `<>>`.