Bug 1404789: Be a bit better at detecting distribution changes. r=bz
authorEmilio Cobos Álvarez <emilio@crisal.io>
Wed, 18 Oct 2017 16:03:41 +0200
changeset 683847 fff75157a8cd8440c588d1c71f9f0bbb9305c3e9
parent 683846 1fff041708223352fbce01c0f179e44e396c7b8b
child 683848 e214368792a2bad363b383e8efb47fd0133e7cd5
push id85474
push userbmo:emilio@crisal.io
push dateFri, 20 Oct 2017 10:02:12 +0000
reviewersbz
bugs1404789
milestone58.0a1
Bug 1404789: Be a bit better at detecting distribution changes. r=bz MozReview-Commit-ID: JqutdNJURZU
dom/base/ShadowRoot.cpp
dom/base/ShadowRoot.h
layout/base/crashtests/1404789-1.html
layout/base/crashtests/1404789-2.html
layout/base/crashtests/crashtests.list
--- a/dom/base/ShadowRoot.cpp
+++ b/dom/base/ShadowRoot.cpp
@@ -250,57 +250,58 @@ ShadowRoot::RemoveDestInsertionPoint(nsI
   }
 }
 
 void
 ShadowRoot::DistributionChanged()
 {
   // FIXME(emilio): We could be more granular in a bunch of cases.
   auto* host = GetHost();
-  if (!host) {
+  if (!host || !host->IsInComposedDoc()) {
     return;
   }
 
   auto* shell = OwnerDoc()->GetShell();
   if (!shell) {
     return;
   }
 
   // FIXME(emilio): Rename this to DestroyFramesForAndRestyle?
   shell->DestroyFramesFor(host);
 }
 
-void
+const HTMLContentElement*
 ShadowRoot::DistributeSingleNode(nsIContent* aContent)
 {
   // Find the insertion point to which the content belongs.
   HTMLContentElement* foundInsertionPoint = nullptr;
   for (HTMLContentElement* insertionPoint : mInsertionPoints) {
     if (insertionPoint->Match(aContent)) {
       if (insertionPoint->MatchedNodes().Contains(aContent)) {
         // Node is already matched into the insertion point. We are done.
-        return;
+        return insertionPoint;
       }
 
       // Matching may cause the insertion point to drop fallback content.
       if (insertionPoint->MatchedNodes().IsEmpty() &&
           insertionPoint->HasChildren()) {
         // This match will cause the insertion point to drop all fallback
         // content and used matched nodes instead. Give up on the optimization
         // and just distribute all nodes.
         DistributeAllNodes();
-        return;
+        MOZ_ASSERT(insertionPoint->MatchedNodes().Contains(aContent));
+        return insertionPoint;
       }
       foundInsertionPoint = insertionPoint;
       break;
     }
   }
 
   if (!foundInsertionPoint) {
-    return;
+    return nullptr;
   }
 
   // Find the index into the insertion point.
   nsCOMArray<nsIContent>& matchedNodes = foundInsertionPoint->MatchedNodes();
   // Find the appropriate position in the matched node list for the
   // newly distributed content.
   bool isIndexFound = false;
   ExplicitChildIterator childIterator(GetHost());
@@ -318,57 +319,43 @@ ShadowRoot::DistributeSingleNode(nsICont
   if (!isIndexFound) {
     // We have still not found an index in the insertion point,
     // thus it must be at the end.
     MOZ_ASSERT(childIterator.Seek(aContent, nullptr),
                "Trying to match a node that is not a candidate to be matched");
     foundInsertionPoint->AppendMatchedNode(aContent);
   }
 
-  // Handle the case where the parent of the insertion point has a ShadowRoot.
-  // The node distributed into the insertion point must be reprojected to the
-  // insertion points of the parent's ShadowRoot.
-  if (auto* parentShadow = foundInsertionPoint->GetParent()->GetShadowRoot()) {
-    parentShadow->DistributeSingleNode(aContent);
-  }
-
-  DistributionChanged();
+  return foundInsertionPoint;
 }
 
-void
+const HTMLContentElement*
 ShadowRoot::RemoveDistributedNode(nsIContent* aContent)
 {
   // Find insertion point containing the content and remove the node.
   for (HTMLContentElement* insertionPoint : mInsertionPoints) {
     if (!insertionPoint->MatchedNodes().Contains(aContent)) {
       continue;
     }
 
     // Removing the matched node may cause the insertion point to use
     // fallback content.
     if (insertionPoint->MatchedNodes().Length() == 1 &&
         insertionPoint->HasChildren()) {
       // Removing the matched node will cause fallback content to be
       // used instead. Give up optimization and distribute all nodes.
       DistributeAllNodes();
-      return;
+      return insertionPoint;
     }
 
     insertionPoint->RemoveMatchedNode(aContent);
+    return insertionPoint;
+  }
 
-    // Handle the case where the parent of the insertion point has a ShadowRoot.
-    // The removed node needs to be removed from the insertion points of the
-    // parent's ShadowRoot.
-    if (auto* parentShadow = insertionPoint->GetParent()->GetShadowRoot()) {
-      parentShadow->RemoveDistributedNode(aContent);
-    }
-
-    DistributionChanged();
-    return;
-  }
+  return nullptr;
 }
 
 void
 ShadowRoot::DistributeAllNodes()
 {
   // Create node pool.
   nsTArray<nsIContent*> nodePool;
   ExplicitChildIterator childIterator(GetHost());
@@ -505,74 +492,123 @@ ShadowRoot::AttributeChanged(nsIDocument
                              int32_t aModType,
                              const nsAttrValue* aOldValue)
 {
   if (!IsPooledNode(aElement)) {
     return;
   }
 
   // Attributes may change insertion point matching, find its new distribution.
-  RemoveDistributedNode(aElement);
-  DistributeSingleNode(aElement);
+  //
+  // FIXME(emilio): What about state changes?
+  if (!RedistributeElement(aElement)) {
+    return;
+  }
+
+  if (!aElement->IsInComposedDoc()) {
+    return;
+  }
+
+  auto* shell = OwnerDoc()->GetShell();
+  if (!shell) {
+    return;
+  }
+
+  shell->DestroyFramesFor(aElement);
+}
+
+bool
+ShadowRoot::RedistributeElement(Element* aElement)
+{
+  auto* oldInsertionPoint = RemoveDistributedNode(aElement);
+  auto* newInsertionPoint = DistributeSingleNode(aElement);
+
+  if (oldInsertionPoint == newInsertionPoint) {
+    if (oldInsertionPoint) {
+      if (auto* shadow = oldInsertionPoint->GetParent()->GetShadowRoot()) {
+        return shadow->RedistributeElement(aElement);
+      }
+    }
+
+    return false;
+  }
+
+  while (oldInsertionPoint) {
+    // Handle the case where the parent of the insertion point has a ShadowRoot.
+    // The node distributed into the insertion point must be reprojected to the
+    // insertion points of the parent's ShadowRoot.
+    auto* shadow = oldInsertionPoint->GetParent()->GetShadowRoot();
+    if (!shadow) {
+      break;
+    }
+
+    oldInsertionPoint = shadow->RemoveDistributedNode(aElement);
+  }
+
+  while (newInsertionPoint) {
+    // Handle the case where the parent of the insertion point has a ShadowRoot.
+    // The node distributed into the insertion point must be reprojected to the
+    // insertion points of the parent's ShadowRoot.
+    auto* shadow = newInsertionPoint->GetParent()->GetShadowRoot();
+    if (!shadow) {
+      break;
+    }
+
+    newInsertionPoint = shadow->DistributeSingleNode(aElement);
+  }
+
+  return true;
 }
 
 void
 ShadowRoot::ContentAppended(nsIDocument* aDocument,
                             nsIContent* aContainer,
                             nsIContent* aFirstNewContent)
 {
-  if (mInsertionPointChanged) {
-    DistributeAllNodes();
-    mInsertionPointChanged = false;
-    return;
-  }
-
-  // Watch for new nodes added to the pool because the node
-  // may need to be added to an insertion point.
-  nsIContent* currentChild = aFirstNewContent;
-  while (currentChild) {
-    // Add insertion point to destination insertion points of fallback content.
-    if (nsContentUtils::IsContentInsertionPoint(aContainer)) {
-      HTMLContentElement* content = HTMLContentElement::FromContent(aContainer);
-      if (content && content->MatchedNodes().IsEmpty()) {
-        currentChild->DestInsertionPoints().AppendElement(aContainer);
-      }
-    }
-
-    if (IsPooledNode(currentChild)) {
-      DistributeSingleNode(currentChild);
-    }
-
-    currentChild = currentChild->GetNextSibling();
+  for (nsIContent* content = aFirstNewContent;
+       content;
+       content = content->GetNextSibling()) {
+    ContentInserted(aDocument, aContainer, aFirstNewContent);
   }
 }
 
 void
 ShadowRoot::ContentInserted(nsIDocument* aDocument,
                             nsIContent* aContainer,
                             nsIContent* aChild)
 {
   if (mInsertionPointChanged) {
     DistributeAllNodes();
     mInsertionPointChanged = false;
     return;
   }
 
+  // Add insertion point to destination insertion points of fallback content.
+  if (nsContentUtils::IsContentInsertionPoint(aContainer)) {
+    HTMLContentElement* content = HTMLContentElement::FromContent(aContainer);
+    if (content && content->MatchedNodes().IsEmpty()) {
+      aChild->DestInsertionPoints().AppendElement(aContainer);
+    }
+  }
+
   // Watch for new nodes added to the pool because the node
   // may need to be added to an insertion point.
   if (IsPooledNode(aChild)) {
-    // Add insertion point to destination insertion points of fallback content.
-    if (nsContentUtils::IsContentInsertionPoint(aContainer)) {
-      HTMLContentElement* content = HTMLContentElement::FromContent(aContainer);
-      if (content && content->MatchedNodes().IsEmpty()) {
-        aChild->DestInsertionPoints().AppendElement(aContainer);
+    auto* insertionPoint = DistributeSingleNode(aChild);
+    while (insertionPoint) {
+      // Handle the case where the parent of the insertion point has a ShadowRoot.
+      // The node distributed into the insertion point must be reprojected to the
+      // insertion points of the parent's ShadowRoot.
+      auto* parentShadow = insertionPoint->GetParent()->GetShadowRoot();
+      if (!parentShadow) {
+        break;
       }
+
+      insertionPoint = parentShadow->DistributeSingleNode(aChild);
     }
-
-    DistributeSingleNode(aChild);
   }
 }
 
 void
 ShadowRoot::ContentRemoved(nsIDocument* aDocument,
                            nsIContent* aContainer,
                            nsIContent* aChild,
                            nsIContent* aPreviousSibling)
@@ -590,17 +626,30 @@ ShadowRoot::ContentRemoved(nsIDocument* 
     if (content && content->MatchedNodes().IsEmpty()) {
       aChild->DestInsertionPoints().Clear();
     }
   }
 
   // Watch for node that is removed from the pool because
   // it may need to be removed from an insertion point.
   if (IsPooledNode(aChild)) {
-    RemoveDistributedNode(aChild);
+    auto* insertionPoint = RemoveDistributedNode(aChild);
+    while (insertionPoint) {
+      // Handle the case where the parent of the insertion point has a
+      // ShadowRoot.
+      //
+      // The removed node needs to be removed from the insertion points of the
+      // parent's ShadowRoot.
+      auto* parentShadow = insertionPoint->GetParent()->GetShadowRoot();
+      if (!parentShadow) {
+        break;
+      }
+
+      insertionPoint = parentShadow->RemoveDistributedNode(aChild);
+    }
   }
 }
 
 nsresult
 ShadowRoot::Clone(mozilla::dom::NodeInfo *aNodeInfo, nsINode **aResult,
                   bool aPreallocateChildren) const
 {
   *aResult = nullptr;
--- a/dom/base/ShadowRoot.h
+++ b/dom/base/ShadowRoot.h
@@ -58,28 +58,43 @@ public:
    * insertion points in this ShadowRoot.
    */
   void DistributeAllNodes();
 
 private:
   /**
    * Distributes a single explicit child of the pool host to the content
    * insertion points in this ShadowRoot.
+   *
+   * Returns the insertion point the element is distributed to after this call.
+   *
+   * Note that this doesn't handle distributing the node in the insertion point
+   * parent's shadow root.
    */
-  void DistributeSingleNode(nsIContent* aContent);
+  const HTMLContentElement* DistributeSingleNode(nsIContent* aContent);
 
   /**
    * Removes a single explicit child of the pool host from the content
    * insertion points in this ShadowRoot.
+   *
+   * Returns the old insertion point, if any.
+   *
+   * Note that this doesn't handle removing the node in the returned insertion
+   * point parent's shadow root.
    */
-  void RemoveDistributedNode(nsIContent* aContent);
+  const HTMLContentElement* RemoveDistributedNode(nsIContent* aContent);
 
   /**
-   * Called when we redistribute content in such a way that new insertion points
-   * come into existence, or elements are moved between insertion points.
+   * Redistributes a node of the pool, and returns whether the distribution
+   * changed.
+   */
+  bool RedistributeElement(Element*);
+
+  /**
+   * Called when we redistribute content after insertion points have changed.
    */
   void DistributionChanged();
 
   bool IsPooledNode(nsIContent* aChild) const;
 
 public:
   void AddInsertionPoint(HTMLContentElement* aInsertionPoint);
   void RemoveInsertionPoint(HTMLContentElement* aInsertionPoint);
new file mode 100644
--- /dev/null
+++ b/layout/base/crashtests/1404789-1.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<script>
+  // Test for content redistribution outside of the document.
+  // Passes if it doesn't assert.
+  let host = document.createElement('div');
+  host.innerHTML = "<div id='foo'></div>";
+
+  let shadowRoot = host.createShadowRoot();
+  shadowRoot.innerHTML = "<content select='#foo'></content>";
+
+  host.firstElementChild.removeAttribute('id');
+
+  // Move to the document, do the same.
+  document.documentElement.appendChild(host);
+  host.firstElementChild.setAttribute('id', 'foo');
+</script>
new file mode 100644
--- /dev/null
+++ b/layout/base/crashtests/1404789-2.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<iframe style="display: none" src="1404789-1.html"></iframe>
--- a/layout/base/crashtests/crashtests.list
+++ b/layout/base/crashtests/crashtests.list
@@ -502,11 +502,13 @@ load 1397398-1.html
 load 1397398-2.html
 load 1397398-3.html
 load 1398500.html
 load 1400438-1.html
 load 1400599-1.html
 load 1401739.html
 load 1401840.html
 load 1402476.html
+load 1404789-1.html
+load 1404789-2.html
 load 1406562.html
 load 1409088.html
 load 1409147.html