Implement slowCompareTo(NamingLens) with visitor.

Bug: 171867022
Change-Id: I430632330fc5e7aeb37aaa8a0ffa14d9fa7909ad
diff --git a/src/main/java/com/android/tools/r8/graph/DexField.java b/src/main/java/com/android/tools/r8/graph/DexField.java
index 755ff8fe..5f95369 100644
--- a/src/main/java/com/android/tools/r8/graph/DexField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexField.java
@@ -5,19 +5,17 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.references.FieldReference;
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import com.android.tools.r8.utils.structural.StructuralAccept;
-import com.android.tools.r8.utils.structural.StructuralItem;
 import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.Collections;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-public class DexField extends DexMember<DexEncodedField, DexField>
-    implements StructuralItem<DexField> {
+public class DexField extends DexMember<DexEncodedField, DexField> {
 
   public final DexType type;
 
@@ -144,16 +142,8 @@
   }
 
   @Override
-  public int slowCompareTo(DexField other, NamingLens namingLens) {
-    int result = holder.slowCompareTo(other.holder, namingLens);
-    if (result != 0) {
-      return result;
-    }
-    result = namingLens.lookupName(this).compareTo(namingLens.lookupName(other));
-    if (result != 0) {
-      return result;
-    }
-    return type.slowCompareTo(other.type, namingLens);
+  public void acceptCompareTo(DexField other, CompareToVisitor visitor) {
+    visitor.visitDexField(this, other);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethod.java b/src/main/java/com/android/tools/r8/graph/DexMethod.java
index 6a07b34..249e77e 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -5,12 +5,11 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import com.android.tools.r8.utils.structural.StructuralAccept;
-import com.android.tools.r8.utils.structural.StructuralItem;
 import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.ArrayList;
 import java.util.List;
@@ -18,8 +17,7 @@
 import java.util.function.Consumer;
 import java.util.function.Function;
 
-public class DexMethod extends DexMember<DexEncodedMethod, DexMethod>
-    implements StructuralItem<DexMethod> {
+public class DexMethod extends DexMember<DexEncodedMethod, DexMethod> {
 
   public final DexProto proto;
 
@@ -47,6 +45,11 @@
     return this;
   }
 
+  @Override
+  public void acceptCompareTo(DexMethod other, CompareToVisitor visitor) {
+    visitor.visitDexMethod(this, other);
+  }
+
   public DexType getHolderType() {
     return holder;
   }
@@ -199,19 +202,6 @@
   }
 
   @Override
-  public int slowCompareTo(DexMethod other, NamingLens namingLens) {
-    int result = holder.slowCompareTo(other.holder, namingLens);
-    if (result != 0) {
-      return result;
-    }
-    result = namingLens.lookupName(this).compareTo(namingLens.lookupName(other));
-    if (result != 0) {
-      return result;
-    }
-    return proto.slowCompareTo(other.proto, namingLens);
-  }
-
-  @Override
   public boolean match(DexMethod method) {
     return method.name == name && method.proto == proto;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java b/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
index f34c70c..55522d5 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
@@ -8,12 +8,14 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.Objects;
 import org.objectweb.asm.Handle;
 import org.objectweb.asm.Opcodes;
 
-public class DexMethodHandle extends IndexedDexItem implements
-    PresortedComparable<DexMethodHandle> {
+public class DexMethodHandle extends IndexedDexItem
+    implements PresortedComparable<DexMethodHandle> {
 
   public enum MethodHandleType {
     STATIC_PUT((short) 0x00),
@@ -313,31 +315,21 @@
   }
 
   @Override
-  public int compareTo(DexMethodHandle other) {
-    int result = type.getValue() - other.type.getValue();
-    if (result == 0) {
-      if (isFieldHandle()) {
-        result = asField().compareTo(other.asField());
-      } else {
-        assert isMethodHandle();
-        result = asMethod().compareTo(other.asMethod());
-      }
-    }
-    return result;
+  public DexMethodHandle self() {
+    return this;
   }
 
   @Override
-  public int slowCompareTo(DexMethodHandle other, NamingLens namingLens) {
-    int result = type.getValue() - other.type.getValue();
-    if (result == 0) {
-      if (isFieldHandle()) {
-        result = asField().slowCompareTo(other.asField(), namingLens);
-      } else {
-        assert isMethodHandle();
-        result = asMethod().slowCompareTo(other.asMethod(), namingLens);
-      }
-    }
-    return result;
+  public StructuralAccept<DexMethodHandle> getStructuralAccept() {
+    return DexMethodHandle::specify;
+  }
+
+  private static void specify(StructuralSpecification<DexMethodHandle, ?> spec) {
+    spec.withInt(m -> m.type.getValue())
+        .withConditionalItem(DexMethodHandle::isFieldHandle, DexMethodHandle::asField)
+        .withConditionalItem(DexMethodHandle::isMethodHandle, DexMethodHandle::asMethod)
+        .withBool(m -> m.isInterface)
+        .withItem(m -> m.rewrittenTarget);
   }
 
   public Handle toAsmHandle(NamingLens lens) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexProto.java b/src/main/java/com/android/tools/r8/graph/DexProto.java
index 5ec1cfb..2caf3a7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProto.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProto.java
@@ -6,14 +6,12 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.structural.StructuralAccept;
-import com.android.tools.r8.utils.structural.StructuralItem;
 import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.collect.Iterables;
 import java.util.Collections;
 import java.util.function.Consumer;
 
-public class DexProto extends IndexedDexItem
-    implements PresortedComparable<DexProto>, StructuralItem<DexProto> {
+public class DexProto extends IndexedDexItem implements PresortedComparable<DexProto> {
 
   public static final DexProto SENTINEL = new DexProto(null, null, null);
 
@@ -28,8 +26,10 @@
   }
 
   private static void accept(StructuralSpecification<DexProto, ?> spec) {
-    // TODO(b/172206529): Consider removing shorty.
-    spec.withItem(p1 -> p1.shorty).withItem(DexProto::getReturnType).withItem(p -> p.parameters);
+    spec.withItem(DexProto::getReturnType)
+        .withItem(p -> p.parameters)
+        // TODO(b/172206529): Consider removing shorty.
+        .withItem(p1 -> p1.shorty);
   }
 
   @Override
@@ -106,15 +106,6 @@
   }
 
   @Override
-  public int slowCompareTo(DexProto other, NamingLens namingLens) {
-    int result = returnType.slowCompareTo(other.returnType, namingLens);
-    if (result == 0) {
-      result = parameters.slowCompareTo(other.parameters, namingLens);
-    }
-    return result;
-  }
-
-  @Override
   public String toSmaliString() {
     return toDescriptorString();
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index 595e8d3..9fde6f0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -6,7 +6,6 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.IdentifierUtils;
 import com.android.tools.r8.utils.StringUtils;
@@ -14,13 +13,11 @@
 import com.android.tools.r8.utils.structural.CompareToVisitor;
 import com.android.tools.r8.utils.structural.HashingVisitor;
 import com.android.tools.r8.utils.structural.StructuralAccept;
-import com.android.tools.r8.utils.structural.StructuralItem;
 import java.io.UTFDataFormatException;
 import java.util.Arrays;
 import java.util.NoSuchElementException;
 
-public class DexString extends IndexedDexItem
-    implements PresortedComparable<DexString>, StructuralItem<DexString> {
+public class DexString extends IndexedDexItem implements PresortedComparable<DexString> {
 
   public static final DexString[] EMPTY_ARRAY = {};
   private static final int ARRAY_CHARACTER = '[';
@@ -299,12 +296,6 @@
     }
   }
 
-  @Override
-  public int slowCompareTo(DexString other, NamingLens lens) {
-    // The naming lens cannot affect strings.
-    return compareTo(other);
-  }
-
   private static boolean isValidClassDescriptor(String string) {
     if (string.length() < 3
         || string.charAt(0) != 'L'
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 3dd7133..b757e40 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -20,7 +20,6 @@
 import com.android.tools.r8.ir.desugar.NestBasedAccessDesugaring;
 import com.android.tools.r8.ir.desugar.TwrCloseResourceRewriter;
 import com.android.tools.r8.ir.optimize.ServiceLoaderRewriter;
-import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.DescriptorUtils;
@@ -28,7 +27,6 @@
 import com.android.tools.r8.utils.structural.CompareToVisitor;
 import com.android.tools.r8.utils.structural.HashingVisitor;
 import com.android.tools.r8.utils.structural.StructuralAccept;
-import com.android.tools.r8.utils.structural.StructuralItem;
 import com.google.common.collect.ImmutableList;
 import java.util.Arrays;
 import java.util.List;
@@ -38,8 +36,7 @@
 import java.util.function.Function;
 import java.util.function.Predicate;
 
-public class DexType extends DexReference
-    implements PresortedComparable<DexType>, StructuralItem<DexType> {
+public class DexType extends DexReference implements PresortedComparable<DexType> {
   public static final DexType[] EMPTY_ARRAY = {};
 
   // Bundletool is merging classes that may originate from a build with an old version of R8.
@@ -229,13 +226,6 @@
     return this;
   }
 
-  @Override
-  public int slowCompareTo(DexType other, NamingLens namingLens) {
-    DexString thisDescriptor = namingLens.lookupDescriptor(this);
-    DexString otherDescriptor = namingLens.lookupDescriptor(other);
-    return thisDescriptor.slowCompareTo(otherDescriptor, namingLens);
-  }
-
   public boolean isPrimitiveType() {
     return DescriptorUtils.isPrimitiveType((char) descriptor.content[0]);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexTypeList.java b/src/main/java/com/android/tools/r8/graph/DexTypeList.java
index 28ff803..a641039 100644
--- a/src/main/java/com/android/tools/r8/graph/DexTypeList.java
+++ b/src/main/java/com/android/tools/r8/graph/DexTypeList.java
@@ -6,7 +6,6 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
 import com.android.tools.r8.utils.structural.HashingVisitor;
@@ -118,38 +117,6 @@
     return builder.toString();
   }
 
-  public int slowCompareTo(DexTypeList other) {
-    for (int i = 0; i <= Math.min(values.length, other.values.length); i++) {
-      if (i == values.length) {
-        return i == other.values.length ? 0 : -1;
-      } else if (i == other.values.length) {
-        return 1;
-      } else {
-        int result = values[i].compareTo(other.values[i]);
-        if (result != 0) {
-          return result;
-        }
-      }
-    }
-    throw new Unreachable();
-  }
-
-  public int slowCompareTo(DexTypeList other, NamingLens namingLens) {
-    for (int i = 0; i <= Math.min(values.length, other.values.length); i++) {
-      if (i == values.length) {
-        return i == other.values.length ? 0 : -1;
-      } else if (i == other.values.length) {
-        return 1;
-      } else {
-        int result = values[i].slowCompareTo(other.values[i], namingLens);
-        if (result != 0) {
-          return result;
-        }
-      }
-    }
-    throw new Unreachable();
-  }
-
   @Override
   public Iterator<DexType> iterator() {
     return Iterators.forArray(values);
diff --git a/src/main/java/com/android/tools/r8/graph/PresortedComparable.java b/src/main/java/com/android/tools/r8/graph/PresortedComparable.java
index 82e53c3..623e2f4 100644
--- a/src/main/java/com/android/tools/r8/graph/PresortedComparable.java
+++ b/src/main/java/com/android/tools/r8/graph/PresortedComparable.java
@@ -4,8 +4,12 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitorWithNamingLens;
+import com.android.tools.r8.utils.structural.StructuralItem;
 
-public interface PresortedComparable<T> extends Comparable<T> {
+public interface PresortedComparable<T extends PresortedComparable<T>> extends StructuralItem<T> {
 
-  int slowCompareTo(T other, NamingLens namingLens);
+  default int slowCompareTo(T other, NamingLens lens) {
+    return CompareToVisitorWithNamingLens.run(self(), other, lens, StructuralItem::acceptCompareTo);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitor.java
index 782324f..832d172 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitor.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitor.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils.structural;
 
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
@@ -22,6 +24,14 @@
 
   public abstract void visitDexTypeList(DexTypeList types1, DexTypeList types2);
 
+  public void visitDexField(DexField field1, DexField field2) {
+    visit(field1, field2, field1.getStructuralAccept());
+  }
+
+  public void visitDexMethod(DexMethod method1, DexMethod method2) {
+    visit(method1, method2, method1.getStructuralAccept());
+  }
+
   public abstract <S> void visit(S item1, S item2, StructuralAccept<S> accept);
 
   @Deprecated
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java
new file mode 100644
index 0000000..5ef5e72
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java
@@ -0,0 +1,172 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils.structural;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
+import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
+import java.util.Comparator;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.ToIntFunction;
+
+public class CompareToVisitorWithNamingLens extends CompareToVisitor {
+
+  public static <T> int run(T item1, T item2, NamingLens namingLens, StructuralAccept<T> visit) {
+    return run(item1, item2, namingLens, (i1, i2, visitor) -> visitor.visit(i1, i2, visit));
+  }
+
+  public static <T> int run(
+      T item1, T item2, NamingLens namingLens, CompareToAccept<T> compareToAccept) {
+    CompareToVisitorWithNamingLens state = new CompareToVisitorWithNamingLens(namingLens);
+    compareToAccept.accept(item1, item2, state);
+    return state.order;
+  }
+
+  private final NamingLens namingLens;
+  private int order = 0;
+
+  public CompareToVisitorWithNamingLens(NamingLens namingLens) {
+    this.namingLens = namingLens;
+  }
+
+  @Override
+  public void visitBool(boolean value1, boolean value2) {
+    if (order == 0) {
+      order = Boolean.compare(value1, value2);
+    }
+  }
+
+  @Override
+  public void visitInt(int value1, int value2) {
+    if (order == 0) {
+      order = Integer.compare(value1, value2);
+    }
+  }
+
+  @Override
+  public <S> void visit(S item1, S item2, Comparator<S> comparator) {
+    if (order == 0) {
+      order = comparator.compare(item1, item2);
+    }
+  }
+
+  @Override
+  public <S> void visit(S item1, S item2, StructuralAccept<S> accept) {
+    if (order == 0) {
+      accept.accept(new ItemSpecification<>(item1, item2, this));
+    }
+  }
+
+  @Override
+  public void visitDexString(
+      DexString string1, DexString string2, Comparator<DexString> comparator) {
+    if (order == 0) {
+      order = comparator.compare(string1, string2);
+    }
+  }
+
+  @Override
+  public void visitDexType(DexType type1, DexType type2) {
+    if (order == 0) {
+      namingLens.lookupDescriptor(type1).acceptCompareTo(namingLens.lookupDescriptor(type2), this);
+    }
+  }
+
+  @Override
+  public void visitDexField(DexField field1, DexField field2) {
+    if (order == 0) {
+      field1.holder.acceptCompareTo(field2.holder, this);
+      if (order == 0) {
+        namingLens.lookupName(field1).acceptCompareTo(namingLens.lookupName(field2), this);
+        if (order == 0) {
+          field1.type.acceptCompareTo(field2.type, this);
+        }
+      }
+    }
+  }
+
+  @Override
+  public void visitDexMethod(DexMethod method1, DexMethod method2) {
+    if (order == 0) {
+      method1.holder.acceptCompareTo(method2.holder, this);
+      if (order == 0) {
+        namingLens.lookupName(method1).acceptCompareTo(namingLens.lookupName(method2), this);
+        if (order == 0) {
+          method1.proto.acceptCompareTo(method2.proto, this);
+        }
+      }
+    }
+  }
+
+  @Override
+  public void visitDexTypeList(DexTypeList types1, DexTypeList types2) {
+    // Comparison is lexicographic with comparisons between items prior to the length of the lists.
+    if (order == 0) {
+      int length = Math.min(types1.size(), types2.size());
+      for (int i = 0; i < length && order == 0; i++) {
+        visitDexType(types1.values[i], types2.values[i]);
+      }
+      if (order == 0) {
+        visitInt(types1.size(), types2.size());
+      }
+    }
+  }
+
+  private static class ItemSpecification<T>
+      extends StructuralSpecification<T, ItemSpecification<T>> {
+
+    private final CompareToVisitorWithNamingLens parent;
+    private final T item1;
+    private final T item2;
+
+    private ItemSpecification(T item1, T item2, CompareToVisitorWithNamingLens parent) {
+      this.item1 = item1;
+      this.item2 = item2;
+      this.parent = parent;
+    }
+
+    @Override
+    public ItemSpecification<T> withAssert(Predicate<T> predicate) {
+      assert predicate.test(item1);
+      assert predicate.test(item2);
+      return this;
+    }
+
+    @Override
+    public ItemSpecification<T> withBool(Predicate<T> getter) {
+      parent.visitBool(getter.test(item1), getter.test(item2));
+      return this;
+    }
+
+    @Override
+    public ItemSpecification<T> withInt(ToIntFunction<T> getter) {
+      parent.visitInt(getter.applyAsInt(item1), getter.applyAsInt(item2));
+      return this;
+    }
+
+    @Override
+    public <S> ItemSpecification<T> withConditionalCustomItem(
+        Predicate<T> predicate,
+        Function<T, S> getter,
+        CompareToAccept<S> compare,
+        HashingAccept<S> hasher) {
+      if (parent.order == 0) {
+        boolean test1 = predicate.test(item1);
+        boolean test2 = predicate.test(item2);
+        if (test1 && test2) {
+          compare.accept(getter.apply(item1), getter.apply(item2), parent);
+        } else {
+          parent.visitBool(test1, test2);
+        }
+      }
+      return this;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java
index 7baa091..39d6c7e 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java
@@ -125,10 +125,19 @@
     }
 
     @Override
-    public <S> ItemSpecification<T> withCustomItem(
-        Function<T, S> getter, CompareToAccept<S> compare, HashingAccept<S> hasher) {
+    protected <S> ItemSpecification<T> withConditionalCustomItem(
+        Predicate<T> predicate,
+        Function<T, S> getter,
+        CompareToAccept<S> compare,
+        HashingAccept<S> hasher) {
       if (parent.order == 0) {
-        compare.accept(getter.apply(item1), getter.apply(item2), parent);
+        boolean test1 = predicate.test(item1);
+        boolean test2 = predicate.test(item2);
+        if (test1 && test2) {
+          compare.accept(getter.apply(item1), getter.apply(item2), parent);
+        } else {
+          parent.visitBool(test1, test2);
+        }
       }
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
index af354d0..ab75fb4 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
@@ -56,11 +56,17 @@
   }
 
   @Override
-  public <S> HashCodeVisitor<T> withCustomItem(
-      Function<T, S> getter, CompareToAccept<S> compare, HashingAccept<S> hasher) {
-    S member = getter.apply(item);
-    // Use the value 1 for the null-member case such that a different hash is obtained for
-    // {null, null} and {null}.
-    return amend(member == null ? 1 : member.hashCode());
+  protected <S> HashCodeVisitor<T> withConditionalCustomItem(
+      Predicate<T> predicate,
+      Function<T, S> getter,
+      CompareToAccept<S> compare,
+      HashingAccept<S> hasher) {
+    if (predicate.test(item)) {
+      return amend(getter.apply(item).hashCode());
+    } else {
+      // Use the value 1 for the failing-predicate case such that a different hash is obtained for
+      // eg, {null, null} and {null}.
+      return amend(1);
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
index b64fa45..2036b1b 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
@@ -100,9 +100,17 @@
     }
 
     @Override
-    public <S> ItemSpecification<T> withCustomItem(
-        Function<T, S> getter, CompareToAccept<S> compare, HashingAccept<S> hasher) {
-      hasher.accept(getter.apply(item), parent);
+    protected <S> ItemSpecification<T> withConditionalCustomItem(
+        Predicate<T> predicate,
+        Function<T, S> getter,
+        CompareToAccept<S> compare,
+        HashingAccept<S> hasher) {
+      boolean test = predicate.test(item);
+      // Always hash the predicate result to distinguish, eg, {null, null} and {null}.
+      parent.visitBool(test);
+      if (test) {
+        hasher.accept(getter.apply(item), parent);
+      }
       return this;
     }
   }
diff --git a/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java b/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
index 0c2eea4..bb8f7c7 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
@@ -20,8 +20,16 @@
    * <p>It is preferable to use withStructuralItem.
    */
   @Deprecated
-  public abstract <S> V withCustomItem(
-      Function<T, S> getter, CompareToAccept<S> compare, HashingAccept<S> hasher);
+  public final <S> V withCustomItem(
+      Function<T, S> getter, CompareToAccept<S> compare, HashingAccept<S> hasher) {
+    return withConditionalCustomItem(t -> true, getter, compare, hasher);
+  }
+
+  protected abstract <S> V withConditionalCustomItem(
+      Predicate<T> predicate,
+      Function<T, S> getter,
+      CompareToAccept<S> compare,
+      HashingAccept<S> hasher);
 
   /**
    * Specification for a "specified" item.
@@ -29,8 +37,17 @@
    * <p>Using this the visiting methods are could based on the implementation of the Specified
    * interface.
    */
-  public <S extends StructuralItem<S>> V withItem(Function<T, S> getter) {
-    return withCustomItem(getter, S::acceptCompareTo, S::acceptHashing);
+  public final <S extends StructuralItem<S>> V withItem(Function<T, S> getter) {
+    return withConditionalItem(t -> true, getter);
+  }
+
+  final <S extends StructuralItem<S>> V withNullableItem(Function<T, S> getter) {
+    return withConditionalItem(s -> getter.apply(s) != null, getter);
+  }
+
+  public final <S extends StructuralItem<S>> V withConditionalItem(
+      Predicate<T> predicate, Function<T, S> getter) {
+    return withConditionalCustomItem(predicate, getter, S::acceptCompareTo, S::acceptHashing);
   }
 
   /**