Add --source-file-template command line flag to R8.

Bug: 201269335
Change-Id: I75a24428c0f408571cf202a09745d626118fab89
diff --git a/src/main/java/com/android/tools/r8/R8CommandParser.java b/src/main/java/com/android/tools/r8/R8CommandParser.java
index ab664c3..464e4d3 100644
--- a/src/main/java/com/android/tools/r8/R8CommandParser.java
+++ b/src/main/java/com/android/tools/r8/R8CommandParser.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.FlagFile;
+import com.android.tools.r8.utils.SourceFileTemplateProvider;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
@@ -34,6 +35,7 @@
           "--pg-map-output",
           "--desugared-lib",
           "--desugared-lib-pg-conf-output",
+          "--source-file-template",
           THREAD_COUNT_FLAG);
 
   private static final Set<String> OPTIONS_WITH_TWO_PARAMETERS = ImmutableSet.of("--feature");
@@ -262,6 +264,9 @@
         builder.setDesugaredLibraryKeepRuleConsumer(consumer);
       } else if (arg.equals("--no-data-resources")) {
         state.includeDataResources = false;
+      } else if (arg.equals("--source-file-template")) {
+        builder.setSourceFileProvider(
+            SourceFileTemplateProvider.create(nextArg, builder.getReporter()));
       } else if (arg.startsWith("--")) {
         if (tryParseAssertionArgument(builder, arg, argsOrigin)) {
           continue;
diff --git a/src/main/java/com/android/tools/r8/utils/SourceFileTemplateProvider.java b/src/main/java/com/android/tools/r8/utils/SourceFileTemplateProvider.java
new file mode 100644
index 0000000..b1c3cda
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/SourceFileTemplateProvider.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2021, 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.utils;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.SourceFileEnvironment;
+import com.android.tools.r8.SourceFileProvider;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+
+public class SourceFileTemplateProvider implements SourceFileProvider {
+
+  private static final char VARIABLE_PREFIX = '%';
+
+  private static final Map<String, SourceFileProvider> HANDLERS =
+      ImmutableMap.<String, SourceFileProvider>builder()
+          .put(var("MAP_ID"), SourceFileEnvironment::getMapId)
+          .put(var("MAP_HASH"), SourceFileEnvironment::getMapHash)
+          .build();
+
+  private static String var(String name) {
+    return VARIABLE_PREFIX + name;
+  }
+
+  private static int getMaxVariableLength() {
+    int max = 0;
+    for (String key : HANDLERS.keySet()) {
+      max = Math.max(max, key.length());
+    }
+    return max;
+  }
+
+  public static SourceFileProvider create(String template, DiagnosticsHandler handler) {
+    String cleaned = template;
+    for (String variable : HANDLERS.keySet()) {
+      // Maintain the same size as template so indexing remains valid for error reporting.
+      cleaned = cleaned.replace(variable, ' ' + variable.substring(1));
+    }
+    assert template.length() == cleaned.length();
+    int unhandled = cleaned.indexOf(VARIABLE_PREFIX);
+    if (unhandled >= 0) {
+      while (unhandled >= 0) {
+        int endIndex = Math.min(unhandled + getMaxVariableLength(), template.length());
+        String variablePrefix = template.substring(unhandled, endIndex);
+        handler.error(
+            new StringDiagnostic("Invalid template variable starting with " + variablePrefix));
+        unhandled = cleaned.indexOf(VARIABLE_PREFIX, unhandled + 1);
+      }
+      return null;
+    }
+    return new SourceFileTemplateProvider(template);
+  }
+
+  private final String template;
+  private String cachedValue = null;
+
+  private SourceFileTemplateProvider(String template) {
+    this.template = template;
+  }
+
+  @Override
+  public String get(SourceFileEnvironment environment) {
+    if (cachedValue == null) {
+      cachedValue = template;
+      HANDLERS.forEach(
+          (variable, getter) -> {
+            cachedValue = cachedValue.replace(variable, getter.get(environment));
+          });
+    }
+    return cachedValue;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/sourcefile/SourceFileTemplateTest.java b/src/test/java/com/android/tools/r8/naming/sourcefile/SourceFileTemplateTest.java
new file mode 100644
index 0000000..cdff91c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/sourcefile/SourceFileTemplateTest.java
@@ -0,0 +1,168 @@
+// Copyright (c) 2021, 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.sourcefile;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.ClassFileConsumer.ArchiveConsumer;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.R8;
+import com.android.tools.r8.R8Command.Builder;
+import com.android.tools.r8.R8CommandParser;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessagesImpl;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class SourceFileTemplateTest extends TestBase {
+
+  @Parameterized.Parameters(name = "{0}, {1}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withNoneRuntime().build(), Backend.values());
+  }
+
+  private final Backend backend;
+
+  public SourceFileTemplateTest(TestParameters parameters, Backend backend) {
+    parameters.assertNoneRuntime();
+    this.backend = backend;
+  }
+
+  @Test
+  public void testNoVariables() throws Exception {
+    String template = "MySourceFile";
+    assertEquals(
+        template,
+        new CodeInspector(compileWithSourceFileTemplate(template))
+            .clazz(TestClass.class)
+            .getDexProgramClass()
+            .getSourceFile()
+            .toString());
+  }
+
+  @Test
+  public void testInvalidVariables() {
+    TestDiagnosticMessagesImpl messages = new TestDiagnosticMessagesImpl();
+    parseSourceFileTemplate("My%Source%File", messages);
+    messages
+        .assertOnlyErrors()
+        .assertErrorsMatch(
+            Arrays.asList(
+                diagnosticMessage(containsString("Invalid template variable starting with %So")),
+                diagnosticMessage(containsString("Invalid template variable starting with %Fi"))));
+  }
+
+  @Test
+  public void testInvalidVariablesMix() {
+    TestDiagnosticMessagesImpl messages = new TestDiagnosticMessagesImpl();
+    parseSourceFileTemplate("My%%MAP_IDJUNK", messages);
+    messages
+        .assertOnlyErrors()
+        .assertErrorsMatch(
+            diagnosticMessage(containsString("Invalid template variable starting with %%MAP_")));
+  }
+
+  @Test
+  public void testNoEscape() {
+    TestDiagnosticMessagesImpl messages = new TestDiagnosticMessagesImpl();
+    parseSourceFileTemplate("My%%SourceFile", messages);
+    messages
+        .assertOnlyErrors()
+        .assertErrorsMatch(
+            Arrays.asList(
+                diagnosticMessage(containsString("Invalid template variable starting with %%S")),
+                diagnosticMessage(containsString("Invalid template variable starting with %So"))));
+  }
+
+  @Test
+  public void testMapId() throws Exception {
+    String template = "MySourceFile %MAP_ID";
+    String actual =
+        new CodeInspector(compileWithSourceFileTemplate(template))
+            .clazz(TestClass.class)
+            .getDexProgramClass()
+            .getSourceFile()
+            .toString();
+    assertThat(actual, startsWith("MySourceFile "));
+    assertThat(actual, not(containsString("%")));
+    assertEquals("MySourceFile ".length() + 7, actual.length());
+  }
+
+  @Test
+  public void testMapHash() throws Exception {
+    String template = "MySourceFile %MAP_HASH";
+    String actual =
+        new CodeInspector(compileWithSourceFileTemplate(template))
+            .clazz(TestClass.class)
+            .getDexProgramClass()
+            .getSourceFile()
+            .toString();
+    assertThat(actual, startsWith("MySourceFile "));
+    assertThat(actual, not(containsString("%")));
+    assertEquals("MySourceFile ".length() + 64, actual.length());
+  }
+
+  @Test
+  public void testMultiple() throws Exception {
+    String template = "id %MAP_ID hash %MAP_HASH id %MAP_ID hash %MAP_HASH";
+    String actual =
+        new CodeInspector(compileWithSourceFileTemplate(template))
+            .clazz(TestClass.class)
+            .getDexProgramClass()
+            .getSourceFile()
+            .toString();
+    assertEquals("id  hash  id  hash ".length() + 2 * 7 + 2 * 64, actual.length());
+  }
+
+  private Path compileWithSourceFileTemplate(String template)
+      throws IOException, CompilationFailedException {
+    Path out = temp.newFolder().toPath().resolve("out.jar");
+    TestDiagnosticMessagesImpl messages = new TestDiagnosticMessagesImpl();
+    R8.run(
+        parseSourceFileTemplate(template, messages)
+            .addProguardConfiguration(
+                Arrays.asList("-keep class * { *; }", "-dontwarn " + typeName(TestClass.class)),
+                Origin.unknown())
+            .setProgramConsumer(
+                backend.isCf()
+                    ? new ArchiveConsumer(out)
+                    : new DexIndexedConsumer.ArchiveConsumer(out))
+            .addProgramFiles(ToolHelper.getClassFileForTestClass(TestClass.class))
+            // TODO(b/201269335): What should be the expected result when no map is created?
+            .setProguardMapConsumer(StringConsumer.emptyConsumer())
+            .build());
+    messages.assertNoMessages();
+    return out;
+  }
+
+  private Builder parseSourceFileTemplate(String template, DiagnosticsHandler handler) {
+    return R8CommandParser.parse(
+        new String[] {"--source-file-template", template}, Origin.unknown(), handler);
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println("Hello world");
+    }
+  }
+}