From 40727ca604751723f80c151754472553927de70f Mon Sep 17 00:00:00 2001 From: Denis Bezrukov <6227442+denbezrukov@users.noreply.github.com> Date: Sun, 7 Jul 2024 16:13:28 +0300 Subject: [PATCH] fix(css_parser): fix the CSS parser doesn't support nested selectors with pseudo-classes #3287 (#3318) --- CHANGELOG.md | 4 + .../css/comments/custom-properties.css.snap | 116 +++--- .../prettier/css/comments/selectors.css.snap | 92 +++-- .../postcss-nested-props.css.snap | 100 ++--- .../css/variables/apply-rule.css.snap | 375 +++++++++++------- crates/biome_css_parser/src/parser.rs | 1 - .../block/declaration_or_rule_list_block.rs | 64 ++- .../biome_css_parser/src/syntax/block/mod.rs | 2 +- .../css_test_suite/ok/nesting/nesting_1.css | 10 + .../ok/nesting/nesting_1.css.snap | 272 +++++++++++++ crates/biome_css_parser/tests/spec_test.rs | 5 +- 11 files changed, 767 insertions(+), 274 deletions(-) create mode 100644 crates/biome_css_parser/tests/css_test_suite/ok/nesting/nesting_1.css create mode 100644 crates/biome_css_parser/tests/css_test_suite/ok/nesting/nesting_1.css.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d9cab96eea7..64381784d5da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,10 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b ### Parser +#### Bug fixes + +- Fix [#3287](https://github.com/biomejs/biome/issues/3287) nested selectors with pseudo-classes. Contributed by @denbezrukov + ## v1.8.3 (2024-06-27) ### CLI diff --git a/crates/biome_css_formatter/tests/specs/prettier/css/comments/custom-properties.css.snap b/crates/biome_css_formatter/tests/specs/prettier/css/comments/custom-properties.css.snap index 6d716c4ebe7c..3a4a4d341ace 100644 --- a/crates/biome_css_formatter/tests/specs/prettier/css/comments/custom-properties.css.snap +++ b/crates/biome_css_formatter/tests/specs/prettier/css/comments/custom-properties.css.snap @@ -2,7 +2,6 @@ source: crates/biome_formatter_test/src/snapshot_builder.rs info: css/comments/custom-properties.css --- - # Input ```css @@ -33,20 +32,16 @@ font-size: 12px; :root { /* comment 2 */ - --prop: { -- /* comment 3 */ ++ --prop : { + /* comment 3 */ - color/* comment 4 */: /* comment 5 */ #fff /* comment 6 */; /* comment 7 */ -- /* comment 8 */ -- font-size: 12px; -- /* comment 9 */ ++ /* comment 4 */ /* comment 5 */ color: #fff /* comment 6 */; /* comment 7 */ + /* comment 8 */ + font-size: 12px; + /* comment 9 */ - }; -+ --prop : { -+ /* comment 3 */ -+color/* comment 4 */: /* comment 5 */#fff /* comment 6 */; /* comment 7 */ -+ /* comment 8 */ -+ font-size: 12px; -+ /* comment 9 */ -+} -+; ++ } ++ ; /* comment 10 */ } /* comment 11 */ @@ -59,13 +54,13 @@ font-size: 12px; :root { /* comment 2 */ --prop : { - /* comment 3 */ -color/* comment 4 */: /* comment 5 */#fff /* comment 6 */; /* comment 7 */ - /* comment 8 */ - font-size: 12px; - /* comment 9 */ -} -; + /* comment 3 */ + /* comment 4 */ /* comment 5 */ color: #fff /* comment 6 */; /* comment 7 */ + /* comment 8 */ + font-size: 12px; + /* comment 9 */ + } + ; /* comment 10 */ } /* comment 11 */ @@ -81,51 +76,76 @@ custom-properties.css:4:12 parse ━━━━━━━━━━━━━━━ 3 │ /* comment 2 */ > 4 │ --prop : { │ ^ - > 5 │ /* comment 3 */ - > 6 │ color/* comment 4 */: /* comment 5 */#fff/* comment 6 */;/* comment 7 */ - │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 7 │ /* comment 8 */ - 8 │ font-size: 12px; + 5 │ /* comment 3 */ + 6 │ color/* comment 4 */: /* comment 5 */#fff/* comment 6 */;/* comment 7 */ i Expected one of: - - identifier - - string - - number - - dimension - - ratio - - custom property - - function + - hover + - focus + - active + - first-child + - last-child + - nth-child + - nth-last-child + - first-of-type + - last-of-type + - nth-of-type + - nth-last-of-type + - only-child + - only-of-type + - checked + - disabled + - enabled + - required + - optional + - valid + - invalid + - in-range + - out-of-range + - read-only + - read-write + - placeholder-shown + - default + - checked + - indeterminate + - blank + - empty + - root + - target + - lang + - not + - is + - where + - fullscreen + - link + - visited + - any-link + - local-link + - scope + - current + - past + - future custom-properties.css:10:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × Expected a qualified rule, or an at rule but instead found '; - /* comment 10 */ - }'. + × Expected a declaration, or an at rule but instead found ';'. 8 │ font-size: 12px; 9 │ /* comment 9 */ > 10 │ }; │ ^ - > 11 │ /* comment 10 */ - > 12 │ } - │ ^ - 13 │ /* comment 11 */ - 14 │ + 11 │ /* comment 10 */ + 12 │ } - i Expected a qualified rule, or an at rule here. + i Expected a declaration, or an at rule here. 8 │ font-size: 12px; 9 │ /* comment 9 */ > 10 │ }; │ ^ - > 11 │ /* comment 10 */ - > 12 │ } - │ ^ - 13 │ /* comment 11 */ - 14 │ + 11 │ /* comment 10 */ + 12 │ } ``` - - diff --git a/crates/biome_css_formatter/tests/specs/prettier/css/comments/selectors.css.snap b/crates/biome_css_formatter/tests/specs/prettier/css/comments/selectors.css.snap index 9ae5f4001b02..4fc67ba10979 100644 --- a/crates/biome_css_formatter/tests/specs/prettier/css/comments/selectors.css.snap +++ b/crates/biome_css_formatter/tests/specs/prettier/css/comments/selectors.css.snap @@ -476,16 +476,13 @@ input:not(/* comment 125 */[/* comment 126 */disabled/* comment 127 */]/* commen :root { - /* comments 192 */ - --centered/* comments 193 */ : /* comments 194 */ { -- display: flex; -- align-items: center; -- justify-content: center; -- }; + /* comments 192 */ --centered /* comments 193 */ : /* comments 194 */ { -+ display: flex; -+ align-items: center; -+ justify-content: center; -+} -+; + display: flex; + align-items: center; + justify-content: center; +- }; ++ } ++ ; } ``` @@ -708,11 +705,11 @@ input:not( /* custom properties set & @apply rule */ :root { /* comments 192 */ --centered /* comments 193 */ : /* comments 194 */ { - display: flex; - align-items: center; - justify-content: center; -} -; + display: flex; + align-items: center; + justify-content: center; + } + ; } ``` @@ -806,42 +803,75 @@ selectors.css:152:75 parse ━━━━━━━━━━━━━━━━━ 151 │ :root { > 152 │ /* comments 192 */ --centered /* comments 193 */ : /* comments 194 */ { │ ^ - > 153 │ display: flex; - │ ^^^^^^^^^^^^^ + 153 │ display: flex; 154 │ align-items: center; - 155 │ justify-content: center; i Expected one of: - - identifier - - string - - number - - dimension - - ratio - - custom property - - function + - hover + - focus + - active + - first-child + - last-child + - nth-child + - nth-last-child + - first-of-type + - last-of-type + - nth-of-type + - nth-last-of-type + - only-child + - only-of-type + - checked + - disabled + - enabled + - required + - optional + - valid + - invalid + - in-range + - out-of-range + - read-only + - read-write + - placeholder-shown + - default + - checked + - indeterminate + - blank + - empty + - root + - target + - lang + - not + - is + - where + - fullscreen + - link + - visited + - any-link + - local-link + - scope + - current + - past + - future selectors.css:156:6 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × Expected a qualified rule, or an at rule but instead found '; - }'. + × Expected a declaration, or an at rule but instead found ';'. 154 │ align-items: center; 155 │ justify-content: center; > 156 │ }; │ ^ - > 157 │ } - │ ^ + 157 │ } 158 │ - i Expected a qualified rule, or an at rule here. + i Expected a declaration, or an at rule here. 154 │ align-items: center; 155 │ justify-content: center; > 156 │ }; │ ^ - > 157 │ } - │ ^ + 157 │ } 158 │ diff --git a/crates/biome_css_formatter/tests/specs/prettier/css/postcss-plugins/postcss-nested-props.css.snap b/crates/biome_css_formatter/tests/specs/prettier/css/postcss-plugins/postcss-nested-props.css.snap index 352156dfb4be..9bc89acd4536 100644 --- a/crates/biome_css_formatter/tests/specs/prettier/css/postcss-plugins/postcss-nested-props.css.snap +++ b/crates/biome_css_formatter/tests/specs/prettier/css/postcss-plugins/postcss-nested-props.css.snap @@ -2,7 +2,6 @@ source: crates/biome_formatter_test/src/snapshot_builder.rs info: css/postcss-plugins/postcss-nested-props.css --- - # Input ```css @@ -28,18 +27,7 @@ info: css/postcss-plugins/postcss-nested-props.css ```diff --- Prettier +++ Biome -@@ -1,13 +1,13 @@ - .funky { - font: { -- family: fantasy; -- size: 30em; -- weight: bold; -- } -+ family: fantasy; -+ size: 30em; -+ weight: bold; -+} - } +@@ -8,6 +8,6 @@ .funky { font: 20px/24px fantasy { @@ -55,10 +43,10 @@ info: css/postcss-plugins/postcss-nested-props.css ```css .funky { font: { - family: fantasy; - size: 30em; - weight: bold; -} + family: fantasy; + size: 30em; + weight: bold; + } } .funky { @@ -77,40 +65,56 @@ postcss-nested-props.css:2:11 parse ━━━━━━━━━━━━━━ 1 │ .funky { > 2 │ font: { │ ^ - > 3 │ family: fantasy; - │ ^^^^^^^^^^^^^^^ + 3 │ family: fantasy; 4 │ size: 30em; - 5 │ weight: bold; i Expected one of: - - identifier - - string - - number - - dimension - - ratio - - custom property - - function - -postcss-nested-props.css:7:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Expected a qualified rule, or an at rule but instead found '}'. - - 5 │ weight: bold; - 6 │ } - > 7 │ } - │ ^ - 8 │ - 9 │ .funky { - - i Expected a qualified rule, or an at rule here. - - 5 │ weight: bold; - 6 │ } - > 7 │ } - │ ^ - 8 │ - 9 │ .funky { + - hover + - focus + - active + - first-child + - last-child + - nth-child + - nth-last-child + - first-of-type + - last-of-type + - nth-of-type + - nth-last-of-type + - only-child + - only-of-type + - checked + - disabled + - enabled + - required + - optional + - valid + - invalid + - in-range + - out-of-range + - read-only + - read-write + - placeholder-shown + - default + - checked + - indeterminate + - blank + - empty + - root + - target + - lang + - not + - is + - where + - fullscreen + - link + - visited + - any-link + - local-link + - scope + - current + - past + - future postcss-nested-props.css:10:29 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -154,5 +158,3 @@ postcss-nested-props.css:13:1 parse ━━━━━━━━━━━━━━ ``` - - diff --git a/crates/biome_css_formatter/tests/specs/prettier/css/variables/apply-rule.css.snap b/crates/biome_css_formatter/tests/specs/prettier/css/variables/apply-rule.css.snap index f5ada1d549d6..710ae4f8d5b2 100644 --- a/crates/biome_css_formatter/tests/specs/prettier/css/variables/apply-rule.css.snap +++ b/crates/biome_css_formatter/tests/specs/prettier/css/variables/apply-rule.css.snap @@ -44,56 +44,46 @@ info: css/variables/apply-rule.css ```diff --- Prettier +++ Biome -@@ -3,30 +3,35 @@ - :root { - --toolbar-theme: { +@@ -5,28 +5,33 @@ background-color: hsl(120, 70%, 95%); -- border-radius: 4px; -- border: 1px solid var(--theme-color late); + border-radius: 4px; + border: 1px solid var(--theme-color late); - }; -- --toolbar-title-theme: { -- color: green; ++ } ++ ; + --toolbar-title-theme: { + color: green; - }; -+ border-radius: 4px; -+ border: 1px solid var(--theme-color late); -+} -+; -+--toolbar-title-theme: { -+ color: green; -+} -+; ++ } ++ ; } :root { -- --without-semi: { -- color: red; + --without-semi: { + color: red; - }; -+ --without-semi: {color:red; -+} ++ } } :root { --like-a-apply-rule: { - color:red;} /* no semi here*/ -- --another-prop: blue; -+ color:red; -+} /* no semi here*/ -+--another-prop:blue -+; ++ color: red; ++ } /* no semi here*/ + --another-prop: blue; } :root { --like-a-apply-rule: { - color:red;} /* no semi here*/ -- --another-one-like-a-apply-rule: { ++ color: red; ++ } /* no semi here*/ + --another-one-like-a-apply-rule: { - color:red; - }; -+ color:red; -+} /* no semi here*/ -+--another-one-like-a-apply-rule: { -+ color: red; -+} -+; ++ color: red; ++ } ++ ; } ``` @@ -105,37 +95,37 @@ info: css/variables/apply-rule.css :root { --toolbar-theme: { background-color: hsl(120, 70%, 95%); - border-radius: 4px; - border: 1px solid var(--theme-color late); -} -; ---toolbar-title-theme: { - color: green; -} -; + border-radius: 4px; + border: 1px solid var(--theme-color late); + } + ; + --toolbar-title-theme: { + color: green; + } + ; } :root { - --without-semi: {color:red; -} + --without-semi: { + color: red; + } } :root { --like-a-apply-rule: { - color:red; -} /* no semi here*/ ---another-prop:blue -; + color: red; + } /* no semi here*/ + --another-prop: blue; } :root { --like-a-apply-rule: { - color:red; -} /* no semi here*/ ---another-one-like-a-apply-rule: { - color: red; -} -; + color: red; + } /* no semi here*/ + --another-one-like-a-apply-rule: { + color: red; + } + ; } ``` @@ -148,24 +138,60 @@ apply-rule.css:4:20 parse ━━━━━━━━━━━━━━━━━━ 3 │ :root { > 4 │ --toolbar-theme: { │ ^ - > 5 │ background-color: hsl(120, 70%, 95%); - │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 5 │ background-color: hsl(120, 70%, 95%); 6 │ border-radius: 4px; - 7 │ border: 1px solid var(--theme-color late); i Expected one of: - - identifier - - string - - number - - dimension - - ratio - - custom property - - function + - hover + - focus + - active + - first-child + - last-child + - nth-child + - nth-last-child + - first-of-type + - last-of-type + - nth-of-type + - nth-last-of-type + - only-child + - only-of-type + - checked + - disabled + - enabled + - required + - optional + - valid + - invalid + - in-range + - out-of-range + - read-only + - read-write + - placeholder-shown + - default + - checked + - indeterminate + - blank + - empty + - root + - target + - lang + - not + - is + - where + - fullscreen + - link + - visited + - any-link + - local-link + - scope + - current + - past + - future apply-rule.css:8:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × Expected a qualified rule, or an at rule but instead found ';'. + × Expected a declaration, or an at rule but instead found ';'. 6 │ border-radius: 4px; 7 │ border: 1px solid var(--theme-color late); @@ -174,7 +200,7 @@ apply-rule.css:8:4 parse ━━━━━━━━━━━━━━━━━━ 9 │ --toolbar-title-theme: { 10 │ color: green; - i Expected a qualified rule, or an at rule here. + i Expected a declaration, or an at rule here. 6 │ border-radius: 4px; 7 │ border: 1px solid var(--theme-color late); @@ -244,28 +270,23 @@ apply-rule.css:9:26 parse ━━━━━━━━━━━━━━━━━━ apply-rule.css:11:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × Expected a qualified rule, or an at rule but instead found '; - }'. + × Expected a declaration, or an at rule but instead found ';'. 9 │ --toolbar-title-theme: { 10 │ color: green; > 11 │ }; │ ^ - > 12 │ } - │ ^ + 12 │ } 13 │ - 14 │ :root { - i Expected a qualified rule, or an at rule here. + i Expected a declaration, or an at rule here. 9 │ --toolbar-title-theme: { 10 │ color: green; > 11 │ }; │ ^ - > 12 │ } - │ ^ + 12 │ } 13 │ - 14 │ :root { apply-rule.css:15:19 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -273,39 +294,57 @@ apply-rule.css:15:19 parse ━━━━━━━━━━━━━━━━━ 14 │ :root { > 15 │ --without-semi: {color:red;} - │ ^^^^^^^^^^ + │ ^ 16 │ } 17 │ i Expected one of: - - identifier - - string - - number - - dimension - - ratio - - custom property - - function - -apply-rule.css:16:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Expected a qualified rule, or an at rule but instead found '}'. - - 14 │ :root { - 15 │ --without-semi: {color:red;} - > 16 │ } - │ ^ - 17 │ - 18 │ :root { - - i Expected a qualified rule, or an at rule here. - - 14 │ :root { - 15 │ --without-semi: {color:red;} - > 16 │ } - │ ^ - 17 │ - 18 │ :root { + - hover + - focus + - active + - first-child + - last-child + - nth-child + - nth-last-child + - first-of-type + - last-of-type + - nth-of-type + - nth-last-of-type + - only-child + - only-of-type + - checked + - disabled + - enabled + - required + - optional + - valid + - invalid + - in-range + - out-of-range + - read-only + - read-write + - placeholder-shown + - default + - checked + - indeterminate + - blank + - empty + - root + - target + - lang + - not + - is + - where + - fullscreen + - link + - visited + - any-link + - local-link + - scope + - current + - past + - future apply-rule.css:19:24 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -314,33 +353,56 @@ apply-rule.css:19:24 parse ━━━━━━━━━━━━━━━━━ 18 │ :root { > 19 │ --like-a-apply-rule: { │ ^ - > 20 │ color:red;} /* no semi here*/ - │ ^^^^^^^^^ + 20 │ color:red;} /* no semi here*/ 21 │ --another-prop: blue; - 22 │ } i Expected one of: - - identifier - - string - - number - - dimension - - ratio - - custom property - - function - -apply-rule.css:21:23 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × expected `,` but instead found `;` - - 19 │ --like-a-apply-rule: { - 20 │ color:red;} /* no semi here*/ - > 21 │ --another-prop: blue; - │ ^ - 22 │ } - 23 │ - - i Remove ; + - hover + - focus + - active + - first-child + - last-child + - nth-child + - nth-last-child + - first-of-type + - last-of-type + - nth-of-type + - nth-last-of-type + - only-child + - only-of-type + - checked + - disabled + - enabled + - required + - optional + - valid + - invalid + - in-range + - out-of-range + - read-only + - read-write + - placeholder-shown + - default + - checked + - indeterminate + - blank + - empty + - root + - target + - lang + - not + - is + - where + - fullscreen + - link + - visited + - any-link + - local-link + - scope + - current + - past + - future apply-rule.css:25:24 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -349,20 +411,56 @@ apply-rule.css:25:24 parse ━━━━━━━━━━━━━━━━━ 24 │ :root { > 25 │ --like-a-apply-rule: { │ ^ - > 26 │ color:red;} /* no semi here*/ - │ ^^^^^^^^^ + 26 │ color:red;} /* no semi here*/ 27 │ --another-one-like-a-apply-rule: { - 28 │ color:red; i Expected one of: - - identifier - - string - - number - - dimension - - ratio - - custom property - - function + - hover + - focus + - active + - first-child + - last-child + - nth-child + - nth-last-child + - first-of-type + - last-of-type + - nth-of-type + - nth-last-of-type + - only-child + - only-of-type + - checked + - disabled + - enabled + - required + - optional + - valid + - invalid + - in-range + - out-of-range + - read-only + - read-write + - placeholder-shown + - default + - checked + - indeterminate + - blank + - empty + - root + - target + - lang + - not + - is + - where + - fullscreen + - link + - visited + - any-link + - local-link + - scope + - current + - past + - future apply-rule.css:27:36 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -425,25 +523,22 @@ apply-rule.css:27:36 parse ━━━━━━━━━━━━━━━━━ apply-rule.css:29:4 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × Expected a qualified rule, or an at rule but instead found '; - }'. + × Expected a declaration, or an at rule but instead found ';'. 27 │ --another-one-like-a-apply-rule: { 28 │ color:red; > 29 │ }; │ ^ - > 30 │ } - │ ^ + 30 │ } 31 │ - i Expected a qualified rule, or an at rule here. + i Expected a declaration, or an at rule here. 27 │ --another-one-like-a-apply-rule: { 28 │ color:red; > 29 │ }; │ ^ - > 30 │ } - │ ^ + 30 │ } 31 │ diff --git a/crates/biome_css_parser/src/parser.rs b/crates/biome_css_parser/src/parser.rs index 8ac7c9d81e61..8c48b98ee25e 100644 --- a/crates/biome_css_parser/src/parser.rs +++ b/crates/biome_css_parser/src/parser.rs @@ -70,7 +70,6 @@ impl<'source> CssParser<'source> { self.source_mut().re_lex(context) } - #[allow(dead_code)] //TODO remove this allow once we actually use it pub(crate) fn state(&self) -> &CssParserState { &self.state } diff --git a/crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs b/crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs index 679ee3a7fd64..b561bfdc7722 100644 --- a/crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs +++ b/crates/biome_css_parser/src/syntax/block/declaration_or_rule_list_block.rs @@ -4,7 +4,7 @@ use crate::syntax::block::ParseBlockBody; use crate::syntax::parse_error::expected_any_declaration_or_at_rule; use crate::syntax::{ is_at_declaration, is_at_nested_qualified_rule, parse_declaration_with_semicolon, - parse_nested_qualified_rule, + parse_nested_qualified_rule, try_parse, }; use biome_css_syntax::CssSyntaxKind::*; use biome_css_syntax::{CssSyntaxKind, T}; @@ -59,6 +59,68 @@ impl ParseNodeList for DeclarationOrRuleList { if is_at_at_rule(p) { parse_at_rule(p) } else if is_at_declaration(p) { + // if we are at a declaration, + // we still can have a nested qualified rule or a declaration + // E.g. + // main { + // label:hover { <--- + // it looks like a declaration but it is a nested qualified rule + // font-weight: 500; + // } + // } + // Attempt to parse the current block as a declaration. + let declaration = try_parse(p, |p| { + let declaration = parse_declaration_with_semicolon(p); + // Check if the *last* token parsed is a semicolon + // (;) or if the parser is at a closing brace (}). + // ; - Indicates the end of a declaration. + // Indicates the end of the last declaration because `;` is optional. + // E.g + // .class { + // color: red; <---- + // The semicolon indicates the end of the declaration. + // font-size: 16px + // } <--- + // The closing brace indicates the end of the declaration block. + // If either condition is true, the declaration is considered valid. + if matches!(p.last(), Some(T![;])) || p.at(T!['}']) { + Ok(declaration) + } else { + // If neither condition is met, return an error to indicate parsing failure. + // And rewind the parser to the start of the declaration. + Err(()) + } + }); + + // If parsing as a declaration was successful, return the parsed declaration. + if let Ok(declaration) = declaration { + return declaration; + } + + // If parsing as a declaration failed, + // attempt to parse the current block as a nested qualified rule. + let rule = try_parse(p, |p| { + // Parse the block as a nested qualified rule. + let rule = parse_nested_qualified_rule(p); + // Check if the *last* token parsed is a closing brace (}). + // Indicates the end of a rule block. + // If true, the nested qualified rule is considered valid. + if matches!(p.last(), Some(T!['}'])) { + Ok(rule) + } else { + // If the condition is not met, return an error to indicate parsing failure. + Err(()) + } + }); + + // If parsing as a nested qualified rule was successful, return the parsed rule. + if let Ok(rule) = rule { + return rule; + } + + // If both parsing attempts fail, + // fall back to parsing the block as a declaration, + // because declaration error is more relevant. parse_declaration_with_semicolon(p) } else if is_at_nested_qualified_rule(p) { parse_nested_qualified_rule(p) diff --git a/crates/biome_css_parser/src/syntax/block/mod.rs b/crates/biome_css_parser/src/syntax/block/mod.rs index dd00a25632d5..0642a934d391 100644 --- a/crates/biome_css_parser/src/syntax/block/mod.rs +++ b/crates/biome_css_parser/src/syntax/block/mod.rs @@ -37,7 +37,7 @@ pub(crate) trait ParseBlockBody { let is_open_brace_missing = !p.expect(T!['{']); - if is_open_brace_missing && !self.is_at_element(p) { + if is_open_brace_missing && (!self.is_at_element(p) || p.state().speculative_parsing) { p.error(expected_block(p, p.cur_range())); return m.complete(p, CSS_BOGUS_BLOCK); } diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/nesting/nesting_1.css b/crates/biome_css_parser/tests/css_test_suite/ok/nesting/nesting_1.css new file mode 100644 index 000000000000..3dcbe7ad3913 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/nesting/nesting_1.css @@ -0,0 +1,10 @@ + +main { + label:hover { + font-weight: 500; + } + + a:link, a:visited { + color: inherit; + } +} diff --git a/crates/biome_css_parser/tests/css_test_suite/ok/nesting/nesting_1.css.snap b/crates/biome_css_parser/tests/css_test_suite/ok/nesting/nesting_1.css.snap new file mode 100644 index 000000000000..c39eedde7a63 --- /dev/null +++ b/crates/biome_css_parser/tests/css_test_suite/ok/nesting/nesting_1.css.snap @@ -0,0 +1,272 @@ +--- +source: crates/biome_css_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```css + +main { + label:hover { + font-weight: 500; + } + + a:link, a:visited { + color: inherit; + } +} + +``` + + +## AST + +``` +CssRoot { + bom_token: missing (optional), + rules: CssRuleList [ + CssQualifiedRule { + prelude: CssSelectorList [ + CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: CssTypeSelector { + namespace: missing (optional), + ident: CssIdentifier { + value_token: IDENT@0..6 "main" [Newline("\n")] [Whitespace(" ")], + }, + }, + sub_selectors: CssSubSelectorList [], + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@6..7 "{" [] [], + items: CssDeclarationOrRuleList [ + CssNestedQualifiedRule { + prelude: CssRelativeSelectorList [ + CssRelativeSelector { + combinator: missing (optional), + selector: CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: CssTypeSelector { + namespace: missing (optional), + ident: CssIdentifier { + value_token: IDENT@7..14 "label" [Newline("\n"), Whitespace("\t")] [], + }, + }, + sub_selectors: CssSubSelectorList [ + CssPseudoClassSelector { + colon_token: COLON@14..15 ":" [] [], + class: CssPseudoClassIdentifier { + name: CssIdentifier { + value_token: IDENT@15..21 "hover" [] [Whitespace(" ")], + }, + }, + }, + ], + }, + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@21..22 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@22..36 "font-weight" [Newline("\n"), Whitespace("\t\t")] [], + }, + colon_token: COLON@36..38 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssNumber { + value_token: CSS_NUMBER_LITERAL@38..41 "500" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@41..42 ";" [] [], + }, + ], + r_curly_token: R_CURLY@42..45 "}" [Newline("\n"), Whitespace("\t")] [], + }, + }, + CssNestedQualifiedRule { + prelude: CssRelativeSelectorList [ + CssRelativeSelector { + combinator: missing (optional), + selector: CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: CssTypeSelector { + namespace: missing (optional), + ident: CssIdentifier { + value_token: IDENT@45..49 "a" [Newline("\n"), Newline("\n"), Whitespace("\t")] [], + }, + }, + sub_selectors: CssSubSelectorList [ + CssPseudoClassSelector { + colon_token: COLON@49..50 ":" [] [], + class: CssPseudoClassIdentifier { + name: CssIdentifier { + value_token: IDENT@50..54 "link" [] [], + }, + }, + }, + ], + }, + }, + COMMA@54..56 "," [] [Whitespace(" ")], + CssRelativeSelector { + combinator: missing (optional), + selector: CssCompoundSelector { + nesting_selector_token: missing (optional), + simple_selector: CssTypeSelector { + namespace: missing (optional), + ident: CssIdentifier { + value_token: IDENT@56..57 "a" [] [], + }, + }, + sub_selectors: CssSubSelectorList [ + CssPseudoClassSelector { + colon_token: COLON@57..58 ":" [] [], + class: CssPseudoClassIdentifier { + name: CssIdentifier { + value_token: IDENT@58..66 "visited" [] [Whitespace(" ")], + }, + }, + }, + ], + }, + }, + ], + block: CssDeclarationOrRuleBlock { + l_curly_token: L_CURLY@66..67 "{" [] [], + items: CssDeclarationOrRuleList [ + CssDeclarationWithSemicolon { + declaration: CssDeclaration { + property: CssGenericProperty { + name: CssIdentifier { + value_token: IDENT@67..75 "color" [Newline("\n"), Whitespace("\t\t")] [], + }, + colon_token: COLON@75..77 ":" [] [Whitespace(" ")], + value: CssGenericComponentValueList [ + CssIdentifier { + value_token: IDENT@77..84 "inherit" [] [], + }, + ], + }, + important: missing (optional), + }, + semicolon_token: SEMICOLON@84..85 ";" [] [], + }, + ], + r_curly_token: R_CURLY@85..88 "}" [Newline("\n"), Whitespace("\t")] [], + }, + }, + ], + r_curly_token: R_CURLY@88..90 "}" [Newline("\n")] [], + }, + }, + ], + eof_token: EOF@90..91 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: CSS_ROOT@0..91 + 0: (empty) + 1: CSS_RULE_LIST@0..90 + 0: CSS_QUALIFIED_RULE@0..90 + 0: CSS_SELECTOR_LIST@0..6 + 0: CSS_COMPOUND_SELECTOR@0..6 + 0: (empty) + 1: CSS_TYPE_SELECTOR@0..6 + 0: (empty) + 1: CSS_IDENTIFIER@0..6 + 0: IDENT@0..6 "main" [Newline("\n")] [Whitespace(" ")] + 2: CSS_SUB_SELECTOR_LIST@6..6 + 1: CSS_DECLARATION_OR_RULE_BLOCK@6..90 + 0: L_CURLY@6..7 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@7..88 + 0: CSS_NESTED_QUALIFIED_RULE@7..45 + 0: CSS_RELATIVE_SELECTOR_LIST@7..21 + 0: CSS_RELATIVE_SELECTOR@7..21 + 0: (empty) + 1: CSS_COMPOUND_SELECTOR@7..21 + 0: (empty) + 1: CSS_TYPE_SELECTOR@7..14 + 0: (empty) + 1: CSS_IDENTIFIER@7..14 + 0: IDENT@7..14 "label" [Newline("\n"), Whitespace("\t")] [] + 2: CSS_SUB_SELECTOR_LIST@14..21 + 0: CSS_PSEUDO_CLASS_SELECTOR@14..21 + 0: COLON@14..15 ":" [] [] + 1: CSS_PSEUDO_CLASS_IDENTIFIER@15..21 + 0: CSS_IDENTIFIER@15..21 + 0: IDENT@15..21 "hover" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@21..45 + 0: L_CURLY@21..22 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@22..42 + 0: CSS_DECLARATION_WITH_SEMICOLON@22..42 + 0: CSS_DECLARATION@22..41 + 0: CSS_GENERIC_PROPERTY@22..41 + 0: CSS_IDENTIFIER@22..36 + 0: IDENT@22..36 "font-weight" [Newline("\n"), Whitespace("\t\t")] [] + 1: COLON@36..38 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@38..41 + 0: CSS_NUMBER@38..41 + 0: CSS_NUMBER_LITERAL@38..41 "500" [] [] + 1: (empty) + 1: SEMICOLON@41..42 ";" [] [] + 2: R_CURLY@42..45 "}" [Newline("\n"), Whitespace("\t")] [] + 1: CSS_NESTED_QUALIFIED_RULE@45..88 + 0: CSS_RELATIVE_SELECTOR_LIST@45..66 + 0: CSS_RELATIVE_SELECTOR@45..54 + 0: (empty) + 1: CSS_COMPOUND_SELECTOR@45..54 + 0: (empty) + 1: CSS_TYPE_SELECTOR@45..49 + 0: (empty) + 1: CSS_IDENTIFIER@45..49 + 0: IDENT@45..49 "a" [Newline("\n"), Newline("\n"), Whitespace("\t")] [] + 2: CSS_SUB_SELECTOR_LIST@49..54 + 0: CSS_PSEUDO_CLASS_SELECTOR@49..54 + 0: COLON@49..50 ":" [] [] + 1: CSS_PSEUDO_CLASS_IDENTIFIER@50..54 + 0: CSS_IDENTIFIER@50..54 + 0: IDENT@50..54 "link" [] [] + 1: COMMA@54..56 "," [] [Whitespace(" ")] + 2: CSS_RELATIVE_SELECTOR@56..66 + 0: (empty) + 1: CSS_COMPOUND_SELECTOR@56..66 + 0: (empty) + 1: CSS_TYPE_SELECTOR@56..57 + 0: (empty) + 1: CSS_IDENTIFIER@56..57 + 0: IDENT@56..57 "a" [] [] + 2: CSS_SUB_SELECTOR_LIST@57..66 + 0: CSS_PSEUDO_CLASS_SELECTOR@57..66 + 0: COLON@57..58 ":" [] [] + 1: CSS_PSEUDO_CLASS_IDENTIFIER@58..66 + 0: CSS_IDENTIFIER@58..66 + 0: IDENT@58..66 "visited" [] [Whitespace(" ")] + 1: CSS_DECLARATION_OR_RULE_BLOCK@66..88 + 0: L_CURLY@66..67 "{" [] [] + 1: CSS_DECLARATION_OR_RULE_LIST@67..85 + 0: CSS_DECLARATION_WITH_SEMICOLON@67..85 + 0: CSS_DECLARATION@67..84 + 0: CSS_GENERIC_PROPERTY@67..84 + 0: CSS_IDENTIFIER@67..75 + 0: IDENT@67..75 "color" [Newline("\n"), Whitespace("\t\t")] [] + 1: COLON@75..77 ":" [] [Whitespace(" ")] + 2: CSS_GENERIC_COMPONENT_VALUE_LIST@77..84 + 0: CSS_IDENTIFIER@77..84 + 0: IDENT@77..84 "inherit" [] [] + 1: (empty) + 1: SEMICOLON@84..85 ";" [] [] + 2: R_CURLY@85..88 "}" [Newline("\n"), Whitespace("\t")] [] + 2: R_CURLY@88..90 "}" [Newline("\n")] [] + 2: EOF@90..91 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_css_parser/tests/spec_test.rs b/crates/biome_css_parser/tests/spec_test.rs index 2d71690fc7e4..c3d84252bffa 100644 --- a/crates/biome_css_parser/tests/spec_test.rs +++ b/crates/biome_css_parser/tests/spec_test.rs @@ -171,9 +171,8 @@ pub fn run(test_case: &str, _snapshot_name: &str, test_directory: &str, outcome_ #[test] pub fn quick_test() { let code = r#" -.formTable tbody td { - border-left: 1px # solid; -} + +.test { @color: red; color: @color; } "#; let root = parse_css(