From dcdbb1a9da8db8c5a7ca4532784ca29612b646ab Mon Sep 17 00:00:00 2001
From: Marvin Hagemeister
Date: Fri, 29 Nov 2024 16:36:25 +0100
Subject: [PATCH] feat: add jsx-no-useless-fragment rule (#1357)
* feat: add jsx-no-useless-fragment rule
* fix: clippy
* fix: update schemas
---
docs/rules/jsx_no_useless_fragment.md | 20 +++++
schemas/rules.v1.json | 1 +
src/rules.rs | 2 +
src/rules/jsx_no_useless_fragment.rs | 120 ++++++++++++++++++++++++++
www/static/docs.json | 10 +++
5 files changed, 153 insertions(+)
create mode 100644 docs/rules/jsx_no_useless_fragment.md
create mode 100644 src/rules/jsx_no_useless_fragment.rs
diff --git a/docs/rules/jsx_no_useless_fragment.md b/docs/rules/jsx_no_useless_fragment.md
new file mode 100644
index 000000000..3a1176308
--- /dev/null
+++ b/docs/rules/jsx_no_useless_fragment.md
@@ -0,0 +1,20 @@
+Fragments are only necessary at the top of a JSX "block" and only when there are
+multiple children. Fragments are not needed in other scenarios.
+
+### Invalid:
+
+```tsx
+<>>
+<>>
+<>>
+foo <>bar>
+```
+
+### Valid:
+
+```tsx
+<>{foo}>
+<>>
+<>foo >
+foo bar
+```
diff --git a/schemas/rules.v1.json b/schemas/rules.v1.json
index 3cfcc2999..f5218ab28 100644
--- a/schemas/rules.v1.json
+++ b/schemas/rules.v1.json
@@ -24,6 +24,7 @@
"jsx-curly-braces",
"jsx-no-children-prop",
"jsx-no-duplicate-props",
+ "jsx-no-useless-fragment",
"jsx-props-no-spread-multi",
"jsx-void-dom-elements-no-children",
"no-array-constructor",
diff --git a/src/rules.rs b/src/rules.rs
index 8361a34c9..a323d6f26 100644
--- a/src/rules.rs
+++ b/src/rules.rs
@@ -31,6 +31,7 @@ pub mod jsx_boolean_value;
pub mod jsx_curly_braces;
pub mod jsx_no_children_prop;
pub mod jsx_no_duplicate_props;
+pub mod jsx_no_useless_fragment;
pub mod jsx_props_no_spread_multi;
pub mod jsx_void_dom_elements_no_children;
pub mod no_array_constructor;
@@ -270,6 +271,7 @@ fn get_all_rules_raw() -> Vec> {
Box::new(jsx_curly_braces::JSXCurlyBraces),
Box::new(jsx_no_children_prop::JSXNoChildrenProp),
Box::new(jsx_no_duplicate_props::JSXNoDuplicateProps),
+ Box::new(jsx_no_useless_fragment::JSXNoUselessFragment),
Box::new(jsx_props_no_spread_multi::JSXPropsNoSpreadMulti),
Box::new(jsx_void_dom_elements_no_children::JSXVoidDomElementsNoChildren),
Box::new(no_array_constructor::NoArrayConstructor),
diff --git a/src/rules/jsx_no_useless_fragment.rs b/src/rules/jsx_no_useless_fragment.rs
new file mode 100644
index 000000000..a708be972
--- /dev/null
+++ b/src/rules/jsx_no_useless_fragment.rs
@@ -0,0 +1,120 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+use super::{Context, LintRule};
+use crate::handler::{Handler, Traverse};
+use crate::tags::{self, Tags};
+use crate::Program;
+use deno_ast::view::{JSXElement, JSXElementChild, JSXFragment};
+use deno_ast::SourceRanged;
+
+#[derive(Debug)]
+pub struct JSXNoUselessFragment;
+
+const CODE: &str = "jsx-no-useless-fragment";
+
+impl LintRule for JSXNoUselessFragment {
+ fn tags(&self) -> Tags {
+ &[tags::RECOMMENDED, tags::REACT, tags::JSX, tags::FRESH]
+ }
+
+ fn code(&self) -> &'static str {
+ CODE
+ }
+
+ fn lint_program_with_ast_view(
+ &self,
+ context: &mut Context,
+ program: Program,
+ ) {
+ JSXNoUselessFragmentHandler.traverse(program, context);
+ }
+
+ #[cfg(feature = "docs")]
+ fn docs(&self) -> &'static str {
+ include_str!("../../docs/rules/jsx_no_useless_fragment.md")
+ }
+}
+
+const MESSAGE: &str = "Unnecessary Fragment detected";
+const HINT: &str = "Remove this Fragment";
+
+struct JSXNoUselessFragmentHandler;
+
+impl Handler for JSXNoUselessFragmentHandler {
+ // Check root fragments
+ fn jsx_fragment(&mut self, node: &JSXFragment, ctx: &mut Context) {
+ if node.children.is_empty() {
+ ctx.add_diagnostic_with_hint(node.range(), CODE, MESSAGE, HINT);
+ } else if node.children.len() == 1 {
+ if let Some(
+ JSXElementChild::JSXElement(_) | JSXElementChild::JSXFragment(_),
+ ) = &node.children.first()
+ {
+ ctx.add_diagnostic_with_hint(node.range(), CODE, MESSAGE, HINT);
+ }
+ }
+ }
+
+ fn jsx_element(&mut self, node: &JSXElement, ctx: &mut Context) {
+ for child in node.children {
+ if let JSXElementChild::JSXFragment(frag) = child {
+ ctx.add_diagnostic_with_hint(frag.range(), CODE, MESSAGE, HINT);
+ }
+ }
+ }
+}
+
+// most tests are taken from ESlint, commenting those
+// requiring code path support
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn jsx_no_useless_fragment_valid() {
+ assert_lint_ok! {
+ JSXNoUselessFragment,
+ filename: "file:///foo.jsx",
+ r#"<>>"#,
+ r#"<>foo>"#,
+ r#"<>{foo}>"#,
+ r#"<>{foo}bar>"#,
+ };
+ }
+
+ #[test]
+ fn jsx_no_useless_fragment_invalid() {
+ assert_lint_err! {
+ JSXNoUselessFragment,
+ filename: "file:///foo.jsx",
+ r#"<>>"#: [
+ {
+ col: 0,
+ message: MESSAGE,
+ hint: HINT,
+ }
+ ],
+ r#"<>>"#: [
+ {
+ col: 0,
+ message: MESSAGE,
+ hint: HINT,
+ }
+ ],
+ r#"foo <>bar>
"#: [
+ {
+ col: 7,
+ message: MESSAGE,
+ hint: HINT,
+ }
+ ],
+ r#"foo <>
>
"#: [
+ {
+ col: 7,
+ message: MESSAGE,
+ hint: HINT,
+ }
+ ],
+ };
+ }
+}
diff --git a/www/static/docs.json b/www/static/docs.json
index a46f1ad1d..c9fb29497 100644
--- a/www/static/docs.json
+++ b/www/static/docs.json
@@ -158,6 +158,16 @@
"jsx"
]
},
+ {
+ "code": "jsx-no-useless-fragment",
+ "docs": "Fragments are only necessary at the top of a JSX \"block\" and only when there are\nmultiple children. Fragments are not needed in other scenarios.\n\n### Invalid:\n\n```tsx\n<>>\n<>>\n<>>\nfoo <>bar>
\n```\n\n### Valid:\n\n```tsx\n<>{foo}>\n<>>\n<>foo >\nfoo bar
\n```\n",
+ "tags": [
+ "recommended",
+ "react",
+ "jsx",
+ "fresh"
+ ]
+ },
{
"code": "jsx-props-no-spread-multi",
"docs": "Spreading the same expression twice is typically a mistake and causes\nunnecessary computations.\n\n### Invalid:\n\n```tsx\n\n\n\n```\n\n### Valid:\n\n```tsx\n\n\n\n```\n",