Fix :has() invalidation error with nesting parent containing hover

Sets ContainsPseudoInsideHasPseudoClass flag of a :has() selector if its
argument contains nesting parent.

Currently, the CSSSelectorParser doesn't set the above flag if the :has()
selector's argument has a nesting parent selector and the nesting parent
contains a pseudo selector.

Due to this, even if the pseudo state in the nesting parent is changed,
StyleEngine skips :has() invalidation for the pseudo state change because
the :has() anchor element doesn't have AffectedByPseudoInHas flag set.

To fix this, CSSSelectorParser sets ContainsPseudoInsideHasPseudoClass
flag if the :has() selector contains a nesting parent selector in its
argument selector.

Bug: 1517866
Change-Id: I41b6f69a83db41a4e519490018bfb214a724f807
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5203146
Reviewed-by: Rune Lillesveen <futhark@chromium.org>
Commit-Queue: Byungwoo Lee <blee@igalia.com>
Cr-Commit-Position: refs/heads/main@{#1248019}
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 38d5324..db6547d 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
@@ -5391,4 +5391,63 @@
   ASSERT_EQ(GetStyleEngine().StyleForElementCount() - start_count, 1U);
 }
 
+TEST_F(AffectedByPseudoTest, AffectedByPseudoInHasWithNestingParent) {
+  SetHtmlInnerHTML(R"HTML(
+    <style>
+      .b:hover {
+        .a:has(~ &) { background-color: green; }
+      }
+    </style>
+    <div id=div1></div>
+    <div id=div2 class='a'></div>
+    <div id=div3></div>
+    <div id=div4 class='b'></div>
+    <div id=div5></div>
+  )HTML");
+
+  UpdateAllLifecyclePhasesForTest();
+  CheckAffectedByFlagsForHas(
+      "div1", {{kAffectedBySubjectHas, false},
+               {kAffectedByPseudoInHas, false},
+               {kSiblingsAffectedByHasForSiblingRelationship, false},
+               {kAncestorsOrSiblingsAffectedByHoverInHas, false}});
+  CheckAffectedByFlagsForHas(
+      "div2", {{kAffectedBySubjectHas, true},
+               {kAffectedByPseudoInHas, true},
+               {kSiblingsAffectedByHasForSiblingRelationship, true},
+               {kAncestorsOrSiblingsAffectedByHoverInHas, false}});
+  CheckAffectedByFlagsForHas(
+      "div3", {{kAffectedBySubjectHas, false},
+               {kAffectedByPseudoInHas, false},
+               {kSiblingsAffectedByHasForSiblingRelationship, true},
+               {kAncestorsOrSiblingsAffectedByHoverInHas, false}});
+  CheckAffectedByFlagsForHas(
+      "div4", {{kAffectedBySubjectHas, false},
+               {kAffectedByPseudoInHas, false},
+               {kSiblingsAffectedByHasForSiblingRelationship, true},
+               {kAncestorsOrSiblingsAffectedByHoverInHas, true}});
+  CheckAffectedByFlagsForHas(
+      "div5", {{kAffectedBySubjectHas, false},
+               {kAffectedByPseudoInHas, false},
+               {kSiblingsAffectedByHasForSiblingRelationship, true},
+               {kAncestorsOrSiblingsAffectedByHoverInHas, false}});
+
+  unsigned start_count = GetStyleEngine().StyleForElementCount();
+  GetElementById("div3")->SetHovered(true);
+  UpdateAllLifecyclePhasesForTest();
+  unsigned element_count =
+      GetStyleEngine().StyleForElementCount() - start_count;
+  ASSERT_EQ(0U, element_count);
+  GetElementById("div3")->SetHovered(false);
+  UpdateAllLifecyclePhasesForTest();
+
+  start_count = GetStyleEngine().StyleForElementCount();
+  GetElementById("div4")->SetHovered(true);
+  UpdateAllLifecyclePhasesForTest();
+  element_count = GetStyleEngine().StyleForElementCount() - start_count;
+  ASSERT_EQ(2U, element_count);
+  GetElementById("div4")->SetHovered(false);
+  UpdateAllLifecyclePhasesForTest();
+}
+
 }  // 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 70e370a..908f33b 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
@@ -1694,6 +1694,18 @@
 
   output_.push_back(
       CSSSelector(parent_rule_for_nesting_, /*is_implicit=*/false));
+
+  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.
+    found_pseudo_in_has_argument_ = true;
+  }
+
   return true;
 }
 
diff --git a/third_party/blink/web_tests/external/wpt/css/selectors/invalidation/has-with-nesting-parent-containing-hover.html b/third_party/blink/web_tests/external/wpt/css/selectors/invalidation/has-with-nesting-parent-containing-hover.html
new file mode 100644
index 0000000..8082980e
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/selectors/invalidation/has-with-nesting-parent-containing-hover.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selector Invalidation: :has() with nesting parent containing :hover</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>
+  dd, dt { background: white; }
+  dd:hover {
+    dt:has(~ &) { background: lime; }
+  }
+</style>
+<dt id=dt1>#dt1</dt>
+<dd id=dd1>#dd1, Hover me, the DT above should go lime</dd>
+<dt id=dt2>#dt2</dt>
+<dd id=dd2>#dd2, Hover me, both DTs above should go lime</dd>
+<script>
+  const white = 'rgb(255, 255, 255)';
+  const lime = 'rgb(0, 255, 0)';
+
+  function bg_color(element, color, message) {
+    assert_equals(getComputedStyle(element)['background-color'], color, message);
+  }
+
+  promise_test(async () => {
+    bg_color(dt1, white, "#dt1 initially white");
+    bg_color(dd1, white, "#dd1 initially white");
+    bg_color(dt2, white, "#dt2 initially white");
+    bg_color(dd2, white, "#dd2 initially white");
+
+    await new test_driver.Actions().pointerMove(0, 0, {origin: dd1}).send();
+
+    bg_color(dt1, lime, "#dt1 should go lime when hover #dd1");
+    bg_color(dd1, white, "#dd1 doesn't change when hover #dd1");
+    bg_color(dt2, white, "#dt2 doesn't change when hover #dd1");
+    bg_color(dd2, white, "#dd2 doesn't change when hover #dd1");
+
+    await new test_driver.Actions().pointerMove(0, 0, {origin: dt1}).send();
+
+    bg_color(dt1, white, "#dt1 should go white when hover #dt2");
+    bg_color(dd1, white, "#dd1 doesn't change when hover #dt2");
+    bg_color(dt2, white, "#dt2 doesn't change when hover #dt2");
+    bg_color(dd2, white, "#dd2 doesn't change when hover #dt2");
+
+    await new test_driver.Actions().pointerMove(0, 0, {origin: dd2}).send();
+
+    bg_color(dt1, lime, "#dt1 should go lime when hover #dd2");
+    bg_color(dd1, white, "#dd1 doesn't change when hover #dd2");
+    bg_color(dt2, lime, "#dt2 should go lime when hover #dd2");
+    bg_color(dd2, white, "#dd2 doesn't change when hover #dd2");
+  });
+</script>
\ No newline at end of file