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
14 changes: 14 additions & 0 deletions crates/biome_tailwind_factory/src/generated/node_factory.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions crates/biome_tailwind_factory/src/generated/syntax_factory.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 24 additions & 2 deletions crates/biome_tailwind_parser/src/lexer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ impl<'src> TailwindLexer<'src> {
bracket @ (b'[' | b']' | b'(' | b')') => self.consume_bracket(bracket),
_ if self.current_kind == T!['['] => self.consume_bracketed_thing(TW_SELECTOR, b']'),
_ if self.current_kind == T!['('] => self.consume_bracketed_thing(TW_VALUE, b')'),
_ if self.current_kind == T![-] => self.consume_named_value(),
_ if self.current_kind == T![-] => self.consume_after_dash(),
_ if self.current_kind == T![/] => self.consume_modifier(),
b':' => self.consume_byte(T![:]),
b'-' => self.consume_byte(T![-]),
Expand Down Expand Up @@ -130,7 +130,11 @@ impl<'src> TailwindLexer<'src> {
let end = BASENAME_STORE.matcher(slice).base_end();
self.advance(end);

TW_BASE
if end == 4 && &slice[..end] == b"data" {
DATA_KW
} else {
TW_BASE
}
}

fn consume_named_value(&mut self) -> TailwindSyntaxKind {
Expand All @@ -153,6 +157,24 @@ impl<'src> TailwindLexer<'src> {
TW_VALUE
}

/// After seeing a '-', we usually lex a value. However, if the next bytes are "data"
/// and they are followed by another '-', this is a Tailwind data-attribute variant.
/// In that case, emit DATA_KW so the parser can recognize `TwDataAttribute`.
fn consume_after_dash(&mut self) -> TailwindSyntaxKind {
self.assert_current_char_boundary();

let bytes = self.source.as_bytes();
let slice = &bytes[self.position..];

if slice.len() >= 5 && &slice[..4] == b"data" && slice[4] == b'-' {
// Advance past "data" only. The following '-' will be emitted as its own token.
self.advance(4);
return DATA_KW;
}

self.consume_named_value()
}

fn consume_modifier(&mut self) -> TailwindSyntaxKind {
self.assert_current_char_boundary();

Expand Down
4 changes: 4 additions & 0 deletions crates/biome_tailwind_parser/src/syntax/value.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::parser::TailwindParser;
use crate::syntax::variant::parse_data_attribute;
use crate::token_source::TailwindLexContext;
use biome_parser::Parser;
use biome_parser::parsed_syntax::ParsedSyntax::{Absent, Present};
Expand All @@ -13,6 +14,9 @@ pub(crate) fn parse_value(p: &mut TailwindParser) -> ParsedSyntax {
if p.at(T!['(']) {
return parse_css_variable_value(p);
}
if p.at(T![data]) {
return parse_data_attribute(p);
}
parse_named_value(p)
}

Expand Down
16 changes: 16 additions & 0 deletions crates/biome_tailwind_parser/src/syntax/variant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ pub(crate) fn parse_variant(p: &mut TailwindParser) -> ParsedSyntax {
// variants can't start with a negative sign
return Absent;
}
if p.at(T![data]) {
return parse_data_attribute(p);
}
if p.at(T!['[']) {
return parse_arbitrary_variant(p);
}
Expand Down Expand Up @@ -135,3 +138,16 @@ fn parse_static_or_functional_variant(p: &mut TailwindParser) -> ParsedSyntax {

Present(m.complete(p, TW_FUNCTIONAL_VARIANT))
}

pub(crate) fn parse_data_attribute(p: &mut TailwindParser) -> ParsedSyntax {
if !p.at(T![data]) {
return Absent;
}

let m = p.start();
p.bump(T![data]);
p.expect(T![-]);
parse_value(p).or_add_diagnostic(p, expected_value);

Present(m.complete(p, TW_DATA_ATTRIBUTE))
}
Comment on lines +142 to +153
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.

⚠️ Potential issue | 🔴 Critical

Critical: Fix recursion bug by implementing separate parse function for AnyTwDataAttributeValue.

The implementation doesn't match the grammar specification. Line 150 calls parse_value, which parses AnyTwValue (including TwDataAttribute). However, the grammar defines AnyTwDataAttributeValue as:

AnyTwDataAttributeValue =
    TwNamedValue |
    TwArbitraryValue |
    TwBogusValue

This creates infinite recursion for inputs like data-data-active:

  1. Parser sees DATA_KW, calls parse_data_attribute
  2. Bumps data, expects -, calls parse_value
  3. parse_value sees DATA_KW again, calls parse_data_attribute
  4. → Infinite recursion

The lexer will emit DATA_KW for "data" following a dash (via consume_after_dash), enabling this recursion.

🔎 Recommended fix

Implement a separate parsing function that matches the grammar:

 pub(crate) fn parse_data_attribute(p: &mut TailwindParser) -> ParsedSyntax {
     if !p.at(T![data]) {
         return Absent;
     }

     let m = p.start();
     p.bump(T![data]);
     p.expect(T![-]);
-    parse_value(p).or_add_diagnostic(p, expected_value);
+    parse_data_attribute_value(p).or_add_diagnostic(p, expected_value);

     Present(m.complete(p, TW_DATA_ATTRIBUTE))
 }
+
+fn parse_data_attribute_value(p: &mut TailwindParser) -> ParsedSyntax {
+    if p.at(T!['[']) {
+        return parse_arbitrary_value(p);
+    }
+    parse_named_value(p)
+}

You'll need to make parse_arbitrary_value and parse_named_value crate-visible or move this helper into value.rs.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In crates/biome_tailwind_parser/src/syntax/variant.rs around lines 142 to 153,
the current parse_data_attribute calls parse_value which includes
TwDataAttribute and leads to infinite recursion for inputs like
"data-data-active"; replace that call with a new function that implements the
grammar AnyTwDataAttributeValue = TwNamedValue | TwArbitraryValue | TwBogusValue
(e.g., parse_any_tw_data_attribute_value) that tries parse_named_value, then
parse_arbitrary_value, then produces a bogus value on failure; make
parse_named_value and parse_arbitrary_value crate-visible (pub(crate)) or move
the helper into value.rs so parse_data_attribute can call only this
non-recursive helper and avoid re-entering parse_data_attribute via parse_value.

2 changes: 1 addition & 1 deletion crates/biome_tailwind_parser/tests/quick_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use biome_test_utils::has_bogus_nodes_or_empty_slots;
#[ignore]
#[test]
pub fn quick_test() {
let code = r#"-top-4 -mb-2"#;
let code = r#"group-data-[collapsible=icon]:hidden"#;

let root = parse_tailwind(code);
let syntax = root.syntax();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data-active:text-red-500
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
source: crates/biome_tailwind_parser/tests/spec_test.rs
expression: snapshot
---
## Input

```text
data-active:text-red-500

```


## AST

```
TwRoot {
bom_token: missing (optional),
candidates: TwCandidateList [
TwFullCandidate {
variants: TwVariantList [
TwDataAttribute {
data_token: DATA_KW@0..4 "data" [] [],
minus_token: DASH@4..5 "-" [] [],
value: TwNamedValue {
value_token: TW_VALUE@5..11 "active" [] [],
},
},
COLON@11..12 ":" [] [],
],
negative_token: missing (optional),
candidate: TwFunctionalCandidate {
base_token: TW_BASE@12..16 "text" [] [],
minus_token: DASH@16..17 "-" [] [],
value: TwNamedValue {
value_token: TW_VALUE@17..24 "red-500" [] [],
},
modifier: missing (optional),
},
excl_token: missing (optional),
},
],
eof_token: EOF@24..25 "" [Newline("\n")] [],
}
```

## CST

```
0: TW_ROOT@0..25
0: (empty)
1: TW_CANDIDATE_LIST@0..24
0: TW_FULL_CANDIDATE@0..24
0: TW_VARIANT_LIST@0..12
0: TW_DATA_ATTRIBUTE@0..11
0: DATA_KW@0..4 "data" [] []
1: DASH@4..5 "-" [] []
2: TW_NAMED_VALUE@5..11
0: TW_VALUE@5..11 "active" [] []
1: COLON@11..12 ":" [] []
1: (empty)
2: TW_FUNCTIONAL_CANDIDATE@12..24
0: TW_BASE@12..16 "text" [] []
1: DASH@16..17 "-" [] []
2: TW_NAMED_VALUE@17..24
0: TW_VALUE@17..24 "red-500" [] []
3: (empty)
3: (empty)
2: EOF@24..25 "" [Newline("\n")] []

```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
group-data-[collapsible=icon]:hidden
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
source: crates/biome_tailwind_parser/tests/spec_test.rs
expression: snapshot
---
## Input

```text
group-data-[collapsible=icon]:hidden

```


## AST

```
TwRoot {
bom_token: missing (optional),
candidates: TwCandidateList [
TwFullCandidate {
variants: TwVariantList [
TwFunctionalVariant {
base_token: TW_BASE@0..5 "group" [] [],
minus_token: DASH@5..6 "-" [] [],
value: TwDataAttribute {
data_token: DATA_KW@6..10 "data" [] [],
minus_token: DASH@10..11 "-" [] [],
value: TwArbitraryValue {
l_brack_token: L_BRACKET@11..12 "[" [] [],
value_token: TW_VALUE@12..28 "collapsible=icon" [] [],
r_brack_token: R_BRACKET@28..29 "]" [] [],
},
},
},
COLON@29..30 ":" [] [],
],
negative_token: missing (optional),
candidate: TwStaticCandidate {
base_token: TW_BASE@30..36 "hidden" [] [],
},
excl_token: missing (optional),
},
],
eof_token: EOF@36..37 "" [Newline("\n")] [],
}
```

## CST

```
0: TW_ROOT@0..37
0: (empty)
1: TW_CANDIDATE_LIST@0..36
0: TW_FULL_CANDIDATE@0..36
0: TW_VARIANT_LIST@0..30
0: TW_FUNCTIONAL_VARIANT@0..29
0: TW_BASE@0..5 "group" [] []
1: DASH@5..6 "-" [] []
2: TW_DATA_ATTRIBUTE@6..29
0: DATA_KW@6..10 "data" [] []
1: DASH@10..11 "-" [] []
2: TW_ARBITRARY_VALUE@11..29
0: L_BRACKET@11..12 "[" [] []
1: TW_VALUE@12..28 "collapsible=icon" [] []
2: R_BRACKET@28..29 "]" [] []
1: COLON@29..30 ":" [] []
1: (empty)
2: TW_STATIC_CANDIDATE@30..36
0: TW_BASE@30..36 "hidden" [] []
3: (empty)
2: EOF@36..37 "" [Newline("\n")] []

```
12 changes: 9 additions & 3 deletions crates/biome_tailwind_syntax/src/generated/kind.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/biome_tailwind_syntax/src/generated/macros.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading