From ca87f977e6f5f5bf344eaee0f8ee3daa81ec7e73 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Thu, 18 Jun 2026 22:05:38 +0200 Subject: [PATCH] LibWeb: Reject child selectors with a parent hash filter For selectors whose rightmost relation is an immediate child combinator, collect hashes that must be present on the subject parent and reject the rule early when the current parent cannot satisfy them. This avoids full selector matching for common child-combinator tails. --- Libraries/LibWeb/CSS/StyleComputer.cpp | 190 +++++++++++++++++++------ 1 file changed, 149 insertions(+), 41 deletions(-) diff --git a/Libraries/LibWeb/CSS/StyleComputer.cpp b/Libraries/LibWeb/CSS/StyleComputer.cpp index 9f55c63c13..cfe4d2b199 100644 --- a/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -104,6 +104,18 @@ namespace Web::CSS { GC_DEFINE_ALLOCATOR(StyleComputer); +static void for_each_element_hash(DOM::Element const& element, auto callback) +{ + callback(ancestor_filter_hash_for_tag_name(element.local_name().ascii_case_insensitive_hash())); + if (element.id().has_value()) + callback(ancestor_filter_hash_for_id(element.id().value().hash())); + for (auto const& class_ : element.class_names()) + callback(ancestor_filter_hash_for_class(class_.hash())); + element.for_each_attribute([&](auto& attribute) { + callback(ancestor_filter_hash_for_attribute(attribute.name().ascii_case_insensitive_hash())); + }); +} + static bool property_affects_font_metrics(PropertyID property_id) { return property_id == PropertyID::FontSize || property_id == PropertyID::LineHeight; @@ -382,40 +394,146 @@ static u64 pseudo_element_style_bit(PseudoElement pseudo_element) return 1ull << to_underlying(pseudo_element); } -template -static IterationDecision for_each_matching_rule_bucket(DOM::AbstractElement abstract_element, RuleBuckets const& rule_buckets, Function const&)> const& callback) +struct ParentFilterHashCollector { + static bool contains_hash(Vector const& hashes, u32 hash) + { + for (auto existing_hash : hashes) { + if (existing_hash == hash) + return true; + } + return false; + } + + static void append_unique_hash(Vector& hashes, u32 hash) + { + if (!contains_hash(hashes, hash)) + hashes.append(hash); + } + + static void intersect_hashes(Vector& hashes, Vector const& other_hashes) + { + for (size_t i = 0; i < hashes.size();) { + if (contains_hash(other_hashes, hashes[i])) { + ++i; + continue; + } + hashes.remove(i); + } + } + + static Vector hashes_from_simple_selector(Selector::SimpleSelector const& simple_selector) + { + Vector hashes; + switch (simple_selector.type) { + case Selector::SimpleSelector::Type::Id: + hashes.append(ancestor_filter_hash_for_id(simple_selector.name().hash())); + break; + case Selector::SimpleSelector::Type::Class: + hashes.append(ancestor_filter_hash_for_class(simple_selector.name().hash())); + break; + case Selector::SimpleSelector::Type::TagName: + hashes.append(ancestor_filter_hash_for_tag_name(simple_selector.qualified_name().name.lowercase_name.hash())); + break; + case Selector::SimpleSelector::Type::Attribute: + hashes.append(ancestor_filter_hash_for_attribute(simple_selector.attribute().qualified_name.name.lowercase_name.hash())); + break; + case Selector::SimpleSelector::Type::PseudoClass: { + auto const& pseudo_class = simple_selector.pseudo_class(); + if (pseudo_class.type != PseudoClass::Is && pseudo_class.type != PseudoClass::Where) + break; + + hashes = common_hashes_from_selector_list(pseudo_class.argument_selector_list); + break; + } + default: + break; + } + return hashes; + } + + static Vector hashes_from_compound(Selector::CompoundSelector const& compound_selector) + { + Vector hashes; + for (auto const& simple_selector : compound_selector.simple_selectors) { + for (auto hash : hashes_from_simple_selector(simple_selector)) + append_unique_hash(hashes, hash); + } + return hashes; + } + + static Vector hashes_from_selector_subject(Selector const& selector) + { + auto const& compound_selectors = selector.compound_selectors(); + if (compound_selectors.is_empty()) + return {}; + return hashes_from_compound(compound_selectors.last()); + } + + static Vector common_hashes_from_selector_list(SelectorList const& selector_list) + { + if (selector_list.is_empty()) + return {}; + + Optional> common_hashes; + for (auto const& argument_selector : selector_list) { + auto hashes = hashes_from_selector_subject(*argument_selector); + if (!common_hashes.has_value()) { + common_hashes = move(hashes); + continue; + } + + intersect_hashes(common_hashes.value(), hashes); + if (common_hashes->is_empty()) + break; + } + + return common_hashes.release_value(); + } +}; + +static Vector parent_filter_hashes_for_selector(Selector const& selector) { - for (auto const& class_name : abstract_element.element().class_names()) { - if (auto it = rule_buckets.rules_by_class.find(class_name); it != rule_buckets.rules_by_class.end()) { - if (callback(it->value) == IterationDecision::Break) - return IterationDecision::Break; - } - } - if (auto id = abstract_element.element().id(); id.has_value()) { - if (auto it = rule_buckets.rules_by_id.find(id.value()); it != rule_buckets.rules_by_id.end()) { - if (callback(it->value) == IterationDecision::Break) - return IterationDecision::Break; - } - } - if (auto it = rule_buckets.rules_by_tag_name.find(abstract_element.element().lowercased_local_name()); it != rule_buckets.rules_by_tag_name.end()) { - if (callback(it->value) == IterationDecision::Break) - return IterationDecision::Break; - } + if (selector.target_pseudo_element().has_value()) + return {}; - if (abstract_element.element().is_document_element()) { - if (callback(rule_buckets.root_rules) == IterationDecision::Break) - return IterationDecision::Break; - } + auto const& compound_selectors = selector.compound_selectors(); + if (compound_selectors.size() < 2) + return {}; + if (compound_selectors.last().combinator != Selector::Combinator::ImmediateChild) + return {}; - IterationDecision decision = IterationDecision::Continue; - abstract_element.element().for_each_attribute([&](auto& name, auto&) { - if (auto it = rule_buckets.rules_by_attribute_name.find(name); it != rule_buckets.rules_by_attribute_name.end()) - decision = callback(it->value); + // The compound immediately to the left of the subject must match the + // subject's parent. Only collect hashes that are required on that parent + // itself; ancestor requirements inside selector-list pseudos remain the + // job of the normal ancestor filter. + return ParentFilterHashCollector::hashes_from_compound(compound_selectors[compound_selectors.size() - 2]); +} + +static bool parent_filter_may_contain_all(DOM::Element const& parent, Vector const& required_hashes) +{ + Vector parent_hashes; + for_each_element_hash(parent, [&](u32 hash) { + ParentFilterHashCollector::append_unique_hash(parent_hashes, hash); }); - if (decision == IterationDecision::Break) - return IterationDecision::Break; - return callback(rule_buckets.other_rules); + for (auto hash : required_hashes) { + if (!ParentFilterHashCollector::contains_hash(parent_hashes, hash)) + return false; + } + return true; +} + +static bool should_reject_with_parent_filter(DOM::AbstractElement abstract_element, Selector const& selector) +{ + auto required_hashes = parent_filter_hashes_for_selector(selector); + if (required_hashes.is_empty()) + return false; + + auto parent = abstract_element.parent_element(); + if (!parent) + return true; + + return !parent_filter_may_contain_all(*parent, required_hashes); } Vector StyleComputer::collect_matching_rules_from_context(DOM::AbstractElement abstract_element, CascadeOrigin cascade_origin, GC::Ptr context_shadow_root, Optional qualified_layer_name, u64* matching_pseudo_element_styles) const @@ -495,6 +613,8 @@ Vector StyleComputer::collect_matching_rules_ auto const& selector = rule_to_run.selector; if (selector.can_use_ancestor_filter() && should_reject_with_ancestor_filter(selector)) return; + if (should_reject_with_parent_filter(abstract_element, selector)) + return; rules_to_run.unchecked_append({ .rule = &rule_to_run, @@ -4058,18 +4178,6 @@ NonnullRefPtr StyleComputer::compute_math_depth(NonnullRefPtr< return IntegerStyleValue::create(inherited_math_depth); } -static void for_each_element_hash(DOM::Element const& element, auto callback) -{ - callback(ancestor_filter_hash_for_tag_name(element.local_name().ascii_case_insensitive_hash())); - if (element.id().has_value()) - callback(ancestor_filter_hash_for_id(element.id().value().hash())); - for (auto const& class_ : element.class_names()) - callback(ancestor_filter_hash_for_class(class_.hash())); - element.for_each_attribute([&](auto& attribute) { - callback(ancestor_filter_hash_for_attribute(attribute.name().ascii_case_insensitive_hash())); - }); -} - void StyleComputer::reset_ancestor_filter() { m_ancestor_filter->clear();