diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e8ccc473fc9..36d0023ea4b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -74,6 +74,19 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b
Contributed by @Conaclos
+- Fixes [#4059](https://github.com/biomejs/biome/issues/4059), the rule [noUselessFragments](https://biomejs.dev/linter/rules/no-useless-fragments/) now correctly handles fragments containing HTML escapes (e.g. ` `) inside expression escapes `{ ... }`.
+The following code is no longer reported:
+
+```jsx
+function Component() {
+ return (
+
{line || <> >}
+ )
+}
+```
+
+Contributed by @fireairforce
+
### Parser
#### Bug Fixes
diff --git a/crates/biome_js_analyze/src/lint/complexity/no_useless_fragments.rs b/crates/biome_js_analyze/src/lint/complexity/no_useless_fragments.rs
index a846f71944b5..0043a119fe5f 100644
--- a/crates/biome_js_analyze/src/lint/complexity/no_useless_fragments.rs
+++ b/crates/biome_js_analyze/src/lint/complexity/no_useless_fragments.rs
@@ -11,9 +11,11 @@ use biome_js_factory::make::{
use biome_js_syntax::{
AnyJsxChild, AnyJsxElementName, AnyJsxTag, JsLanguage, JsLogicalExpression,
JsParenthesizedExpression, JsSyntaxKind, JsxChildList, JsxElement, JsxExpressionAttributeValue,
- JsxFragment, JsxTagExpression, JsxText, T,
+ JsxExpressionChild, JsxFragment, JsxTagExpression, JsxText, T,
+};
+use biome_rowan::{
+ declare_node_union, AstNode, AstNodeList, BatchMutation, BatchMutationExt, SyntaxNodeText,
};
-use biome_rowan::{declare_node_union, AstNode, AstNodeList, BatchMutation, BatchMutationExt};
declare_lint_rule! {
/// Disallow unnecessary fragments
@@ -124,6 +126,7 @@ impl Rule for NoUselessFragments {
let model = ctx.model();
let mut in_jsx_attr_expr = false;
let mut in_js_logical_expr = false;
+ let mut in_jsx_expr = false;
match node {
NoUselessFragmentsQuery::JsxFragment(fragment) => {
let parents_where_fragments_must_be_preserved = node.syntax().parent().map_or(
@@ -139,6 +142,9 @@ impl Rule for NoUselessFragments {
if JsLogicalExpression::can_cast(parent.kind()) {
in_js_logical_expr = true;
}
+ if JsxExpressionChild::can_cast(parent.kind()) {
+ in_jsx_expr = true;
+ }
match JsParenthesizedExpression::try_cast(parent) {
Ok(parenthesized_expression) => {
parenthesized_expression.syntax().parent()
@@ -190,7 +196,19 @@ impl Rule for NoUselessFragments {
}
}
JsSyntaxKind::JSX_TEXT => {
- if !child.syntax().text().to_string().trim().is_empty() {
+ // We need to whitespaces and newlines from the original string.
+ // Since in the JSX newlines aren't trivia, we require to allocate a string to trim from those characters.
+ let original_text = child.text();
+ let child_text = original_text.trim();
+
+ if (in_jsx_expr || in_js_logical_expr)
+ && contains_html_character_references(child_text)
+ {
+ children_where_fragments_must_preserved = true;
+ break;
+ }
+
+ if !child_text.is_empty() {
significant_children += 1;
if first_significant_child.is_none() {
first_significant_child = Some(child);
@@ -401,3 +419,9 @@ impl Rule for NoUselessFragments {
}))
}
}
+
+fn contains_html_character_references(s: &str) -> bool {
+ let and = s.find('&');
+ let semi = s.find(';');
+ matches!((and, semi), (Some(and), Some(semi)) if and < semi)
+}
diff --git a/crates/biome_js_analyze/tests/specs/complexity/noUselessFragments/issue_4059.jsx b/crates/biome_js_analyze/tests/specs/complexity/noUselessFragments/issue_4059.jsx
new file mode 100644
index 000000000000..2bd7ec6335c5
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/complexity/noUselessFragments/issue_4059.jsx
@@ -0,0 +1,17 @@
+function MyComponent() {
+ return (
+ {line || <> >}
+ )
+}
+
+function MyComponent2() {
+ return (
+ {<> >}
+ )
+}
+
+function MyComponent3() {
+ return (
+ {value ?? <> >}
+ )
+}
\ No newline at end of file
diff --git a/crates/biome_js_analyze/tests/specs/complexity/noUselessFragments/issue_4059.jsx.snap b/crates/biome_js_analyze/tests/specs/complexity/noUselessFragments/issue_4059.jsx.snap
new file mode 100644
index 000000000000..4dc837242e07
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/complexity/noUselessFragments/issue_4059.jsx.snap
@@ -0,0 +1,24 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: issue_4059.jsx
+---
+# Input
+```jsx
+function MyComponent() {
+ return (
+ {line || <> >}
+ )
+}
+
+function MyComponent2() {
+ return (
+ {<> >}
+ )
+}
+
+function MyComponent3() {
+ return (
+ {value ?? <> >}
+ )
+}
+```