fix: support enum restore for constructor without args (#2821)

This commit is contained in:
Skylot
2026-03-16 22:25:26 +00:00
parent 11b38ffed2
commit 165ae24722
11 changed files with 228 additions and 63 deletions
@@ -34,6 +34,7 @@ import jadx.core.dex.attributes.nodes.EnumClassAttr;
import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField;
import jadx.core.dex.attributes.nodes.LineAttrNode;
import jadx.core.dex.attributes.nodes.MethodInlineAttr;
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.info.ClassInfo;
@@ -164,12 +165,7 @@ public class ClassGen {
if (af.isInterface()) {
af = af.remove(AccessFlags.ABSTRACT)
.remove(AccessFlags.STATIC);
} else if (af.isEnum()) {
af = af.remove(AccessFlags.FINAL)
.remove(AccessFlags.ABSTRACT)
.remove(AccessFlags.STATIC);
}
// 'static' and 'private' modifier not allowed for top classes (not inner)
if (!cls.getClassInfo().isInner()) {
af = af.remove(AccessFlags.STATIC).remove(AccessFlags.PRIVATE);
@@ -294,7 +290,7 @@ public class ClassGen {
private void addInnerClsAndMethods(ICodeWriter clsCode) {
Stream.of(cls.getInnerClasses(), cls.getMethods())
.flatMap(Collection::stream)
.filter(node -> !node.contains(AFlag.DONT_GENERATE) || fallback)
.filter(node -> !skipNode(node))
.sorted(Comparator.comparingInt(LineAttrNode::getSourceLine))
.forEach(node -> {
if (node instanceof ClassNode) {
@@ -305,6 +301,18 @@ public class ClassGen {
});
}
private boolean skipNode(NotificationAttrNode node) {
if (fallback) {
return false;
}
if (Consts.DEBUG_ATTRIBUTES) {
if (node.contains(AType.JADX_COMMENTS)) {
return false;
}
}
return node.contains(AFlag.DONT_GENERATE);
}
private void addInnerClass(ICodeWriter code, ClassNode innerCls) {
try {
ClassGen inClGen = new ClassGen(innerCls, getParentGen());
@@ -1,7 +1,5 @@
package jadx.core.codegen;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.jetbrains.annotations.Nullable;
@@ -29,6 +27,7 @@ import jadx.core.dex.attributes.nodes.JadxError;
import jadx.core.dex.attributes.nodes.JumpInfo;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.attributes.nodes.MethodReplaceAttr;
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.instructions.ConstStringNode;
import jadx.core.dex.instructions.IfNode;
@@ -109,10 +108,6 @@ public class MethodGen {
if (clsAccFlags.isAnnotation()) {
ai = ai.remove(AccessFlags.PUBLIC);
}
if (mth.getMethodInfo().isConstructor() && mth.getParentClass().isEnum()) {
ai = ai.remove(AccessInfo.VISIBILITY_FLAGS);
}
if (mth.getMethodInfo().hasAlias() && !ai.isConstructor()) {
CodeGenUtils.addRenamedComment(code, mth, mth.getName());
}
@@ -152,21 +147,7 @@ public class MethodGen {
code.add(defMth.getAlias());
}
code.add('(');
List<RegisterArg> args = mth.getArgRegs();
if (mth.getMethodInfo().isConstructor()
&& mth.getParentClass().contains(AType.ENUM_CLASS)) {
if (args.size() == 2) {
args = Collections.emptyList();
} else if (args.size() > 2) {
args = args.subList(2, args.size());
} else {
mth.addWarnComment("Incorrect number of args for enum constructor: " + args.size() + " (expected >= 2)");
}
} else if (mth.contains(AFlag.SKIP_FIRST_ARG)) {
args = args.subList(1, args.size());
}
addMethodArguments(code, args);
addMethodArguments(code);
code.add(')');
annotationGen.addThrows(mth, code);
@@ -209,12 +190,22 @@ public class MethodGen {
}
}
private void addMethodArguments(ICodeWriter code, List<RegisterArg> args) {
private void addMethodArguments(ICodeWriter code) {
List<RegisterArg> args = mth.getArgRegs();
AnnotationMethodParamsAttr paramsAnnotation = mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS);
int i = 0;
Iterator<RegisterArg> it = args.iterator();
while (it.hasNext()) {
RegisterArg mthArg = it.next();
int argNum = -1;
int lastArgNum = args.size() - 1;
boolean first = true;
for (RegisterArg mthArg : args) {
argNum++;
if (SkipMethodArgsAttr.isSkip(mth, argNum)) {
continue;
}
if (first) {
first = false;
} else {
code.add(", ");
}
SSAVar ssaVar = mthArg.getSVar();
CodeVar var;
if (ssaVar == null) {
@@ -226,7 +217,7 @@ public class MethodGen {
// add argument annotation
if (paramsAnnotation != null) {
annotationGen.addForParameter(code, paramsAnnotation, i);
annotationGen.addForParameter(code, paramsAnnotation, argNum);
}
if (var.isFinal()) {
code.add("final ");
@@ -239,7 +230,7 @@ public class MethodGen {
} else {
argType = varType;
}
if (!it.hasNext() && mth.getAccessFlags().isVarArgs()) {
if (argNum == lastArgNum && mth.getAccessFlags().isVarArgs()) {
// change last array argument to varargs
if (argType.isArray()) {
ArgType elType = argType.getArrayElement();
@@ -258,11 +249,6 @@ public class MethodGen {
code.attachDefinition(VarNode.get(mth, var));
}
code.add(varName);
i++;
if (it.hasNext()) {
code.add(", ");
}
}
}
@@ -25,6 +25,7 @@ public enum AFlag {
REMOVE_SUPER_CLASS, // don't add super class
HIDDEN, // instruction used inside other instruction but not listed in args
CONVERTED_ENUM, // enum class successfully restored to original form
DONT_RENAME, // do not rename during deobfuscation
FORCE_RAW_NAME, // force use of raw name instead alias
@@ -53,4 +53,9 @@ public class CodeFeaturesAttr implements IJadxAttribute {
public String toAttrString() {
return "CodeFeatures{" + codeFeatures + '}';
}
@Override
public String toString() {
return toAttrString();
}
}
@@ -2,6 +2,8 @@ package jadx.core.dex.attributes.nodes;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.input.data.attributes.IJadxAttribute;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.instructions.mods.ConstructorInsn;
@@ -14,11 +16,13 @@ public class EnumClassAttr implements IJadxAttribute {
public static class EnumField {
private final FieldNode field;
private final ConstructorInsn constrInsn;
private final @Nullable String nameStr;
private ClassNode cls;
public EnumField(FieldNode field, ConstructorInsn co) {
public EnumField(FieldNode field, ConstructorInsn co, @Nullable String nameStr) {
this.field = field;
this.constrInsn = co;
this.nameStr = nameStr;
}
public FieldNode getField() {
@@ -37,6 +41,10 @@ public class EnumClassAttr implements IJadxAttribute {
this.cls = cls;
}
public @Nullable String getNameStr() {
return nameStr;
}
@Override
public String toString() {
return field + "(" + constrInsn + ") " + cls;
@@ -5,6 +5,7 @@ import java.util.BitSet;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.input.data.attributes.PinnedAttribute;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.MethodNode;
@@ -34,6 +35,9 @@ public class SkipMethodArgsAttr extends PinnedAttribute {
if (mth == null) {
return false;
}
if (argNum == 0 && mth.contains(AFlag.SKIP_FIRST_ARG)) {
return true;
}
SkipMethodArgsAttr attr = mth.get(AType.SKIP_MTH_ARGS);
if (attr == null) {
return false;
@@ -44,7 +44,8 @@ import jadx.core.utils.exceptions.JadxException;
runAfter = {
ModVisitor.class,
FixAccessModifiers.class,
ProcessAnonymous.class
ProcessAnonymous.class,
ExtractFieldInit.class
}
)
public class ClassModifier extends AbstractVisitor {
@@ -326,8 +327,9 @@ public class ClassModifier extends AbstractVisitor {
}
AccessInfo af = mth.getAccessFlags();
boolean publicConstructor = mth.isConstructor() && af.isPublic();
boolean enumDefConstructor = mth.isConstructor() && mth.getParentClass().contains(AFlag.CONVERTED_ENUM);
boolean clsInit = mth.getMethodInfo().isClassInit() && af.isStatic();
if (publicConstructor || clsInit) {
if (publicConstructor || enumDefConstructor || clsInit) {
if (!BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) {
return;
}
@@ -164,7 +164,7 @@ public class ConstructorVisitor extends AbstractVisitor {
private static boolean canRemoveConstructor(MethodNode mth, ConstructorInsn co) {
ClassNode parentClass = mth.getParentClass();
if (co.isSuper() && (co.getArgsCount() == 0 || parentClass.isEnum())) {
if (co.isSuper() && co.getArgsCount() == 0) {
return true;
}
if (co.isThis() && co.getArgsCount() == 0) {
@@ -73,6 +73,7 @@ import static jadx.core.utils.InsnUtils.getWrappedInsn;
}
)
public class EnumVisitor extends AbstractVisitor {
private static final String ENUM_SUPER_CONSTRUCTOR_ID = "java.lang.Enum.<init>(Ljava/lang/String;I)V";
private MethodInfo enumValueOfMth;
private MethodInfo cloneMth;
@@ -171,11 +172,8 @@ public class EnumVisitor extends AbstractVisitor {
cls.addAttr(attr);
for (EnumField enumField : attr.getFields()) {
ConstructorInsn co = enumField.getConstrInsn();
FieldNode fieldNode = enumField.getField();
// use string arg from the constructor as enum field name
String name = getConstString(cls.root(), co.getArg(0));
String name = enumField.getNameStr();
if (name != null
&& !fieldNode.getAlias().equals(name)
&& NameMapper.isValidAndPrintable(name)
@@ -193,9 +191,24 @@ public class EnumVisitor extends AbstractVisitor {
CodeShrinkVisitor.shrinkMethod(classInitMth);
}
removeEnumMethods(cls, data.valuesField);
fixAccessFlags(cls);
cls.add(AFlag.CONVERTED_ENUM);
return true;
}
private static void fixAccessFlags(ClassNode cls) {
// remove invalid access flags
cls.setAccessFlags(cls.getAccessFlags()
.remove(AccessFlags.FINAL)
.remove(AccessFlags.ABSTRACT)
.remove(AccessFlags.STATIC));
for (MethodNode mth : cls.getMethods()) {
if (mth.getMethodInfo().isConstructor()) {
mth.setAccessFlags(mth.getAccessFlags().remove(AccessInfo.VISIBILITY_FLAGS));
}
}
}
/**
* Search "$VALUES" field (holds all enum values)
*/
@@ -433,13 +446,9 @@ public class EnumVisitor extends AbstractVisitor {
return enumFieldNode;
}
@SuppressWarnings("StatementWithEmptyBody")
private EnumField createEnumFieldByConstructor(EnumData data, FieldNode enumFieldNode, ConstructorInsn co) {
// usually constructor signature is '<init>(Ljava/lang/String;I)V'.
// sometimes for one field enum second arg can be omitted
if (co.getArgsCount() < 1) {
return null;
}
// usually constructor signature is '<init>(Ljava/lang/String;I)V', sometimes one or both args can
// be omitted
ClassNode cls = data.cls;
ClassInfo clsInfo = co.getClassType();
ClassNode constrCls = cls.root().resolveClass(clsInfo);
@@ -457,17 +466,45 @@ public class EnumVisitor extends AbstractVisitor {
if (ctrMth == null) {
return null;
}
List<RegisterArg> regs = new ArrayList<>();
co.getRegisterArgs(regs);
if (!regs.isEmpty()) {
ConstructorInsn replacedCo = inlineExternalRegs(data, co);
if (replacedCo == null) {
throw new JadxRuntimeException("Init of enum field '" + enumFieldNode.getName() + "' uses external variables");
// usually constructor signature is '<init>(Ljava/lang/String;I)V'
// sometimes one or both args can be inlined or omitted
String nameStr = null;
if (co.getArgsCount() == 0) {
ConstructorInsn ctrInsn = searchEnumSuperCtrInsn(ctrMth);
if (ctrInsn != null && ctrInsn.getArgsCount() != 0) {
nameStr = getConstString(ctrMth.root(), ctrInsn.getArg(0));
}
} else {
nameStr = getConstString(cls.root(), co.getArg(0));
// verify and try to inline additional constructor args
List<RegisterArg> regs = new ArrayList<>();
co.getRegisterArgs(regs);
if (!regs.isEmpty()) {
ConstructorInsn replacedCo = inlineExternalRegs(data, co);
if (replacedCo == null) {
throw new JadxRuntimeException("Init of enum field '" + enumFieldNode.getName() + "' uses external variables");
}
data.toRemove.add(co);
co = replacedCo;
}
data.toRemove.add(co);
co = replacedCo;
}
return new EnumField(enumFieldNode, co);
return new EnumField(enumFieldNode, co, nameStr);
}
private @Nullable ConstructorInsn searchEnumSuperCtrInsn(MethodNode ctrMth) {
for (BlockNode block : ctrMth.getBasicBlocks()) {
for (InsnNode insn : block.getInstructions()) {
if (insn.getType() == InsnType.CONSTRUCTOR) {
ConstructorInsn ctrCall = (ConstructorInsn) insn;
if (ctrCall.isSuper()
&& ctrCall.getArgsCount() != 0
&& ctrCall.getCallMth().getRawFullId().equals(ENUM_SUPER_CONSTRUCTOR_ID)) {
return ctrCall;
}
}
}
}
return null;
}
private ConstructorInsn inlineExternalRegs(EnumData data, ConstructorInsn co) {
@@ -574,10 +611,16 @@ public class EnumVisitor extends AbstractVisitor {
}
String shortId = mi.getShortId();
if (mi.isConstructor()) {
markArgsForSkip(mth);
// remove super constructor call
ConstructorInsn superCtrInsn = searchEnumSuperCtrInsn(mth);
if (superCtrInsn != null) {
superCtrInsn.add(AFlag.DONT_GENERATE);
InsnRemover.remove(mth, superCtrInsn);
}
if (isDefaultConstructor(mth, shortId)) {
mth.add(AFlag.DONT_GENERATE);
}
markArgsForSkip(mth);
} else if (mi.getShortId().equals(valuesMethodShortId)) {
if (isValuesMethod(mth, clsType)) {
valuesMethod = mth;
@@ -757,4 +800,9 @@ public class EnumVisitor extends AbstractVisitor {
this.staticBlocks = staticBlocks;
}
}
@Override
public String getName() {
return "EnumVisitor";
}
}
@@ -0,0 +1,29 @@
package jadx.tests.integration.enums;
import org.junit.jupiter.api.Test;
import jadx.tests.api.RaungTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestEnums11 extends RaungTest {
@Test
public void test() {
assertThat(getClassNodeFromRaung())
.code()
.containsLines("public enum TestEnums11 {", indent(1) + "UNKNOWN;")
.containsOne("public final int a = -99;")
.doesNotContain("TestEnums11() {");
}
@Test
public void testDisableEnumRestore() {
// constructor method incorrectly removed
getArgs().getDisabledPasses().add("EnumVisitor");
disableCompilation();
assertThat(getClassNodeFromRaung())
.code()
.containsOne("public TestEnums11() {");
}
}
@@ -0,0 +1,74 @@
.version 52 # Java 8
.class public final enum enums/TestEnums11
.super java/lang/Enum
.signature Ljava/lang/Enum<Lenums/TestEnums11;>;
.source "SourceFile"
.field public static final enum A Lenums/TestEnums11;
.field public static final synthetic b [Lenums/TestEnums11;
.field public final a I
.method public static values()[Lenums/TestEnums11;
.max stack 1
.max locals 1
getstatic enums/TestEnums11 b [Lenums/TestEnums11;
invokevirtual [Lenums/TestEnums11; clone ()Ljava/lang/Object;
checkcast [Lenums/TestEnums11;
areturn
.end method
.method public static valueOf(Ljava/lang/String;)Lenums/TestEnums11;
.max stack 2
.max locals 1
ldc Lenums/TestEnums11;
aload 0
invokestatic java/lang/Enum valueOf (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
checkcast enums/TestEnums11
areturn
.end method
.method public <init>()V
.signature (I)V
.max stack 4
.max locals 1
aload 0
dup
ldc "UNKNOWN"
iconst_0
invokespecial java/lang/Enum <init> (Ljava/lang/String;I)V
bipush -99
putfield enums/TestEnums11 a I
return
.end method
.method public static <clinit>()V
.max stack 4
.max locals 1
new enums/TestEnums11
dup
dup
astore 0
invokespecial enums/TestEnums11 <init> ()V
putstatic enums/TestEnums11 A Lenums/TestEnums11;
iconst_1
anewarray enums/TestEnums11
dup
iconst_0
aload 0
aastore
putstatic enums/TestEnums11 b [Lenums/TestEnums11;
return
.end method
.method public getErrorCode()I
.max stack 1
.max locals 1
aload 0
getfield enums/TestEnums11 a I
ireturn
.end method