Prevent minifier to rename into classpath names
Bug: b/414335863
Change-Id: I616d16a7d4a3fd2a4074e97da2735b57b720e55d
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index f6279b7..b148ad9 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -541,6 +541,8 @@
// Collect the already pruned types before creating a new app info without liveness.
// TODO: we should avoid removing liveness.
Set<DexType> prunedTypes = appView.withLiveness().appInfo().getPrunedTypes();
+ Set<DexType> prunedClasspathTypes =
+ appView.withLiveness().appInfo().getClasspathTypesIncludingPruned();
assert ArtProfileCompletenessChecker.verify(appView);
@@ -584,6 +586,7 @@
ImmediateAppSubtypingInfo.create(appView),
keptGraphConsumer,
prunedTypes,
+ prunedClasspathTypes,
finalRuntimeTypeCheckInfoBuilder);
EnqueuerResult enqueuerResult =
enqueuer.traceApplication(appView.rootSet(), executorService, timing);
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
index 4d87bcd..c6bc840 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -88,11 +88,18 @@
ClassRenaming computeRenaming(Timing timing) {
// Collect names we have to keep.
timing.begin("reserve");
+ if (appView.options().isMinifying()
+ && !appView.options().getProguardConfiguration().hasApplyMappingFile()) {
+ for (DexType reserved : appView.appInfo().getClasspathTypesIncludingPruned()) {
+ registerClassAsUsed(reserved, reserved.getDescriptor());
+ }
+ }
for (ProgramOrClasspathClass clazz : classes) {
- DexString descriptor = classNamingStrategy.reservedDescriptor(clazz.getType());
+ DexType type = clazz.getType();
+ DexString descriptor = classNamingStrategy.reservedDescriptor(type);
if (descriptor != null) {
- assert !renaming.containsKey(clazz.getType());
- registerClassAsUsed(clazz.getType(), descriptor);
+ assert !renaming.containsKey(type);
+ registerClassAsUsed(type, descriptor);
}
}
appView
diff --git a/src/main/java/com/android/tools/r8/naming/Minifier.java b/src/main/java/com/android/tools/r8/naming/Minifier.java
index 6828ffd..d1ac802 100644
--- a/src/main/java/com/android/tools/r8/naming/Minifier.java
+++ b/src/main/java/com/android/tools/r8/naming/Minifier.java
@@ -76,7 +76,7 @@
assert new MinifiedRenaming(
appView, classRenaming, MethodRenaming.empty(), FieldRenaming.empty())
- .verifyNoCollisions(appView.appInfo().classes(), appView.dexItemFactory());
+ .verifyNoCollisions(appView.appInfo(), appView.dexItemFactory());
MemberNamingStrategy minifyMembers = new MinifierMemberNamingStrategy(appView);
timing.begin("MinifyMethods");
@@ -86,7 +86,7 @@
timing.end();
assert new MinifiedRenaming(appView, classRenaming, methodRenaming, FieldRenaming.empty())
- .verifyNoCollisions(appView.appInfo().classes(), appView.dexItemFactory());
+ .verifyNoCollisions(appView.appInfo(), appView.dexItemFactory());
timing.begin("MinifyFields");
FieldRenaming fieldRenaming =
@@ -100,7 +100,7 @@
timing.end();
NamingLens lens = new MinifiedRenaming(appView, classRenaming, methodRenaming, fieldRenaming);
- assert lens.verifyNoCollisions(appView.appInfo().classes(), appView.dexItemFactory());
+ assert lens.verifyNoCollisions(appView.appInfo(), appView.dexItemFactory());
appView.testing().namingLensConsumer.accept(appView.dexItemFactory(), lens);
appView.notifyOptimizationFinishedForTesting();
diff --git a/src/main/java/com/android/tools/r8/naming/NamingLens.java b/src/main/java/com/android/tools/r8/naming/NamingLens.java
index bc8b12f..dbe8fa4 100644
--- a/src/main/java/com/android/tools/r8/naming/NamingLens.java
+++ b/src/main/java/com/android/tools/r8/naming/NamingLens.java
@@ -24,6 +24,8 @@
import com.android.tools.r8.utils.collections.DexClassAndMethodSet;
import com.google.common.collect.Sets;
import java.util.Arrays;
+import java.util.IdentityHashMap;
+import java.util.Map;
import java.util.Set;
/**
@@ -157,14 +159,35 @@
public abstract boolean verifyRenamingConsistentWithResolution(DexMethod item);
public final boolean verifyNoCollisions(
- Iterable<DexProgramClass> classes, DexItemFactory dexItemFactory) {
+ AppInfoWithLiveness appInfo, DexItemFactory dexItemFactory) {
Set<DexReference> references = Sets.newIdentityHashSet();
- for (DexProgramClass clazz : classes) {
+ Map<DexType, DexType> classpathTypes = new IdentityHashMap<>();
+ appInfo
+ .getClasspathTypesIncludingPruned()
+ .forEach(
+ type -> {
+ DexType newType = lookupType(type, dexItemFactory);
+ classpathTypes.put(newType, type);
+ });
+ for (DexProgramClass clazz : appInfo.classes()) {
{
DexType newType = lookupType(clazz.type, dexItemFactory);
boolean referencesChanged = references.add(newType);
assert referencesChanged
: "Duplicate definition of type `" + newType.toSourceString() + "`";
+ if (classpathTypes.containsKey(newType)) {
+ // If we have a classpath and program type, then it can be renamed the same way. It is
+ // forbidden to rename a program type into a classpath type, since it would lead to
+ // duplicated types.
+ assert classpathTypes.get(newType).isIdenticalTo(clazz.type)
+ : "Duplicate predecessor of type `"
+ + newType.toSourceString()
+ + "`: `"
+ + classpathTypes.get(newType).toSourceString()
+ + "`,`"
+ + clazz.type.toSourceString()
+ + "`";
+ }
}
for (DexEncodedField field : clazz.fields()) {
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index b9dee9f..3d5de3b 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -65,6 +65,7 @@
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.PredicateSet;
+import com.android.tools.r8.utils.SetUtils;
import com.android.tools.r8.utils.Visibility;
import com.android.tools.r8.utils.collections.DexClassAndMethodSet;
import com.android.tools.r8.utils.collections.ProgramMethodSet;
@@ -171,6 +172,13 @@
* ).
*/
public final Object2BooleanMap<DexMember<?, ?>> identifierNameStrings;
+
+ /**
+ * A set of classpath types that are not referenced from the app, but which names can still lead
+ * to collusions in the minifier.
+ */
+ final Set<DexType> prunedClasspathTypes;
+
/** A set of types that have been removed by the {@link TreePruner}. */
final Set<DexType> prunedTypes;
/** A map from switchmap class types to their corresponding switchmaps. */
@@ -205,6 +213,7 @@
PredicateSet<DexType> alwaysClassInline,
Object2BooleanMap<DexMember<?, ?>> identifierNameStrings,
Set<DexType> prunedTypes,
+ Set<DexType> prunedClasspathTypes,
Map<DexField, Int2ReferenceMap<DexField>> switchMaps,
Set<DexType> lockCandidates,
Map<DexType, Visibility> initClassReferences,
@@ -230,6 +239,7 @@
this.alwaysClassInline = alwaysClassInline;
this.identifierNameStrings = identifierNameStrings;
this.prunedTypes = prunedTypes;
+ this.prunedClasspathTypes = prunedClasspathTypes;
this.switchMaps = switchMaps;
this.lockCandidates = lockCandidates;
this.initClassReferences = initClassReferences;
@@ -263,6 +273,7 @@
previous.alwaysClassInline,
previous.identifierNameStrings,
previous.prunedTypes,
+ previous.prunedClasspathTypes,
previous.switchMaps,
previous.lockCandidates,
previous.initClassReferences,
@@ -299,6 +310,7 @@
prunedItems.hasRemovedClasses()
? CollectionUtils.mergeSets(previous.prunedTypes, prunedItems.getRemovedClasses())
: previous.prunedTypes,
+ previous.prunedClasspathTypes,
previous.switchMaps,
pruneClasses(previous.lockCandidates, prunedItems, tasks),
pruneMapFromClasses(previous.initClassReferences, prunedItems, tasks),
@@ -439,6 +451,7 @@
alwaysClassInline,
identifierNameStrings,
prunedTypes,
+ prunedClasspathTypes,
switchMaps,
lockCandidates,
initClassReferences,
@@ -509,6 +522,7 @@
this.alwaysClassInline = previous.alwaysClassInline;
this.identifierNameStrings = previous.identifierNameStrings;
this.prunedTypes = previous.prunedTypes;
+ this.prunedClasspathTypes = previous.prunedClasspathTypes;
this.switchMaps = switchMaps;
this.lockCandidates = previous.lockCandidates;
this.initClassReferences = previous.initClassReferences;
@@ -1007,6 +1021,7 @@
lens.rewriteReferenceKeys(identifierNameStrings),
// Don't rewrite pruned types - the removed types are identified by their original name.
prunedTypes,
+ prunedClasspathTypes,
lens.rewriteFieldKeys(switchMaps),
lens.rewriteReferences(lockCandidates),
rewriteInitClassReferences(lens),
@@ -1046,6 +1061,18 @@
return prunedTypes;
}
+ public Set<DexType> getClasspathTypesIncludingPruned() {
+ assert checkIfObsolete();
+ Collection<DexClasspathClass> classpathClasses = app().asDirect().classpathClasses();
+ Set<DexType> classpath =
+ SetUtils.newIdentityHashSet(classpathClasses.size() + prunedClasspathTypes.size());
+ for (DexClasspathClass cp : classpathClasses) {
+ classpath.add(cp.getType());
+ }
+ classpath.addAll(prunedClasspathTypes);
+ return classpath;
+ }
+
public DexClassAndMethod lookupSingleTarget(
AppView<AppInfoWithLiveness> appView,
InvokeType type,
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 f17e905..9788f61 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -334,6 +334,9 @@
/** Set of types that was pruned during the first round of tree shaking. */
private final Set<DexType> initialPrunedTypes;
+ /** List of classpath types that was pruned during the first round of tree shaking. */
+ private final Set<DexType> prunedClasspathTypes;
+
private final Set<DexType> noClassMerging = Sets.newIdentityHashSet();
/** Mapping from each unused interface to the set of live types that implements the interface. */
@@ -477,6 +480,7 @@
keptGraphConsumer,
mode,
null,
+ null,
null);
}
@@ -488,6 +492,7 @@
GraphConsumer keptGraphConsumer,
Mode mode,
Set<DexType> initialPrunedTypes,
+ Set<DexType> prunedClasspathTypes,
RuntimeTypeCheckInfo.Builder runtimeTypeCheckInfoBuilder) {
assert appView.appServices() != null;
InternalOptions options = appView.options();
@@ -512,6 +517,7 @@
? ProguardCompatibilityActions.builder()
: null;
this.initialPrunedTypes = initialPrunedTypes;
+ this.prunedClasspathTypes = prunedClasspathTypes;
if (options.isOptimizedResourceShrinking()) {
R8ResourceShrinkerState resourceShrinkerState = appView.getResourceShrinkerState();
@@ -4530,6 +4536,15 @@
// Add just referenced non-program types. We can't replace the program classes at this point as
// they are needed in tree pruning.
+ ImmutableSet.Builder<DexType> prunedClasspathTypesBuilder = ImmutableSet.builder();
+ if (prunedClasspathTypes != null) {
+ prunedClasspathTypesBuilder.addAll(prunedClasspathTypes);
+ }
+ for (DexClasspathClass classpathClass : appInfo.app().asDirect().classpathClasses()) {
+ if (!classpathClasses.contains(classpathClass)) {
+ prunedClasspathTypesBuilder.add(classpathClass.getType());
+ }
+ }
DirectMappedDexApplication app =
appInfo
.app()
@@ -4585,6 +4600,7 @@
joinIdentifierNameStrings(
rootSet.identifierNameStrings, reflectiveIdentification.getIdentifierNameStrings()),
emptySet(),
+ prunedClasspathTypesBuilder.build(),
Collections.emptyMap(),
lockCandidates,
initClassReferences,
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerFactory.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerFactory.java
index 1095378..b4f2984 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerFactory.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerFactory.java
@@ -36,6 +36,7 @@
ImmediateAppSubtypingInfo subtypingInfo,
GraphConsumer keptGraphConsumer,
Set<DexType> initialPrunedTypes,
+ Set<DexType> prunedClasspathTypes,
RuntimeTypeCheckInfo.Builder runtimeTypeCheckInfoBuilder) {
ProfileCollectionAdditions profileCollectionAdditions =
ProfileCollectionAdditions.create(appView);
@@ -48,6 +49,7 @@
keptGraphConsumer,
Mode.FINAL_TREE_SHAKING,
initialPrunedTypes,
+ prunedClasspathTypes,
runtimeTypeCheckInfoBuilder);
appView.withProtoShrinker(
shrinker -> enqueuer.setInitialDeadProtoTypes(shrinker.getDeadProtoTypes()));
diff --git a/src/test/java/com/android/tools/r8/naming/ProgramTypeRenamedAsClasspathTypeTest.java b/src/test/java/com/android/tools/r8/naming/ProgramTypeRenamedAsClasspathTypeTest.java
new file mode 100644
index 0000000..1ba9022
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/ProgramTypeRenamedAsClasspathTypeTest.java
@@ -0,0 +1,115 @@
+// Copyright (c) 2025, 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;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.JdkClassFileProvider;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import java.nio.file.Path;
+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.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ProgramTypeRenamedAsClasspathTypeTest extends TestBase {
+
+ @Parameter public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters()
+ .withCfRuntimesStartingFromIncluding(CfVm.JDK24)
+ .withDexRuntimes()
+ .withAllApiLevelsAlsoForCf()
+ .withPartialCompilation()
+ .build();
+ }
+
+ public static String EXPECTED_OUTPUT = StringUtils.lines("program side", "classpath side");
+
+ @Test
+ public void testJvm() throws Exception {
+ parameters.assumeJvmTestParameters();
+ testForJvm(parameters)
+ .addInnerClassesAndStrippedOuter(getClass())
+ .run(parameters.getRuntime(), ProgramMain.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ @Test
+ public void testR8Split() throws Exception {
+ parameters.assumeR8TestParameters();
+ Set<String> types = new HashSet<>();
+ R8TestCompileResult compile =
+ testForR8(Backend.CF)
+ .addProgramClasses(ClasspathSide.class, ClasspathMain.class)
+ .addLibraryProvider(JdkClassFileProvider.fromSystemJdk())
+ .addKeepMainRule(ClasspathMain.class)
+ .addKeepClassAndMembersRulesWithAllowObfuscation(ClasspathSide.class)
+ .compile();
+ compile.inspect(
+ i -> {
+ assertEquals(2, i.allClasses().size());
+ for (FoundClassSubject clazz : i.allClasses()) {
+ types.add(clazz.getDexProgramClass().getTypeName());
+ }
+ });
+ Path classpath = compile.writeToZip();
+ Path runClasspath = testForD8(parameters).addProgramFiles(classpath).compile().writeToZip();
+ testForR8(parameters)
+ .addProgramClasses(ProgramSide.class, ProgramMain.class)
+ .addClasspathFiles(classpath)
+ .addKeepMainRule(ProgramMain.class)
+ .addKeepClassAndMembersRulesWithAllowObfuscation(ProgramSide.class)
+ .compile()
+ .inspect(
+ i -> {
+ assertEquals(2, i.allClasses().size());
+ for (FoundClassSubject clazz : i.allClasses()) {
+ types.add(clazz.getDexProgramClass().getTypeName());
+ }
+ assertEquals(4, types.size());
+ })
+ .addRunClasspathFiles(runClasspath)
+ .run(parameters.getRuntime(), ProgramMain.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT);
+ }
+
+ static class ClasspathMain {
+ public static void main(String[] args) {
+ ClasspathSide.print();
+ }
+ }
+
+ static class ClasspathSide {
+ public static void print() {
+ System.out.println("classpath side");
+ }
+ }
+
+ public static class ProgramMain {
+ public static void main(String[] args) {
+ ProgramSide.print();
+ ClasspathMain.main(args);
+ }
+ }
+
+ static class ProgramSide {
+ public static void print() {
+ System.out.println("program side");
+ }
+ }
+}
diff --git a/src/test/java24/com/android/tools/r8/jdk24/switchpatternmatching/EnumSwitchOldSyntaxV2Test.java b/src/test/java24/com/android/tools/r8/jdk24/switchpatternmatching/EnumSwitchOldSyntaxV2Test.java
index c1e3f1d..fede4cb 100644
--- a/src/test/java24/com/android/tools/r8/jdk24/switchpatternmatching/EnumSwitchOldSyntaxV2Test.java
+++ b/src/test/java24/com/android/tools/r8/jdk24/switchpatternmatching/EnumSwitchOldSyntaxV2Test.java
@@ -12,7 +12,6 @@
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.TestRuntime.CfVm;
import com.android.tools.r8.utils.StringUtils;
-import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -69,7 +68,6 @@
@Test
public void testR8Split() throws Exception {
- Assume.assumeFalse("TODO(b/414335863)", parameters.isRandomPartialCompilation());
parameters.assumeR8TestParameters();
R8TestCompileResult compile =
testForR8(Backend.CF)