Merge "Rewrite instance.getClass() to const-class if possible."
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index 3c2faea..9c0cd21 100644
--- a/src/main/java/com/android/tools/r8/Version.java
+++ b/src/main/java/com/android/tools/r8/Version.java
@@ -11,7 +11,7 @@
 
   // This field is accessed from release scripts using simple pattern matching.
   // Therefore, changing this field could break our release scripts.
-  public static final String LABEL = "1.4.6-dev";
+  public static final String LABEL = "1.4.9-dev";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java
index 1132193..ce98387 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java
@@ -402,10 +402,12 @@
   public boolean isPreciseType() {
     return isArrayType()
         || isClassType()
+        || isNull()
         || isInt()
         || isFloat()
         || isLong()
-        || isDouble();
+        || isDouble()
+        || isBottom();
   }
 
   public boolean isNull() {
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 89a5f78..5d7b1f8 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -676,8 +676,10 @@
   }
 
   private void removeCatchHandlerWithGuard(DexType guard) {
-    int successorIndex = catchHandlers.getGuards().indexOf(guard);
-    if (successorIndex >= 0) {
+    int guardIndex = catchHandlers.getGuards().indexOf(guard);
+    if (guardIndex >= 0) {
+      int successorIndex = catchHandlers.getAllTargets().get(guardIndex);
+      assert successorIndex >= 0;
       catchHandlers = catchHandlers.removeGuard(guard);
       if (getCatchHandlers().getAllTargets().stream()
           .noneMatch(target -> target == successors.get(successorIndex))) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/CatchHandlers.java b/src/main/java/com/android/tools/r8/ir/code/CatchHandlers.java
index a85e826..3f6a2ef 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CatchHandlers.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CatchHandlers.java
@@ -14,6 +14,17 @@
 
 public class CatchHandlers<T> {
 
+  public static class CatchHandler<T> {
+
+    public final DexType guard;
+    public final T target;
+
+    public CatchHandler(DexType guard, T target) {
+      this.guard = guard;
+      this.target = target;
+    }
+  }
+
   private final List<DexType> guards;
   private final List<T> targets;
   private Set<T> uniqueTargets;
diff --git a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
index 75a95cd..c68baf9 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
@@ -124,10 +124,7 @@
 
     TypeLatticeElement inType = object().getTypeLattice();
 
-    // TODO(b/72693244): There should never be a value with imprecise type lattice.
-    if (!inType.isPreciseType()) {
-      return true;
-    }
+    assert inType.isPreciseType();
 
     TypeLatticeElement outType = outValue().getTypeLattice();
     TypeLatticeElement castType =
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
index 95b3f45..1ea6ab0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
@@ -3,8 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
 import com.android.tools.r8.ir.optimize.InliningOracle;
 import java.util.List;
@@ -38,4 +41,11 @@
   public boolean throwsNpeIfValueIsNull(Value value) {
     return getReceiver() == value;
   }
+
+  @Override
+  public boolean verifyTypes(AppInfo appInfo, GraphLense graphLense) {
+    TypeLatticeElement receiverType = getReceiver().getTypeLattice();
+    assert receiverType.isPreciseType();
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index de05fcc..1ee4dbd 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -44,7 +44,6 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.Xor;
 import com.android.tools.r8.ir.optimize.CodeRewriter;
-import com.android.tools.r8.ir.optimize.DeadCodeRemover;
 import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
 import com.android.tools.r8.ir.optimize.peepholes.BasicBlockMuncher;
 import com.android.tools.r8.utils.InternalOptions;
@@ -145,7 +144,7 @@
     typeVerificationHelper = new TypeVerificationHelper(code, factory, appInfo);
     typeVerificationHelper.computeVerificationTypes();
     splitExceptionalBlocks();
-    new DeadCodeRemover(appInfo, code, rewriter, graphLense, options).run();
+    rewriter.converter.deadCodeRemover.run(code);
     rewriteNots();
     LoadStoreHelper loadStoreHelper = new LoadStoreHelper(code, typeVerificationHelper, appInfo);
     loadStoreHelper.insertLoadsAndStores();
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 84fb0bd..a41ef09 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
@@ -122,6 +122,8 @@
   private final StringOptimizer stringOptimizer;
   private final UninstantiatedTypeOptimization uninstantiatedTypeOptimization;
 
+  final DeadCodeRemover deadCodeRemover;
+
   public final boolean enableWholeProgramOptimizations;
 
   private final OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
@@ -204,6 +206,9 @@
             : null;
     this.classStaticizer = options.enableClassStaticizer && appInfo.hasLiveness()
         ? new ClassStaticizer(appInfo.withLiveness(), this) : null;
+    this.deadCodeRemover =
+        new DeadCodeRemover(
+            appInfo, codeRewriter, graphLense(), options, enableWholeProgramOptimizations);
   }
 
   public GraphLense graphLense() {
@@ -577,7 +582,7 @@
                 // StringBuilder/StringBuffer method invocations, and removeDeadCode() to remove
                 // unused out-values.
                 codeRewriter.rewriteMoveResult(code);
-                new DeadCodeRemover(appInfo, code, codeRewriter, graphLense(), options).run();
+                deadCodeRemover.run(code);
                 consumer.accept(code, method);
                 return null;
               }));
@@ -846,6 +851,7 @@
       stringOptimizer.removeValueOfIfTrivial(code, appInfo);
     }
     if (devirtualizer != null) {
+      assert code.verifyTypes(appInfo, graphLense());
       devirtualizer.devirtualizeInvokeInterface(code, method.method.getHolder());
     }
     if (uninstantiatedTypeOptimization != null) {
@@ -853,9 +859,9 @@
     }
 
     assert code.verifyTypes(appInfo, graphLense());
-
     codeRewriter.removeTrivialCheckCastAndInstanceOfInstructions(
         code, enableWholeProgramOptimizations);
+
     codeRewriter.rewriteLongCompareAndRequireNonNull(code, options);
     codeRewriter.commonSubexpressionElimination(code);
     codeRewriter.simplifyArrayConstruction(code);
@@ -885,7 +891,7 @@
     // Dead code removal. Performed after simplifications to remove code that becomes dead
     // as a result of those simplifications. The following optimizations could reveal more
     // dead code which is removed right before register allocation in performRegisterAllocation.
-    new DeadCodeRemover(appInfo, code, codeRewriter, graphLense(), options).run();
+    deadCodeRemover.run(code);
     assert code.isConsistentSSA();
 
     if (options.enableDesugaring && enableTryWithResourcesDesugaring()) {
@@ -1108,7 +1114,7 @@
   private RegisterAllocator performRegisterAllocation(IRCode code, DexEncodedMethod method) {
     // Always perform dead code elimination before register allocation. The register allocator
     // does not allow dead code (to make sure that we do not waste registers for unneeded values).
-    new DeadCodeRemover(appInfo, code, codeRewriter, graphLense(), options).run();
+    deadCodeRemover.run(code);
     materializeInstructionBeforeLongOperationsWorkaround(code);
     workaroundForwardingInitializerBug(code);
     LinearScanRegisterAllocator registerAllocator = new LinearScanRegisterAllocator(code, options);
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 5a7e6a5..df3f4ca 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
@@ -1808,11 +1808,6 @@
     }
 
     TypeLatticeElement inTypeLattice = inValue.getTypeLattice();
-    // TODO(b/72693244): Soon, there won't be a value with imprecise type at this point.
-    if (!inTypeLattice.isPreciseType() && !inTypeLattice.isNull()) {
-      return false;
-    }
-
     TypeLatticeElement outTypeLattice = outValue.getTypeLattice();
     TypeLatticeElement castTypeLattice =
         TypeLatticeElement.fromDexType(castType, inTypeLattice.isNullable(), appInfo);
@@ -1866,11 +1861,6 @@
 
     Value inValue = instanceOf.value();
     TypeLatticeElement inType = inValue.getTypeLattice();
-    // TODO(b/72693244): Soon, there won't be a value with imprecise type at this point.
-    if (!inType.isPreciseType() && !inType.isNull()) {
-      return false;
-    }
-
     TypeLatticeElement instanceOfType =
         TypeLatticeElement.fromDexType(instanceOf.type(), inType.isNullable(), appInfo);
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
index 7e76dc0..91ac562 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
@@ -5,10 +5,12 @@
 
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.CatchHandlers;
+import com.android.tools.r8.ir.code.CatchHandlers.CatchHandler;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -16,6 +18,8 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.Queue;
@@ -23,26 +27,26 @@
 public class DeadCodeRemover {
 
   private final AppInfo appInfo;
-  private final IRCode code;
   private final CodeRewriter codeRewriter;
   private final GraphLense graphLense;
   private final InternalOptions options;
+  private final boolean enableWholeProgramOptimizations;
 
   public DeadCodeRemover(
       AppInfo appInfo,
-      IRCode code,
       CodeRewriter codeRewriter,
       GraphLense graphLense,
-      InternalOptions options) {
+      InternalOptions options,
+      boolean enableWholeProgramOptimizations) {
     this.appInfo = appInfo;
-    this.code = code;
     this.codeRewriter = codeRewriter;
     this.graphLense = graphLense;
     this.options = options;
+    this.enableWholeProgramOptimizations = enableWholeProgramOptimizations;
   }
 
-  public void run() {
-    removeUnneededCatchHandlers();
+  public void run(IRCode code) {
+    removeUnneededCatchHandlers(code);
     Queue<BasicBlock> worklist = new LinkedList<>();
     worklist.addAll(code.blocks);
     for (BasicBlock block = worklist.poll(); block != null; block = worklist.poll()) {
@@ -51,7 +55,7 @@
     }
     // We may encounter unneeded catch handlers again, e.g., if a dead instruction (due to
     // const-string canonicalization for example) is the only throwing instruction in a block.
-    removeUnneededCatchHandlers();
+    removeUnneededCatchHandlers(code);
     assert code.isConsistentSSA();
     codeRewriter.rewriteMoveResult(code);
   }
@@ -70,8 +74,7 @@
   }
 
   // Add all blocks from where the in/debug-values to the instruction originates.
-  private static void updateWorklist(
-      Queue<BasicBlock> worklist, Instruction instruction) {
+  private static void updateWorklist(Queue<BasicBlock> worklist, Instruction instruction) {
     for (Value inValue : instruction.inValues()) {
       updateWorklist(worklist, inValue);
     }
@@ -80,8 +83,8 @@
     }
   }
 
-  private static void removeDeadPhis(Queue<BasicBlock> worklist, BasicBlock block,
-      InternalOptions options) {
+  private static void removeDeadPhis(
+      Queue<BasicBlock> worklist, BasicBlock block, InternalOptions options) {
     Iterator<Phi> phiIt = block.getPhis().iterator();
     while (phiIt.hasNext()) {
       Phi phi = phiIt.next();
@@ -124,24 +127,18 @@
     }
   }
 
-  private void removeUnneededCatchHandlers() {
+  private void removeUnneededCatchHandlers(IRCode code) {
     boolean mayHaveIntroducedUnreachableBlocks = false;
     for (BasicBlock block : code.blocks) {
       if (block.hasCatchHandlers()) {
         if (block.canThrow()) {
-          if (appInfo.hasLiveness()) {
-            AppInfoWithLiveness appInfoWithLiveness = appInfo.withLiveness();
-            CatchHandlers<BasicBlock> catchHandlers = block.getCatchHandlers();
-            for (int i = 0; i < catchHandlers.size(); ++i) {
-              DexType guard = catchHandlers.getGuards().get(i);
-              BasicBlock target = catchHandlers.getAllTargets().get(i);
-              DexClass clazz = appInfo.definitionFor(guard);
-              if (clazz != null
-                  && clazz.isProgramClass()
-                  && !appInfoWithLiveness.isInstantiatedDirectlyOrIndirectly(guard)) {
-                target.unlinkCatchHandlerForGuard(guard);
-                mayHaveIntroducedUnreachableBlocks = true;
+          if (enableWholeProgramOptimizations) {
+            Collection<CatchHandler<BasicBlock>> deadCatchHandlers = getDeadCatchHandlers(block);
+            if (!deadCatchHandlers.isEmpty()) {
+              for (CatchHandler<BasicBlock> catchHandler : deadCatchHandlers) {
+                catchHandler.target.unlinkCatchHandlerForGuard(catchHandler.guard);
               }
+              mayHaveIntroducedUnreachableBlocks = true;
             }
           }
         } else {
@@ -158,4 +155,48 @@
     }
     assert code.isConsistentGraph();
   }
+
+  /**
+   * Returns the catch handlers of the given block that are dead, if any.
+   */
+  private Collection<CatchHandler<BasicBlock>> getDeadCatchHandlers(BasicBlock block) {
+    AppInfoWithLiveness appInfoWithLiveness = appInfo.withLiveness();
+    ImmutableList.Builder<CatchHandler<BasicBlock>> builder = ImmutableList.builder();
+    CatchHandlers<BasicBlock> catchHandlers = block.getCatchHandlers();
+    for (int i = 0; i < catchHandlers.size(); ++i) {
+      DexType guard = catchHandlers.getGuards().get(i);
+      BasicBlock target = catchHandlers.getAllTargets().get(i);
+      if (guard == DexItemFactory.catchAllType) {
+        continue;
+      }
+
+      // We can exploit subtyping information to eliminate a catch handler if the guard is
+      // subsumed by a previous guard.
+      boolean isSubsumedByPreviousGuard = false;
+      for (int j = 0; j < i; ++j) {
+        DexType previousGuard = catchHandlers.getGuards().get(j);
+        if (guard.isSubtypeOf(previousGuard, appInfo)) {
+          isSubsumedByPreviousGuard = true;
+          break;
+        }
+      }
+      if (isSubsumedByPreviousGuard) {
+        builder.add(new CatchHandler<>(guard, target));
+        continue;
+      }
+
+      // We can exploit that a catch handler must be dead if its guard is never instantiated
+      // directly or indirectly.
+      if (appInfoWithLiveness != null && options.enableUninstantiatedTypeOptimization) {
+        DexClass clazz = appInfo.definitionFor(guard);
+        if (clazz != null
+            && clazz.isProgramClass()
+            && !appInfoWithLiveness.isInstantiatedDirectlyOrIndirectly(guard)) {
+          builder.add(new CatchHandler<>(guard, target));
+          continue;
+        }
+      }
+    }
+    return builder.build();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
index 2f27e83..ba5f1b3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
@@ -116,9 +116,7 @@
           // Avoid adding trivial cast and up-cast.
           // We should not use strictlyLessThan(castType, receiverType), which detects downcast,
           // due to side-casts, e.g., A (unused) < I, B < I, and cast from A to B.
-          // TODO(b/72693244): Soon, there won't be a value with imprecise type at this point.
-          if (!receiverTypeLattice.isPreciseType()
-              || !receiverTypeLattice.lessThanOrEqual(castTypeLattice, appInfo)) {
+          if (!receiverTypeLattice.lessThanOrEqual(castTypeLattice, appInfo)) {
             Value newReceiver = null;
             // If this value is ever downcast'ed to the same holder type before, and that casted
             // value is safely accessible, i.e., the current line is dominated by that cast, use it.
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index 19eb127..e96497b 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -127,7 +127,7 @@
             // Members mentioned at -keepclassmembers always depend on their holder.
             preconditionSupplier = ImmutableMap.of(definition -> true, clazz);
             markMatchingVisibleMethods(clazz, memberKeepRules, rule, preconditionSupplier);
-            markMatchingFields(clazz, memberKeepRules, rule, preconditionSupplier);
+            markMatchingVisibleFields(clazz, memberKeepRules, rule, preconditionSupplier);
             break;
           }
           case KEEP_CLASSES_WITH_MEMBERS: {
@@ -150,7 +150,7 @@
               preconditionSupplier.put((definition -> true), null);
             }
             markMatchingVisibleMethods(clazz, memberKeepRules, rule, preconditionSupplier);
-            markMatchingFields(clazz, memberKeepRules, rule, preconditionSupplier);
+            markMatchingVisibleFields(clazz, memberKeepRules, rule, preconditionSupplier);
             break;
           }
           case CONDITIONAL:
@@ -170,10 +170,10 @@
           || rule instanceof ProguardKeepPackageNamesRule) {
         markClass(clazz, rule);
         markMatchingVisibleMethods(clazz, memberKeepRules, rule, null);
-        markMatchingFields(clazz, memberKeepRules, rule, null);
+        markMatchingVisibleFields(clazz, memberKeepRules, rule, null);
       } else if (rule instanceof ProguardAssumeNoSideEffectRule) {
         markMatchingVisibleMethods(clazz, memberKeepRules, rule, null);
-        markMatchingFields(clazz, memberKeepRules, rule, null);
+        markMatchingVisibleFields(clazz, memberKeepRules, rule, null);
       } else if (rule instanceof ClassMergingRule) {
         if (allRulesSatisfied(memberKeepRules, clazz)) {
           markClass(clazz, rule);
@@ -182,7 +182,7 @@
         markMatchingMethods(clazz, memberKeepRules, rule, null);
       } else if (rule instanceof ProguardAssumeValuesRule) {
         markMatchingVisibleMethods(clazz, memberKeepRules, rule, null);
-        markMatchingFields(clazz, memberKeepRules, rule, null);
+        markMatchingVisibleFields(clazz, memberKeepRules, rule, null);
       } else {
         assert rule instanceof ProguardIdentifierNameStringRule;
         markMatchingFields(clazz, memberKeepRules, rule, null);
@@ -460,6 +460,21 @@
     });
   }
 
+  private void markMatchingVisibleFields(
+      DexClass clazz,
+      Collection<ProguardMemberRule> memberKeepRules,
+      ProguardConfigurationRule rule,
+      Map<Predicate<DexDefinition>, DexDefinition> preconditionSupplier) {
+    while (clazz != null) {
+      clazz.forEachField(
+          field -> {
+            DexDefinition precondition = testAndGetPrecondition(field, preconditionSupplier);
+            markField(field, memberKeepRules, rule, precondition);
+          });
+      clazz = clazz.superType == null ? null : application.definitionFor(clazz.superType);
+    }
+  }
+
   private void markMatchingFields(
       DexClass clazz,
       Collection<ProguardMemberRule> memberKeepRules,
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/CatchHandlerRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/CatchHandlerRemovalTest.java
new file mode 100644
index 0000000..5e5a490
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/CatchHandlerRemovalTest.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.ir.optimize;
+
+import com.android.tools.r8.TestBase;
+import inlining.NeverInline;
+import java.util.Arrays;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class CatchHandlerRemovalTest extends TestBase {
+
+  private final Backend backend;
+
+  @Parameters(name = "Backend: {0}")
+  public static Collection<Backend> data() {
+    return Arrays.asList(Backend.values());
+  }
+
+  public CatchHandlerRemovalTest(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Test
+  public void test() throws Exception {
+    String expected = "In TestClass.method()";
+
+    testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expected);
+
+    testForR8(backend)
+        .addInnerClasses(CatchHandlerRemovalTest.class)
+        .addKeepMainRule(TestClass.class)
+        .enableInliningAnnotations()
+        .run(TestClass.class)
+        .assertSuccessWithOutput(expected)
+        .inspector();
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      // Instantiate ExceptionA and ExceptionB such that the uninstantiated type optimization
+      // cannot unlink the catch handlers guarded by these types.
+      new ExceptionA();
+      new ExceptionB();
+
+      // Since ExceptionC is never instantiated, the catch handler for it will be removed by the
+      // uninstantiated type optimization.
+      try {
+        method();
+      } catch (ExceptionA | ExceptionB | ExceptionC e) {
+        System.out.println("Caught exception");
+      }
+    }
+
+    @NeverInline
+    public static void method() {
+      System.out.print("In TestClass.method()");
+    }
+  }
+
+  static class ExceptionA extends RuntimeException {}
+
+  static class ExceptionB extends RuntimeException {}
+
+  static class ExceptionC extends RuntimeException {}
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/SubsumedCatchHandlerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/SubsumedCatchHandlerTest.java
new file mode 100644
index 0000000..e31325b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/SubsumedCatchHandlerTest.java
@@ -0,0 +1,120 @@
+// 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;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.ForceInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.cf.code.CfTryCatch;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexCode.TryHandler;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SubsumedCatchHandlerTest extends TestBase {
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      int exitCode = 0;
+      try {
+        exitCode = foo();
+      } catch (RuntimeException e) {
+        // This catch handler will be subsumed by the catch handler in foo() after inlining.
+        handleCaughtRuntimeException();
+      }
+      System.out.print(" -> " + exitCode);
+    }
+
+    @ForceInline
+    private static int foo() {
+      try {
+        bar();
+      } catch (Exception e) {
+        // No statements with side-effects here; otherwise this block will be guarded by the
+        // RuntimeException catch handler from main().
+        return 1;
+      }
+      return 0;
+    }
+
+    @NeverInline
+    private static void bar() {
+      System.out.print("In bar()");
+    }
+
+    @NeverInline
+    private static void handleCaughtRuntimeException() {
+      System.out.print("In handleCaughtRuntimeException()");
+    }
+  }
+
+  @Parameters(name = "Backend: {0}")
+  public static Backend[] data() {
+    return Backend.values();
+  }
+
+  private final Backend backend;
+
+  public SubsumedCatchHandlerTest(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Test
+  public void test() throws Exception {
+    String expected = "In bar() -> 0";
+
+    testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expected);
+
+    CodeInspector inspector =
+        testForR8(backend)
+            .addInnerClasses(SubsumedCatchHandlerTest.class)
+            .addKeepMainRule(TestClass.class)
+            .enableInliningAnnotations()
+            .run(TestClass.class)
+            .assertSuccessWithOutput(expected)
+            .inspector();
+
+    ClassSubject classSubject = inspector.clazz(TestClass.class);
+    assertThat(classSubject, isPresent());
+    assertThat(classSubject.method("void", "handleCaughtRuntimeException"), not(isPresent()));
+
+    MethodSubject mainMethodSubject = classSubject.mainMethod();
+    Code code = mainMethodSubject.getMethod().getCode();
+    if (code.isDexCode()) {
+      DexCode dexCode = code.asDexCode();
+      assertEquals(1, dexCode.handlers.length);
+
+      TryHandler handler = dexCode.handlers[0];
+      assertEquals(1, handler.pairs.length);
+
+      DexType guard = handler.pairs[0].type;
+      assertEquals("java.lang.Exception", guard.toSourceString());
+    } else {
+      assert code.isCfCode();
+      CfCode cfCode = code.asCfCode();
+      assertEquals(1, cfCode.getTryCatchRanges().size());
+
+      CfTryCatch handler = cfCode.getTryCatchRanges().get(0);
+      assertEquals(1, handler.guards.size());
+
+      DexType guard = handler.guards.get(0);
+      assertEquals("java.lang.Exception", guard.toSourceString());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java
index 3377aa3..2bf8b2a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -101,7 +102,7 @@
         "Another_shared",
         "2",
         "1",
-        "𐀀",
+        "𐀀", // Different output in Windows.
         "3"
     );
     main = StringLengthTestMain.class;
@@ -110,7 +111,9 @@
   @Before
   public void setUp() throws Exception {
     classes = ImmutableList.of(ForceInline.class, NeverInline.class, StringLengthTestMain.class);
-    testForJvm().addTestClasspath().run(main).assertSuccessWithOutput(javaOutput);
+    if (!ToolHelper.isWindows()) {
+      testForJvm().addTestClasspath().run(main).assertSuccessWithOutput(javaOutput);
+    }
   }
 
   private static boolean isStringLength(DexMethod method) {
@@ -156,15 +159,19 @@
     TestRunResult result = testForD8()
         .release()
         .addProgramClasses(classes)
-        .run(main)
-        .assertSuccessWithOutput(javaOutput);
+        .run(main);
+    if (!ToolHelper.isWindows()) {
+      result.assertSuccessWithOutput(javaOutput);
+    }
     test(result, 1, 4);
 
     result = testForD8()
         .debug()
         .addProgramClasses(classes)
-        .run(main)
-        .assertSuccessWithOutput(javaOutput);
+        .run(main);
+    if (!ToolHelper.isWindows()) {
+      result.assertSuccessWithOutput(javaOutput);
+    }
     test(result, 6, 0);
   }
 
@@ -175,8 +182,10 @@
         .enableProguardTestOptions()
         .enableInliningAnnotations()
         .addKeepMainRule(main)
-        .run(main)
-        .assertSuccessWithOutput(javaOutput);
+        .run(main);
+    if (!ToolHelper.isWindows()) {
+      result.assertSuccessWithOutput(javaOutput);
+    }
     // TODO we could remove const counting if it needs to be changed too frequently, since
     // the string length count is what we're interested in.
     test(result, 0, backend == Backend.DEX ? 5 : 6);
diff --git a/src/test/java/com/android/tools/r8/shaking/fields/FieldsTestBase.java b/src/test/java/com/android/tools/r8/shaking/fields/FieldsTestBase.java
new file mode 100644
index 0000000..e5f9799
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/fields/FieldsTestBase.java
@@ -0,0 +1,80 @@
+// 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.shaking.fields;
+
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.graph.invokesuper.Consumer;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.List;
+
+public abstract class FieldsTestBase extends TestBase {
+
+  public abstract Collection<Class<?>> getClasses();
+
+  public abstract Class<?> getMainClass();
+
+  public void testOnR8(List<String> keepRules, Consumer<CodeInspector> inspector, String expected)
+      throws Throwable {
+    testForR8(Backend.DEX)
+        .enableMergeAnnotations()
+        .addProgramClasses(getClasses())
+        .addKeepRules(keepRules)
+        .compile()
+        .inspect(inspector)
+        .run(getMainClass())
+        .assertSuccessWithOutput(expected);
+  }
+
+  public void testOnProguard(
+      List<String> keepRules, Consumer<CodeInspector> inspector, String expected) throws Throwable {
+    testForProguard()
+        .addProgramClasses(getClasses())
+        .addProgramClasses(NeverMerge.class)
+        .addKeepRules(keepRules)
+        .compile()
+        .inspect(inspector)
+        .run(getMainClass())
+        .assertSuccessWithOutput(expected);
+  }
+
+  public void runTest(List<String> keepRules, Consumer<CodeInspector> inspector, String expected)
+      throws Throwable {
+    testOnProguard(keepRules, inspector, expected);
+    testOnR8(keepRules, inspector, expected);
+  }
+
+  public void runTest(String keepRules, Consumer<CodeInspector> inspector, String expected)
+      throws Throwable {
+    runTest(
+        ImmutableList.of(
+            keepRules,
+            "-dontobfuscate",
+            "-keep class " + getMainClass().getTypeName() + " {",
+            "  public static void main(java.lang.String[]);",
+            "}"),
+        inspector,
+        expected);
+  }
+
+  public String allFieldsOutput() {
+    return StringUtils.lines("Super.f1 found", "Sub.f2 found", "SubSub.f3 found");
+  }
+
+  public String onlyF1Output() {
+    return StringUtils.lines("Super.f1 found", "Sub.f2 not found", "SubSub.f3 not found");
+  }
+
+  public String onlyF2Output() {
+    return StringUtils.lines("Super.f1 not found", "Sub.f2 found", "SubSub.f3 not found");
+  }
+
+  public String onlyF3Output() {
+    return StringUtils.lines("Super.f1 not found", "Sub.f2 not found", "SubSub.f3 found");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/fields/pblc/PublicFieldsTest.java b/src/test/java/com/android/tools/r8/shaking/fields/pblc/PublicFieldsTest.java
new file mode 100644
index 0000000..6b8048e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/fields/pblc/PublicFieldsTest.java
@@ -0,0 +1,137 @@
+// 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.shaking.fields.pblc;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.shaking.fields.FieldsTestBase;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+import java.util.Set;
+import org.junit.Test;
+
+@NeverMerge
+class Super {
+  public int f1;
+}
+
+@NeverMerge
+class Sub extends Super {
+  public int f2;
+}
+
+@NeverMerge
+class SubSub extends Sub {
+  public int f3;
+}
+
+class Main {
+
+  private static void printField(Class<?> clazz, String name) {
+    try {
+      clazz.getDeclaredField(name);
+      System.out.println(clazz.getSimpleName() + "." + name + " found");
+    } catch (NoSuchFieldException e) {
+      System.out.println(clazz.getSimpleName() + "." + name + " not found");
+    }
+  }
+
+  public static void main(String[] args) {
+    printField(Super.class, "f1");
+    printField(Sub.class, "f2");
+    printField(SubSub.class, "f3");
+  }
+}
+
+public class PublicFieldsTest extends FieldsTestBase {
+
+  @Override
+  public Collection<Class<?>> getClasses() {
+    return ImmutableSet.of(Super.class, Sub.class, SubSub.class, getMainClass());
+  }
+
+  @Override
+  public Class<?> getMainClass() {
+    return Main.class;
+  }
+
+  private void checkFields(CodeInspector inspector, Set<String> expected) {
+    ClassSubject superSubject = inspector.clazz(Super.class);
+    assertThat(superSubject, isPresent());
+    assertEquals(superSubject.field("int", "f1").isPresent(), expected.contains("f1"));
+    ClassSubject subSubject = inspector.clazz(Sub.class);
+    assertThat(subSubject, isPresent());
+    assertEquals(subSubject.field("int", "f2").isPresent(), expected.contains("f2"));
+    ClassSubject subSubSubject = inspector.clazz(SubSub.class);
+    assertThat(subSubSubject, isPresent());
+    assertEquals(subSubSubject.field("int", "f3").isPresent(), expected.contains("f3"));
+  }
+
+  private void checkAllFields(CodeInspector inspector) {
+    checkFields(inspector, ImmutableSet.of("f1", "f2", "f3"));
+  }
+
+  private void checkOnlyF1(CodeInspector inspector) {
+    checkFields(inspector, ImmutableSet.of("f1"));
+  }
+
+  private void checkOnlyF2(CodeInspector inspector) {
+    checkFields(inspector, ImmutableSet.of("f2"));
+  }
+
+  private void checkOnlyF3(CodeInspector inspector) {
+    checkFields(inspector, ImmutableSet.of("f3"));
+  }
+
+  @Test
+  public void testKeepAllFieldsWithWildcard() throws Throwable {
+    runTest("-keep class **.SubSub { *; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepAllFieldsWithNameWildcard() throws Throwable {
+    runTest("-keep class **.SubSub { int f*; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepAllFieldsWithFields() throws Throwable {
+    runTest("-keep class **.SubSub { <fields>; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepF1() throws Throwable {
+    runTest("-keep class **.SubSub { int f1; }", this::checkOnlyF1, onlyF1Output());
+  }
+
+  @Test
+  public void testKeepF2() throws Throwable {
+    runTest("-keep class **.SubSub { int f2; }", this::checkOnlyF2, onlyF2Output());
+  }
+
+  @Test
+  public void testKeepF3() throws Throwable {
+    runTest("-keep class **.SubSub { int f3; }", this::checkOnlyF3, onlyF3Output());
+  }
+
+  @Test
+  public void testKeepF1WithExtends() throws Throwable {
+    runTest("-keep class * extends **.Sub { int f1; }", this::checkOnlyF1, onlyF1Output());
+  }
+
+  @Test
+  public void testKeepF2WithExtends() throws Throwable {
+    runTest("-keep class * extends **.Sub { int f2; }", this::checkOnlyF2, onlyF2Output());
+  }
+
+  @Test
+  public void testKeepF3WithExtends() throws Throwable {
+    runTest("-keep class * extends **.Sub { int f3; }", this::checkOnlyF3, onlyF3Output());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/fields/pckg/PackageFieldsTest.java b/src/test/java/com/android/tools/r8/shaking/fields/pckg/PackageFieldsTest.java
new file mode 100644
index 0000000..00b4ead
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/fields/pckg/PackageFieldsTest.java
@@ -0,0 +1,134 @@
+// 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.shaking.fields.pckg;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.shaking.fields.FieldsTestBase;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+import java.util.Set;
+import org.junit.Test;
+
+@NeverMerge
+class Super {
+  int f1;
+}
+
+@NeverMerge
+class Sub extends Super {
+  int f2;
+}
+
+@NeverMerge
+class SubSub extends Sub {
+  int f3;
+}
+
+class Main {
+
+  private static void printField(Class<?> clazz, String name) {
+    try {
+      clazz.getDeclaredField(name);
+      System.out.println(clazz.getSimpleName() + "." + name + " found");
+    } catch (NoSuchFieldException e) {
+      System.out.println(clazz.getSimpleName() + "." + name + " not found");
+    }
+  }
+
+  public static void main(String[] args) {
+    printField(Super.class, "f1");
+    printField(Sub.class, "f2");
+    printField(SubSub.class, "f3");
+  }
+}
+
+public class PackageFieldsTest extends FieldsTestBase {
+  public Collection<Class<?>> getClasses() {
+    return ImmutableSet.of(Super.class, Sub.class, SubSub.class, getMainClass());
+  }
+
+  public Class<?> getMainClass() {
+    return Main.class;
+  }
+
+  private void checkFields(CodeInspector inspector, Set<String> expected) {
+    ClassSubject superSubject = inspector.clazz(Super.class);
+    assertThat(superSubject, isPresent());
+    assertEquals(superSubject.field("int", "f1").isPresent(), expected.contains("f1"));
+    ClassSubject subSubject = inspector.clazz(Sub.class);
+    assertThat(subSubject, isPresent());
+    assertEquals(subSubject.field("int", "f2").isPresent(), expected.contains("f2"));
+    ClassSubject subSubSubject = inspector.clazz(SubSub.class);
+    assertThat(subSubSubject, isPresent());
+    assertEquals(subSubSubject.field("int", "f3").isPresent(), expected.contains("f3"));
+  }
+
+  private void checkAllFields(CodeInspector inspector) {
+    checkFields(inspector, ImmutableSet.of("f1", "f2", "f3"));
+  }
+
+  private void checkOnlyF1(CodeInspector inspector) {
+    checkFields(inspector, ImmutableSet.of("f1"));
+  }
+
+  private void checkOnlyF2(CodeInspector inspector) {
+    checkFields(inspector, ImmutableSet.of("f2"));
+  }
+
+  private void checkOnlyF3(CodeInspector inspector) {
+    checkFields(inspector, ImmutableSet.of("f3"));
+  }
+
+  @Test
+  public void testKeepAllFieldsWithWildcard() throws Throwable {
+    runTest("-keep class **.SubSub { *; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepAllFieldsWithNameWildcard() throws Throwable {
+    runTest("-keep class **.SubSub { int f*; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepAllFieldsWithFields() throws Throwable {
+    runTest("-keep class **.SubSub { <fields>; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepF1() throws Throwable {
+    runTest("-keep class **.SubSub { int f1; }", this::checkOnlyF1, onlyF1Output());
+  }
+
+  @Test
+  public void testKeepF2() throws Throwable {
+    runTest("-keep class **.SubSub { int f2; }", this::checkOnlyF2, onlyF2Output());
+  }
+
+  @Test
+  public void testKeepF3() throws Throwable {
+    runTest("-keep class **.SubSub { int f3; }", this::checkOnlyF3, onlyF3Output());
+  }
+
+  @Test
+  public void testKeepF1WithExtends() throws Throwable {
+    runTest("-keep class * extends **.Sub { int f1; }", this::checkOnlyF1, onlyF1Output());
+  }
+
+  @Test
+  public void testKeepF2WithExtends() throws Throwable {
+    runTest("-keep class * extends **.Sub { int f2; }", this::checkOnlyF2, onlyF2Output());
+  }
+
+  @Test
+  public void testKeepF3WithExtends() throws Throwable {
+    runTest("-keep class * extends **.Sub { int f3; }", this::checkOnlyF3, onlyF3Output());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/fields/prvt/PrivateFieldsTest.java b/src/test/java/com/android/tools/r8/shaking/fields/prvt/PrivateFieldsTest.java
new file mode 100644
index 0000000..28cf204
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/fields/prvt/PrivateFieldsTest.java
@@ -0,0 +1,135 @@
+// 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.shaking.fields.prvt;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.shaking.fields.FieldsTestBase;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+import java.util.Set;
+import org.junit.Test;
+
+@NeverMerge
+class Super {
+  private int f1;
+}
+
+@NeverMerge
+class Sub extends Super {
+  private int f2;
+}
+
+@NeverMerge
+class SubSub extends Sub {
+  private int f3;
+}
+
+class Main {
+
+  private static void printField(Class<?> clazz, String name) {
+    try {
+      clazz.getDeclaredField(name);
+      System.out.println(clazz.getSimpleName() + "." + name + " found");
+    } catch (NoSuchFieldException e) {
+      System.out.println(clazz.getSimpleName() + "." + name + " not found");
+    }
+  }
+
+  public static void main(String[] args) {
+    printField(Super.class, "f1");
+    printField(Sub.class, "f2");
+    printField(SubSub.class, "f3");
+  }
+}
+
+public class PrivateFieldsTest extends FieldsTestBase {
+
+  public Collection<Class<?>> getClasses() {
+    return ImmutableSet.of(Super.class, Sub.class, SubSub.class, getMainClass());
+  }
+
+  public Class<?> getMainClass() {
+    return Main.class;
+  }
+
+  private void checkFields(CodeInspector inspector, Set<String> expected) {
+    ClassSubject superSubject = inspector.clazz(Super.class);
+    assertThat(superSubject, isPresent());
+    assertEquals(superSubject.field("int", "f1").isPresent(), expected.contains("f1"));
+    ClassSubject subSubject = inspector.clazz(Sub.class);
+    assertThat(subSubject, isPresent());
+    assertEquals(subSubject.field("int", "f2").isPresent(), expected.contains("f2"));
+    ClassSubject subSubSubject = inspector.clazz(SubSub.class);
+    assertThat(subSubSubject, isPresent());
+    assertEquals(subSubSubject.field("int", "f3").isPresent(), expected.contains("f3"));
+  }
+
+  private void checkAllFields(CodeInspector inspector) {
+    checkFields(inspector, ImmutableSet.of("f1", "f2", "f3"));
+  }
+
+  private void checkOnlyF1(CodeInspector inspector) {
+    checkFields(inspector, ImmutableSet.of("f1"));
+  }
+
+  private void checkOnlyF2(CodeInspector inspector) {
+    checkFields(inspector, ImmutableSet.of("f2"));
+  }
+
+  private void checkOnlyF3(CodeInspector inspector) {
+    checkFields(inspector, ImmutableSet.of("f3"));
+  }
+
+  @Test
+  public void testKeepAllFieldsWithWildcard() throws Throwable {
+    runTest("-keep class **.SubSub { *; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepAllFieldsWithNameWildcard() throws Throwable {
+    runTest("-keep class **.SubSub { int f*; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepAllFieldsWithFields() throws Throwable {
+    runTest("-keep class **.SubSub { <fields>; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepF1() throws Throwable {
+    runTest("-keep class **.SubSub { int f1; }", this::checkOnlyF1, onlyF1Output());
+  }
+
+  @Test
+  public void testKeepF2() throws Throwable {
+    runTest("-keep class **.SubSub { int f2; }", this::checkOnlyF2, onlyF2Output());
+  }
+
+  @Test
+  public void testKeepF3() throws Throwable {
+    runTest("-keep class **.SubSub { int f3; }", this::checkOnlyF3, onlyF3Output());
+  }
+
+  @Test
+  public void testKeepF1WithExtends() throws Throwable {
+    runTest("-keep class * extends **.Sub { int f1; }", this::checkOnlyF1, onlyF1Output());
+  }
+
+  @Test
+  public void testKeepF2WithExtends() throws Throwable {
+    runTest("-keep class * extends **.Sub { int f2; }", this::checkOnlyF2, onlyF2Output());
+  }
+
+  @Test
+  public void testKeepF3WithExtends() throws Throwable {
+    runTest("-keep class * extends **.Sub { int f3; }", this::checkOnlyF3, onlyF3Output());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/fields/shadow/ShadowFieldsTest.java b/src/test/java/com/android/tools/r8/shaking/fields/shadow/ShadowFieldsTest.java
new file mode 100644
index 0000000..f8f403d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/fields/shadow/ShadowFieldsTest.java
@@ -0,0 +1,105 @@
+// 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.shaking.fields.shadow;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.shaking.fields.FieldsTestBase;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+import org.junit.Test;
+
+@NeverMerge
+class Super {
+  public int f1;
+}
+
+@NeverMerge
+class Sub extends Super {
+  public int f1;
+}
+
+@NeverMerge
+class SubSub extends Sub {
+  public int f1;
+}
+
+class Main {
+
+  private static void printField(Class<?> clazz, String name) {
+    try {
+      clazz.getDeclaredField(name);
+      System.out.println(clazz.getSimpleName() + "." + name + " found");
+    } catch (NoSuchFieldException e) {
+      System.out.println(clazz.getSimpleName() + "." + name + " not found");
+    }
+  }
+
+  public static void main(String[] args) {
+    printField(Super.class, "f1");
+    printField(Sub.class, "f1");
+    printField(SubSub.class, "f1");
+  }
+}
+
+public class ShadowFieldsTest extends FieldsTestBase {
+
+  @Override
+  public Collection<Class<?>> getClasses() {
+    return ImmutableSet.of(Super.class, Sub.class, SubSub.class, getMainClass());
+  }
+
+  @Override
+  public Class<?> getMainClass() {
+    return Main.class;
+  }
+
+  private void checkAllFields(CodeInspector inspector) {
+    ClassSubject superSubject = inspector.clazz(Super.class);
+    assertThat(superSubject, isPresent());
+    assertThat(superSubject.field("int", "f1"), isPresent());
+    ClassSubject subSubject = inspector.clazz(Sub.class);
+    assertThat(subSubject, isPresent());
+    assertThat(subSubject.field("int", "f1"), isPresent());
+    ClassSubject subSubSubject = inspector.clazz(SubSub.class);
+    assertThat(subSubSubject, isPresent());
+    assertThat(subSubSubject.field("int", "f1"), isPresent());
+  }
+
+  @Override
+  public String allFieldsOutput() {
+    return StringUtils.lines("Super.f1 found", "Sub.f1 found", "SubSub.f1 found");
+  }
+
+  @Test
+  public void testKeepAllFieldsWithWildcard() throws Throwable {
+    runTest("-keep class **.SubSub { *; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepAllFieldsWithNameWildcard() throws Throwable {
+    runTest("-keep class **.SubSub { int f*; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepAllFieldsWithFields() throws Throwable {
+    runTest("-keep class **.SubSub { <fields>; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepF1() throws Throwable {
+    runTest("-keep class **.SubSub { int f1; }", this::checkAllFields, allFieldsOutput());
+  }
+
+  @Test
+  public void testKeepF1WithExtends() throws Throwable {
+    runTest("-keep class * extends **.Sub { int f1; }", this::checkAllFields, allFieldsOutput());
+  }
+}