Skip to content

Commit

Permalink
feat: add jsx-no-useless-fragment rule (#1357)
Browse files Browse the repository at this point in the history
* feat: add jsx-no-useless-fragment rule

* fix: clippy

* fix: update schemas
  • Loading branch information
marvinhagemeister authored Nov 29, 2024
1 parent c9b8aec commit dcdbb1a
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 0 deletions.
20 changes: 20 additions & 0 deletions docs/rules/jsx_no_useless_fragment.md
Original file line number Diff line number Diff line change
@@ -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
<></>
<><div /></>
<><App /></>
<p>foo <>bar</></p>
```

### Valid:

```tsx
<>{foo}</>
<><div /><div /></>
<>foo <div /></>
<p>foo bar</p>
```
1 change: 1 addition & 0 deletions schemas/rules.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -270,6 +271,7 @@ fn get_all_rules_raw() -> Vec<Box<dyn LintRule>> {
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),
Expand Down
120 changes: 120 additions & 0 deletions src/rules/jsx_no_useless_fragment.rs
Original file line number Diff line number Diff line change
@@ -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#"<><div /><div /></>"#,
r#"<>foo<div /></>"#,
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#"<><div /></>"#: [
{
col: 0,
message: MESSAGE,
hint: HINT,
}
],
r#"<p>foo <>bar</></p>"#: [
{
col: 7,
message: MESSAGE,
hint: HINT,
}
],
r#"<p>foo <><div /><div /></></p>"#: [
{
col: 7,
message: MESSAGE,
hint: HINT,
}
],
};
}
}
10 changes: 10 additions & 0 deletions www/static/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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<><div /></>\n<><App /></>\n<p>foo <>bar</></p>\n```\n\n### Valid:\n\n```tsx\n<>{foo}</>\n<><div /><div /></>\n<>foo <div /></>\n<p>foo bar</p>\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<div {...foo} {...foo} />\n<div {...foo} a {...foo} />\n<Foo {...foo.bar} {...foo.bar} />\n```\n\n### Valid:\n\n```tsx\n<div {...foo} />\n<div {...foo.bar} a />\n<Foo {...foo.bar} />\n```\n",
Expand Down

0 comments on commit dcdbb1a

Please sign in to comment.