diff --git a/crates/biome_css_factory/src/generated/node_factory.rs b/crates/biome_css_factory/src/generated/node_factory.rs index 181c87ececd2..c9b4aea7bc3a 100644 --- a/crates/biome_css_factory/src/generated/node_factory.rs +++ b/crates/biome_css_factory/src/generated/node_factory.rs @@ -189,6 +189,60 @@ pub fn css_complex_selector( ], )) } +pub fn css_composes_import_specifier( + from_token: SyntaxToken, + source: AnyCssComposesImportSource, +) -> CssComposesImportSpecifier { + CssComposesImportSpecifier::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::CSS_COMPOSES_IMPORT_SPECIFIER, + [ + Some(SyntaxElement::Token(from_token)), + Some(SyntaxElement::Node(source.into_syntax())), + ], + )) +} +pub fn css_composes_property( + name: CssIdentifier, + colon_token: SyntaxToken, + value: CssComposesPropertyValue, +) -> CssComposesProperty { + CssComposesProperty::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::CSS_COMPOSES_PROPERTY, + [ + Some(SyntaxElement::Node(name.into_syntax())), + Some(SyntaxElement::Token(colon_token)), + Some(SyntaxElement::Node(value.into_syntax())), + ], + )) +} +pub fn css_composes_property_value( + classes: CssComposesClassList, +) -> CssComposesPropertyValueBuilder { + CssComposesPropertyValueBuilder { + classes, + specifier: None, + } +} +pub struct CssComposesPropertyValueBuilder { + classes: CssComposesClassList, + specifier: Option, +} +impl CssComposesPropertyValueBuilder { + pub fn with_specifier(mut self, specifier: CssComposesImportSpecifier) -> Self { + self.specifier = Some(specifier); + self + } + pub fn build(self) -> CssComposesPropertyValue { + CssComposesPropertyValue::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::CSS_COMPOSES_PROPERTY_VALUE, + [ + Some(SyntaxElement::Node(self.classes.into_syntax())), + self.specifier + .map(|token| SyntaxElement::Node(token.into_syntax())), + ], + )) + } +} pub fn css_compound_selector(sub_selectors: CssSubSelectorList) -> CssCompoundSelectorBuilder { CssCompoundSelectorBuilder { sub_selectors, @@ -2152,6 +2206,18 @@ where .map(|item| Some(item.into_syntax().into())), )) } +pub fn css_composes_class_list(items: I) -> CssComposesClassList +where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, +{ + CssComposesClassList::unwrap_cast(SyntaxNode::new_detached( + CssSyntaxKind::CSS_COMPOSES_CLASS_LIST, + items + .into_iter() + .map(|item| Some(item.into_syntax().into())), + )) +} pub fn css_compound_selector_list(items: I, separators: S) -> CssCompoundSelectorList where I: IntoIterator, diff --git a/crates/biome_css_factory/src/generated/syntax_factory.rs b/crates/biome_css_factory/src/generated/syntax_factory.rs index 6001dc5553ce..9b7c62fddfac 100644 --- a/crates/biome_css_factory/src/generated/syntax_factory.rs +++ b/crates/biome_css_factory/src/generated/syntax_factory.rs @@ -374,6 +374,91 @@ impl SyntaxFactory for CssSyntaxFactory { } slots.into_node(CSS_COMPLEX_SELECTOR, children) } + CSS_COMPOSES_IMPORT_SPECIFIER => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<2usize> = RawNodeSlots::default(); + let mut current_element = elements.next(); + if let Some(element) = ¤t_element { + if element.kind() == T![from] { + slots.mark_present(); + current_element = elements.next(); + } + } + slots.next_slot(); + if let Some(element) = ¤t_element { + if AnyCssComposesImportSource::can_cast(element.kind()) { + slots.mark_present(); + current_element = elements.next(); + } + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + CSS_COMPOSES_IMPORT_SPECIFIER.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(CSS_COMPOSES_IMPORT_SPECIFIER, children) + } + CSS_COMPOSES_PROPERTY => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<3usize> = RawNodeSlots::default(); + let mut current_element = elements.next(); + if let Some(element) = ¤t_element { + if CssIdentifier::can_cast(element.kind()) { + slots.mark_present(); + current_element = elements.next(); + } + } + slots.next_slot(); + if let Some(element) = ¤t_element { + if element.kind() == T ! [:] { + slots.mark_present(); + current_element = elements.next(); + } + } + slots.next_slot(); + if let Some(element) = ¤t_element { + if CssComposesPropertyValue::can_cast(element.kind()) { + slots.mark_present(); + current_element = elements.next(); + } + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + CSS_COMPOSES_PROPERTY.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(CSS_COMPOSES_PROPERTY, children) + } + CSS_COMPOSES_PROPERTY_VALUE => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<2usize> = RawNodeSlots::default(); + let mut current_element = elements.next(); + if let Some(element) = ¤t_element { + if CssComposesClassList::can_cast(element.kind()) { + slots.mark_present(); + current_element = elements.next(); + } + } + slots.next_slot(); + if let Some(element) = ¤t_element { + if CssComposesImportSpecifier::can_cast(element.kind()) { + slots.mark_present(); + current_element = elements.next(); + } + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + CSS_COMPOSES_PROPERTY_VALUE.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(CSS_COMPOSES_PROPERTY_VALUE, children) + } CSS_COMPOUND_SELECTOR => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<3usize> = RawNodeSlots::default(); @@ -4368,6 +4453,9 @@ impl SyntaxFactory for CssSyntaxFactory { CSS_COMPONENT_VALUE_LIST => { Self::make_node_list_syntax(kind, children, AnyCssValue::can_cast) } + CSS_COMPOSES_CLASS_LIST => { + Self::make_node_list_syntax(kind, children, CssCustomIdentifier::can_cast) + } CSS_COMPOUND_SELECTOR_LIST => Self::make_separated_list_syntax( kind, children, diff --git a/crates/biome_css_formatter/src/css/any/composes_import_source.rs b/crates/biome_css_formatter/src/css/any/composes_import_source.rs new file mode 100644 index 000000000000..2addcb653872 --- /dev/null +++ b/crates/biome_css_formatter/src/css/any/composes_import_source.rs @@ -0,0 +1,15 @@ +//! This is a generated file. Don't modify it by hand! Run 'cargo codegen formatter' to re-generate the file. + +use crate::prelude::*; +use biome_css_syntax::AnyCssComposesImportSource; +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatAnyCssComposesImportSource; +impl FormatRule for FormatAnyCssComposesImportSource { + type Context = CssFormatContext; + fn fmt(&self, node: &AnyCssComposesImportSource, f: &mut CssFormatter) -> FormatResult<()> { + match node { + AnyCssComposesImportSource::CssIdentifier(node) => node.format().fmt(f), + AnyCssComposesImportSource::CssString(node) => node.format().fmt(f), + } + } +} diff --git a/crates/biome_css_formatter/src/css/any/mod.rs b/crates/biome_css_formatter/src/css/any/mod.rs index d217ddc9bc90..708a91ab0eb3 100644 --- a/crates/biome_css_formatter/src/css/any/mod.rs +++ b/crates/biome_css_formatter/src/css/any/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod at_rule; pub(crate) mod attribute_matcher_value; +pub(crate) mod composes_import_source; pub(crate) mod compound_selector; pub(crate) mod container_and_combinable_query; pub(crate) mod container_or_combinable_query; diff --git a/crates/biome_css_formatter/src/css/any/property.rs b/crates/biome_css_formatter/src/css/any/property.rs index 4ea996af981d..e70d66530cc8 100644 --- a/crates/biome_css_formatter/src/css/any/property.rs +++ b/crates/biome_css_formatter/src/css/any/property.rs @@ -9,6 +9,7 @@ impl FormatRule for FormatAnyCssProperty { fn fmt(&self, node: &AnyCssProperty, f: &mut CssFormatter) -> FormatResult<()> { match node { AnyCssProperty::CssBogusProperty(node) => node.format().fmt(f), + AnyCssProperty::CssComposesProperty(node) => node.format().fmt(f), AnyCssProperty::CssGenericProperty(node) => node.format().fmt(f), } } diff --git a/crates/biome_css_formatter/src/css/auxiliary/composes_import_specifier.rs b/crates/biome_css_formatter/src/css/auxiliary/composes_import_specifier.rs new file mode 100644 index 000000000000..ed7e81015e84 --- /dev/null +++ b/crates/biome_css_formatter/src/css/auxiliary/composes_import_specifier.rs @@ -0,0 +1,17 @@ +use crate::prelude::*; +use biome_css_syntax::{CssComposesImportSpecifier, CssComposesImportSpecifierFields}; +use biome_formatter::write; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatCssComposesImportSpecifier; +impl FormatNodeRule for FormatCssComposesImportSpecifier { + fn fmt_fields( + &self, + node: &CssComposesImportSpecifier, + f: &mut CssFormatter, + ) -> FormatResult<()> { + let CssComposesImportSpecifierFields { from_token, source } = node.as_fields(); + + write![f, [space(), from_token.format(), space(), source.format()]] + } +} diff --git a/crates/biome_css_formatter/src/css/auxiliary/composes_property_value.rs b/crates/biome_css_formatter/src/css/auxiliary/composes_property_value.rs new file mode 100644 index 000000000000..ad177c0fba69 --- /dev/null +++ b/crates/biome_css_formatter/src/css/auxiliary/composes_property_value.rs @@ -0,0 +1,17 @@ +use crate::prelude::*; +use biome_css_syntax::{CssComposesPropertyValue, CssComposesPropertyValueFields}; +use biome_formatter::write; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatCssComposesPropertyValue; +impl FormatNodeRule for FormatCssComposesPropertyValue { + fn fmt_fields( + &self, + node: &CssComposesPropertyValue, + f: &mut CssFormatter, + ) -> FormatResult<()> { + let CssComposesPropertyValueFields { classes, specifier } = node.as_fields(); + + write![f, [classes.format(), specifier.format()]] + } +} diff --git a/crates/biome_css_formatter/src/css/auxiliary/mod.rs b/crates/biome_css_formatter/src/css/auxiliary/mod.rs index 6ea364809f25..c6eb232e7051 100644 --- a/crates/biome_css_formatter/src/css/auxiliary/mod.rs +++ b/crates/biome_css_formatter/src/css/auxiliary/mod.rs @@ -4,6 +4,8 @@ pub(crate) mod attribute_matcher; pub(crate) mod attribute_matcher_value; pub(crate) mod attribute_name; pub(crate) mod binary_expression; +pub(crate) mod composes_import_specifier; +pub(crate) mod composes_property_value; pub(crate) mod container_and_query; pub(crate) mod container_not_query; pub(crate) mod container_or_query; diff --git a/crates/biome_css_formatter/src/css/lists/composes_class_list.rs b/crates/biome_css_formatter/src/css/lists/composes_class_list.rs new file mode 100644 index 000000000000..2421a5c3a1f5 --- /dev/null +++ b/crates/biome_css_formatter/src/css/lists/composes_class_list.rs @@ -0,0 +1,12 @@ +use crate::prelude::*; +use biome_css_syntax::CssComposesClassList; +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatCssComposesClassList; +impl FormatRule for FormatCssComposesClassList { + type Context = CssFormatContext; + fn fmt(&self, node: &CssComposesClassList, f: &mut CssFormatter) -> FormatResult<()> { + f.join_with(&space()) + .entries(node.iter().formatted()) + .finish() + } +} diff --git a/crates/biome_css_formatter/src/css/lists/mod.rs b/crates/biome_css_formatter/src/css/lists/mod.rs index b16c1c0b4cc4..3d5910d08484 100644 --- a/crates/biome_css_formatter/src/css/lists/mod.rs +++ b/crates/biome_css_formatter/src/css/lists/mod.rs @@ -1,6 +1,7 @@ //! This is a generated file. Don't modify it by hand! Run 'cargo codegen formatter' to re-generate the file. pub(crate) mod component_value_list; +pub(crate) mod composes_class_list; pub(crate) mod compound_selector_list; pub(crate) mod custom_identifier_list; pub(crate) mod declaration_list; diff --git a/crates/biome_css_formatter/src/css/properties/composes_property.rs b/crates/biome_css_formatter/src/css/properties/composes_property.rs new file mode 100644 index 000000000000..33300599fcd2 --- /dev/null +++ b/crates/biome_css_formatter/src/css/properties/composes_property.rs @@ -0,0 +1,18 @@ +use crate::prelude::*; +use biome_css_syntax::{CssComposesProperty, CssComposesPropertyFields}; +use biome_formatter::write; +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatCssComposesProperty; +impl FormatNodeRule for FormatCssComposesProperty { + fn fmt_fields(&self, node: &CssComposesProperty, f: &mut CssFormatter) -> FormatResult<()> { + let CssComposesPropertyFields { + name, + colon_token, + value, + } = node.as_fields(); + write!( + f, + [name.format(), colon_token.format(), space(), value.format()] + ) + } +} diff --git a/crates/biome_css_formatter/src/css/properties/mod.rs b/crates/biome_css_formatter/src/css/properties/mod.rs index 391baed69ad3..1b7d25e2ebad 100644 --- a/crates/biome_css_formatter/src/css/properties/mod.rs +++ b/crates/biome_css_formatter/src/css/properties/mod.rs @@ -1,4 +1,5 @@ //! This is a generated file. Don't modify it by hand! Run 'cargo codegen formatter' to re-generate the file. +pub(crate) mod composes_property; pub(crate) mod generic_property; pub(crate) mod value_at_rule_generic_property; diff --git a/crates/biome_css_formatter/src/generated.rs b/crates/biome_css_formatter/src/generated.rs index 2d80f21b1a20..046602207dde 100644 --- a/crates/biome_css_formatter/src/generated.rs +++ b/crates/biome_css_formatter/src/generated.rs @@ -423,6 +423,122 @@ impl IntoFormat for biome_css_syntax::CssComplexSelector { ) } } +impl FormatRule + for crate::css::auxiliary::composes_import_specifier::FormatCssComposesImportSpecifier +{ + type Context = CssFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_css_syntax::CssComposesImportSpecifier, + f: &mut CssFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_css_syntax::CssComposesImportSpecifier { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::CssComposesImportSpecifier, + crate::css::auxiliary::composes_import_specifier::FormatCssComposesImportSpecifier, + >; + fn format(&self) -> Self::Format<'_> { + #![allow(clippy::default_constructed_unit_structs)] + FormatRefWithRule :: new (self , crate :: css :: auxiliary :: composes_import_specifier :: FormatCssComposesImportSpecifier :: default ()) + } +} +impl IntoFormat for biome_css_syntax::CssComposesImportSpecifier { + type Format = FormatOwnedWithRule< + biome_css_syntax::CssComposesImportSpecifier, + crate::css::auxiliary::composes_import_specifier::FormatCssComposesImportSpecifier, + >; + fn into_format(self) -> Self::Format { + #![allow(clippy::default_constructed_unit_structs)] + FormatOwnedWithRule :: new (self , crate :: css :: auxiliary :: composes_import_specifier :: FormatCssComposesImportSpecifier :: default ()) + } +} +impl FormatRule + for crate::css::properties::composes_property::FormatCssComposesProperty +{ + type Context = CssFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_css_syntax::CssComposesProperty, + f: &mut CssFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_css_syntax::CssComposesProperty { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::CssComposesProperty, + crate::css::properties::composes_property::FormatCssComposesProperty, + >; + fn format(&self) -> Self::Format<'_> { + #![allow(clippy::default_constructed_unit_structs)] + FormatRefWithRule::new( + self, + crate::css::properties::composes_property::FormatCssComposesProperty::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::CssComposesProperty { + type Format = FormatOwnedWithRule< + biome_css_syntax::CssComposesProperty, + crate::css::properties::composes_property::FormatCssComposesProperty, + >; + fn into_format(self) -> Self::Format { + #![allow(clippy::default_constructed_unit_structs)] + FormatOwnedWithRule::new( + self, + crate::css::properties::composes_property::FormatCssComposesProperty::default(), + ) + } +} +impl FormatRule + for crate::css::auxiliary::composes_property_value::FormatCssComposesPropertyValue +{ + type Context = CssFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_css_syntax::CssComposesPropertyValue, + f: &mut CssFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_css_syntax::CssComposesPropertyValue { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::CssComposesPropertyValue, + crate::css::auxiliary::composes_property_value::FormatCssComposesPropertyValue, + >; + fn format(&self) -> Self::Format<'_> { + #![allow(clippy::default_constructed_unit_structs)] + FormatRefWithRule::new( + self, + crate::css::auxiliary::composes_property_value::FormatCssComposesPropertyValue::default( + ), + ) + } +} +impl IntoFormat for biome_css_syntax::CssComposesPropertyValue { + type Format = FormatOwnedWithRule< + biome_css_syntax::CssComposesPropertyValue, + crate::css::auxiliary::composes_property_value::FormatCssComposesPropertyValue, + >; + fn into_format(self) -> Self::Format { + #![allow(clippy::default_constructed_unit_structs)] + FormatOwnedWithRule::new( + self, + crate::css::auxiliary::composes_property_value::FormatCssComposesPropertyValue::default( + ), + ) + } +} impl FormatRule for crate::css::selectors::compound_selector::FormatCssCompoundSelector { @@ -5181,6 +5297,33 @@ impl IntoFormat for biome_css_syntax::CssComponentValueList { ) } } +impl AsFormat for biome_css_syntax::CssComposesClassList { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::CssComposesClassList, + crate::css::lists::composes_class_list::FormatCssComposesClassList, + >; + fn format(&self) -> Self::Format<'_> { + #![allow(clippy::default_constructed_unit_structs)] + FormatRefWithRule::new( + self, + crate::css::lists::composes_class_list::FormatCssComposesClassList::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::CssComposesClassList { + type Format = FormatOwnedWithRule< + biome_css_syntax::CssComposesClassList, + crate::css::lists::composes_class_list::FormatCssComposesClassList, + >; + fn into_format(self) -> Self::Format { + #![allow(clippy::default_constructed_unit_structs)] + FormatOwnedWithRule::new( + self, + crate::css::lists::composes_class_list::FormatCssComposesClassList::default(), + ) + } +} impl AsFormat for biome_css_syntax::CssCompoundSelectorList { type Format<'a> = FormatRefWithRule< 'a, @@ -6823,6 +6966,33 @@ impl IntoFormat for biome_css_syntax::AnyCssAttributeMatcherVa ) } } +impl AsFormat for biome_css_syntax::AnyCssComposesImportSource { + type Format<'a> = FormatRefWithRule< + 'a, + biome_css_syntax::AnyCssComposesImportSource, + crate::css::any::composes_import_source::FormatAnyCssComposesImportSource, + >; + fn format(&self) -> Self::Format<'_> { + #![allow(clippy::default_constructed_unit_structs)] + FormatRefWithRule::new( + self, + crate::css::any::composes_import_source::FormatAnyCssComposesImportSource::default(), + ) + } +} +impl IntoFormat for biome_css_syntax::AnyCssComposesImportSource { + type Format = FormatOwnedWithRule< + biome_css_syntax::AnyCssComposesImportSource, + crate::css::any::composes_import_source::FormatAnyCssComposesImportSource, + >; + fn into_format(self) -> Self::Format { + #![allow(clippy::default_constructed_unit_structs)] + FormatOwnedWithRule::new( + self, + crate::css::any::composes_import_source::FormatAnyCssComposesImportSource::default(), + ) + } +} impl AsFormat for biome_css_syntax::AnyCssCompoundSelector { type Format<'a> = FormatRefWithRule< 'a, diff --git a/crates/biome_css_formatter/tests/specs/css/composes.css b/crates/biome_css_formatter/tests/specs/css/composes.css new file mode 100644 index 000000000000..8ca3af8113fb --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/composes.css @@ -0,0 +1,31 @@ +.a { + composes: + myClass; +} + +.otherClassName { + composes: + className + from + "./style.css"; +} + +.otherClassName { + composes: + globalClassName + from + global; +} + +.b { + composes: + classNameA + classNameB; +} + +.c { + composes: + classNameA + classNameB + from './namespace.css'; +} diff --git a/crates/biome_css_formatter/tests/specs/css/composes.css.snap b/crates/biome_css_formatter/tests/specs/css/composes.css.snap new file mode 100644 index 000000000000..42fa8de5db1b --- /dev/null +++ b/crates/biome_css_formatter/tests/specs/css/composes.css.snap @@ -0,0 +1,77 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: css/composes.css +--- +# Input + +```css +.a { + composes: + myClass; +} + +.otherClassName { + composes: + className + from + "./style.css"; +} + +.otherClassName { + composes: + globalClassName + from + global; +} + +.b { + composes: + classNameA + classNameB; +} + +.c { + composes: + classNameA + classNameB + from './namespace.css'; +} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Quote style: Double Quotes +----- + +```css +.a { + composes: myClass; +} + +.otherClassName { + composes: className from "./style.css"; +} + +.otherClassName { + composes: globalClassName from global; +} + +.b { + composes: classNameA classNameB; +} + +.c { + composes: classNameA classNameB from "./namespace.css"; +} +``` diff --git a/crates/biome_css_formatter/tests/specs/prettier/css/composes/composes.css.snap b/crates/biome_css_formatter/tests/specs/prettier/css/composes/composes.css.snap deleted file mode 100644 index 662ba32eb952..000000000000 --- a/crates/biome_css_formatter/tests/specs/prettier/css/composes/composes.css.snap +++ /dev/null @@ -1,40 +0,0 @@ ---- -source: crates/biome_formatter_test/src/snapshot_builder.rs -info: css/composes/composes.css ---- -# Input - -```css -.reference { - composes: selector from "a/long/file/path/exceeding/the/maximum/length/forcing/a/line-wrap/file.css"; -} - -``` - - -# Prettier differences - -```diff ---- Prettier -+++ Biome -@@ -1,3 +1,4 @@ - .reference { -- composes: selector from "a/long/file/path/exceeding/the/maximum/length/forcing/a/line-wrap/file.css"; -+ composes: selector from -+ "a/long/file/path/exceeding/the/maximum/length/forcing/a/line-wrap/file.css"; - } -``` - -# Output - -```css -.reference { - composes: selector from - "a/long/file/path/exceeding/the/maximum/length/forcing/a/line-wrap/file.css"; -} -``` - -# Lines exceeding max width of 80 characters -``` - 3: "a/long/file/path/exceeding/the/maximum/length/forcing/a/line-wrap/file.css"; -``` diff --git a/crates/biome_css_formatter/tests/specs/prettier/css/modules/modules.css.snap b/crates/biome_css_formatter/tests/specs/prettier/css/modules/modules.css.snap index 78c83979aead..20abb67682e1 100644 --- a/crates/biome_css_formatter/tests/specs/prettier/css/modules/modules.css.snap +++ b/crates/biome_css_formatter/tests/specs/prettier/css/modules/modules.css.snap @@ -83,26 +83,6 @@ info: css/modules/modules.css .className { color: green; -@@ -13,16 +14,16 @@ - } - - .otherClassName { -- composes: className; -+ composes: classname; - color: yellow; - } - - .otherClassName { -- composes: className from "./style.css"; -+ composes: classname from "./style.css"; - } - - .otherClassName { -- composes: globalClassName from global; -+ composes: globalclassname from global; - } - - :global { @@ -42,7 +43,7 @@ localAlias: keyFromDep; } @@ -133,16 +113,16 @@ info: css/modules/modules.css } .otherClassName { - composes: classname; + composes: className; color: yellow; } .otherClassName { - composes: classname from "./style.css"; + composes: className from "./style.css"; } .otherClassName { - composes: globalclassname from global; + composes: globalClassName from global; } :global { diff --git a/crates/biome_css_parser/src/lexer/mod.rs b/crates/biome_css_parser/src/lexer/mod.rs index 2fbc878083a4..f0c53ad51f76 100644 --- a/crates/biome_css_parser/src/lexer/mod.rs +++ b/crates/biome_css_parser/src/lexer/mod.rs @@ -845,6 +845,7 @@ impl<'src> CssLexer<'src> { b"regexp" => REGEXP_KW, b"value" => VALUE_KW, b"as" => AS_KW, + b"composes" => COMPOSES_KW, _ => IDENT, } } diff --git a/crates/biome_css_parser/src/parser.rs b/crates/biome_css_parser/src/parser.rs index ba36514c8596..de43e8824cd1 100644 --- a/crates/biome_css_parser/src/parser.rs +++ b/crates/biome_css_parser/src/parser.rs @@ -43,6 +43,11 @@ impl CssParserOptions { self.css_modules = true; self } + + /// Checks if parsing of CSS Modules features is disabled. + pub fn is_css_modules_disabled(&self) -> bool { + !self.css_modules + } } impl<'source> CssParser<'source> { diff --git a/crates/biome_css_parser/src/syntax/at_rule/keyframes.rs b/crates/biome_css_parser/src/syntax/at_rule/keyframes.rs index 06eb6775fa68..9a1963e7104b 100644 --- a/crates/biome_css_parser/src/syntax/at_rule/keyframes.rs +++ b/crates/biome_css_parser/src/syntax/at_rule/keyframes.rs @@ -106,7 +106,7 @@ fn parse_keyframes_scoped_name(p: &mut CssParser) -> ParsedSyntax { p.bump(T![:]); - if !p.options().css_modules { + if p.options().is_css_modules_disabled() { // :local and :global are not standard CSS features // provide a hint on how to enable parsing of these pseudo-classes p.error(local_or_global_not_allowed(p, p.cur_range())); diff --git a/crates/biome_css_parser/src/syntax/at_rule/value.rs b/crates/biome_css_parser/src/syntax/at_rule/value.rs index 80061861eee9..2b54bc62ae83 100644 --- a/crates/biome_css_parser/src/syntax/at_rule/value.rs +++ b/crates/biome_css_parser/src/syntax/at_rule/value.rs @@ -37,7 +37,7 @@ pub(crate) fn parse_value_at_rule(p: &mut CssParser) -> ParsedSyntax { return Absent; } - if !p.options().css_modules { + if p.options().is_css_modules_disabled() { // @value at-rule is not a standard CSS feature. // Provide a hint on how to enable parsing of @value at-rules. p.error(value_at_rule_not_allowed(p, p.cur_range())); diff --git a/crates/biome_css_parser/src/syntax/css_modules.rs b/crates/biome_css_parser/src/syntax/css_modules.rs index 47a67efe3f32..f2ac3b773a34 100644 --- a/crates/biome_css_parser/src/syntax/css_modules.rs +++ b/crates/biome_css_parser/src/syntax/css_modules.rs @@ -25,3 +25,37 @@ pub(crate) fn local_or_global_not_allowed(p: &CssParser, range: TextRange) -> Pa pub(crate) fn expected_any_css_module_scope(p: &CssParser, range: TextRange) -> ParseDiagnostic { expect_one_of(&["global", "local"], range).into_diagnostic(p) } + +/// Generates a parse diagnostic for the `composes` declaration when it is not allowed. +/// +/// This function returns an error diagnostic indicating that the `composes` declaration +/// is not a standard CSS feature. It also provides a hint on how to enable parsing of the +/// `composes` declaration by setting the `css_modules` option to `true` in the configuration file. +pub(crate) fn composes_not_allowed(p: &CssParser, range: TextRange) -> ParseDiagnostic { + p.err_builder( + "`composes` declaration is not a standard CSS feature.", + range, + ) + .with_hint( + "You can enable `composes` declaration parsing by setting the `css_modules` option to `true` in your configuration file.", + ) +} + +/// Generates a parse diagnostic for an expected `composes` import source. +/// +/// This function returns a diagnostic error indicating that an `` or `` +/// was expected as the source for a `composes` import declaration at the given range in the CSS parser. +pub(crate) fn expected_composes_import_source(p: &CssParser, range: TextRange) -> ParseDiagnostic { + expect_one_of(&["", ""], range).into_diagnostic(p) +} + +/// Generates a parse diagnostic for an empty list of classes after `composes`. +/// +/// This function returns a diagnostic error indicating that a non-empty list of classes was expected +/// after the `composes` keyword in a CSS Modules declaration, but an empty list was found. +pub(crate) fn expected_classes_list(p: &CssParser, range: TextRange) -> ParseDiagnostic { + p.err_builder( + "Expected a non-empty list of classes after `composes`.", + range, + ) +} diff --git a/crates/biome_css_parser/src/syntax/property/mod.rs b/crates/biome_css_parser/src/syntax/property/mod.rs index 803bae3f0c35..abba7279ba73 100644 --- a/crates/biome_css_parser/src/syntax/property/mod.rs +++ b/crates/biome_css_parser/src/syntax/property/mod.rs @@ -1,33 +1,165 @@ +use crate::lexer::CssLexContext; use crate::parser::CssParser; -use crate::syntax::parse_error::expected_component_value; -use crate::syntax::{is_at_any_value, is_at_identifier, parse_any_value, parse_regular_identifier}; +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::{ + is_at_any_value, is_at_identifier, is_at_string, parse_any_value, + parse_custom_identifier_with_keywords, 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::{ParseRecoveryTokenSet, RecoveryResult}; +use biome_parser::parse_recovery::{ParseRecovery, ParseRecoveryTokenSet, RecoveryResult}; use biome_parser::prelude::ParsedSyntax; use biome_parser::prelude::ParsedSyntax::{Absent, Present}; use biome_parser::{token_set, Parser, TokenSet}; +#[inline] pub(crate) fn is_at_any_property(p: &mut CssParser) -> bool { is_at_generic_property(p) } +#[inline] pub(crate) fn parse_any_property(p: &mut CssParser) -> ParsedSyntax { if !is_at_any_property(p) { return Absent; } - parse_generic_property(p) + match p.cur() { + T![composes] => parse_composes_property(p), + _ => parse_generic_property(p), + } +} + +/// Checks if the current parser position is at a `composes` property. +/// +/// This function determines if the parser is currently positioned at a `composes` property, +/// which is indicated by the presence of the `composes` keyword followed by a colon (`:`). +#[inline] +fn is_at_composes_property(p: &mut CssParser) -> bool { + p.at(T![composes]) && p.nth_at(1, T![:]) +} + +/// Parses a `composes` property in CSS Modules. +/// +/// This function parses a `composes` property, which is used in CSS Modules to compose classes from other modules. +/// If the current parser position is not at a `composes` property, it returns `Absent`. If CSS Modules are disabled, +/// it generates a diagnostic error and falls back to parsing a generic property. +/// +/// Basic usage in CSS: +/// ```css +/// .button { +/// composes: baseButton alertButton from 'base.css'; +/// } +/// +/// .alert { +/// composes: alertText; +/// } +/// ``` +fn parse_composes_property(p: &mut CssParser) -> ParsedSyntax { + if !is_at_composes_property(p) { + return Absent; + } + + if p.options().is_css_modules_disabled() { + // `composes` is not a standard CSS feature. + // Provide a hint on how to enable parsing of the `composes` declaration. + p.error(composes_not_allowed(p, p.cur_range())); + + // Fallback to a generic property + return parse_generic_property(p); + } + + let m = p.start(); + // remap the `composes` keyword to a regular identifier + parse_regular_identifier(p).ok(); + p.bump(T![:]); + + { + let m = p.start(); + + let classes = ComposesClassList.parse_list(p); + + // If the list of classes is empty, generate a diagnostic error. + if classes.range(p).is_empty() { + p.error(expected_classes_list(p, p.cur_range())); + } + + if p.at(T![from]) { + let m = p.start(); + p.bump(T![from]); + + if is_at_identifier(p) { + parse_regular_identifier(p).ok(); + } else if is_at_string(p) { + parse_string(p).ok(); + } else { + p.error(expected_composes_import_source(p, p.cur_range())); + } + + m.complete(p, CSS_COMPOSES_IMPORT_SPECIFIER); + } + m.complete(p, CSS_COMPOSES_PROPERTY_VALUE); + } + + Present(m.complete(p, CSS_COMPOSES_PROPERTY)) +} + +/// A struct representing a list of classes in a `composes` property. +struct ComposesClassList; + +impl ParseNodeList for ComposesClassList { + type Kind = CssSyntaxKind; + type Parser<'source> = CssParser<'source>; + const LIST_KIND: Self::Kind = CSS_COMPOSES_CLASS_LIST; + + /// Parses an individual element in the `composes` class list. + /// + /// This function parses an identifier as a custom identifier because it is a selector, + /// which is case-sensitive. For more information, see: + /// https://github.com/css-modules/css-modules/blob/master/docs/composition.md + fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { + parse_custom_identifier_with_keywords(p, CssLexContext::Regular, true) + } + + fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { + p.at_ts(CSS_END_OF_COMPOSES_CLASS_TOKEN_SET) + } + + fn recover( + &mut self, + p: &mut Self::Parser<'_>, + parsed_element: ParsedSyntax, + ) -> RecoveryResult { + parsed_element.or_recover(p, &ComposesClassListParseRecovery, expected_identifier) + } +} + +struct ComposesClassListParseRecovery; + +impl ParseRecovery for ComposesClassListParseRecovery { + type Kind = CssSyntaxKind; + type Parser<'source> = CssParser<'source>; + const RECOVERED_KIND: Self::Kind = CSS_BOGUS; + + fn is_at_recovered(&self, p: &mut Self::Parser<'_>) -> bool { + // If the next token is the end of the list or the next element, we're at a recovery point. + p.at_ts(CSS_END_OF_COMPOSES_CLASS_TOKEN_SET) || is_at_identifier(p) + } } +const CSS_END_OF_COMPOSES_CLASS_TOKEN_SET: TokenSet = + CSS_END_OF_PROPERTY_VALUE_TOKEN_SET.union(token_set!(T![from])); + #[inline] -pub(crate) fn is_at_generic_property(p: &mut CssParser) -> bool { +fn is_at_generic_property(p: &mut CssParser) -> bool { is_at_identifier(p) && p.nth_at(1, T![:]) } #[inline] -pub(crate) fn parse_generic_property(p: &mut CssParser) -> ParsedSyntax { +fn parse_generic_property(p: &mut CssParser) -> ParsedSyntax { if !is_at_generic_property(p) { return Absent; } @@ -43,7 +175,7 @@ pub(crate) fn parse_generic_property(p: &mut CssParser) -> ParsedSyntax { } const CSS_END_OF_PROPERTY_VALUE_TOKEN_SET: TokenSet = token_set!(T!['}'], T![;]); -pub(crate) struct GenericComponentValueList; +struct GenericComponentValueList; impl ParseNodeList for GenericComponentValueList { type Kind = CssSyntaxKind; @@ -75,12 +207,12 @@ impl ParseNodeList for GenericComponentValueList { } #[inline] -pub(crate) fn is_at_generic_component_value(p: &mut CssParser) -> bool { +fn is_at_generic_component_value(p: &mut CssParser) -> bool { is_at_any_value(p) || is_at_generic_delimiter(p) } #[inline] -pub(crate) fn parse_generic_component_value(p: &mut CssParser) -> ParsedSyntax { +fn parse_generic_component_value(p: &mut CssParser) -> ParsedSyntax { if !is_at_generic_component_value(p) { return Absent; } @@ -94,12 +226,12 @@ pub(crate) fn parse_generic_component_value(p: &mut CssParser) -> ParsedSyntax { const GENERIC_DELIMITER_SET: TokenSet = token_set![T![,], T![/]]; #[inline] -pub(crate) fn is_at_generic_delimiter(p: &mut CssParser) -> bool { +fn is_at_generic_delimiter(p: &mut CssParser) -> bool { p.at_ts(GENERIC_DELIMITER_SET) } #[inline] -pub(crate) fn parse_generic_delimiter(p: &mut CssParser) -> ParsedSyntax { +fn parse_generic_delimiter(p: &mut CssParser) -> ParsedSyntax { if !is_at_generic_delimiter(p) { return Absent; } diff --git a/crates/biome_css_parser/src/syntax/selector/pseudo_class/function_selector.rs b/crates/biome_css_parser/src/syntax/selector/pseudo_class/function_selector.rs index 215334707c57..71e7f9a9268f 100644 --- a/crates/biome_css_parser/src/syntax/selector/pseudo_class/function_selector.rs +++ b/crates/biome_css_parser/src/syntax/selector/pseudo_class/function_selector.rs @@ -42,7 +42,7 @@ pub(crate) fn parse_pseudo_class_function_selector(p: &mut CssParser) -> ParsedS return Absent; } - if !p.options().css_modules { + if p.options().is_css_modules_disabled() { // :local and :global are not standard CSS features // provide a hint on how to enable parsing of these pseudo-classes p.error(local_or_global_not_allowed(p, p.cur_range())); diff --git a/crates/biome_css_parser/tests/css_test_suite/error/property/composes.enabled/composes_error_enabled.css b/crates/biome_css_parser/tests/css_test_suite/error/property/composes.enabled/composes_error_enabled.css new file mode 100644 index 000000000000..2b59e449572d --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/error/property/composes.enabled/composes_error_enabled.css @@ -0,0 +1,19 @@ +.a { + composes: ; +} + +.otherClassName { + composes: className from ; +} + +.otherClassName { + composes: globalClassName from ; +} + +.otherClassName { + composes: from ; +} + +.otherClassName { + composes: from global; +} diff --git a/crates/biome_css_parser/tests/css_test_suite/error/property/composes.enabled/composes_error_enabled.css.snap b/crates/biome_css_parser/tests/css_test_suite/error/property/composes.enabled/composes_error_enabled.css.snap new file mode 100644 index 000000000000..3edaeb77a873 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/error/property/composes.enabled/composes_error_enabled.css.snap @@ -0,0 +1,480 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```css +.a { + composes: ; +} + +.otherClassName { + composes: className from ; +} + +.otherClassName { + composes: globalClassName from ; +} + +.otherClassName { + composes: from ; +} + +.otherClassName { + composes: from global; +} + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + rules: CssRuleList [ + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@0..1 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@1..3 "a" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@3..4 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssComposesProperty { + name: CssIdentifier { + value_token: IDENT@4..14 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@14..16 ":" [] [Whitespace(" ")], + value: CssComposesPropertyValue { + classes: CssComposesClassList [], + specifier: missing (optional), + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@16..17 ";" [] [], + }, + ], + r_curly_token: R_CURLY@17..19 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@19..22 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@22..37 "otherClassName" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@37..38 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssComposesProperty { + name: CssIdentifier { + value_token: IDENT@38..48 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@48..50 ":" [] [Whitespace(" ")], + value: CssComposesPropertyValue { + classes: CssComposesClassList [ + CssCustomIdentifier { + value_token: IDENT@50..60 "className" [] [Whitespace(" ")], + }, + ], + specifier: CssComposesImportSpecifier { + from_token: FROM_KW@60..65 "from" [] [Whitespace(" ")], + source: missing (required), + }, + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@65..66 ";" [] [], + }, + ], + r_curly_token: R_CURLY@66..68 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@68..71 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@71..86 "otherClassName" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@86..87 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssComposesProperty { + name: CssIdentifier { + value_token: IDENT@87..97 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@97..99 ":" [] [Whitespace(" ")], + value: CssComposesPropertyValue { + classes: CssComposesClassList [ + CssCustomIdentifier { + value_token: IDENT@99..115 "globalClassName" [] [Whitespace(" ")], + }, + ], + specifier: CssComposesImportSpecifier { + from_token: FROM_KW@115..120 "from" [] [Whitespace(" ")], + source: missing (required), + }, + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@120..121 ";" [] [], + }, + ], + r_curly_token: R_CURLY@121..123 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@123..126 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@126..141 "otherClassName" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@141..142 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssComposesProperty { + name: CssIdentifier { + value_token: IDENT@142..152 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@152..155 ":" [] [Whitespace(" ")], + value: CssComposesPropertyValue { + classes: CssComposesClassList [], + specifier: CssComposesImportSpecifier { + from_token: FROM_KW@155..160 "from" [] [Whitespace(" ")], + source: missing (required), + }, + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@160..161 ";" [] [], + }, + ], + r_curly_token: R_CURLY@161..163 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@163..166 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@166..181 "otherClassName" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@181..182 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssComposesProperty { + name: CssIdentifier { + value_token: IDENT@182..192 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@192..195 ":" [] [Whitespace(" ")], + value: CssComposesPropertyValue { + classes: CssComposesClassList [], + specifier: CssComposesImportSpecifier { + from_token: FROM_KW@195..200 "from" [] [Whitespace(" ")], + source: CssIdentifier { + value_token: IDENT@200..206 "global" [] [], + }, + }, + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@206..207 ";" [] [], + }, + ], + r_curly_token: R_CURLY@207..209 "}" [Newline("\n")] [], + }, + }, + ], + eof_token: EOF@209..210 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..210 + 0: (empty) + 1: CSS_RULE_LIST@0..209 + 0: CSS_QUALIFIED_RULE@0..19 + 0: CSS_SELECTOR_LIST@0..3 + 0: CSS_COMPOUND_SELECTOR@0..3 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@0..3 + 0: CSS_CLASS_SELECTOR@0..3 + 0: DOT@0..1 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@1..3 + 0: IDENT@1..3 "a" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@3..19 + 0: L_CURLY@3..4 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@4..17 + 0: CSS_DECLARATION_WITH_SEMICOLON@4..17 + 0: CSS_DECLARATION@4..16 + 0: CSS_COMPOSES_PROPERTY@4..16 + 0: CSS_IDENTIFIER@4..14 + 0: IDENT@4..14 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@14..16 ":" [] [Whitespace(" ")] + 2: CSS_COMPOSES_PROPERTY_VALUE@16..16 + 0: CSS_COMPOSES_CLASS_LIST@16..16 + 1: (empty) + 1: (empty) + 1: SEMICOLON@16..17 ";" [] [] + 2: R_CURLY@17..19 "}" [Newline("\n")] [] + 1: CSS_QUALIFIED_RULE@19..68 + 0: CSS_SELECTOR_LIST@19..37 + 0: CSS_COMPOUND_SELECTOR@19..37 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@19..37 + 0: CSS_CLASS_SELECTOR@19..37 + 0: DOT@19..22 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@22..37 + 0: IDENT@22..37 "otherClassName" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@37..68 + 0: L_CURLY@37..38 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@38..66 + 0: CSS_DECLARATION_WITH_SEMICOLON@38..66 + 0: CSS_DECLARATION@38..65 + 0: CSS_COMPOSES_PROPERTY@38..65 + 0: CSS_IDENTIFIER@38..48 + 0: IDENT@38..48 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@48..50 ":" [] [Whitespace(" ")] + 2: CSS_COMPOSES_PROPERTY_VALUE@50..65 + 0: CSS_COMPOSES_CLASS_LIST@50..60 + 0: CSS_CUSTOM_IDENTIFIER@50..60 + 0: IDENT@50..60 "className" [] [Whitespace(" ")] + 1: CSS_COMPOSES_IMPORT_SPECIFIER@60..65 + 0: FROM_KW@60..65 "from" [] [Whitespace(" ")] + 1: (empty) + 1: (empty) + 1: SEMICOLON@65..66 ";" [] [] + 2: R_CURLY@66..68 "}" [Newline("\n")] [] + 2: CSS_QUALIFIED_RULE@68..123 + 0: CSS_SELECTOR_LIST@68..86 + 0: CSS_COMPOUND_SELECTOR@68..86 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@68..86 + 0: CSS_CLASS_SELECTOR@68..86 + 0: DOT@68..71 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@71..86 + 0: IDENT@71..86 "otherClassName" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@86..123 + 0: L_CURLY@86..87 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@87..121 + 0: CSS_DECLARATION_WITH_SEMICOLON@87..121 + 0: CSS_DECLARATION@87..120 + 0: CSS_COMPOSES_PROPERTY@87..120 + 0: CSS_IDENTIFIER@87..97 + 0: IDENT@87..97 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@97..99 ":" [] [Whitespace(" ")] + 2: CSS_COMPOSES_PROPERTY_VALUE@99..120 + 0: CSS_COMPOSES_CLASS_LIST@99..115 + 0: CSS_CUSTOM_IDENTIFIER@99..115 + 0: IDENT@99..115 "globalClassName" [] [Whitespace(" ")] + 1: CSS_COMPOSES_IMPORT_SPECIFIER@115..120 + 0: FROM_KW@115..120 "from" [] [Whitespace(" ")] + 1: (empty) + 1: (empty) + 1: SEMICOLON@120..121 ";" [] [] + 2: R_CURLY@121..123 "}" [Newline("\n")] [] + 3: CSS_QUALIFIED_RULE@123..163 + 0: CSS_SELECTOR_LIST@123..141 + 0: CSS_COMPOUND_SELECTOR@123..141 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@123..141 + 0: CSS_CLASS_SELECTOR@123..141 + 0: DOT@123..126 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@126..141 + 0: IDENT@126..141 "otherClassName" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@141..163 + 0: L_CURLY@141..142 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@142..161 + 0: CSS_DECLARATION_WITH_SEMICOLON@142..161 + 0: CSS_DECLARATION@142..160 + 0: CSS_COMPOSES_PROPERTY@142..160 + 0: CSS_IDENTIFIER@142..152 + 0: IDENT@142..152 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@152..155 ":" [] [Whitespace(" ")] + 2: CSS_COMPOSES_PROPERTY_VALUE@155..160 + 0: CSS_COMPOSES_CLASS_LIST@155..155 + 1: CSS_COMPOSES_IMPORT_SPECIFIER@155..160 + 0: FROM_KW@155..160 "from" [] [Whitespace(" ")] + 1: (empty) + 1: (empty) + 1: SEMICOLON@160..161 ";" [] [] + 2: R_CURLY@161..163 "}" [Newline("\n")] [] + 4: CSS_QUALIFIED_RULE@163..209 + 0: CSS_SELECTOR_LIST@163..181 + 0: CSS_COMPOUND_SELECTOR@163..181 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@163..181 + 0: CSS_CLASS_SELECTOR@163..181 + 0: DOT@163..166 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@166..181 + 0: IDENT@166..181 "otherClassName" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@181..209 + 0: L_CURLY@181..182 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@182..207 + 0: CSS_DECLARATION_WITH_SEMICOLON@182..207 + 0: CSS_DECLARATION@182..206 + 0: CSS_COMPOSES_PROPERTY@182..206 + 0: CSS_IDENTIFIER@182..192 + 0: IDENT@182..192 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@192..195 ":" [] [Whitespace(" ")] + 2: CSS_COMPOSES_PROPERTY_VALUE@195..206 + 0: CSS_COMPOSES_CLASS_LIST@195..195 + 1: CSS_COMPOSES_IMPORT_SPECIFIER@195..206 + 0: FROM_KW@195..200 "from" [] [Whitespace(" ")] + 1: CSS_IDENTIFIER@200..206 + 0: IDENT@200..206 "global" [] [] + 1: (empty) + 1: SEMICOLON@206..207 ";" [] [] + 2: R_CURLY@207..209 "}" [Newline("\n")] [] + 2: EOF@209..210 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +composes_error_enabled.css:2:12 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a non-empty list of classes after `composes`. + + 1 │ .a { + > 2 │ composes: ; + │ ^ + 3 │ } + 4 │ + +composes_error_enabled.css:6:27 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected value or character. + + 5 │ .otherClassName { + > 6 │ composes: className from ; + │ ^ + 7 │ } + 8 │ + + i Expected one of: + + - + - + +composes_error_enabled.css:10:33 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected value or character. + + 9 │ .otherClassName { + > 10 │ composes: globalClassName from ; + │ ^ + 11 │ } + 12 │ + + i Expected one of: + + - + - + +composes_error_enabled.css:14:13 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a non-empty list of classes after `composes`. + + 13 │ .otherClassName { + > 14 │ composes: from ; + │ ^^^^ + 15 │ } + 16 │ + +composes_error_enabled.css:14:18 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected value or character. + + 13 │ .otherClassName { + > 14 │ composes: from ; + │ ^ + 15 │ } + 16 │ + + i Expected one of: + + - + - + +composes_error_enabled.css:18:13 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a non-empty list of classes after `composes`. + + 17 │ .otherClassName { + > 18 │ composes: from global; + │ ^^^^ + 19 │ } + 20 │ + +``` diff --git a/crates/biome_css_parser/tests/css_test_suite/error/property/composes.enabled/options.json b/crates/biome_css_parser/tests/css_test_suite/error/property/composes.enabled/options.json new file mode 100644 index 000000000000..f789463bbcbd --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/error/property/composes.enabled/options.json @@ -0,0 +1,8 @@ +{ + "$schema": "../../../../../../../packages/@biomejs/biome/configuration_schema.json", + "css": { + "parser": { + "cssModules": true + } + } +} diff --git a/crates/biome_css_parser/tests/css_test_suite/error/property/composes/disabled/composes_error_disabled.css b/crates/biome_css_parser/tests/css_test_suite/error/property/composes/disabled/composes_error_disabled.css new file mode 100644 index 000000000000..540e56b0cd5a --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/error/property/composes/disabled/composes_error_disabled.css @@ -0,0 +1,11 @@ +.a { + composes: myClass; +} + +.otherClassName { + composes: className from "./style.css"; +} + +.otherClassName { + composes: globalClassName from global; +} diff --git a/crates/biome_css_parser/tests/css_test_suite/error/property/composes/disabled/composes_error_disabled.css.snap b/crates/biome_css_parser/tests/css_test_suite/error/property/composes/disabled/composes_error_disabled.css.snap new file mode 100644 index 000000000000..3b5017b360cf --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/error/property/composes/disabled/composes_error_disabled.css.snap @@ -0,0 +1,295 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```css +.a { + composes: myClass; +} + +.otherClassName { + composes: className from "./style.css"; +} + +.otherClassName { + composes: globalClassName from global; +} + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + rules: CssRuleList [ + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@0..1 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@1..3 "a" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@3..4 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@4..14 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@14..16 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@16..23 "myClass" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@23..24 ";" [] [], + }, + ], + r_curly_token: R_CURLY@24..26 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@26..29 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@29..44 "otherClassName" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@44..45 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@45..55 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@55..57 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@57..67 "className" [] [Whitespace(" ")], + }, + CssIdentifier { + value_token: IDENT@67..72 "from" [] [Whitespace(" ")], + }, + CssString { + value_token: CSS_STRING_LITERAL@72..85 "\"./style.css\"" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@85..86 ";" [] [], + }, + ], + r_curly_token: R_CURLY@86..88 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@88..91 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@91..106 "otherClassName" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@106..107 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@107..117 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@117..119 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@119..135 "globalClassName" [] [Whitespace(" ")], + }, + CssIdentifier { + value_token: IDENT@135..140 "from" [] [Whitespace(" ")], + }, + CssIdentifier { + value_token: IDENT@140..146 "global" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@146..147 ";" [] [], + }, + ], + r_curly_token: R_CURLY@147..149 "}" [Newline("\n")] [], + }, + }, + ], + eof_token: EOF@149..150 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..150 + 0: (empty) + 1: CSS_RULE_LIST@0..149 + 0: CSS_QUALIFIED_RULE@0..26 + 0: CSS_SELECTOR_LIST@0..3 + 0: CSS_COMPOUND_SELECTOR@0..3 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@0..3 + 0: CSS_CLASS_SELECTOR@0..3 + 0: DOT@0..1 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@1..3 + 0: IDENT@1..3 "a" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@3..26 + 0: L_CURLY@3..4 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@4..24 + 0: CSS_DECLARATION_WITH_SEMICOLON@4..24 + 0: CSS_DECLARATION@4..23 + 0: CSS_GENERIC_PROPERTY@4..23 + 0: CSS_IDENTIFIER@4..14 + 0: IDENT@4..14 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@14..16 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@16..23 + 0: CSS_IDENTIFIER@16..23 + 0: IDENT@16..23 "myClass" [] [] + 1: (empty) + 1: SEMICOLON@23..24 ";" [] [] + 2: R_CURLY@24..26 "}" [Newline("\n")] [] + 1: CSS_QUALIFIED_RULE@26..88 + 0: CSS_SELECTOR_LIST@26..44 + 0: CSS_COMPOUND_SELECTOR@26..44 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@26..44 + 0: CSS_CLASS_SELECTOR@26..44 + 0: DOT@26..29 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@29..44 + 0: IDENT@29..44 "otherClassName" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@44..88 + 0: L_CURLY@44..45 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@45..86 + 0: CSS_DECLARATION_WITH_SEMICOLON@45..86 + 0: CSS_DECLARATION@45..85 + 0: CSS_GENERIC_PROPERTY@45..85 + 0: CSS_IDENTIFIER@45..55 + 0: IDENT@45..55 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@55..57 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@57..85 + 0: CSS_IDENTIFIER@57..67 + 0: IDENT@57..67 "className" [] [Whitespace(" ")] + 1: CSS_IDENTIFIER@67..72 + 0: IDENT@67..72 "from" [] [Whitespace(" ")] + 2: CSS_STRING@72..85 + 0: CSS_STRING_LITERAL@72..85 "\"./style.css\"" [] [] + 1: (empty) + 1: SEMICOLON@85..86 ";" [] [] + 2: R_CURLY@86..88 "}" [Newline("\n")] [] + 2: CSS_QUALIFIED_RULE@88..149 + 0: CSS_SELECTOR_LIST@88..106 + 0: CSS_COMPOUND_SELECTOR@88..106 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@88..106 + 0: CSS_CLASS_SELECTOR@88..106 + 0: DOT@88..91 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@91..106 + 0: IDENT@91..106 "otherClassName" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@106..149 + 0: L_CURLY@106..107 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@107..147 + 0: CSS_DECLARATION_WITH_SEMICOLON@107..147 + 0: CSS_DECLARATION@107..146 + 0: CSS_GENERIC_PROPERTY@107..146 + 0: CSS_IDENTIFIER@107..117 + 0: IDENT@107..117 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@117..119 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@119..146 + 0: CSS_IDENTIFIER@119..135 + 0: IDENT@119..135 "globalClassName" [] [Whitespace(" ")] + 1: CSS_IDENTIFIER@135..140 + 0: IDENT@135..140 "from" [] [Whitespace(" ")] + 2: CSS_IDENTIFIER@140..146 + 0: IDENT@140..146 "global" [] [] + 1: (empty) + 1: SEMICOLON@146..147 ";" [] [] + 2: R_CURLY@147..149 "}" [Newline("\n")] [] + 2: EOF@149..150 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +composes_error_disabled.css:2:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × `composes` declaration is not a standard CSS feature. + + 1 │ .a { + > 2 │ composes: myClass; + │ ^^^^^^^^ + 3 │ } + 4 │ + + i You can enable `composes` declaration parsing by setting the `css_modules` option to `true` in your configuration file. + +composes_error_disabled.css:6:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × `composes` declaration is not a standard CSS feature. + + 5 │ .otherClassName { + > 6 │ composes: className from "./style.css"; + │ ^^^^^^^^ + 7 │ } + 8 │ + + i You can enable `composes` declaration parsing by setting the `css_modules` option to `true` in your configuration file. + +composes_error_disabled.css:10:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × `composes` declaration is not a standard CSS feature. + + 9 │ .otherClassName { + > 10 │ composes: globalClassName from global; + │ ^^^^^^^^ + 11 │ } + 12 │ + + i You can enable `composes` declaration parsing by setting the `css_modules` option to `true` in your configuration file. + +``` diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/property/composes/composes.css b/crates/biome_css_parser/tests/css_test_suite/ok/property/composes/composes.css new file mode 100644 index 000000000000..2bd6495bff8f --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/property/composes/composes.css @@ -0,0 +1,19 @@ +.a { + composes: myClass; +} + +.otherClassName { + composes: className from "./style.css"; +} + +.otherClassName { + composes: globalClassName from global; +} + +.b { + composes: classNameA classNameB; +} + +.c { + composes: classNameA classNameB from './module.css'; +} diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/property/composes/composes.css.snap b/crates/biome_css_parser/tests/css_test_suite/ok/property/composes/composes.css.snap new file mode 100644 index 000000000000..f4cec2eff99d --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/property/composes/composes.css.snap @@ -0,0 +1,429 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```css +.a { + composes: myClass; +} + +.otherClassName { + composes: className from "./style.css"; +} + +.otherClassName { + composes: globalClassName from global; +} + +.b { + composes: classNameA classNameB; +} + +.c { + composes: classNameA classNameB from './module.css'; +} + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + rules: CssRuleList [ + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@0..1 "." [] [], + name: CssCustomIdentifier { + value_token: IDENT@1..3 "a" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@3..4 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssComposesProperty { + name: CssIdentifier { + value_token: IDENT@4..14 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@14..16 ":" [] [Whitespace(" ")], + value: CssComposesPropertyValue { + classes: CssComposesClassList [ + CssCustomIdentifier { + value_token: IDENT@16..23 "myClass" [] [], + }, + ], + specifier: missing (optional), + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@23..24 ";" [] [], + }, + ], + r_curly_token: R_CURLY@24..26 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@26..29 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@29..44 "otherClassName" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@44..45 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssComposesProperty { + name: CssIdentifier { + value_token: IDENT@45..55 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@55..57 ":" [] [Whitespace(" ")], + value: CssComposesPropertyValue { + classes: CssComposesClassList [ + CssCustomIdentifier { + value_token: IDENT@57..67 "className" [] [Whitespace(" ")], + }, + ], + specifier: CssComposesImportSpecifier { + from_token: FROM_KW@67..72 "from" [] [Whitespace(" ")], + source: CssString { + value_token: CSS_STRING_LITERAL@72..85 "\"./style.css\"" [] [], + }, + }, + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@85..86 ";" [] [], + }, + ], + r_curly_token: R_CURLY@86..88 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@88..91 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@91..106 "otherClassName" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@106..107 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssComposesProperty { + name: CssIdentifier { + value_token: IDENT@107..117 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@117..119 ":" [] [Whitespace(" ")], + value: CssComposesPropertyValue { + classes: CssComposesClassList [ + CssCustomIdentifier { + value_token: IDENT@119..135 "globalClassName" [] [Whitespace(" ")], + }, + ], + specifier: CssComposesImportSpecifier { + from_token: FROM_KW@135..140 "from" [] [Whitespace(" ")], + source: CssIdentifier { + value_token: IDENT@140..146 "global" [] [], + }, + }, + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@146..147 ";" [] [], + }, + ], + r_curly_token: R_CURLY@147..149 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@149..152 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@152..154 "b" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@154..155 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssComposesProperty { + name: CssIdentifier { + value_token: IDENT@155..165 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@165..167 ":" [] [Whitespace(" ")], + value: CssComposesPropertyValue { + classes: CssComposesClassList [ + CssCustomIdentifier { + value_token: IDENT@167..178 "classNameA" [] [Whitespace(" ")], + }, + CssCustomIdentifier { + value_token: IDENT@178..188 "classNameB" [] [], + }, + ], + specifier: missing (optional), + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@188..189 ";" [] [], + }, + ], + r_curly_token: R_CURLY@189..191 "}" [Newline("\n")] [], + }, + }, + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: missing (optional), + sub_selectors: CssSubSelectorList [ + CssClassSelector { + dot_token: DOT@191..194 "." [Newline("\n"), Newline("\n")] [], + name: CssCustomIdentifier { + value_token: IDENT@194..196 "c" [] [Whitespace(" ")], + }, + }, + ], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@196..197 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssComposesProperty { + name: CssIdentifier { + value_token: IDENT@197..207 "composes" [Newline("\n"), Whitespace("\t")] [], + }, + colon_token: COLON@207..209 ":" [] [Whitespace(" ")], + value: CssComposesPropertyValue { + classes: CssComposesClassList [ + CssCustomIdentifier { + value_token: IDENT@209..220 "classNameA" [] [Whitespace(" ")], + }, + CssCustomIdentifier { + value_token: IDENT@220..231 "classNameB" [] [Whitespace(" ")], + }, + ], + specifier: CssComposesImportSpecifier { + from_token: FROM_KW@231..236 "from" [] [Whitespace(" ")], + source: CssString { + value_token: CSS_STRING_LITERAL@236..250 "'./module.css'" [] [], + }, + }, + }, + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@250..251 ";" [] [], + }, + ], + r_curly_token: R_CURLY@251..253 "}" [Newline("\n")] [], + }, + }, + ], + eof_token: EOF@253..254 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..254 + 0: (empty) + 1: CSS_RULE_LIST@0..253 + 0: CSS_QUALIFIED_RULE@0..26 + 0: CSS_SELECTOR_LIST@0..3 + 0: CSS_COMPOUND_SELECTOR@0..3 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@0..3 + 0: CSS_CLASS_SELECTOR@0..3 + 0: DOT@0..1 "." [] [] + 1: CSS_CUSTOM_IDENTIFIER@1..3 + 0: IDENT@1..3 "a" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@3..26 + 0: L_CURLY@3..4 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@4..24 + 0: CSS_DECLARATION_WITH_SEMICOLON@4..24 + 0: CSS_DECLARATION@4..23 + 0: CSS_COMPOSES_PROPERTY@4..23 + 0: CSS_IDENTIFIER@4..14 + 0: IDENT@4..14 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@14..16 ":" [] [Whitespace(" ")] + 2: CSS_COMPOSES_PROPERTY_VALUE@16..23 + 0: CSS_COMPOSES_CLASS_LIST@16..23 + 0: CSS_CUSTOM_IDENTIFIER@16..23 + 0: IDENT@16..23 "myClass" [] [] + 1: (empty) + 1: (empty) + 1: SEMICOLON@23..24 ";" [] [] + 2: R_CURLY@24..26 "}" [Newline("\n")] [] + 1: CSS_QUALIFIED_RULE@26..88 + 0: CSS_SELECTOR_LIST@26..44 + 0: CSS_COMPOUND_SELECTOR@26..44 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@26..44 + 0: CSS_CLASS_SELECTOR@26..44 + 0: DOT@26..29 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@29..44 + 0: IDENT@29..44 "otherClassName" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@44..88 + 0: L_CURLY@44..45 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@45..86 + 0: CSS_DECLARATION_WITH_SEMICOLON@45..86 + 0: CSS_DECLARATION@45..85 + 0: CSS_COMPOSES_PROPERTY@45..85 + 0: CSS_IDENTIFIER@45..55 + 0: IDENT@45..55 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@55..57 ":" [] [Whitespace(" ")] + 2: CSS_COMPOSES_PROPERTY_VALUE@57..85 + 0: CSS_COMPOSES_CLASS_LIST@57..67 + 0: CSS_CUSTOM_IDENTIFIER@57..67 + 0: IDENT@57..67 "className" [] [Whitespace(" ")] + 1: CSS_COMPOSES_IMPORT_SPECIFIER@67..85 + 0: FROM_KW@67..72 "from" [] [Whitespace(" ")] + 1: CSS_STRING@72..85 + 0: CSS_STRING_LITERAL@72..85 "\"./style.css\"" [] [] + 1: (empty) + 1: SEMICOLON@85..86 ";" [] [] + 2: R_CURLY@86..88 "}" [Newline("\n")] [] + 2: CSS_QUALIFIED_RULE@88..149 + 0: CSS_SELECTOR_LIST@88..106 + 0: CSS_COMPOUND_SELECTOR@88..106 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@88..106 + 0: CSS_CLASS_SELECTOR@88..106 + 0: DOT@88..91 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@91..106 + 0: IDENT@91..106 "otherClassName" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@106..149 + 0: L_CURLY@106..107 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@107..147 + 0: CSS_DECLARATION_WITH_SEMICOLON@107..147 + 0: CSS_DECLARATION@107..146 + 0: CSS_COMPOSES_PROPERTY@107..146 + 0: CSS_IDENTIFIER@107..117 + 0: IDENT@107..117 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@117..119 ":" [] [Whitespace(" ")] + 2: CSS_COMPOSES_PROPERTY_VALUE@119..146 + 0: CSS_COMPOSES_CLASS_LIST@119..135 + 0: CSS_CUSTOM_IDENTIFIER@119..135 + 0: IDENT@119..135 "globalClassName" [] [Whitespace(" ")] + 1: CSS_COMPOSES_IMPORT_SPECIFIER@135..146 + 0: FROM_KW@135..140 "from" [] [Whitespace(" ")] + 1: CSS_IDENTIFIER@140..146 + 0: IDENT@140..146 "global" [] [] + 1: (empty) + 1: SEMICOLON@146..147 ";" [] [] + 2: R_CURLY@147..149 "}" [Newline("\n")] [] + 3: CSS_QUALIFIED_RULE@149..191 + 0: CSS_SELECTOR_LIST@149..154 + 0: CSS_COMPOUND_SELECTOR@149..154 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@149..154 + 0: CSS_CLASS_SELECTOR@149..154 + 0: DOT@149..152 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@152..154 + 0: IDENT@152..154 "b" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@154..191 + 0: L_CURLY@154..155 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@155..189 + 0: CSS_DECLARATION_WITH_SEMICOLON@155..189 + 0: CSS_DECLARATION@155..188 + 0: CSS_COMPOSES_PROPERTY@155..188 + 0: CSS_IDENTIFIER@155..165 + 0: IDENT@155..165 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@165..167 ":" [] [Whitespace(" ")] + 2: CSS_COMPOSES_PROPERTY_VALUE@167..188 + 0: CSS_COMPOSES_CLASS_LIST@167..188 + 0: CSS_CUSTOM_IDENTIFIER@167..178 + 0: IDENT@167..178 "classNameA" [] [Whitespace(" ")] + 1: CSS_CUSTOM_IDENTIFIER@178..188 + 0: IDENT@178..188 "classNameB" [] [] + 1: (empty) + 1: (empty) + 1: SEMICOLON@188..189 ";" [] [] + 2: R_CURLY@189..191 "}" [Newline("\n")] [] + 4: CSS_QUALIFIED_RULE@191..253 + 0: CSS_SELECTOR_LIST@191..196 + 0: CSS_COMPOUND_SELECTOR@191..196 + 0: (empty) + 1: (empty) + 2: CSS_SUB_SELECTOR_LIST@191..196 + 0: CSS_CLASS_SELECTOR@191..196 + 0: DOT@191..194 "." [Newline("\n"), Newline("\n")] [] + 1: CSS_CUSTOM_IDENTIFIER@194..196 + 0: IDENT@194..196 "c" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@196..253 + 0: L_CURLY@196..197 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@197..251 + 0: CSS_DECLARATION_WITH_SEMICOLON@197..251 + 0: CSS_DECLARATION@197..250 + 0: CSS_COMPOSES_PROPERTY@197..250 + 0: CSS_IDENTIFIER@197..207 + 0: IDENT@197..207 "composes" [Newline("\n"), Whitespace("\t")] [] + 1: COLON@207..209 ":" [] [Whitespace(" ")] + 2: CSS_COMPOSES_PROPERTY_VALUE@209..250 + 0: CSS_COMPOSES_CLASS_LIST@209..231 + 0: CSS_CUSTOM_IDENTIFIER@209..220 + 0: IDENT@209..220 "classNameA" [] [Whitespace(" ")] + 1: CSS_CUSTOM_IDENTIFIER@220..231 + 0: IDENT@220..231 "classNameB" [] [Whitespace(" ")] + 1: CSS_COMPOSES_IMPORT_SPECIFIER@231..250 + 0: FROM_KW@231..236 "from" [] [Whitespace(" ")] + 1: CSS_STRING@236..250 + 0: CSS_STRING_LITERAL@236..250 "'./module.css'" [] [] + 1: (empty) + 1: SEMICOLON@250..251 ";" [] [] + 2: R_CURLY@251..253 "}" [Newline("\n")] [] + 2: EOF@253..254 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/property/composes/options.json b/crates/biome_css_parser/tests/css_test_suite/ok/property/composes/options.json new file mode 100644 index 000000000000..f789463bbcbd --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/property/composes/options.json @@ -0,0 +1,8 @@ +{ + "$schema": "../../../../../../../packages/@biomejs/biome/configuration_schema.json", + "css": { + "parser": { + "cssModules": true + } + } +} diff --git a/crates/biome_css_parser/tests/spec_test.rs b/crates/biome_css_parser/tests/spec_test.rs index cabf229eaa8e..b23746e221b1 100644 --- a/crates/biome_css_parser/tests/spec_test.rs +++ b/crates/biome_css_parser/tests/spec_test.rs @@ -174,7 +174,18 @@ pub fn run(test_case: &str, _snapshot_name: &str, test_directory: &str, outcome_ #[test] pub fn quick_test() { let code = r#" -@keyframes :local(a) {} +.a { + composes: myClass; +} + +.otherClassName { + composes: className from "./style.css"; +} + +.otherClassName { + composes: globalClassName from global; +} + "#; let root = parse_css( diff --git a/crates/biome_css_syntax/src/generated/kind.rs b/crates/biome_css_syntax/src/generated/kind.rs index f95903d335a6..a778634a4ac0 100644 --- a/crates/biome_css_syntax/src/generated/kind.rs +++ b/crates/biome_css_syntax/src/generated/kind.rs @@ -229,6 +229,7 @@ pub enum CssSyntaxKind { REGEXP_KW, VALUE_KW, AS_KW, + COMPOSES_KW, FONT_FACE_KW, CSS_STRING_LITERAL, CSS_NUMBER_LITERAL, @@ -274,6 +275,10 @@ pub enum CssSyntaxKind { CSS_GENERIC_COMPONENT_VALUE_LIST, CSS_GENERIC_DELIMITER, CSS_GENERIC_PROPERTY, + CSS_COMPOSES_PROPERTY, + CSS_COMPOSES_PROPERTY_VALUE, + CSS_COMPOSES_IMPORT_SPECIFIER, + CSS_COMPOSES_CLASS_LIST, CSS_UNKNOWN_PROPERTY_VALUE, CSS_PARAMETER_LIST, CSS_DECLARATION_IMPORTANT, @@ -495,6 +500,7 @@ impl CssSyntaxKind { | CSS_DECLARATION_LIST | CSS_COMPONENT_VALUE_LIST | CSS_GENERIC_COMPONENT_VALUE_LIST + | CSS_COMPOSES_CLASS_LIST | CSS_PARAMETER_LIST | CSS_ANY_SELECTOR_LIST | CSS_SUB_SELECTOR_LIST @@ -700,6 +706,7 @@ impl CssSyntaxKind { "regexp" => REGEXP_KW, "value" => VALUE_KW, "as" => AS_KW, + "composes" => COMPOSES_KW, "font-face" => FONT_FACE_KW, _ => return None, }; @@ -923,6 +930,7 @@ impl CssSyntaxKind { REGEXP_KW => "regexp", VALUE_KW => "value", AS_KW => "as", + COMPOSES_KW => "composes", FONT_FACE_KW => "font-face", CSS_STRING_LITERAL => "string literal", _ => return None, @@ -932,4 +940,4 @@ impl CssSyntaxKind { } #[doc = r" Utility macro for creating a SyntaxKind through simple macro syntax"] #[macro_export] -macro_rules ! T { [;] => { $ crate :: CssSyntaxKind :: SEMICOLON } ; [,] => { $ crate :: CssSyntaxKind :: COMMA } ; ['('] => { $ crate :: CssSyntaxKind :: L_PAREN } ; [')'] => { $ crate :: CssSyntaxKind :: R_PAREN } ; ['{'] => { $ crate :: CssSyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: CssSyntaxKind :: R_CURLY } ; ['['] => { $ crate :: CssSyntaxKind :: L_BRACK } ; [']'] => { $ crate :: CssSyntaxKind :: R_BRACK } ; [<] => { $ crate :: CssSyntaxKind :: L_ANGLE } ; [>] => { $ crate :: CssSyntaxKind :: R_ANGLE } ; [~] => { $ crate :: CssSyntaxKind :: TILDE } ; [#] => { $ crate :: CssSyntaxKind :: HASH } ; [&] => { $ crate :: CssSyntaxKind :: AMP } ; [|] => { $ crate :: CssSyntaxKind :: PIPE } ; [||] => { $ crate :: CssSyntaxKind :: PIPE2 } ; [+] => { $ crate :: CssSyntaxKind :: PLUS } ; [*] => { $ crate :: CssSyntaxKind :: STAR } ; [/] => { $ crate :: CssSyntaxKind :: SLASH } ; [^] => { $ crate :: CssSyntaxKind :: CARET } ; [%] => { $ crate :: CssSyntaxKind :: PERCENT } ; [.] => { $ crate :: CssSyntaxKind :: DOT } ; [:] => { $ crate :: CssSyntaxKind :: COLON } ; [::] => { $ crate :: CssSyntaxKind :: COLON2 } ; [=] => { $ crate :: CssSyntaxKind :: EQ } ; [!] => { $ crate :: CssSyntaxKind :: BANG } ; [!=] => { $ crate :: CssSyntaxKind :: NEQ } ; [-] => { $ crate :: CssSyntaxKind :: MINUS } ; [<=] => { $ crate :: CssSyntaxKind :: LTEQ } ; [>=] => { $ crate :: CssSyntaxKind :: GTEQ } ; [+=] => { $ crate :: CssSyntaxKind :: PLUSEQ } ; [|=] => { $ crate :: CssSyntaxKind :: PIPEEQ } ; [&=] => { $ crate :: CssSyntaxKind :: AMPEQ } ; [^=] => { $ crate :: CssSyntaxKind :: CARETEQ } ; [/=] => { $ crate :: CssSyntaxKind :: SLASHEQ } ; [*=] => { $ crate :: CssSyntaxKind :: STAREQ } ; [%=] => { $ crate :: CssSyntaxKind :: PERCENTEQ } ; [@] => { $ crate :: CssSyntaxKind :: AT } ; ["$="] => { $ crate :: CssSyntaxKind :: DOLLAR_EQ } ; [~=] => { $ crate :: CssSyntaxKind :: TILDE_EQ } ; [-->] => { $ crate :: CssSyntaxKind :: CDC } ; [] => { $ crate :: CssSyntaxKind :: CDC } ; [