Merge "Remove spurious call graph edges to <clinit>()"
diff --git a/infra/config/global/cr-buildbucket.cfg b/infra/config/global/cr-buildbucket.cfg
index 0136562..863bbbe 100644
--- a/infra/config/global/cr-buildbucket.cfg
+++ b/infra/config/global/cr-buildbucket.cfg
@@ -86,6 +86,7 @@
     builders {
       name: "archive"
       mixins: "linux"
+      execution_timeout_secs: 1800  # 1/2h
       recipe {
         properties: "archive:True"
       }
@@ -93,6 +94,7 @@
     builders {
       name: "archive_release"
       mixins: "linux"
+      execution_timeout_secs: 1800  # 1/2h
       recipe {
         # TODO(ricow): set archive flag when we flip over
         # properties: "archive:True"
diff --git a/src/main/java/com/android/tools/r8/Version.java b/src/main/java/com/android/tools/r8/Version.java
index f26fb09..b0cef90 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.5.8-dev";
+  public static final String LABEL = "1.5.9-dev";
 
   private Version() {
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 3ce4a75..0e58ec0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -143,6 +143,7 @@
   public final DexString compareToMethodName = createString("compareTo");
   public final DexString compareToIgnoreCaseMethodName = createString("compareToIgnoreCase");
   public final DexString cloneMethodName = createString("clone");
+  public final DexString substringName = createString("substring");
 
   public final DexString valueOfMethodName = createString("valueOf");
   public final DexString toStringMethodName = createString("toString");
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLense.java b/src/main/java/com/android/tools/r8/graph/GraphLense.java
index b964dd3..aa8523e 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLense.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLense.java
@@ -814,6 +814,12 @@
     protected final BiMap<DexField, DexField> originalFieldSignatures;
     protected final BiMap<DexMethod, DexMethod> originalMethodSignatures;
 
+    // Overrides this if the sub type needs to be a nested lense while it doesn't have any mappings
+    // at all, e.g., publicizer lense that changes invocation type only.
+    protected boolean isLegitimateToHaveEmptyMappings() {
+      return false;
+    }
+
     public NestedGraphLense(
         Map<DexType, DexType> typeMap,
         Map<DexMethod, DexMethod> methodMap,
@@ -822,6 +828,8 @@
         BiMap<DexMethod, DexMethod> originalMethodSignatures,
         GraphLense previousLense,
         DexItemFactory dexItemFactory) {
+      assert !typeMap.isEmpty() || !methodMap.isEmpty() || !fieldMap.isEmpty()
+          || isLegitimateToHaveEmptyMappings();
       this.typeMap = typeMap.isEmpty() ? null : typeMap;
       this.methodMap = methodMap;
       this.fieldMap = fieldMap;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
index 5dfbb78..5096294 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
@@ -66,6 +66,8 @@
   // int String#lastIndexOf(int)
   // int String#compareTo(String)
   // int String#compareToIgnoreCase(String)
+  // String String#substring(int)
+  // String String#substring(int, int)
   public void computeTrivialOperationsOnConstString(IRCode code) {
     if (!code.hasConstString) {
       return;
@@ -77,7 +79,55 @@
         continue;
       }
       InvokeVirtual invoke = instr.asInvokeVirtual();
+      if (!invoke.hasOutValue()) {
+        continue;
+      }
       DexMethod invokedMethod = invoke.getInvokedMethod();
+      if (invokedMethod.name == appInfo.dexItemFactory.substringName) {
+        assert invoke.inValues().size() == 2 || invoke.inValues().size() == 3;
+        Value rcv = invoke.getReceiver().getAliasedValue();
+        if (rcv.definition == null
+            || !rcv.definition.isConstString()
+            || rcv.hasLocalInfo()) {
+          continue;
+        }
+        Value beginIndex = invoke.inValues().get(1).getAliasedValue();
+        if (beginIndex.definition == null
+            || !beginIndex.definition.isConstNumber()
+            || beginIndex.hasLocalInfo()) {
+          continue;
+        }
+        int beginIndexValue = beginIndex.definition.asConstNumber().getIntValue();
+        Value endIndex = null;
+        if (invoke.inValues().size() == 3) {
+          endIndex = invoke.inValues().get(2).getAliasedValue();
+          if (endIndex.definition == null
+              || !endIndex.definition.isConstNumber()
+              || endIndex.hasLocalInfo()) {
+            continue;
+          }
+        }
+        String rcvString = rcv.definition.asConstString().getValue().toString();
+        int endIndexValue =
+            endIndex == null
+                ? rcvString.length()
+                : endIndex.definition.asConstNumber().getIntValue();
+        if (beginIndexValue < 0
+            || endIndexValue > rcvString.length()
+            || beginIndexValue > endIndexValue) {
+          // This will raise StringIndexOutOfBoundsException.
+          continue;
+        }
+        String sub = rcvString.substring(beginIndexValue, endIndexValue);
+        Value stringValue =
+            code.createValue(
+                TypeLatticeElement.stringClassType(appInfo, definitelyNotNull()),
+                invoke.getLocalInfo());
+        it.replaceCurrentInstruction(
+            new ConstString(stringValue, factory.createString(sub), throwingInfo));
+        continue;
+      }
+
       Function<String, Integer> operatorWithNoArg = null;
       BiFunction<String, String, Integer> operatorWithString = null;
       BiFunction<String, Integer, Integer> operatorWithInt = null;
@@ -115,7 +165,7 @@
       Value rcv = invoke.getReceiver().getAliasedValue();
       if (rcv.definition == null
           || !rcv.definition.isConstString()
-          || !rcv.isConstant()) {
+          || rcv.hasLocalInfo()) {
         continue;
       }
       DexString rcvString = rcv.definition.asConstString().getValue();
@@ -130,7 +180,7 @@
         Value arg = invoke.inValues().get(1).getAliasedValue();
         if (arg.definition == null
             || !arg.definition.isConstString()
-            || !arg.isConstant()) {
+            || arg.hasLocalInfo()) {
           continue;
         }
         int v = operatorWithString.apply(
@@ -143,7 +193,7 @@
         Value arg = invoke.inValues().get(1).getAliasedValue();
         if (arg.definition == null
             || !arg.definition.isConstNumber()
-            || !arg.isConstant()) {
+            || arg.hasLocalInfo()) {
           continue;
         }
         int v = operatorWithInt.apply(
@@ -203,7 +253,7 @@
       Value in = invoke.getReceiver().getAliasedValue();
       if (in.definition == null
           || !in.definition.isConstClass()
-          || !in.isConstant()) {
+          || in.hasLocalInfo()) {
         continue;
       }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/PublicizerLense.java b/src/main/java/com/android/tools/r8/optimize/PublicizerLense.java
index c685d84..29ecf33 100644
--- a/src/main/java/com/android/tools/r8/optimize/PublicizerLense.java
+++ b/src/main/java/com/android/tools/r8/optimize/PublicizerLense.java
@@ -20,8 +20,6 @@
   private final Set<DexMethod> publicizedMethods;
 
   private PublicizerLense(AppView appView, Set<DexMethod> publicizedMethods) {
-    // This lense does not map any DexItem's at all.
-    // It will just tweak invoke type for publicized methods from invoke-direct to invoke-virtual.
     super(
         ImmutableMap.of(),
         ImmutableMap.of(),
@@ -35,6 +33,13 @@
   }
 
   @Override
+  protected boolean isLegitimateToHaveEmptyMappings() {
+    // This lense does not map any DexItem's at all.
+    // It will just tweak invoke type for publicized methods from invoke-direct to invoke-virtual.
+    return true;
+  }
+
+  @Override
   public GraphLenseLookupResult lookupMethod(DexMethod method, DexMethod context, Type type) {
     GraphLenseLookupResult previous = previousLense.lookupMethod(method, context, type);
     method = previous.getMethod();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringContentCheckTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringContentCheckTest.java
index 7044366..0578b85 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringContentCheckTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringContentCheckTest.java
@@ -56,6 +56,12 @@
       System.out.println(s1.lastIndexOf("ix"));
       System.out.println(s1.compareTo("prefix-CONST-suffix") == 0);
       System.out.println(s1.compareToIgnoreCase("PREFIX-const-SUFFIX") == 0);
+      // "prefix" exists
+      System.out.println(s1.substring(0, 6));
+      // "suffix" exists
+      System.out.println(s1.substring(13));
+      // "-suffix" doesn't.
+      System.out.println(s1.substring(12));
     }
 
     {
@@ -74,6 +80,7 @@
       System.out.println(s2.lastIndexOf("ix"));
       System.out.println(s2.compareTo("prefix-CONST-suffix") == 0);
       System.out.println(s2.compareToIgnoreCase("pre-con-suf") == 0);
+      System.out.println(s2.substring(13));
     }
 
     {
@@ -85,11 +92,21 @@
         // expected
       }
     }
+
+    {
+      try {
+        System.out.println("qwerty".substring(8));
+        fail("Should raise StringIndexOutOfBoundsException");
+      } catch (StringIndexOutOfBoundsException e) {
+        // expected
+      }
+    }
   }
 }
 
 @RunWith(Parameterized.class)
 public class StringContentCheckTest extends TestBase {
+  private static final String STRING_DESCRIPTOR = "Ljava/lang/String;";
   private final Backend backend;
   private static final List<Class<?>> CLASSES = ImmutableList.of(
       NeverInline.class,
@@ -122,6 +139,12 @@
       "true",
       // s1, compareToIgnoreCase(String)
       "true",
+      // s1, substring(int)
+      "prefix",
+      // s1, substring(int, int)
+      "suffix",
+      // s1, substring(int, int)
+      "-suffix",
       // s2, contains(String)
       "false",
       // s2, startsWith(String)
@@ -148,6 +171,8 @@
       "false",
       // s2, compareToIgnoreCase(String)
       "false",
+      // s2, substring(int, int)
+      "SUFFIX",
       // argCouldBeNull
       "false"
   );
@@ -169,10 +194,10 @@
   }
 
   private static boolean isStringContentChecker(DexMethod method) {
-    return method.getHolder().toDescriptorString().equals("Ljava/lang/String;")
-        && method.getArity() == 1
+    return method.getHolder().toDescriptorString().equals(STRING_DESCRIPTOR)
         && (method.proto.returnType.isBooleanType()
-            || method.proto.returnType.isIntType())
+            || method.proto.returnType.isIntType()
+            || method.proto.returnType.toDescriptorString().equals(STRING_DESCRIPTOR))
         && (method.name.toString().equals("contains")
             || method.name.toString().equals("startsWith")
             || method.name.toString().equals("endsWith")
@@ -182,7 +207,8 @@
             || method.name.toString().equals("indexOf")
             || method.name.toString().equals("lastIndexOf")
             || method.name.toString().equals("compareTo")
-            || method.name.toString().equals("compareToIgnoreCase"));
+            || method.name.toString().equals("compareToIgnoreCase")
+            || method.name.toString().equals("substring"));
   }
 
   private long countStringContentChecker(MethodSubject method) {
@@ -218,14 +244,14 @@
         .addProgramClasses(CLASSES)
         .run(MAIN)
         .assertSuccessWithOutput(JAVA_OUTPUT);
-    test(result, 26);
+    test(result, 31);
 
     result = testForD8()
         .release()
         .addProgramClasses(CLASSES)
         .run(MAIN)
         .assertSuccessWithOutput(JAVA_OUTPUT);
-    test(result, 14);
+    test(result, 16);
   }
 
   @Test
@@ -237,6 +263,6 @@
         .addKeepMainRule(MAIN)
         .run(MAIN)
         .assertSuccessWithOutput(JAVA_OUTPUT);
-    test(result, 14);
+    test(result, 16);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java
index f7b95f9..0ea1d7c 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java
@@ -48,6 +48,7 @@
         testForR8(backend)
             .setMode(mode)
             .enableProguardTestOptions()
+            .enableMergeAnnotations()
             .enableInliningAnnotations()
             .addProgramClasses(getClasses())
             .addKeepMainRule(getMainClass())
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
new file mode 100644
index 0000000..6ef62be
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
@@ -0,0 +1,122 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.naming.retrace;
+
+import static com.android.tools.r8.naming.retrace.StackTrace.isSameExceptForFileName;
+import static com.android.tools.r8.naming.retrace.StackTrace.isSameExceptForFileNameAndLineNumber;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+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 VerticalClassMergingRetraceTest extends RetraceTestBase {
+  private Set<StackTraceLine> haveSeenLines = new HashSet<>();
+
+  @Parameters(name = "Backend: {0}, mode: {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(Backend.values(), CompilationMode.values());
+  }
+
+  public VerticalClassMergingRetraceTest(Backend backend, CompilationMode mode) {
+    super(backend, mode);
+  }
+
+  @Override
+  public Collection<Class<?>> getClasses() {
+    return ImmutableList.of(getMainClass(), ResourceWrapper.class, TintResources.class);
+  }
+
+  @Override
+  public Class<?> getMainClass() {
+    return MainApp.class;
+  }
+
+  private int expectedActualStackTraceHeight() {
+    // In RELEASE mode, a synthetic bridge will be added by vertical class merger.
+    return mode == CompilationMode.RELEASE ? 3 : 2;
+  }
+
+  private boolean filterSynthesizedMethodWhenLineNumberAvailable(
+      StackTraceLine retracedStackTraceLine) {
+    return retracedStackTraceLine.lineNumber > 0;
+  }
+
+  private boolean filterSynthesizedMethod(StackTraceLine retracedStackTraceLine) {
+    return haveSeenLines.add(retracedStackTraceLine)
+        && (retracedStackTraceLine.className.contains("ResourceWrapper")
+            || retracedStackTraceLine.className.contains("MainApp"));
+  }
+
+  @Test
+  public void testSourceFileAndLineNumberTable() throws Exception {
+    runTest(
+        ImmutableList.of("-keepattributes SourceFile,LineNumberTable"),
+        (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
+          // Even when SourceFile is present retrace replaces the file name in the stack trace.
+          StackTrace reprocessedStackTrace =
+              mode == CompilationMode.DEBUG ? retracedStackTrace
+                  : retracedStackTrace.filter(this::filterSynthesizedMethodWhenLineNumberAvailable);
+          assertThat(reprocessedStackTrace, isSameExceptForFileName(expectedStackTrace));
+          assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
+        });
+  }
+
+  @Test
+  public void testLineNumberTableOnly() throws Exception {
+    runTest(
+        ImmutableList.of("-keepattributes LineNumberTable"),
+        (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
+          StackTrace reprocessedStackTrace =
+              mode == CompilationMode.DEBUG ? retracedStackTrace
+                  : retracedStackTrace.filter(this::filterSynthesizedMethodWhenLineNumberAvailable);
+          assertThat(reprocessedStackTrace, isSameExceptForFileName(expectedStackTrace));
+          assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
+        });
+  }
+
+  @Test
+  public void testNoLineNumberTable() throws Exception {
+    haveSeenLines.clear();
+    runTest(
+        ImmutableList.of(),
+        (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
+          StackTrace reprocessedStackTrace =
+              mode == CompilationMode.DEBUG ? retracedStackTrace
+                  : retracedStackTrace.filter(this::filterSynthesizedMethod);
+          assertThat(reprocessedStackTrace,
+              isSameExceptForFileNameAndLineNumber(expectedStackTrace));
+          assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
+        });
+  }
+}
+
+class ResourceWrapper {
+  // Will be merged down, and represented as:
+  //     java.lang.String ...ResourceWrapper.foo() -> a
+  @NeverInline
+  String foo() {
+    throw null;
+  }
+}
+
+class TintResources extends ResourceWrapper {
+}
+
+class MainApp {
+  public static void main(String[] args) {
+    TintResources t = new TintResources();
+    System.out.println(t.foo());
+  }
+}
diff --git a/tools/archive.py b/tools/archive.py
index 833b4b0..f86530a 100755
--- a/tools/archive.py
+++ b/tools/archive.py
@@ -94,9 +94,8 @@
   if not utils.is_bot() and not options.dry_run:
     raise Exception('You are not a bot, don\'t archive builds')
 
-  if utils.is_old_bot():
-    print("Archiving is disabled on old bots, new bots are archiving, see " +
-          "https://ci.chromium.org/p/r8")
+  if utils.is_new_bot():
+    print("Archiving is disabled on new bots.")
     return
 
   # Create maven release which uses a build that exclude dependencies.
diff --git a/tools/run_on_as_app.py b/tools/run_on_as_app.py
index 0a4f740..998c9ef 100755
--- a/tools/run_on_as_app.py
+++ b/tools/run_on_as_app.py
@@ -783,7 +783,7 @@
     return
 
   with utils.TempDir() as temp_dir:
-    if not options.no_build or options.golem:
+    if not (options.no_build or options.golem):
       gradle.RunGradle(['r8', 'r8lib'])
 
     if options.version:
diff --git a/tools/utils.py b/tools/utils.py
index 2b0caaf..f2df0ed 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -481,8 +481,8 @@
       android_optional_jar for android_optional_jar in android_optional_jars
       if os.path.isfile(android_optional_jar)]
 
-def is_old_bot():
-  return 'BUILDBOT_BUILDERNAME' in os.environ
+def is_new_bot():
+  return 'SWARMING_BOT_ID' in os.environ
 
 def is_bot():
   return 'USER' in os.environ and os.environ['USER'] == 'chrome-bot'