Skip to content

Commit 7818009

Browse files
authored
fix(analyzer): noDuplicateSelectors false positives with scope at-rule (#10111)
1 parent d29dd19 commit 7818009

9 files changed

Lines changed: 121 additions & 4 deletions

File tree

.changeset/gentle-bars-drop.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#9997](https://github.com/biomejs/biome/issues/9997): [`noDuplicateSelectors`](https://biomejs.dev/linter/rules/no-duplicate-selectors/) no longer reports false positives for selectors inside `@scope` queries. Biome now treats `@scope` as a separate at-rule context, like `@media`, `@supports`, `@container`, and `@starting-style`.
6+
7+
The following snippet is no longer flagged as a duplicate:
8+
9+
```css
10+
.Example {
11+
padding: 0;
12+
}
13+
14+
@scope (.theme-dark) {
15+
.Example {
16+
color: white;
17+
}
18+
}
19+
```

crates/biome_css_analyze/src/lint/nursery/no_duplicate_selectors.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ impl Rule for NoDuplicateSelectors {
146146
AnyRuleStart::CssMediaAtRule(_)
147147
| AnyRuleStart::CssSupportsAtRule(_)
148148
| AnyRuleStart::CssContainerAtRule(_)
149+
| AnyRuleStart::CssScopeAtRule(_)
149150
| AnyRuleStart::CssStartingStyleAtRule(_)
150151
);
151152

crates/biome_css_analyze/tests/specs/nursery/noDuplicateSelectors/invalid.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,9 @@ h3 {
8282
#main {
8383
.content {}
8484
}
85+
86+
/* 16. Duplicate inside the same @scope block */
87+
@scope (.theme-dark) {
88+
.card {}
89+
.card {}
90+
}

crates/biome_css_analyze/tests/specs/nursery/noDuplicateSelectors/invalid.css.snap

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ h3 {
8989
.content {}
9090
}
9191
92+
/* 16. Duplicate inside the same @scope block */
93+
@scope (.theme-dark) {
94+
.card {}
95+
.card {}
96+
}
97+
9298
```
9399

94100
# Diagnostics
@@ -498,3 +504,31 @@ invalid.css:83:3 lint/nursery/noDuplicateSelectors ━━━━━━━━━
498504
499505
500506
```
507+
508+
```
509+
invalid.css:89:3 lint/nursery/noDuplicateSelectors ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
510+
511+
! Duplicate selector list ".card".
512+
513+
87 │ @scope (.theme-dark) {
514+
88 │ .card {}
515+
> 89 │ .card {}
516+
^^^^^^^^
517+
90}
518+
91 │
519+
520+
i It was first defined here.
521+
522+
86 │ /* 16. Duplicate inside the same @scope block */
523+
87 │ @scope (.theme-dark) {
524+
> 88 │ .card {}
525+
^^^^^^^^
526+
89 │ .card {}
527+
90}
528+
529+
i Remove or merge the duplicate rule to keep the stylesheet clean.
530+
531+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
532+
533+
534+
```

crates/biome_css_analyze/tests/specs/nursery/noDuplicateSelectors/valid.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,28 @@
4444
.child {}
4545
}
4646
.other .child {}
47+
48+
/* 9. Same selector inside @scope is NOT a duplicate of the one outside:
49+
`@scope` defines its own at-rule context. */
50+
.Example {
51+
padding: 0;
52+
}
53+
@scope (.theme-dark) {
54+
.Example {
55+
color: white;
56+
}
57+
}
58+
59+
/* 10. Same selector in different @scope blocks: each block is its own context. */
60+
@scope (.theme-dark) {
61+
.card {}
62+
}
63+
@scope (.theme-light) {
64+
.card {}
65+
}
66+
67+
/* 11. @scope with `to`: inner selector is in its own context. */
68+
.note {}
69+
@scope (.doc) to (.footnote) {
70+
.note {}
71+
}

