Warn when default init member rules are synthesized
This also changes the current default behavior so that the synthesis of default <init> member rules is enabled.
Bug: b/356350498
Change-Id: I1b2d3ca2b038831ec81e432cd8c80c6b0b655ed8
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index 339e80b..0bd58e6 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -387,6 +387,7 @@
R8Command.Builder r8Builder =
R8Command.builder(getReporter())
.addProgramResourceProvider((ProgramResourceProvider) l8CfConsumer)
+ .setEnableEmptyMemberRulesToDefaultInitRuleConversion(false)
.setSynthesizedClassesPrefix(
desugaredLibrarySpecification.getSynthesizedLibraryClassesPackagePrefix())
.setMinApiLevel(getMinApiLevel())
diff --git a/src/main/java/com/android/tools/r8/errors/EmptyMemberRulesToDefaultInitRuleConversionDiagnostic.java b/src/main/java/com/android/tools/r8/errors/EmptyMemberRulesToDefaultInitRuleConversionDiagnostic.java
new file mode 100644
index 0000000..9a441ce
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/EmptyMemberRulesToDefaultInitRuleConversionDiagnostic.java
@@ -0,0 +1,52 @@
+// Copyright (c) 2024, 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.errors;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import com.android.tools.r8.shaking.ProguardConfigurationRule;
+import com.android.tools.r8.utils.StringUtils;
+
+@KeepForApi
+public class EmptyMemberRulesToDefaultInitRuleConversionDiagnostic implements Diagnostic {
+
+ private final ProguardConfigurationRule rule;
+
+ EmptyMemberRulesToDefaultInitRuleConversionDiagnostic(ProguardConfigurationRule rule) {
+ this.rule = rule;
+ }
+
+ @Override
+ public Origin getOrigin() {
+ return rule.getOrigin();
+ }
+
+ @Override
+ public Position getPosition() {
+ return rule.getPosition();
+ }
+
+ @Override
+ public String getDiagnosticMessage() {
+ // TODO(b/356350498): Explain how to opt-in to the new behavior when this is implemented in AGP.
+ return StringUtils.joinLines(
+ "The current version of R8 implicitly keeps the default constructor for Proguard"
+ + " configuration rules that have no member pattern. If the following rule should"
+ + " continue to keep the default constructor in the next major version of R8, then it"
+ + " must be augmented with the member pattern `{ void <init>(); }` to explicitly keep"
+ + " the default constructor:",
+ rule.toString());
+ }
+
+ // Non-kept factory to avoid that the constructor of the diagnostic is public API.
+ public static class Factory {
+
+ public static EmptyMemberRulesToDefaultInitRuleConversionDiagnostic create(
+ ProguardConfigurationRule rule) {
+ return new EmptyMemberRulesToDefaultInitRuleConversionDiagnostic(rule);
+ }
+ }
+}
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 fc42207..f511bee 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -10,6 +10,7 @@
import com.android.tools.r8.InputDependencyGraphConsumer;
import com.android.tools.r8.Version;
import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.errors.EmptyMemberRulesToDefaultInitRuleConversionDiagnostic;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
@@ -32,6 +33,7 @@
import com.android.tools.r8.utils.ThrowingAction;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -127,6 +129,7 @@
dexItemFactory,
reporter,
ProguardConfigurationParserOptions.builder()
+ .setEnableEmptyMemberRulesToDefaultInitRuleConversion(false)
.setEnableExperimentalCheckEnumUnboxed(false)
.setEnableExperimentalConvertCheckNotNull(false)
.setEnableExperimentalWhyAreYouNotInlining(false)
@@ -846,28 +849,31 @@
.setStart(start);
parseRuleTypeAndModifiers(keepRuleBuilder);
parseClassSpec(keepRuleBuilder);
- if (options.isEmptyMemberRulesToDefaultInitRuleConversionEnabled(configurationBuilder)) {
- if (keepRuleBuilder.getMemberRules().isEmpty()
- && keepRuleBuilder.getKeepRuleType()
- != ProguardKeepRuleType.KEEP_CLASSES_WITH_MEMBERS) {
- // If there are no member rules, a default rule for the parameterless constructor applies
- // in compatibility mode.
- keepRuleBuilder
- .getMemberRules()
- .add(
- ProguardMemberRule.builder()
- .setName(
- IdentifierPatternWithWildcards.withoutWildcards(
- Constants.INSTANCE_INITIALIZER_NAME))
- .setRuleType(ProguardMemberType.INIT)
- .setArguments(Collections.emptyList())
- .build());
- }
- }
Position end = getPosition();
- keepRuleBuilder.setSource(getSourceSnippet(contents, start, end));
- keepRuleBuilder.setEnd(end);
- return keepRuleBuilder.build();
+ ProguardKeepRule rule =
+ keepRuleBuilder.setSource(getSourceSnippet(contents, start, end)).setEnd(end).build();
+ if (options.isEmptyMemberRulesToDefaultInitRuleConversionEnabled(configurationBuilder)
+ && rule.getMemberRules().isEmpty()
+ && rule.getType() != ProguardKeepRuleType.KEEP_CLASSES_WITH_MEMBERS) {
+ // If there are no member rules, a default rule for the parameterless constructor applies
+ // in compatibility mode.
+ if (options.isEmptyMemberRulesToDefaultInitRuleConversionWarningsEnabled(
+ configurationBuilder)) {
+ reporter.warning(
+ EmptyMemberRulesToDefaultInitRuleConversionDiagnostic.Factory.create(rule));
+ }
+ List<ProguardMemberRule> memberRules =
+ Lists.newArrayList(
+ ProguardMemberRule.builder()
+ .setName(
+ IdentifierPatternWithWildcards.withoutWildcards(
+ Constants.INSTANCE_INITIALIZER_NAME))
+ .setRuleType(ProguardMemberType.INIT)
+ .setArguments(Collections.emptyList())
+ .build());
+ rule = keepRuleBuilder.setMemberRules(memberRules).build();
+ }
+ return rule;
}
@SuppressWarnings("NonCanonicalType")
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java
index 157be68..07bea60 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParserOptions.java
@@ -6,16 +6,18 @@
import static com.android.tools.r8.utils.SystemPropertyUtils.parseSystemPropertyOrDefault;
+import com.android.tools.r8.utils.OptionalBool;
+
public class ProguardConfigurationParserOptions {
- private final boolean enableEmptyMemberRulesToDefaultInitRuleConversion;
+ private final OptionalBool enableEmptyMemberRulesToDefaultInitRuleConversion;
private final boolean enableExperimentalCheckEnumUnboxed;
private final boolean enableExperimentalConvertCheckNotNull;
private final boolean enableExperimentalWhyAreYouNotInlining;
private final boolean enableTestingOptions;
ProguardConfigurationParserOptions(
- boolean enableEmptyMemberRulesToDefaultInitRuleConversion,
+ OptionalBool enableEmptyMemberRulesToDefaultInitRuleConversion,
boolean enableExperimentalCheckEnumUnboxed,
boolean enableExperimentalConvertCheckNotNull,
boolean enableExperimentalWhyAreYouNotInlining,
@@ -34,8 +36,16 @@
public boolean isEmptyMemberRulesToDefaultInitRuleConversionEnabled(
ProguardConfiguration.Builder configurationBuilder) {
- return enableEmptyMemberRulesToDefaultInitRuleConversion
- || configurationBuilder.isForceProguardCompatibility();
+ // TODO(b/356344563): Disable in full mode in the next major version.
+ return configurationBuilder.isForceProguardCompatibility()
+ || enableEmptyMemberRulesToDefaultInitRuleConversion.getOrDefault(true);
+ }
+
+ public boolean isEmptyMemberRulesToDefaultInitRuleConversionWarningsEnabled(
+ ProguardConfiguration.Builder configurationBuilder) {
+ assert isEmptyMemberRulesToDefaultInitRuleConversionEnabled(configurationBuilder);
+ return !configurationBuilder.isForceProguardCompatibility()
+ && enableEmptyMemberRulesToDefaultInitRuleConversion.isUnknown();
}
public boolean isExperimentalCheckEnumUnboxedEnabled() {
@@ -56,7 +66,7 @@
public static class Builder {
- private boolean enableEmptyMemberRulesToDefaultInitRuleConversion;
+ private OptionalBool enableEmptyMemberRulesToDefaultInitRuleConversion = OptionalBool.UNKNOWN;
private boolean enableExperimentalCheckEnumUnboxed;
private boolean enableExperimentalConvertCheckNotNull;
private boolean enableExperimentalWhyAreYouNotInlining;
@@ -65,7 +75,8 @@
public Builder readEnvironment() {
enableEmptyMemberRulesToDefaultInitRuleConversion =
parseSystemPropertyOrDefault(
- "com.android.tools.r8.enableEmptyMemberRulesToDefaultInitRuleConversion", false);
+ "com.android.tools.r8.enableEmptyMemberRulesToDefaultInitRuleConversion",
+ OptionalBool.UNKNOWN);
enableExperimentalCheckEnumUnboxed =
parseSystemPropertyOrDefault(
"com.android.tools.r8.experimental.enablecheckenumunboxed", false);
@@ -83,7 +94,7 @@
public Builder setEnableEmptyMemberRulesToDefaultInitRuleConversion(
boolean enableEmptyMemberRulesToDefaultInitRuleConversion) {
this.enableEmptyMemberRulesToDefaultInitRuleConversion =
- enableEmptyMemberRulesToDefaultInitRuleConversion;
+ OptionalBool.of(enableEmptyMemberRulesToDefaultInitRuleConversion);
return this;
}
diff --git a/src/main/java/com/android/tools/r8/utils/OptionalBool.java b/src/main/java/com/android/tools/r8/utils/OptionalBool.java
index 06cd098..b7c0708 100644
--- a/src/main/java/com/android/tools/r8/utils/OptionalBool.java
+++ b/src/main/java/com/android/tools/r8/utils/OptionalBool.java
@@ -58,6 +58,11 @@
new OptionalBool() {
@Override
+ public boolean getOrDefault(boolean defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
public boolean isUnknown() {
return true;
}
@@ -93,6 +98,10 @@
return this;
}
+ public boolean getOrDefault(boolean defaultValue) {
+ return toBoolean();
+ }
+
public abstract boolean toBoolean();
@Override
diff --git a/src/main/java/com/android/tools/r8/utils/SystemPropertyUtils.java b/src/main/java/com/android/tools/r8/utils/SystemPropertyUtils.java
index 57cb56d..33dffa1 100644
--- a/src/main/java/com/android/tools/r8/utils/SystemPropertyUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/SystemPropertyUtils.java
@@ -7,6 +7,8 @@
import static com.google.common.base.Predicates.alwaysTrue;
import com.android.tools.r8.Version;
+import com.android.tools.r8.errors.Unreachable;
+import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@@ -59,19 +61,34 @@
public static boolean parseSystemPropertyOrDefault(String propertyName, boolean defaultValue) {
return internalParseSystemPropertyForDevelopmentOrDefault(
- propertyName, System.getProperty(propertyName), defaultValue);
+ propertyName, System.getProperty(propertyName), () -> defaultValue);
+ }
+
+ public static OptionalBool parseSystemPropertyOrDefault(
+ String propertyName, OptionalBool defaultValue) {
+ if (System.getProperty(propertyName) == null) {
+ return defaultValue;
+ }
+ boolean value =
+ internalParseSystemPropertyForDevelopmentOrDefault(
+ propertyName,
+ System.getProperty(propertyName),
+ () -> {
+ throw new Unreachable();
+ });
+ return OptionalBool.of(value);
}
public static boolean parseSystemPropertyForDevelopmentOrDefault(
String propertyName, boolean defaultValue) {
return internalParseSystemPropertyForDevelopmentOrDefault(
- propertyName, getSystemPropertyForDevelopment(propertyName), defaultValue);
+ propertyName, getSystemPropertyForDevelopment(propertyName), () -> defaultValue);
}
private static boolean internalParseSystemPropertyForDevelopmentOrDefault(
- String propertyName, String propertyValue, boolean defaultValue) {
+ String propertyName, String propertyValue, BooleanSupplier defaultValue) {
if (propertyValue == null) {
- return defaultValue;
+ return defaultValue.getAsBoolean();
}
if (StringUtils.isFalsy(propertyValue)) {
return false;
diff --git a/src/test/java/com/android/tools/r8/compilerapi/diagnostics/ProguardKeepRuleDiagnosticsApiTest.java b/src/test/java/com/android/tools/r8/compilerapi/diagnostics/ProguardKeepRuleDiagnosticsApiTest.java
index fedb449..e4ed3bb 100644
--- a/src/test/java/com/android/tools/r8/compilerapi/diagnostics/ProguardKeepRuleDiagnosticsApiTest.java
+++ b/src/test/java/com/android/tools/r8/compilerapi/diagnostics/ProguardKeepRuleDiagnosticsApiTest.java
@@ -98,6 +98,7 @@
.addProguardConfiguration(
Collections.singletonList("-keep class NotPresent {}"), Origin.unknown())
.addLibraryFiles(getJava8RuntimeJar())
+ .setEnableEmptyMemberRulesToDefaultInitRuleConversion(false)
.setProgramConsumer(DexIndexedConsumer.emptyConsumer())
.setMinApiLevel(1)
.build());
diff --git a/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java b/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java
index bdb0a85..564eac9 100644
--- a/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/EmptyMemberRulesToDefaultInitRuleConversionTest.java
@@ -3,13 +3,23 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.shaking;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentIf;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeTrue;
+import com.android.tools.r8.R8CompatTestBuilder;
+import com.android.tools.r8.R8TestBuilder;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.errors.EmptyMemberRulesToDefaultInitRuleConversionDiagnostic;
import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.StringUtils;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -34,19 +44,63 @@
}
@Test
- public void testCompat() throws Exception {
+ public void testCompatDefault() throws Exception {
+ assumeTrue(enableEmptyMemberRulesToDefaultInitRuleConversion);
+ testCompat(R8TestBuilder::clearEnableEmptyMemberRulesToDefaultInitRuleConversion);
+ }
+
+ @Test
+ public void testCompatExplicit() throws Exception {
+ testCompat(
+ testBuilder ->
+ testBuilder.enableEmptyMemberRulesToDefaultInitRuleConversion(
+ enableEmptyMemberRulesToDefaultInitRuleConversion));
+ }
+
+ private void testCompat(ThrowableConsumer<? super R8CompatTestBuilder> configuration)
+ throws Exception {
testForR8Compat(parameters.getBackend())
.addInnerClasses(getClass())
.addKeepClassRules(Main.class)
- .enableEmptyMemberRulesToDefaultInitRuleConversion(
- enableEmptyMemberRulesToDefaultInitRuleConversion)
+ .apply(configuration)
.setMinApi(parameters)
.compile()
.inspect(inspector -> assertThat(inspector.clazz(Main.class).init(), isPresent()));
}
@Test
- public void testFull() throws Exception {
+ public void testFullDefault() throws Exception {
+ assumeTrue(enableEmptyMemberRulesToDefaultInitRuleConversion);
+ testForR8(parameters.getBackend())
+ .addInnerClasses(getClass())
+ .addKeepClassRules(Main.class)
+ .allowDiagnosticWarningMessages()
+ .clearEnableEmptyMemberRulesToDefaultInitRuleConversion()
+ .setMinApi(parameters)
+ .compileWithExpectedDiagnostics(
+ diagnostics ->
+ diagnostics
+ .assertOnlyWarnings()
+ .assertWarningsMatch(
+ allOf(
+ diagnosticType(
+ EmptyMemberRulesToDefaultInitRuleConversionDiagnostic.class),
+ diagnosticMessage(
+ equalTo(
+ StringUtils.joinLines(
+ "The current version of R8 implicitly keeps the default"
+ + " constructor for Proguard configuration rules that"
+ + " have no member pattern. If the following rule"
+ + " should continue to keep the default constructor in"
+ + " the next major version of R8, then it must be"
+ + " augmented with the member pattern `{ void <init>();"
+ + " }` to explicitly keep the default constructor:",
+ "-keep class " + Main.class.getTypeName()))))))
+ .inspect(inspector -> assertThat(inspector.clazz(Main.class).init(), isPresent()));
+ }
+
+ @Test
+ public void testFullExplicit() throws Exception {
testForR8(parameters.getBackend())
.addInnerClasses(getClass())
.addKeepClassRules(Main.class)
diff --git a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
index 3309783..0b7d432 100644
--- a/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/testbase/java/com/android/tools/r8/R8TestBuilder.java
@@ -40,6 +40,7 @@
import com.android.tools.r8.utils.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.MapIdTemplateProvider;
+import com.android.tools.r8.utils.OptionalBool;
import com.android.tools.r8.utils.Pair;
import com.android.tools.r8.utils.SemanticVersion;
import com.android.tools.r8.utils.SourceFileTemplateProvider;
@@ -76,7 +77,7 @@
private AllowedDiagnosticMessages allowedDiagnosticMessages = AllowedDiagnosticMessages.NONE;
private boolean allowUnusedProguardConfigurationRules = false;
- private boolean enableEmptyMemberRulesToDefaultInitRuleConversion = false;
+ private OptionalBool enableEmptyMemberRulesToDefaultInitRuleConversion = OptionalBool.FALSE;
private boolean enableIsolatedSplits = false;
private boolean enableMissingLibraryApiModeling = true;
private boolean enableStartupLayoutOptimization = true;
@@ -140,8 +141,10 @@
ToolHelper.addSyntheticProguardRulesConsumerForTesting(
builder, rules -> box.syntheticProguardRules = rules);
libraryDesugaringTestConfiguration.configure(builder);
- builder.setEnableEmptyMemberRulesToDefaultInitRuleConversion(
- enableEmptyMemberRulesToDefaultInitRuleConversion);
+ if (!enableEmptyMemberRulesToDefaultInitRuleConversion.isUnknown()) {
+ builder.setEnableEmptyMemberRulesToDefaultInitRuleConversion(
+ enableEmptyMemberRulesToDefaultInitRuleConversion.toBoolean());
+ }
builder.setEnableIsolatedSplits(enableIsolatedSplits);
builder.setEnableExperimentalMissingLibraryApiModeling(enableMissingLibraryApiModeling);
builder.setEnableStartupLayoutOptimization(enableStartupLayoutOptimization);
@@ -888,7 +891,12 @@
public T enableEmptyMemberRulesToDefaultInitRuleConversion(
boolean enableEmptyMemberRulesToDefaultInitRuleConversion) {
this.enableEmptyMemberRulesToDefaultInitRuleConversion =
- enableEmptyMemberRulesToDefaultInitRuleConversion;
+ OptionalBool.of(enableEmptyMemberRulesToDefaultInitRuleConversion);
+ return self();
+ }
+
+ public T clearEnableEmptyMemberRulesToDefaultInitRuleConversion() {
+ this.enableEmptyMemberRulesToDefaultInitRuleConversion = OptionalBool.UNKNOWN;
return self();
}
diff --git a/tools/create_r8lib.py b/tools/create_r8lib.py
index c8eb00e..1f02869 100755
--- a/tools/create_r8lib.py
+++ b/tools/create_r8lib.py
@@ -95,6 +95,8 @@
'-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005'
])
cmd.append('-Dcom.android.tools.r8.enableKeepAnnotations=1')
+ # TODO(b/356344563): Remove when this is default.
+ cmd.append('-Dcom.android.tools.r8.enableEmptyMemberRulesToDefaultInitRuleConversion=0')
cmd.extend(['-cp', args.r8compiler, 'com.android.tools.r8.R8'])
cmd.append(args.r8jar)
if args.debug_variant: