From 6dfd878ebb7c4c5b417fbff6cc7f5f84e6d41147 Mon Sep 17 00:00:00 2001 From: "niccolo.piazzesi" Date: Tue, 14 Oct 2025 12:00:16 +0200 Subject: [PATCH] Run InterfaceUsageMarker before NestUsageMarker to prevent missing marking of permitted subclasses attribute Closes #501 1. [ClassUsageMarker assumes that the NestUsageMarker will mark the permitted subclasses](https://github.com/Guardsquare/proguard/blob/869ce156b1e9a731612aca142e712a0564da3847/base/src/main/java/proguard/shrink/ClassUsageMarker.java#L1144). 2. [ NestUsageMarker only marks class constants in the permittedSubclasses attribute if the referenced class is used ](https://github.com/Guardsquare/proguard/blob/869ce156b1e9a731612aca142e712a0564da3847/base/src/main/java/proguard/shrink/NestUsageMarker.java#L100). 3. [ If the referenced class is another interface, this is marked by the InterfaceUsageMarker ](https://github.com/Guardsquare/proguard/blob/869ce156b1e9a731612aca142e712a0564da3847/base/src/main/java/proguard/shrink/InterfaceUsageMarker.java#L36). 4. [However, the interfaceUsageMarker only runs after the NestUsageMarker, hence nothing gets marked if a sealed interface permits another sealed interface](https://github.com/Guardsquare/proguard/blob/869ce156b1e9a731612aca142e712a0564da3847/base/src/main/java/proguard/shrink/UsageMarker.java#L119-130) --- .../java/proguard/shrink/UsageMarker.java | 7 +- .../kotlin/proguard/shrink/UsageMarkerTest.kt | 150 ++++++++++++++++++ docs/md/manual/releasenotes.md | 6 + gradle.properties | 2 +- 4 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 base/src/test/kotlin/proguard/shrink/UsageMarkerTest.kt diff --git a/base/src/main/java/proguard/shrink/UsageMarker.java b/base/src/main/java/proguard/shrink/UsageMarker.java index e4fdc71b..27ef0b3a 100644 --- a/base/src/main/java/proguard/shrink/UsageMarker.java +++ b/base/src/main/java/proguard/shrink/UsageMarker.java @@ -106,6 +106,10 @@ public class UsageMarker classUsageMarker)) )); + // Mark interfaces that have to be kept. This must be before the NestUsageMarker call right after, + // see https://github.com/Guardsquare/proguard/issues/501. + programClassPool.classesAccept(new InterfaceUsageMarker(classUsageMarker)); + // Mark the elements of Kotlin metadata that need to be kept. if (configuration.keepKotlinMetadata) { @@ -114,7 +118,6 @@ public class UsageMarker new ReferencedKotlinMetadataVisitor( classUsageMarker)); } - // Mark the inner class and annotation information that has to be kept. programClassPool.classesAccept( new UsedClassFilter(simpleUsageMarker, @@ -126,8 +129,6 @@ public class UsageMarker new LocalVariableTypeUsageMarker(classUsageMarker) )))); - // Mark interfaces that have to be kept. - programClassPool.classesAccept(new InterfaceUsageMarker(classUsageMarker)); if (configuration.keepKotlinMetadata) { diff --git a/base/src/test/kotlin/proguard/shrink/UsageMarkerTest.kt b/base/src/test/kotlin/proguard/shrink/UsageMarkerTest.kt new file mode 100644 index 00000000..7fad8bf9 --- /dev/null +++ b/base/src/test/kotlin/proguard/shrink/UsageMarkerTest.kt @@ -0,0 +1,150 @@ +package proguard.shrink + +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import proguard.Configuration +import proguard.classfile.ClassPool +import proguard.classfile.Clazz +import proguard.classfile.attribute.Attribute +import proguard.classfile.attribute.PermittedSubclassesAttribute +import proguard.classfile.attribute.visitor.AllAttributeVisitor +import proguard.classfile.attribute.visitor.AttributeVisitor +import proguard.classfile.constant.ClassConstant +import proguard.classfile.constant.Constant +import proguard.classfile.constant.visitor.ConstantVisitor +import proguard.classfile.visitor.AllMemberVisitor +import proguard.classfile.visitor.MultiClassVisitor +import proguard.resources.file.ResourceFilePool +import proguard.testutils.ClassPoolBuilder +import proguard.testutils.JavaSource +import proguard.testutils.RequiresJavaVersion +import proguard.util.ProcessingFlagSetter +import proguard.util.ProcessingFlags.DONT_SHRINK + +@RequiresJavaVersion(15) +class Java15UsageMarkerTest : BehaviorSpec({ + // Regression test for https://github.com/Guardsquare/proguard/issues/501 + Given("A class pool containing a sealed interface extending another sealed interface, and final classes implementing both") { + val (programClassPool, _) = ClassPoolBuilder.fromSource( + JavaSource("sample/Animal.java",""" + package sample; + public sealed interface Animal permits Fish, Mammal { + static Animal ofType(String type) { + if (Cat.TYPE.matches(type)) { + return new Cat(); + } else if (Dog.TYPE.matches(type)) { + return new Dog(); + } else if (Fish.TYPE.matches(type)) { + return new Fish(); + } + throw new IllegalArgumentException("Wrong animal type: " + type); + } + } + """.trimIndent()), + JavaSource("sample/AnimalType.java",""" + package sample; + + public enum AnimalType { + + CAT("CAT"), + DOG("DOG"), + FISH("FISH"); + + + private final String typeString; + + AnimalType(String typeString){ + this.typeString = typeString; + } + + + public boolean matches(String typeString) { + return this.typeString.equalsIgnoreCase(typeString); + } + + } + """.trimIndent()), + JavaSource("sample/Mammal.java", """ + package sample; + public sealed interface Mammal extends Animal permits Cat, Dog { + } + + """.trimIndent()), + JavaSource("sample/Cat.java",""" + package sample; + public final class Cat implements Mammal { + public static final AnimalType TYPE = AnimalType.CAT; + } + + """.trimIndent()), + JavaSource("sample/Dog.java",""" + package sample; + public final class Dog implements Mammal { + public static final AnimalType TYPE = AnimalType.DOG; + } + """.trimIndent()), + JavaSource("sample/Fish.java",""" + package sample; + public final class Fish implements Animal { + public static final AnimalType TYPE = AnimalType.FISH; + } + """.trimIndent()), + JavaSource("sample/Main.java",""" + package sample; + public class Main { + + public static void main(String[] args) { + System.out.println("Trying to create animal"); + Animal animal = Animal.ofType("fish"); + System.out.println("Successfully created animal"); + } + } + + """.trimIndent() + ), javacArguments = listOf("--enable-preview", "--release", "15"), + ) + val animal = programClassPool.getClass("sample/Animal") + + val main = programClassPool.getClass("sample/Main") + main.accept( + MultiClassVisitor(ProcessingFlagSetter(DONT_SHRINK), AllMemberVisitor( + ProcessingFlagSetter(DONT_SHRINK) + ))) + When("marking") { + val simpleUsageMarker = SimpleUsageMarker() + UsageMarker(Configuration()).mark(programClassPool,ClassPool(), ResourceFilePool(),simpleUsageMarker) + Then("The Animal class permitted subclasses constants should all be marked as used") { + var visited = false + animal.accept(AllAttributeVisitor(object : AttributeVisitor, ConstantVisitor { + override fun visitAnyAttribute( + clazz: Clazz, + attribute: Attribute + ) { + } + + override fun visitPermittedSubclassesAttribute( + clazz: Clazz, + permittedSubclassesAttribute: PermittedSubclassesAttribute + ) { + permittedSubclassesAttribute.permittedSubclassConstantsAccept(clazz,this) + } + + override fun visitAnyConstant( + clazz: Clazz, + constant: Constant + ) { + } + + override fun visitClassConstant( + clazz: Clazz, + classConstant: ClassConstant + ) { + visited = true + simpleUsageMarker.isUsed(classConstant) shouldBe true + } + })) + visited shouldBe true + } + } + } +}) diff --git a/docs/md/manual/releasenotes.md b/docs/md/manual/releasenotes.md index 2192c6b8..8a2d916e 100644 --- a/docs/md/manual/releasenotes.md +++ b/docs/md/manual/releasenotes.md @@ -1,3 +1,9 @@ +## Version 7.8.1 + +### Bugfixes + +- Prevent `java.lang.IncompatibleClassChangeError` when shrinking is enabled and sealed interfaces are used (#501). + ## Version 7.8 ### Kotlin support diff --git a/gradle.properties b/gradle.properties index 560a770b..f8d60196 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -proguardVersion = 7.8.0 +proguardVersion = 7.8.1 # The version of ProGuardCORE that sub-projects are built with proguardCoreVersion = 9.2.0