Skip to content

Commit ab471da

Browse files
committed
fix: mark :has selectors with multiple preceding selectors as used
Fixes #13717 There are two parts to this: 1. the parent selectors weren't passed along for the check inside `:has`, which in case of a leading combinator would mean it would always count as unused 2. In case if a selector like `x > y:has(z)`, the prior logic would correctly determine that for element `z` there's a match for the `:has` selector, by first checking its contents and then walking up the tree. But after it did that, it would try to walk up the tree once more, which is a) wasteful b) buggy because the tree walking mechanism would no longer be adjusted for the `:has` special case, resulting in false negatives. To fix that, the `:has` will return a new value from the function, signaling that it already fully checked the upper selectors, and so the function calling it will skip doing that.
1 parent 28c8d2b commit ab471da

File tree

4 files changed

+22
-3
lines changed

4 files changed

+22
-3
lines changed

.changeset/five-zoos-brush.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: mark `:has` selectors with multiple preceding selectors as used

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,17 @@ function apply_selector(relative_selectors, rule, element, stylesheet, check_has
202202

203203
const possible_match = relative_selector_might_apply_to_node(
204204
relative_selector,
205+
parent_selectors,
205206
rule,
206207
element,
207208
stylesheet,
208209
check_has
209210
);
210211

212+
if (possible_match === 'definite_match') {
213+
return true;
214+
}
215+
211216
if (!possible_match) {
212217
return false;
213218
}
@@ -388,14 +393,16 @@ const regex_backslash_and_following_character = /\\(.)/g;
388393
* Ensure that `element` satisfies each simple selector in `relative_selector`
389394
*
390395
* @param {Compiler.Css.RelativeSelector} relative_selector
396+
* @param {Compiler.Css.RelativeSelector[]} parent_selectors
391397
* @param {Compiler.Css.Rule} rule
392398
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
393399
* @param {Compiler.Css.StyleSheet} stylesheet
394400
* @param {boolean} check_has Whether or not to check the `:has(...)` selectors
395-
* @returns {boolean}
401+
* @returns {boolean | 'definite_match'}
396402
*/
397403
function relative_selector_might_apply_to_node(
398404
relative_selector,
405+
parent_selectors,
399406
rule,
400407
element,
401408
stylesheet,
@@ -446,7 +453,7 @@ function relative_selector_might_apply_to_node(
446453
apply_combinator(
447454
left_most_combinator,
448455
selectors[0] ?? [],
449-
[relative_selector],
456+
[...parent_selectors, relative_selector],
450457
rule,
451458
element,
452459
stylesheet,
@@ -478,7 +485,8 @@ function relative_selector_might_apply_to_node(
478485
}
479486
}
480487

481-
return true;
488+
// We return this to signal the parent "don't bother checking the rest of the selectors, I already did that"
489+
return 'definite_match';
482490
}
483491

484492
for (const selector of other_selectors) {

packages/svelte/tests/css/samples/has/expected.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,6 @@
8585
/* (unused) x:has(> z) {
8686
color: red;
8787
}*/
88+
x.svelte-xyz > y:where(.svelte-xyz):has(z:where(.svelte-xyz)) {
89+
color: green;
90+
}

packages/svelte/tests/css/samples/has/input.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,7 @@
9191
x:has(> z) {
9292
color: red;
9393
}
94+
x > y:has(z) {
95+
color: green;
96+
}
9497
</style>

0 commit comments

Comments
 (0)