fix: handle Kotlin 1.9+ $ENTRIES pattern in enum restoration (PR #2814)

* fix: handle Kotlin 1.9+ $ENTRIES pattern in enum restoration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* keep $ENTRIES field, it still used in getEntries() method

---------

Co-authored-by: clawdbot-silly-waddle <clawdbot-silly-waddle@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
This commit is contained in:
Ananya Sharma
2026-03-07 20:37:12 +01:00
committed by GitHub
parent 7b3563fb62
commit f2f145019d
3 changed files with 241 additions and 2 deletions
@@ -49,6 +49,7 @@ import jadx.core.utils.BlockInsnPair;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnRemover;
import jadx.core.utils.InsnUtils;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -149,6 +150,14 @@ public class EnumVisitor extends AbstractVisitor {
if (arrArg.isInsnWrap()) {
InsnNode wrappedInsn = ((InsnWrapArg) arrArg).getWrapInsn();
enumFields = extractEnumFieldsFromInsn(data, wrappedInsn);
} else if (arrArg.isRegister()) {
// Kotlin 1.9+ $ENTRIES pattern: array register has multiple uses,
// preventing CodeShrinkVisitor from inlining into the SPUT
RegisterArg regArg = (RegisterArg) arrArg;
InsnNode assignInsn = regArg.getAssignInsn();
if (assignInsn != null) {
enumFields = extractEnumFieldsFromInsn(data, assignInsn);
}
}
if (enumFields == null) {
cls.addWarnComment("Unknown enum class pattern. Please report as an issue!");
@@ -291,8 +300,13 @@ public class EnumVisitor extends AbstractVisitor {
return null;
}
List<EnumField> enumFields = extractEnumFieldsFromInsn(enumData, wrappedInsn);
if (enumFields != null) {
if (enumFields != null && ListUtils.isSingleElement(valuesMth.getUseIn(), enumData.classInitMth)) {
valuesMth.add(AFlag.DONT_GENERATE);
if (valuesMth.getName().equals("$values")) {
// Kotlin synthetic method used for init values
// rename to actual values method to use in $ENTRIES init code
valuesMth.getMethodInfo().setAlias("values");
}
}
return enumFields;
}
@@ -506,7 +520,18 @@ public class EnumVisitor extends AbstractVisitor {
}
case FILLED_NEW_ARRAY: {
// allow usage in values init instruction
if (!data.valuesInitInsn.getArg(0).unwrap().equals(useInsn)) {
InsnArg valuesArg = data.valuesInitInsn.getArg(0);
InsnNode unwrapped = valuesArg.unwrap();
if (unwrapped != null) {
if (unwrapped != useInsn) {
return null;
}
} else if (valuesArg.isRegister()) {
InsnNode valuesAssign = ((RegisterArg) valuesArg).getAssignInsn();
if (valuesAssign != useInsn) {
return null;
}
} else {
return null;
}
break;
@@ -0,0 +1,24 @@
package jadx.tests.integration.enums;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
/**
* Test for Kotlin 1.9+ enum $ENTRIES pattern.
*/
public class TestEnumKotlinEntries extends SmaliTest {
@Test
public void test() {
disableCompilation(); // kotlin.enums.EnumEntries not on test classpath
assertThat(getClassNodeFromSmali())
.code()
.containsLines(1, "ALPHA,", "BETA,", "GAMMA;")
.containsOne("EnumEntries $ENTRIES = EnumEntriesKt.enumEntries(values());")
.doesNotContain("$VALUES")
.doesNotContain("Failed to restore enum");
}
}
@@ -0,0 +1,190 @@
.class public final enum Lenums/TestEnumKotlinEntries;
.super Ljava/lang/Enum;
.source "TestEnumKotlinEntries.kt"
# annotations
.annotation system Ldalvik/annotation/Signature;
value = {
"Ljava/lang/Enum<",
"Lenums/TestEnumKotlinEntries;",
">;"
}
.end annotation
.annotation runtime Lkotlin/Metadata;
d1 = {
"\u0000\u000c\n\u0002\u0018\u0002\n\u0002\u0010\u0010\n\u0002\u0008\u0005\u0008\u0086\u0081\u0002\u0018\u00002\u0008\u0012\u0004\u0012\u00020\u00000\u0001B\t\u0008\u0002\u00a2\u0006\u0004\u0008\u0002\u0010\u0003j\u0002\u0008\u0004j\u0002\u0008\u0005j\u0002\u0008\u0006"
}
d2 = {
"Lenums/TestEnumKotlinEntries;",
"",
"<init>",
"(Ljava/lang/String;I)V",
"ALPHA",
"BETA",
"GAMMA"
}
k = 0x1
mv = {
0x2,
0x3,
0x0
}
xi = 0x30
.end annotation
# static fields
.field private static final synthetic $ENTRIES:Lkotlin/enums/EnumEntries;
.field private static final synthetic $VALUES:[Lenums/TestEnumKotlinEntries;
.field public static final enum ALPHA:Lenums/TestEnumKotlinEntries;
.field public static final enum BETA:Lenums/TestEnumKotlinEntries;
.field public static final enum GAMMA:Lenums/TestEnumKotlinEntries;
# direct methods
.method private static final synthetic $values()[Lenums/TestEnumKotlinEntries;
.registers 3
const/4 v0, 0x3
new-array v0, v0, [Lenums/TestEnumKotlinEntries;
const/4 v1, 0x0
sget-object v2, Lenums/TestEnumKotlinEntries;->ALPHA:Lenums/TestEnumKotlinEntries;
aput-object v2, v0, v1
const/4 v1, 0x1
sget-object v2, Lenums/TestEnumKotlinEntries;->BETA:Lenums/TestEnumKotlinEntries;
aput-object v2, v0, v1
const/4 v1, 0x2
sget-object v2, Lenums/TestEnumKotlinEntries;->GAMMA:Lenums/TestEnumKotlinEntries;
aput-object v2, v0, v1
return-object v0
.end method
.method static constructor <clinit>()V
.registers 3
.line 4
new-instance v0, Lenums/TestEnumKotlinEntries;
const-string v1, "ALPHA"
const/4 v2, 0x0
invoke-direct {v0, v1, v2}, Lenums/TestEnumKotlinEntries;-><init>(Ljava/lang/String;I)V
sput-object v0, Lenums/TestEnumKotlinEntries;->ALPHA:Lenums/TestEnumKotlinEntries;
.line 5
new-instance v0, Lenums/TestEnumKotlinEntries;
const-string v1, "BETA"
const/4 v2, 0x1
invoke-direct {v0, v1, v2}, Lenums/TestEnumKotlinEntries;-><init>(Ljava/lang/String;I)V
sput-object v0, Lenums/TestEnumKotlinEntries;->BETA:Lenums/TestEnumKotlinEntries;
.line 6
new-instance v0, Lenums/TestEnumKotlinEntries;
const-string v1, "GAMMA"
const/4 v2, 0x2
invoke-direct {v0, v1, v2}, Lenums/TestEnumKotlinEntries;-><init>(Ljava/lang/String;I)V
sput-object v0, Lenums/TestEnumKotlinEntries;->GAMMA:Lenums/TestEnumKotlinEntries;
invoke-static {}, Lenums/TestEnumKotlinEntries;->$values()[Lenums/TestEnumKotlinEntries;
move-result-object v0
sput-object v0, Lenums/TestEnumKotlinEntries;->$VALUES:[Lenums/TestEnumKotlinEntries;
check-cast v0, [Ljava/lang/Enum;
invoke-static {v0}, Lkotlin/enums/EnumEntriesKt;->enumEntries([Ljava/lang/Enum;)Lkotlin/enums/EnumEntries;
move-result-object v0
sput-object v0, Lenums/TestEnumKotlinEntries;->$ENTRIES:Lkotlin/enums/EnumEntries;
return-void
.end method
.method private constructor <init>(Ljava/lang/String;I)V
.registers 3
.param p1, "$enum$name" # Ljava/lang/String;
.param p2, "$enum$ordinal" # I
.annotation system Ldalvik/annotation/Signature;
value = {
"()V"
}
.end annotation
.line 3
invoke-direct {p0, p1, p2}, Ljava/lang/Enum;-><init>(Ljava/lang/String;I)V
return-void
.end method
.method public static getEntries()Lkotlin/enums/EnumEntries;
.registers 1
.annotation system Ldalvik/annotation/Signature;
value = {
"()",
"Lkotlin/enums/EnumEntries<",
"Lenums/TestEnumKotlinEntries;",
">;"
}
.end annotation
sget-object v0, Lenums/TestEnumKotlinEntries;->$ENTRIES:Lkotlin/enums/EnumEntries;
return-object v0
.end method
.method public static valueOf(Ljava/lang/String;)Lenums/TestEnumKotlinEntries;
.registers 2
const-class v0, Lenums/TestEnumKotlinEntries;
invoke-static {v0, p0}, Ljava/lang/Enum;->valueOf(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
move-result-object v0
check-cast v0, Lenums/TestEnumKotlinEntries;
return-object v0
.end method
.method public static values()[Lenums/TestEnumKotlinEntries;
.registers 1
sget-object v0, Lenums/TestEnumKotlinEntries;->$VALUES:[Lenums/TestEnumKotlinEntries;
invoke-virtual {v0}, Ljava/lang/Object;->clone()Ljava/lang/Object;
move-result-object v0
check-cast v0, [Lenums/TestEnumKotlinEntries;
return-object v0
.end method