Skip to content

Commit 8f073a7

Browse files
Pranav Acharematipico
andauthored
fix(css/parser): allow & as a trailing sub-selector in compound selectors (#10678)
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
1 parent 4396496 commit 8f073a7

9 files changed

Lines changed: 224 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#7718](https://github.com/biomejs/biome/issues/7718): Biome now correctly parses CSS nesting selectors when `&` appears as a trailing sub-selector after a type selector, e.g. `h1& { color: red; }`.

crates/biome_css_formatter/src/css/any/sub_selector.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ impl FormatRule<AnyCssSubSelector> for FormatAnyCssSubSelector {
1212
AnyCssSubSelector::CssBogusSubSelector(node) => node.format().fmt(f),
1313
AnyCssSubSelector::CssClassSelector(node) => node.format().fmt(f),
1414
AnyCssSubSelector::CssIdSelector(node) => node.format().fmt(f),
15+
AnyCssSubSelector::CssNestedSelector(node) => node.format().fmt(f),
1516
AnyCssSubSelector::CssPseudoClassSelector(node) => node.format().fmt(f),
1617
AnyCssSubSelector::CssPseudoElementSelector(node) => node.format().fmt(f),
1718
}

crates/biome_css_parser/src/syntax/selector/mod.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ use crate::syntax::scss::{
1616
parse_scss_selector_custom_identifier, parse_scss_selector_identifier,
1717
};
1818
use crate::syntax::selector::attribute::parse_attribute_selector;
19-
use crate::syntax::selector::nested_selector::NestedSelectorList;
19+
use crate::syntax::selector::nested_selector::{
20+
NestedSelectorList, parse_nested_selector_as_sub_selector,
21+
};
2022
use crate::syntax::selector::pseudo_class::parse_pseudo_class_selector;
2123
pub(crate) use crate::syntax::selector::pseudo_class::{
2224
PSEUDO_CLASS_NTH_SIGN_SET, PseudoValueList, is_at_pseudo_class_nth_argument,
@@ -413,7 +415,7 @@ fn parse_namespace_prefix(p: &mut CssParser) -> ParsedSyntax {
413415
pub(crate) struct SubSelectorList;
414416
impl SubSelectorList {
415417
pub(crate) const START_SET: TokenSet<CssSyntaxKind> =
416-
token_set![T![#], T![.], T![:], T![::], T!['[']];
418+
token_set![T![#], T![.], T![:], T![::], T!['['], T![&]];
417419
}
418420
impl ParseNodeList for SubSelectorList {
419421
type Kind = CssSyntaxKind;
@@ -442,7 +444,8 @@ impl ParseNodeList for SubSelectorList {
442444
///
443445
/// This function is responsible for identifying and parsing different types of sub-selectors
444446
/// based on the current token in the CSS parser. It dispatches to specific parsing functions
445-
/// for class selectors, ID selectors, attribute selectors, pseudo-classes, and pseudo-elements.
447+
/// for class selectors, ID selectors, attribute selectors, pseudo-classes, pseudo-elements,
448+
/// and the nesting selector (`&`), e.g. the trailing `&` in `h1&`.
446449
#[inline]
447450
fn parse_sub_selector(p: &mut CssParser) -> ParsedSyntax {
448451
match p.cur() {
@@ -451,6 +454,7 @@ fn parse_sub_selector(p: &mut CssParser) -> ParsedSyntax {
451454
T!['['] => parse_attribute_selector(p),
452455
T![:] => parse_pseudo_class_selector(p),
453456
T![::] => parse_pseudo_element_selector(p),
457+
T![&] => parse_nested_selector_as_sub_selector(p),
454458
_ => Absent,
455459
}
456460
}

crates/biome_css_parser/src/syntax/selector/nested_selector.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,21 @@ fn parse_nested_selector(p: &mut CssParser) -> ParsedSyntax {
6464

6565
Present(m.complete(p, CSS_NESTED_SELECTOR))
6666
}
67+
68+
/// Parses a `&` (nesting selector) appearing as a sub-selector, e.g. the
69+
/// trailing `&` in `h1&`. Unlike [parse_nested_selector], this never produces
70+
/// a `ScssParentSelector`, since `AnyCssSubSelector` only allows
71+
/// `CssNestedSelector`; the SCSS parent-selector suffix syntax (`&-100`,
72+
/// `&#{$state}`) is only meaningful when `&` opens a compound selector.
73+
#[inline]
74+
pub(crate) fn parse_nested_selector_as_sub_selector(p: &mut CssParser) -> ParsedSyntax {
75+
if !is_at_nested_selector(p) {
76+
return Absent;
77+
}
78+
79+
let m = p.start();
80+
let context = selector_lex_context(p);
81+
p.bump_with_context(T![&], context);
82+
83+
Present(m.complete(p, CSS_NESTED_SELECTOR))
84+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[data-smth="true"] {
2+
h1& {
3+
color: red;
4+
}
5+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
---
2+
source: crates/biome_css_parser/tests/spec_test.rs
3+
expression: snapshot
4+
---
5+
6+
## Input
7+
8+
```css
9+
[data-smth="true"] {
10+
h1& {
11+
color: red;
12+
}
13+
}
14+
15+
```
16+
17+
18+
## AST
19+
20+
```
21+
CssRoot {
22+
bom_token: missing (optional),
23+
items: CssRootItemList [
24+
CssQualifiedRule {
25+
prelude: CssSelectorList [
26+
CssCompoundSelector {
27+
nesting_selectors: CssNestedSelectorList [],
28+
simple_selector: missing (optional),
29+
sub_selectors: CssSubSelectorList [
30+
CssAttributeSelector {
31+
l_brack_token: L_BRACK@0..1 "[" [] [],
32+
name: CssAttributeName {
33+
namespace: missing (optional),
34+
name: CssIdentifier {
35+
value_token: IDENT@1..10 "data-smth" [] [],
36+
},
37+
},
38+
matcher: CssAttributeMatcher {
39+
operator: EQ@10..11 "=" [] [],
40+
value: CssAttributeMatcherValue {
41+
name: CssString {
42+
value_token: CSS_STRING_LITERAL@11..17 "\"true\"" [] [],
43+
},
44+
},
45+
modifier: missing (optional),
46+
},
47+
r_brack_token: R_BRACK@17..19 "]" [] [Whitespace(" ")],
48+
},
49+
],
50+
},
51+
],
52+
block: CssDeclarationOrRuleBlock {
53+
l_curly_token: L_CURLY@19..20 "{" [] [],
54+
items: CssDeclarationOrRuleList [
55+
CssNestedQualifiedRule {
56+
prelude: CssRelativeSelectorList [
57+
CssRelativeSelector {
58+
combinator: missing (optional),
59+
selector: CssCompoundSelector {
60+
nesting_selectors: CssNestedSelectorList [],
61+
simple_selector: CssTypeSelector {
62+
namespace: missing (optional),
63+
ident: CssIdentifier {
64+
value_token: IDENT@20..24 "h1" [Newline("\n"), Whitespace("\t")] [],
65+
},
66+
},
67+
sub_selectors: CssSubSelectorList [
68+
CssNestedSelector {
69+
amp_token: AMP@24..26 "&" [] [Whitespace(" ")],
70+
},
71+
],
72+
},
73+
},
74+
],
75+
block: CssDeclarationOrRuleBlock {
76+
l_curly_token: L_CURLY@26..27 "{" [] [],
77+
items: CssDeclarationOrRuleList [
78+
CssDeclarationWithSemicolon {
79+
declaration: CssDeclaration {
80+
property: CssGenericProperty {
81+
name: CssIdentifier {
82+
value_token: IDENT@27..35 "color" [Newline("\n"), Whitespace("\t\t")] [],
83+
},
84+
colon_token: COLON@35..37 ":" [] [Whitespace(" ")],
85+
value: CssGenericComponentValueList [
86+
CssIdentifier {
87+
value_token: IDENT@37..40 "red" [] [],
88+
},
89+
],
90+
},
91+
important: missing (optional),
92+
},
93+
semicolon_token: SEMICOLON@40..41 ";" [] [],
94+
},
95+
],
96+
r_curly_token: R_CURLY@41..44 "}" [Newline("\n"), Whitespace("\t")] [],
97+
},
98+
},
99+
],
100+
r_curly_token: R_CURLY@44..46 "}" [Newline("\n")] [],
101+
},
102+
},
103+
],
104+
eof_token: EOF@46..47 "" [Newline("\n")] [],
105+
}
106+
```
107+
108+
## CST
109+
110+
```
111+
0: CSS_ROOT@0..47
112+
0: (empty)
113+
1: CSS_ROOT_ITEM_LIST@0..46
114+
0: CSS_QUALIFIED_RULE@0..46
115+
0: CSS_SELECTOR_LIST@0..19
116+
0: CSS_COMPOUND_SELECTOR@0..19
117+
0: CSS_NESTED_SELECTOR_LIST@0..0
118+
1: (empty)
119+
2: CSS_SUB_SELECTOR_LIST@0..19
120+
0: CSS_ATTRIBUTE_SELECTOR@0..19
121+
0: L_BRACK@0..1 "[" [] []
122+
1: CSS_ATTRIBUTE_NAME@1..10
123+
0: (empty)
124+
1: CSS_IDENTIFIER@1..10
125+
0: IDENT@1..10 "data-smth" [] []
126+
2: CSS_ATTRIBUTE_MATCHER@10..17
127+
0: EQ@10..11 "=" [] []
128+
1: CSS_ATTRIBUTE_MATCHER_VALUE@11..17
129+
0: CSS_STRING@11..17
130+
0: CSS_STRING_LITERAL@11..17 "\"true\"" [] []
131+
2: (empty)
132+
3: R_BRACK@17..19 "]" [] [Whitespace(" ")]
133+
1: CSS_DECLARATION_OR_RULE_BLOCK@19..46
134+
0: L_CURLY@19..20 "{" [] []
135+
1: CSS_DECLARATION_OR_RULE_LIST@20..44
136+
0: CSS_NESTED_QUALIFIED_RULE@20..44
137+
0: CSS_RELATIVE_SELECTOR_LIST@20..26
138+
0: CSS_RELATIVE_SELECTOR@20..26
139+
0: (empty)
140+
1: CSS_COMPOUND_SELECTOR@20..26
141+
0: CSS_NESTED_SELECTOR_LIST@20..20
142+
1: CSS_TYPE_SELECTOR@20..24
143+
0: (empty)
144+
1: CSS_IDENTIFIER@20..24
145+
0: IDENT@20..24 "h1" [Newline("\n"), Whitespace("\t")] []
146+
2: CSS_SUB_SELECTOR_LIST@24..26
147+
0: CSS_NESTED_SELECTOR@24..26
148+
0: AMP@24..26 "&" [] [Whitespace(" ")]
149+
1: CSS_DECLARATION_OR_RULE_BLOCK@26..44
150+
0: L_CURLY@26..27 "{" [] []
151+
1: CSS_DECLARATION_OR_RULE_LIST@27..41
152+
0: CSS_DECLARATION_WITH_SEMICOLON@27..41
153+
0: CSS_DECLARATION@27..40
154+
0: CSS_GENERIC_PROPERTY@27..40
155+
0: CSS_IDENTIFIER@27..35
156+
0: IDENT@27..35 "color" [Newline("\n"), Whitespace("\t\t")] []
157+
1: COLON@35..37 ":" [] [Whitespace(" ")]
158+
2: CSS_GENERIC_COMPONENT_VALUE_LIST@37..40
159+
0: CSS_IDENTIFIER@37..40
160+
0: IDENT@37..40 "red" [] []
161+
1: (empty)
162+
1: SEMICOLON@40..41 ";" [] []
163+
2: R_CURLY@41..44 "}" [Newline("\n"), Whitespace("\t")] []
164+
2: R_CURLY@44..46 "}" [Newline("\n")] []
165+
2: EOF@46..47 "" [Newline("\n")] []
166+
167+
```

crates/biome_css_semantic/src/semantic_model/specificity.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ fn evaluate_any_subselector(selector: &AnyCssSubSelector) -> Specificity {
130130
AnyCssSubSelector::CssAttributeSelector(_) => CLASS_SPECIFICITY,
131131
AnyCssSubSelector::CssPseudoClassSelector(s) => evaluate_pseudo_selector(s),
132132
AnyCssSubSelector::CssPseudoElementSelector(_) => TYPE_SPECIFICITY,
133+
AnyCssSubSelector::CssNestedSelector(_) => ZERO_SPECIFICITY,
133134
AnyCssSubSelector::CssBogusSubSelector(_) => ZERO_SPECIFICITY,
134135
}
135136
}

crates/biome_css_syntax/src/generated/nodes.rs

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

xtask/codegen/css.ungram

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ AnyCssSubSelector =
190190
| CssAttributeSelector
191191
| CssPseudoClassSelector
192192
| CssPseudoElementSelector
193+
| CssNestedSelector
193194
| CssBogusSubSelector
194195

195196
// * {}

0 commit comments

Comments
 (0)