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");
+ }
+ }
+}