diff --git a/docs/rules/jsx_no_useless_fragment.md b/docs/rules/jsx_no_useless_fragment.md new file mode 100644 index 00000000..3a117630 --- /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 3cfcc299..f5218ab2 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 8361a34c..a323d6f2 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 00000000..a708be97 --- /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 a46f1ad1..c9fb2949 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<>\n

foo <>bar

\n```\n\n### Valid:\n\n```tsx\n<>{foo}\n<>
\n<>foo
\n

foo 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",