Skip to content

Commit d2cf4f6

Browse files
committed
fix: correctly visit elements that may match :has() during pruning
1 parent f67cf20 commit d2cf4f6

File tree

5 files changed

+214
-121
lines changed

5 files changed

+214
-121
lines changed

.changeset/cuddly-turtles-work.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: correctly visit elements that may match :has() during pruning

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

Lines changed: 107 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../
55
import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
66

77
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
8+
/** @typedef {Compiler.AST.CSS.BaseNode & { type: 'ElementSelector', element: Compiler.AST.RegularElement | Compiler.AST.SvelteElement }} ElementSelector */
9+
/** @typedef {Omit<Compiler.AST.CSS.RelativeSelector, 'selectors'> & { selectors: Array<Compiler.AST.CSS.SimpleSelector | ElementSelector> }} ExtendedRelativeSelector */
810

911
const NODE_PROBABLY_EXISTS = 0;
1012
const NODE_DEFINITELY_EXISTS = 1;
@@ -234,7 +236,7 @@ function apply_combinator(relative_selector, parent_selectors, rule, node) {
234236

235237
case '+':
236238
case '~': {
237-
const siblings = get_possible_element_siblings(node, name === '+');
239+
const siblings = get_possible_element_preceding_siblings(node, name === '+');
238240

239241
let sibling_matched = false;
240242

@@ -310,7 +312,7 @@ const regex_backslash_and_following_character = /\\(.)/g;
310312
/**
311313
* Ensure that `element` satisfies each simple selector in `relative_selector`
312314
*
313-
* @param {Compiler.AST.CSS.RelativeSelector} relative_selector
315+
* @param {ExtendedRelativeSelector} relative_selector
314316
* @param {Compiler.AST.CSS.Rule} rule
315317
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
316318
* @returns {boolean}
@@ -331,13 +333,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
331333
// If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match.
332334
// In that case ignore this check (because we just came from this) to avoid an infinite loop.
333335
if (has_selectors.length > 0) {
334-
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
335-
const child_elements = [];
336-
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
337-
const descendant_elements = [];
338-
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
339-
let sibling_elements; // do them lazy because it's rarely used and expensive to calculate
340-
341336
// If this is a :has inside a global selector, we gotta include the element itself, too,
342337
// because the global selector might be for an element that's outside the component,
343338
// e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} }
@@ -353,46 +348,33 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
353348
)
354349
)
355350
);
356-
if (include_self) {
357-
child_elements.push(element);
358-
descendant_elements.push(element);
359-
}
360351

361-
const seen = new Set();
352+
// set them lazy because it's expensive to calculate
353+
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
354+
let descendant_elements;
355+
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
356+
let sibling_elements;
357+
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
358+
let sibling_descendant_elements;
362359

363360
/**
364-
* @param {Compiler.AST.SvelteNode} node
365-
* @param {{ is_child: boolean }} state
361+
* @param {ExtendedRelativeSelector[]} selectors
366362
*/
367-
function walk_children(node, state) {
368-
walk(node, state, {
369-
_(node, context) {
370-
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
371-
descendant_elements.push(node);
372-
373-
if (context.state.is_child) {
374-
child_elements.push(node);
375-
context.state.is_child = false;
376-
context.next();
377-
context.state.is_child = true;
378-
} else {
379-
context.next();
380-
}
381-
} else if (node.type === 'RenderTag') {
382-
for (const snippet of node.metadata.snippets) {
383-
if (seen.has(snippet)) continue;
363+
const get_elements = (selectors) => {
364+
const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator;
384365

385-
seen.add(snippet);
386-
walk_children(snippet.body, context.state);
387-
}
388-
} else {
389-
context.next();
390-
}
391-
}
392-
});
393-
}
366+
if (left_most_combinator.name === ' ' || left_most_combinator.name === '>') {
367+
descendant_elements ??= get_descendant_elements(element, include_self);
368+
return descendant_elements;
369+
}
394370

395-
walk_children(element.fragment, { is_child: true });
371+
sibling_elements ??= get_following_sibling_elements(element, include_self);
372+
if (selectors.some(s => s.combinator?.name === ' ' || s.combinator?.name === '>')) {
373+
sibling_descendant_elements ??= sibling_elements.flatMap(el => get_descendant_elements(el, false));
374+
return sibling_descendant_elements;
375+
}
376+
return sibling_elements;
377+
}
396378

397379
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
398380
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
@@ -403,32 +385,23 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
403385
let matched = false;
404386

405387
for (const complex_selector of complex_selectors) {
388+
/** @type {ExtendedRelativeSelector[]} */
406389
const selectors = truncate(complex_selector);
407-
const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator;
408-
// In .x:has(> y), we want to search for y, ignoring the left-most combinator
409-
// (else it would try to walk further up and fail because there are no selectors left)
390+
const elements = get_elements(selectors);
391+
// In .x:has(> y), we complete the selector by prepending a special one that
392+
// matches only this `element`, otherwise it can mismatch with an ancestor element
410393
if (selectors.length > 0) {
411-
selectors[0] = {
412-
...selectors[0],
413-
combinator: null
414-
};
394+
selectors.unshift(make_element_selector(element));
415395
}
416396

417-
const descendants =
418-
left_most_combinator.name === '+' || left_most_combinator.name === '~'
419-
? (sibling_elements ??= get_following_sibling_elements(element, include_self))
420-
: left_most_combinator.name === '>'
421-
? child_elements
422-
: descendant_elements;
423-
424397
let selector_matched = false;
425398

426399
// Iterate over all descendant elements and check if the selector inside :has matches
427-
for (const element of descendants) {
400+
for (const element of elements) {
428401
if (
429402
selectors.length === 0 /* is :global(...) */ ||
430403
(element.metadata.scoped && selector_matched) ||
431-
apply_selector(selectors, rule, element)
404+
apply_selector(/** @type {Compiler.AST.CSS.RelativeSelector[]} */ (selectors), rule, element)
432405
) {
433406
complex_selector.metadata.used = true;
434407
selector_matched = matched = true;
@@ -445,6 +418,10 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element)
445418
for (const selector of other_selectors) {
446419
if (selector.type === 'Percentage' || selector.type === 'Nth') continue;
447420

421+
if (selector.type === 'ElementSelector') {
422+
return element === selector.element;
423+
}
424+
448425
const name = selector.name.replace(regex_backslash_and_following_character, '$1');
449426

450427
switch (selector.type) {
@@ -686,6 +663,51 @@ function get_following_sibling_elements(element, include_self) {
686663
return siblings;
687664
}
688665

666+
/**
667+
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
668+
* @param {boolean} include_self
669+
*/
670+
function get_descendant_elements(element, include_self) {
671+
/** @type {Array<Compiler.AST.RegularElement | Compiler.AST.SvelteElement>} */
672+
const descendants = include_self ? [element] : [];
673+
const seen = new Set();
674+
675+
/**
676+
* @param {Compiler.AST.SvelteNode} node
677+
* @param {{ is_child: boolean }} state
678+
*/
679+
function walk_children(node, state) {
680+
walk(node, state, {
681+
_(node, context) {
682+
if (node.type === 'RegularElement' || node.type === 'SvelteElement') {
683+
descendants.push(node);
684+
685+
if (context.state.is_child) {
686+
context.state.is_child = false;
687+
context.next();
688+
context.state.is_child = true;
689+
} else {
690+
context.next();
691+
}
692+
} else if (node.type === 'RenderTag') {
693+
for (const snippet of node.metadata.snippets) {
694+
if (seen.has(snippet)) continue;
695+
696+
seen.add(snippet);
697+
walk_children(snippet.body, context.state);
698+
}
699+
} else {
700+
context.next();
701+
}
702+
}
703+
});
704+
}
705+
706+
walk_children(element.fragment, { is_child: true });
707+
708+
return descendants;
709+
}
710+
689711
/**
690712
* @param {any} operator
691713
* @param {any} expected_value
@@ -847,7 +869,7 @@ function get_element_parent(node) {
847869
* @param {Set<Compiler.AST.SnippetBlock>} seen
848870
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>}
849871
*/
850-
function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
872+
function get_possible_element_preceding_siblings(node, adjacent_only, seen = new Set()) {
851873
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */
852874
const result = new Map();
853875
const path = node.metadata.path;
@@ -910,7 +932,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) {
910932
seen.add(current);
911933

912934
for (const site of current.metadata.sites) {
913-
const siblings = get_possible_element_siblings(site, adjacent_only, seen);
935+
const siblings = get_possible_element_preceding_siblings(site, adjacent_only, seen);
914936
add_to_map(siblings, result);
915937

916938
if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) {
@@ -1067,3 +1089,27 @@ function is_block(node) {
10671089
node.type === 'SlotElement'
10681090
);
10691091
}
1092+
1093+
/**
1094+
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
1095+
* @return {ExtendedRelativeSelector}
1096+
*/
1097+
function make_element_selector(element) {
1098+
return {
1099+
type: 'RelativeSelector',
1100+
selectors: [{
1101+
type: 'ElementSelector',
1102+
element,
1103+
start: -1,
1104+
end: -1,
1105+
}],
1106+
combinator: null,
1107+
metadata: {
1108+
is_global: false,
1109+
is_global_like: false,
1110+
scoped: false,
1111+
},
1112+
start: -1,
1113+
end: -1,
1114+
};
1115+
}

0 commit comments

Comments
 (0)