Ignore -assumenosideeffects rules for Object members

This CL additionally refines the side effect models for Object.equals(), Object.hashCode(), and Object.toString(), since it is no longer possible to specify using -assumenosideeffects that these methods do not have side effects.

Fixes: 171380978
Change-Id: Ieebd750800e50449f0508bc617e1d937da92ff7d
diff --git a/src/main/java/com/android/tools/r8/errors/AssumeNoSideEffectsRuleForObjectMembersDiagnostic.java b/src/main/java/com/android/tools/r8/errors/AssumeNoSideEffectsRuleForObjectMembersDiagnostic.java
new file mode 100644
index 0000000..8910cda
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/AssumeNoSideEffectsRuleForObjectMembersDiagnostic.java
@@ -0,0 +1,92 @@
+// 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.errors;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.errors.CheckDiscardDiagnostic.Builder;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+@Keep
+public class AssumeNoSideEffectsRuleForObjectMembersDiagnostic implements Diagnostic {
+
+  private final List<MethodReference> methods;
+  private final Origin origin;
+  private final Position position;
+
+  private AssumeNoSideEffectsRuleForObjectMembersDiagnostic(
+      List<MethodReference> methods, Origin origin, Position position) {
+    this.methods = methods;
+    this.origin = origin;
+    this.position = position;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public Position getPosition() {
+    return position;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    Iterator<MethodReference> iterator = methods.iterator();
+    StringBuilder message =
+        new StringBuilder("The -assumenosideeffects rule matches the following method(s) ")
+            .append("on java.lang.Object: ")
+            .append(MethodReferenceUtils.toSourceStringWithoutHolderAndReturnType(iterator.next()));
+    while (iterator.hasNext()) {
+      MethodReference method = iterator.next();
+      message
+          .append(iterator.hasNext() ? ", " : " and ")
+          .append(MethodReferenceUtils.toSourceStringWithoutHolderAndReturnType(method));
+    }
+    return message
+        .append(". ")
+        .append("This is most likely not intended. ")
+        .append("Consider specifying the methods more precisely.")
+        .toString();
+  }
+
+  public static class Builder {
+
+    private final List<MethodReference> methods = new ArrayList<>();
+    private Origin origin;
+    private Position position;
+
+    public Builder() {}
+
+    public Builder addMatchedMethods(Set<DexMethod> methods) {
+      for (DexMethod method : methods) {
+        this.methods.add(method.asMethodReference());
+      }
+      return this;
+    }
+
+    public Builder setOrigin(Origin origin) {
+      this.origin = origin;
+      return this;
+    }
+
+    public Builder setPosition(Position position) {
+      this.position = position;
+      return this;
+    }
+
+    public AssumeNoSideEffectsRuleForObjectMembersDiagnostic build() {
+      return new AssumeNoSideEffectsRuleForObjectMembersDiagnostic(methods, origin, position);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 877f2ba..37f45c6 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.ir.optimize.CallSiteOptimizationInfoPropagator;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoFactory;
 import com.android.tools.r8.ir.optimize.library.LibraryMemberOptimizer;
+import com.android.tools.r8.ir.optimize.library.LibraryMethodSideEffectModelCollection;
 import com.android.tools.r8.optimize.MemberRebindingLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepInfoCollection;
@@ -69,6 +70,9 @@
   // Desugared library prefix rewriter.
   public final PrefixRewritingMapper rewritePrefix;
 
+  // Modeling.
+  private final LibraryMethodSideEffectModelCollection libraryMethodSideEffectModelCollection;
+
   // Optimizations.
   private final CallSiteOptimizationInfoPropagator callSiteOptimizationInfoPropagator;
   private final LibraryMemberOptimizer libraryMemberOptimizer;
@@ -108,6 +112,8 @@
     }
 
     this.libraryMemberOptimizer = new LibraryMemberOptimizer(this);
+    this.libraryMethodSideEffectModelCollection =
+        new LibraryMethodSideEffectModelCollection(dexItemFactory());
 
     if (enableWholeProgramOptimizations() && options().protoShrinking().isProtoShrinkingEnabled()) {
       this.protoShrinker = new ProtoShrinker(withLiveness());
@@ -287,6 +293,10 @@
     return libraryMemberOptimizer;
   }
 
+  public LibraryMethodSideEffectModelCollection getLibraryMethodSideEffectModelCollection() {
+    return libraryMethodSideEffectModelCollection;
+  }
+
   public ProtoShrinker protoShrinker() {
     return protoShrinker;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java b/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java
index ecb1b4f..0a17134 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java
@@ -20,7 +20,7 @@
     if (holder.isProgramClass()) {
       return new ProgramMethod(holder.asProgramClass(), method);
     } else if (holder.isLibraryClass()) {
-      return new DexClassAndMethod(holder, method);
+      return new LibraryMethod(holder.asLibraryClass(), method);
     } else {
       assert holder.isClasspathClass();
       return new ClasspathMethod(holder.asClasspathClass(), method);
@@ -50,6 +50,14 @@
     return null;
   }
 
+  public boolean isLibraryMethod() {
+    return false;
+  }
+
+  public LibraryMethod asLibraryMethod() {
+    return null;
+  }
+
   public boolean isProgramMethod() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index cf30eed..b192017 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -5,7 +5,6 @@
 
 import static com.android.tools.r8.ir.analysis.type.ClassTypeElement.computeLeastUpperBoundOfInterfaces;
 import static com.android.tools.r8.ir.optimize.ServiceLoaderRewriter.SERVICE_LOADER_CLASS_NAME;
-import static com.google.common.base.Predicates.alwaysTrue;
 
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.Marker;
@@ -31,7 +30,6 @@
 import com.android.tools.r8.kotlin.Kotlin;
 import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.LRUCacheTable;
-import com.android.tools.r8.utils.Pair;
 import com.google.common.base.Strings;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
@@ -39,7 +37,6 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
-import com.google.common.collect.Streams;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
@@ -57,7 +54,6 @@
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 public class DexItemFactory {
 
@@ -109,8 +105,6 @@
 
   public DexItemFactory() {
     this.kotlin = new Kotlin(this);
-    assert libraryMethodsWithReturnValueDependingOnlyOnArguments.stream()
-        .allMatch(libraryMethodsWithoutSideEffects::containsKey);
   }
 
   public static boolean isInternalSentinel(DexItem item) {
@@ -604,7 +598,7 @@
   }
 
   // Boxed Boxed#valueOf(Primitive), e.g., Boolean Boolean#valueOf(B)
-  private Set<DexMethod> boxedValueOfMethods() {
+  public Set<DexMethod> boxedValueOfMethods() {
     return primitiveToBoxed.entrySet().stream()
         .map(
             entry -> {
@@ -686,31 +680,6 @@
           objectsMethods.requireNonNullWithMessageSupplier,
           stringMembers.valueOf);
 
-  // We assume library methods listed here are `public`, i.e., free from visibility side effects.
-  // If not, that library method should not be added here because it literally has side effects.
-  public Map<DexMethod, Predicate<InvokeMethod>> libraryMethodsWithoutSideEffects =
-      Streams.<Pair<DexMethod, Predicate<InvokeMethod>>>concat(
-              Stream.of(new Pair<>(enumMembers.constructor, alwaysTrue())),
-              Stream.of(new Pair<>(npeMethods.init, alwaysTrue())),
-              Stream.of(new Pair<>(npeMethods.initWithMessage, alwaysTrue())),
-              Stream.of(new Pair<>(objectMembers.constructor, alwaysTrue())),
-              Stream.of(new Pair<>(objectMembers.getClass, alwaysTrue())),
-              Stream.of(new Pair<>(stringMembers.hashCode, alwaysTrue())),
-              mapToPredicate(classMethods.getNames, alwaysTrue()),
-              mapToPredicate(
-                  stringBufferMethods.constructorMethods,
-                  stringBufferMethods::constructorInvokeIsSideEffectFree),
-              mapToPredicate(
-                  stringBuilderMethods.constructorMethods,
-                  stringBuilderMethods::constructorInvokeIsSideEffectFree),
-              mapToPredicate(boxedValueOfMethods(), alwaysTrue()))
-          .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
-
-  private static Stream<Pair<DexMethod, Predicate<InvokeMethod>>> mapToPredicate(
-      Set<DexMethod> methods, Predicate<InvokeMethod> predicate) {
-    return methods.stream().map(method -> new Pair<>(method, predicate));
-  }
-
   // TODO(b/119596718): More idempotent methods? Any singleton accessors? E.g.,
   // java.util.Calendar#getInstance(...) // 4 variants
   // java.util.Locale#getDefault() // returns JVM default locale.
@@ -1130,7 +1099,10 @@
     public final DexField clinitField = createField(objectType, intType, "$r8$clinit");
 
     public final DexMethod clone;
+    public final DexMethod equals =
+        createMethod(objectType, createProto(booleanType, objectType), "equals");
     public final DexMethod getClass;
+    public final DexMethod hashCode = createMethod(objectType, createProto(intType), "hashCode");
     public final DexMethod constructor;
     public final DexMethod finalize;
     public final DexMethod toString;
@@ -1199,7 +1171,7 @@
     public final DexMethod getDeclaredMethod;
     public final DexMethod newInstance;
     private final Set<DexMethod> getMembers;
-    private final Set<DexMethod> getNames;
+    public final Set<DexMethod> getNames;
 
     private ClassMethods() {
       desiredAssertionStatus = createMethod(classDescriptor,
@@ -1561,8 +1533,6 @@
     public final DexMethod toString;
 
     private final Set<DexMethod> appendMethods;
-    private final Set<DexMethod> constructorMethods;
-
     private StringBuildingMethods(DexType receiver) {
       DexString append = createString("append");
 
@@ -1611,6 +1581,8 @@
               charSequenceConstructor, defaultConstructor, intConstructor, stringConstructor);
     }
 
+    public final Set<DexMethod> constructorMethods;
+
     public boolean isAppendMethod(DexMethod method) {
       return appendMethods.contains(method);
     }
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 6f77653..74b0fff 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -223,15 +223,22 @@
 
   @Override
   public String toSourceString() {
-    return toSourceString(true);
+    return toSourceString(true, true);
   }
 
   public String toSourceStringWithoutHolder() {
-    return toSourceString(false);
+    return toSourceString(false, true);
   }
 
-  private String toSourceString(boolean includeHolder) {
-    StringBuilder builder = new StringBuilder(proto.returnType.toSourceString()).append(" ");
+  public String toSourceStringWithoutHolderAndReturnType() {
+    return toSourceString(false, false);
+  }
+
+  private String toSourceString(boolean includeHolder, boolean includeReturnType) {
+    StringBuilder builder = new StringBuilder();
+    if (includeReturnType) {
+      builder.append(getReturnType().toSourceString()).append(" ");
+    }
     if (includeHolder) {
       builder.append(holder.toSourceString()).append(".");
     }
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLens.java b/src/main/java/com/android/tools/r8/graph/GraphLens.java
index d00b9d9..a69be5e 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -502,15 +502,15 @@
     return result;
   }
 
-  public DexReference rewriteReference(DexReference reference) {
+  public <T extends DexReference> T rewriteReference(T reference) {
     if (reference.isDexField()) {
-      return getRenamedFieldSignature(reference.asDexField());
+      return (T) getRenamedFieldSignature(reference.asDexField());
     }
     if (reference.isDexMethod()) {
-      return getRenamedMethodSignature(reference.asDexMethod());
+      return (T) getRenamedMethodSignature(reference.asDexMethod());
     }
     assert reference.isDexType();
-    return lookupType(reference.asDexType());
+    return (T) lookupType(reference.asDexType());
   }
 
   public Set<DexReference> rewriteReferences(Set<DexReference> references) {
@@ -521,8 +521,8 @@
     return result;
   }
 
-  public <T> ImmutableMap<DexReference, T> rewriteReferenceKeys(Map<DexReference, T> map) {
-    ImmutableMap.Builder<DexReference, T> builder = ImmutableMap.builder();
+  public <R extends DexReference, T> ImmutableMap<R, T> rewriteReferenceKeys(Map<R, T> map) {
+    ImmutableMap.Builder<R, T> builder = ImmutableMap.builder();
     map.forEach((reference, value) -> builder.put(rewriteReference(reference), value));
     return builder.build();
   }
diff --git a/src/main/java/com/android/tools/r8/graph/LibraryMethod.java b/src/main/java/com/android/tools/r8/graph/LibraryMethod.java
new file mode 100644
index 0000000..c06ebab
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/LibraryMethod.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2019, 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.graph;
+
+/** Type representing a method definition from the library and its holder. */
+public final class LibraryMethod extends DexClassAndMethod {
+
+  public LibraryMethod(DexLibraryClass holder, DexEncodedMethod method) {
+    super(holder, method);
+  }
+
+  @Override
+  public boolean isLibraryMethod() {
+    return true;
+  }
+
+  @Override
+  public LibraryMethod asLibraryMethod() {
+    return this;
+  }
+
+  @Override
+  public DexLibraryClass getHolder() {
+    DexClass holder = super.getHolder();
+    assert holder.isLibraryClass();
+    return holder.asLibraryClass();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/modeling/LibraryMethodReadSetModeling.java b/src/main/java/com/android/tools/r8/ir/analysis/modeling/LibraryMethodReadSetModeling.java
index 34ab168..f9a2e25 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/modeling/LibraryMethodReadSetModeling.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/modeling/LibraryMethodReadSetModeling.java
@@ -4,41 +4,39 @@
 
 package com.android.tools.r8.ir.analysis.modeling;
 
-import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.AbstractFieldSet;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.EmptyFieldSet;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.UnknownFieldSet;
 import com.android.tools.r8.ir.code.InvokeMethod;
-import java.util.function.Predicate;
 
 /** Models if a given library method may cause a program field to be read. */
 public class LibraryMethodReadSetModeling {
 
   public static AbstractFieldSet getModeledReadSetOrUnknown(
-      InvokeMethod invoke, DexItemFactory dexItemFactory) {
+      AppView<?> appView, InvokeMethod invoke) {
     DexMethod invokedMethod = invoke.getInvokedMethod();
 
     // Check if it is a library method that does not have side effects. In that case it is safe to
     // assume that the method does not read any fields, since even if it did, it would not be able
     // to do anything with the values it read (since we will remove such invocations without side
     // effects).
-    Predicate<InvokeMethod> noSideEffectsPredicate =
-        dexItemFactory.libraryMethodsWithoutSideEffects.get(invokedMethod);
-    if (noSideEffectsPredicate != null && noSideEffectsPredicate.test(invoke)) {
+    if (appView
+        .getLibraryMethodSideEffectModelCollection()
+        .isCallToSideEffectFreeFinalMethod(invoke)) {
       return EmptyFieldSet.getInstance();
     }
 
     // Already handled above.
-    assert !dexItemFactory.classMethods.isReflectiveNameLookup(invokedMethod);
+    assert !appView.dexItemFactory().classMethods.isReflectiveNameLookup(invokedMethod);
 
     // Modeling of other library methods.
     DexType holder = invokedMethod.holder;
-    if (holder == dexItemFactory.objectType) {
-      if (invokedMethod == dexItemFactory.objectMembers.constructor) {
-        return EmptyFieldSet.getInstance();
-      }
+    if (holder == appView.dexItemFactory().objectType
+        && invokedMethod == appView.dexItemFactory().objectMembers.constructor) {
+      return EmptyFieldSet.getInstance();
     }
     return UnknownFieldSet.getInstance();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
index a6d77f7..c61ca0a 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
@@ -207,6 +207,6 @@
       }
     }
 
-    return LibraryMethodReadSetModeling.getModeledReadSetOrUnknown(this, appView.dexItemFactory());
+    return LibraryMethodReadSetModeling.getModeledReadSetOrUnknown(appView, this);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
index a78a16b..6daa9a4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
@@ -202,7 +202,7 @@
 
   @Override
   public AbstractFieldSet readSet(AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
-    return LibraryMethodReadSetModeling.getModeledReadSetOrUnknown(this, appView.dexItemFactory());
+    return LibraryMethodReadSetModeling.getModeledReadSetOrUnknown(appView, this);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
index 09db288..2aeb9b5 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
@@ -25,7 +25,6 @@
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.List;
-import java.util.function.Predicate;
 
 public abstract class InvokeMethodWithReceiver extends InvokeMethod {
 
@@ -195,9 +194,9 @@
     }
 
     // Check if it is a call to one of library methods that are known to be side-effect free.
-    Predicate<InvokeMethod> noSideEffectsPredicate =
-        appView.dexItemFactory().libraryMethodsWithoutSideEffects.get(getInvokedMethod());
-    if (noSideEffectsPredicate != null && noSideEffectsPredicate.test(this)) {
+    if (appView
+        .getLibraryMethodSideEffectModelCollection()
+        .isCallToSideEffectFreeFinalMethod(this)) {
       return false;
     }
 
@@ -240,6 +239,13 @@
       return true;
     }
 
+    if (singleTarget.isLibraryMethod()
+        && appView
+            .getLibraryMethodSideEffectModelCollection()
+            .isSideEffectFree(this, singleTarget.asLibraryMethod())) {
+      return false;
+    }
+
     // Verify that the target method does not have side-effects.
     if (appViewWithLiveness.appInfo().noSideEffects.containsKey(singleTarget.getReference())) {
       return false;
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
index 4e8746d..bf3a599 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
@@ -29,7 +29,6 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.Sets;
 import java.util.List;
-import java.util.function.Predicate;
 
 public class InvokeStatic extends InvokeMethod {
 
@@ -179,9 +178,9 @@
     }
 
     // Check if it is a call to one of library methods that are known to be side-effect free.
-    Predicate<InvokeMethod> noSideEffectsPredicate =
-        appView.dexItemFactory().libraryMethodsWithoutSideEffects.get(getInvokedMethod());
-    if (noSideEffectsPredicate != null && noSideEffectsPredicate.test(this)) {
+    if (appView
+        .getLibraryMethodSideEffectModelCollection()
+        .isCallToSideEffectFreeFinalMethod(this)) {
       return false;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizer.java
index 72abb27..50443c6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizer.java
@@ -30,7 +30,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Predicate;
 
 /**
  * Canonicalize idempotent function calls.
@@ -277,12 +276,10 @@
 
   private boolean isIdempotentLibraryMethodInvoke(InvokeMethod invoke) {
     DexMethod invokedMethod = invoke.getInvokedMethod();
-    Predicate<InvokeMethod> noSideEffectPredicate =
-        factory.libraryMethodsWithoutSideEffects.get(invokedMethod);
-    if (noSideEffectPredicate == null || !noSideEffectPredicate.test(invoke)) {
-      return false;
-    }
-    return factory.libraryMethodsWithReturnValueDependingOnlyOnArguments.contains(invokedMethod);
+    return appView
+            .getLibraryMethodSideEffectModelCollection()
+            .isCallToSideEffectFreeFinalMethod(invoke)
+        && factory.libraryMethodsWithReturnValueDependingOnlyOnArguments.contains(invokedMethod);
   }
 
   private static void insertCanonicalizedInvokeWithInValues(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index d0fc9c5..6b0b61d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -6,7 +6,6 @@
 
 import static com.android.tools.r8.graph.DexEncodedMethod.asProgramMethodOrNull;
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
-import static com.google.common.base.Predicates.alwaysFalse;
 
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.errors.Unreachable;
@@ -19,6 +18,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.LibraryMethod;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.ResolutionResult;
 import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
@@ -302,7 +302,8 @@
             return user; // Not eligible.
           }
 
-          if (isEligibleLibraryMethodCall(invokeMethod, singleTarget)) {
+          if (singleTarget.isLibraryMethod()
+              && isEligibleLibraryMethodCall(invokeMethod, singleTarget.asLibraryMethod())) {
             continue;
           }
 
@@ -648,11 +649,12 @@
         }
 
         DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, method);
-        if (singleTarget != null) {
-          Predicate<InvokeMethod> noSideEffectsPredicate =
-              dexItemFactory.libraryMethodsWithoutSideEffects.getOrDefault(
-                  singleTarget.getReference(), alwaysFalse());
-          if (noSideEffectsPredicate.test(invoke)) {
+        if (singleTarget != null && singleTarget.isLibraryMethod()) {
+          boolean isSideEffectFree =
+              appView
+                  .getLibraryMethodSideEffectModelCollection()
+                  .isSideEffectFree(invoke, singleTarget.asLibraryMethod());
+          if (isSideEffectFree) {
             if (!invoke.hasOutValue() || !invoke.outValue().hasAnyUsers()) {
               removeInstruction(invoke);
               continue;
@@ -1170,10 +1172,10 @@
     return true;
   }
 
-  private boolean isEligibleLibraryMethodCall(InvokeMethod invoke, DexClassAndMethod singleTarget) {
-    Predicate<InvokeMethod> noSideEffectsPredicate =
-        dexItemFactory.libraryMethodsWithoutSideEffects.get(singleTarget.getReference());
-    if (noSideEffectsPredicate != null && noSideEffectsPredicate.test(invoke)) {
+  private boolean isEligibleLibraryMethodCall(InvokeMethod invoke, LibraryMethod singleTarget) {
+    boolean isSideEffectFree =
+        appView.getLibraryMethodSideEffectModelCollection().isSideEffectFree(invoke, singleTarget);
+    if (isSideEffectFree) {
       return !invoke.hasOutValue() || !invoke.outValue().hasAnyUsers();
     }
     if (singleTarget.getReference() == dexItemFactory.objectsMethods.requireNonNull) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
new file mode 100644
index 0000000..55d1dc3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
@@ -0,0 +1,82 @@
+// 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.ir.optimize.library;
+
+import static com.google.common.base.Predicates.alwaysFalse;
+import static com.google.common.base.Predicates.alwaysTrue;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.LibraryMethod;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+
+public class LibraryMethodSideEffectModelCollection {
+
+  private final Map<DexMethod, Predicate<InvokeMethod>> finalMethodsWithoutSideEffects;
+  private final Set<DexMethod> nonFinalMethodsWithoutSideEffects;
+
+  public LibraryMethodSideEffectModelCollection(DexItemFactory dexItemFactory) {
+    finalMethodsWithoutSideEffects = buildFinalMethodsWithoutSideEffects(dexItemFactory);
+    nonFinalMethodsWithoutSideEffects = buildNonFinalMethodsWithoutSideEffects(dexItemFactory);
+  }
+
+  private static Map<DexMethod, Predicate<InvokeMethod>> buildFinalMethodsWithoutSideEffects(
+      DexItemFactory dexItemFactory) {
+    ImmutableMap.Builder<DexMethod, Predicate<InvokeMethod>> builder =
+        ImmutableMap.<DexMethod, Predicate<InvokeMethod>>builder()
+            .put(dexItemFactory.enumMembers.constructor, alwaysTrue())
+            .put(dexItemFactory.npeMethods.init, alwaysTrue())
+            .put(dexItemFactory.npeMethods.initWithMessage, alwaysTrue())
+            .put(dexItemFactory.objectMembers.constructor, alwaysTrue())
+            .put(dexItemFactory.objectMembers.getClass, alwaysTrue())
+            .put(dexItemFactory.stringMembers.hashCode, alwaysTrue());
+    putAll(builder, dexItemFactory.classMethods.getNames, alwaysTrue());
+    putAll(
+        builder,
+        dexItemFactory.stringBufferMethods.constructorMethods,
+        dexItemFactory.stringBufferMethods::constructorInvokeIsSideEffectFree);
+    putAll(
+        builder,
+        dexItemFactory.stringBuilderMethods.constructorMethods,
+        dexItemFactory.stringBuilderMethods::constructorInvokeIsSideEffectFree);
+    putAll(builder, dexItemFactory.boxedValueOfMethods(), alwaysTrue());
+    return builder.build();
+  }
+
+  private static Set<DexMethod> buildNonFinalMethodsWithoutSideEffects(
+      DexItemFactory dexItemFactory) {
+    return ImmutableSet.of(
+        dexItemFactory.objectMembers.equals,
+        dexItemFactory.objectMembers.hashCode,
+        dexItemFactory.objectMembers.toString);
+  }
+
+  private static void putAll(
+      ImmutableMap.Builder<DexMethod, Predicate<InvokeMethod>> builder,
+      Iterable<DexMethod> methods,
+      Predicate<InvokeMethod> predicate) {
+    for (DexMethod method : methods) {
+      builder.put(method, predicate);
+    }
+  }
+
+  public boolean isCallToSideEffectFreeFinalMethod(InvokeMethod invoke) {
+    return finalMethodsWithoutSideEffects
+        .getOrDefault(invoke.getInvokedMethod(), alwaysFalse())
+        .test(invoke);
+  }
+
+  // This intentionally takes the invoke instruction since the determination of whether a library
+  // method has side effects may depend on the arguments.
+  public boolean isSideEffectFree(InvokeMethod invoke, LibraryMethod singleTarget) {
+    return isCallToSideEffectFreeFinalMethod(invoke)
+        || nonFinalMethodsWithoutSideEffects.contains(singleTarget.getReference());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index 7c6d911..b54a0db 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMember;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexReference;
@@ -136,9 +137,9 @@
   /** All items with assumemayhavesideeffects rule. */
   public final Map<DexReference, ProguardMemberRule> mayHaveSideEffects;
   /** All items with assumenosideeffects rule. */
-  public final Map<DexReference, ProguardMemberRule> noSideEffects;
+  public final Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects;
   /** All items with assumevalues rule. */
-  public final Map<DexReference, ProguardMemberRule> assumedValues;
+  public final Map<DexMember<?, ?>, ProguardMemberRule> assumedValues;
   /** All methods that should be inlined if possible due to a configuration directive. */
   public final Set<DexMethod> alwaysInline;
   /** All methods that *must* be inlined due to a configuration directive (testing only). */
@@ -213,8 +214,8 @@
       Map<DexCallSite, ProgramMethodSet> callSites,
       KeepInfoCollection keepInfo,
       Map<DexReference, ProguardMemberRule> mayHaveSideEffects,
-      Map<DexReference, ProguardMemberRule> noSideEffects,
-      Map<DexReference, ProguardMemberRule> assumedValues,
+      Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects,
+      Map<DexMember<?, ?>, ProguardMemberRule> assumedValues,
       Set<DexMethod> alwaysInline,
       Set<DexMethod> forceInline,
       Set<DexMethod> neverInline,
@@ -294,8 +295,8 @@
       Map<DexCallSite, ProgramMethodSet> callSites,
       KeepInfoCollection keepInfo,
       Map<DexReference, ProguardMemberRule> mayHaveSideEffects,
-      Map<DexReference, ProguardMemberRule> noSideEffects,
-      Map<DexReference, ProguardMemberRule> assumedValues,
+      Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects,
+      Map<DexMember<?, ?>, ProguardMemberRule> assumedValues,
       Set<DexMethod> alwaysInline,
       Set<DexMethod> forceInline,
       Set<DexMethod> neverInline,
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index 5b3038f..a5ee8b7 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -5,8 +5,8 @@
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 
-import com.android.tools.r8.Diagnostic;
 import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.errors.AssumeNoSideEffectsRuleForObjectMembersDiagnostic;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
@@ -67,6 +67,7 @@
 import java.util.Queue;
 import java.util.Set;
 import java.util.Stack;
+import java.util.TreeSet;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -109,8 +110,8 @@
   private final Map<DexType, Set<ProguardKeepRuleBase>> dependentKeepClassCompatRule =
       new IdentityHashMap<>();
   private final Map<DexReference, ProguardMemberRule> mayHaveSideEffects = new IdentityHashMap<>();
-  private final Map<DexReference, ProguardMemberRule> noSideEffects = new IdentityHashMap<>();
-  private final Map<DexReference, ProguardMemberRule> assumedValues = new IdentityHashMap<>();
+  private final Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects = new IdentityHashMap<>();
+  private final Map<DexMember<?, ?>, ProguardMemberRule> assumedValues = new IdentityHashMap<>();
   private final Set<DexReference> identifierNameStrings = Sets.newIdentityHashSet();
   private final Queue<DelayedRootSetActionItem> delayedRootSetActionItems =
       new ConcurrentLinkedQueue<>();
@@ -119,8 +120,8 @@
   private final DexStringCache dexStringCache = new DexStringCache();
   private final Set<ProguardIfRule> ifRules = Sets.newIdentityHashSet();
 
-  private final Map<OriginWithPosition, List<DexMethod>> assumeNoSideEffectsWarnings =
-      new HashMap<>();
+  private final Map<OriginWithPosition, Set<DexMethod>> assumeNoSideEffectsWarnings =
+      new LinkedHashMap<>();
 
   public RootSetBuilder(
       AppView<? extends AppInfoWithClassHierarchy> appView,
@@ -387,7 +388,7 @@
       DexType type,
       DexMethod reference,
       Set<DexType> subTypes,
-      Map<DexReference, ProguardMemberRule> assumeRulePool) {
+      Map<DexMember<?, ?>, ProguardMemberRule> assumeRulePool) {
     ProguardMemberRule ruleToBePropagated = null;
     for (DexType subType : subTypes) {
       DexMethod referenceInSubType =
@@ -1170,15 +1171,25 @@
       mayHaveSideEffects.put(item.toReference(), rule);
       context.markAsUsed();
     } else if (context instanceof ProguardAssumeNoSideEffectRule) {
-      checkAssumeNoSideEffectsWarnings(item, (ProguardAssumeNoSideEffectRule) context, rule);
-      noSideEffects.put(item.toReference(), rule);
-      context.markAsUsed();
+      if (item.isDexEncodedMember()) {
+        DexEncodedMember<?, ?> member = item.asDexEncodedMember();
+        if (member.holder() == appView.dexItemFactory().objectType) {
+          assert member.isDexEncodedMethod();
+          reportAssumeNoSideEffectsWarningForJavaLangClassMethod(
+              member.asDexEncodedMethod(), (ProguardAssumeNoSideEffectRule) context);
+        } else {
+          noSideEffects.put(member.toReference(), rule);
+        }
+        context.markAsUsed();
+      }
     } else if (context instanceof ProguardWhyAreYouKeepingRule) {
       reasonAsked.computeIfAbsent(item.toReference(), i -> i);
       context.markAsUsed();
     } else if (context instanceof ProguardAssumeValuesRule) {
-      assumedValues.put(item.toReference(), rule);
-      context.markAsUsed();
+      if (item.isDexEncodedMember()) {
+        assumedValues.put(item.asDexEncodedMember().toReference(), rule);
+        context.markAsUsed();
+      }
     } else if (context instanceof ProguardCheckDiscardRule) {
       checkDiscarded.computeIfAbsent(item.toReference(), i -> i);
       context.markAsUsed();
@@ -1683,18 +1694,13 @@
     }
   }
 
-  private void checkAssumeNoSideEffectsWarnings(
-      DexDefinition item, ProguardAssumeNoSideEffectRule context, ProguardMemberRule rule) {
-    if (rule.getRuleType() == ProguardMemberType.METHOD && rule.isSpecific()) {
-      return;
-    }
-    if (item.isDexEncodedMethod()) {
-      DexEncodedMethod method = item.asDexEncodedMethod();
-      if (method.holder() == options.itemFactory.objectType) {
-        OriginWithPosition key = new OriginWithPosition(context.getOrigin(), context.getPosition());
-        assumeNoSideEffectsWarnings.computeIfAbsent(key, k -> new ArrayList<>()).add(method.method);
-      }
-    }
+  private void reportAssumeNoSideEffectsWarningForJavaLangClassMethod(
+      DexEncodedMethod method, ProguardAssumeNoSideEffectRule context) {
+    assert method.getHolderType() == options.dexItemFactory().objectType;
+    OriginWithPosition key = new OriginWithPosition(context.getOrigin(), context.getPosition());
+    assumeNoSideEffectsWarnings
+        .computeIfAbsent(key, ignore -> new TreeSet<>(DexMethod::slowCompareTo))
+        .add(method.getReference());
   }
 
   private boolean isWaitOrNotifyMethod(DexMethod method) {
@@ -1708,50 +1714,27 @@
         options.getProguardConfiguration() != null
             ? options.getProguardConfiguration().getDontWarnPatterns()
             : ProguardClassFilter.empty();
+    if (dontWarnPatterns.matches(options.itemFactory.objectType)) {
+      // Don't report any warnings since we don't apply -assumenosideeffects rules to notify() or
+      // wait() anyway.
+      return;
+    }
 
     assumeNoSideEffectsWarnings.forEach(
         (originWithPosition, methods) -> {
-          boolean waitOrNotifyMethods = methods.stream().anyMatch(this::isWaitOrNotifyMethod);
-          boolean dontWarnObject = dontWarnPatterns.matches(options.itemFactory.objectType);
-          StringBuilder message = new StringBuilder();
-          message.append(
-              "The -assumenosideeffects rule matches methods on `java.lang.Object` with wildcards");
-          message.append(" including the method(s) ");
-          for (int i = 0; i < methods.size(); i++) {
-            if (i > 0) {
-              message.append(i < methods.size() - 1 ? ", " : " and ");
-            }
-            message.append("`");
-            message.append(methods.get(i).toSourceStringWithoutHolder());
-            message.append("`.");
+          boolean matchesWaitOrNotifyMethods =
+              methods.stream().anyMatch(this::isWaitOrNotifyMethod);
+          if (!matchesWaitOrNotifyMethods) {
+            // We model the remaining methods on java.lang.Object, and thus there should be no need
+            // to warn in this case.
+            return;
           }
-          if (waitOrNotifyMethods) {
-            message.append(" This will most likely cause problems.");
-          } else {
-            message.append(" This is most likely not intended.");
-          }
-          if (waitOrNotifyMethods && !dontWarnObject) {
-            message.append(" Specify the methods more precisely.");
-          } else {
-            message.append(" Consider specifying the methods more precisely.");
-          }
-          Diagnostic diagnostic =
-              new StringDiagnostic(
-                  message.toString(),
-                  originWithPosition.getOrigin(),
-                  originWithPosition.getPosition());
-          if (waitOrNotifyMethods) {
-            if (!dontWarnObject) {
-              options.reporter.error(diagnostic);
-            } else {
-              options.reporter.warning(diagnostic);
-            }
-
-          } else {
-            if (!dontWarnObject) {
-              options.reporter.warning(diagnostic);
-            }
-          }
+          options.reporter.warning(
+              new AssumeNoSideEffectsRuleForObjectMembersDiagnostic.Builder()
+                  .addMatchedMethods(methods)
+                  .setOrigin(originWithPosition.getOrigin())
+                  .setPosition(originWithPosition.getPosition())
+                  .build());
         });
   }
 
@@ -1773,8 +1756,8 @@
     public final Set<DexType> noStaticClassMerging;
     public final Set<DexReference> neverPropagateValue;
     public final Map<DexReference, ProguardMemberRule> mayHaveSideEffects;
-    public final Map<DexReference, ProguardMemberRule> noSideEffects;
-    public final Map<DexReference, ProguardMemberRule> assumedValues;
+    public final Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects;
+    public final Map<DexMember<?, ?>, ProguardMemberRule> assumedValues;
     public final Set<DexReference> identifierNameStrings;
     public final Set<ProguardIfRule> ifRules;
 
@@ -1800,8 +1783,8 @@
         Set<DexType> noStaticClassMerging,
         Set<DexReference> neverPropagateValue,
         Map<DexReference, ProguardMemberRule> mayHaveSideEffects,
-        Map<DexReference, ProguardMemberRule> noSideEffects,
-        Map<DexReference, ProguardMemberRule> assumedValues,
+        Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects,
+        Map<DexMember<?, ?>, ProguardMemberRule> assumedValues,
         Map<DexReference, MutableItemsWithRules> dependentNoShrinking,
         Map<DexReference, MutableItemsWithRules> dependentSoftPinned,
         Map<DexType, Set<ProguardKeepRuleBase>> dependentKeepClassCompatRule,
@@ -1890,11 +1873,15 @@
       if (noObfuscation.contains(original)) {
         noObfuscation.add(rewritten);
       }
-      if (noSideEffects.containsKey(original)) {
-        noSideEffects.put(rewritten, noSideEffects.get(original));
-      }
-      if (assumedValues.containsKey(original)) {
-        assumedValues.put(rewritten, assumedValues.get(original));
+      if (original.isDexMember()) {
+        assert rewritten.isDexMember();
+        DexMember<?, ?> originalMember = original.asDexMember();
+        if (noSideEffects.containsKey(originalMember)) {
+          noSideEffects.put(rewritten.asDexMember(), noSideEffects.get(originalMember));
+        }
+        if (assumedValues.containsKey(originalMember)) {
+          assumedValues.put(rewritten.asDexMember(), assumedValues.get(originalMember));
+        }
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java b/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
new file mode 100644
index 0000000..7f48f2a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
@@ -0,0 +1,36 @@
+// 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;
+
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.TypeReference;
+import java.util.Iterator;
+
+public class MethodReferenceUtils {
+
+  public static String toSourceStringWithoutHolderAndReturnType(MethodReference methodReference) {
+    return toSourceString(methodReference, false, false);
+  }
+
+  public static String toSourceString(
+      MethodReference methodReference, boolean includeHolder, boolean includeReturnType) {
+    StringBuilder builder = new StringBuilder();
+    if (includeReturnType) {
+      builder.append(methodReference.getReturnType().getTypeName()).append(" ");
+    }
+    if (includeHolder) {
+      builder.append(methodReference.getHolderClass().getTypeName()).append(".");
+    }
+    builder.append(methodReference.getMethodName()).append("(");
+    Iterator<TypeReference> formalTypesIterator = methodReference.getFormalTypes().iterator();
+    if (formalTypesIterator.hasNext()) {
+      builder.append(formalTypesIterator.next().getTypeName());
+      while (formalTypesIterator.hasNext()) {
+        builder.append(", ").append(formalTypesIterator.next().getTypeName());
+      }
+    }
+    return builder.append(")").toString();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumeNoSideEffectsForJavaLangClassTest.java b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumeNoSideEffectsForJavaLangClassTest.java
new file mode 100644
index 0000000..eb29f88
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumeNoSideEffectsForJavaLangClassTest.java
@@ -0,0 +1,141 @@
+// 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.shaking.assumenosideeffects;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.AssumeMayHaveSideEffects;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class AssumeNoSideEffectsForJavaLangClassTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return TestBase.getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public AssumeNoSideEffectsForJavaLangClassTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .enableInliningAnnotations()
+        .enableSideEffectAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject testClassSubject = inspector.clazz(TestClass.class);
+    assertThat(testClassSubject, isPresent());
+
+    inspectMethod(testClassSubject.uniqueMethodWithName("testModelingOfSideEffects"), false, false);
+    inspectMethod(
+        testClassSubject.uniqueMethodWithName("testModelingOfSideEffectsMaybeNull"), true, false);
+    inspectMethod(
+        testClassSubject.uniqueMethodWithName("testModelingOfSideEffectsMaybeSubclass"),
+        false,
+        true);
+  }
+
+  private void inspectMethod(
+      MethodSubject methodSubject, boolean maybeNullReceiver, boolean maybeSubtype) {
+    assertThat(methodSubject, isPresent());
+    assertThat(
+        methodSubject, onlyIf(maybeNullReceiver || maybeSubtype, invokesMethodWithName("equals")));
+    assertThat(
+        methodSubject,
+        onlyIf(maybeNullReceiver || maybeSubtype, invokesMethodWithName("hashCode")));
+    assertThat(methodSubject, onlyIf(maybeNullReceiver, invokesMethodWithName("getClass")));
+    assertThat(
+        methodSubject,
+        onlyIf(maybeNullReceiver || maybeSubtype, invokesMethodWithName("toString")));
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      testModelingOfSideEffects();
+      testModelingOfSideEffectsMaybeNull();
+      testModelingOfSideEffectsMaybeSubclass();
+    }
+
+    @AssumeMayHaveSideEffects
+    @NeverInline
+    static void testModelingOfSideEffects() {
+      Object o = new Object();
+      o.equals(new Object());
+      o.hashCode();
+      o.getClass();
+      o.toString();
+    }
+
+    @NeverInline
+    static void testModelingOfSideEffectsMaybeNull() {
+      createNullableObject().equals(new Object());
+      createNullableObject().hashCode();
+      createNullableObject().getClass();
+      createNullableObject().toString();
+    }
+
+    @NeverInline
+    static void testModelingOfSideEffectsMaybeSubclass() {
+      Object o = System.currentTimeMillis() > 0 ? new Object() : new A();
+      o.equals(new Object());
+      o.hashCode();
+      o.getClass();
+      o.toString();
+    }
+
+    static Object createNullableObject() {
+      return System.currentTimeMillis() > 0 ? new Object() : null;
+    }
+  }
+
+  static class A {
+
+    public A() {
+      super();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      throw new RuntimeException();
+    }
+
+    @Override
+    public int hashCode() {
+      throw new RuntimeException();
+    }
+
+    @Override
+    public String toString() {
+      throw new RuntimeException();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/B152492625.java b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/B152492625.java
index 5b0f344..8da32bc 100644
--- a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/B152492625.java
+++ b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/B152492625.java
@@ -7,17 +7,19 @@
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticPosition;
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
+import static java.util.Collections.emptyList;
 import static org.hamcrest.CoreMatchers.allOf;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.ProguardVersion;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestDiagnosticMessages;
 import com.android.tools.r8.TestParameters;
@@ -25,14 +27,12 @@
 import com.android.tools.r8.position.TextPosition;
 import com.android.tools.r8.position.TextRange;
 import com.android.tools.r8.utils.BooleanUtils;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
 import java.util.List;
 import org.hamcrest.Matcher;
-import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -54,53 +54,45 @@
     this.dontWarnObject = dontWarnObject;
   }
 
-  private void noCallToWait(CodeInspector inspector) {
-    ClassSubject classSubject = inspector.clazz(TestClass.class);
-    assertThat(classSubject, isPresent());
-    classSubject.forAllMethods(
-        foundMethodSubject ->
-            foundMethodSubject
-                .instructions(InstructionSubject::isInvokeVirtual)
-                .forEach(
-                    instructionSubject -> {
-                      Assert.assertNotEquals(
-                          "wait", instructionSubject.getMethod().name.toString());
-                    }));
+  private void checkIfWaitIsInvokedFromMain(CodeInspector inspector, boolean isR8) {
+    MethodSubject mainMethodSubject = inspector.clazz(TestClass.class).mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+    assertThat(
+        mainMethodSubject,
+        onlyIf(isR8, invokesMethod("void", "java.lang.Object", "wait", emptyList())));
   }
 
   private Matcher<Diagnostic> matchAssumeNoSideEffectsMessage() {
     return diagnosticMessage(
         containsString(
-            "The -assumenosideeffects rule matches methods on `java.lang.Object` with"
-                + " wildcards"));
+            "The -assumenosideeffects rule matches the following method(s) on java.lang.Object: "));
   }
 
   private Matcher<Diagnostic> matchMessageForAllProblematicMethods() {
     return diagnosticMessage(
         allOf(
-            containsString("void notify()"),
-            containsString("void notifyAll()"),
-            containsString("void wait()"),
-            containsString("void wait(long)"),
-            containsString("void wait(long, int)")));
+            containsString("notify()"),
+            containsString("notifyAll()"),
+            containsString("wait()"),
+            containsString("wait(long)"),
+            containsString("wait(long, int)")));
   }
 
   private Matcher<Diagnostic> matchMessageForWaitMethods() {
     return diagnosticMessage(
         allOf(
-            containsString("void wait()"),
-            containsString("void wait(long)"),
-            containsString("void wait(long, int)")));
+            containsString("wait()"),
+            containsString("wait(long)"),
+            containsString("wait(long, int)")));
   }
 
   private void assertErrorsOrWarnings(
       TestDiagnosticMessages diagnostics, List<Matcher<Diagnostic>> matchers) {
     if (dontWarnObject) {
+      diagnostics.assertNoMessages();
+    } else {
       diagnostics.assertOnlyWarnings();
       diagnostics.assertWarningsMatch(matchers);
-    } else {
-      diagnostics.assertOnlyErrors();
-      diagnostics.assertErrorsMatch(matchers);
     }
   }
 
@@ -115,23 +107,18 @@
         ImmutableList.of(
             allOf(matchAssumeNoSideEffectsMessage(), matchMessageForAllProblematicMethods()));
 
-    try {
-      testForR8(parameters.getBackend())
-          .addProgramClasses(TestClass.class, B.class)
-          .addKeepMainRule(TestClass.class)
-          .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
-          .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { *; }")
-          .setMinApi(parameters.getApiLevel())
-          .allowDiagnosticWarningMessages()
-          .compileWithExpectedDiagnostics(
-              diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
-          .inspect(this::noCallToWait)
-          .run(parameters.getRuntime(), TestClass.class)
-          .assertSuccessWithOutputLines("Hello, world");
-      assertTrue(dontWarnObject);
-    } catch (CompilationFailedException e) {
-      assertFalse(dontWarnObject);
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, B.class)
+        .addKeepMainRule(TestClass.class)
+        .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
+        .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { *; }")
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticWarningMessages(!dontWarnObject)
+        .compileWithExpectedDiagnostics(
+            diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
+        .inspect(inspector -> checkIfWaitIsInvokedFromMain(inspector, true))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello, world");
   }
 
   @Test
@@ -169,30 +156,24 @@
                 diagnosticOrigin(methodsRuleOrigin),
                 diagnosticPosition(textRangeForString(methodsRule))));
 
-    try {
-      testForR8(parameters.getBackend())
-          .addProgramClasses(TestClass.class, B.class)
-          .addKeepMainRule(TestClass.class)
-          .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
-          .apply(
-              b ->
-                  b.getBuilder()
-                      .addProguardConfiguration(ImmutableList.of(starRule), starRuleOrigin))
-          .apply(
-              b ->
-                  b.getBuilder()
-                      .addProguardConfiguration(ImmutableList.of(methodsRule), methodsRuleOrigin))
-          .setMinApi(parameters.getApiLevel())
-          .allowDiagnosticWarningMessages()
-          .compileWithExpectedDiagnostics(
-              diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
-          .inspect(this::noCallToWait)
-          .run(parameters.getRuntime(), TestClass.class)
-          .assertSuccessWithOutputLines("Hello, world");
-      assertTrue(dontWarnObject);
-    } catch (CompilationFailedException e) {
-      assertFalse(dontWarnObject);
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, B.class)
+        .addKeepMainRule(TestClass.class)
+        .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
+        .apply(
+            b ->
+                b.getBuilder().addProguardConfiguration(ImmutableList.of(starRule), starRuleOrigin))
+        .apply(
+            b ->
+                b.getBuilder()
+                    .addProguardConfiguration(ImmutableList.of(methodsRule), methodsRuleOrigin))
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticWarningMessages(!dontWarnObject)
+        .compileWithExpectedDiagnostics(
+            diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
+        .inspect(inspector -> checkIfWaitIsInvokedFromMain(inspector, true))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello, world");
   }
 
   @Test
@@ -203,22 +184,9 @@
         .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
         .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { hash*(); }")
         .setMinApi(parameters.getApiLevel())
-        .allowDiagnosticWarningMessages(!dontWarnObject)
-        .compileWithExpectedDiagnostics(
-            diagnostics -> {
-              if (dontWarnObject) {
-                diagnostics.assertNoMessages();
-              } else {
-                diagnostics.assertOnlyWarnings();
-                diagnostics.assertWarningsMatch(
-                    allOf(
-                        matchAssumeNoSideEffectsMessage(),
-                        diagnosticMessage(containsString("int hashCode()"))));
-              }
-            })
+        .compile()
         .run(parameters.getRuntime(), TestClass.class)
-        // Code fails with exception if wait call is not removed.
-        .assertFailureWithErrorThatThrows(IllegalMonitorStateException.class);
+        .assertSuccessWithOutputLines("Hello, world");
   }
 
   @Test
@@ -227,23 +195,18 @@
         ImmutableList.of(
             allOf(matchAssumeNoSideEffectsMessage(), matchMessageForAllProblematicMethods()));
 
-    try {
-      testForR8(parameters.getBackend())
-          .addProgramClasses(TestClass.class, B.class)
-          .addKeepMainRule(TestClass.class)
-          .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
-          .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { <methods>; }")
-          .setMinApi(parameters.getApiLevel())
-          .allowDiagnosticWarningMessages()
-          .compileWithExpectedDiagnostics(
-              diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
-          .inspect(this::noCallToWait)
-          .run(parameters.getRuntime(), TestClass.class)
-          .assertSuccessWithOutputLines("Hello, world");
-      assertTrue(dontWarnObject);
-    } catch (CompilationFailedException e) {
-      assertFalse(dontWarnObject);
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, B.class)
+        .addKeepMainRule(TestClass.class)
+        .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
+        .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { <methods>; }")
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticWarningMessages(!dontWarnObject)
+        .compileWithExpectedDiagnostics(
+            diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
+        .inspect(inspector -> checkIfWaitIsInvokedFromMain(inspector, true))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello, world");
   }
 
   @Test
@@ -251,36 +214,35 @@
     List<Matcher<Diagnostic>> matchers =
         ImmutableList.of(allOf(matchAssumeNoSideEffectsMessage(), matchMessageForWaitMethods()));
 
-    try {
-      testForR8(parameters.getBackend())
-          .addProgramClasses(TestClass.class, B.class)
-          .addKeepMainRule(TestClass.class)
-          .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
-          .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { *** w*(...); }")
-          .setMinApi(parameters.getApiLevel())
-          .allowDiagnosticWarningMessages()
-          .compileWithExpectedDiagnostics(
-              diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
-          .inspect(this::noCallToWait)
-          .run(parameters.getRuntime(), TestClass.class)
-          .assertSuccessWithOutputLines("Hello, world");
-      assertTrue(dontWarnObject);
-    } catch (CompilationFailedException e) {
-      assertFalse(dontWarnObject);
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClasses(TestClass.class, B.class)
+        .addKeepMainRule(TestClass.class)
+        .applyIf(dontWarnObject, tb -> tb.addKeepRules("-dontwarn java.lang.Object"))
+        .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { *** w*(...); }")
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticWarningMessages(!dontWarnObject)
+        .compileWithExpectedDiagnostics(
+            diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
+        .inspect(inspector -> checkIfWaitIsInvokedFromMain(inspector, true))
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello, world");
   }
 
   @Test
   public void testR8WaitSpecificMethodMatch() throws Exception {
     assumeTrue("No need to run this with -dontwarn java.lang.Object", !dontWarnObject);
 
+    List<Matcher<Diagnostic>> matchers = ImmutableList.of(matchAssumeNoSideEffectsMessage());
+
     testForR8(parameters.getBackend())
         .addProgramClasses(TestClass.class, B.class)
         .addKeepMainRule(TestClass.class)
         .addKeepRules("-assumenosideeffects class java.lang.Object { void wait(); }")
         .setMinApi(parameters.getApiLevel())
-        .compile()
-        .inspect(this::noCallToWait)
+        .allowDiagnosticWarningMessages()
+        .compileWithExpectedDiagnostics(
+            diagnostics -> assertErrorsOrWarnings(diagnostics, matchers))
+        .inspect(inspector -> checkIfWaitIsInvokedFromMain(inspector, true))
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutputLines("Hello, world");
   }
@@ -313,7 +275,7 @@
     assumeTrue("No need to run this with -dontwarn java.lang.Object", !dontWarnObject);
     assumeTrue(parameters.isCfRuntime());
 
-    testForProguard()
+    testForProguard(ProguardVersion.getLatest())
         .addProgramClasses(TestClass.class, B.class)
         .addKeepMainRule(TestClass.class)
         .addKeepRules("-assumenosideeffects class " + B.class.getTypeName() + " { *; }")
@@ -321,7 +283,7 @@
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), TestClass.class)
-        .assertFailureWithErrorThatThrows(IllegalMonitorStateException.class);
+        .assertSuccessWithOutputLines("Hello, world");
   }
 
   @Test
@@ -329,24 +291,28 @@
     assumeTrue("No need to run this with -dontwarn java.lang.Object", !dontWarnObject);
     assumeTrue(parameters.isCfRuntime());
 
-    testForProguard()
+    testForProguard(ProguardVersion.getLatest())
         .addProgramClasses(TestClass.class, B.class)
         .addKeepMainRule(TestClass.class)
         .addKeepRules("-assumenosideeffects class java.lang.Object { void wait(); }")
         .addKeepRules("-dontwarn " + B152492625.class.getTypeName())
         .setMinApi(parameters.getApiLevel())
         .compile()
-        .inspect(this::noCallToWait)
+        .inspect(inspector -> checkIfWaitIsInvokedFromMain(inspector, false))
         .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutputLines("Hello, world");
+        .assertSuccessWithOutput("Hello");
   }
 
   static class TestClass {
 
     public void m() throws Exception {
-      System.out.println("Hello, world");
-      // test fails if wait is not removed.
-      wait();
+      System.out.print("Hello");
+      // test fails if wait is removed.
+      try {
+        wait();
+      } catch (IllegalMonitorStateException e) {
+        System.out.println(", world");
+      }
     }
 
     public static void main(String[] args) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/smali/OutlineTest.java b/src/test/java/com/android/tools/r8/smali/OutlineTest.java
index 578376e..e85f2d8 100644
--- a/src/test/java/com/android/tools/r8/smali/OutlineTest.java
+++ b/src/test/java/com/android/tools/r8/smali/OutlineTest.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.code.InvokeStaticRange;
 import com.android.tools.r8.code.InvokeVirtual;
 import com.android.tools.r8.code.MoveResult;
+import com.android.tools.r8.code.MoveResultObject;
 import com.android.tools.r8.code.MoveResultWide;
 import com.android.tools.r8.code.Return;
 import com.android.tools.r8.code.ReturnObject;
@@ -42,7 +43,6 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.function.Consumer;
 import org.junit.Assert;
@@ -1299,24 +1299,23 @@
     SmaliBuilder builder = new SmaliBuilder(DEFAULT_CLASS_NAME);
 
     // The result from neither the div-int is never used.
-    MethodSignature signature = builder.addStaticMethod(
-        "void",
-        DEFAULT_METHOD_NAME,
-        ImmutableList.of("int", "int"),
-        1,
-        "    div-int             v0, p0, p1",
-        "    new-instance        v0, Ljava/lang/StringBuilder;",
-        "    invoke-direct       { v0 }, Ljava/lang/StringBuilder;-><init>()V",
-        "    return-void"
-    );
+    MethodSignature signature =
+        builder.addStaticMethod(
+            "java.lang.Object",
+            DEFAULT_METHOD_NAME,
+            ImmutableList.of("int", "int"),
+            1,
+            "    div-int             v0, p0, p1",
+            "    new-instance        v0, Ljava/lang/StringBuilder;",
+            "    invoke-direct       { v0 }, Ljava/lang/StringBuilder;-><init>()V",
+            "    return-object       v0");
 
     builder.addMainMethod(
         2,
         "    const               v0, 1",
         "    const               v1, 2",
-        "    invoke-static       { v0, v1 }, LTest;->method(II)V",
-        "    return-void"
-    );
+        "    invoke-static       { v0, v1 }, LTest;->method(II)Ljava/lang/Object;",
+        "    return-void");
 
     Consumer<InternalOptions> options =
         configureOptions(
@@ -1324,20 +1323,6 @@
               opts.outline.threshold = 1;
               opts.outline.minSize = 3;
               opts.outline.maxSize = 3;
-
-              // Do not allow dead code elimination of the new-instance and invoke-direct
-              // instructions.
-              // This can be achieved by not assuming that StringBuilder is present.
-              DexItemFactory dexItemFactory = opts.itemFactory;
-              dexItemFactory.libraryTypesAssumedToBePresent =
-                  new HashSet<>(dexItemFactory.libraryTypesAssumedToBePresent);
-              dexItemFactory.libraryTypesAssumedToBePresent.remove(
-                  dexItemFactory.stringBuilderType);
-              // ... and not assuming that StringBuilder.<init>() cannot have side effects.
-              dexItemFactory.libraryMethodsWithoutSideEffects =
-                  new IdentityHashMap<>(dexItemFactory.libraryMethodsWithoutSideEffects);
-              dexItemFactory.libraryMethodsWithoutSideEffects.remove(
-                  dexItemFactory.stringBuilderMethods.defaultConstructor);
             });
 
     AndroidApp originalApplication = buildApplicationWithAndroidJar(builder);
@@ -1347,9 +1332,10 @@
     // Return the processed method for inspection.
     DexEncodedMethod method = getMethod(processedApplication, signature);
     DexCode code = method.getCode().asDexCode();
-    assertEquals(2, code.instructions.length);
+    assertEquals(3, code.instructions.length);
     assertTrue(code.instructions[0] instanceof InvokeStatic);
-    assertTrue(code.instructions[1] instanceof ReturnVoid);
+    assertTrue(code.instructions[1] instanceof MoveResultObject);
+    assertTrue(code.instructions[2] instanceof ReturnObject);
     InvokeStatic invoke = (InvokeStatic) code.instructions[0];
     assertEquals(firstOutlineMethodName(), invoke.getMethod().qualifiedName());
 
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
index 86a21c1..ed4d158 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
@@ -6,7 +6,9 @@
 
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
+import java.util.List;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.hamcrest.TypeSafeMatcher;
@@ -71,10 +73,93 @@
     };
   }
 
+  public static Matcher<MethodSubject> invokesMethod(
+      String returnType, String holderType, String methodName, List<String> parameterTypes) {
+    return new TypeSafeMatcher<MethodSubject>() {
+      @Override
+      protected boolean matchesSafely(MethodSubject subject) {
+        if (!subject.isPresent()) {
+          return false;
+        }
+        if (!subject.getMethod().hasCode()) {
+          return false;
+        }
+        return subject
+            .streamInstructions()
+            .anyMatch(isInvokeWithTarget(returnType, holderType, methodName, parameterTypes));
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        StringBuilder text =
+            new StringBuilder("invokes method `")
+                .append(returnType != null ? returnType : "*")
+                .append(" ")
+                .append(holderType != null ? holderType : "*")
+                .append(".")
+                .append(methodName != null ? methodName : "*")
+                .append("(");
+        if (parameterTypes != null) {
+          text.append(
+              parameterTypes.stream()
+                  .map(parameterType -> parameterType != null ? parameterType : "*")
+                  .collect(Collectors.joining(", ")));
+        } else {
+          text.append("...");
+        }
+        text.append(")`");
+        description.appendText(text.toString());
+      }
+
+      @Override
+      public void describeMismatchSafely(final MethodSubject subject, Description description) {
+        description.appendText("method did not");
+      }
+    };
+  }
+
+  public static Matcher<MethodSubject> invokesMethodWithName(String name) {
+    return invokesMethod(null, null, name, null);
+  }
+
   public static Predicate<InstructionSubject> isInvokeWithTarget(DexMethod target) {
     return instruction -> instruction.isInvoke() && instruction.getMethod() == target;
   }
 
+  public static Predicate<InstructionSubject> isInvokeWithTarget(
+      String returnType, String holderType, String methodName, List<String> parameterTypes) {
+    return instruction -> {
+      if (!instruction.isInvoke()) {
+        return false;
+      }
+      DexMethod invokedMethod = instruction.getMethod();
+      if (returnType != null
+          && !invokedMethod.getReturnType().toSourceString().equals(returnType)) {
+        return false;
+      }
+      if (holderType != null
+          && !invokedMethod.getHolderType().toSourceString().equals(holderType)) {
+        return false;
+      }
+      if (methodName != null && !invokedMethod.getName().toSourceString().equals(methodName)) {
+        return false;
+      }
+      if (parameterTypes != null) {
+        if (parameterTypes.size() != invokedMethod.getArity()) {
+          return false;
+        }
+        for (int i = 0; i < parameterTypes.size(); i++) {
+          String parameterType = parameterTypes.get(i);
+          if (parameterType != null
+              && !invokedMethod.getParameter(i).toSourceString().equals(parameterType)) {
+            return false;
+          }
+        }
+      }
+      return true;
+    };
+  }
+
   public static Predicate<InstructionSubject> isFieldAccessWithTarget(DexField target) {
     return instruction -> instruction.isFieldAccess() && instruction.getField() == target;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
index 98c8562..29bebe1 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
@@ -713,6 +713,10 @@
     }
   }
 
+  public static <T> Matcher<T> onlyIf(boolean condition, Matcher<T> matcher) {
+    return notIf(matcher, !condition);
+  }
+
   public static <T> Matcher<T> notIf(Matcher<T> matcher, boolean condition) {
     if (condition) {
       return not(matcher);