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
10 changes: 10 additions & 0 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.

1 change: 1 addition & 0 deletions crates/biome_css_factory/src/generated/syntax_factory.rs

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

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ impl FormatRule<AnyCssIfTestBooleanExpr> for FormatAnyCssIfTestBooleanExpr {
AnyCssIfTestBooleanExpr::AnyCssIfTestBooleanOrCombinableExpr(node) => {
node.format().fmt(f)
}
AnyCssIfTestBooleanExpr::CssBogusIfTestBooleanExpr(node) => node.format().fmt(f),
AnyCssIfTestBooleanExpr::CssIfTestBooleanNotExpr(node) => node.format().fmt(f),
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use crate::FormatBogusNodeRule;
use biome_css_syntax::CssBogusIfTestBooleanExpr;
#[derive(Debug, Clone, Default)]
pub(crate) struct FormatCssBogusIfTestBooleanExpr;
impl FormatBogusNodeRule<CssBogusIfTestBooleanExpr> for FormatCssBogusIfTestBooleanExpr {}
1 change: 1 addition & 0 deletions crates/biome_css_formatter/src/css/bogus/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub(crate) mod bogus_font_family_name;
pub(crate) mod bogus_font_feature_values_item;
pub(crate) mod bogus_if_branch;
pub(crate) mod bogus_if_test;
pub(crate) mod bogus_if_test_boolean_expr;
pub(crate) mod bogus_keyframes_item;
pub(crate) mod bogus_keyframes_name;
pub(crate) mod bogus_layer;
Expand Down
40 changes: 40 additions & 0 deletions crates/biome_css_formatter/src/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7874,6 +7874,46 @@ impl IntoFormat<CssFormatContext> for biome_css_syntax::CssBogusIfTest {
)
}
}
impl FormatRule<biome_css_syntax::CssBogusIfTestBooleanExpr>
for crate::css::bogus::bogus_if_test_boolean_expr::FormatCssBogusIfTestBooleanExpr
{
type Context = CssFormatContext;
#[inline(always)]
fn fmt(
&self,
node: &biome_css_syntax::CssBogusIfTestBooleanExpr,
f: &mut CssFormatter,
) -> FormatResult<()> {
FormatBogusNodeRule::<biome_css_syntax::CssBogusIfTestBooleanExpr>::fmt(self, node, f)
}
}
impl AsFormat<CssFormatContext> for biome_css_syntax::CssBogusIfTestBooleanExpr {
type Format<'a> = FormatRefWithRule<
'a,
biome_css_syntax::CssBogusIfTestBooleanExpr,
crate::css::bogus::bogus_if_test_boolean_expr::FormatCssBogusIfTestBooleanExpr,
>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(
self,
crate::css::bogus::bogus_if_test_boolean_expr::FormatCssBogusIfTestBooleanExpr::default(
),
)
}
}
impl IntoFormat<CssFormatContext> for biome_css_syntax::CssBogusIfTestBooleanExpr {
type Format = FormatOwnedWithRule<
biome_css_syntax::CssBogusIfTestBooleanExpr,
crate::css::bogus::bogus_if_test_boolean_expr::FormatCssBogusIfTestBooleanExpr,
>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(
self,
crate::css::bogus::bogus_if_test_boolean_expr::FormatCssBogusIfTestBooleanExpr::default(
),
)
}
}
impl FormatRule<biome_css_syntax::CssBogusKeyframesItem>
for crate::css::bogus::bogus_keyframes_item::FormatCssBogusKeyframesItem
{
Expand Down
10 changes: 10 additions & 0 deletions crates/biome_css_parser/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,23 @@ pub(crate) struct CssParserState {
/// handling top-level `@rules` or style declarations directly under the stylesheet.
/// This distinction is critical for correctly interpreting and parsing different sections of a CSS document.
pub(crate) is_nesting_block: bool,

/// Indicates whether the parser encountered an `if()` function during speculative parsing.
///
/// This flag is used to handle a conflict between speculative parsing and `if()` function parsing.
/// The `if()` function uses semicolons as branch separators which conflicts with speculative parsing
/// that uses semicolons to detect declaration endings. When this flag is set during a failed
/// speculative parse, the parser knows to retry without speculative mode, allowing proper error
/// recovery inside the `if()` function.
pub(crate) encountered_if_function: bool,
}

impl CssParserState {
pub fn new() -> Self {
Self {
speculative_parsing: false,
is_nesting_block: false,
encountered_if_function: false,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,23 @@ impl ParseNodeList for DeclarationOrRuleList {
// font-weight: 500;
// }
// }

// Reset the if-function flag before parsing. This flag is used to detect
// if we encountered an if() function during speculative parsing.
p.state_mut().encountered_if_function = false;

// Attempt to parse the current block as a declaration.
let declaration = try_parse(p, |p| {
let declaration = parse_any_declaration_with_semicolon(p);

// If we encountered an if() function, always fail speculative parsing.
// The if() function uses semicolons as branch separators, which can cause
// p.last() to be `;` even when the declaration is incomplete. By failing
// here, we force a non-speculative retry where recovery is enabled.
if p.state().encountered_if_function {
return Err(());
}

// Check if the *last* token parsed is a semicolon
// (;) or if the parser is at a closing brace (}).
// ; - Indicates the end of a declaration.
Expand All @@ -99,6 +113,13 @@ impl ParseNodeList for DeclarationOrRuleList {
return declaration;
}

// If the speculative parse failed and we encountered an if() function,
// we know this is a declaration (not a rule) because if() can only appear
// in declaration values. Parse again with recovery enabled.
if std::mem::take(&mut p.state_mut().encountered_if_function) {
return parse_any_declaration_with_semicolon(p);
}

// If parsing as a declaration failed,
// attempt to parse the current block as a nested qualified rule.
let rule = try_parse(p, |p| {
Expand Down
2 changes: 1 addition & 1 deletion crates/biome_css_parser/src/syntax/property/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ impl ParseNodeList for GenericComponentValueList {
}

#[inline]
fn is_at_generic_component_value(p: &mut CssParser) -> bool {
pub(crate) fn is_at_generic_component_value(p: &mut CssParser) -> bool {
is_at_any_value(p) || is_at_generic_delimiter(p)
}

Expand Down
80 changes: 60 additions & 20 deletions crates/biome_css_parser/src/syntax/value/if.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ use biome_css_syntax::CssSyntaxKind;
use biome_css_syntax::CssSyntaxKind::*;
use biome_css_syntax::T;
use biome_parser::Parser;
use biome_parser::TokenSet;
use biome_parser::parse_lists::ParseNodeList;
use biome_parser::parse_lists::ParseSeparatedList;
use biome_parser::parse_recovery::ParseRecovery;
use biome_parser::parse_recovery::ParseRecoveryTokenSet;
use biome_parser::parse_recovery::RecoveryResult;
use biome_parser::parsed_syntax::ParsedSyntax::{Absent, Present};
use biome_parser::prelude::{CompletedMarker, ParsedSyntax};
use biome_parser::token_set;

use crate::parser::CssParser;
use crate::syntax::at_rule::container::error::expected_any_container_style_query;
Expand All @@ -25,6 +28,10 @@ use crate::syntax::parse_declaration;
use crate::syntax::property::GenericComponentValueList;
use crate::syntax::value::parse_error::expected_if_branch;
use crate::syntax::value::parse_error::expected_if_test_boolean_expr_group;
use crate::syntax::value::parse_error::expected_if_test_boolean_not_expr;

const IF_BRANCH_RECOVERY_TOKEN_SET: TokenSet<CssSyntaxKind> =
token_set![T![;], T![')'], T!['}'], EOF];

pub(crate) fn is_at_if_function(p: &mut CssParser) -> bool {
p.at(T![if])
Expand Down Expand Up @@ -94,6 +101,12 @@ pub(crate) fn parse_if_function(p: &mut CssParser) -> ParsedSyntax {
return Absent;
}

// Signal that we encountered an if() function. This flag is used by
// declaration_or_rule_list_block to handle speculative parsing conflicts.
if p.state().speculative_parsing {
p.state_mut().encountered_if_function = true;
}

let m = p.start();

p.bump(T![if]);
Expand Down Expand Up @@ -217,6 +230,11 @@ fn parse_if_media_test(p: &mut CssParser) -> ParsedSyntax {
Present(m.complete(p, CSS_IF_MEDIA_TEST))
}

#[inline]
fn is_at_if_test(p: &mut CssParser) -> bool {
is_at_if_supports_test(p) || is_at_if_style_test(p) || is_at_if_media_test(p)
}

#[inline]
fn parse_if_test(p: &mut CssParser) -> ParsedSyntax {
if is_at_if_supports_test(p) {
Expand All @@ -234,13 +252,26 @@ fn parse_if_test(p: &mut CssParser) -> ParsedSyntax {
Absent
}

#[inline]
fn is_at_if_test_boolean_expr_group(p: &mut CssParser) -> bool {
p.at(T!['(']) || is_at_if_test(p)
}

#[inline]
fn parse_any_if_test_boolean_expr_group(p: &mut CssParser) -> ParsedSyntax {
// ( <boolean-expr> )
if p.at(T!['(']) {
let m = p.start();
p.bump(T!['(']);
parse_any_if_test_boolean_expr(p).ok();

parse_any_if_test_boolean_expr(p)
.or_recover_with_token_set(
p,
&ParseRecoveryTokenSet::new(CSS_BOGUS_IF_TEST_BOOLEAN_EXPR, token_set![T![')']]),
expected_if_test_boolean_not_expr,
)
.ok();
Comment on lines +266 to +273
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 | 🟡 Minor

Diagnostic mismatch inside parenthesised boolean group.
When parsing ( <boolean-expr> ), the recovery should reference a boolean‑expr group, not just “not” specifically.

🔧 Proposed fix
-            expected_if_test_boolean_not_expr,
+            expected_if_test_boolean_expr_group,
🤖 Prompt for AI Agents
In `@crates/biome_css_parser/src/syntax/value/if.rs` around lines 266 - 273, The
recovery for the parenthesised boolean group is using the wrong expectation
token—update the recovery to reference a boolean-expr group instead of the “not”
variant: in the call chain around parse_any_if_test_boolean_expr(...)
.or_recover_with_token_set(...), replace the expectation identifier
expected_if_test_boolean_not_expr with the appropriate boolean-expr expectation
(e.g. expected_if_test_boolean_expr or the project’s boolean-expr-group
expectation constant) and ensure the
ParseRecoveryTokenSet/CSS_BOGUS_IF_TEST_BOOLEAN_EXPR message reflects
"boolean-expr group" rather than "not" so the recovery matches parenthesised
boolean expressions.


p.expect(T![')']);
return Present(m.complete(p, CSS_IF_TEST_BOOLEAN_EXPR_IN_PARENS));
}
Expand Down Expand Up @@ -269,7 +300,14 @@ fn parse_if_test_boolean_not_expr(p: &mut CssParser) -> ParsedSyntax {
let m = p.start();

p.bump(T![not]);
parse_any_if_test_boolean_expr_group(p).ok();

parse_any_if_test_boolean_expr_group(p)
.or_recover_with_token_set(
p,
&ParseRecoveryTokenSet::new(CSS_BOGUS_IF_TEST_BOOLEAN_EXPR, token_set![T![')'], T![:]]),
expected_if_test_boolean_expr_group,
)
.ok();

Present(m.complete(p, CSS_IF_TEST_BOOLEAN_NOT_EXPR))
}
Expand Down Expand Up @@ -309,7 +347,7 @@ fn parse_if_test_boolean_and_expr(p: &mut CssParser, lhs: CompletedMarker) -> Co
// parse_any_if_test_boolean_expr_group failed to parse,
// but the parser is already at a recovered position.
let m = p.start();
let rhs = m.complete(p, CSS_BOGUS);
let rhs = m.complete(p, CSS_BOGUS_IF_TEST_BOOLEAN_EXPR);
parse_if_test_boolean_and_expr(p, rhs);
}

Expand Down Expand Up @@ -351,7 +389,7 @@ fn parse_if_test_boolean_or_expr(p: &mut CssParser, lhs: CompletedMarker) -> Com
// parse_any_if_test_boolean_expr_group failed to parse,
// but the parser is already at a recovered position.
let m = p.start();
let rhs = m.complete(p, CSS_BOGUS);
let rhs = m.complete(p, CSS_BOGUS_IF_TEST_BOOLEAN_EXPR);
parse_if_test_boolean_or_expr(p, rhs);
}

Expand All @@ -371,6 +409,11 @@ fn parse_any_if_test_boolean_expr(p: &mut CssParser) -> ParsedSyntax {
})
}

#[inline]
fn is_at_any_if_condition(p: &mut CssParser) -> bool {
p.at(T![else]) || is_at_if_test_boolean_expr_group(p) || is_at_if_test_boolean_not_expr(p)
}

#[inline]
fn parse_any_if_condition(p: &mut CssParser) -> ParsedSyntax {
if p.at(T![else]) {
Expand All @@ -384,10 +427,18 @@ fn parse_any_if_condition(p: &mut CssParser) -> ParsedSyntax {

#[inline]
fn parse_if_branch(p: &mut CssParser) -> ParsedSyntax {
if !is_at_any_if_condition(p) {
return Absent;
}

let m = p.start();

parse_any_if_condition(p)
.or_recover(p, &AnyIfTestParseRecovery, expected_if_branch)
.or_recover_with_token_set(
p,
&ParseRecoveryTokenSet::new(CSS_BOGUS_IF_BRANCH, token_set![T![')'], T![:]]),
expected_if_branch,
)
.ok();

p.expect(T![:]);
Expand All @@ -402,28 +453,17 @@ struct AnyIfTestBooleanExprChainParseRecovery;
impl ParseRecovery for AnyIfTestBooleanExprChainParseRecovery {
type Kind = CssSyntaxKind;
type Parser<'source> = CssParser<'source>;
const RECOVERED_KIND: Self::Kind = CSS_BOGUS;
const RECOVERED_KIND: Self::Kind = CSS_BOGUS_IF_TEST_BOOLEAN_EXPR;

fn is_at_recovered(&self, p: &mut Self::Parser<'_>) -> bool {
is_at_if_test_boolean_not_expr(p)
p.at(T![')'])
|| is_at_if_test_boolean_not_expr(p)
|| is_at_if_test_boolean_and_expr(p)
|| is_at_if_test_boolean_or_expr(p)
|| p.has_preceding_line_break()
}
}

struct AnyIfTestParseRecovery;

impl ParseRecovery for AnyIfTestParseRecovery {
type Kind = CssSyntaxKind;
type Parser<'source> = CssParser<'source>;
const RECOVERED_KIND: Self::Kind = CSS_BOGUS_IF_TEST;

fn is_at_recovered(&self, p: &mut Self::Parser<'_>) -> bool {
p.at(T![')']) || p.has_preceding_line_break()
}
}

struct IfBranchListParseRecovery;

impl ParseRecovery for IfBranchListParseRecovery {
Expand All @@ -433,7 +473,7 @@ impl ParseRecovery for IfBranchListParseRecovery {
const RECOVERED_KIND: Self::Kind = CSS_BOGUS_IF_BRANCH;

fn is_at_recovered(&self, p: &mut Self::Parser<'_>) -> bool {
p.at(T![;]) || p.at(T![')']) || p.has_preceding_line_break()
p.at_ts(IF_BRANCH_RECOVERY_TOKEN_SET)
}
}

Expand Down
7 changes: 7 additions & 0 deletions crates/biome_css_parser/src/syntax/value/parse_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,10 @@ pub(crate) fn expected_if_test_boolean_expr_group(
) -> ParseDiagnostic {
expected_any(&["parenthesized boolean expression", "if test"], range, p)
}

pub(crate) fn expected_if_test_boolean_not_expr(
p: &CssParser,
range: TextRange,
) -> ParseDiagnostic {
expected_any(&["not boolean expression", "if test"], range, p)
}
Loading
Loading