fix: keep generics while applying debug info (#2687)

This commit is contained in:
Skylot
2025-11-07 20:27:29 +00:00
parent cda1b1ad2c
commit ef99412de1
8 changed files with 91 additions and 139 deletions
@@ -201,6 +201,11 @@ public class JadxArgs implements Closeable {
*/
private boolean runDebugChecks = false;
/**
* Passes to exclude from processing.
*/
private final List<String> disabledPasses = new ArrayList<>();
private Map<String, String> pluginOptions = new HashMap<>();
private Set<String> disabledPlugins = new HashSet<>();
@@ -803,6 +808,10 @@ public class JadxArgs implements Closeable {
this.runDebugChecks = runDebugChecks;
}
public List<String> getDisabledPasses() {
return disabledPasses;
}
public Map<String, String> getPluginOptions() {
return pluginOptions;
}
@@ -4,8 +4,11 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
@@ -342,6 +345,19 @@ public class RootNode {
preDecompilePasses = DebugChecks.insertPasses(preDecompilePasses);
processClasses = new ProcessClass(DebugChecks.insertPasses(processClasses.getPasses()));
}
List<String> disabledPasses = args.getDisabledPasses();
if (!disabledPasses.isEmpty()) {
Set<String> disabledSet = new HashSet<>(disabledPasses);
Predicate<IDexTreeVisitor> filter = p -> {
if (disabledSet.contains(p.getName())) {
LOG.debug("Disable pass: {}", p.getName());
return true;
}
return false;
};
preDecompilePasses.removeIf(filter);
processClasses.getPasses().removeIf(filter);
}
}
public void runPreDecompileStage() {
@@ -134,7 +134,7 @@ public class DebugInfoApplyVisitor extends AbstractVisitor {
}
public static boolean applyDebugInfo(MethodNode mth, SSAVar ssaVar, ArgType type, String varName) {
TypeUpdateResult result = mth.root().getTypeUpdate().applyWithWiderIgnoreUnknown(mth, ssaVar, type);
TypeUpdateResult result = mth.root().getTypeUpdate().applyDebugInfo(mth, ssaVar, type);
if (result == TypeUpdateResult.REJECT) {
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Reject debug info of type: {} and name: '{}' for {}, mth: {}", type, varName, ssaVar, mth);
@@ -73,8 +73,8 @@ public final class TypeUpdate {
return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_WIDER_IGNORE_SAME);
}
public TypeUpdateResult applyWithWiderIgnoreUnknown(MethodNode mth, SSAVar ssaVar, ArgType candidateType) {
return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_WIDER_IGNORE_UNKNOWN);
public TypeUpdateResult applyDebugInfo(MethodNode mth, SSAVar ssaVar, ArgType candidateType) {
return apply(mth, ssaVar, candidateType, TypeUpdateFlags.FLAGS_APPLY_DEBUG);
}
private TypeUpdateResult apply(MethodNode mth, SSAVar ssaVar, ArgType candidateType, TypeUpdateFlags flags) {
@@ -124,8 +124,9 @@ public final class TypeUpdate {
private @Nullable TypeUpdateResult verifyType(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) {
ArgType currentType = arg.getType();
TypeUpdateFlags typeUpdateFlags = updateInfo.getFlags();
if (Objects.equals(currentType, candidateType)) {
if (!updateInfo.getFlags().isIgnoreSame()) {
if (!typeUpdateFlags.isIgnoreSame()) {
return SAME;
}
} else {
@@ -143,7 +144,7 @@ public final class TypeUpdate {
}
return REJECT;
}
if (compareResult == TypeCompareEnum.UNKNOWN && updateInfo.getFlags().isIgnoreUnknown()) {
if (compareResult == TypeCompareEnum.UNKNOWN && typeUpdateFlags.isIgnoreUnknown()) {
return REJECT;
}
if (arg.isTypeImmutable() && currentType != ArgType.UNKNOWN) {
@@ -156,7 +157,13 @@ public final class TypeUpdate {
}
return REJECT;
}
if (compareResult.isWider() && !updateInfo.getFlags().isAllowWider()) {
if (compareResult == TypeCompareEnum.WIDER_BY_GENERIC && typeUpdateFlags.isKeepGenerics()) {
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Type rejected for {}: candidate={} is removing generic from current={}", arg, candidateType, currentType);
}
return REJECT;
}
if (compareResult.isWider() && !typeUpdateFlags.isAllowWider()) {
if (Consts.DEBUG_TYPE_INFERENCE) {
LOG.debug("Type rejected for {}: candidate={} is wider than current={}", arg, candidateType, currentType);
}
@@ -1,55 +1,61 @@
package jadx.core.dex.visitors.typeinference;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import static jadx.core.dex.visitors.typeinference.TypeUpdateFlags.FlagsEnum.ALLOW_WIDER;
import static jadx.core.dex.visitors.typeinference.TypeUpdateFlags.FlagsEnum.IGNORE_SAME;
import static jadx.core.dex.visitors.typeinference.TypeUpdateFlags.FlagsEnum.IGNORE_UNKNOWN;
import static jadx.core.dex.visitors.typeinference.TypeUpdateFlags.FlagsEnum.KEEP_GENERICS;
public class TypeUpdateFlags {
private static final int ALLOW_WIDER = 1;
private static final int IGNORE_SAME = 2;
private static final int IGNORE_UNKNOWN = 4;
public static final TypeUpdateFlags FLAGS_EMPTY = build(0);
public static final TypeUpdateFlags FLAGS_WIDER = build(ALLOW_WIDER);
public static final TypeUpdateFlags FLAGS_WIDER_IGNORE_SAME = build(ALLOW_WIDER | IGNORE_SAME);
public static final TypeUpdateFlags FLAGS_WIDER_IGNORE_UNKNOWN = build(ALLOW_WIDER | IGNORE_UNKNOWN);
private final int flags;
private static TypeUpdateFlags build(int flags) {
return new TypeUpdateFlags(flags);
enum FlagsEnum {
ALLOW_WIDER,
IGNORE_SAME,
IGNORE_UNKNOWN,
KEEP_GENERICS,
}
private TypeUpdateFlags(int flags) {
static final TypeUpdateFlags FLAGS_EMPTY = build();
static final TypeUpdateFlags FLAGS_WIDER = build(ALLOW_WIDER);
static final TypeUpdateFlags FLAGS_WIDER_IGNORE_SAME = build(ALLOW_WIDER, IGNORE_SAME);
static final TypeUpdateFlags FLAGS_APPLY_DEBUG = build(ALLOW_WIDER, KEEP_GENERICS, IGNORE_UNKNOWN);
private final Set<FlagsEnum> flags;
private static TypeUpdateFlags build(FlagsEnum... flags) {
EnumSet<FlagsEnum> set;
if (flags.length == 0) {
set = EnumSet.noneOf(FlagsEnum.class);
} else {
set = EnumSet.copyOf(List.of(flags));
}
return new TypeUpdateFlags(set);
}
private TypeUpdateFlags(Set<FlagsEnum> flags) {
this.flags = flags;
}
public boolean isAllowWider() {
return (flags & ALLOW_WIDER) != 0;
return flags.contains(ALLOW_WIDER);
}
public boolean isIgnoreSame() {
return (flags & IGNORE_SAME) != 0;
return flags.contains(IGNORE_SAME);
}
public boolean isIgnoreUnknown() {
return (flags & IGNORE_UNKNOWN) != 0;
return flags.contains(IGNORE_UNKNOWN);
}
public boolean isKeepGenerics() {
return flags.contains(KEEP_GENERICS);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (isAllowWider()) {
sb.append("ALLOW_WIDER");
}
if (isIgnoreSame()) {
if (sb.length() != 0) {
sb.append('|');
}
sb.append("IGNORE_SAME");
}
if (isIgnoreUnknown()) {
if (sb.length() != 0) {
sb.append('|');
}
sb.append("IGNORE_UNKNOWN");
}
return sb.toString();
return flags.toString();
}
}
@@ -1,32 +0,0 @@
package jadx.tests.integration.generics;
import org.junit.jupiter.api.Test;
import jadx.NotYetImplemented;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class MissingGenericsTypesTest extends SmaliTest {
// @formatter:off
/*
private int x;
public void test() {
Map<String, String> map = new HashMap();
x = 1;
for (String s : map.keySet()) {
System.out.println(s);
}
}
*/
// @formatter:on
@Test
@NotYetImplemented
public void test() {
assertThat(getClassNodeFromSmaliWithPath("generics", "MissingGenericsTypesTest"))
.code()
.contains("Map<String");
}
}
@@ -32,13 +32,23 @@ public class TestMissingGenericsTypes2 extends SmaliTest {
}
}
*/
// @formatter:on
// @formatter:on
@Test
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.contains("for (String s : l) {")
.doesNotContain("Iterator i");
.doesNotContain("Iterator i")
.containsOne("for (String s : l) {");
}
@Test
public void testTypes() {
// prevent loop from converting to 'for-each' to keep iterator variable type in code
getArgs().getDisabledPasses().add("LoopRegionVisitor");
assertThat(getClassNodeFromSmali())
.code()
.doesNotContain("Iterator i")
.containsOne("Iterator<String> it = "); // variable name reject along with type
}
}
@@ -1,64 +0,0 @@
.class public LMissingGenericsTypesTest;
.super Ljava/lang/Object;
# instance fields
.field private x:I
# direct methods
.method public constructor <init>()V
.locals 0
.line 9
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public test()V
.locals 3
.line 14
new-instance v0, Ljava/util/HashMap;
invoke-direct {v0}, Ljava/util/HashMap;-><init>()V
const/4 v1, 0x1
.line 15
iput v1, p0, LMissingGenericsTypesTest;->x:I
.line 16
invoke-interface {v0}, Ljava/util/Map;->keySet()Ljava/util/Set;
move-result-object v0
invoke-interface {v0}, Ljava/util/Set;->iterator()Ljava/util/Iterator;
move-result-object v0
:goto_0
invoke-interface {v0}, Ljava/util/Iterator;->hasNext()Z
move-result v1
if-eqz v1, :cond_0
invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object;
move-result-object v1
check-cast v1, Ljava/lang/String;
.line 17
sget-object v2, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v2, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
goto :goto_0
:cond_0
return-void
.end method