diff --git a/.changeset/deep-areas-feel.md b/.changeset/deep-areas-feel.md new file mode 100644 index 000000000000..0c1e11df41c2 --- /dev/null +++ b/.changeset/deep-areas-feel.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed [#7920](https://github.com/biomejs/biome/issues/7920): The CSS parser, with Tailwind directives enabled, will no longer error when you use things like `prefix(tw)` in `@import` at rules. diff --git a/crates/biome_css_formatter/src/css/statements/import_at_rule.rs b/crates/biome_css_formatter/src/css/statements/import_at_rule.rs index 0fc1f46a8309..06365a4b61c4 100644 --- a/crates/biome_css_formatter/src/css/statements/import_at_rule.rs +++ b/crates/biome_css_formatter/src/css/statements/import_at_rule.rs @@ -18,7 +18,7 @@ impl FormatNodeRule for FormatCssImportAtRule { write!(f, [import_token.format(), space()])?; // Determine if there are any modifiers present. - let has_any_modifiers = layer.is_some() || supports.is_some() || media.len() > 0; + let has_any_modifiers = layer.is_some() || supports.is_some() || !media.is_empty(); if has_any_modifiers { // If there are, we need to group them together and try to fill them. diff --git a/crates/biome_css_parser/src/syntax/at_rule/import.rs b/crates/biome_css_parser/src/syntax/at_rule/import.rs index 4e081d6046ee..9a9602b39ee7 100644 --- a/crates/biome_css_parser/src/syntax/at_rule/import.rs +++ b/crates/biome_css_parser/src/syntax/at_rule/import.rs @@ -3,6 +3,7 @@ use crate::syntax::at_rule::layer::LayerNameList; use crate::syntax::at_rule::media::MediaQueryList; use crate::syntax::at_rule::supports::error::expected_any_supports_condition; use crate::syntax::at_rule::supports::parse_any_supports_condition; +use crate::syntax::util::skip_possible_tailwind_syntax; use crate::syntax::value::url::{is_at_url_function, parse_url_function}; use crate::syntax::{is_at_declaration, is_at_string, parse_declaration, parse_string}; use biome_css_syntax::CssSyntaxKind::*; @@ -44,6 +45,8 @@ pub(crate) fn parse_import_at_rule(p: &mut CssParser) -> ParsedSyntax { CSS_BOGUS_AT_RULE }; + skip_possible_tailwind_syntax(p); + // An optional cascade layer name, or for an anonymous layer. if is_at_import_named_layer(p) { parse_import_named_layer(p).ok(); @@ -51,12 +54,16 @@ pub(crate) fn parse_import_at_rule(p: &mut CssParser) -> ParsedSyntax { parse_import_anonymous_layer(p).ok(); } + skip_possible_tailwind_syntax(p); + if is_at_import_supports(p) { // An optional supports condition, we don't have an error here // is_at_import_supports validates the supports condition parse_import_supports(p).ok(); } + skip_possible_tailwind_syntax(p); + MediaQueryList::new(T![;]).parse_list(p); p.expect(T![;]); diff --git a/crates/biome_css_parser/src/syntax/at_rule/media.rs b/crates/biome_css_parser/src/syntax/at_rule/media.rs index 6f10e8c88b93..b77a5757f41a 100644 --- a/crates/biome_css_parser/src/syntax/at_rule/media.rs +++ b/crates/biome_css_parser/src/syntax/at_rule/media.rs @@ -2,6 +2,7 @@ use super::parse_error::expected_media_query; use crate::parser::CssParser; use crate::syntax::at_rule::feature::parse_any_query_feature; use crate::syntax::block::parse_conditional_block; +use crate::syntax::util::skip_possible_tailwind_syntax; use crate::syntax::{ is_at_identifier, is_at_metavariable, is_nth_at_identifier, parse_metavariable, parse_regular_identifier, @@ -66,7 +67,11 @@ impl ParseSeparatedList for MediaQueryList { const LIST_KIND: Self::Kind = CSS_MEDIA_QUERY_LIST; fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { - parse_any_media_query(p) + let parsed = parse_any_media_query(p); + if parsed.is_present() { + skip_possible_tailwind_syntax(p); + } + parsed } fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { diff --git a/crates/biome_css_parser/src/syntax/mod.rs b/crates/biome_css_parser/src/syntax/mod.rs index 5160f86c558d..d5d1d72eb674 100644 --- a/crates/biome_css_parser/src/syntax/mod.rs +++ b/crates/biome_css_parser/src/syntax/mod.rs @@ -4,6 +4,7 @@ mod css_modules; mod parse_error; mod property; mod selector; +mod util; mod value; use crate::lexer::CssLexContext; diff --git a/crates/biome_css_parser/src/syntax/util.rs b/crates/biome_css_parser/src/syntax/util.rs new file mode 100644 index 000000000000..c02b99636471 --- /dev/null +++ b/crates/biome_css_parser/src/syntax/util.rs @@ -0,0 +1,32 @@ +use biome_css_syntax::CssSyntaxKind::*; +use biome_css_syntax::T; +use biome_parser::Parser; +use biome_parser::SyntaxFeature; +use biome_parser::token_set; + +use crate::parser::CssParser; +use crate::syntax::CssSyntaxFeatures; + +/// Skips possible Tailwind CSS specific syntax in the `@import` rule that we don't know how to handle yet. +/// +/// See: https://github.com/biomejs/biome/issues/7920 +pub(crate) fn skip_possible_tailwind_syntax(p: &mut CssParser) { + if CssSyntaxFeatures::Tailwind.is_supported(p) + && p.at_ts(token_set![IDENT, T![source], T![theme], T![important]]) + { + if p.cur_text() == "prefix" || p.cur_text() == "source" || p.cur_text() == "theme" { + p.parse_as_skipped_trivia_tokens(skip_tailwind_function_clause) + } else if p.cur_text() == "important" { + p.parse_as_skipped_trivia_tokens(|p| p.bump_any()); + } + } +} + +fn skip_tailwind_function_clause(p: &mut CssParser) { + while !(p.at(EOF) || p.at(T![')'])) { + p.bump_any(); + } + if p.at(T![')']) { + p.bump_any(); // consume ')' + } +} diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/at_rule/at_rule_import.css.snap b/crates/biome_css_parser/tests/css_test_suite/ok/at_rule/at_rule_import.css.snap index 5eddb09320a4..ea91c375de5b 100644 --- a/crates/biome_css_parser/tests/css_test_suite/ok/at_rule/at_rule_import.css.snap +++ b/crates/biome_css_parser/tests/css_test_suite/ok/at_rule/at_rule_import.css.snap @@ -2,7 +2,6 @@ source: crates/biome_css_parser/tests/spec_test.rs expression: snapshot --- - ## Input ```css @@ -6804,5 +6803,3 @@ CssRoot { 2: EOF@6505..6506 "" [Newline("\n")] [] ``` - - diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/tw-import.css b/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/tw-import.css new file mode 100644 index 000000000000..aa0d3d37d23c --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/tw-import.css @@ -0,0 +1,6 @@ +@import "tailwindcss/theme.css" layer(theme) supports(display: flex) screen prefix(tw); +@import "tailwindcss/theme.css" layer(theme) prefix(tw); +@import "tailwindcss/utilities.css" prefix(tw) layer(utilities); +@import "tailwindcss" source("../src"); +@import "tailwindcss/utilities.css" layer(utilities) important; +@import "tailwindcss/theme.css" layer(theme) theme(static); diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/tw-import.css.snap b/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/tw-import.css.snap new file mode 100644 index 000000000000..0e5d3955a779 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/tw-import.css.snap @@ -0,0 +1,294 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```css +@import "tailwindcss/theme.css" layer(theme) supports(display: flex) screen prefix(tw); +@import "tailwindcss/theme.css" layer(theme) prefix(tw); +@import "tailwindcss/utilities.css" prefix(tw) layer(utilities); +@import "tailwindcss" source("../src"); +@import "tailwindcss/utilities.css" layer(utilities) important; +@import "tailwindcss/theme.css" layer(theme) theme(static); + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + rules: CssRuleList [ + CssAtRule { + at_token: AT@0..1 "@" [] [], + rule: CssImportAtRule { + import_token: IMPORT_KW@1..8 "import" [] [Whitespace(" ")], + url: CssString { + value_token: CSS_STRING_LITERAL@8..32 "\"tailwindcss/theme.css\"" [] [Whitespace(" ")], + }, + layer: CssImportNamedLayer { + layer_token: LAYER_KW@32..37 "layer" [] [], + l_paren_token: L_PAREN@37..38 "(" [] [], + name: CssLayerNameList [ + CssIdentifier { + value_token: IDENT@38..43 "theme" [] [], + }, + ], + r_paren_token: R_PAREN@43..45 ")" [] [Whitespace(" ")], + }, + supports: CssImportSupports { + supports_token: SUPPORTS_KW@45..53 "supports" [] [], + l_paren_token: L_PAREN@53..54 "(" [] [], + condition: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@54..61 "display" [] [], + }, + colon_token: COLON@61..63 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@63..67 "flex" [] [], + }, + ], + }, + important: missing (optional), + }, + r_paren_token: R_PAREN@67..69 ")" [] [Whitespace(" ")], + }, + media: CssMediaQueryList [ + CssMediaTypeQuery { + modifier: missing (optional), + ty: CssMediaType { + value: CssIdentifier { + value_token: IDENT@69..76 "screen" [] [Whitespace(" ")], + }, + }, + }, + ], + semicolon_token: SEMICOLON@76..87 ";" [Skipped("prefix"), Skipped("("), Skipped("tw"), Skipped(")")] [], + }, + }, + CssAtRule { + at_token: AT@87..89 "@" [Newline("\n")] [], + rule: CssImportAtRule { + import_token: IMPORT_KW@89..96 "import" [] [Whitespace(" ")], + url: CssString { + value_token: CSS_STRING_LITERAL@96..120 "\"tailwindcss/theme.css\"" [] [Whitespace(" ")], + }, + layer: CssImportNamedLayer { + layer_token: LAYER_KW@120..125 "layer" [] [], + l_paren_token: L_PAREN@125..126 "(" [] [], + name: CssLayerNameList [ + CssIdentifier { + value_token: IDENT@126..131 "theme" [] [], + }, + ], + r_paren_token: R_PAREN@131..133 ")" [] [Whitespace(" ")], + }, + supports: missing (optional), + media: CssMediaQueryList [], + semicolon_token: SEMICOLON@133..144 ";" [Skipped("prefix"), Skipped("("), Skipped("tw"), Skipped(")")] [], + }, + }, + CssAtRule { + at_token: AT@144..146 "@" [Newline("\n")] [], + rule: CssImportAtRule { + import_token: IMPORT_KW@146..153 "import" [] [Whitespace(" ")], + url: CssString { + value_token: CSS_STRING_LITERAL@153..181 "\"tailwindcss/utilities.css\"" [] [Whitespace(" ")], + }, + layer: CssImportNamedLayer { + layer_token: LAYER_KW@181..197 "layer" [Skipped("prefix"), Skipped("("), Skipped("tw"), Skipped(")"), Whitespace(" ")] [], + l_paren_token: L_PAREN@197..198 "(" [] [], + name: CssLayerNameList [ + CssIdentifier { + value_token: IDENT@198..207 "utilities" [] [], + }, + ], + r_paren_token: R_PAREN@207..208 ")" [] [], + }, + supports: missing (optional), + media: CssMediaQueryList [], + semicolon_token: SEMICOLON@208..209 ";" [] [], + }, + }, + CssAtRule { + at_token: AT@209..211 "@" [Newline("\n")] [], + rule: CssImportAtRule { + import_token: IMPORT_KW@211..218 "import" [] [Whitespace(" ")], + url: CssString { + value_token: CSS_STRING_LITERAL@218..232 "\"tailwindcss\"" [] [Whitespace(" ")], + }, + layer: missing (optional), + supports: missing (optional), + media: CssMediaQueryList [], + semicolon_token: SEMICOLON@232..249 ";" [Skipped("source"), Skipped("("), Skipped("\"../src\""), Skipped(")")] [], + }, + }, + CssAtRule { + at_token: AT@249..251 "@" [Newline("\n")] [], + rule: CssImportAtRule { + import_token: IMPORT_KW@251..258 "import" [] [Whitespace(" ")], + url: CssString { + value_token: CSS_STRING_LITERAL@258..286 "\"tailwindcss/utilities.css\"" [] [Whitespace(" ")], + }, + layer: CssImportNamedLayer { + layer_token: LAYER_KW@286..291 "layer" [] [], + l_paren_token: L_PAREN@291..292 "(" [] [], + name: CssLayerNameList [ + CssIdentifier { + value_token: IDENT@292..301 "utilities" [] [], + }, + ], + r_paren_token: R_PAREN@301..303 ")" [] [Whitespace(" ")], + }, + supports: missing (optional), + media: CssMediaQueryList [], + semicolon_token: SEMICOLON@303..313 ";" [Skipped("important")] [], + }, + }, + CssAtRule { + at_token: AT@313..315 "@" [Newline("\n")] [], + rule: CssImportAtRule { + import_token: IMPORT_KW@315..322 "import" [] [Whitespace(" ")], + url: CssString { + value_token: CSS_STRING_LITERAL@322..346 "\"tailwindcss/theme.css\"" [] [Whitespace(" ")], + }, + layer: CssImportNamedLayer { + layer_token: LAYER_KW@346..351 "layer" [] [], + l_paren_token: L_PAREN@351..352 "(" [] [], + name: CssLayerNameList [ + CssIdentifier { + value_token: IDENT@352..357 "theme" [] [], + }, + ], + r_paren_token: R_PAREN@357..359 ")" [] [Whitespace(" ")], + }, + supports: missing (optional), + media: CssMediaQueryList [], + semicolon_token: SEMICOLON@359..373 ";" [Skipped("theme"), Skipped("("), Skipped("static"), Skipped(")")] [], + }, + }, + ], + eof_token: EOF@373..374 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..374 + 0: (empty) + 1: CSS_RULE_LIST@0..373 + 0: CSS_AT_RULE@0..87 + 0: AT@0..1 "@" [] [] + 1: CSS_IMPORT_AT_RULE@1..87 + 0: IMPORT_KW@1..8 "import" [] [Whitespace(" ")] + 1: CSS_STRING@8..32 + 0: CSS_STRING_LITERAL@8..32 "\"tailwindcss/theme.css\"" [] [Whitespace(" ")] + 2: CSS_IMPORT_NAMED_LAYER@32..45 + 0: LAYER_KW@32..37 "layer" [] [] + 1: L_PAREN@37..38 "(" [] [] + 2: CSS_LAYER_NAME_LIST@38..43 + 0: CSS_IDENTIFIER@38..43 + 0: IDENT@38..43 "theme" [] [] + 3: R_PAREN@43..45 ")" [] [Whitespace(" ")] + 3: CSS_IMPORT_SUPPORTS@45..69 + 0: SUPPORTS_KW@45..53 "supports" [] [] + 1: L_PAREN@53..54 "(" [] [] + 2: CSS_DECLARATION@54..67 + 0: CSS_GENERIC_PROPERTY@54..67 + 0: CSS_IDENTIFIER@54..61 + 0: IDENT@54..61 "display" [] [] + 1: COLON@61..63 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@63..67 + 0: CSS_IDENTIFIER@63..67 + 0: IDENT@63..67 "flex" [] [] + 1: (empty) + 3: R_PAREN@67..69 ")" [] [Whitespace(" ")] + 4: CSS_MEDIA_QUERY_LIST@69..76 + 0: CSS_MEDIA_TYPE_QUERY@69..76 + 0: (empty) + 1: CSS_MEDIA_TYPE@69..76 + 0: CSS_IDENTIFIER@69..76 + 0: IDENT@69..76 "screen" [] [Whitespace(" ")] + 5: SEMICOLON@76..87 ";" [Skipped("prefix"), Skipped("("), Skipped("tw"), Skipped(")")] [] + 1: CSS_AT_RULE@87..144 + 0: AT@87..89 "@" [Newline("\n")] [] + 1: CSS_IMPORT_AT_RULE@89..144 + 0: IMPORT_KW@89..96 "import" [] [Whitespace(" ")] + 1: CSS_STRING@96..120 + 0: CSS_STRING_LITERAL@96..120 "\"tailwindcss/theme.css\"" [] [Whitespace(" ")] + 2: CSS_IMPORT_NAMED_LAYER@120..133 + 0: LAYER_KW@120..125 "layer" [] [] + 1: L_PAREN@125..126 "(" [] [] + 2: CSS_LAYER_NAME_LIST@126..131 + 0: CSS_IDENTIFIER@126..131 + 0: IDENT@126..131 "theme" [] [] + 3: R_PAREN@131..133 ")" [] [Whitespace(" ")] + 3: (empty) + 4: CSS_MEDIA_QUERY_LIST@133..133 + 5: SEMICOLON@133..144 ";" [Skipped("prefix"), Skipped("("), Skipped("tw"), Skipped(")")] [] + 2: CSS_AT_RULE@144..209 + 0: AT@144..146 "@" [Newline("\n")] [] + 1: CSS_IMPORT_AT_RULE@146..209 + 0: IMPORT_KW@146..153 "import" [] [Whitespace(" ")] + 1: CSS_STRING@153..181 + 0: CSS_STRING_LITERAL@153..181 "\"tailwindcss/utilities.css\"" [] [Whitespace(" ")] + 2: CSS_IMPORT_NAMED_LAYER@181..208 + 0: LAYER_KW@181..197 "layer" [Skipped("prefix"), Skipped("("), Skipped("tw"), Skipped(")"), Whitespace(" ")] [] + 1: L_PAREN@197..198 "(" [] [] + 2: CSS_LAYER_NAME_LIST@198..207 + 0: CSS_IDENTIFIER@198..207 + 0: IDENT@198..207 "utilities" [] [] + 3: R_PAREN@207..208 ")" [] [] + 3: (empty) + 4: CSS_MEDIA_QUERY_LIST@208..208 + 5: SEMICOLON@208..209 ";" [] [] + 3: CSS_AT_RULE@209..249 + 0: AT@209..211 "@" [Newline("\n")] [] + 1: CSS_IMPORT_AT_RULE@211..249 + 0: IMPORT_KW@211..218 "import" [] [Whitespace(" ")] + 1: CSS_STRING@218..232 + 0: CSS_STRING_LITERAL@218..232 "\"tailwindcss\"" [] [Whitespace(" ")] + 2: (empty) + 3: (empty) + 4: CSS_MEDIA_QUERY_LIST@232..232 + 5: SEMICOLON@232..249 ";" [Skipped("source"), Skipped("("), Skipped("\"../src\""), Skipped(")")] [] + 4: CSS_AT_RULE@249..313 + 0: AT@249..251 "@" [Newline("\n")] [] + 1: CSS_IMPORT_AT_RULE@251..313 + 0: IMPORT_KW@251..258 "import" [] [Whitespace(" ")] + 1: CSS_STRING@258..286 + 0: CSS_STRING_LITERAL@258..286 "\"tailwindcss/utilities.css\"" [] [Whitespace(" ")] + 2: CSS_IMPORT_NAMED_LAYER@286..303 + 0: LAYER_KW@286..291 "layer" [] [] + 1: L_PAREN@291..292 "(" [] [] + 2: CSS_LAYER_NAME_LIST@292..301 + 0: CSS_IDENTIFIER@292..301 + 0: IDENT@292..301 "utilities" [] [] + 3: R_PAREN@301..303 ")" [] [Whitespace(" ")] + 3: (empty) + 4: CSS_MEDIA_QUERY_LIST@303..303 + 5: SEMICOLON@303..313 ";" [Skipped("important")] [] + 5: CSS_AT_RULE@313..373 + 0: AT@313..315 "@" [Newline("\n")] [] + 1: CSS_IMPORT_AT_RULE@315..373 + 0: IMPORT_KW@315..322 "import" [] [Whitespace(" ")] + 1: CSS_STRING@322..346 + 0: CSS_STRING_LITERAL@322..346 "\"tailwindcss/theme.css\"" [] [Whitespace(" ")] + 2: CSS_IMPORT_NAMED_LAYER@346..359 + 0: LAYER_KW@346..351 "layer" [] [] + 1: L_PAREN@351..352 "(" [] [] + 2: CSS_LAYER_NAME_LIST@352..357 + 0: CSS_IDENTIFIER@352..357 + 0: IDENT@352..357 "theme" [] [] + 3: R_PAREN@357..359 ")" [] [Whitespace(" ")] + 3: (empty) + 4: CSS_MEDIA_QUERY_LIST@359..359 + 5: SEMICOLON@359..373 ";" [Skipped("theme"), Skipped("("), Skipped("static"), Skipped(")")] [] + 2: EOF@373..374 "" [Newline("\n")] [] + +```