Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-seals-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#7320](https://github.com/biomejs/biome/issues/7320): The [`useConsistentCurlyBraces`](https://biomejs.dev/linter/rules/use-consistent-curly-braces/) rule now correctly detects a string literal including `"` inside a JSX attribute value.
5 changes: 5 additions & 0 deletions .changeset/short-cases-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#7256](https://github.com/biomejs/biome/issues/7256): The [`useConsistentCurlyBraces`](https://biomejs.dev/linter/rules/use-consistent-curly-braces/) rule now correctly ignores a string literal with braces that contains only whitespaces. Previously, literals that contains single whitespace were only allowed.
Original file line number Diff line number Diff line change
Expand Up @@ -339,17 +339,8 @@ fn handle_attr_init_clause(
match node {
AnyJsxAttributeValue::AnyJsxTag(_) => Some(CurlyBraceResolution::AddBraces),
AnyJsxAttributeValue::JsxExpressionAttributeValue(node) => {
if has_curly_braces && contains_single_space(&node) {
None
} else if has_curly_braces && contains_string_literal(&node) {
let expression = node.expression().ok()?;
let literal = expression
.as_any_js_literal_expression()?
.as_js_string_literal_expression()?;
if !contains_forbidden_chars(literal) {
return Some(CurlyBraceResolution::RemoveBraces);
}
None
if has_curly_braces && contains_string_literal(&node) {
Some(CurlyBraceResolution::RemoveBraces)
} else if !has_curly_braces && contains_jsx_tag(&node) {
Some(CurlyBraceResolution::AddBraces)
} else {
Expand All @@ -368,11 +359,7 @@ fn handle_jsx_child(child: &AnyJsxChild, has_curly_braces: bool) -> Option<Curly
)) = child.expression().as_ref()
{
// Don't suggest removing braces for single space or if forbidden chars found
if literal
.inner_string_text()
.is_ok_and(|text| text.text() == " ")
|| contains_forbidden_chars(literal)
{
if contains_only_spaces(literal) || contains_forbidden_chars(literal) {
return None;
}

Expand Down Expand Up @@ -417,15 +404,10 @@ fn contains_jsx_tag(node: &JsxExpressionAttributeValue) -> bool {
.is_ok_and(|expr| matches!(expr, AnyJsExpression::JsxTagExpression(_)))
}

fn contains_single_space(node: &JsxExpressionAttributeValue) -> bool {
node.expression().is_ok_and(|expr| {
matches!(
expr,
AnyJsExpression::AnyJsLiteralExpression(
AnyJsLiteralExpression::JsStringLiteralExpression(literal)
) if literal.inner_string_text().is_ok_and(|text| text.text() == " ")
)
})
fn contains_only_spaces(literal: &JsStringLiteralExpression) -> bool {
literal
.inner_string_text()
.is_ok_and(|text| text.bytes().all(|b| b == b' '))
}

const FORBIDDEN_CHARS: [char; 4] = ['>', '"', '\'', '}'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,18 @@
<Foo>{/*comment*/'Hello world'/*comment*/}</Foo>

<Foo>{x}{'y'}{z}</Foo>

<Foo foo={' '}></Foo>

{/* https://github.com/biomejs/biome/issues/7320 */}
<ConnectedFieldTemplate
description1=
'Upload a CSV file containing an "email" column, and optional "first_name" and "last_name" columns'
description2={
'Upload a CSV file containing an "email" column, and optional "first_name" and "last_name" columns'
}
className={
"d-block"
}
/>
Comment on lines +18 to +28
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Expand attribute quote/brace cases to lock in #7320.

Add explicit props with ", ', >, and } to prove they unwrap safely in attributes.

Example to append here:

<Foo a={'"'} b={"'"} c={'>'} d={'}'} />

🏁 Script executed:

#!/bin/bash
# Check if we already cover the four attribute characters in specs
rg -n --glob 'crates/biome_js_analyze/tests/specs/style/useConsistentCurlyBraces/**/*' \
  -e "<[^>]+\\{'\\\"'\\}" -e "<[^>]+\\{\"'\"\\}" -e "<[^>]+\\{'>'\\}" -e "<[^>]+\\{'}'\\}"

Length of output: 131


🏁 Script executed:

#!/bin/bash
set -euo pipefail
dir='crates/biome_js_analyze/tests/specs/style/useConsistentCurlyBraces'
if [ ! -d "$dir" ]; then
  echo "DIR_NOT_FOUND: $dir"
  exit 0
fi

python3 - <<'PY'
import os,sys
root='crates/biome_js_analyze/tests/specs/style/useConsistentCurlyBraces'
patterns = ["{'\"'}", "{\"'\"}", "{'>'}", "{'}'}"]
exts = ('.js','.jsx','.ts','.tsx','.mjs','.cjs')
found = False
for dp,_,files in os.walk(root):
    for f in files:
        if not f.lower().endswith(exts):
            continue
        path = os.path.join(dp,f)
        try:
            with open(path,'r',encoding='utf-8') as fh:
                for i,line in enumerate(fh,1):
                    for p in patterns:
                        if p in line:
                            print(f"{path}:{i}:{line.rstrip()}")
                            found = True
        except Exception as e:
            print(f"ERR reading {path}: {e}", file=sys.stderr)
if not found:
    print("NO_MATCHES")
PY

Length of output: 227


Add attribute-level tests for quote/brace cases

valid.jsx already contains {'}'} and {'>'} as children (crates/biome_js_analyze/tests/specs/style/useConsistentCurlyBraces/valid.jsx:27,33). Still need explicit attribute props for all four characters — add to the useConsistentCurlyBraces spec (e.g. crates/biome_js_analyze/tests/specs/style/useConsistentCurlyBraces/invalid.jsx:18–28):

<Foo a={'"'} b={"'"} c={'>'} d={'}'} />
🤖 Prompt for AI Agents
In
crates/biome_js_analyze/tests/specs/style/useConsistentCurlyBraces/invalid.jsx
around lines 18–28, add attribute-level test cases for the four special
characters so the rule sees quote/brace cases as props; specifically add a JSX
element with props a, b, c, d whose values are the characters double-quote,
single-quote, greater-than, and closing brace respectively (use JSX attribute
expressions where appropriate to represent these characters), placing it
alongside the existing ConnectedFieldTemplate example so the file contains
explicit attribute props for all four characters.

</>
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ expression: invalid.jsx
<Foo>{/*comment*/'Hello world'/*comment*/}</Foo>

<Foo>{x}{'y'}{z}</Foo>

<Foo foo={' '}></Foo>

{/* https://github.com/biomejs/biome/issues/7320 */}
<ConnectedFieldTemplate
description1=
'Upload a CSV file containing an "email" column, and optional "first_name" and "last_name" columns'
description2={
'Upload a CSV file containing an "email" column, and optional "first_name" and "last_name" columns'
}
className={
"d-block"
}
/>
</>

```
Expand Down Expand Up @@ -151,8 +165,8 @@ invalid.jsx:14:9 lint/style/useConsistentCurlyBraces FIXABLE ━━━━━
13 │
> 14 │ <Foo>{x}{'y'}{z}</Foo>
│ ^^^^^
15 │ </>
16 │
15 │
16 │ <Foo foo={' '}></Foo>

i JSX child does not need to be wrapped in curly braces.

Expand All @@ -162,3 +176,86 @@ invalid.jsx:14:9 lint/style/useConsistentCurlyBraces FIXABLE ━━━━━
│ -- --

```

```
invalid.jsx:16:10 lint/style/useConsistentCurlyBraces FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Should not have curly braces around expression.

14 │ <Foo>{x}{'y'}{z}</Foo>
15 │
> 16 │ <Foo foo={' '}></Foo>
│ ^^^^^
17 │
18 │ {/* https://github.com/biomejs/biome/issues/7320 */}

i JSX attribute value does not need to be wrapped in curly braces.

i Unsafe fix: Remove curly braces around the expression.

16 │ <Foo·foo={'·'}></Foo>
│ - -

```

```
invalid.jsx:22:15 lint/style/useConsistentCurlyBraces FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Should not have curly braces around expression.

20 │ description1=
21 │ 'Upload a CSV file containing an "email" column, and optional "first_name" and "last_name" columns'
> 22 │ description2={
│ ^
> 23 │ 'Upload a CSV file containing an "email" column, and optional "first_name" and "last_name" columns'
> 24 │ }
│ ^
25 │ className={
26 │ "d-block"

i JSX attribute value does not need to be wrapped in curly braces.

i Unsafe fix: Remove curly braces around the expression.

20 20 │ description1=
21 21 │ 'Upload a CSV file containing an "email" column, and optional "first_name" and "last_name" columns'
22 │ - → description2={
22 │ + → description2=
23 23 │ 'Upload a CSV file containing an "email" column, and optional "first_name" and "last_name" columns'
24 │ - → }
25 24 │ className={
26 25 │ "d-block"


```

```
invalid.jsx:25:12 lint/style/useConsistentCurlyBraces FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Should not have curly braces around expression.

23 │ 'Upload a CSV file containing an "email" column, and optional "first_name" and "last_name" columns'
24 │ }
> 25 │ className={
│ ^
> 26 │ "d-block"
> 27 │ }
│ ^
28 │ />
29 │ </>

i JSX attribute value does not need to be wrapped in curly braces.

i Unsafe fix: Remove curly braces around the expression.

23 23 │ 'Upload a CSV file containing an "email" column, and optional "first_name" and "last_name" columns'
24 24 │ }
25 │ - → className={
25 │ + → className=
26 26 │ "d-block"
27 │ - → }
28 27 │ />
29 28 │ </>


```
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ let baz = 4;

<Foo>{' '}</Foo>

<Foo>{' '}</Foo>

<Foo>Invalid closing tag {'}'}</Foo>

<Foo>{'Invalid closing tag }'}</Foo>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ let baz = 4;

<Foo>{' '}</Foo>

<Foo>{' '}</Foo>

<Foo>Invalid closing tag {'}'}</Foo>

<Foo>{'Invalid closing tag }'}</Foo>
Expand Down
Loading