Fix ':has()' matching and invalidation error with nesting parent

1. Fix incorrect cache hit for ':has()' selector matching:

In case of having multiple style rules with same ':has()' selector
expression but different nesting parent, the ':has()' selector
matching cache (CheckPseudoCacheScope) returned incorrect cache hit
result because it used ':has()' argument selector text as the cache
key for the ':has()' match result.
To fix this bug, this CL added a 'CSSSelector' method that returns
selector text with replacing pseudo parent expressions ('&') with
equivalent pseudo ':is' expression (':is(<nesting parent>)'), and let
the cache uses the replaced selector string as its cache key.

2. Fix ':has()' invalidation error with complex nesting parent:

To build invalidation set for a ':has()' that contains a logical
combinations containing a complex selector, 'RuleFeatureSet' checks
a flag in the ':has()' selector:
- ContainsComplexLogicalCombinationsInsideHasPseudoClass
'CSSSelectorParser' need to set the flag while parsing ':has()'
argument selector, but the flag was not set in case that the ':has()'
contains complex nesting parent.
This CL sets the flag when a ':has()' contains nesting parent selector
so that 'RuleFeatureSet' successfully build invalidation sets for the
complex nesting parent inside ':has()'.

Low-Coverage-Reason: COVERAGE_UNDERREPORTED - The unit tests and wpt tests in this CL cover the changes, and most of the lines detected in css_selector.cc are existing code that are not directly related to this change and should have been tested previously.

Bug: 350946979
Change-Id: Ie3b457df2ac66e0e5884beb0e29f3877b436e2a3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5680906
Reviewed-by: Anders Hartvoll Ruud <andruud@chromium.org>
Commit-Queue: Byungwoo Lee <blee@igalia.com>
Cr-Commit-Position: refs/heads/main@{#1335644}
diff --git a/third_party/blink/renderer/core/css/affected_by_pseudo_test.cc b/third_party/blink/renderer/core/css/affected_by_pseudo_test.cc
index c94caa8..b2650e8 100644
--- a/third_party/blink/renderer/core/css/affected_by_pseudo_test.cc
+++ b/third_party/blink/renderer/core/css/affected_by_pseudo_test.cc
@@ -5456,4 +5456,70 @@
   UpdateAllLifecyclePhasesForTest();
 }
 
