LibWeb: Delay generic :has() sibling scans until sibling roots

Mark elements reached by stepping through sibling combinators inside
:has() and use that breadcrumb during generic invalidation walks.

Keep the existing conservative sibling scans for mutations outside
those marked subtrees so nested :is(), :not(), and nesting cases
continue to invalidate correctly.

Also keep :has() eager within compounds that contain ::part(). Those
selectors retarget the remaining simple selectors to the part host, so
deferring :has() there changes which element the pseudo-class runs
against and can make ::part(foo):has(.match) spuriously match.

Add a counter-based sibling-scan test and a regression test covering
the ::part()/ :has() selector orderings.
This commit is contained in:
Andreas Kling
2026-04-18 23:38:57 +02:00
committed by Alexander Kalenik
parent 85ff13870f
commit 7a5b1d9de1
Notes: github-actions[bot] 2026-04-20 11:21:48 +00:00
7 changed files with 118 additions and 12 deletions

View File

@@ -297,8 +297,10 @@ static inline bool matches_relative_selector(CSS::Selector const& selector, size
auto* sibling = element.next_element_sibling();
if (!sibling)
return false;
if (context.inside_has_argument_match && context.collect_per_element_selector_involvement_metadata)
if (context.inside_has_argument_match && context.collect_per_element_selector_involvement_metadata) {
const_cast<DOM::Element&>(*sibling).set_in_has_scope(true);
const_cast<DOM::Element&>(*sibling).set_in_subtree_of_has_pseudo_class_relative_selector_with_sibling_combinator(true);
}
if (!matches_compound_selector(selector, compound_index, *sibling, shadow_host, context, scope, SelectorKind::Relative, anchor))
return false;
return matches_relative_selector(selector, compound_index + 1, *sibling, shadow_host, context, anchor, scope);
@@ -308,8 +310,10 @@ static inline bool matches_relative_selector(CSS::Selector const& selector, size
const_cast<DOM::Element&>(*anchor).set_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator(true);
}
for (auto const* sibling = element.next_element_sibling(); sibling; sibling = sibling->next_element_sibling()) {
if (context.inside_has_argument_match && context.collect_per_element_selector_involvement_metadata)
if (context.inside_has_argument_match && context.collect_per_element_selector_involvement_metadata) {
const_cast<DOM::Element&>(*sibling).set_in_has_scope(true);
const_cast<DOM::Element&>(*sibling).set_in_subtree_of_has_pseudo_class_relative_selector_with_sibling_combinator(true);
}
if (!matches_compound_selector(selector, compound_index, *sibling, shadow_host, context, scope, SelectorKind::Relative, anchor))
continue;
if (matches_relative_selector(selector, compound_index + 1, *sibling, shadow_host, context, anchor, scope))
@@ -1513,10 +1517,19 @@ bool matches_compound_selector(CSS::Selector const& selector, int component_list
return s.type == CSS::Selector::SimpleSelector::Type::PseudoClass
&& s.pseudo_class().type == CSS::PseudoClass::Has;
};
bool has_part_pseudo_element = false;
for (auto const& simple_selector : compound_selector.simple_selectors) {
if (simple_selector.type == CSS::Selector::SimpleSelector::Type::PseudoElement
&& simple_selector.pseudo_element().type() == CSS::PseudoElement::Part) {
has_part_pseudo_element = true;
break;
}
}
auto defer_has_pseudo_class = !has_part_pseudo_element;
auto element_for_compound_matching { target };
for (auto& simple_selector : compound_selector.simple_selectors.in_reverse()) {
if (is_has_pseudo_class(simple_selector))
if (defer_has_pseudo_class && is_has_pseudo_class(simple_selector))
continue;
if (!matches_simple_selector(simple_selector, element_for_compound_matching, shadow_host, context, scope, selector_kind, anchor)) {
return false;
@@ -1540,11 +1553,13 @@ bool matches_compound_selector(CSS::Selector const& selector, int component_list
}
}
}
for (auto& simple_selector : compound_selector.simple_selectors.in_reverse()) {
if (!is_has_pseudo_class(simple_selector))
continue;
if (!matches_simple_selector(simple_selector, element_for_compound_matching, shadow_host, context, scope, selector_kind, anchor)) {
return false;
if (defer_has_pseudo_class) {
for (auto& simple_selector : compound_selector.simple_selectors.in_reverse()) {
if (!is_has_pseudo_class(simple_selector))
continue;
if (!matches_simple_selector(simple_selector, element_for_compound_matching, shadow_host, context, scope, selector_kind, anchor)) {
return false;
}
}
}
auto const& element = element_for_compound_matching;