From 7aab35397a88708b17ebea3aea351bbfd7017bc9 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 26 Nov 2025 16:39:51 -0700 Subject: [PATCH 1/7] fix(linter): react/jsx-fragments rule should take string argument. The original rule has a config option in the format of `["error", "element"]` or `["error", "syntax"]`. We required a config object, which is technically incorrect. This fixes that in the docs, but we still need to fix the code to accept a string argument instead of an object. I guess we have to make this backward-compatible and accept either? --- .../src/rules/react/jsx_fragments.rs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 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..140fd5f4d2741 100644 --- a/crates/oxc_linter/src/rules/react/jsx_fragments.rs +++ b/crates/oxc_linter/src/rules/react/jsx_fragments.rs @@ -22,11 +22,14 @@ fn jsx_fragments_diagnostic(span: Span, mode: FragmentMode) -> OxcDiagnostic { OxcDiagnostic::warn(msg).with_help(help).with_label(span) } -#[derive(Debug, Default, Clone, JsonSchema, Deserialize, Serialize)] -#[serde(rename_all = "camelCase", default)] +#[derive(Debug, Default, Clone)] pub struct JsxFragments { - /// `syntax` mode: - /// + mode: FragmentMode +} + +#[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 +46,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,14 +63,6 @@ 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, } @@ -89,7 +84,7 @@ declare_oxc_lint!( react, style, fix, - config = JsxFragments, + config = FragmentMode, ); impl Rule for JsxFragments { From 82c876fae79013ecf8b8bb4ffe54b9856706673f Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 26 Nov 2025 16:41:35 -0700 Subject: [PATCH 2/7] Update the diagnostics to use backticks. --- crates/oxc_linter/src/rules/react/jsx_fragments.rs | 8 ++++---- .../src/snapshots/react_jsx_fragments.snap | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_fragments.rs b/crates/oxc_linter/src/rules/react/jsx_fragments.rs index 140fd5f4d2741..a0f50ca8256ad 100644 --- a/crates/oxc_linter/src/rules/react/jsx_fragments.rs +++ b/crates/oxc_linter/src/rules/react/jsx_fragments.rs @@ -10,14 +10,14 @@ use crate::{AstNode, context::LintContext, rule::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) } diff --git a/crates/oxc_linter/src/snapshots/react_jsx_fragments.snap b/crates/oxc_linter/src/snapshots/react_jsx_fragments.snap index 30dc51f0877d5..a62e84940e78c 100644 --- a/crates/oxc_linter/src/snapshots/react_jsx_fragments.snap +++ b/crates/oxc_linter/src/snapshots/react_jsx_fragments.snap @@ -1,23 +1,23 @@ --- 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 `<>`. From 266e0810986f17b52f8919556fe6f26a62e53ad5 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 26 Nov 2025 16:51:32 -0700 Subject: [PATCH 3/7] Allow a string arg or an object argument for backwards-compatibility. And add tests accordingly. --- .../src/rules/react/jsx_fragments.rs | 32 +++++++++++++------ .../src/snapshots/react_jsx_fragments.snap | 7 ++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_fragments.rs b/crates/oxc_linter/src/rules/react/jsx_fragments.rs index a0f50ca8256ad..89faf48f55ebb 100644 --- a/crates/oxc_linter/src/rules/react/jsx_fragments.rs +++ b/crates/oxc_linter/src/rules/react/jsx_fragments.rs @@ -24,7 +24,7 @@ fn jsx_fragments_diagnostic(span: Span, mode: FragmentMode) -> OxcDiagnostic { #[derive(Debug, Default, Clone)] pub struct JsxFragments { - mode: FragmentMode + mode: FragmentMode, } #[derive(Debug, Default, Clone, PartialEq, Eq, Copy, JsonSchema, Deserialize, Serialize)] @@ -88,15 +88,22 @@ declare_oxc_lint!( ); 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(), - } + let arg = value.get(0); + let mode = arg + .and_then(|v| { + // allow either a string argument, eg: ["syntax"], or an object with `mode` field + if let Some(s) = v.as_str() { + Some(FragmentMode::from(s)) + } else { + v.get("mode").and_then(Value::as_str).map(FragmentMode::from) + } + }) + .unwrap_or_default(); + + Self { mode } } fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { @@ -181,24 +188,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 a62e84940e78c..d693f8a83f6d5 100644 --- a/crates/oxc_linter/src/snapshots/react_jsx_fragments.snap +++ b/crates/oxc_linter/src/snapshots/react_jsx_fragments.snap @@ -21,3 +21,10 @@ source: crates/oxc_linter/src/tester.rs · ── ╰──── 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 `<>`. From 84426a70d8e3d91b57a15291cda237222db67ba3 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 26 Nov 2025 17:07:45 -0700 Subject: [PATCH 4/7] Clean up the from_configuration method a bit. --- .../src/rules/react/jsx_fragments.rs | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_fragments.rs b/crates/oxc_linter/src/rules/react/jsx_fragments.rs index 89faf48f55ebb..8e60f203dbf9d 100644 --- a/crates/oxc_linter/src/rules/react/jsx_fragments.rs +++ b/crates/oxc_linter/src/rules/react/jsx_fragments.rs @@ -91,19 +91,13 @@ 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 arg = value.get(0); - let mode = arg - .and_then(|v| { - // allow either a string argument, eg: ["syntax"], or an object with `mode` field - if let Some(s) = v.as_str() { - Some(FragmentMode::from(s)) - } else { - v.get("mode").and_then(Value::as_str).map(FragmentMode::from) - } - }) - .unwrap_or_default(); - - Self { mode } + Self { + mode: value + .get(0) + .and_then(|v| v.as_str().or_else(|| v.get("mode").and_then(Value::as_str))) + .map(FragmentMode::from) + .unwrap_or_default(), + } } fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { From 28d356a9ce6041c7481fd72f167fae7c5fdf6d80 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 26 Nov 2025 18:01:18 -0700 Subject: [PATCH 5/7] Refactor to use an enum. --- .../src/rules/react/jsx_fragments.rs | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_fragments.rs b/crates/oxc_linter/src/rules/react/jsx_fragments.rs index 8e60f203dbf9d..5f2b3fbe7d0ad 100644 --- a/crates/oxc_linter/src/rules/react/jsx_fragments.rs +++ b/crates/oxc_linter/src/rules/react/jsx_fragments.rs @@ -22,9 +22,26 @@ fn jsx_fragments_diagnostic(span: Span, mode: FragmentMode) -> OxcDiagnostic { OxcDiagnostic::warn(msg).with_help(help).with_label(span) } -#[derive(Debug, Default, Clone)] -pub struct JsxFragments { - mode: FragmentMode, +#[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)] @@ -66,12 +83,6 @@ pub enum FragmentMode { 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 /// @@ -91,18 +102,18 @@ 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 { - Self { - mode: value - .get(0) - .and_then(|v| v.as_str().or_else(|| v.get("mode").and_then(Value::as_str))) - .map(FragmentMode::from) - .unwrap_or_default(), - } + // We expect configuration to be an array with a single argument like ["syntax"] or [{"mode":"element"}] + // Take the first element and deserialize that into our helper enum which supports both forms. + value + .get(0) + .cloned() + .and_then(|v| serde_json::from_value::(v).ok()) + .unwrap_or_default() } 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; }; @@ -112,7 +123,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, @@ -136,9 +147,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, From 65f83444aa3e908e5464dbd27ce53380e24c0a40 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Wed, 26 Nov 2025 18:19:10 -0700 Subject: [PATCH 6/7] Update to use DefaultRuleConfig for configuration parsing --- crates/oxc_linter/src/rules/react/jsx_fragments.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_fragments.rs b/crates/oxc_linter/src/rules/react/jsx_fragments.rs index 5f2b3fbe7d0ad..f77973dfd128b 100644 --- a/crates/oxc_linter/src/rules/react/jsx_fragments.rs +++ b/crates/oxc_linter/src/rules/react/jsx_fragments.rs @@ -6,7 +6,7 @@ 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 { @@ -102,13 +102,9 @@ 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 { - // We expect configuration to be an array with a single argument like ["syntax"] or [{"mode":"element"}] - // Take the first element and deserialize that into our helper enum which supports both forms. - value - .get(0) - .cloned() - .and_then(|v| serde_json::from_value::(v).ok()) + serde_json::from_value::>(value) .unwrap_or_default() + .into_inner() } fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { From 5379f687b0b24ab99f99135f2add49338b60cce7 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 01:20:38 +0000 Subject: [PATCH 7/7] [autofix.ci] apply automated fixes --- crates/oxc_linter/src/rules/react/jsx_fragments.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_fragments.rs b/crates/oxc_linter/src/rules/react/jsx_fragments.rs index f77973dfd128b..ae20b2cf8113b 100644 --- a/crates/oxc_linter/src/rules/react/jsx_fragments.rs +++ b/crates/oxc_linter/src/rules/react/jsx_fragments.rs @@ -6,7 +6,12 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::{AstNode, context::LintContext, rule::{DefaultRuleConfig, 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 {