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](869ce156b1/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 ](869ce156b1/base/src/main/java/proguard/shrink/NestUsageMarker.java (L100)).

3.  [ If the referenced class is another interface, this is marked by the InterfaceUsageMarker ](869ce156b1/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](869ce156b1/base/src/main/java/proguard/shrink/UsageMarker.java (L119-130))
This commit is contained in:
niccolo.piazzesi
2025-10-14 12:00:16 +02:00
parent 869ce156b1
commit 6dfd878ebb
4 changed files with 161 additions and 4 deletions

View File

@@ -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)
{

View File

@@ -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
}
}
}
})