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/itchy-aliens-ask.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed #7843: The CSS parser, when `tailwindDirectives` is enabled, correctly parses `--*: initial;`.
37 changes: 27 additions & 10 deletions crates/biome_css_factory/src/generated/node_factory.rs

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

7 changes: 7 additions & 0 deletions crates/biome_css_parser/src/lexer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,12 @@ impl<'src> CssLexer<'src> {
&& current == b'-'
&& self.peek_byte() == Some(b'*')
{
// HACK: handle `--*`
if self.prev_byte() == Some(b'-') {
self.advance(1);
return Some(current as char);
}
// otherwise, handle cases like `--color-*`
return None;
Comment on lines +1014 to 1020
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don’t swallow the * in --* during ident lexing.

This advances past *, removing it from the token stream. The parser then can’t see *, breaking the new lookaheads (--*: / --color-*:) and the Tailwind-exclusive wrapper that expects *. Let the ident consume -- and stop before * so * remains a separate token.

Fix: remove this special-case entirely and rely on the existing logic (plus the MUL start tweak).

Apply this diff:

-        if self.options.is_tailwind_directives_enabled()
-            && current == b'-'
-            && self.peek_byte() == Some(b'*')
-        {
-            // HACK: handle `--*`
-            if self.prev_byte() == Some(b'-') {
-                self.advance(1);
-                return Some(current as char);
-            }
-            // otherwise, handle cases like `--color-*`
-            return None;
-        }

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

🤖 Prompt for AI Agents
In crates/biome_css_parser/src/lexer/mod.rs around lines 1014 to 1020, the ident
lexing special-case that checks for prev_byte() == Some(b'-') and calls
advance(1) is swallowing a '*' in sequences like `--*` and `--color-*`; remove
this special-case entirely so the ident consumes `--` but stops before `*`,
allowing the `*` to remain as its own token and preserving downstream lookaheads
(`--*:` / `--color-*:`) and Tailwind wrapper handling; rely on the existing
ident logic (and the existing MUL start tweak) instead of advancing past `*`.

}

Expand Down Expand Up @@ -1305,6 +1311,7 @@ impl<'src> CssLexer<'src> {
// or the third and fourth code points are a valid escape
// return true.
BSL => self.is_valid_escape_at(3),
MUL => true,
_ => false,
}
}
Expand Down
39 changes: 27 additions & 12 deletions crates/biome_css_parser/src/syntax/property/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ use crate::parser::CssParser;
use crate::syntax::css_modules::{
composes_not_allowed, expected_classes_list, expected_composes_import_source,
};
use crate::syntax::parse_error::{expected_component_value, expected_identifier};
use crate::syntax::parse_error::{
expected_component_value, expected_identifier, tailwind_disabled,
};
use crate::syntax::{
is_at_any_value, is_at_dashed_identifier, is_at_identifier, is_at_string, parse_any_value,
parse_custom_identifier_with_keywords, parse_dashed_identifier, parse_regular_identifier,
parse_string,
CssSyntaxFeatures, is_at_any_value, is_at_dashed_identifier, is_at_identifier, is_at_string,
parse_any_value, parse_custom_identifier_with_keywords, parse_dashed_identifier,
parse_regular_identifier, parse_string,
};
use biome_css_syntax::CssSyntaxKind::*;
use biome_css_syntax::{CssSyntaxKind, T};
use biome_parser::parse_lists::ParseNodeList;
use biome_parser::parse_recovery::{ParseRecovery, ParseRecoveryTokenSet, RecoveryResult};
use biome_parser::prelude::ParsedSyntax;
use biome_parser::prelude::ParsedSyntax::{Absent, Present};
use biome_parser::{Parser, TokenSet, token_set};
use biome_parser::{Parser, SyntaxFeature, TokenSet, token_set};

