diff --git a/.changeset/soft-rules-feel.md b/.changeset/soft-rules-feel.md new file mode 100644 index 000000000000..d7aa1bf24315 --- /dev/null +++ b/.changeset/soft-rules-feel.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed #7848: The css parser with `tailwindDirectives` enabled will now correctly parse tailwind's source exclude syntax: `@source not "foo.css";` diff --git a/crates/biome_css_factory/src/generated/node_factory.rs b/crates/biome_css_factory/src/generated/node_factory.rs index 1f94f4f76a97..9cb1e659d6ba 100644 --- a/crates/biome_css_factory/src/generated/node_factory.rs +++ b/crates/biome_css_factory/src/generated/node_factory.rs @@ -2484,15 +2484,36 @@ pub fn tw_source_at_rule( source_token: SyntaxToken, path: CssString, semicolon_token: SyntaxToken, -) -> TwSourceAtRule { - TwSourceAtRule::unwrap_cast(SyntaxNode::new_detached( - CssSyntaxKind::TW_SOURCE_AT_RULE, - [ - Some(SyntaxElement::Token(source_token)), - Some(SyntaxElement::Node(path.into_syntax())), - Some(SyntaxElement::Token(semicolon_token)), - ], - )) +) -> TwSourceAtRuleBuilder { + TwSourceAtRuleBuilder { + source_token, + path, + semicolon_token, + not_token: None, + } +} +pub struct TwSourceAtRuleBuilder { + source_token: SyntaxToken, + path: CssString, + semicolon_token: SyntaxToken, + not_token: Option, +} +impl TwSourceAtRuleBuilder { + pub fn with_not_token(mut self, not_token: SyntaxToken) -> Self { + self.not_token = Some(not_token); + self + } + pub fn build(self) -> TwSourceAtRule { + TwSourceAtRule::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::TW_SOURCE_AT_RULE, + [ + Some(SyntaxElement::Token(self.source_token)), + self.not_token.map(|token| SyntaxElement::Token(token)), + Some(SyntaxElement::Node(self.path.into_syntax())), + Some(SyntaxElement::Token(self.semicolon_token)), + ], + )) + } } pub fn tw_theme_at_rule( theme_token: SyntaxToken, diff --git a/crates/biome_css_factory/src/generated/syntax_factory.rs b/crates/biome_css_factory/src/generated/syntax_factory.rs index b5d7e2273aff..0ef45f25ce9d 100644 --- a/crates/biome_css_factory/src/generated/syntax_factory.rs +++ b/crates/biome_css_factory/src/generated/syntax_factory.rs @@ -5114,7 +5114,7 @@ impl SyntaxFactory for CssSyntaxFactory { } TW_SOURCE_AT_RULE => { let mut elements = (&children).into_iter(); - let mut slots: RawNodeSlots<3usize> = RawNodeSlots::default(); + let mut slots: RawNodeSlots<4usize> = RawNodeSlots::default(); let mut current_element = elements.next(); if let Some(element) = ¤t_element && element.kind() == T![source] @@ -5123,6 +5123,13 @@ impl SyntaxFactory for CssSyntaxFactory { current_element = elements.next(); } slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T![not] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); if let Some(element) = ¤t_element && CssString::can_cast(element.kind()) { diff --git a/crates/biome_css_formatter/src/tailwind/statements/source_at_rule.rs b/crates/biome_css_formatter/src/tailwind/statements/source_at_rule.rs index fe7b3180d3c8..027fb07413bc 100644 --- a/crates/biome_css_formatter/src/tailwind/statements/source_at_rule.rs +++ b/crates/biome_css_formatter/src/tailwind/statements/source_at_rule.rs @@ -7,18 +7,15 @@ impl FormatNodeRule for FormatTwSourceAtRule { fn fmt_fields(&self, node: &TwSourceAtRule, f: &mut CssFormatter) -> FormatResult<()> { let TwSourceAtRuleFields { source_token, + not_token, path, semicolon_token, } = node.as_fields(); - write!( - f, - [ - source_token.format(), - space(), - path.format(), - semicolon_token.format() - ] - ) + write!(f, [source_token.format(), space()])?; + if let Some(not_token) = not_token { + write!(f, [not_token.format(), space()])?; + } + write!(f, [path.format(), semicolon_token.format()]) } } diff --git a/crates/biome_css_formatter/tests/specs/css/atrule/tailwind/options.json b/crates/biome_css_formatter/tests/specs/css/atrule/tailwind/options.json new file mode 100644 index 000000000000..dd3e21075965 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/atrule/tailwind/options.json @@ -0,0 +1,7 @@ +{ + "css": { + "parser": { + "tailwindDirectives": true + } + } +} diff --git a/crates/biome_css_formatter/tests/specs/css/atrule/tailwind/source-not.css b/crates/biome_css_formatter/tests/specs/css/atrule/tailwind/source-not.css new file mode 100644 index 000000000000..ee4fd00332a2 --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/atrule/tailwind/source-not.css @@ -0,0 +1 @@ +@source not "../routes/utils/ds.tsx"; diff --git a/crates/biome_css_formatter/tests/specs/css/atrule/tailwind/source-not.css.snap b/crates/biome_css_formatter/tests/specs/css/atrule/tailwind/source-not.css.snap new file mode 100644 index 000000000000..890ca8eb8b7c --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/atrule/tailwind/source-not.css.snap @@ -0,0 +1,43 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: css/atrule/tailwind/source-not.css +--- +# Input + +```css +@source not "../routes/utils/ds.tsx"; + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Quote style: Double Quotes +----- + +```css +@source not "../routes/utils/ds.tsx"; +``` + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Quote style: Double Quotes +----- + +```css +@source not "../routes/utils/ds.tsx"; +``` diff --git a/crates/biome_css_parser/src/syntax/at_rule/tailwind.rs b/crates/biome_css_parser/src/syntax/at_rule/tailwind.rs index 64999e51cd80..df48c047d2f2 100644 --- a/crates/biome_css_parser/src/syntax/at_rule/tailwind.rs +++ b/crates/biome_css_parser/src/syntax/at_rule/tailwind.rs @@ -215,6 +215,9 @@ pub(crate) fn parse_source_at_rule(p: &mut CssParser) -> ParsedSyntax { let m = p.start(); p.bump(T![source]); + if p.at(T![not]) { + p.bump(T![not]); + } parse_string(p).or_add_diagnostic(p, expected_string); p.expect(T![;]); diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/simple.css.snap b/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/simple.css.snap index 598b357a61a1..0c7e323245fd 100644 --- a/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/simple.css.snap +++ b/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/simple.css.snap @@ -224,6 +224,7 @@ CssRoot { at_token: AT@281..284 "@" [Newline("\n"), Newline("\n")] [], rule: TwSourceAtRule { source_token: SOURCE_KW@284..291 "source" [] [Whitespace(" ")], + not_token: missing (optional), path: CssString { value_token: CSS_STRING_LITERAL@291..303 "\"./base.css\"" [] [], }, @@ -480,9 +481,10 @@ CssRoot { 0: AT@281..284 "@" [Newline("\n"), Newline("\n")] [] 1: TW_SOURCE_AT_RULE@284..304 0: SOURCE_KW@284..291 "source" [] [Whitespace(" ")] - 1: CSS_STRING@291..303 + 1: (empty) + 2: CSS_STRING@291..303 0: CSS_STRING_LITERAL@291..303 "\"./base.css\"" [] [] - 2: SEMICOLON@303..304 ";" [] [] + 3: SEMICOLON@303..304 ";" [] [] 5: CSS_QUALIFIED_RULE@304..380 0: CSS_SELECTOR_LIST@304..315 0: CSS_COMPOUND_SELECTOR@304..315 diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/source-not.css b/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/source-not.css new file mode 100644 index 000000000000..d2a831d700e3 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/source-not.css @@ -0,0 +1 @@ +@source not "../routes/utils/ds.tsx"; diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/source-not.css.snap b/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/source-not.css.snap new file mode 100644 index 000000000000..476384cdf8cb --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/source-not.css.snap @@ -0,0 +1,51 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```css +@source not "../routes/utils/ds.tsx"; + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + rules: CssRuleList [ + CssAtRule { + at_token: AT@0..1 "@" [] [], + rule: TwSourceAtRule { + source_token: SOURCE_KW@1..8 "source" [] [Whitespace(" ")], + not_token: NOT_KW@8..12 "not" [] [Whitespace(" ")], + path: CssString { + value_token: CSS_STRING_LITERAL@12..36 "\"../routes/utils/ds.tsx\"" [] [], + }, + semicolon_token: SEMICOLON@36..37 ";" [] [], + }, + }, + ], + eof_token: EOF@37..38 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..38 + 0: (empty) + 1: CSS_RULE_LIST@0..37 + 0: CSS_AT_RULE@0..37 + 0: AT@0..1 "@" [] [] + 1: TW_SOURCE_AT_RULE@1..37 + 0: SOURCE_KW@1..8 "source" [] [Whitespace(" ")] + 1: NOT_KW@8..12 "not" [] [Whitespace(" ")] + 2: CSS_STRING@12..36 + 0: CSS_STRING_LITERAL@12..36 "\"../routes/utils/ds.tsx\"" [] [] + 3: SEMICOLON@36..37 ";" [] [] + 2: EOF@37..38 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/source.css.snap b/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/source.css.snap index 5726984577ec..e4392cb9f4b4 100644 --- a/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/source.css.snap +++ b/crates/biome_css_parser/tests/css_test_suite/ok/tailwind/source.css.snap @@ -20,6 +20,7 @@ CssRoot { at_token: AT@0..1 "@" [] [], rule: TwSourceAtRule { source_token: SOURCE_KW@1..8 "source" [] [Whitespace(" ")], + not_token: missing (optional), path: CssString { value_token: CSS_STRING_LITERAL@8..44 "\"../node_modules/@my-company/ui-lib\"" [] [], }, @@ -41,9 +42,10 @@ CssRoot { 0: AT@0..1 "@" [] [] 1: TW_SOURCE_AT_RULE@1..45 0: SOURCE_KW@1..8 "source" [] [Whitespace(" ")] - 1: CSS_STRING@8..44 + 1: (empty) + 2: CSS_STRING@8..44 0: CSS_STRING_LITERAL@8..44 "\"../node_modules/@my-company/ui-lib\"" [] [] - 2: SEMICOLON@44..45 ";" [] [] + 3: SEMICOLON@44..45 ";" [] [] 2: EOF@45..46 "" [Newline("\n")] [] ``` diff --git a/crates/biome_css_syntax/src/generated/nodes.rs b/crates/biome_css_syntax/src/generated/nodes.rs index e385627f85ac..5a08fb050d05 100644 --- a/crates/biome_css_syntax/src/generated/nodes.rs +++ b/crates/biome_css_syntax/src/generated/nodes.rs @@ -7219,6 +7219,7 @@ impl TwSourceAtRule { pub fn as_fields(&self) -> TwSourceAtRuleFields { TwSourceAtRuleFields { source_token: self.source_token(), + not_token: self.not_token(), path: self.path(), semicolon_token: self.semicolon_token(), } @@ -7226,11 +7227,14 @@ impl TwSourceAtRule { pub fn source_token(&self) -> SyntaxResult { support::required_token(&self.syntax, 0usize) } + pub fn not_token(&self) -> Option { + support::token(&self.syntax, 1usize) + } pub fn path(&self) -> SyntaxResult { - support::required_node(&self.syntax, 1usize) + support::required_node(&self.syntax, 2usize) } pub fn semicolon_token(&self) -> SyntaxResult { - support::required_token(&self.syntax, 2usize) + support::required_token(&self.syntax, 3usize) } } impl Serialize for TwSourceAtRule { @@ -7244,6 +7248,7 @@ impl Serialize for TwSourceAtRule { #[derive(Serialize)] pub struct TwSourceAtRuleFields { pub source_token: SyntaxResult, + pub not_token: Option, pub path: SyntaxResult, pub semicolon_token: SyntaxResult, } @@ -18461,6 +18466,10 @@ impl std::fmt::Debug for TwSourceAtRule { "source_token", &support::DebugSyntaxResult(self.source_token()), ) + .field( + "not_token", + &support::DebugOptionalElement(self.not_token()), + ) .field("path", &support::DebugSyntaxResult(self.path())) .field( "semicolon_token", diff --git a/crates/biome_css_syntax/src/generated/nodes_mut.rs b/crates/biome_css_syntax/src/generated/nodes_mut.rs index 3d07d71d7df2..0cd0b8282af0 100644 --- a/crates/biome_css_syntax/src/generated/nodes_mut.rs +++ b/crates/biome_css_syntax/src/generated/nodes_mut.rs @@ -2932,16 +2932,22 @@ impl TwSourceAtRule { .splice_slots(0usize..=0usize, once(Some(element.into()))), ) } + pub fn with_not_token(self, element: Option) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(1usize..=1usize, once(element.map(|element| element.into()))), + ) + } pub fn with_path(self, element: CssString) -> Self { Self::unwrap_cast( self.syntax - .splice_slots(1usize..=1usize, once(Some(element.into_syntax().into()))), + .splice_slots(2usize..=2usize, once(Some(element.into_syntax().into()))), ) } pub fn with_semicolon_token(self, element: SyntaxToken) -> Self { Self::unwrap_cast( self.syntax - .splice_slots(2usize..=2usize, once(Some(element.into()))), + .splice_slots(3usize..=3usize, once(Some(element.into()))), ) } } diff --git a/xtask/codegen/css.ungram b/xtask/codegen/css.ungram index 7509c76a82a2..5d3012554f84 100644 --- a/xtask/codegen/css.ungram +++ b/xtask/codegen/css.ungram @@ -1941,6 +1941,7 @@ TwApplyClassList = CssIdentifier* // @source "../node_modules/@my-company/ui-lib"; TwSourceAtRule = 'source' + 'not'? path: CssString ';'