Add Proguard testing rules for controlling inlining

This adds two new rules -forceinline and -neverinling which is only
available for testing. This should help in getting the inlining
required for specific tests instead of having to use more indirect
methods to either ensure or prohibit inlining.

This CL does not change any existing tests to use these rules.

Bug: 110190426
Change-Id: Icdaf71e353b90f4c6ceb452ea3c49bc4948c7edd
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 9fcca89..584b06d 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -68,6 +68,7 @@
     Path proguardCompatibilityRulesOutput = null;
 
     private boolean allowPartiallyImplementedProguardOptions = false;
+    private boolean allowTestProguardOptions = false;
 
     private StringConsumer mainDexListConsumer = null;
 
@@ -290,7 +291,8 @@
       }
 
       ProguardConfigurationParser parser = new ProguardConfigurationParser(
-          factory, reporter, !allowPartiallyImplementedProguardOptions);
+          factory, reporter,
+          !allowPartiallyImplementedProguardOptions, allowTestProguardOptions);
       if (!proguardConfigs.isEmpty()) {
         parser.parse(proguardConfigs);
       }
@@ -393,6 +395,11 @@
     void allowPartiallyImplementedProguardOptions() {
       allowPartiallyImplementedProguardOptions = true;
     }
+
+    // Internal for-testing method to allow proguard options only available for testing.
+    void allowTestProguardOptions() {
+      allowTestProguardOptions = true;
+    }
   }
 
   // Wrapper class to ensure that R8 does not allow DEX as program inputs.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index cc7f571..631f234 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -91,7 +91,9 @@
   }
 
   private Reason computeInliningReason(DexEncodedMethod target) {
-    if (target.getOptimizationInfo().forceInline()) {
+    if (target.getOptimizationInfo().forceInline()
+        || (inliner.appInfo.hasLiveness()
+            && inliner.appInfo.withLiveness().forceInline.contains(target))) {
       return Reason.FORCE;
     }
     if (inliner.appInfo.hasLiveness()
@@ -252,7 +254,7 @@
   public InlineAction computeForInvokeWithReceiver(
       InvokeMethodWithReceiver invoke, DexType invocationContext) {
     DexEncodedMethod candidate = validateCandidate(invoke, invocationContext);
-    if (candidate == null || inliner.isBlackListed(candidate.method)) {
+    if (candidate == null || inliner.isBlackListed(candidate)) {
       return null;
     }
 
@@ -268,6 +270,7 @@
       if (info != null) {
         info.exclude(invoke, "receiver for candidate can be null");
       }
+      assert !inliner.appInfo.forceInline.contains(candidate.method);
       return null;
     }
 
@@ -293,7 +296,7 @@
   @Override
   public InlineAction computeForInvokeStatic(InvokeStatic invoke, DexType invocationContext) {
     DexEncodedMethod candidate = validateCandidate(invoke, invocationContext);
-    if (candidate == null || inliner.isBlackListed(candidate.method)) {
+    if (candidate == null || inliner.isBlackListed(candidate)) {
       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 44ebdc7..35e9799 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
@@ -70,8 +70,8 @@
     blackList.add(appInfo.dexItemFactory.kotlin.intrinsics.throwNpe);
   }
 
-  public boolean isBlackListed(DexMethod method) {
-    return blackList.contains(method);
+  public boolean isBlackListed(DexEncodedMethod method) {
+    return blackList.contains(method.method) || appInfo.neverInline.contains(method);
   }
 
   private Constraint instructionAllowedForInlining(
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 baacb1e..a1cc440 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -1544,10 +1544,18 @@
      */
     public final Map<DexItem, ProguardMemberRule> assumedValues;
     /**
-     * All methods that have to be inlined due to a configuration directive.
+     * All methods that should be inlined if possible due to a configuration directive.
      */
     public final Set<DexItem> alwaysInline;
     /**
+     * All methods that *must* be inlined due to a configuration directive (testing only).
+     */
+    public final Set<DexItem> forceInline;
+    /**
+     * All methods that *must* never be inlined due to a configuration directive (testing only).
+     */
+    public final Set<DexItem> neverInline;
+    /**
      * All items with -identifiernamestring rule.
      */
     public final Set<DexItem> identifierNameStrings;
@@ -1602,6 +1610,8 @@
       this.noSideEffects = enqueuer.rootSet.noSideEffects;
       this.assumedValues = enqueuer.rootSet.assumedValues;
       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.protoLiteFields = enqueuer.protoLiteFields;
@@ -1640,6 +1650,8 @@
       this.brokenSuperInvokes = previous.brokenSuperInvokes;
       this.protoLiteFields = previous.protoLiteFields;
       this.alwaysInline = previous.alwaysInline;
+      this.forceInline = previous.forceInline;
+      this.neverInline = previous.neverInline;
       this.identifierNameStrings = previous.identifierNameStrings;
       this.prunedTypes = mergeSets(previous.prunedTypes, removedClasses);
       this.switchMaps = previous.switchMaps;
@@ -1683,6 +1695,8 @@
       this.assumedValues = previous.assumedValues;
       assert lense.assertNotModified(previous.alwaysInline);
       this.alwaysInline = previous.alwaysInline;
+      this.forceInline = previous.forceInline;
+      this.neverInline = previous.neverInline;
       this.identifierNameStrings =
           rewriteMixedItemsConservatively(previous.identifierNameStrings, lense);
       // Switchmap classes should never be affected by renaming.
@@ -1724,6 +1738,8 @@
       this.brokenSuperInvokes = previous.brokenSuperInvokes;
       this.protoLiteFields = previous.protoLiteFields;
       this.alwaysInline = previous.alwaysInline;
+      this.forceInline = previous.forceInline;
+      this.neverInline = previous.neverInline;
       this.identifierNameStrings = previous.identifierNameStrings;
       this.prunedTypes = previous.prunedTypes;
       this.switchMaps = switchMaps;
diff --git a/src/main/java/com/android/tools/r8/shaking/InlineRule.java b/src/main/java/com/android/tools/r8/shaking/InlineRule.java
new file mode 100644
index 0000000..565856c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/InlineRule.java
@@ -0,0 +1,88 @@
+// Copyright (c) 2017, 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;
+
+import com.android.tools.r8.errors.Unreachable;
+import java.util.List;
+
+public class InlineRule extends ProguardConfigurationRule {
+
+  public enum Type {
+    ALWAYS, FORCE, NEVER
+  }
+
+  public static class Builder extends ProguardConfigurationRule.Builder {
+
+    private Builder() {
+    }
+
+    Type type;
+
+    public Builder setType(Type type) {
+      this.type = type;
+      return this;
+    }
+
+    public InlineRule build() {
+      return new InlineRule(classAnnotation, classAccessFlags,
+          negatedClassAccessFlags, classTypeNegated, classType, classNames, inheritanceAnnotation,
+          inheritanceClassName, inheritanceIsExtends, memberRules, type);
+    }
+  }
+
+  private final Type type;
+
+  private InlineRule(
+      ProguardTypeMatcher classAnnotation,
+      ProguardAccessFlags classAccessFlags,
+      ProguardAccessFlags negatedClassAccessFlags,
+      boolean classTypeNegated,
+      ProguardClassType classType,
+      ProguardClassNameList classNames,
+      ProguardTypeMatcher inheritanceAnnotation,
+      ProguardTypeMatcher inheritanceClassName,
+      boolean inheritanceIsExtends,
+      List<ProguardMemberRule> memberRules,
+      Type type) {
+    super(classAnnotation, classAccessFlags, negatedClassAccessFlags, classTypeNegated, classType,
+        classNames, inheritanceAnnotation, inheritanceClassName, inheritanceIsExtends, memberRules);
+    this.type = type;
+  }
+
+  public static InlineRule.Builder builder() {
+    return new InlineRule.Builder();
+  }
+
+  public Type getType() {
+    return type;
+  }
+
+  public ProguardCheckDiscardRule asProguardCheckDiscardRule() {
+    assert type == Type.FORCE;
+    ProguardCheckDiscardRule.Builder builder = ProguardCheckDiscardRule.builder();
+    builder.setClassAnnotation(getClassAnnotation());
+    builder.setClassAccessFlags(getClassAccessFlags());
+    builder.setNegatedClassAccessFlags(getNegatedClassAccessFlags());
+    builder.setClassTypeNegated(getClassTypeNegated());
+    builder.setClassType(getClassType());
+    builder.setClassNames(getClassNames());
+    builder.setInheritanceAnnotation(getInheritanceAnnotation());
+    builder.setInheritanceIsExtends(getInheritanceIsExtends());
+    builder.setMemberRules(getMemberRules());
+    return builder.build();
+  }
+
+  @Override
+  String typeString() {
+    switch (type) {
+      case ALWAYS:
+        return "alwaysinline";
+      case FORCE:
+        return "forceinline";
+      case NEVER:
+        return "neverinline";
+    }
+    throw new Unreachable("Unknown inline type " + type);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardAlwaysInlineRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardAlwaysInlineRule.java
deleted file mode 100644
index 8d4a14b..0000000
--- a/src/main/java/com/android/tools/r8/shaking/ProguardAlwaysInlineRule.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (c) 2017, 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;
-
-import java.util.List;
-
-public class ProguardAlwaysInlineRule extends ProguardConfigurationRule {
-
-  public static class Builder extends ProguardConfigurationRule.Builder {
-
-    private Builder() {
-    }
-
-    public ProguardAlwaysInlineRule build() {
-      return new ProguardAlwaysInlineRule(classAnnotation, classAccessFlags,
-          negatedClassAccessFlags, classTypeNegated, classType, classNames, inheritanceAnnotation,
-          inheritanceClassName, inheritanceIsExtends, memberRules);
-    }
-  }
-
-  private ProguardAlwaysInlineRule(
-      ProguardTypeMatcher classAnnotation,
-      ProguardAccessFlags classAccessFlags,
-      ProguardAccessFlags negatedClassAccessFlags,
-      boolean classTypeNegated,
-      ProguardClassType classType,
-      ProguardClassNameList classNames,
-      ProguardTypeMatcher inheritanceAnnotation,
-      ProguardTypeMatcher inheritanceClassName,
-      boolean inheritanceIsExtends,
-      List<ProguardMemberRule> memberRules) {
-    super(classAnnotation, classAccessFlags, negatedClassAccessFlags, classTypeNegated, classType,
-        classNames, inheritanceAnnotation, inheritanceClassName, inheritanceIsExtends, memberRules);
-  }
-
-  public static ProguardAlwaysInlineRule.Builder builder() {
-    return new ProguardAlwaysInlineRule.Builder();
-  }
-
-  @Override
-  String typeString() {
-    return "alwaysinline";
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 1de3f17..5b22456 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -5,6 +5,7 @@
 
 import static com.android.tools.r8.utils.DescriptorUtils.javaTypeToDescriptor;
 
+import com.android.tools.r8.Version;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -15,6 +16,7 @@
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.position.TextPosition;
 import com.android.tools.r8.position.TextRange;
+import com.android.tools.r8.shaking.InlineRule.Type;
 import com.android.tools.r8.shaking.ProguardConfiguration.Builder;
 import com.android.tools.r8.shaking.ProguardTypeMatcher.ClassOrType;
 import com.android.tools.r8.shaking.ProguardTypeMatcher.MatchSpecificType;
@@ -48,6 +50,7 @@
 
   private final Reporter reporter;
   private final boolean failOnPartiallyImplementedOptions;
+  private final boolean allowTestOptions;
 
   private static final List<String> IGNORED_SINGLE_ARG_OPTIONS = ImmutableList.of(
       "protomapping",
@@ -97,16 +100,18 @@
 
   public ProguardConfigurationParser(
       DexItemFactory dexItemFactory, Reporter reporter) {
-    this(dexItemFactory, reporter, true);
+    this(dexItemFactory, reporter, true, false);
   }
 
   public ProguardConfigurationParser(
-      DexItemFactory dexItemFactory, Reporter reporter, boolean failOnPartiallyImplementedOptions) {
+      DexItemFactory dexItemFactory, Reporter reporter, boolean failOnPartiallyImplementedOptions,
+      boolean allowTestOptions) {
     this.dexItemFactory = dexItemFactory;
     configurationBuilder = ProguardConfiguration.builder(dexItemFactory, reporter);
 
     this.reporter = reporter;
     this.failOnPartiallyImplementedOptions = failOnPartiallyImplementedOptions;
+    this.allowTestOptions = allowTestOptions;
   }
 
   public ProguardConfiguration.Builder getConfigurationBuilder() {
@@ -343,7 +348,16 @@
       } else if (acceptString("packageobfuscationdictionary")) {
         configurationBuilder.setPackageObfuscationDictionary(parseFileName());
       } else if (acceptString("alwaysinline")) {
-        ProguardAlwaysInlineRule rule = parseAlwaysInlineRule();
+        InlineRule rule = parseInlineRule(Type.ALWAYS);
+        configurationBuilder.addRule(rule);
+      } else if (allowTestOptions && acceptString("forceinline")) {
+        InlineRule rule = parseInlineRule(Type.FORCE);
+        configurationBuilder.addRule(rule);
+        // Insert a matching -checkdiscard rule to ensure force inlining happens.
+        ProguardCheckDiscardRule ruled = rule.asProguardCheckDiscardRule();
+        configurationBuilder.addRule(ruled);
+      } else if (allowTestOptions && acceptString("neverinline")) {
+        InlineRule rule = parseInlineRule(Type.NEVER);
         configurationBuilder.addRule(rule);
       } else if (acceptString("useuniqueclassmembernames")) {
         configurationBuilder.setUseUniqueClassMemberNames(true);
@@ -367,8 +381,15 @@
         configurationBuilder.addRule(parseIfRule(optionStart));
       } else {
         String unknownOption = acceptString();
+        String devMessage = "";
+        if (Version.isDev()
+            && unknownOption != null
+            && (unknownOption.equals("forceinline") || unknownOption.equals("neverinline"))) {
+          devMessage = ", this option needs to be turned on explicitly if used for tests.";
+        }
         reporter.error(new StringDiagnostic(
-            "Unknown option \"-" + unknownOption + "\"", origin, getPosition(optionStart)));
+            "Unknown option \"-" + unknownOption + "\"" + devMessage,
+            origin, getPosition(optionStart)));
       }
       return true;
     }
@@ -563,9 +584,9 @@
       return keepRuleBuilder.build();
     }
 
-    private ProguardAlwaysInlineRule parseAlwaysInlineRule()
+    private InlineRule parseInlineRule(InlineRule.Type type)
         throws ProguardRuleParserException {
-      ProguardAlwaysInlineRule.Builder keepRuleBuilder = ProguardAlwaysInlineRule.builder();
+      InlineRule.Builder keepRuleBuilder = InlineRule.builder().setType(type);
       parseClassSpec(keepRuleBuilder, false);
       return keepRuleBuilder.build();
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
index ce1840f..e4c5691 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
@@ -56,7 +56,7 @@
     if (!(o instanceof ProguardConfigurationRule)) {
       return false;
     }
-    ProguardKeepRule that = (ProguardKeepRule) o;
+    ProguardConfigurationRule that = (ProguardConfigurationRule) o;
     return super.equals(that);
   }
 
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 c860d83..c95cfe9 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -60,6 +60,8 @@
       Sets.newIdentityHashSet();
   private final Set<DexItem> checkDiscarded = Sets.newIdentityHashSet();
   private final Set<DexItem> alwaysInline = Sets.newIdentityHashSet();
+  private final Set<DexItem> forceInline = Sets.newIdentityHashSet();
+  private final Set<DexItem> neverInline = Sets.newIdentityHashSet();
   private final Map<DexItem, Map<DexItem, ProguardKeepRule>> dependentNoShrinking =
       new IdentityHashMap<>();
   private final Map<DexItem, ProguardMemberRule> noSideEffects = new IdentityHashMap<>();
@@ -240,7 +242,7 @@
       } else if (rule instanceof ProguardAssumeNoSideEffectRule) {
         markMatchingVisibleMethods(clazz, memberKeepRules, rule, null);
         markMatchingFields(clazz, memberKeepRules, rule, null);
-      } else if (rule instanceof ProguardAlwaysInlineRule) {
+      } else if (rule instanceof InlineRule) {
         markMatchingMethods(clazz, memberKeepRules, rule, null);
       } else if (rule instanceof ProguardAssumeValuesRule) {
         markMatchingVisibleMethods(clazz, memberKeepRules, rule, null);
@@ -310,6 +312,8 @@
         keepPackageName,
         checkDiscarded,
         alwaysInline,
+        forceInline,
+        neverInline,
         noSideEffects,
         assumedValues,
         dependentNoShrinking,
@@ -720,8 +724,20 @@
       assumedValues.put(item, rule);
     } else if (context instanceof ProguardCheckDiscardRule) {
       checkDiscarded.add(item);
-    } else if (context instanceof ProguardAlwaysInlineRule) {
-      alwaysInline.add(item);
+    } else if (context instanceof InlineRule) {
+      switch (((InlineRule) context).getType()) {
+        case ALWAYS:
+          alwaysInline.add(item);
+          break;
+        case FORCE:
+          forceInline.add(item);
+          break;
+        case NEVER:
+          neverInline.add(item);
+          break;
+        default:
+          throw new Unreachable();
+      }
     } else if (context instanceof ProguardIdentifierNameStringRule) {
       if (item instanceof DexEncodedField) {
         identifierNameStrings.add(((DexEncodedField) item).field);
@@ -740,6 +756,8 @@
     public final Set<DexItem> keepPackageName;
     public final Set<DexItem> checkDiscarded;
     public final Set<DexItem> alwaysInline;
+    public final Set<DexItem> forceInline;
+    public final Set<DexItem> neverInline;
     public final Map<DexItem, ProguardMemberRule> noSideEffects;
     public final Map<DexItem, ProguardMemberRule> assumedValues;
     private final Map<DexItem, Map<DexItem, ProguardKeepRule>> dependentNoShrinking;
@@ -775,6 +793,8 @@
         Set<DexItem> keepPackageName,
         Set<DexItem> checkDiscarded,
         Set<DexItem> alwaysInline,
+        Set<DexItem> forceInline,
+        Set<DexItem> neverInline,
         Map<DexItem, ProguardMemberRule> noSideEffects,
         Map<DexItem, ProguardMemberRule> assumedValues,
         Map<DexItem, Map<DexItem, ProguardKeepRule>> dependentNoShrinking,
@@ -787,6 +807,8 @@
       this.keepPackageName = Collections.unmodifiableSet(keepPackageName);
       this.checkDiscarded = Collections.unmodifiableSet(checkDiscarded);
       this.alwaysInline = Collections.unmodifiableSet(alwaysInline);
+      this.forceInline = Collections.unmodifiableSet(forceInline);
+      this.neverInline = Collections.unmodifiableSet(neverInline);
       this.noSideEffects = Collections.unmodifiableMap(noSideEffects);
       this.assumedValues = Collections.unmodifiableMap(assumedValues);
       this.dependentNoShrinking = dependentNoShrinking;
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index d5d0558..10f62a1 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -1546,6 +1546,12 @@
     return builder;
   }
 
+  public static R8Command.Builder allowTestProguardOptions(
+      R8Command.Builder builder) {
+    builder.allowTestProguardOptions();
+    return builder;
+  }
+
   public static AndroidApp getApp(BaseCommand command) {
     return command.getInputApp();
   }
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
index f484bc8..1379b95 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -148,7 +148,14 @@
   public void resetAllowPartiallyImplementedOptions() {
     handler = new KeepingDiagnosticHandler();
     reporter = new Reporter(handler);
-    parser = new ProguardConfigurationParser(new DexItemFactory(), reporter, false);
+    parser = new ProguardConfigurationParser(new DexItemFactory(), reporter, false, false);
+  }
+
+  @Before
+  public void resetAllowTestOptions() {
+    handler = new KeepingDiagnosticHandler();
+    reporter = new Reporter(handler);
+    parser = new ProguardConfigurationParser(new DexItemFactory(), reporter, true, true);
   }
 
   @Test
@@ -861,7 +868,7 @@
   @Test
   public void parseKeepdirectories() throws Exception {
     ProguardConfigurationParser parser =
-        new ProguardConfigurationParser(new DexItemFactory(), reporter, false);
+        new ProguardConfigurationParser(new DexItemFactory(), reporter, false, false);
     parser.parse(Paths.get(KEEPDIRECTORIES));
     verifyParserEndsCleanly();
   }
@@ -1357,6 +1364,22 @@
   }
 
   @Test
+  public void parse_testInlineOptions() {
+    List<String> options = ImmutableList.of(
+        "-neverinline", "-forceinline");
+    for (String option : options) {
+      try {
+        reset();
+        parser.parse(createConfigurationForTesting(ImmutableList.of(option + " class A { *; }")));
+        fail("Expect to fail due to testing option being turned off.");
+      } catch (AbortException e) {
+        assertEquals(2, handler.errors.size());
+        checkDiagnostics(handler.errors, 0, null, 1, 1, "Unknown option \"" + option + "\"");
+      }
+    }
+  }
+
+  @Test
   public void parse_if() throws Exception {
     Path proguardConfig = writeTextToTempFile(
         "-if   class **$$ModuleAdapter",
diff --git a/src/test/java/com/android/tools/r8/shaking/testrules/A.java b/src/test/java/com/android/tools/r8/shaking/testrules/A.java
new file mode 100644
index 0000000..af05562
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/testrules/A.java
@@ -0,0 +1,18 @@
+// 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.testrules;
+
+public class A {
+
+  public static int m(int a, int b) {
+    int r = a + b;
+    System.out.println(a + " + " + b + " = " + r);
+    return r;
+  }
+
+  public static int method() {
+    return m(m(m(1, 2), m(3, 4)), m(m(5, 6), m(7, 8)));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/testrules/B.java b/src/test/java/com/android/tools/r8/shaking/testrules/B.java
new file mode 100644
index 0000000..4d1f085
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/testrules/B.java
@@ -0,0 +1,17 @@
+// 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.testrules;
+
+public class B {
+
+  public int m(int a, int b) {
+    int r = a + b;
+    System.out.println(a + " + " + b + " = " + r);
+    return r;
+  }
+  public int method() {
+    return m(m(m(1, 2), m(3, 4)), m(m(5, 6), m(7, 8)));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/testrules/C.java b/src/test/java/com/android/tools/r8/shaking/testrules/C.java
new file mode 100644
index 0000000..5ee4d53
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/testrules/C.java
@@ -0,0 +1,14 @@
+// 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.testrules;
+
+public class C {
+
+  private static int i;
+
+  public static int x() {
+    return i;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java b/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java
new file mode 100644
index 0000000..e4be101
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.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.
+
+// 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.testrules;
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.junit.Test;
+
+public class ForceInlineTest extends TestBase {
+
+  private DexInspector runTest(List<String> proguardConfiguration) throws Exception {
+    R8Command.Builder builder =
+        ToolHelper.prepareR8CommandBuilder(readClasses(Main.class, A.class, B.class, C.class));
+    ToolHelper.allowTestProguardOptions(builder);
+    builder.addProguardConfiguration(proguardConfiguration, Origin.unknown());
+    return new DexInspector(ToolHelper.runR8(builder.build()));
+  }
+
+  @Test
+  public void testDefaultInlining() throws Exception {
+    DexInspector inspector = runTest(ImmutableList.of(
+        "-keep class **.Main { *; }",
+        "-dontobfuscate"
+    ));
+
+    ClassSubject classA = inspector.clazz(A.class);
+    ClassSubject classB = inspector.clazz(B.class);
+    ClassSubject classC = inspector.clazz(C.class);
+    ClassSubject classMain = inspector.clazz(Main.class);
+    assertThat(classA, isPresent());
+    assertThat(classB, isPresent());
+    assertThat(classC, isPresent());
+    assertThat(classMain, isPresent());
+
+    // By default A.m *will not* be inlined (called several times and not small).
+    assertThat(classA.method("int", "m", ImmutableList.of("int", "int")), isPresent());
+    // By default A.method *will* be inlined (called only once).
+    assertThat(classA.method("int", "method", ImmutableList.of()), not(isPresent()));
+    // By default B.m *will not* be inlined (called several times and not small).
+    assertThat(classB.method("int", "m", ImmutableList.of("int", "int")), isPresent());
+    // By default B.method *will* be inlined (called only once).
+    assertThat(classB.method("int", "method", ImmutableList.of()), not(isPresent()));
+  }
+
+  @Test
+  public void testNeverInline() throws Exception {
+    DexInspector inspector = runTest(ImmutableList.of(
+        "-neverinline class **.A { method(); }",
+        "-neverinline class **.B { method(); }",
+        "-keep class **.Main { *; }",
+        "-dontobfuscate"
+    ));
+
+    ClassSubject classA = inspector.clazz(A.class);
+    ClassSubject classB = inspector.clazz(B.class);
+    ClassSubject classC = inspector.clazz(C.class);
+    ClassSubject classMain = inspector.clazz(Main.class);
+    assertThat(classA, isPresent());
+    assertThat(classB, isPresent());
+    assertThat(classC, isPresent());
+    assertThat(classMain, isPresent());
+
+    // Compared to the default method is no longer inlined.
+    assertThat(classA.method("int", "m", ImmutableList.of("int", "int")), isPresent());
+    assertThat(classA.method("int", "method", ImmutableList.of()), isPresent());
+    assertThat(classB.method("int", "m", ImmutableList.of("int", "int")), isPresent());
+    assertThat(classB.method("int", "method", ImmutableList.of()), isPresent());
+  }
+
+  @Test
+  public void testForceInline() throws Exception {
+    DexInspector inspector = runTest(ImmutableList.of(
+        "-forceinline class **.A { int m(int, int); }",
+        "-forceinline class **.B { int m(int, int); }",
+        "-keep class **.Main { *; }",
+        "-dontobfuscate"
+    ));
+
+    ClassSubject classA = inspector.clazz(A.class);
+    ClassSubject classB = inspector.clazz(B.class);
+    ClassSubject classC = inspector.clazz(C.class);
+    ClassSubject classMain = inspector.clazz(Main.class);
+
+    // Compared to the default m is now inlined and method still is, so classes A and B are gone.
+    assertThat(classA, not(isPresent()));
+    assertThat(classB, not(isPresent()));
+    assertThat(classC, isPresent());
+    assertThat(classMain, isPresent());
+  }
+
+  @Test
+  public void testForceInlineFails() throws Exception {
+    try {
+      DexInspector inspector = runTest(ImmutableList.of(
+          "-forceinline class **.A { int x(); }",
+          "-keep class **.Main { *; }",
+          "-dontobfuscate"
+      ));
+      fail("Force inline of non-inlinable method succeeded");
+    } catch (Throwable t) {
+      // Ignore assertion error.
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/shaking/testrules/Main.java b/src/test/java/com/android/tools/r8/shaking/testrules/Main.java
new file mode 100644
index 0000000..d326216
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/testrules/Main.java
@@ -0,0 +1,14 @@
+// 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.testrules;
+
+public class Main {
+
+  public static void main(String[] args) {
+    System.out.println(A.method());
+    System.out.println(new B().method());
+    System.out.println(C.x());
+  }
+}