crates/biome_css_analyze/tests/specs/nursery/noDuplicateSelectors/valid.css.snap

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,29 @@ expression: valid.css
5151
}
5252
.other .child {}
5353
54+
/* 9. Same selector inside @scope is NOT a duplicate of the one outside:
55+
`@scope` defines its own at-rule context. */
56+
.Example {
57+
padding: 0;
58+
}
59+
@scope (.theme-dark) {
60+
.Example {
61+
color: white;
62+
}
63+
}
64+
65+
/* 10. Same selector in different @scope blocks: each block is its own context. */
66+
@scope (.theme-dark) {
67+
.card {}
68+
}
69+
@scope (.theme-light) {
70+
.card {}
71+
}
72+
73+
/* 11. @scope with `to`: inner selector is in its own context. */
74+
.note {}
75+
@scope (.doc) to (.footnote) {
76+
.note {}
77+
}
78+
5479
```

crates/biome_css_semantic/src/events.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ impl SemanticEventExtractor {
6565
|| kind == CSS_NESTED_QUALIFIED_RULE
6666
|| kind == CSS_CONTAINER_AT_RULE
6767
|| kind == CSS_MEDIA_AT_RULE
68+
|| kind == CSS_SCOPE_AT_RULE
6869
|| kind == CSS_STARTING_STYLE_AT_RULE
6970
|| kind == CSS_SUPPORTS_AT_RULE =>
7071
{
@@ -278,6 +279,7 @@ impl SemanticEventExtractor {
278279
| CSS_NESTED_QUALIFIED_RULE
279280
| CSS_CONTAINER_AT_RULE
280281
| CSS_MEDIA_AT_RULE
282+
| CSS_SCOPE_AT_RULE
281283
| CSS_STARTING_STYLE_AT_RULE
282284
| CSS_SUPPORTS_AT_RULE
283285
) {

crates/biome_css_semantic/src/semantic_model/builder.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ impl SemanticModelBuilder {
5353
let typed_node = rule.node.to_node(self.root.syntax());
5454
if matches!(
5555
typed_node,
56-
AnyRuleStart::CssMediaAtRule(_) | AnyRuleStart::CssSupportsAtRule(_)
56+
AnyRuleStart::CssMediaAtRule(_)
57+
| AnyRuleStart::CssScopeAtRule(_)
58+
| AnyRuleStart::CssSupportsAtRule(_)
5759
) {
5860
current_parent_id = iterator
5961
.next()
@@ -86,7 +88,9 @@ impl SemanticModelBuilder {
8688
let typed_node = rule.node.to_node(self.root.syntax());
8789
if matches!(
8890
typed_node,
89-
AnyRuleStart::CssMediaAtRule(_) | AnyRuleStart::CssSupportsAtRule(_)
91+
AnyRuleStart::CssMediaAtRule(_)
92+
| AnyRuleStart::CssScopeAtRule(_)
93+
| AnyRuleStart::CssSupportsAtRule(_)
9094
) {
9195
current_parent_id = iterator
9296
.next()

crates/biome_css_semantic/src/semantic_model/model.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use biome_css_syntax::{
22
AnyCssRoot, CssComplexSelector, CssComposesPropertyValue, CssCompoundSelector,
33
CssContainerAtRule, CssDashedIdentifier, CssDeclaration, CssGenericComponentValueList,
4-
CssIdentifier, CssMediaAtRule, CssNestedQualifiedRule, CssQualifiedRule,
4+
CssIdentifier, CssMediaAtRule, CssNestedQualifiedRule, CssQualifiedRule, CssScopeAtRule,
55
CssStartingStyleAtRule, CssSupportsAtRule, CssSyntaxKind, CssSyntaxToken, ScssExpression,
66
};
77
use biome_rowan::{
@@ -174,7 +174,7 @@ impl Rule {
174174
}
175175

176176
declare_node_union! {
177-
pub AnyRuleStart = CssQualifiedRule | CssNestedQualifiedRule | CssContainerAtRule | CssMediaAtRule | CssStartingStyleAtRule | CssSupportsAtRule
177+
pub AnyRuleStart = CssQualifiedRule | CssNestedQualifiedRule | CssContainerAtRule | CssMediaAtRule | CssScopeAtRule | CssStartingStyleAtRule | CssSupportsAtRule
178178
}
179179

180180
impl AnyRuleStart {
@@ -184,6 +184,7 @@ impl AnyRuleStart {
184184
Self::CssNestedQualifiedRule(node) => node.syntax().text_trimmed_range(),
185185
Self::CssContainerAtRule(node) => node.syntax().text_trimmed_range(),
186186
Self::CssMediaAtRule(node) => node.syntax().text_trimmed_range(),
187+
Self::CssScopeAtRule(node) => node.syntax().text_trimmed_range(),
187188
Self::CssStartingStyleAtRule(node) => node.syntax().text_trimmed_range(),
188189
Self::CssSupportsAtRule(node) => node.syntax().text_trimmed_range(),
189190
}

0 commit comments

Comments
 (0)