#[inline]
pub(crate) fn is_at_any_property(p: &mut CssParser) -> bool {
Expand Down Expand Up @@ -160,7 +162,11 @@ const END_OF_COMPOSES_CLASS_TOKEN_SET: TokenSet<CssSyntaxKind> =
#[inline]
fn is_at_generic_property(p: &mut CssParser) -> bool {
is_at_identifier(p)
&& (p.nth_at(1, T![:]) || (p.nth_at(1, T![-]) && p.nth_at(2, T![*]) && p.nth_at(3, T![:])))
&& (p.nth_at(1, T![:])
// handle --*:
|| (p.nth_at(1, T![*]) && p.nth_at(2, T![:]))
// handle --color-*:
|| (p.nth_at(1, T![-]) && p.nth_at(2, T![*]) && p.nth_at(3, T![:])))
}

#[inline]
Expand All @@ -174,13 +180,22 @@ fn parse_generic_property(p: &mut CssParser) -> ParsedSyntax {
if is_at_dashed_identifier(p) {
let ident = parse_dashed_identifier(p).ok();
if let Some(ident) = ident
&& p.options().is_tailwind_directives_enabled()
&& p.at(T![-])
&& p.at_ts(token_set![T![-], T![*]])
{
let m = ident.precede(p);
p.expect(T![-]);
p.expect(T![*]);
m.complete(p, TW_VALUE_THEME_REFERENCE);
CssSyntaxFeatures::Tailwind
.parse_exclusive_syntax(
p,
|p| {
let m = ident.precede(p);
if p.at(T![-]) {
p.expect(T![-]);
}
p.expect(T![*]);
Present(m.complete(p, TW_VALUE_THEME_REFERENCE))
},
|p, m| tailwind_disabled(p, m.range(p)),
)
.ok();
}
} else {
parse_regular_identifier(p).ok();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* Negative test for crates/biome_css_parser/tests/css_test_suite/ok/tailwind/theme/custom-theme.css */

.reset {
--*: initial;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
---
source: crates/biome_css_parser/tests/spec_test.rs
expression: snapshot
---
## Input

```css
/* Negative test for crates/biome_css_parser/tests/css_test_suite/ok/tailwind/theme/custom-theme.css */

.reset {
--*: initial;
}

```


## AST

```
CssRoot {
bom_token: missing (optional),
rules: CssRuleList [
CssQualifiedRule {
prelude: CssSelectorList [
CssCompoundSelector {
nesting_selectors: CssNestedSelectorList [],
simple_selector: missing (optional),
sub_selectors: CssSubSelectorList [
CssClassSelector {
dot_token: [email protected] "." [Comments("/* Negative test for ..."), Newline("\n"), Newline("\n")] [],
name: CssCustomIdentifier {
value_token: [email protected] "reset" [] [Whitespace(" ")],
},
},
],
},
],
block: CssDeclarationOrRuleBlock {
l_curly_token: [email protected] "{" [] [],
items: CssDeclarationOrRuleList [
CssDeclarationWithSemicolon {
declaration: CssDeclaration {
property: CssBogusProperty {
items: [
CssBogusSupportsCondition {
items: [
CssDashedIdentifier {
value_token: [email protected] "--" [Newline("\n"), Whitespace("\t")] [],
},
[email protected] "*" [] [],
],
},
[email protected] ":" [] [Whitespace(" ")],
CssGenericComponentValueList [
CssIdentifier {
value_token: [email protected] "initial" [] [],
},
],
],
},
important: missing (optional),
},
semicolon_token: [email protected] ";" [] [],
},
],
r_curly_token: [email protected] "}" [Newline("\n")] [],
},
},
],
eof_token: [email protected] "" [Newline("\n")] [],
}
```

## CST

```
0: [email protected]
0: (empty)
1: [email protected]
0: [email protected]
0: [email protected]
0: [email protected]
0: [email protected]
1: (empty)
2: [email protected]
0: [email protected]
0: [email protected] "." [Comments("/* Negative test for ..."), Newline("\n"), Newline("\n")] []
1: [email protected]
0: [email protected] "reset" [] [Whitespace(" ")]
1: [email protected]
0: [email protected] "{" [] []
1: [email protected]
0: [email protected]
0: [email protected]
0: [email protected]
0: [email protected]
0: [email protected]
0: [email protected] "--" [Newline("\n"), Whitespace("\t")] []
1: [email protected] "*" [] []
1: [email protected] ":" [] [Whitespace(" ")]
2: [email protected]
0: [email protected]
0: [email protected] "initial" [] []
1: (empty)
1: [email protected] ";" [] []
2: [email protected] "}" [Newline("\n")] []
2: [email protected] "" [Newline("\n")] []

```

## Diagnostics

```
custom-theme.css:4:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Tailwind-specific syntax is disabled.

3 │ .reset {
> 4 │ --*: initial;
│ ^^^
5 │ }
6 │

i Enable `tailwindDirectives` in the css parser options, or remove this if you are not using Tailwind CSS.

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* From tailwind docs: https://tailwindcss.com/docs/theme#using-a-custom-theme */

@theme {
--*: initial;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
source: crates/biome_css_parser/tests/spec_test.rs
expression: snapshot
---
## Input

```css
/* From tailwind docs: https://tailwindcss.com/docs/theme#using-a-custom-theme */

@theme {
--*: initial;
}

```


## AST

```
CssRoot {
bom_token: missing (optional),
rules: CssRuleList [
CssAtRule {
at_token: [email protected] "@" [Comments("/* From tailwind docs ..."), Newline("\n"), Newline("\n")] [],
rule: TwThemeAtRule {
theme_token: [email protected] "theme" [] [Whitespace(" ")],
name: missing (optional),
block: CssDeclarationOrRuleBlock {
l_curly_token: [email protected] "{" [] [],
items: CssDeclarationOrRuleList [
CssDeclarationWithSemicolon {
declaration: CssDeclaration {
property: CssGenericProperty {
name: TwValueThemeReference {
reference: CssDashedIdentifier {
value_token: [email protected] "--" [Newline("\n"), Whitespace("\t")] [],
},
minus_token: missing (optional),
star_token: [email protected] "*" [] [],
},
colon_token: [email protected] ":" [] [Whitespace(" ")],
value: CssGenericComponentValueList [
CssIdentifier {
value_token: [email protected] "initial" [] [],
},
],
},
important: missing (optional),
},
semicolon_token: [email protected] ";" [] [],
},
],
r_curly_token: [email protected] "}" [Newline("\n")] [],
},
},
},
],
eof_token: [email protected] "" [Newline("\n")] [],
}
```

## CST

```
0: [email protected]
0: (empty)
1: [email protected]
0: [email protected]
0: [email protected] "@" [Comments("/* From tailwind docs ..."), Newline("\n"), Newline("\n")] []
1: [email protected]
0: [email protected] "theme" [] [Whitespace(" ")]
1: (empty)
2: [email protected]
0: [email protected] "{" [] []
1: [email protected]
0: [email protected]
0: [email protected]
0: [email protected]
0: [email protected]
0: [email protected]
0: [email protected] "--" [Newline("\n"), Whitespace("\t")] []
1: (empty)
2: [email protected] "*" [] []
1: [email protected] ":" [] [Whitespace(" ")]
2: [email protected]
0: [email protected]
0: [email protected] "initial" [] []
1: (empty)
1: [email protected] ";" [] []
2: [email protected] "}" [Newline("\n")] []
2: [email protected] "" [Newline("\n")] []

```
Loading
Loading