Merge "Preparation for -adaptresourcefilecontents support"
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 405096e..9f12853 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -160,6 +160,7 @@
       options.enableMinification = false;
       options.enableInlining = false;
       options.enableClassInlining = false;
+      options.enableClassStaticizer = false;
       options.outline.enabled = false;
 
       DexApplication app = new ApplicationReader(inputApp, options, timing).read(executor);
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 8c6c02b..99b0a1e 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -237,6 +237,8 @@
     internal.enableInlining = false;
     assert internal.enableClassInlining;
     internal.enableClassInlining = false;
+    assert internal.enableClassStaticizer;
+    internal.enableClassStaticizer = false;
     assert internal.enableSwitchMapRemoval;
     internal.enableSwitchMapRemoval = false;
     assert internal.outline.enabled;
diff --git a/src/main/java/com/android/tools/r8/DiagnosticsHandler.java b/src/main/java/com/android/tools/r8/DiagnosticsHandler.java
index 74a9a96..666e358 100644
--- a/src/main/java/com/android/tools/r8/DiagnosticsHandler.java
+++ b/src/main/java/com/android/tools/r8/DiagnosticsHandler.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
 
 /**
  * A DiagnosticsHandler can be provided to customize handling of diagnostics information.
@@ -20,7 +21,11 @@
    */
   default void error(Diagnostic error) {
     if (error.getOrigin() != Origin.unknown()) {
-      System.err.print("Error in " + error.getOrigin() + ":\n  ");
+      System.err.print("Error in " + error.getOrigin());
+      if (error.getPosition() != Position.UNKNOWN) {
+        System.err.print(" at " + error.getPosition().getDescription());
+      }
+      System.err.println(":");
     } else {
       System.err.print("Error: ");
     }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index e2b8ea8..215ced9 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -399,6 +399,7 @@
             new IRConverter(
                 appView.getAppInfo(), options, timing, printer, appView.getGraphLense());
         application = converter.optimize(application, executorService);
+        appView.setGraphLense(converter.getGraphLense());
       } finally {
         timing.end();
       }
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index f0ae9e0..4eb4b37 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -571,6 +571,7 @@
       // TODO(zerny): Should we support inlining in debug mode? b/62937285
       internal.enableInlining = false;
       internal.enableClassInlining = false;
+      internal.enableClassStaticizer = false;
       // TODO(zerny): Should we support outlining in debug mode? b/62937285
       internal.outline.enabled = false;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index 0ef8a34..c29453a 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -528,7 +528,7 @@
     return false;
   }
 