+TEST_F(AffectedByPseudoTest, AffectedByPseudoInHasWithNestingComplexParent) {
+  SetHtmlInnerHTML(R"HTML(
+    <style>
+      .b .c {
+        .a:has(> &) { background-color: green; }
+      }
+    </style>
+    <div id=div1></div>
+    <div id=div2>
+      <div id=div3></div>
+      <div id=div4 class='a'>
+        <div id=div5 class='c'></div>
+      </div>
+    </div>
+  )HTML");
+
+  UpdateAllLifecyclePhasesForTest();
+  CheckAffectedByFlagsForHas(
+      "div1", {{kAffectedBySubjectHas, false},
+               {kAffectedByPseudoInHas, false},
+               {kAncestorsOrAncestorSiblingsAffectedByHas, false},
+               {kAffectedByLogicalCombinationsInHas, false}});
+  CheckAffectedByFlagsForHas(
+      "div2", {{kAffectedBySubjectHas, false},
+               {kAffectedByPseudoInHas, false},
+               {kAncestorsOrAncestorSiblingsAffectedByHas, false},
+               {kAffectedByLogicalCombinationsInHas, false}});
+  CheckAffectedByFlagsForHas(
+      "div3", {{kAffectedBySubjectHas, false},
+               {kAffectedByPseudoInHas, false},
+               {kAncestorsOrAncestorSiblingsAffectedByHas, false},
+               {kAffectedByLogicalCombinationsInHas, false}});
+  CheckAffectedByFlagsForHas("div4",
+                             {{kAffectedBySubjectHas, true},
+                              {kAffectedByPseudoInHas, true},
+                              {kAncestorsOrAncestorSiblingsAffectedByHas, true},
+                              {kAffectedByLogicalCombinationsInHas, true}});
+  CheckAffectedByFlagsForHas("div5",
+                             {{kAffectedBySubjectHas, false},
+                              {kAffectedByPseudoInHas, false},
+                              {kAncestorsOrAncestorSiblingsAffectedByHas, true},
+                              {kAffectedByLogicalCombinationsInHas, false}});
+
+  unsigned start_count = GetStyleEngine().StyleForElementCount();
+  GetElementById("div1")->setAttribute(html_names::kClassAttr,
+                                       AtomicString("b"));
+  UpdateAllLifecyclePhasesForTest();
+  unsigned element_count =
+      GetStyleEngine().StyleForElementCount() - start_count;
+  ASSERT_EQ(0U, element_count);
+
+  start_count = GetStyleEngine().StyleForElementCount();
+  GetElementById("div3")->setAttribute(html_names::kClassAttr,
+                                       AtomicString("b"));
+  UpdateAllLifecyclePhasesForTest();
+  element_count = GetStyleEngine().StyleForElementCount() - start_count;
+  ASSERT_EQ(0U, element_count);
+
+  start_count = GetStyleEngine().StyleForElementCount();
+  GetElementById("div2")->setAttribute(html_names::kClassAttr,
+                                       AtomicString("b"));
+  UpdateAllLifecyclePhasesForTest();
+  element_count = GetStyleEngine().StyleForElementCount() - start_count;
+  ASSERT_EQ(2U, element_count);
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/css/check_pseudo_has_cache_scope.cc b/third_party/blink/renderer/core/css/check_pseudo_has_cache_scope.cc
index 4a630c3..01e5fbd 100644
--- a/third_party/blink/renderer/core/css/check_pseudo_has_cache_scope.cc
+++ b/third_party/blink/renderer/core/css/check_pseudo_has_cache_scope.cc
@@ -48,7 +48,7 @@
   // To increase the cache hit ratio, we need to have a same cache key
   // for multiple selector instances those are actually has a same selector.
   // TODO(blee@igalia.com) Find a way to get hash key without serialization.
-  String selector_text = selector->SelectorText();
+  String selector_text = selector->SelectorTextExpandingPseudoParent();
 
   DCHECK(document);
   DCHECK(document->GetCheckPseudoHasCacheScope());
diff --git a/third_party/blink/renderer/core/css/css_selector.cc b/third_party/blink/renderer/core/css/css_selector.cc
index ae55bd1..f174c00 100644
--- a/third_party/blink/renderer/core/css/css_selector.cc
+++ b/third_party/blink/renderer/core/css/css_selector.cc
@@ -60,6 +60,8 @@
 
 namespace {
 
+constexpr bool kExpandPseudoParent = true;
+
 unsigned MaximumSpecificity(const CSSSelectorList* list) {
   if (!list) {
     return 0;
@@ -1030,18 +1032,29 @@
   builder.Append('|');
 }
 
-static void SerializeSelectorList(const CSSSelectorList* selector_list,
-                                  StringBuilder& builder) {
+// static
+template <bool expand_pseudo_parent>
+void CSSSelector::SerializeSelectorList(const CSSSelectorList* selector_list,
+                                        StringBuilder& builder) {
   const CSSSelector* first_sub_selector = selector_list->First();
   for (const CSSSelector* sub_selector = first_sub_selector; sub_selector;
        sub_selector = CSSSelectorList::Next(*sub_selector)) {
     if (sub_selector != first_sub_selector) {
       builder.Append(", ");
     }
-    builder.Append(sub_selector->SelectorText());
+    builder.Append(sub_selector->SelectorTextInternal<expand_pseudo_parent>());
   }
 }
 
+String CSSSelector::SelectorText() const {
+  return SelectorTextInternal<!kExpandPseudoParent>();
+}
+
+String CSSSelector::SelectorTextExpandingPseudoParent() const {
+  return SelectorTextInternal<kExpandPseudoParent>();
+}
+
+template <bool expand_pseudo_parent>
 bool CSSSelector::SerializeSimpleSelector(StringBuilder& builder) const {
   bool suppress_selector_list = false;
   if (Match() == kId) {
@@ -1091,7 +1104,8 @@
         // Only relevant for :nth-child, not :nth-of-type.
         if (data_.rare_data_->selector_list_ != nullptr) {
           builder.Append(" of ");
-          SerializeSelectorList(data_.rare_data_->selector_list_, builder);
+          SerializeSelectorList<expand_pseudo_parent>(
+              data_.rare_data_->selector_list_, builder);
           suppress_selector_list = true;
         }
 
@@ -1120,7 +1134,17 @@
       case kPseudoWhere:
         break;
       case kPseudoParent:
-        builder.Append('&');
+        if constexpr (expand_pseudo_parent) {
+          // Replace parent pseudo with equivalent :is() pseudo.
+          builder.Append(":is");
+          if (auto* parent = SelectorListOrParent()) {
+            builder.Append('(');
+            builder.Append(parent->SelectorTextExpandingPseudoParent());
+            builder.Append(')');
+          }
+        } else {
+          builder.Append('&');
+        }
         break;
       case kPseudoRelativeAnchor:
         NOTREACHED_IN_MIGRATION();
@@ -1232,12 +1256,13 @@
 
   if (SelectorList() && !suppress_selector_list) {
     builder.Append('(');
-    SerializeSelectorList(SelectorList(), builder);
+    SerializeSelectorList<expand_pseudo_parent>(SelectorList(), builder);
     builder.Append(')');
   }
   return true;
 }
 
+template <bool expand_pseudo_parent>
 const CSSSelector* CSSSelector::SerializeCompound(
     StringBuilder& builder) const {
   if (Match() == kTag && !IsImplicit()) {
@@ -1249,7 +1274,8 @@
 
   for (const CSSSelector* simple_selector = this; simple_selector;
        simple_selector = simple_selector->NextSimpleSelector()) {
-    if (!simple_selector->SerializeSimpleSelector(builder)) {
+    if (!simple_selector->SerializeSimpleSelector<expand_pseudo_parent>(
+            builder)) {
       return nullptr;
     }
     if (simple_selector->Relation() != kSubSelector &&
@@ -1260,12 +1286,13 @@
   return nullptr;
 }
 
-String CSSSelector::SelectorText() const {
+template <bool expand_pseudo_parent>
+String CSSSelector::SelectorTextInternal() const {
   String result;
   for (const CSSSelector* compound = this; compound;
        compound = compound->NextSimpleSelector()) {
     StringBuilder builder;
-    compound = compound->SerializeCompound(builder);
+    compound = compound->SerializeCompound<expand_pseudo_parent>(builder);
     if (!compound) {
       return builder.ReleaseString() + result;
     }
@@ -1335,7 +1362,7 @@
     SerializeIdentifierOrAny(TagQName().LocalName(), UniversalSelectorAtom(),
                              builder);
   } else {
-    SerializeSimpleSelector(builder);
+    SerializeSimpleSelector<!kExpandPseudoParent>(builder);
   }
   return builder.ToString();
 }
diff --git a/third_party/blink/renderer/core/css/css_selector.h b/third_party/blink/renderer/core/css/css_selector.h
index ab3eb7b..a86023ce 100644
--- a/third_party/blink/renderer/core/css/css_selector.h
+++ b/third_party/blink/renderer/core/css/css_selector.h
@@ -156,6 +156,11 @@
   ~CSSSelector();
 
   String SelectorText() const;
+  // Like `SelectorText`, but replaces any '&' selectors
+  // with ':is(<parent rule selector list>)'. This is needed
+  // by the :has() cache, because it uses the serialization of
+  // the selector as a key.
+  String SelectorTextExpandingPseudoParent() const;
   String SimpleSelectorTextForDebug() const;
 
   CSSSelector& operator=(const CSSSelector&) = delete;
@@ -712,9 +717,20 @@
 
   unsigned SpecificityForOneSelector() const;
   unsigned SpecificityForPage() const;
+
+  template <bool expand_pseudo_parent>
   bool SerializeSimpleSelector(WTF::StringBuilder& builder) const;
+
+  template <bool expand_pseudo_parent>
   const CSSSelector* SerializeCompound(WTF::StringBuilder&) const;
 
+  template <bool expand_pseudo_parent>
+  static void SerializeSelectorList(const CSSSelectorList* selector_list,
+                                    WTF::StringBuilder& builder);
+
+  template <bool expand_pseudo_parent>
+  String SelectorTextInternal() const;
+
   struct RareData : public GarbageCollected<RareData> {
     explicit RareData(const AtomicString& value);
     ~RareData();
diff --git a/third_party/blink/renderer/core/css/css_selector_test.cc b/third_party/blink/renderer/core/css/css_selector_test.cc
index ba67a7b..5fef426 100644
--- a/third_party/blink/renderer/core/css/css_selector_test.cc
+++ b/third_party/blink/renderer/core/css/css_selector_test.cc
@@ -347,4 +347,51 @@
             selector[0].Specificity());
 }
 
+TEST(CSSSelector, CheckSelectorTextExpandingPseudoParent) {
+  test::TaskEnvironment task_environment;
+
+  css_test_helpers::TestStyleSheet sheet;
+  sheet.AddCSSRules(
+      ".a { .b { .c, &.c, .c:has(&) {} } }"
+      ".d .e { .f:has(> &) {} }");
+  RuleSet& rule_set = sheet.GetRuleSet();
+
+  base::span<const RuleData> rules = rule_set.ClassRules(AtomicString("a"));
+  ASSERT_EQ(1u, rules.size());
+  const CSSSelector* selector = &rules[0].Selector();
+  EXPECT_EQ(".a", selector->SelectorText());
+
+  rules = rule_set.ClassRules(AtomicString("b"));
+  ASSERT_EQ(1u, rules.size());
+  selector = &rules[0].Selector();
+  EXPECT_EQ("& .b", selector->SelectorText());
+  EXPECT_EQ(":is(.a) .b", selector->SelectorTextExpandingPseudoParent());
+
+  rules = rule_set.ClassRules(AtomicString("c"));
+  ASSERT_EQ(3u, rules.size());
+  selector = &rules[0].Selector();
+  EXPECT_EQ("& .c", selector->SelectorText());
+  EXPECT_EQ(":is(:is(.a) .b) .c",
+            selector->SelectorTextExpandingPseudoParent());
+  selector = &rules[1].Selector();
+  EXPECT_EQ("&.c", selector->SelectorText());
+  EXPECT_EQ(":is(:is(.a) .b).c", selector->SelectorTextExpandingPseudoParent());
+  selector = &rules[2].Selector();
+  EXPECT_EQ(".c:has(&)", selector->SelectorText());
+  EXPECT_EQ(".c:has(:is(:is(.a) .b))",
+            selector->SelectorTextExpandingPseudoParent());
+
+  rules = rule_set.ClassRules(AtomicString("e"));
+  ASSERT_EQ(1u, rules.size());
+  selector = &rules[0].Selector();
+  EXPECT_EQ(".d .e", selector->SelectorText());
+
+  rules = rule_set.ClassRules(AtomicString("f"));
+  ASSERT_EQ(1u, rules.size());
+  selector = &rules[0].Selector();
+  EXPECT_EQ(".f:has(> &)", selector->SelectorText());
+  EXPECT_EQ(".f:has(> :is(.d .e))",
+            selector->SelectorTextExpandingPseudoParent());
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/css/parser/css_selector_parser.cc b/third_party/blink/renderer/core/css/parser/css_selector_parser.cc
index 2e255a8a..42e2f252e 100644
--- a/third_party/blink/renderer/core/css/parser/css_selector_parser.cc
+++ b/third_party/blink/renderer/core/css/parser/css_selector_parser.cc
@@ -1916,13 +1916,15 @@
 
   if (is_inside_has_argument_) {
     // In case that a nesting parent selector is inside a :has() pseudo class,
-    // mark the :has() containing a pseudo selector so that the StyleEngine can
-    // invalidate the anchor element of the :has() for a pseudo state change
-    // in the parent selector. (crbug.com/1517866)
-    // This ignores whether the nesting parent actually contains a pseudo to
-    // avoid nesting parent lookup overhead and the complexity caused by
-    // reparenting style rules.
+    // mark the :has() containing a pseudo selector and a complex selector
+    // so that the StyleEngine can invalidate the anchor element of the :has()
+    // for a pseudo state change (crbug.com/1517866) or a complex selector
+    // state change (crbug.com/350946979) in the parent selector.
+    // These ignore whether the nesting parent actually contains a pseudo or
+    // complex selector to avoid nesting parent lookup overhead and the
+    // complexity caused by reparenting style rules.
     found_pseudo_in_has_argument_ = true;
+    found_complex_logical_combinations_in_has_argument_ = true;
   }
 
   return true;
diff --git a/third_party/blink/web_tests/external/wpt/css/selectors/invalidation/has-with-nesting-parent-containing-complex.html b/third_party/blink/web_tests/external/wpt/css/selectors/invalidation/has-with-nesting-parent-containing-complex.html
new file mode 100644
index 0000000..4ed6111
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/selectors/invalidation/has-with-nesting-parent-containing-complex.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selector Invalidation: :has() with nesting parent containing complex selector</title>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<style>
+  .anchor { background-color: white; }
+
+  .ancestor .descendant {
+    .anchor:has(&) { background-color: blue; }
+  }
+
+  .ancestor .child {
+    .anchor:has(> &) { background-color: lightblue; }
+  }
+
+  .ancestor_prev ~ div .descendant {
+    .anchor:has(&) { background-color: yellow; }
+  }
+
+  .ancestor_prev ~ div.ancestor .descendant {
+    .anchor:has(&) { background-color: yellowgreen; }
+  }
+
+  .prev ~ .indirect_next {
+    .anchor:has(~ &) { background-color: green; }
+  }
+
+  .prev ~ .direct_next {
+    .anchor:has(+ &) { background-color: lightgreen; }
+  }
+</style>
+<div><div id="grand_parent1">
+  <div id="parent1">
+    <div id="anchor1" class="anchor">
+      <div><div class="descendant"></div></div>
+    </div>
+  </div>
+</div></div>
+<div><div id="grand_parent2">
+  <div id="parent2">
+    <div id="anchor2" class="anchor">
+      <div class="child"></div>
+    </div>
+  </div>
+</div></div>
+<div><div id="grand_parent_indirect_prev3"></div>
+     <div id="grand_parent_direct_prev3"></div>
+     <div id="grand_parent3">
+       <div id="parent_indirect_prev3"></div>
+       <div id="parent_direct_prev3"></div>
+       <div id="parent3">
+         <div id="anchor_indirect_prev3"></div>
+         <div id="anchor_direct_prev3"></div>
+         <div id="anchor3" class="anchor">
+           <div><div class="descendant"></div></div>
+         </div>
+       </div>
+</div></div>
+<div><div id="indirect_prev4"></div>
+     <div id="direct_prev4"></div>
+     <div id="anchor4" class="anchor"></div>
+     <div></div><div class="indirect_next">
+</div></div>
+<div><div id="indirect_prev5"></div>
+     <div id="direct_prev5"></div>
+     <div id="anchor5" class="anchor"></div>
+     <div class="direct_next">
+</div></div>
+<script>
+  const white = "rgb(255, 255, 255)";
+  const blue = "rgb(0, 0, 255)";
+  const lightblue = "rgb(173, 216, 230)";
+  const yellow = "rgb(255, 255, 0)";
+  const yellowgreen = "rgb(154, 205, 50)";
+  const green = "rgb(0, 128, 0)";
+  const lightgreen = "rgb(144, 238, 144)";
+
+  function bg_color(element, color, message) {
+    promise_test(async () => {
+      assert_equals(getComputedStyle(element)['background-color'], color);
+    }, message);
+  }
+
+  function add_class_and_check_bg_color(
+      element_to_add, class_name, has_anchor, color) {
+    promise_test(async () => {
+      element_to_add.classList.add(class_name);
+      assert_equals(getComputedStyle(has_anchor)['background-color'], color);
+    }, `#${has_anchor.id} becomes ${color} after adding .${class_name} to #${element_to_add.id}`);
+  }
+
+  function remove_class_and_check_bg_color(
+      element_to_remove, class_name, has_anchor, color) {
+    promise_test(async () => {
+      element_to_remove.classList.remove(class_name);
+      assert_equals(getComputedStyle(has_anchor)['background-color'], color);
+    }, `#${has_anchor.id} becomes ${color} after removing .${class_name} from #${element_to_remove.id}`);
+  }
+
+  bg_color(anchor1, white, "#anchor1 initially white");
+  add_class_and_check_bg_color(grand_parent1, "ancestor", anchor1, blue);
+  remove_class_and_check_bg_color(grand_parent1, "ancestor", anchor1, white);
+  add_class_and_check_bg_color(parent1, "ancestor", anchor1, blue);
+  remove_class_and_check_bg_color(parent1, "ancestor", anchor1, white);
+
+  bg_color(anchor2, white, "#anchor2 initially white");
+  add_class_and_check_bg_color(grand_parent2, "ancestor", anchor2, lightblue);
+  remove_class_and_check_bg_color(grand_parent2, "ancestor", anchor2, white);
+  add_class_and_check_bg_color(parent2, "ancestor", anchor2, lightblue);
+  remove_class_and_check_bg_color(parent2, "ancestor", anchor2, white);
+
+  bg_color(anchor3, white, "#anchor3 initially white");
+  add_class_and_check_bg_color(grand_parent_indirect_prev3, "ancestor_prev",
+                               anchor3, yellow);
+  add_class_and_check_bg_color(grand_parent3, "ancestor", anchor3, yellowgreen);
+  remove_class_and_check_bg_color(grand_parent3, "ancestor", anchor3, yellow);
+  remove_class_and_check_bg_color(grand_parent_indirect_prev3, "ancestor_prev",
+                                  anchor3, white);
+  add_class_and_check_bg_color(grand_parent_direct_prev3, "ancestor_prev",
+                               anchor3, yellow);
+  remove_class_and_check_bg_color(grand_parent_direct_prev3, "ancestor_prev",
+                                  anchor3, white);
+  add_class_and_check_bg_color(parent_indirect_prev3, "ancestor_prev",
+                               anchor3, yellow);
+  add_class_and_check_bg_color(parent3, "ancestor", anchor3, yellowgreen);
+  remove_class_and_check_bg_color(parent3, "ancestor", anchor3, yellow);
+  remove_class_and_check_bg_color(parent_indirect_prev3, "ancestor_prev",
+                                  anchor3, white);
+  add_class_and_check_bg_color(parent_direct_prev3, "ancestor_prev",
+                               anchor3, yellow);
+  remove_class_and_check_bg_color(parent_direct_prev3, "ancestor_prev",
+                                  anchor3, white);
+  add_class_and_check_bg_color(anchor_indirect_prev3, "ancestor_prev",
+                               anchor3, yellow);
+  remove_class_and_check_bg_color(anchor_indirect_prev3, "ancestor_prev",
+                                  anchor3, white);
+  add_class_and_check_bg_color(anchor_direct_prev3, "ancestor_prev",
+                               anchor3, yellow);
+  remove_class_and_check_bg_color(anchor_direct_prev3, "ancestor_prev",
+                                  anchor3, white);
+
+  bg_color(anchor4, white, "#anchor4 initially white");
+  add_class_and_check_bg_color(indirect_prev4, "prev", anchor4, green);
+  remove_class_and_check_bg_color(indirect_prev4, "prev", anchor4, white);
+  add_class_and_check_bg_color(direct_prev4, "prev", anchor4, green);
+  remove_class_and_check_bg_color(direct_prev4, "prev", anchor4, white);
+
+  bg_color(anchor5, white, "#anchor5 initially white");
+  add_class_and_check_bg_color(indirect_prev5, "prev", anchor5, lightgreen);
+  remove_class_and_check_bg_color(indirect_prev5, "prev", anchor5, white);
+  add_class_and_check_bg_color(direct_prev5, "prev", anchor5, lightgreen);
+  remove_class_and_check_bg_color(direct_prev5, "prev", anchor5, white);
+</script>
\ No newline at end of file