Merge "Add Proguard testing rules for controlling inlining"
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());
+  }
+}