-  private static boolean canTriggerStaticInitializer(DexClass clazz) {
+  public static boolean canTriggerStaticInitializer(DexClass clazz) {
     // Assume it *may* trigger if we didn't find the definition.
     return clazz == null || clazz.hasClassInitializer();
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index 4a0a483..d0c40e1 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -62,6 +62,17 @@
     hashCode();  // Cache the hash code eagerly.
   }
 
+  public DexCode withoutThisParameter() {
+    // Note that we assume the original code has a register associated with 'this'
+    // argument of the (former) instance method. We also assume (but do not check)
+    // that 'this' register is never used, so when we decrease incoming register size
+    // by 1, it becomes just a regular register which is never used, and thus will be
+    // gone when we build an IR from this code. Rebuilding IR for methods 'staticized'
+    // this way is highly recommended to improve register allocation.
+    return new DexCode(registerSize, incomingRegisterSize - 1, outgoingRegisterSize,
+        instructions, tries, handlers, debugInfoWithoutFirstParameter(), highestSortingString);
+  }
+
   @Override
   public boolean isDexCode() {
     return true;
@@ -107,6 +118,19 @@
     return new DexDebugInfo(debugInfo.startLine, newParameters, debugInfo.events);
   }
 
+  public DexDebugInfo debugInfoWithoutFirstParameter() {
+    if (debugInfo == null) {
+      return null;
+    }
+    DexString[] parameters = debugInfo.parameters;
+    if(parameters.length == 0) {
+      return debugInfo;
+    }
+    DexString[] newParameters = new DexString[parameters.length - 1];
+    System.arraycopy(parameters, 1, newParameters, 0, parameters.length - 1);
+    return new DexDebugInfo(debugInfo.startLine, newParameters, debugInfo.events);
+  }
+
   public int codeSizeInBytes() {
     Instruction last = instructions[instructions.length - 1];
     return last.getOffset() + last.getSize();
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index cf1a105..e90839c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -24,6 +24,7 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.JumboStringRewriter;
 import com.android.tools.r8.dex.MixedSectionCollection;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo.ResolutionResult;
 import com.android.tools.r8.graph.ParameterUsagesInfo.ParameterUsage;
 import com.android.tools.r8.ir.code.IRCode;
@@ -541,6 +542,14 @@
     return builder.build();
   }
 
+  public DexEncodedMethod toStaticMethodWithoutThis() {
+    assert !accessFlags.isStatic();
+    Builder builder = builder(this);
+    builder.setStatic();
+    builder.withoutThisParameter();
+    return builder.build();
+  }
+
   /**
    * Rewrites the code in this method to have JumboString bytecode if required by mapping.
    * <p>
@@ -658,6 +667,7 @@
       forceInline = template.forceInline;
       useIdentifierNameString = template.useIdentifierNameString;
       checksNullReceiverBeforeAnySideEffect = template.checksNullReceiverBeforeAnySideEffect;
+      trivialInitializerInfo = template.trivialInitializerInfo;
     }
 
     public void setParameterUsages(ParameterUsagesInfo parametersUsages) {
@@ -928,6 +938,19 @@
       this.method = method;
     }
 
+    public void setStatic() {
+      this.accessFlags.setStatic();
+    }
+
+    public void withoutThisParameter() {
+      assert code != null;
+      if (code.isDexCode()) {
+        code = code.asDexCode().withoutThisParameter();
+      } else {
+        throw new Unreachable("Code " + code.getClass().getSimpleName() + " is not supported.");
+      }
+    }
+
     public void setCode(Code code) {
       this.code = code;
     }
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 02d2898..006da92 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -118,6 +118,10 @@
     return this == other || isStrictSubtypeOf(other, appInfo);
   }
 
+  public boolean hasSubtypes() {
+    return !directSubtypes.isEmpty();
+  }
+
   public boolean isStrictSubtypeOf(DexType other, AppInfo appInfo) {
     if (this == other) {
       return false;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Goto.java b/src/main/java/com/android/tools/r8/ir/code/Goto.java
index cd5fcc8..80a76fe 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Goto.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Goto.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.utils.CfgPrinter;
 import java.util.List;
+import java.util.ListIterator;
 
 public class Goto extends JumpInstruction {
 
@@ -103,6 +104,18 @@
     // Nothing to do.
   }
 
+  public boolean isTrivialGotoToTheNextBlock(IRCode code) {
+    BasicBlock thisBlock = getBlock();
+    ListIterator<BasicBlock> blockIterator = code.blocks.listIterator();
+    while (blockIterator.hasNext()) {
+      BasicBlock block = blockIterator.next();
+      if (thisBlock == block) {
+        return blockIterator.hasNext() && blockIterator.next() == getTarget();
+      }
+    }
+    return false;
+  }
+
   @Override
   public void buildCf(CfBuilder builder) {
     builder.add(new CfGoto(builder.getLabel(getTarget())));
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index d9bf8d0..deae051 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -266,6 +266,14 @@
     block = null;
   }
 
+  public void removeOrReplaceByDebugLocalRead() {
+    getBlock().listIterator(this).removeOrReplaceByDebugLocalRead();
+  }
+
+  public void replace(Instruction newInstruction) {
+    getBlock().listIterator(this).replaceCurrentInstruction(newInstruction);
+  }
+
   /**
    * Returns true if the instruction is in the IR and therefore has a block.
    */
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index f523b46..af98f4b 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -50,6 +50,7 @@
 import com.android.tools.r8.ir.optimize.RedundantFieldLoadElimination;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInliner;
 import com.android.tools.r8.ir.optimize.lambda.LambdaMerger;
+import com.android.tools.r8.ir.optimize.staticizer.ClassStaticizer;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.kotlin.KotlinInfo;
@@ -94,9 +95,10 @@
   private final InterfaceMethodRewriter interfaceMethodRewriter;
   private final LambdaMerger lambdaMerger;
   private final ClassInliner classInliner;
+  private final ClassStaticizer classStaticizer;
   private final InternalOptions options;
   private final CfgPrinter printer;
-  private final GraphLense graphLense;
+  private GraphLense graphLense;
   private final CodeRewriter codeRewriter;
   private final MemberValuePropagation memberValuePropagation;
   private final LensCodeRewriter lensCodeRewriter;
@@ -144,7 +146,7 @@
     if (enableWholeProgramOptimizations) {
       assert appInfo.hasLiveness();
       this.nonNullTracker = new NonNullTracker();
-      this.inliner = new Inliner(appInfo.withLiveness(), graphLense, options);
+      this.inliner = new Inliner(this, options);
       this.outliner = new Outliner(appInfo.withLiveness(), options);
       this.memberValuePropagation =
           options.enableValuePropagation ?
@@ -182,6 +184,17 @@
             ? new ClassInliner(
             appInfo.dexItemFactory, lambdaRewriter, options.classInliningInstructionLimit)
             : null;
+    this.classStaticizer = options.enableClassStaticizer && appInfo.hasLiveness()
+        ? new ClassStaticizer(appInfo.withLiveness(), this) : null;
+  }
+
+  public void setGraphLense(GraphLense graphLense) {
+    assert graphLense != null;
+    this.graphLense = graphLense;
+  }
+
+  public GraphLense getGraphLense() {
+    return graphLense;
   }
 
   /**
@@ -257,6 +270,18 @@
     }
   }
 
+  private void staticizeClasses(OptimizationFeedback feedback) {
+    if (classStaticizer != null) {
+      classStaticizer.staticizeCandidates(feedback);
+    }
+  }
+
+  private void collectStaticizerCandidates(DexApplication application) {
+    if (classStaticizer != null) {
+      classStaticizer.collectCandidates(application);
+    }
+  }
+
   private void desugarInterfaceMethods(
       Builder<?> builder, InterfaceMethodRewriter.Flavor includeAllResources) {
     if (interfaceMethodRewriter != null) {
@@ -408,6 +433,7 @@
       throws ExecutionException {
     removeLambdaDeserializationMethods();
     collectLambdaMergingCandidates(application);
+    collectStaticizerCandidates(application);
 
     // The process is in two phases.
     // 1) Subject all DexEncodedMethods to optimization (except outlining).
@@ -438,6 +464,8 @@
     Builder<?> builder = application.builder();
     builder.setHighestSortingString(highestSortingString);
 
+    staticizeClasses(directFeedback);
+
     // Second inlining pass for dealing with double inline callers.
     if (inliner != null) {
       // Use direct feedback still, since methods after inlining may
@@ -676,6 +704,11 @@
       }
     }
 
+    if (classStaticizer != null) {
+      classStaticizer.fixupMethodCode(method, code);
+      assert code.isConsistentSSA();
+    }
+
     if (identifierNameStringMarker != null) {
       identifierNameStringMarker.decoupleIdentifierNameStringsInMethod(method, code);
       assert code.isConsistentSSA();
@@ -802,6 +835,9 @@
       codeRewriter.identifyTrivialInitializer(method, code, feedback);
     }
     codeRewriter.identifyParameterUsages(method, code, feedback);
+    if (classStaticizer != null) {
+      classStaticizer.examineMethodCode(method, code);
+    }
 
     if (options.canHaveNumberConversionRegisterAllocationBug()) {
       codeRewriter.workaroundNumberConversionRegisterAllocationBug(code);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index d87a508..a8059bd 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -953,6 +953,14 @@
         continue;
       }
 
+      if (insn.isGoto()) {
+        // Trivial goto to the next block.
+        if (insn.asGoto().isTrivialGotoToTheNextBlock(code)) {
+          continue;
+        }
+        return null;
+      }
+
       // Other instructions make the instance initializer not eligible.
       return null;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 1cbd2d9..04cd083 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -43,8 +43,8 @@
 public class Inliner {
   private static final int INITIAL_INLINING_INSTRUCTION_ALLOWANCE = 1500;
 
+  private final IRConverter converter;
   protected final AppInfoWithLiveness appInfo;
-  private final GraphLense graphLense;
   final InternalOptions options;
 
   // State for inlining methods which are known to be called twice.
@@ -55,12 +55,9 @@
 
   private final Set<DexMethod> blackList = Sets.newIdentityHashSet();
 
-  public Inliner(
-      AppInfoWithLiveness appInfo,
-      GraphLense graphLense,
-      InternalOptions options) {
-    this.appInfo = appInfo;
-    this.graphLense = graphLense;
+  public Inliner(IRConverter converter, InternalOptions options) {
+    this.converter = converter;
+    this.appInfo = converter.appInfo.withLiveness();
     this.options = options;
     fillInBlackList(appInfo);
   }
@@ -588,11 +585,11 @@
             }
             assert invokePosition.callerPosition == null
                 || invokePosition.getOutermostCaller().method
-                    == graphLense.getOriginalMethodSignature(method.method);
+                    == converter.getGraphLense().getOriginalMethodSignature(method.method);
 
             IRCode inlinee =
-                result.buildInliningIR(
-                    code.valueNumberGenerator, appInfo, graphLense, options, invokePosition);
+                result.buildInliningIR(code.valueNumberGenerator,
+                    appInfo, converter.getGraphLense(), options, invokePosition);
             if (inlinee != null) {
               // TODO(64432527): Get rid of this additional check by improved inlining.
               if (block.hasCatchHandlers() && inlinee.computeNormalExitBlocks().isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
new file mode 100644
index 0000000..9b5af03
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
@@ -0,0 +1,628 @@
+// Copyright (c) 2018, 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.staticizer;
+
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+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.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.NewInstance;
+import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.ir.code.StaticPut;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.conversion.OptimizationFeedback;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
+
+public final class ClassStaticizer {
+  final AppInfoWithLiveness appInfo;
+  final DexItemFactory factory;
+  final IRConverter converter;
+
+  private enum Phase {
+    None, Examine, Fixup
+  }
+
+  private Phase phase = Phase.None;
+  private BiConsumer<DexEncodedMethod, IRCode> fixupStrategy = null;
+
+  // Represents a staticizing candidate with all information
+  // needed for staticizing.
+  final class CandidateInfo {
+    final DexProgramClass candidate;
+    final DexEncodedField singletonField;
+    final AtomicBoolean preserveRead = new AtomicBoolean(false);
+    // Number of singleton field writes.
+    final AtomicInteger fieldWrites = new AtomicInteger();
+    // Number of instances created.
+    final AtomicInteger instancesCreated = new AtomicInteger();
+    final Set<DexEncodedMethod> referencedFrom = Sets.newConcurrentHashSet();
+    final AtomicReference<DexEncodedMethod> constructor = new AtomicReference<>();
+
+    CandidateInfo(DexProgramClass candidate, DexEncodedField singletonField) {
+      assert candidate != null;
+      assert singletonField != null;
+      this.candidate = candidate;
+      this.singletonField = singletonField;
+
+      // register itself
+      candidates.put(candidate.type, this);
+    }
+
+    boolean isHostClassInitializer(DexEncodedMethod method) {
+      return factory.isClassConstructor(method.method) && method.method.holder == hostType();
+    }
+
+    DexType hostType() {
+      return singletonField.field.clazz;
+    }
+
+    DexClass hostClass() {
+      DexClass hostClass = appInfo.definitionFor(hostType());
+      assert hostClass != null;
+      return hostClass;
+    }
+
+    CandidateInfo invalidate() {
+      candidates.remove(candidate.type);
+      return null;
+    }
+  }
+
+  // The map storing all the potential candidates for staticizing.
+  final ConcurrentHashMap<DexType, CandidateInfo> candidates = new ConcurrentHashMap<>();
+
+  public ClassStaticizer(AppInfoWithLiveness appInfo, IRConverter converter) {
+    this.appInfo = appInfo;
+    this.factory = appInfo.dexItemFactory;
+    this.converter = converter;
+  }
+
+  // Before doing any usage-based analysis we collect a set of classes that can be
+  // candidates for staticizing. This analysis is very simple, but minimizes the
+  // set of eligible classes staticizer tracks and thus time and memory it needs.
+  public final void collectCandidates(DexApplication app) {
+    Set<DexType> notEligible = Sets.newIdentityHashSet();
+    Map<DexType, DexEncodedField> singletonFields = new HashMap<>();
+
+    app.classes().forEach(cls -> {
+      // We only consider classes eligible for staticizing if there is just
+      // one single static field in the whole app which has a type of this
+      // class. This field will be considered to be a candidate for a singleton
+      // field. The requirements for the initialization of this field will be
+      // checked later.
+      for (DexEncodedField field : cls.staticFields()) {
+        DexType type = field.field.type;
+        if (singletonFields.put(type, field) != null) {
+          // There is already candidate singleton field found.
+          notEligible.add(type);
+        }
+      }
+
+      // Don't allow fields with this candidate types.
+      for (DexEncodedField field : cls.instanceFields()) {
+        notEligible.add(field.field.type);
+      }
+
+      // Let's also assume no methods should take or return a
+      // value of this type.
+      for (DexEncodedMethod method : cls.methods()) {
+        DexProto proto = method.method.proto;
+        notEligible.add(proto.returnType);
+        notEligible.addAll(Arrays.asList(proto.parameters.values));
+      }
+
+      // High-level limitations on what classes we consider eligible.
+      if (cls.isInterface() || // Must not be an interface or an abstract class.
+          cls.accessFlags.isAbstract() ||
+          // Don't support candidates with instance fields
+          cls.instanceFields().length > 0 ||
+          // Only support classes directly extending java.lang.Object
+          cls.superType != factory.objectType ||
+          // Instead of requiring the class being final,
+          // just ensure it does not have subtypes
+          cls.type.hasSubtypes() ||
+          // Staticizing classes implementing interfaces is more
+          // difficult, so don't support it until we really need it.
+          !cls.interfaces.isEmpty()) {
+        notEligible.add(cls.type);
+      }
+    });
+
+    // Finalize the set of the candidates.
+    app.classes().forEach(cls -> {
+      DexType type = cls.type;
+      if (!notEligible.contains(type)) {
+        DexEncodedField field = singletonFields.get(type);
+        if (field != null && // Singleton field found
+            !field.accessFlags.isVolatile() && // Don't remove volatile fields.
+            !isPinned(cls, field)) { // Don't remove pinned objects.
+          assert field.accessFlags.isStatic();
+          // Note: we don't check that the field is final, since we will analyze
+          //       later how and where it is initialized.
+          new CandidateInfo(cls, field); // will self-register
+        }
+      }
+    });
+
+    // Next phase -- examine code for candidate usages
+    phase = Phase.Examine;
+  }
+
+  private boolean isPinned(DexClass clazz, DexEncodedField singletonField) {
+    if (appInfo.isPinned(clazz.type) || appInfo.isPinned(singletonField.field)) {
+      return true;
+    }
+    for (DexEncodedMethod method : clazz.methods()) {
+      if (!method.isStaticMethod() && appInfo.isPinned(method.method)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // Check staticizing candidates' usages to ensure the candidate can be staticized.
+  //
+  // The criteria for type CANDIDATE to be eligible for staticizing fall into
+  // these categories:
+  //
+  //  * checking that there is only one instance of the class created, and it is created
+  //    inside the host class initializer, and it is guaranteed that nobody can access this
+  //    field before it is assigned.
+  //
+  //  * no other singleton field writes (except for those used to store the only candidate
+  //    class instance described above) are allowed.
+  //
+  //  * values read from singleton field should only be used for instance method calls.
+  //
+  // NOTE: there are more criteria eligible class needs to satisfy to be actually staticized,
+  // those will be checked later in staticizeCandidates().
+  //
+  // This method also collects all DexEncodedMethod instances that need to be rewritten if
+  // appropriate candidate is staticized. Essentially anything that references instance method
+  // or field defined in the class.
+  //
+  // NOTE: can be called concurrently.
+  public final void examineMethodCode(DexEncodedMethod method, IRCode code) {
+    if (phase != Phase.Examine) {
+      return;
+    }
+
+    Set<Instruction> alreadyProcessed = Sets.newIdentityHashSet();
+
+    CandidateInfo receiverClassCandidateInfo = candidates.get(method.method.holder);
+    Value receiverValue = code.getThis(); // NOTE: is null for static methods.
+    if (receiverClassCandidateInfo != null && receiverValue != null) {
+      // We are inside an instance method of candidate class (not an instance initializer
+      // which we will check later), check if all the references to 'this' are valid
+      // (the call will invalidate the candidate if some of them are not valid).
+      analyzeAllValueUsers(receiverClassCandidateInfo,
+          receiverValue, factory.isConstructor(method.method));
+
+      // If the candidate is still valid, ignore all instructions
+      // we treat as valid usages on receiver.
+      if (candidates.get(method.method.holder) != null) {
+        alreadyProcessed.addAll(receiverValue.uniqueUsers());
+      }
+    }
+
+    ListIterator<Instruction> iterator =
+        Lists.newArrayList(code.instructionIterator()).listIterator();
+    while (iterator.hasNext()) {
+      Instruction instruction = iterator.next();
+      if (alreadyProcessed.contains(instruction)) {
+        continue;
+      }
+
+      if (instruction.isNewInstance()) {
+        // Check the class being initialized against valid staticizing candidates.
+        NewInstance newInstance = instruction.asNewInstance();
+        CandidateInfo candidateInfo = processInstantiation(method, iterator, newInstance);
+        if (candidateInfo != null) {
+          // For host class initializers having eligible instantiation we also want to
+          // ensure that the rest of the initializer consist of code w/o side effects.
+          // This must guarantee that removing field access will not result in missing side
+          // effects, otherwise we can still staticize, but cannot remove singleton reads.
+          while (iterator.hasNext()) {
+            if (!isAllowedInHostClassInitializer(method.method.holder, iterator.next(), code)) {
+              candidateInfo.preserveRead.set(true);
+              iterator.previous();
+              break;
+            }
+            // Ignore just read instruction.
+          }
+          candidateInfo.referencedFrom.add(method);
+        }
+        continue;
+      }
+
+      if (instruction.isStaticPut()) {
+        // Check the field being written to: no writes to singleton fields are allowed
+        // except for those processed in processInstantiation(...).
+        DexType candidateType = instruction.asStaticPut().getField().type;
+        CandidateInfo candidateInfo = candidates.get(candidateType);
+        if (candidateInfo != null) {
+          candidateInfo.invalidate();
+        }
+        continue;
+      }
+
+      if (instruction.isStaticGet()) {
+        // Check the field being read: make sure all usages are valid.
+        CandidateInfo info = processStaticFieldRead(instruction.asStaticGet());
+        if (info != null) {
+          info.referencedFrom.add(method);
+          // If the candidate still valid, ignore all usages in further analysis.
+          Value value = instruction.outValue();
+          if (value != null) {
+            alreadyProcessed.addAll(value.uniqueUsers());
+          }
+        }
+        continue;
+      }
+
+      if (instruction.isInvokeMethodWithReceiver()) {
+        DexMethod invokedMethod = instruction.asInvokeMethodWithReceiver().getInvokedMethod();
+        CandidateInfo candidateInfo = candidates.get(invokedMethod.holder);
+        if (candidateInfo != null) {
+          // A call to instance method of the candidate class we don't know how to deal with.
+          candidateInfo.invalidate();
+        }
+        continue;
+      }
+
+      if (instruction.isInvokeCustom()) {
+        // Just invalidate any candidates referenced from non-static context.
+        CallSiteReferencesInvalidator invalidator = new CallSiteReferencesInvalidator();
+        invalidator.registerCallSite(instruction.asInvokeCustom().getCallSite());
+        continue;
+      }
+
+      if (instruction.isInstanceGet() || instruction.isInstancePut()) {
+        DexField fieldReferenced = instruction.asFieldInstruction().getField();
+        CandidateInfo candidateInfo = candidates.get(fieldReferenced.clazz);
+        if (candidateInfo != null) {
+          // Reads/writes to instance field of the candidate class are not supported.
+          candidateInfo.invalidate();
+        }
+        continue;
+      }
+    }
+  }
+
+  private boolean isAllowedInHostClassInitializer(
+      DexType host, Instruction insn, IRCode code) {
+    return (insn.isStaticPut() && insn.asStaticPut().getField().clazz == host) ||
+        insn.isConstNumber() ||
+        insn.isConstString() ||
+        (insn.isGoto() && insn.asGoto().isTrivialGotoToTheNextBlock(code)) ||
+        insn.isReturn();
+  }
+
+  private CandidateInfo processInstantiation(
+      DexEncodedMethod method, ListIterator<Instruction> iterator, NewInstance newInstance) {
+
+    DexType candidateType = newInstance.clazz;
+    CandidateInfo candidateInfo = candidates.get(candidateType);
+    if (candidateInfo == null) {
+      return null; // Not interested.
+    }
+
+    if (iterator.previousIndex() != 0) {
+      // Valid new instance must be the first instruction in the class initializer
+      return candidateInfo.invalidate();
+    }
+
+    if (!candidateInfo.isHostClassInitializer(method)) {
+      // A valid candidate must only have one instantiation which is
+      // done in the static initializer of the host class.
+      return candidateInfo.invalidate();
+    }
+
+    if (candidateInfo.instancesCreated.incrementAndGet() > 1) {
+      // Only one instance must be ever created.
+      return candidateInfo.invalidate();
+    }
+
+    Value candidateValue = newInstance.dest();
+    if (candidateValue == null) {
+      // Must be assigned to a singleton field.
+      return candidateInfo.invalidate();
+    }
+
+    if (candidateValue.numberOfPhiUsers() > 0) {
+      return candidateInfo.invalidate();
+    }
+
+    if (candidateValue.numberOfUsers() != 2) {
+      // We expect only two users for each instantiation: constructor call and
+      // static field write. We only check count here, since the exact instructions
+      // will be checked later.
+      return candidateInfo.invalidate();
+    }
+
+    // Check usages. Currently we only support the patterns like:
+    //
+    //     static constructor void <clinit>() {
+    //        new-instance v0, <candidate-type>
+    //  (opt) const/4 v1, #int 0 // (optional)
+    //        invoke-direct {v0, ...}, void <candidate-type>.<init>(...)
+    //        sput-object v0, <instance-field>
+    //        ...
+    //        ...
+    //
+    // In case we guarantee candidate constructor does not access <instance-field>
+    // directly or indirectly we can guarantee that all the potential reads get
+    // same non-null value.
+
+    // Skip potential constant instructions
+    while (iterator.hasNext() && isNonThrowingConstInstruction(iterator.next())) {
+      // Intentionally empty.
+    }
+    iterator.previous();
+
+    if (!iterator.hasNext()) {
+      return candidateInfo.invalidate();
+    }
+    if (!isValidInitCall(candidateInfo, iterator.next(), candidateValue, candidateType)) {
+      iterator.previous();
+      return candidateInfo.invalidate();
+    }
+
+    if (!iterator.hasNext()) {
+      return candidateInfo.invalidate();
+    }
+    if (!isValidStaticPut(candidateInfo, iterator.next())) {
+      iterator.previous();
+      return candidateInfo.invalidate();
+    }
+    if (candidateInfo.fieldWrites.incrementAndGet() > 1) {
+      return candidateInfo.invalidate();
+    }
+
+    return candidateInfo;
+  }
+
+  private boolean isNonThrowingConstInstruction(Instruction instruction) {
+    return instruction.isConstInstruction() && !instruction.instructionTypeCanThrow();
+  }
+
+  private boolean isValidInitCall(
+      CandidateInfo info, Instruction instruction, Value candidateValue, DexType candidateType) {
+    if (!instruction.isInvokeDirect()) {
+      return false;
+    }
+
+    // Check constructor.
+    InvokeDirect invoke = instruction.asInvokeDirect();
+    DexEncodedMethod methodInvoked = appInfo.lookupDirectTarget(invoke.getInvokedMethod());
+    List<Value> values = invoke.inValues();
+
+    if (values.lastIndexOf(candidateValue) != 0 ||
+        methodInvoked == null || methodInvoked.method.holder != candidateType) {
+      return false;
+    }
+
+    // Check arguments.
+    for (int i = 1; i < values.size(); i++) {
+      if (!values.get(i).definition.isConstInstruction()) {
+        return false;
+      }
+    }
+
+    DexEncodedMethod previous = info.constructor.getAndSet(methodInvoked);
+    assert previous == null;
+    return true;
+  }
+
+  private boolean isValidStaticPut(CandidateInfo info, Instruction instruction) {
+    if (!instruction.isStaticPut()) {
+      return false;
+    }
+    // Allow single assignment to a singleton field.
+    StaticPut staticPut = instruction.asStaticPut();
+    DexEncodedField fieldAccessed =
+        appInfo.lookupStaticTarget(staticPut.getField().clazz, staticPut.getField());
+    return fieldAccessed == info.singletonField;
+  }
+
+  // Static field get: can be a valid singleton field for a
+  // candidate in which case we should check if all the usages of the
+  // value read are eligible.
+  private CandidateInfo processStaticFieldRead(StaticGet staticGet) {
+    DexField field = staticGet.getField();
+    DexType candidateType = field.type;
+    CandidateInfo candidateInfo = candidates.get(candidateType);
+    if (candidateInfo == null) {
+      return null;
+    }
+
+    assert candidateInfo.singletonField == appInfo.lookupStaticTarget(field.clazz, field)
+        : "Added reference after collectCandidates(...)?";
+
+    Value singletonValue = staticGet.dest();
+    if (singletonValue != null) {
+      candidateInfo = analyzeAllValueUsers(candidateInfo, singletonValue, false);
+    }
+    return candidateInfo;
+  }
+
+  private CandidateInfo analyzeAllValueUsers(
+      CandidateInfo candidateInfo, Value value, boolean ignoreSuperClassInitInvoke) {
+    assert value != null;
+
+    if (value.numberOfPhiUsers() > 0) {
+      return candidateInfo.invalidate();
+    }
+
+    for (Instruction user : value.uniqueUsers()) {
+      if (user.isInvokeVirtual() || user.isInvokeDirect() /* private methods */) {
+        InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
+        DexMethod methodReferenced = invoke.getInvokedMethod();
+        if (factory.isConstructor(methodReferenced)) {
+          assert user.isInvokeDirect();
+          if (ignoreSuperClassInitInvoke &&
+              invoke.inValues().lastIndexOf(value) == 0 &&
+              methodReferenced == factory.objectMethods.constructor) {
+            // If we are inside candidate constructor and analyzing usages
+            // of the receiver, we want to ignore invocations of superclass
+            // constructor which will be removed after staticizing.
+            continue;
+          }
+          return candidateInfo.invalidate();
+        }
+        DexEncodedMethod methodInvoked = user.isInvokeDirect()
+            ? appInfo.lookupDirectTarget(methodReferenced)
+            : appInfo.lookupVirtualTarget(methodReferenced.holder, methodReferenced);
+        if (invoke.inValues().lastIndexOf(value) == 0 &&
+            methodInvoked != null && methodInvoked.method.holder == candidateInfo.candidate.type) {
+          continue;
+        }
+      }
+
+      // All other users are not allowed.
+      return candidateInfo.invalidate();
+    }
+
+    return candidateInfo;
+  }
+
+  // Perform staticizing candidates:
+  //
+  //  1. After filtering candidates based on usage, finalize the list of candidates by
+  //  filtering out candidates which don't satisfy the requirements:
+  //
+  //    * there must be one instance of the class
+  //    * constructor of the class used to create this instance must be a trivial one
+  //    * class initializer should only be present if candidate itself is own host
+  //    * no abstract or native instance methods
+  //
+  //  2. Rewrite instance methods of classes being staticized into static ones
+  //  3. Rewrite methods referencing staticized members, also remove instance creation
+  //
+  public final void staticizeCandidates(OptimizationFeedback feedback) {
+    phase = Phase.None; // We are done with processing/examining methods.
+    new StaticizingProcessor(this).run(feedback);
+  }
+
+  public final void fixupMethodCode(DexEncodedMethod method, IRCode code) {
+    if (phase == Phase.Fixup) {
+      assert fixupStrategy != null;
+      fixupStrategy.accept(method, code);
+    }
+  }
+
+  void setFixupStrategy(BiConsumer<DexEncodedMethod, IRCode> strategy) {
+    assert phase == Phase.None;
+    assert strategy != null;
+    phase = Phase.Fixup;
+    fixupStrategy = strategy;
+  }
+
+  void cleanFixupStrategy() {
+    assert phase == Phase.Fixup;
+    assert fixupStrategy != null;
+    phase = Phase.None;
+    fixupStrategy = null;
+  }
+
+  private class CallSiteReferencesInvalidator extends UseRegistry {
+    private boolean registerMethod(DexMethod method) {
+      registerTypeReference(method.holder);
+      registerProto(method.proto);
+      return true;
+    }
+
+    private boolean registerField(DexField field) {
+      registerTypeReference(field.clazz);
+      registerTypeReference(field.type);
+      return true;
+    }
+
+    @Override
+    public boolean registerInvokeVirtual(DexMethod method) {
+      return registerMethod(method);
+    }
+
+    @Override
+    public boolean registerInvokeDirect(DexMethod method) {
+      return registerMethod(method);
+    }
+
+    @Override
+    public boolean registerInvokeStatic(DexMethod method) {
+      return registerMethod(method);
+    }
+
+    @Override
+    public boolean registerInvokeInterface(DexMethod method) {
+      return registerMethod(method);
+    }
+
+    @Override
+    public boolean registerInvokeSuper(DexMethod method) {
+      return registerMethod(method);
+    }
+
+    @Override
+    public boolean registerInstanceFieldWrite(DexField field) {
+      return registerField(field);
+    }
+
+    @Override
+    public boolean registerInstanceFieldRead(DexField field) {
+      return registerField(field);
+    }
+
+    @Override
+    public boolean registerNewInstance(DexType type) {
+      return registerTypeReference(type);
+    }
+
+    @Override
+    public boolean registerStaticFieldRead(DexField field) {
+      return registerField(field);
+    }
+
+    @Override
+    public boolean registerStaticFieldWrite(DexField field) {
+      return registerField(field);
+    }
+
+    @Override
+    public boolean registerTypeReference(DexType type) {
+      CandidateInfo candidateInfo = candidates.get(type);
+      if (candidateInfo != null) {
+        candidateInfo.invalidate();
+      }
+      return true;
+    }
+  }
+}
+
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLense.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLense.java
new file mode 100644
index 0000000..4f6c33c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLense.java
@@ -0,0 +1,39 @@
+// Copyright (c) 2018, 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.staticizer;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.graph.GraphLense.NestedGraphLense;
+import com.android.tools.r8.ir.code.Invoke.Type;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableMap;
+
+class ClassStaticizerGraphLense extends NestedGraphLense {
+  ClassStaticizerGraphLense(GraphLense previous, DexItemFactory factory,
+      BiMap<DexField, DexField> fieldMapping, BiMap<DexMethod, DexMethod> methodMapping) {
+    super(ImmutableMap.of(),
+        methodMapping,
+        fieldMapping,
+        fieldMapping.inverse(),
+        methodMapping.inverse(),
+        previous,
+        factory);
+  }
+
+  @Override
+  protected Type mapInvocationType(
+      DexMethod newMethod, DexMethod originalMethod,
+      DexEncodedMethod context, Type type) {
+    if (methodMap.get(originalMethod) == newMethod) {
+      assert type == Type.VIRTUAL || type == Type.DIRECT;
+      return Type.STATIC;
+    }
+    return super.mapInvocationType(newMethod, originalMethod, context, type);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
new file mode 100644
index 0000000..c5993dd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
@@ -0,0 +1,444 @@
+// Copyright (c) 2018, 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.staticizer;
+
+import com.android.tools.r8.graph.DebugLocalInfo;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
+import com.android.tools.r8.graph.DexField;
+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.DexType;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.MemberType;
+import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.ir.code.StaticPut;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.conversion.CallSiteInformation;
+import com.android.tools.r8.ir.conversion.OptimizationFeedback;
+import com.android.tools.r8.ir.optimize.Outliner;
+import com.android.tools.r8.ir.optimize.staticizer.ClassStaticizer.CandidateInfo;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+final class StaticizingProcessor {
+  private final ClassStaticizer classStaticizer;
+
+  private final Set<DexEncodedMethod> referencingExtraMethods = Sets.newIdentityHashSet();
+  private final Map<DexEncodedMethod, CandidateInfo> hostClassInits = new IdentityHashMap<>();
+  private final Set<DexEncodedMethod> methodsToBeStaticized = Sets.newIdentityHashSet();
+  private final Map<DexField, CandidateInfo> singletonFields = new IdentityHashMap<>();
+  private final Map<DexType, DexType> candidateToHostMapping = new IdentityHashMap<>();
+
+  StaticizingProcessor(ClassStaticizer classStaticizer) {
+    this.classStaticizer = classStaticizer;
+  }
+
+  final void run(OptimizationFeedback feedback) {
+    // Filter out candidates based on the information we collected
+    // while examining methods.
+    finalEligibilityCheck();
+
+    // Prepare interim data.
+    prepareCandidates();
+
+    // Process all host class initializers (only remove instantiations).
+    processMethods(hostClassInits.keySet().stream(), this::removeCandidateInstantiation, feedback);
+
+    // Process instance methods to be staticized (only remove references to 'this').
+    processMethods(methodsToBeStaticized.stream(), this::removeReferencesToThis, feedback);
+
+    // Convert instance methods into static methods with an extra parameter.
+    Set<DexEncodedMethod> staticizedMethods = staticizeMethodSymbols();
+
+    // Process all other methods that may reference singleton fields
+    // and call methods on them. (Note that we exclude the former instance methods,
+    // but include new static methods created as a result of staticizing.
+    Stream<DexEncodedMethod> methods = Streams.concat(
+        referencingExtraMethods.stream(),
+        staticizedMethods.stream(),
+        hostClassInits.keySet().stream());
+    processMethods(methods, this::rewriteReferences, feedback);
+  }
+
+  private void finalEligibilityCheck() {
+    Iterator<Entry<DexType, CandidateInfo>> it =
+        classStaticizer.candidates.entrySet().iterator();
+    while (it.hasNext()) {
+      Entry<DexType, CandidateInfo> entry = it.next();
+      DexType candidateType = entry.getKey();
+      CandidateInfo info = entry.getValue();
+      DexProgramClass candidateClass = info.candidate;
+      DexType candidateHostType = info.hostType();
+      DexEncodedMethod constructorUsed = info.constructor.get();
+
+      int instancesCreated = info.instancesCreated.get();
+      assert instancesCreated == info.fieldWrites.get();
+      assert instancesCreated <= 1;
+      assert (instancesCreated == 0) == (constructorUsed == null);
+
+      // CHECK: One instance, one singleton field, known constructor
+      if (instancesCreated == 0) {
+        // Give up on the candidate, if there are any reads from instance
+        // field the user should read null.
+        it.remove();
+        continue;
+      }
+
+      // CHECK: instance initializer used to create an instance is trivial.
+      // NOTE: Along with requirement that candidate does not have instance
+      // fields this should guarantee that the constructor is empty.
+      assert candidateClass.instanceFields().length == 0;
+      assert constructorUsed.isProcessed();
+      TrivialInitializer trivialInitializer =
+          constructorUsed.getOptimizationInfo().getTrivialInitializerInfo();
+      if (trivialInitializer == null) {
+        it.remove();
+        continue;
+      }
+
+      // CHECK: class initializer should only be present if candidate itself is its own host.
+      DexEncodedMethod classInitializer = candidateClass.getClassInitializer();
+      assert classInitializer != null || candidateType != candidateHostType;
+      if (classInitializer != null && candidateType != candidateHostType) {
+        it.remove();
+        continue;
+      }
+
+      // CHECK: no abstract or native instance methods.
+      if (Streams.stream(candidateClass.methods()).anyMatch(
+          method -> !method.isStaticMethod() &&
+              (method.accessFlags.isAbstract() || method.accessFlags.isNative()))) {
+        it.remove();
+        continue;
+      }
+    }
+  }
+
+  private void prepareCandidates() {
+    Set<DexEncodedMethod> removedInstanceMethods = Sets.newIdentityHashSet();
+
+    for (CandidateInfo candidate : classStaticizer.candidates.values()) {
+      DexProgramClass candidateClass = candidate.candidate;
+      // Host class initializer
+      DexClass hostClass = candidate.hostClass();
+      DexEncodedMethod hostClassInitializer = hostClass.getClassInitializer();
+      assert hostClassInitializer != null;
+      CandidateInfo previous = hostClassInits.put(hostClassInitializer, candidate);
+      assert previous == null;
+
+      // Collect instance methods to be staticized.
+      for (DexEncodedMethod method : candidateClass.methods()) {
+        if (!method.isStaticMethod()) {
+          removedInstanceMethods.add(method);
+          if (!factory().isConstructor(method.method)) {
+            methodsToBeStaticized.add(method);
+          }
+        }
+      }
+      singletonFields.put(candidate.singletonField.field, candidate);
+      referencingExtraMethods.addAll(candidate.referencedFrom);
+    }
+
+    referencingExtraMethods.removeAll(removedInstanceMethods);
+  }
+
+  private void processMethods(Stream<DexEncodedMethod> methods,
+      BiConsumer<DexEncodedMethod, IRCode> strategy, OptimizationFeedback feedback) {
+    classStaticizer.setFixupStrategy(strategy);
+    methods.sorted(DexEncodedMethod::slowCompare).forEach(
+        method -> classStaticizer.converter.processMethod(method, feedback,
+            x -> false, CallSiteInformation.empty(), Outliner::noProcessing));
+    classStaticizer.cleanFixupStrategy();
+  }
+
+  private void removeCandidateInstantiation(DexEncodedMethod method, IRCode code) {
+    CandidateInfo candidateInfo = hostClassInits.get(method);
+    assert candidateInfo != null;
+
+    // Find and remove instantiation and its users.
+    InstructionIterator iterator = code.instructionIterator();
+    while (iterator.hasNext()) {
+      Instruction instruction = iterator.next();
+      if (instruction.isNewInstance() &&
+          instruction.asNewInstance().clazz == candidateInfo.candidate.type) {
+        // Remove all usages
+        // NOTE: requiring (a) the instance initializer to be trivial, (b) not allowing
+        //       candidates with instance fields and (c) requiring candidate to directly
+        //       extend java.lang.Object guarantees that the constructor is actually
+        //       empty and does not need to be inlined.
+        assert candidateInfo.candidate.superType == factory().objectType;
+        assert candidateInfo.candidate.instanceFields().length == 0;
+
+        Value singletonValue = instruction.outValue();
+        assert singletonValue != null;
+        singletonValue.uniqueUsers().forEach(Instruction::removeOrReplaceByDebugLocalRead);
+        instruction.removeOrReplaceByDebugLocalRead();
+        return;
+      }
+    }
+
+    assert false : "Must always be able to find and remove the instantiation";
+  }
+
+  private void removeReferencesToThis(DexEncodedMethod method, IRCode code) {
+    fixupStaticizedValueUsers(code, code.getThis());
+  }
+
+  private void rewriteReferences(DexEncodedMethod method, IRCode code) {
+    // Process all singleton field reads and rewrite their users.
+    List<StaticGet> singletonFieldReads =
+        Streams.stream(code.instructionIterator())
+            .filter(Instruction::isStaticGet)
+            .map(Instruction::asStaticGet)
+            .filter(get -> singletonFields.containsKey(get.getField()))
+            .collect(Collectors.toList());
+
+    singletonFieldReads.forEach(read -> {
+      CandidateInfo candidateInfo = singletonFields.get(read.getField());
+      assert candidateInfo != null;
+      Value value = read.dest();
+      if (value != null) {
+        fixupStaticizedValueUsers(code, value);
+      }
+      if (!candidateInfo.preserveRead.get()) {
+        read.removeOrReplaceByDebugLocalRead();
+      }
+    });
+
+    if (!candidateToHostMapping.isEmpty()) {
+      remapMovedCandidates(code);
+    }
+  }
+
+  // Fixup value usages: rewrites all method calls so that they point to static methods.
+  private void fixupStaticizedValueUsers(IRCode code, Value thisValue) {
+    assert thisValue != null;
+    assert thisValue.numberOfPhiUsers() == 0;
+
+    for (Instruction user : thisValue.uniqueUsers()) {
+      assert user.isInvokeVirtual() || user.isInvokeDirect();
+      InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
+      Value newValue = null;
+      Value outValue = invoke.outValue();
+      if (outValue != null) {
+        newValue = code.createValue(outValue.outType());
+        DebugLocalInfo localInfo = outValue.getLocalInfo();
+        if (localInfo != null) {
+          newValue.setLocalInfo(localInfo);
+        }
+      }
+      List<Value> args = invoke.inValues();
+      invoke.replace(new InvokeStatic(
+          invoke.getInvokedMethod(), newValue, args.subList(1, args.size())));
+    }
+
+    assert thisValue.numberOfUsers() == 0;
+  }
+
+  private void remapMovedCandidates(IRCode code) {
+    InstructionIterator it = code.instructionIterator();
+    while (it.hasNext()) {
+      Instruction instruction = it.next();
+
+      if (instruction.isStaticGet()) {
+        StaticGet staticGet = instruction.asStaticGet();
+        DexField field = mapFieldIfMoved(staticGet.getField());
+        if (field != staticGet.getField()) {
+          Value outValue = staticGet.dest();
+          assert outValue != null;
+          it.replaceCurrentInstruction(
+              new StaticGet(
+                  MemberType.fromDexType(field.type),
+                  code.createValue(ValueType.fromDexType(field.type), outValue.getLocalInfo()),
+                  field
+              )
+          );
+        }
+        continue;
+      }
+
+      if (instruction.isStaticPut()) {
+        StaticPut staticPut = instruction.asStaticPut();
+        DexField field = mapFieldIfMoved(staticPut.getField());
+        if (field != staticPut.getField()) {
+          it.replaceCurrentInstruction(
+              new StaticPut(MemberType.fromDexType(field.type), staticPut.inValue(), field)
+          );
+        }
+        continue;
+      }
+
+      if (instruction.isInvokeStatic()) {
+        InvokeStatic invoke = instruction.asInvokeStatic();
+        DexMethod method = invoke.getInvokedMethod();
+        DexType hostType = candidateToHostMapping.get(method.holder);
+        if (hostType != null) {
+          DexMethod newMethod = factory().createMethod(hostType, method.proto, method.name);
+          Value outValue = invoke.outValue();
+          Value newOutValue = method.proto.returnType.isVoidType() ? null
+              : code.createValue(
+                  ValueType.fromDexType(method.proto.returnType),
+                  outValue == null ? null : outValue.getLocalInfo());
+          it.replaceCurrentInstruction(
+              new InvokeStatic(newMethod, newOutValue, invoke.inValues()));
+        }
+        continue;
+      }
+    }
+  }
+
+  private DexField mapFieldIfMoved(DexField field) {
+    DexType hostType = candidateToHostMapping.get(field.clazz);
+    if (hostType != null) {
+      field = factory().createField(hostType, field.type, field.name);
+    }
+    hostType = candidateToHostMapping.get(field.type);
+    if (hostType != null) {
+      field = factory().createField(field.clazz, hostType, field.name);
+    }
+    return field;
+  }
+
+  private Set<DexEncodedMethod> staticizeMethodSymbols() {
+    BiMap<DexMethod, DexMethod> methodMapping = HashBiMap.create();
+    BiMap<DexField, DexField> fieldMapping = HashBiMap.create();
+
+    Set<DexEncodedMethod> staticizedMethods = Sets.newIdentityHashSet();
+    for (CandidateInfo candidate : classStaticizer.candidates.values()) {
+      DexProgramClass candidateClass = candidate.candidate;
+
+      // Move instance methods into static ones.
+      List<DexEncodedMethod> newDirectMethods = new ArrayList<>();
+      for (DexEncodedMethod method : candidateClass.methods()) {
+        if (method.isStaticMethod()) {
+          newDirectMethods.add(method);
+        } else if (!factory().isConstructor(method.method)) {
+          DexEncodedMethod staticizedMethod = method.toStaticMethodWithoutThis();
+          newDirectMethods.add(staticizedMethod);
+          staticizedMethods.add(staticizedMethod);
+          methodMapping.put(method.method, staticizedMethod.method);
+        }
+      }
+      candidateClass.setVirtualMethods(DexEncodedMethod.EMPTY_ARRAY);
+      candidateClass.setDirectMethods(
+          newDirectMethods.toArray(new DexEncodedMethod[newDirectMethods.size()]));
+
+      // Consider moving static members from candidate into host.
+      DexType hostType = candidate.hostType();
+      if (candidateClass.type != hostType) {
+        DexClass hostClass = classStaticizer.appInfo.definitionFor(hostType);
+        assert hostClass != null;
+        if (!classMembersConflict(candidateClass, hostClass)) {
+          // Move all members of the candidate class into host class.
+          moveMembersIntoHost(staticizedMethods,
+              candidateClass, hostType, hostClass, methodMapping, fieldMapping);
+        }
+      }
+    }
+
+    if (!methodMapping.isEmpty() || fieldMapping.isEmpty()) {
+      classStaticizer.converter.setGraphLense(
+          new ClassStaticizerGraphLense(
+              classStaticizer.converter.getGraphLense(),
+              classStaticizer.factory,
+              fieldMapping,
+              methodMapping));
+    }
+    return staticizedMethods;
+  }
+
+  private boolean classMembersConflict(DexClass a, DexClass b) {
+    assert Streams.stream(a.methods()).allMatch(DexEncodedMethod::isStaticMethod);
+    assert a.instanceFields().length == 0;
+    return Stream.of(a.staticFields()).anyMatch(fld -> b.lookupField(fld.field) != null) ||
+        Streams.stream(a.methods()).anyMatch(method -> b.lookupMethod(method.method) != null);
+  }
+
+  private void moveMembersIntoHost(Set<DexEncodedMethod> staticizedMethods,
+      DexProgramClass candidateClass,
+      DexType hostType, DexClass hostClass,
+      BiMap<DexMethod, DexMethod> methodMapping,
+      BiMap<DexField, DexField> fieldMapping) {
+    candidateToHostMapping.put(candidateClass.type, hostType);
+
+    // Process static fields.
+    // Append fields first.
+    if (candidateClass.staticFields().length > 0) {
+      DexEncodedField[] oldFields = hostClass.staticFields();
+      DexEncodedField[] extraFields = candidateClass.staticFields();
+      DexEncodedField[] newFields = new DexEncodedField[oldFields.length + extraFields.length];
+      System.arraycopy(oldFields, 0, newFields, 0, oldFields.length);
+      System.arraycopy(extraFields, 0, newFields, oldFields.length, extraFields.length);
+      hostClass.setStaticFields(newFields);
+    }
+
+    // Fixup field types.
+    DexEncodedField[] staticFields = hostClass.staticFields();
+    for (int i = 0; i < staticFields.length; i++) {
+      DexEncodedField field = staticFields[i];
+      DexField newField = mapCandidateField(field.field, candidateClass.type, hostType);
+      if (newField != field.field) {
+        staticFields[i] = field.toTypeSubstitutedField(newField);
+        fieldMapping.put(field.field, newField);
+      }
+    }
+
+    // Process static methods.
+    if (candidateClass.directMethods().length > 0) {
+      DexEncodedMethod[] oldMethods = hostClass.directMethods();
+      DexEncodedMethod[] extraMethods = candidateClass.directMethods();
+      DexEncodedMethod[] newMethods = new DexEncodedMethod[oldMethods.length + extraMethods.length];
+      System.arraycopy(oldMethods, 0, newMethods, 0, oldMethods.length);
+      for (int i = 0; i < extraMethods.length; i++) {
+        DexEncodedMethod method = extraMethods[i];
+        DexEncodedMethod newMethod = method.toTypeSubstitutedMethod(
+            factory().createMethod(hostType, method.method.proto, method.method.name));
+        newMethods[oldMethods.length + i] = newMethod;
+        staticizedMethods.add(newMethod);
+        staticizedMethods.remove(method);
+        DexMethod originalMethod = methodMapping.inverse().get(method.method);
+        if (originalMethod == null) {
+          methodMapping.put(method.method, newMethod.method);
+        } else {
+          methodMapping.put(originalMethod, newMethod.method);
+        }
+      }
+      hostClass.setDirectMethods(newMethods);
+    }
+  }
+
+  private DexField mapCandidateField(DexField field, DexType candidateType, DexType hostType) {
+    return field.clazz != candidateType && field.type != candidateType ? field
+        : factory().createField(
+            field.clazz == candidateType ? hostType : field.clazz,
+            field.type == candidateType ? hostType : field.type,
+            field.name);
+  }
+
+  private DexItemFactory factory() {
+    return classStaticizer.factory;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java b/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
index 9c61a7a..f45a3de 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNamingForNameMapper.java
@@ -69,17 +69,17 @@
     /** The parameters are forwarded to MappedRange constructor, see explanation there. */
     @Override
     public void addMappedRange(
-        Range obfuscatedRange,
+        Range minifiedRange,
         MemberNaming.MethodSignature originalSignature,
         Object originalRange,
-        String obfuscatedName) {
+        String renamedName) {
       mappedRangesByName
-          .computeIfAbsent(obfuscatedName, k -> new ArrayList<>())
-          .add(new MappedRange(obfuscatedRange, originalSignature, originalRange, obfuscatedName));
+          .computeIfAbsent(renamedName, k -> new ArrayList<>())
+          .add(new MappedRange(minifiedRange, originalSignature, originalRange, renamedName));
     }
   }
 
-  /** List of MappedRanges that belong to the same obfuscated name. */
+  /** List of MappedRanges that belong to the same renamed name. */
   public static class MappedRangesOfName {
     private final List<MappedRange> mappedRanges;
 
@@ -94,14 +94,14 @@
     public MappedRange firstRangeForLine(int line) {
       MappedRange bestRange = null;
       for (MappedRange range : mappedRanges) {
-        if (range.obfuscatedRange == null) {
+        if (range.minifiedRange == null) {
           if (bestRange == null) {
             // This is an "a() -> b" mapping (no concrete line numbers), remember this if there'll
             // be no better one.
             bestRange = range;
           }
-        } else if (range.obfuscatedRange.contains(line)) {
-          // Concrete obfuscated range found ("x:y:a()[:u[:v]] -> b")
+        } else if (range.minifiedRange.contains(line)) {
+          // Concrete minified range found ("x:y:a()[:u[:v]] -> b")
           return range;
         }
       }
@@ -109,25 +109,25 @@
     }
 
     /**
-     * Search for a MappedRange where the obfuscated range contains the specified {@code line} and
-     * return that and the subsequent MappedRanges with the same obfuscated range. Return general
+     * Search for a MappedRange where the minified range contains the specified {@code line} and
+     * return that and the subsequent MappedRanges with the same minified range. Return general
      * MappedRange ("a() -> b") if no concrete mapping found or empty list if nothing found.
      */
     public List<MappedRange> allRangesForLine(int line) {
       MappedRange noLineRange = null;
       for (int i = 0; i < mappedRanges.size(); ++i) {
         MappedRange rangeI = mappedRanges.get(i);
-        if (rangeI.obfuscatedRange == null) {
+        if (rangeI.minifiedRange == null) {
           if (noLineRange == null) {
             // This is an "a() -> b" mapping (no concrete line numbers), remember this if there'll
             // be no better one.
             noLineRange = rangeI;
           }
-        } else if (rangeI.obfuscatedRange.contains(line)) {
-          // Concrete obfuscated range found ("x:y:a()[:u[:v]] -> b")
+        } else if (rangeI.minifiedRange.contains(line)) {
+          // Concrete minified range found ("x:y:a()[:u[:v]] -> b")
           int j = i + 1;
           for (; j < mappedRanges.size(); ++j) {
-            if (!Objects.equals(mappedRanges.get(j).obfuscatedRange, rangeI.obfuscatedRange)) {
+            if (!Objects.equals(mappedRanges.get(j).minifiedRange, rangeI.minifiedRange)) {
               break;
             }
           }
@@ -166,10 +166,10 @@
 
   /**
    * Mapping from the renamed signature to the naming information for a member.
-   * <p>
-   * A renamed signature is a signature where the member's name has been obfuscated but not the type
+   *
+   * <p>A renamed signature is a signature where the member's name has been renamed but not the type
    * information.
-   **/
+   */
   private final ImmutableMap<MethodSignature, MemberNaming> methodMembers;
   private final ImmutableMap<FieldSignature, MemberNaming> fieldMembers;
 
@@ -325,22 +325,22 @@
   }
 
   /**
-   * MappedRange describes an (original line numbers, signature) <-> (obfuscated line numbers)
+   * MappedRange describes an (original line numbers, signature) <-> (minified line numbers)
    * mapping. It can describe 3 different things:
    *
    * <p>1. The method is renamed. The original source lines are preserved. The corresponding
    * Proguard-map syntax is "a(...) -> b"
    *
-   * <p>2. The source lines of a method in the original range are renumbered to the obfuscated
-   * range. In this case the {@link MappedRange#originalRange} is either a {@code Range} or null,
+   * <p>2. The source lines of a method in the original range are renumbered to the minified range.
+   * In this case the {@link MappedRange#originalRange} is either a {@code Range} or null,
    * indicating that the original range is unknown or is the same as the {@link
-   * MappedRange#obfuscatedRange}. The corresponding Proguard-map syntax is "x:y:a(...) -> b" or
+   * MappedRange#minifiedRange}. The corresponding Proguard-map syntax is "x:y:a(...) -> b" or
    * "x:y:a(...):u:v -> b"
    *
    * <p>3. The source line of a method is the inlining caller of the previous {@code MappedRange}.
    * In this case the {@link MappedRange@originalRange} is either an {@code int} or null, indicating
-   * that the original source line is unknown, or may be identical to a line of the obfuscated
-   * range. The corresponding Proguard-map syntax is "x:y:a(...) -> b" or "x:y:a(...):u -> b"
+   * that the original source line is unknown, or may be identical to a line of the minified range.
+   * The corresponding Proguard-map syntax is "x:y:a(...) -> b" or "x:y:a(...):u -> b"
    */
   public static class MappedRange {
 
@@ -350,10 +350,10 @@
       return nextSequenceNumber++;
     }
 
-    public final Range obfuscatedRange; // Can be null, if so then originalRange must also be null.
+    public final Range minifiedRange; // Can be null, if so then originalRange must also be null.
     public final MethodSignature signature;
     public final Object originalRange; // null, Integer or Range.
-    public final String obfuscatedName;
+    public final String renamedName;
 
     /**
      * The sole purpose of {@link #sequenceNumber} is to preserve the order of members read from a
@@ -362,52 +362,49 @@
     private final int sequenceNumber = getNextSequenceNumber();
 
     private MappedRange(
-        Range obfuscatedRange,
-        MethodSignature signature,
-        Object originalRange,
-        String obfuscatedName) {
+        Range minifiedRange, MethodSignature signature, Object originalRange, String renamedName) {
 
-      assert obfuscatedRange != null || originalRange == null;
+      assert minifiedRange != null || originalRange == null;
       assert originalRange == null
           || originalRange instanceof Integer
           || originalRange instanceof Range;
 
-      this.obfuscatedRange = obfuscatedRange;
+      this.minifiedRange = minifiedRange;
       this.signature = signature;
       this.originalRange = originalRange;
-      this.obfuscatedName = obfuscatedName;
+      this.renamedName = renamedName;
     }
 
-    public int originalLineFromObfuscated(int obfuscatedLineNumber) {
-      if (obfuscatedRange == null) {
+    public int getOriginalLineNumber(int lineNumberAfterMinification) {
+      if (minifiedRange == null) {
         // General mapping without concrete line numbers: "a() -> b"
-        return obfuscatedLineNumber;
+        return lineNumberAfterMinification;
       }
-      assert obfuscatedRange.contains(obfuscatedLineNumber);
+      assert minifiedRange.contains(lineNumberAfterMinification);
       if (originalRange == null) {
         // Concrete identity mapping: "x:y:a() -> b"
-        return obfuscatedLineNumber;
+        return lineNumberAfterMinification;
       } else if (originalRange instanceof Integer) {
         // Inlinee: "x:y:a():u -> b"
         return (int) originalRange;
       } else {
         // "x:y:a():u:v -> b"
         assert originalRange instanceof Range;
-        return ((Range) originalRange).from + obfuscatedLineNumber - obfuscatedRange.from;
+        return ((Range) originalRange).from + lineNumberAfterMinification - minifiedRange.from;
       }
     }
 
     @Override
     public String toString() {
       StringBuilder builder = new StringBuilder();
-      if (obfuscatedRange != null) {
-        builder.append(obfuscatedRange).append(':');
+      if (minifiedRange != null) {
+        builder.append(minifiedRange).append(':');
       }
       builder.append(signature);
       if (originalRange != null) {
         builder.append(":").append(originalRange);
       }
-      builder.append(" -> ").append(obfuscatedName);
+      builder.append(" -> ").append(renamedName);
       return builder.toString();
     }
 
@@ -424,19 +421,19 @@
 
       MappedRange that = (MappedRange) o;
 
-      return Objects.equals(obfuscatedRange, that.obfuscatedRange)
+      return Objects.equals(minifiedRange, that.minifiedRange)
           && Objects.equals(originalRange, that.originalRange)
           && signature.equals(that.signature)
-          && obfuscatedName.equals(that.obfuscatedName);
+          && renamedName.equals(that.renamedName);
     }
 
     @Override
     public int hashCode() {
       // sequenceNumber is intentionally omitted from hashCode since it's not used in equality test.
-      int result = Objects.hashCode(obfuscatedRange);
+      int result = Objects.hashCode(minifiedRange);
       result = 31 * result + Objects.hashCode(originalRange);
       result = 31 * result + signature.hashCode();
-      result = 31 * result + obfuscatedName.hashCode();
+      result = 31 * result + renamedName.hashCode();
       return result;
     }
   }
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java b/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
index d92de6b..8714b28 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierMinifier.java
@@ -26,15 +26,15 @@
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ProguardClassFilter;
+import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
 import java.util.Map;
-import java.util.Set;
 
 class IdentifierMinifier {
 
   private final AppInfoWithLiveness appInfo;
   private final ProguardClassFilter adaptClassStrings;
   private final NamingLens lens;
-  private final Set<DexItem> identifierNameStrings;
+  private final Object2BooleanMap<DexItem> identifierNameStrings;
 
   IdentifierMinifier(
       AppInfoWithLiveness appInfo, ProguardClassFilter adaptClassStrings, NamingLens lens) {
diff --git a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java
index b0f4dc0..830391a 100644
--- a/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java
+++ b/src/main/java/com/android/tools/r8/naming/IdentifierNameStringMarker.java
@@ -35,17 +35,17 @@
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.Streams;
+import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
 import java.util.Arrays;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Objects;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 public class IdentifierNameStringMarker {
   private final AppInfo appInfo;
   private final DexItemFactory dexItemFactory;
-  private final Set<DexItem> identifierNameStrings;
+  private final Object2BooleanMap<DexItem> identifierNameStrings;
   private final InternalOptions options;
 
   public IdentifierNameStringMarker(AppInfoWithLiveness appInfo, InternalOptions options) {
@@ -63,7 +63,7 @@
   }
 
   private void decoupleIdentifierNameStringInField(DexEncodedField encodedField) {
-    if (!identifierNameStrings.contains(encodedField.field)) {
+    if (!identifierNameStrings.containsKey(encodedField.field)) {
       return;
     }
     if (!encodedField.accessFlags.isStatic()) {
@@ -102,22 +102,27 @@
         if (instruction.isStaticPut() || instruction.isInstancePut()) {
           FieldInstruction fieldPut = instruction.asFieldInstruction();
           DexField field = fieldPut.getField();
-          if (!identifierNameStrings.contains(field)) {
+          if (!identifierNameStrings.containsKey(field)) {
             continue;
           }
+          boolean isExplicitRule = identifierNameStrings.getBoolean(field);
           Value in = instruction.isStaticPut()
               ? instruction.asStaticPut().inValue()
               : instruction.asInstancePut().value();
           if (!in.isConstString()) {
-            warnUndeterminedIdentifierIfNecessary(
-                appInfo, options, field, originHolder, instruction, null);
+            if (isExplicitRule) {
+              warnUndeterminedIdentifierIfNecessary(
+                  appInfo, options, field, originHolder, instruction, null);
+            }
             continue;
           }
           DexString original = in.getConstInstruction().asConstString().getValue();
           DexItemBasedString itemBasedString = inferMemberOrTypeFromNameString(appInfo, original);
           if (itemBasedString == null) {
-            warnUndeterminedIdentifierIfNecessary(
-                appInfo, options, field, originHolder, instruction, original);
+            if (isExplicitRule) {
+              warnUndeterminedIdentifierIfNecessary(
+                  appInfo, options, field, originHolder, instruction, original);
+            }
             continue;
           }
           // Move the cursor back to $fieldPut
@@ -163,16 +168,19 @@
         } else if (instruction.isInvokeMethod()) {
           InvokeMethod invoke = instruction.asInvokeMethod();
           DexMethod invokedMethod = invoke.getInvokedMethod();
-          if (!identifierNameStrings.contains(invokedMethod)) {
+          if (!identifierNameStrings.containsKey(invokedMethod)) {
             continue;
           }
+          boolean isExplicitRule = identifierNameStrings.getBoolean(invokedMethod);
           List<Value> ins = invoke.arguments();
           Value[] changes = new Value [ins.size()];
           if (isReflectionMethod(dexItemFactory, invokedMethod)) {
             DexItemBasedString itemBasedString = identifyIdentiferNameString(appInfo, invoke);
             if (itemBasedString == null) {
-              warnUndeterminedIdentifierIfNecessary(
-                  appInfo, options, invokedMethod, originHolder, instruction, null);
+              if (isExplicitRule) {
+                warnUndeterminedIdentifierIfNecessary(
+                    appInfo, options, invokedMethod, originHolder, instruction, null);
+              }
               continue;
             }
             DexType returnType = invoke.getReturnType();
@@ -217,16 +225,20 @@
             for (int i = 0; i < ins.size(); i++) {
               Value in = ins.get(i);
               if (!in.isConstString()) {
-                warnUndeterminedIdentifierIfNecessary(
-                    appInfo, options, invokedMethod, originHolder, instruction, null);
+                if (isExplicitRule) {
+                  warnUndeterminedIdentifierIfNecessary(
+                      appInfo, options, invokedMethod, originHolder, instruction, null);
+                }
                 continue;
               }
               DexString original = in.getConstInstruction().asConstString().getValue();
               DexItemBasedString itemBasedString =
                   inferMemberOrTypeFromNameString(appInfo, original);
               if (itemBasedString == null) {
-                warnUndeterminedIdentifierIfNecessary(
-                    appInfo, options, invokedMethod, originHolder, instruction, original);
+                if (isExplicitRule) {
+                  warnUndeterminedIdentifierIfNecessary(
+                      appInfo, options, invokedMethod, originHolder, instruction, original);
+                }
                 continue;
               }
               // Move the cursor back to $invoke
diff --git a/src/main/java/com/android/tools/r8/position/TextPosition.java b/src/main/java/com/android/tools/r8/position/TextPosition.java
index 0dd4090..08d13bc 100644
--- a/src/main/java/com/android/tools/r8/position/TextPosition.java
+++ b/src/main/java/com/android/tools/r8/position/TextPosition.java
@@ -54,12 +54,12 @@
 
   @Override
   public String toString() {
-    return "Offset: " + offset + ", Line: " + line + ", column: " + column;
+    return "offset: " + offset + ", line: " + line + ", column: " + column;
   }
 
   @Override
   public String getDescription() {
-    return "Line: " + line + (column != UNKNOWN_COLUMN ? ", column: " + column: "");
+    return "line " + line + (column != UNKNOWN_COLUMN ? (", column " + column) : "");
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 19a116f..64387ce 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -5,8 +5,6 @@
 
 import static com.android.tools.r8.naming.IdentifierNameStringUtils.identifyIdentiferNameString;
 import static com.android.tools.r8.naming.IdentifierNameStringUtils.isReflectionMethod;
-import static com.android.tools.r8.naming.IdentifierNameStringUtils.warnUndeterminedIdentifierIfNecessary;
-import static com.android.tools.r8.shaking.ProguardConfigurationUtils.buildIdentifierNameStringRule;
 
 import com.android.tools.r8.Diagnostic;
 import com.android.tools.r8.dex.IndexedItemCollection;
@@ -58,6 +56,8 @@
 import com.google.common.collect.Sets;
 import com.google.common.collect.Sets.SetView;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap;
+import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import java.util.ArrayDeque;
 import java.util.Collection;
@@ -187,10 +187,9 @@
   private final Queue<Action> proguardCompatibilityWorkList = Queues.newArrayDeque();
 
   /**
-   * A set of methods that need code inspection for Proguard compatibility rules.
+   * A set of methods that need code inspection for Java reflection in use.
    */
-  private final Set<DexEncodedMethod> pendingProguardReflectiveCompatibility =
-      Sets.newLinkedHashSet();
+  private final Set<DexEncodedMethod> pendingReflectiveUses = Sets.newLinkedHashSet();
 
   /**
    * A cache for DexMethod that have been marked reachable.
@@ -314,13 +313,10 @@
 
     boolean registerInvokeVirtual(DexMethod method, KeepReason keepReason) {
       if (appInfo.dexItemFactory.classMethods.isReflectiveMemberLookup(method)) {
-        if (forceProguardCompatibility) {
-          // TODO(b/76181966): whether or not add this rule in normal mode.
-          if (identifierNameStrings.add(method) && compatibility != null) {
-            compatibility.addRule(buildIdentifierNameStringRule(method));
-          }
-          pendingProguardReflectiveCompatibility.add(currentMethod);
-        }
+        // Implicitly add -identifiernamestring rule for the Java reflection in use.
+        identifierNameStrings.add(method);
+        // Revisit the current method to implicitly add -keep rule for items with reflective access.
+        pendingReflectiveUses.add(currentMethod);
       }
       if (!registerItemWithTarget(virtualInvokes, method)) {
         return false;
@@ -356,13 +352,10 @@
     boolean registerInvokeStatic(DexMethod method, KeepReason keepReason) {
       if (method == appInfo.dexItemFactory.classMethods.forName
           || appInfo.dexItemFactory.atomicFieldUpdaterMethods.isFieldUpdater(method)) {
-        if (forceProguardCompatibility) {
-          // TODO(b/76181966): whether or not add this rule in normal mode.
-          if (identifierNameStrings.add(method) && compatibility != null) {
-            compatibility.addRule(buildIdentifierNameStringRule(method));
-          }
-          pendingProguardReflectiveCompatibility.add(currentMethod);
-        }
+        // Implicitly add -identifiernamestring rule for the Java reflection in use.
+        identifierNameStrings.add(method);
+        // Revisit the current method to implicitly add -keep rule for items with reflective access.
+        pendingReflectiveUses.add(currentMethod);
       }
       if (!registerItemWithTarget(staticInvokes, method)) {
         return false;
@@ -1279,15 +1272,15 @@
         }
 
         // Continue fix-point processing while there are additional work items to ensure
-        // Proguard compatibility.
+        // items that are passed to Java reflections are traced.
         if (proguardCompatibilityWorkList.isEmpty()
-            && pendingProguardReflectiveCompatibility.isEmpty()) {
+            && pendingReflectiveUses.isEmpty()) {
           break;
         }
-        pendingProguardReflectiveCompatibility.forEach(this::handleProguardReflectiveBehavior);
+        pendingReflectiveUses.forEach(this::handleReflectiveBehavior);
         workList.addAll(proguardCompatibilityWorkList);
         proguardCompatibilityWorkList.clear();
-        pendingProguardReflectiveCompatibility.clear();
+        pendingReflectiveUses.clear();
       }
       if (Log.ENABLED) {
         Set<DexEncodedMethod> allLive = Sets.newIdentityHashSet();
@@ -1484,15 +1477,15 @@
         Action.markMethodLive(method, KeepReason.dueToProguardCompatibilityKeepRule(rule)));
   }
 
-  private void handleProguardReflectiveBehavior(DexEncodedMethod method) {
+  private void handleReflectiveBehavior(DexEncodedMethod method) {
     DexType originHolder = method.method.holder;
     Origin origin = appInfo.originFor(originHolder);
     IRCode code = method.buildIR(appInfo, graphLense, options, origin);
     code.instructionIterator().forEachRemaining(instr ->
-        handleProguardReflectiveBehavior(instr, originHolder));
+        handleReflectiveBehavior(instr, originHolder));
   }
 
-  private void handleProguardReflectiveBehavior(Instruction instruction, DexType originHolder) {
+  private void handleReflectiveBehavior(Instruction instruction, DexType originHolder) {
     if (!instruction.isInvokeMethod()) {
       return;
     }
@@ -1503,8 +1496,6 @@
     }
     DexItemBasedString itemBasedString = identifyIdentiferNameString(appInfo, invoke);
     if (itemBasedString == null) {
-      warnUndeterminedIdentifierIfNecessary(
-          appInfo, options, invokedMethod, originHolder, instruction, null);
       return;
     }
     if (itemBasedString.basedOn instanceof DexType) {
@@ -1692,8 +1683,10 @@
     public final Set<DexMethod> neverInline;
     /**
      * All items with -identifiernamestring rule.
+     * Bound boolean value indicates the rule is explicitly specified by users (<code>true</code>)
+     * or not, i.e., implicitly added by R8 (<code>false</code>).
      */
-    public final Set<DexItem> identifierNameStrings;
+    public final Object2BooleanMap<DexItem> identifierNameStrings;
     /**
      * Set of fields that have been identified as proto-lite fields by the
      * {@link ProtoLiteExtension}.
@@ -1750,8 +1743,8 @@
       this.alwaysInline = enqueuer.rootSet.alwaysInline;
       this.forceInline = enqueuer.rootSet.forceInline;
       this.neverInline = enqueuer.rootSet.neverInline;
-      this.identifierNameStrings =
-          Sets.union(enqueuer.rootSet.identifierNameStrings, enqueuer.identifierNameStrings);
+      this.identifierNameStrings = joinIdentifierNameStrings(
+          enqueuer.rootSet.identifierNameStrings, enqueuer.identifierNameStrings);
       this.protoLiteFields = enqueuer.protoLiteFields;
       this.prunedTypes = Collections.emptySet();
       this.switchMaps = Collections.emptyMap();
@@ -1922,6 +1915,18 @@
           .collect(ImmutableSortedSet.toImmutableSortedSet(PresortedComparable::slowCompare));
     }
 
+    private Object2BooleanMap<DexItem> joinIdentifierNameStrings(
+        Set<DexItem> explicit, Set<DexItem> implicit) {
+      Object2BooleanMap<DexItem> result = new Object2BooleanArrayMap<>();
+      for (DexItem e : explicit) {
+        result.putIfAbsent(e, true);
+      }
+      for (DexItem i : implicit) {
+        result.putIfAbsent(i, false);
+      }
+      return result;
+    }
+
     private <T extends PresortedComparable<T>> SortedSet<T> toSortedDescriptorSet(
         Set<? extends KeyedDexItem<T>> set) {
       ImmutableSortedSet.Builder<T> builder =
@@ -2041,6 +2046,32 @@
       return builder.build();
     }
 
+    private static Object2BooleanMap<DexItem> rewriteMixedItemsConservatively(
+        Object2BooleanMap<DexItem> original, GraphLense lense) {
+      Object2BooleanMap<DexItem> result = new Object2BooleanArrayMap<>();
+      for (Object2BooleanMap.Entry<DexItem> entry : original.object2BooleanEntrySet()) {
+        DexItem item = entry.getKey();
+        // TODO(b/67934123) There should be a common interface to perform the dispatch.
+        if (item instanceof DexType) {
+          result.put(lense.lookupType((DexType) item), entry.getBooleanValue());
+        } else if (item instanceof DexMethod) {
+          DexMethod method = (DexMethod) item;
+          if (lense.isContextFreeForMethod(method)) {
+            result.put(lense.lookupMethod(method), entry.getBooleanValue());
+          } else {
+            for (DexMethod candidate: lense.lookupMethodInAllContexts(method)) {
+              result.put(candidate, entry.getBooleanValue());
+            }
+          }
+        } else if (item instanceof DexField) {
+          result.put(lense.lookupField((DexField) item), entry.getBooleanValue());
+        } else {
+          throw new Unreachable();
+        }
+      }
+      return result;
+    }
+
     private static <T> Set<T> mergeSets(Collection<T> first, Collection<T> second) {
       ImmutableSet.Builder<T> builder = ImmutableSet.builder();
       builder.addAll(first);
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index ae99990..d3c9e6d 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -84,6 +84,7 @@
       enableNonNullTracking = false;
       enableInlining = false;
       enableClassInlining = false;
+      enableClassStaticizer = false;
       enableSwitchMapRemoval = false;
       outline.enabled = false;
       enableValuePropagation = false;
@@ -102,6 +103,7 @@
   public boolean enableInlining =
       !Version.isDev() || System.getProperty("com.android.tools.r8.disableinlining") == null;
   public boolean enableClassInlining = true;
+  public boolean enableClassStaticizer = true;
   public int classInliningInstructionLimit = 50;
   public int inliningInstructionLimit = 5;
   public boolean enableSwitchMapRemoval = true;
diff --git a/src/test/examples/identifiernamestring/keep-rules-3.txt b/src/test/examples/identifiernamestring/keep-rules-3.txt
index aa23772..1c69314 100644
--- a/src/test/examples/identifiernamestring/keep-rules-3.txt
+++ b/src/test/examples/identifiernamestring/keep-rules-3.txt
@@ -11,7 +11,7 @@
 
 -dontshrink
 
--identifiernamestring class * {
+-identifiernamestring class **.R {
   static java.lang.reflect.Field *(java.lang.Class, java.lang.String);
   static java.lang.reflect.Method *(java.lang.Class, java.lang.String, java.lang.Class[]);
 }
diff --git a/src/test/examples/shaking1/print-mapping-cf.ref b/src/test/examples/shaking1/print-mapping-cf.ref
new file mode 100644
index 0000000..9bf8013
--- /dev/null
+++ b/src/test/examples/shaking1/print-mapping-cf.ref
@@ -0,0 +1,7 @@
+shaking1.Shaking -> shaking1.Shaking:
+shaking1.Used -> a.a:
+    java.lang.String name -> a
+    1:1:java.lang.String method():17:17 -> a
+    1:1:void main(java.lang.String[]):8:8 -> main
+    1:2:void <init>(java.lang.String):12:13 -> <init>
+    1:1:java.lang.String aMethodThatIsNotUsedButKept():21:21 -> aMethodThatIsNotUsedButKept
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index df2c3e0..e9ef5ed 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -701,7 +701,7 @@
         if (range == null) {
           return obfuscatedLineNumber;
         }
-        return range.originalLineFromObfuscated(obfuscatedLineNumber);
+        return range.getOriginalLineNumber(obfuscatedLineNumber);
       }
 
       @Override
@@ -724,8 +724,7 @@
         for (MappedRange range : mappedRanges) {
           lines.add(
               new SignatureAndLine(
-                  range.signature.toString(),
-                  range.originalLineFromObfuscated(obfuscatedLineNumber)));
+                  range.signature.toString(), range.getOriginalLineNumber(obfuscatedLineNumber)));
         }
         return lines;
       }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
new file mode 100644
index 0000000..9be118c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -0,0 +1,272 @@
+// Copyright (c) 2018, 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.staticizer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.VmTestRunner;
+import com.android.tools.r8.code.Instruction;
+import com.android.tools.r8.code.InvokeDirect;
+import com.android.tools.r8.code.InvokeStatic;
+import com.android.tools.r8.code.InvokeVirtual;
+import com.android.tools.r8.code.SgetObject;
+import com.android.tools.r8.code.SputObject;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateConflictField;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateConflictMethod;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateOk;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateOkSideEffects;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostConflictField;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostConflictMethod;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostOk;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostOkSideEffects;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.MoveToHostTestClass;
+import com.android.tools.r8.ir.optimize.staticizer.trivial.Simple;
+import com.android.tools.r8.ir.optimize.staticizer.trivial.SimpleWithGetter;
+import com.android.tools.r8.ir.optimize.staticizer.trivial.SimpleWithParams;
+import com.android.tools.r8.ir.optimize.staticizer.trivial.SimpleWithSideEffects;
+import com.android.tools.r8.ir.optimize.staticizer.trivial.TrivialTestClass;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Streams;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(VmTestRunner.class)
+public class ClassStaticizerTest extends TestBase {
+  @Test
+  public void testTrivial() throws Exception {
+    byte[][] classes = {
+        ToolHelper.getClassAsBytes(TrivialTestClass.class),
+        ToolHelper.getClassAsBytes(Simple.class),
+        ToolHelper.getClassAsBytes(SimpleWithSideEffects.class),
+        ToolHelper.getClassAsBytes(SimpleWithParams.class),
+        ToolHelper.getClassAsBytes(SimpleWithGetter.class),
+    };
+    AndroidApp app = runR8(buildAndroidApp(classes), TrivialTestClass.class);
+
+    String javaOutput = runOnJava(TrivialTestClass.class);
+    String artOutput = runOnArt(app, TrivialTestClass.class);
+    assertEquals(javaOutput, artOutput);
+
+    CodeInspector inspector = new CodeInspector(app);
+    ClassSubject clazz = inspector.clazz(TrivialTestClass.class);
+
+    assertEquals(
+        Lists.newArrayList(
+            "STATIC: String trivial.Simple.bar(String)",
+            "STATIC: String trivial.Simple.foo()",
+            "STATIC: String trivial.TrivialTestClass.next()"),
+        references(clazz, "testSimple", "void"));
+
+    assertTrue(instanceMethods(inspector.clazz(Simple.class)).isEmpty());
+
+    assertEquals(
+        Lists.newArrayList(
+            "STATIC: String trivial.SimpleWithParams.bar(String)",
+            "STATIC: String trivial.SimpleWithParams.foo()",
+            "STATIC: String trivial.TrivialTestClass.next()"),
+        references(clazz, "testSimpleWithParams", "void"));
+
+    assertTrue(instanceMethods(inspector.clazz(SimpleWithParams.class)).isEmpty());
+
+    assertEquals(
+        Lists.newArrayList(
+            "STATIC: String trivial.SimpleWithSideEffects.bar(String)",
+            "STATIC: String trivial.SimpleWithSideEffects.foo()",
+            "STATIC: String trivial.TrivialTestClass.next()",
+            "trivial.SimpleWithSideEffects trivial.SimpleWithSideEffects.INSTANCE",
+            "trivial.SimpleWithSideEffects trivial.SimpleWithSideEffects.INSTANCE"),
+        references(clazz, "testSimpleWithSideEffects", "void"));
+
+    assertTrue(instanceMethods(inspector.clazz(SimpleWithSideEffects.class)).isEmpty());
+
+    // TODO(b/111832046): add support for singleton instance getters.
+    assertEquals(
+        Lists.newArrayList(
+            "STATIC: String trivial.TrivialTestClass.next()",
+            "VIRTUAL: String trivial.SimpleWithGetter.bar(String)",
+            "VIRTUAL: String trivial.SimpleWithGetter.foo()",
+            "trivial.SimpleWithGetter trivial.SimpleWithGetter.INSTANCE",
+            "trivial.SimpleWithGetter trivial.SimpleWithGetter.INSTANCE"),
+        references(clazz, "testSimpleWithGetter", "void"));
+
+    assertFalse(instanceMethods(inspector.clazz(SimpleWithGetter.class)).isEmpty());
+  }
+
+  @Test
+  public void testMoveToHost() throws Exception {
+    byte[][] classes = {
+        ToolHelper.getClassAsBytes(MoveToHostTestClass.class),
+        ToolHelper.getClassAsBytes(HostOk.class),
+        ToolHelper.getClassAsBytes(CandidateOk.class),
+        ToolHelper.getClassAsBytes(HostOkSideEffects.class),
+        ToolHelper.getClassAsBytes(CandidateOkSideEffects.class),
+        ToolHelper.getClassAsBytes(HostConflictMethod.class),
+        ToolHelper.getClassAsBytes(CandidateConflictMethod.class),
+        ToolHelper.getClassAsBytes(HostConflictField.class),
+        ToolHelper.getClassAsBytes(CandidateConflictField.class),
+    };
+    AndroidApp app = runR8(buildAndroidApp(classes), MoveToHostTestClass.class);
+
+    String javaOutput = runOnJava(MoveToHostTestClass.class);
+    String artOutput = runOnArt(app, MoveToHostTestClass.class);
+    assertEquals(javaOutput, artOutput);
+
+    CodeInspector inspector = new CodeInspector(app);
+    ClassSubject clazz = inspector.clazz(MoveToHostTestClass.class);
+
+    assertEquals(
+        Lists.newArrayList(
+            "STATIC: String movetohost.HostOk.bar(String)",
+            "STATIC: String movetohost.HostOk.foo()",
+            "STATIC: String movetohost.MoveToHostTestClass.next()",
+            "STATIC: String movetohost.MoveToHostTestClass.next()",
+            "STATIC: void movetohost.HostOk.blah(String)"),
+        references(clazz, "testOk", "void"));
+
+    assertFalse(inspector.clazz(CandidateOk.class).isPresent());
+
+    assertEquals(
+        Lists.newArrayList(
+            "STATIC: String movetohost.HostOkSideEffects.bar(String)",
+            "STATIC: String movetohost.HostOkSideEffects.foo()",
+            "STATIC: String movetohost.MoveToHostTestClass.next()",
+            "movetohost.HostOkSideEffects movetohost.HostOkSideEffects.INSTANCE",
+            "movetohost.HostOkSideEffects movetohost.HostOkSideEffects.INSTANCE"),
+        references(clazz, "testOkSideEffects", "void"));
+
+    assertFalse(inspector.clazz(CandidateOkSideEffects.class).isPresent());
+
+    assertEquals(
+        Lists.newArrayList(
+            "DIRECT: void movetohost.HostConflictMethod.<init>()",
+            "STATIC: String movetohost.CandidateConflictMethod.bar(String)",
+            "STATIC: String movetohost.CandidateConflictMethod.foo()",
+            "STATIC: String movetohost.MoveToHostTestClass.next()",
+            "STATIC: String movetohost.MoveToHostTestClass.next()",
+            "VIRTUAL: String movetohost.HostConflictMethod.bar(String)"),
+        references(clazz, "testConflictMethod", "void"));
+
+    assertTrue(inspector.clazz(CandidateConflictMethod.class).isPresent());
+
+    assertEquals(
+        Lists.newArrayList(
+            "DIRECT: void movetohost.HostConflictField.<init>()",
+            "STATIC: String movetohost.CandidateConflictField.bar(String)",
+            "STATIC: String movetohost.CandidateConflictField.foo()",
+            "STATIC: String movetohost.MoveToHostTestClass.next()",
+            "String movetohost.CandidateConflictField.field"),
+        references(clazz, "testConflictField", "void"));
+
+    assertTrue(inspector.clazz(CandidateConflictMethod.class).isPresent());
+  }
+
+  private List<String> instanceMethods(ClassSubject clazz) {
+    assertNotNull(clazz);
+    assertTrue(clazz.isPresent());
+    return Streams.stream(clazz.getDexClass().methods())
+        .filter(method -> !method.isStaticMethod())
+        .map(method -> method.method.toSourceString())
+        .sorted()
+        .collect(Collectors.toList());
+  }
+
+  private List<String> references(
+      ClassSubject clazz, String methodName, String retValue, String... params) {
+    assertNotNull(clazz);
+    assertTrue(clazz.isPresent());
+
+    MethodSignature signature = new MethodSignature(methodName, retValue, params);
+    DexCode code = clazz.method(signature).getMethod().getCode().asDexCode();
+    return Streams.concat(
+        filterInstructionKind(code, SgetObject.class)
+            .map(Instruction::getField)
+            .filter(fld -> isTypeOfInterest(fld.clazz))
+            .map(DexField::toSourceString),
+        filterInstructionKind(code, SputObject.class)
+            .map(Instruction::getField)
+            .filter(fld -> isTypeOfInterest(fld.clazz))
+            .map(DexField::toSourceString),
+        filterInstructionKind(code, InvokeStatic.class)
+            .map(insn -> (InvokeStatic) insn)
+            .map(InvokeStatic::getMethod)
+            .filter(method -> isTypeOfInterest(method.holder))
+            .map(method -> "STATIC: " + method.toSourceString()),
+        filterInstructionKind(code, InvokeVirtual.class)
+            .map(insn -> (InvokeVirtual) insn)
+            .map(InvokeVirtual::getMethod)
+            .filter(method -> isTypeOfInterest(method.holder))
+            .map(method -> "VIRTUAL: " + method.toSourceString()),
+        filterInstructionKind(code, InvokeDirect.class)
+            .map(insn -> (InvokeDirect) insn)
+            .map(InvokeDirect::getMethod)
+            .filter(method -> isTypeOfInterest(method.holder))
+            .map(method -> "DIRECT: " + method.toSourceString()))
+        .map(txt -> txt.replace("java.lang.", ""))
+        .map(txt -> txt.replace("com.android.tools.r8.ir.optimize.staticizer.", ""))
+        .sorted()
+        .collect(Collectors.toList());
+  }
+
+  private boolean isTypeOfInterest(DexType type) {
+    return type.toSourceString().startsWith("com.android.tools.r8.ir.optimize.staticizer");
+  }
+
+  private AndroidApp runR8(AndroidApp app, Class mainClass) throws Exception {
+    AndroidApp compiled =
+        compileWithR8(app, getProguardConfig(mainClass.getCanonicalName()), this::configure);
+
+    // Materialize file for execution.
+    Path generatedDexFile = temp.getRoot().toPath().resolve("classes.jar");
+    compiled.writeToZip(generatedDexFile, OutputMode.DexIndexed);
+
+    // Run with ART.
+    String artOutput = ToolHelper.runArtNoVerificationErrors(
+        generatedDexFile.toString(), mainClass.getCanonicalName());
+
+    // Compare with Java.
+    ProcessResult javaResult = ToolHelper.runJava(
+        ToolHelper.getClassPathForTests(), mainClass.getCanonicalName());
+
+    if (javaResult.exitCode != 0) {
+      System.out.println(javaResult.stdout);
+      System.err.println(javaResult.stderr);
+      fail("JVM failed for: " + mainClass);
+    }
+    assertEquals("JVM and ART output differ", javaResult.stdout, artOutput);
+
+    return compiled;
+  }
+
+  private String getProguardConfig(String main) {
+    return keepMainProguardConfiguration(main)
+        + System.lineSeparator()
+        + "-dontobfuscate"
+        + System.lineSeparator()
+        + "-allowaccessmodification";
+  }
+
+  private void configure(InternalOptions options) {
+    options.enableClassInlining = false;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictField.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictField.java
new file mode 100644
index 0000000..f2108b0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictField.java
@@ -0,0 +1,21 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class CandidateConflictField {
+  public static String field;
+
+  public String foo() {
+    synchronized ("") {
+      return bar("CandidateConflictMethod::foo()");
+    }
+  }
+
+  public String bar(String other) {
+    synchronized ("") {
+      return "CandidateConflictMethod::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictMethod.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictMethod.java
new file mode 100644
index 0000000..40d0576
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictMethod.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class CandidateConflictMethod {
+  public String foo() {
+    synchronized ("") {
+      return bar("CandidateConflictMethod::foo()");
+    }
+  }
+
+  public String bar(String other) {
+    synchronized ("") {
+      return "CandidateConflictMethod::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOk.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOk.java
new file mode 100644
index 0000000..daf5647
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOk.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class CandidateOk {
+  public String foo() {
+    synchronized ("") {
+      return bar("CandidateOk::foo()");
+    }
+  }
+
+  public String bar(String other) {
+    synchronized ("") {
+      return "CandidateOk::bar(" + other + ")";
+    }
+  }
+
+  public void blah(String other) {
+    synchronized ("") {
+      System.out.println("CandidateOk::blah(" + other + ")");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkSideEffects.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkSideEffects.java
new file mode 100644
index 0000000..2077056
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkSideEffects.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class CandidateOkSideEffects {
+  public String foo() {
+    synchronized ("") {
+      return bar("CandidateOkSideEffects::foo()");
+    }
+  }
+
+  public String bar(String other) {
+    synchronized ("") {
+      return "CandidateOkSideEffects::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostConflictField.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostConflictField.java
new file mode 100644
index 0000000..7166b81
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostConflictField.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class HostConflictField {
+  static CandidateConflictField INSTANCE = new CandidateConflictField();
+
+  public String field;
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostConflictMethod.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostConflictMethod.java
new file mode 100644
index 0000000..ad86b35
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostConflictMethod.java
@@ -0,0 +1,15 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class HostConflictMethod {
+  static CandidateConflictMethod INSTANCE = new CandidateConflictMethod();
+
+  public String bar(String other) {
+    synchronized ("") {
+      return "HostConflictMethod::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOk.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOk.java
new file mode 100644
index 0000000..8ecd7ac
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOk.java
@@ -0,0 +1,9 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class HostOk {
+  static CandidateOk INSTANCE = new CandidateOk();
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOkSideEffects.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOkSideEffects.java
new file mode 100644
index 0000000..1f6ec7c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOkSideEffects.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class HostOkSideEffects {
+  static CandidateOkSideEffects INSTANCE = new CandidateOkSideEffects();
+
+  static {
+    System.out.println("Inside HostOkSideEffects::<clinit>()");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostTestClass.java
new file mode 100644
index 0000000..3f31839
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostTestClass.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2018, 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.staticizer.movetohost;
+
+public class MoveToHostTestClass {
+  private static int ID = 0;
+
+  private static String next() {
+    return Integer.toString(ID++);
+  }
+
+  public static void main(String[] args) {
+    MoveToHostTestClass test = new MoveToHostTestClass();
+    test.testOk();
+    test.testOkSideEffects();
+    test.testConflictMethod();
+    test.testConflictField();
+  }
+
+  private synchronized void testOk() {
+    System.out.println(HostOk.INSTANCE.foo());
+    System.out.println(HostOk.INSTANCE.bar(next()));
+    HostOk.INSTANCE.blah(next());
+  }
+
+  private synchronized void testOkSideEffects() {
+    System.out.println(HostOkSideEffects.INSTANCE.foo());
+    System.out.println(HostOkSideEffects.INSTANCE.bar(next()));
+  }
+
+  private synchronized void testConflictMethod() {
+    System.out.println(new HostConflictMethod().bar(next()));
+    System.out.println(HostConflictMethod.INSTANCE.foo());
+    System.out.println(HostConflictMethod.INSTANCE.bar(next()));
+  }
+
+  private synchronized void testConflictField() {
+    System.out.println(new HostConflictField().field);
+    System.out.println(CandidateConflictField.field);
+    System.out.println(HostConflictField.INSTANCE.foo());
+    System.out.println(HostConflictField.INSTANCE.bar(next()));
+  }
+}
+
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/Simple.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/Simple.java
new file mode 100644
index 0000000..8df079c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/Simple.java
@@ -0,0 +1,21 @@
+// Copyright (c) 2018, 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.staticizer.trivial;
+
+public class Simple {
+  static Simple INSTANCE = new Simple();
+
+  String foo() {
+    synchronized ("") {
+      return bar("Simple::foo()");
+    }
+  }
+
+  String bar(String other) {
+    synchronized ("") {
+      return "Simple::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java
new file mode 100644
index 0000000..9ff20c3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2018, 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.staticizer.trivial;
+
+public class SimpleWithGetter {
+  private static SimpleWithGetter INSTANCE = new SimpleWithGetter();
+
+  static SimpleWithGetter getInstance() {
+    return INSTANCE;
+  }
+
+  String foo() {
+    synchronized ("") {
+      return bar("Simple::foo()");
+    }
+  }
+
+  String bar(String other) {
+    synchronized ("") {
+      return "Simple::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithParams.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithParams.java
new file mode 100644
index 0000000..a71f1f5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithParams.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2018, 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.staticizer.trivial;
+
+public class SimpleWithParams {
+  static SimpleWithParams INSTANCE = new SimpleWithParams(123);
+
+  SimpleWithParams(int i) {
+  }
+
+  String foo() {
+    synchronized ("") {
+      return bar("SimpleWithParams::foo()");
+    }
+  }
+
+  String bar(String other) {
+    synchronized ("") {
+      return "SimpleWithParams::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithSideEffects.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithSideEffects.java
new file mode 100644
index 0000000..5ba97dc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithSideEffects.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2018, 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.staticizer.trivial;
+
+public class SimpleWithSideEffects {
+  static SimpleWithSideEffects INSTANCE = new SimpleWithSideEffects();
+
+  static {
+    System.out.println("SimpleWithSideEffects::<clinit>()");
+  }
+
+  String foo() {
+    synchronized ("") {
+      return bar("SimpleWithSideEffects::foo()");
+    }
+  }
+
+  String bar(String other) {
+    synchronized ("") {
+      return "SimpleWithSideEffects::bar(" + other + ")";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/TrivialTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/TrivialTestClass.java
new file mode 100644
index 0000000..2dad273
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/TrivialTestClass.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2018, 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.staticizer.trivial;
+
+public class TrivialTestClass {
+  private static int ID = 0;
+
+  private static String next() {
+    return Integer.toString(ID++);
+  }
+
+  public static void main(String[] args) {
+    TrivialTestClass test = new TrivialTestClass();
+    test.testSimple();
+    test.testSimpleWithSideEffects();
+    test.testSimpleWithParams();
+    test.testSimpleWithGetter();
+  }
+
+  private synchronized void testSimple() {
+    System.out.println(Simple.INSTANCE.foo());
+    System.out.println(Simple.INSTANCE.bar(next()));
+  }
+
+  private synchronized void testSimpleWithSideEffects() {
+    System.out.println(SimpleWithSideEffects.INSTANCE.foo());
+    System.out.println(SimpleWithSideEffects.INSTANCE.bar(next()));
+  }
+
+  private synchronized void testSimpleWithParams() {
+    System.out.println(SimpleWithParams.INSTANCE.foo());
+    System.out.println(SimpleWithParams.INSTANCE.bar(next()));
+  }
+
+  private synchronized void testSimpleWithGetter() {
+    System.out.println(SimpleWithGetter.getInstance().foo());
+    System.out.println(SimpleWithGetter.getInstance().bar(next()));
+  }
+}
+
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
new file mode 100644
index 0000000..c19b80e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2018, 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.kotlin;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.Test;
+import org.junit.runners.Parameterized.Parameters;
+
+public class KotlinClassStaticizerTest extends AbstractR8KotlinTestBase {
+  @Parameters(name = "allowAccessModification: {0} target: {1}")
+  public static Collection<Object[]> data() {
+    ImmutableList.Builder<Object[]> builder = new ImmutableList.Builder<>();
+    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
+      builder.add(new Object[]{Boolean.TRUE, targetVersion});
+    }
+    return builder.build();
+  }
+
+  @Test
+  public void testCompanionAndRegularObjects() throws Exception {
+    final String mainClassName = "class_staticizer.MainKt";
+
+    // Without class staticizer.
+    runTest("class_staticizer", mainClassName, false, (app) -> {
+      CodeInspector inspector = new CodeInspector(app);
+      assertTrue(inspector.clazz("class_staticizer.Regular$Companion").isPresent());
+      assertTrue(inspector.clazz("class_staticizer.Derived$Companion").isPresent());
+
+      ClassSubject utilClass = inspector.clazz("class_staticizer.Util");
+      assertTrue(utilClass.isPresent());
+      AtomicInteger nonStaticMethodCount = new AtomicInteger();
+      utilClass.forAllMethods(method -> {
+        if (!method.isStatic()) {
+          nonStaticMethodCount.incrementAndGet();
+        }
+      });
+      assertEquals(4, nonStaticMethodCount.get());
+    });
+
+    // With class staticizer.
+    runTest("class_staticizer", mainClassName, true, (app) -> {
+      CodeInspector inspector = new CodeInspector(app);
+      assertFalse(inspector.clazz("class_staticizer.Regular$Companion").isPresent());
+      assertFalse(inspector.clazz("class_staticizer.Derived$Companion").isPresent());
+
+      ClassSubject utilClass = inspector.clazz("class_staticizer.Util");
+      assertTrue(utilClass.isPresent());
+      utilClass.forAllMethods(method -> assertTrue(method.isStatic()));
+    });
+  }
+
+  protected void runTest(String folder, String mainClass,
+      boolean enabled, AndroidAppInspector inspector) throws Exception {
+    runTest(
+        folder, mainClass, null,
+        options -> {
+          options.enableClassInlining = false;
+          options.enableClassStaticizer = enabled;
+        }, inspector);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
index 4b1b404..a3aa5e1 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
@@ -16,11 +16,13 @@
 import com.android.tools.r8.kotlin.TestKotlinClass.Visibility;
 import com.android.tools.r8.naming.MemberNaming;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FieldSubject;
 import java.nio.file.Path;
 import java.util.Collections;
+import java.util.function.Consumer;
 import org.junit.Assert;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -59,12 +61,15 @@
           .addProperty("property", JAVA_LANG_STRING, Visibility.PRIVATE)
           .addProperty("indirectPropertyGetter", JAVA_LANG_STRING, Visibility.PRIVATE);
 
+  private Consumer<InternalOptions> disableClassStaticizer =
+      opts -> opts.enableClassStaticizer = false;
+
   @Test
   public void testCompanionProperty_primitivePropertyIsAlwaysInlined() throws Exception {
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_usePrimitiveProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, disableClassStaticizer, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       String propertyName = "primitiveProp";
@@ -93,7 +98,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_usePrivateProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, disableClassStaticizer, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       String propertyName = "privateProp";
@@ -123,7 +128,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_useInternalProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, disableClassStaticizer, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       String propertyName = "internalProp";
@@ -152,7 +157,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_usePublicProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, disableClassStaticizer, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       String propertyName = "publicProp";
@@ -181,7 +186,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePrivateLateInitProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, (app) -> {
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, disableClassStaticizer, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       String propertyName = "privateLateInitProp";
@@ -257,7 +262,7 @@
     TestKotlinCompanionClass testedClass = ACCESSOR_COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("accessors.AccessorKt",
         "accessor_accessPropertyFromCompanionClass");
-    runTest("accessors", mainClass, (app) -> {
+    runTest("accessors", mainClass, disableClassStaticizer, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index 89ff81d..08fcebe 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -83,16 +83,17 @@
           .addProperty("internalLateInitProp", JAVA_LANG_STRING, Visibility.INTERNAL)
           .addProperty("publicLateInitProp", JAVA_LANG_STRING, Visibility.PUBLIC);
 
-  private Consumer<InternalOptions> disableClassInliningAndMerging = o -> {
+  private Consumer<InternalOptions> disableAggressiveClassOptimizations = o -> {
     o.enableClassInlining = false;
     o.enableClassMerging = false;
+    o.enableClassStaticizer = false;
   };
 
   @Test
   public void testMutableProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -115,7 +116,7 @@
   public void testMutableProperty_privateIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -137,7 +138,7 @@
   public void testMutableProperty_protectedIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_useProtectedProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -160,7 +161,7 @@
   public void testMutableProperty_internalIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -183,7 +184,7 @@
   public void testMutableProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -206,7 +207,7 @@
   public void testMutableProperty_primitivePropertyIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           MUTABLE_PROPERTY_CLASS.getClassName());
@@ -231,11 +232,12 @@
   public void testLateInitProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
         "lateInitProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
-      for (Entry<String, KotlinProperty> property : LATE_INIT_PROPERTY_CLASS.properties.entrySet()) {
+      for (Entry<String, KotlinProperty> property : LATE_INIT_PROPERTY_CLASS.properties
+          .entrySet()) {
         MethodSignature getter = LATE_INIT_PROPERTY_CLASS.getGetterForProperty(property.getKey());
         MethodSignature setter = LATE_INIT_PROPERTY_CLASS.getSetterForProperty(property.getKey());
         if (property.getValue().getVisibility() == Visibility.PRIVATE) {
@@ -255,7 +257,7 @@
   public void testLateInitProperty_privateIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_usePrivateLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -278,7 +280,7 @@
   public void testLateInitProperty_protectedIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
         "lateInitProperty_useProtectedLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -299,7 +301,7 @@
   public void testLateInitProperty_internalIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_useInternalLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -318,7 +320,7 @@
   public void testLateInitProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_usePublicLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           LATE_INIT_PROPERTY_CLASS.getClassName());
@@ -337,7 +339,7 @@
   public void testUserDefinedProperty_getterAndSetterAreRemoveIfNotUsed() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/UserDefinedPropertyKt", "userDefinedProperty_noUseOfProperties");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           USER_DEFINED_PROPERTY_CLASS.getClassName());
@@ -354,7 +356,7 @@
   public void testUserDefinedProperty_publicIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties/UserDefinedPropertyKt", "userDefinedProperty_useProperties");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject classSubject = checkClassIsKept(codeInspector,
           USER_DEFINED_PROPERTY_CLASS.getClassName());
@@ -380,7 +382,7 @@
   public void testCompanionProperty_primitivePropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector,
           "properties.CompanionProperties");
@@ -411,7 +413,7 @@
   public void testCompanionProperty_privatePropertyIsAlwaysInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector,
           "properties.CompanionProperties");
@@ -445,7 +447,7 @@
   public void testCompanionProperty_internalPropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector,
           "properties.CompanionProperties");
@@ -476,7 +478,7 @@
   public void testCompanionProperty_publicPropertyCannotBeInlined() throws Exception {
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector,
           "properties.CompanionProperties");
@@ -508,7 +510,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePrivateLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
@@ -539,7 +541,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_useInternalLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
@@ -563,7 +565,7 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePublicLateInitProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
       ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
@@ -587,7 +589,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "primitiveProp";
@@ -614,7 +616,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "privateProp";
@@ -641,7 +643,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "internalProp";
@@ -668,7 +670,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "publicProp";
@@ -695,7 +697,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useLateInitPrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "privateLateInitProp";
@@ -722,7 +724,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useLateInitInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "internalLateInitProp";
@@ -745,7 +747,7 @@
     final TestKotlinClass testedClass = OBJECT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useLateInitPublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "publicLateInitProp";
@@ -768,7 +770,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_usePrimitiveProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "primitiveProp";
@@ -795,7 +797,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_usePrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "privateProp";
@@ -821,7 +823,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "internalProp";
@@ -847,7 +849,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_usePublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "publicProp";
@@ -874,7 +876,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useLateInitPrivateProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject fileClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "privateLateInitProp";
@@ -900,7 +902,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useLateInitInternalProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "internalLateInitProp";
@@ -924,7 +926,7 @@
     final TestKotlinClass testedClass = FILE_PROPERTY_CLASS;
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useLateInitPublicProp");
-    runTest(PACKAGE_NAME, mainClass, disableClassInliningAndMerging, (app) -> {
+    runTest(PACKAGE_NAME, mainClass, disableAggressiveClassOptimizations, (app) -> {
       CodeInspector codeInspector = new CodeInspector(app);
       ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
       String propertyName = "publicLateInitProp";
diff --git a/src/test/java/com/android/tools/r8/naming/IdentifierMinifierTest.java b/src/test/java/com/android/tools/r8/naming/IdentifierMinifierTest.java
index a9ee851..73cdaaf 100644
--- a/src/test/java/com/android/tools/r8/naming/IdentifierMinifierTest.java
+++ b/src/test/java/com/android/tools/r8/naming/IdentifierMinifierTest.java
@@ -142,7 +142,7 @@
     FoundMethodSubject foundMain = (FoundMethodSubject) main;
     verifyPresenceOfConstString(foundMain);
     int renamedYetFoundIdentifierCount = countRenamedClassIdentifier(inspector, foundMain);
-    assertEquals(0, renamedYetFoundIdentifierCount);
+    assertEquals(4, renamedYetFoundIdentifierCount);
 
     ClassSubject aClass = inspector.clazz("adaptclassstrings.A");
     MethodSubject bar = aClass.method("void", "bar", ImmutableList.of());
@@ -165,7 +165,7 @@
     FoundMethodSubject foundMain = (FoundMethodSubject) main;
     verifyPresenceOfConstString(foundMain);
     int renamedYetFoundIdentifierCount = countRenamedClassIdentifier(inspector, foundMain);
-    assertEquals(0, renamedYetFoundIdentifierCount);
+    assertEquals(4, renamedYetFoundIdentifierCount);
 
     ClassSubject aClass = inspector.clazz("adaptclassstrings.A");
     MethodSubject bar = aClass.method("void", "bar", ImmutableList.of());
@@ -231,7 +231,7 @@
     FoundMethodSubject foundMain = (FoundMethodSubject) main;
     verifyPresenceOfConstString(foundMain);
     int renamedYetFoundIdentifierCount = countRenamedClassIdentifier(inspector, foundMain);
-    assertEquals(0, renamedYetFoundIdentifierCount);
+    assertEquals(1, renamedYetFoundIdentifierCount);
 
     ClassSubject aClass = inspector.clazz("identifiernamestring.A");
     MethodSubject aInit =
@@ -255,7 +255,7 @@
     FoundMethodSubject foundMain = (FoundMethodSubject) main;
     verifyPresenceOfConstString(foundMain);
     int renamedYetFoundIdentifierCount = countRenamedClassIdentifier(inspector, foundMain);
-    assertEquals(1, renamedYetFoundIdentifierCount);
+    assertEquals(2, renamedYetFoundIdentifierCount);
 
     ClassSubject aClass = inspector.clazz("identifiernamestring.A");
     MethodSubject aInit =
@@ -271,7 +271,7 @@
     assertEquals(2, renamedYetFoundIdentifierCount);
   }
 
-  // With -identifiernamestring for reflective methods
+  // With -identifiernamestring for reflective methods in testing class R.
   private static void test2_rule3(CodeInspector inspector) {
     ClassSubject mainClass = inspector.clazz("identifiernamestring.Main");
     MethodSubject main = mainClass.method(CodeInspector.MAIN);
diff --git a/src/test/java/com/android/tools/r8/naming/WarnReflectiveAccessTest.java b/src/test/java/com/android/tools/r8/naming/WarnReflectiveAccessTest.java
index 829cdd0..49ba186 100644
--- a/src/test/java/com/android/tools/r8/naming/WarnReflectiveAccessTest.java
+++ b/src/test/java/com/android/tools/r8/naming/WarnReflectiveAccessTest.java
@@ -9,6 +9,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.DiagnosticsChecker;
 import com.android.tools.r8.OutputMode;
@@ -72,15 +73,17 @@
       byte[][] classes,
       Class main,
       Path out,
+      boolean explicitRule,
       boolean enableMinification,
       boolean forceProguardCompatibility)
       throws Exception {
     String minificationModifier = enableMinification ? ",allowobfuscation " : " ";
-    String reflectionRules = forceProguardCompatibility ? ""
-        : "-identifiernamestring class java.lang.Class {\n"
+    String reflectionRules = explicitRule
+        ? "-identifiernamestring class java.lang.Class {\n"
             + "static java.lang.Class forName(java.lang.String);\n"
             + "java.lang.reflect.Method getDeclaredMethod(java.lang.String, java.lang.Class[]);\n"
-            + "}\n";
+            + "}\n"
+        : "";
     R8Command.Builder commandBuilder =
         R8Command.builder(reporter)
             .addProguardConfiguration(
@@ -102,7 +105,8 @@
     });
   }
 
-  private void reflectionWithBuildter(
+  private void reflectionWithBuilder(
+      boolean explicitRule,
       boolean enableMinification,
       boolean forceProguardCompatibility) throws Exception {
     byte[][] classes = {
@@ -110,7 +114,7 @@
     };
     Path out = temp.getRoot().toPath();
     AndroidApp processedApp = runR8(classes, WarnReflectiveAccessTestMain.class, out,
-        enableMinification, forceProguardCompatibility);
+        explicitRule, enableMinification, forceProguardCompatibility);
 
     String main = WarnReflectiveAccessTestMain.class.getCanonicalName();
     CodeInspector codeInspector = new CodeInspector(processedApp);
@@ -133,35 +137,59 @@
   }
 
   @Test
-  public void test_minification_forceProguardCompatibility() throws Exception {
-    reflectionWithBuildter(true, true);
+  public void test_explicit_minification_forceProguardCompatibility() throws Exception {
+    reflectionWithBuilder(true, true, true);
     assertFalse(handler.warnings.isEmpty());
-    DiagnosticsChecker.checkDiagnostic(handler.warnings.get(0), (Path) null, 54, 1,
-        "Cannot determine", "getDeclaredMethod", "resolution failure");
-  }
-
-  @Test
-  public void test_noMinification_forceProguardCompatibility() throws Exception {
-    reflectionWithBuildter(false, true);
-    assertFalse(handler.warnings.isEmpty());
-    DiagnosticsChecker.checkDiagnostic(handler.warnings.get(0), (Path) null, 54, 1,
-        "Cannot determine", "getDeclaredMethod", "resolution failure");
-  }
-
-  @Test
-  public void test_minification_R8() throws Exception {
-    reflectionWithBuildter(true, false);
-    assertFalse(handler.warnings.isEmpty());
-    DiagnosticsChecker.checkDiagnostic(handler.warnings.get(0), (Path) null, 54, 1,
+    DiagnosticsChecker.checkDiagnostic(handler.warnings.get(0), (Path) null, 55, 1,
         "Cannot determine", "getDeclaredMethod", "-identifiernamestring", "resolution failure");
   }
 
   @Test
-  public void test_noMinification_R8() throws Exception {
-    reflectionWithBuildter(false, false);
+  public void test_explicit_noMinification_forceProguardCompatibility() throws Exception {
+    reflectionWithBuilder(true, false, true);
     assertFalse(handler.warnings.isEmpty());
-    DiagnosticsChecker.checkDiagnostic(handler.warnings.get(0), (Path) null, 54, 1,
+    DiagnosticsChecker.checkDiagnostic(handler.warnings.get(0), (Path) null, 55, 1,
         "Cannot determine", "getDeclaredMethod", "-identifiernamestring", "resolution failure");
   }
 
+  @Test
+  public void test_explicit_minification_R8() throws Exception {
+    reflectionWithBuilder(true, true, false);
+    assertFalse(handler.warnings.isEmpty());
+    DiagnosticsChecker.checkDiagnostic(handler.warnings.get(0), (Path) null, 55, 1,
+        "Cannot determine", "getDeclaredMethod", "-identifiernamestring", "resolution failure");
+  }
+
+  @Test
+  public void test_explicit_noMinification_R8() throws Exception {
+    reflectionWithBuilder(true, false, false);
+    assertFalse(handler.warnings.isEmpty());
+    DiagnosticsChecker.checkDiagnostic(handler.warnings.get(0), (Path) null, 55, 1,
+        "Cannot determine", "getDeclaredMethod", "-identifiernamestring", "resolution failure");
+  }
+
+  @Test
+  public void test_implicit_minification_forceProguardCompatibility() throws Exception {
+    reflectionWithBuilder(false, true, true);
+    assertTrue(handler.warnings.isEmpty());
+  }
+
+  @Test
+  public void test_implicit_noMinification_forceProguardCompatibility() throws Exception {
+    reflectionWithBuilder(false, false, true);
+    assertTrue(handler.warnings.isEmpty());
+  }
+
+  @Test
+  public void test_implicit_minification_R8() throws Exception {
+    reflectionWithBuilder(false, true, false);
+    assertTrue(handler.warnings.isEmpty());
+  }
+
+  @Test
+  public void test_implicit_noMinification_R8() throws Exception {
+    reflectionWithBuilder(false, false, false);
+    assertTrue(handler.warnings.isEmpty());
+  }
+
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
index cba8686..ad67919 100644
--- a/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/TreeShakingSpecificTest.java
@@ -14,12 +14,15 @@
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.ToolHelper;
 import java.io.BufferedReader;
+import java.io.IOException;
 import java.io.PrintStream;
 import java.io.StringReader;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.junit.Assert;
@@ -27,8 +30,28 @@
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
+@RunWith(Parameterized.class)
 public class TreeShakingSpecificTest {
+  enum Backend {
+    DEX,
+    CF
+  }
+
+  private Backend backend;
+
+  @Parameters(name = "Backend: {0}")
+  public static Collection<Backend> data() {
+    return Arrays.asList(Backend.values());
+  }
+
+  public TreeShakingSpecificTest(Backend backend) {
+    this.backend = backend;
+  }
+
   private static final String VALID_PROGUARD_DIR = "src/test/proguard/valid/";
 
   @Rule
@@ -37,18 +60,28 @@
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
+  private void finishBuild(R8Command.Builder builder, Path out, String test) throws IOException {
+    Path input;
+    if (backend == Backend.DEX) {
+      builder.setOutput(out, OutputMode.DexIndexed);
+      input = Paths.get(EXAMPLES_BUILD_DIR, test, "classes.dex");
+    } else {
+      builder.setOutput(out, OutputMode.ClassFile);
+      input = Paths.get(EXAMPLES_BUILD_DIR, test + ".jar");
+    }
+    ToolHelper.getAppBuilder(builder).addProgramFiles(input);
+  }
+
   @Test
   public void testIgnoreWarnings() throws Exception {
     // Generate R8 processed version without library option.
     Path out = temp.getRoot().toPath();
     String test = "shaking2";
-    Path originalDex = Paths.get(EXAMPLES_BUILD_DIR, test, "classes.dex");
     Path keepRules = Paths.get(EXAMPLES_DIR, test, "keep-rules.txt");
     Path ignoreWarnings = Paths.get(VALID_PROGUARD_DIR, "ignorewarnings.flags");
     R8Command.Builder builder = R8Command.builder()
-        .setOutput(out, OutputMode.DexIndexed)
         .addProguardConfigurationFiles(keepRules, ignoreWarnings);
-    ToolHelper.getAppBuilder(builder).addProgramFiles(originalDex);
+    finishBuild(builder, out, test);
     R8.run(builder.build());
   }
 
@@ -57,7 +90,6 @@
     // Generate R8 processed version without library option.
     Path out = temp.getRoot().toPath();
     String test = "shaking2";
-    Path originalDex = Paths.get(EXAMPLES_BUILD_DIR, test, "classes.dex");
     Path keepRules = Paths.get(EXAMPLES_DIR, test, "keep-rules.txt");
     DiagnosticsHandler handler = new DiagnosticsHandler() {
       @Override
@@ -68,9 +100,8 @@
       }
     };
     R8Command.Builder builder = R8Command.builder(handler)
-        .setOutput(out, OutputMode.DexIndexed)
         .addProguardConfigurationFiles(keepRules);
-    ToolHelper.getAppBuilder(builder).addProgramFiles(originalDex);
+    finishBuild(builder, out, test);
     R8.run(builder.build());
   }
 
@@ -79,7 +110,6 @@
     // Generate R8 processed version without library option.
     String test = "shaking1";
     Path out = temp.getRoot().toPath();
-    Path originalDex = Paths.get(EXAMPLES_BUILD_DIR, test, "classes.dex");
     Path keepRules = Paths.get(EXAMPLES_DIR, test, "keep-rules.txt");
 
     // Create a flags file in temp dir requesting dump of the mapping.
@@ -90,10 +120,15 @@
     }
 
     R8Command.Builder builder = R8Command.builder()
-        .setOutput(out, OutputMode.DexIndexed)
         .addProguardConfigurationFiles(keepRules, printMapping);
-    ToolHelper.getAppBuilder(builder).addProgramFiles(originalDex);
     // Turn off inlining, as we want the mapping that is printed to be stable.
+    finishBuild(builder, out, test);
+    if (backend == Backend.DEX) {
+      builder.addLibraryFiles(ToolHelper.getDefaultAndroidJar());
+    } else {
+      assert backend == Backend.CF;
+      builder.addLibraryFiles(ToolHelper.getJava8RuntimeJar());
+    }
     ToolHelper.runR8(builder.build(), options -> options.enableInlining = false);
 
     Path outputmapping = out.resolve("mapping.txt");
@@ -102,8 +137,15 @@
         Stream.of(new String(Files.readAllBytes(outputmapping), StandardCharsets.UTF_8).split("\n"))
             .filter(line -> !line.startsWith("#"))
             .collect(Collectors.joining("\n"));
-    String refMapping = new String(Files.readAllBytes(
-        Paths.get(EXAMPLES_DIR, "shaking1", "print-mapping.ref")), StandardCharsets.UTF_8);
+    // For the CF backend we treat ConstString/LDC as a (potentially always) throwing instruction,
+    // as opposed to the DEX backend where it's throwing only if its string is ill-formed.
+    // When ConstString is throwing we preserve its position which makes it show up in the
+    // the output Proguard map. That's why the reference CF map is different from the DEX one.
+    String mapping_ref = backend == Backend.CF ? "print-mapping-cf.ref" : "print-mapping.ref";
+    String refMapping =
+        new String(
+            Files.readAllBytes(Paths.get(EXAMPLES_DIR, "shaking1", mapping_ref)),
+            StandardCharsets.UTF_8);
     Assert.assertEquals(sorted(refMapping), sorted(actualMapping));
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
index 51baed2..9cf0a85 100644
--- a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/ForceProguardCompatibilityTest.java
@@ -23,7 +23,6 @@
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationParser;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
-import com.android.tools.r8.shaking.ProguardIdentifierNameStringRule;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.shaking.ProguardKeepRule;
 import com.android.tools.r8.shaking.ProguardKeepRuleType;
@@ -261,7 +260,7 @@
     assertTrue(inspector.clazz(getJavacGeneratedClassName(mainClass)).isPresent());
     forNameClasses.forEach(clazz -> {
       ClassSubject subject = inspector.clazz(getJavacGeneratedClassName(clazz));
-      assertEquals(forceProguardCompatibility, subject.isPresent());
+      assertTrue(subject.isPresent());
       assertEquals(subject.isPresent() && allowObfuscation, subject.isRenamed());
     });
 
@@ -273,8 +272,10 @@
     ProguardConfiguration configuration = parser.getConfigRawForTesting();
     if (forceProguardCompatibility) {
       List<ProguardConfigurationRule> rules = configuration.getRules();
-      assertEquals(3, rules.size());
-      Iterables.filter(rules, ProguardKeepRule.class).forEach(rule -> {
+      assertEquals(2, rules.size());
+      for (ProguardConfigurationRule r : rules) {
+        assertTrue(r instanceof ProguardKeepRule);
+        ProguardKeepRule rule = (ProguardKeepRule) r;
         assertEquals(ProguardKeepRuleType.KEEP, rule.getType());
         assertTrue(rule.getModifiers().allowsObfuscation);
         assertTrue(rule.getModifiers().allowsOptimization);
@@ -292,18 +293,7 @@
           assertEquals(1, memberRules.size());
           assertEquals(ProguardMemberType.INIT, memberRules.iterator().next().getRuleType());
         }
-      });
-      Iterables.filter(rules, ProguardIdentifierNameStringRule.class).forEach(rule -> {
-        List<ProguardMemberRule> memberRules = rule.getMemberRules();
-        ProguardClassNameList classNames = rule.getClassNames();
-        assertEquals(1, classNames.size());
-        DexType type = classNames.asSpecificDexTypes().get(0);
-        assertEquals("java.lang.Class", type.toSourceString());
-        assertEquals(1, memberRules.size());
-        ProguardMemberRule memberRule = memberRules.iterator().next();
-        assertEquals(ProguardMemberType.METHOD, memberRule.getRuleType());
-        assertEquals("forName", memberRule.getName().toString());
-      });
+      }
     } else {
       assertEquals(0, configuration.getRules().size());
     }
@@ -367,11 +357,11 @@
     assertTrue(classSubject.isPresent());
     assertEquals(allowObfuscation, classSubject.isRenamed());
     FieldSubject foo = classSubject.field("java.lang.String", "foo");
-    assertEquals(forceProguardCompatibility, foo.isPresent());
+    assertTrue(foo.isPresent());
     assertEquals(foo.isPresent() && allowObfuscation, foo.isRenamed());
     MethodSubject bar =
         classSubject.method("java.lang.String", "bar", ImmutableList.of("java.lang.String"));
-    assertEquals(forceProguardCompatibility, bar.isPresent());
+    assertTrue(bar.isPresent());
     assertEquals(bar.isPresent() && allowObfuscation, bar.isRenamed());
 
     // Check the Proguard compatibility rules generated.
@@ -382,8 +372,10 @@
     ProguardConfiguration configuration = parser.getConfigRawForTesting();
     if (forceProguardCompatibility) {
       List<ProguardConfigurationRule> rules = configuration.getRules();
-      assertEquals(4, rules.size());
-      Iterables.filter(rules, ProguardKeepRule.class).forEach(rule -> {
+      assertEquals(2, rules.size());
+      for (ProguardConfigurationRule r : rules) {
+        assertTrue(r instanceof ProguardKeepRule);
+        ProguardKeepRule rule = (ProguardKeepRule) r;
         assertEquals(ProguardKeepRuleType.KEEP_CLASS_MEMBERS, rule.getType());
         assertTrue(rule.getModifiers().allowsObfuscation);
         assertTrue(rule.getModifiers().allowsOptimization);
@@ -400,18 +392,7 @@
           assertEquals(ProguardMemberType.METHOD, memberRule.getRuleType());
           assertTrue(memberRule.getName().matches("bar"));
         }
-      });
-      Iterables.filter(rules, ProguardIdentifierNameStringRule.class).forEach(rule -> {
-        List<ProguardMemberRule> memberRules = rule.getMemberRules();
-        ProguardClassNameList classNames = rule.getClassNames();
-        assertEquals(1, classNames.size());
-        DexType type = classNames.asSpecificDexTypes().get(0);
-        assertEquals("java.lang.Class", type.toSourceString());
-        assertEquals(1, memberRules.size());
-        ProguardMemberRule memberRule = memberRules.iterator().next();
-        assertEquals(ProguardMemberType.METHOD, memberRule.getRuleType());
-        assertTrue(memberRule.getName().toString().startsWith("getDeclared"));
-      });
+      }
     } else {
       assertEquals(0, configuration.getRules().size());
     }
@@ -480,13 +461,13 @@
     assertTrue(classSubject.isPresent());
     assertEquals(allowObfuscation, classSubject.isRenamed());
     FieldSubject f = classSubject.field("int", "intField");
-    assertEquals(forceProguardCompatibility, f.isPresent());
+    assertTrue(f.isPresent());
     assertEquals(f.isPresent() && allowObfuscation, f.isRenamed());
     f = classSubject.field("long", "longField");
-    assertEquals(forceProguardCompatibility, f.isPresent());
+    assertTrue(f.isPresent());
     assertEquals(f.isPresent() && allowObfuscation, f.isRenamed());
     f = classSubject.field("java.lang.Object", "objField");
-    assertEquals(forceProguardCompatibility, f.isPresent());
+    assertTrue(f.isPresent());
     assertEquals(f.isPresent() && allowObfuscation, f.isRenamed());
 
     // Check the Proguard compatibility rules generated.
@@ -497,9 +478,11 @@
     ProguardConfiguration configuration = parser.getConfigRawForTesting();
     if (forceProguardCompatibility) {
       List<ProguardConfigurationRule> rules = configuration.getRules();
-      assertEquals(6, rules.size());
+      assertEquals(3, rules.size());
       Object2BooleanMap<String> keptFields = new Object2BooleanArrayMap<>();
-      Iterables.filter(rules, ProguardKeepRule.class).forEach(rule -> {
+      for (ProguardConfigurationRule r : rules) {
+        assertTrue(r instanceof ProguardKeepRule);
+        ProguardKeepRule rule = (ProguardKeepRule) r;
         assertEquals(ProguardKeepRuleType.KEEP_CLASS_MEMBERS, rule.getType());
         assertTrue(rule.getModifiers().allowsObfuscation);
         assertTrue(rule.getModifiers().allowsOptimization);
@@ -512,23 +495,11 @@
         ProguardMemberRule memberRule = memberRules.iterator().next();
         assertEquals(ProguardMemberType.FIELD, memberRule.getRuleType());
         keptFields.put(memberRule.getName().toString(), true);
-      });
+      }
       assertEquals(3, keptFields.size());
       assertTrue(keptFields.containsKey("intField"));
       assertTrue(keptFields.containsKey("longField"));
       assertTrue(keptFields.containsKey("objField"));
-      Iterables.filter(rules, ProguardIdentifierNameStringRule.class).forEach(rule -> {
-        List<ProguardMemberRule> memberRules = rule.getMemberRules();
-        ProguardClassNameList classNames = rule.getClassNames();
-        assertEquals(1, classNames.size());
-        DexType type = classNames.asSpecificDexTypes().get(0);
-        assertTrue(type.toSourceString().startsWith("java.util.concurrent.atomic.Atomic"));
-        assertTrue(type.toSourceString().endsWith("FieldUpdater"));
-        assertEquals(1, memberRules.size());
-        ProguardMemberRule memberRule = memberRules.iterator().next();
-        assertEquals(ProguardMemberType.METHOD, memberRule.getRuleType());
-        assertEquals("newUpdater", memberRule.getName().toString());
-      });
     } else {
       assertEquals(0, configuration.getRules().size());
     }
diff --git a/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java b/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java
index b22dc1e..b3e6367 100644
--- a/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/proxy/ProxiesTest.java
@@ -79,9 +79,11 @@
     }
     AndroidApp app = ToolHelper.runR8(builder.build(), o -> o.enableDevirtualization = false);
     inspection.accept(new CodeInspector(app, o -> o.enableCfFrontend = true));
-    assertEquals(
-        expectedResult,
-        backend == Backend.DEX ? runOnArt(app, mainClass) : runOnJava(app, mainClass));
+    String result = backend == Backend.DEX ? runOnArt(app, mainClass) : runOnJava(app, mainClass);
+    if (ToolHelper.isWindows()) {
+      result = result.replace(System.lineSeparator(), "\n");
+    }
+    assertEquals(expectedResult, result);
   }
 
   private int countInstructionInX(CodeInspector inspector, Predicate<InstructionSubject> invoke) {
diff --git a/src/test/kotlinR8TestResources/class_staticizer/main.kt b/src/test/kotlinR8TestResources/class_staticizer/main.kt
new file mode 100644
index 0000000..01915c4
--- /dev/null
+++ b/src/test/kotlinR8TestResources/class_staticizer/main.kt
@@ -0,0 +1,43 @@
+// Copyright (c) 2018, 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 class_staticizer
+
+private var COUNT = 0
+
+fun next() = "${COUNT++}".padStart(3, '0')
+
+fun main(args: Array<String>) {
+    println(Regular.foo)
+    println(Regular.bar)
+    println(Regular.blah(next()))
+    println(Derived.foo)
+    println(Derived.bar)
+    println(Derived.blah(next()))
+    println(Util.foo)
+    println(Util.bar)
+    println(Util.blah(next()))
+}
+
+open class Regular {
+    companion object {
+        var foo: String = "Regular::CC::foo[${next()}]"
+        var bar: String = blah(next())
+        fun blah(p: String) = "Regular::CC::blah($p)[${next()}]"
+    }
+}
+
+open class Derived : Regular() {
+    companion object {
+        var foo: String = "Derived::CC::foo[${next()}]"
+        var bar: String = blah(next())
+        fun blah(p: String) = "Derived::CC::blah($p)[${next()}]"
+    }
+}
+
+object Util {
+    var foo: String = "Util::foo[${next()}]"
+    var bar: String = Regular.blah(next()) + Derived.blah(next())
+    fun blah(p: String) = "Util::blah($p)[${next()}]"
+}