Skip to content

Commit 85a37de

Browse files
committed
fix: account for :has(...) as part of :root
We previously marked all `:root` selectors as global-like, which excempted them from further analysis. This causes problems: - things like `:not(...)` are never visited and therefore never marked as used -> we gotta do that directly when coming across this - `:has(...)` was never visited, too. Just marking it as used is not enough though, because we might need to scope its contents Therefore the logic is enhanced to account for these special cases. Fixes #14118
1 parent 438de04 commit 85a37de

File tree

7 files changed

+139
-16
lines changed

7 files changed

+139
-16
lines changed

.changeset/beige-files-pull.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: account for `:has(...)` as part of `:root`

packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,24 @@ const css_visitors = {
156156
].includes(first.name));
157157
}
158158

159-
node.metadata.is_global_like ||= !!node.selectors.find(
160-
(child) => child.type === 'PseudoClassSelector' && child.name === 'root'
161-
);
159+
const is_root_without_scoped_children =
160+
node.selectors.some(
161+
(child) => child.type === 'PseudoClassSelector' && child.name === 'root'
162+
) &&
163+
// :root.y:has(.x) is not a global selector because while .y is unscoped, .x inside `:has(...)` should be scoped
164+
!node.selectors.some((child) => child.type === 'PseudoClassSelector' && child.name === 'has');
165+
166+
if (is_root_without_scoped_children) {
167+
node.metadata.is_global_like ||= true;
168+
// So that nested selectors like `:root:not(.x)` are not marked as unused
169+
for (const child of node.selectors) {
170+
walk(/** @type {Css.Node} */ (child), null, {
171+
ComplexSelector(node) {
172+
node.metadata.used = true;
173+
}
174+
});
175+
}
176+
}
162177

163178
context.next();
164179
},

packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,19 @@ function truncate(node) {
196196
);
197197
});
198198

199-
return node.children.slice(0, i + 1);
199+
return node.children.slice(0, i + 1).map((child) => {
200+
// In case of `:root.y:has(...)`, `y` is unscoped, but everything in `:has(...)` should be scoped (if not global).
201+
// To properly accomplish that, we gotta filter out all selector types except `:has` and `:root`.
202+
const root = child.selectors.find((s) => s.type === 'PseudoClassSelector' && s.name === 'root');
203+
if (!root || child.metadata.is_global_like) return child;
204+
205+
return {
206+
...child,
207+
selectors: child.selectors.filter(
208+
(s) => s.type === 'PseudoClassSelector' && (s.name === 'has' || s.name === 'root')
209+
)
210+
};
211+
});
200212
}
201213

202214
/**
@@ -415,6 +427,16 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
415427
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
416428
let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
417429

430+
// If this is a :has on a :root, we gotta include the element itself, too, because everything's a descendant of :root
431+
const is_root_selector = other_selectors.some(
432+
(s) => s.type === 'PseudoClassSelector' && s.name === 'root'
433+
);
434+
435+
if (is_root_selector) {
436+
child_elements.push(element);
437+
descendant_elements.push(element);
438+
}
439+
418440
walk(
419441
/** @type {Compiler.SvelteNode} */ (element.fragment),
420442
{ is_child: true },
@@ -460,7 +482,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
460482

461483
const descendants =
462484
left_most_combinator.name === '+' || left_most_combinator.name === '~'
463-
? (sibling_elements ??= get_following_sibling_elements(element))
485+
? (sibling_elements ??= get_following_sibling_elements(element, is_root_selector))
464486
: left_most_combinator.name === '>'
465487
? child_elements
466488
: descendant_elements;
@@ -507,9 +529,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
507529

508530
switch (selector.type) {
509531
case 'PseudoClassSelector': {
510-
if (name === 'host' || name === 'root') {
511-
return false;
512-
}
532+
if (name === 'host') return false;
533+
534+
if (name === 'root') break;
513535

514536
if (
515537
name === 'global' &&
@@ -681,8 +703,11 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
681703
return true;
682704
}
683705

684-
/** @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element */
685-
function get_following_sibling_elements(element) {
706+
/**
707+
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
708+
* @param {boolean} include_self
709+
*/
710+
function get_following_sibling_elements(element, include_self) {
686711
/** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.Root | null} */
687712
let parent = get_element_parent(element);
688713

@@ -723,6 +748,10 @@ function get_following_sibling_elements(element) {
723748
}
724749
}
725750

751+
if (include_self) {
752+
sibling_elements.push(element);
753+
}
754+
726755
return sibling_elements;
727756
}
728757

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,34 @@
11
import { test } from '../../test';
22

3-
export default test({});
3+
export default test({
4+
warnings: [
5+
{
6+
code: 'css_unused_selector',
7+
message: 'Unused CSS selector ":root .unused"',
8+
start: {
9+
line: 18,
10+
column: 2,
11+
character: 190
12+
},
13+
end: {
14+
line: 18,
15+
column: 15,
16+
character: 203
17+
}
18+
},
19+
{
20+
code: 'css_unused_selector',
21+
message: 'Unused CSS selector ":root:has(.unused)"',
22+
start: {
23+
line: 25,
24+
column: 2,
25+
character: 269
26+
},
27+
end: {
28+
line: 25,
29+
column: 20,
30+
character: 287
31+
}
32+
}
33+
]
34+
});
Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,31 @@
1+
12
:root {
2-
color: red;
3+
color: green;
34
}
45
.foo:root {
5-
color: blue;
6+
color: green;
67
}
78
:root.foo {
89
color: green;
910
}
11+
:root.unknown {
12+
color: green;
13+
}
14+
15+
:root h1.svelte-xyz {
16+
color: green;
17+
}
18+
/* (unused) :root .unused {
19+
color: red;
20+
}*/
21+
22+
:root:has(h1:where(.svelte-xyz)) {
23+
color: green;
24+
}
25+
/* (unused) :root:has(.unused) {
26+
color: red;
27+
}*/
28+
29+
:root:not(.x) {
30+
color: green;
31+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<h1>Hello!</h1>
1+
<h1 class="svelte-xyz">Hello!</h1>
Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
11
<style>
22
:root {
3-
color: red;
3+
color: green;
44
}
55
.foo:root {
6-
color: blue;
6+
color: green;
77
}
88
:root.foo {
99
color: green;
1010
}
11+
:root.unknown {
12+
color: green;
13+
}
14+
15+
:root h1 {
16+
color: green;
17+
}
18+
:root .unused {
19+
color: red;
20+
}
21+
22+
:root:has(h1) {
23+
color: green;
24+
}
25+
:root:has(.unused) {
26+
color: red;
27+
}
28+
29+
:root:not(.x) {
30+
color: green;
31+
}
1132
</style>
1233

1334
<h1>Hello!</h1>

0 commit comments

Comments
 (0)