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
pull/14229/head
Simon Holthausen 6 days ago
parent 438de04fb2
commit 85a37de092

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: account for `:has(...)` as part of `:root`

@ -156,9 +156,24 @@ const css_visitors = {
].includes(first.name));
}
node.metadata.is_global_like ||= !!node.selectors.find(
(child) => child.type === 'PseudoClassSelector' && child.name === 'root'
);
const is_root_without_scoped_children =
node.selectors.some(
(child) => child.type === 'PseudoClassSelector' && child.name === 'root'
) &&
// :root.y:has(.x) is not a global selector because while .y is unscoped, .x inside `:has(...)` should be scoped
!node.selectors.some((child) => child.type === 'PseudoClassSelector' && child.name === 'has');
if (is_root_without_scoped_children) {
node.metadata.is_global_like ||= true;
// So that nested selectors like `:root:not(.x)` are not marked as unused
for (const child of node.selectors) {
walk(/** @type {Css.Node} */ (child), null, {
ComplexSelector(node) {
node.metadata.used = true;
}
});
}
}
context.next();
},

@ -196,7 +196,19 @@ function truncate(node) {
);
});
return node.children.slice(0, i + 1);
return node.children.slice(0, i + 1).map((child) => {
// In case of `:root.y:has(...)`, `y` is unscoped, but everything in `:has(...)` should be scoped (if not global).
// To properly accomplish that, we gotta filter out all selector types except `:has` and `:root`.
const root = child.selectors.find((s) => s.type === 'PseudoClassSelector' && s.name === 'root');
if (!root || child.metadata.is_global_like) return child;
return {
...child,
selectors: child.selectors.filter(
(s) => s.type === 'PseudoClassSelector' && (s.name === 'has' || s.name === 'root')
)
};
});
}
/**
@ -415,6 +427,16 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
// If this is a :has on a :root, we gotta include the element itself, too, because everything's a descendant of :root
const is_root_selector = other_selectors.some(
(s) => s.type === 'PseudoClassSelector' && s.name === 'root'
);
if (is_root_selector) {
child_elements.push(element);
descendant_elements.push(element);
}
walk(
/** @type {Compiler.SvelteNode} */ (element.fragment),
{ is_child: true },
@ -460,7 +482,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
const descendants =
left_most_combinator.name === '+' || left_most_combinator.name === '~'
? (sibling_elements ??= get_following_sibling_elements(element))
? (sibling_elements ??= get_following_sibling_elements(element, is_root_selector))
: left_most_combinator.name === '>'
? child_elements
: descendant_elements;
@ -507,9 +529,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
switch (selector.type) {
case 'PseudoClassSelector': {
if (name === 'host' || name === 'root') {
return false;
}
if (name === 'host') return false;
if (name === 'root') break;
if (
name === 'global' &&
@ -681,8 +703,11 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
return true;
}
/** @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element */
function get_following_sibling_elements(element) {
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {boolean} include_self
*/
function get_following_sibling_elements(element, include_self) {
/** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.Root | null} */
let parent = get_element_parent(element);
@ -723,6 +748,10 @@ function get_following_sibling_elements(element) {
}
}
if (include_self) {
sibling_elements.push(element);
}
return sibling_elements;
}

@ -1,3 +1,34 @@
import { test } from '../../test';
export default test({});
export default test({
warnings: [
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":root .unused"',
start: {
line: 18,
column: 2,
character: 190
},
end: {
line: 18,
column: 15,
character: 203
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector ":root:has(.unused)"',
start: {
line: 25,
column: 2,
character: 269
},
end: {
line: 25,
column: 20,
character: 287
}
}
]
});

@ -1,9 +1,31 @@
:root {
color: red;
color: green;
}
.foo:root {
color: blue;
color: green;
}
:root.foo {
color: green;
}
:root.unknown {
color: green;
}
:root h1.svelte-xyz {
color: green;
}
/* (unused) :root .unused {
color: red;
}*/
:root:has(h1:where(.svelte-xyz)) {
color: green;
}
/* (unused) :root:has(.unused) {
color: red;
}*/
:root:not(.x) {
color: green;
}

@ -1 +1 @@
<h1>Hello!</h1>
<h1 class="svelte-xyz">Hello!</h1>

@ -1,13 +1,34 @@
<style>
:root {
color: red;
color: green;
}
.foo:root {
color: blue;
color: green;
}
:root.foo {
color: green;
}
:root.unknown {
color: green;
}
:root h1 {
color: green;
}
:root .unused {
color: red;
}
:root:has(h1) {
color: green;
}
:root:has(.unused) {
color: red;
}
:root:not(.x) {
color: green;
}
</style>
<h1>Hello!</h1>

Loading…
Cancel
Save