feat: move variable name apply from codegen to new pass (#2422)

This commit is contained in:
Skylot
2025-05-26 22:01:51 +01:00
parent bcd0c949dc
commit 33f93ddc8a
16 changed files with 308 additions and 215 deletions
@@ -16,6 +16,7 @@ import jadx.core.deobf.DeobfuscatorVisitor;
import jadx.core.deobf.SaveDeobfMapping;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.visitors.AnonymousClassVisitor;
import jadx.core.dex.visitors.ApplyVariableNames;
import jadx.core.dex.visitors.AttachCommentsVisitor;
import jadx.core.dex.visitors.AttachMethodDetails;
import jadx.core.dex.visitors.AttachTryCatchVisitor;
@@ -200,6 +201,7 @@ public class Jadx {
passes.add(new MarkMethodsForInline());
}
passes.add(new ProcessVariables());
passes.add(new ApplyVariableNames());
passes.add(new PrepareForCodeGen());
if (args.isCfgOutput()) {
passes.add(DotGraphVisitor.dumpRegions());
@@ -2,57 +2,22 @@ package jadx.core.codegen;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import jadx.core.Consts;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.nodes.LoopLabelAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.CodeVar;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.NamedArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils;
public class NameGen {
private static final Map<String, String> OBJ_ALIAS;
private final Set<String> varNames = new HashSet<>();
private final MethodNode mth;
private final boolean fallback;
static {
OBJ_ALIAS = Utils.newConstStringMap(
Consts.CLASS_STRING, "str",
Consts.CLASS_CLASS, "cls",
Consts.CLASS_THROWABLE, "th",
Consts.CLASS_OBJECT, "obj",
"java.util.Iterator", "it",
"java.lang.Boolean", "bool",
"java.lang.Short", "sh",
"java.lang.Integer", "num",
"java.lang.Character", "ch",
"java.lang.Byte", "b",
"java.lang.Float", "f",
"java.lang.Long", "l",
"java.lang.Double", "d",
"java.lang.StringBuilder", "sb",
"java.lang.Exception", "exc");
}
private final Set<String> varNames = new HashSet<>();
public NameGen(MethodNode mth, ClassGen classGen) {
this.mth = mth;
@@ -99,9 +64,9 @@ public class NameGen {
if (fallback) {
return name;
}
name = getUniqueVarName(name);
arg.setName(name);
return name;
String uniqName = getUniqueVarName(name);
arg.setName(uniqName);
return uniqName;
}
public String useArg(RegisterArg arg) {
@@ -132,13 +97,10 @@ public class NameGen {
private String makeArgName(CodeVar var) {
String name = var.getName();
if (name == null) {
name = guessName(var);
if (NameMapper.isValidAndPrintable(name)) {
return name;
}
if (!NameMapper.isValidAndPrintable(name)) {
name = getFallbackName(var);
}
return name;
return getFallbackName(var);
}
private String getFallbackName(CodeVar var) {
@@ -152,153 +114,4 @@ public class NameGen {
private String getFallbackName(RegisterArg arg) {
return "r" + arg.getRegNum();
}
private String guessName(CodeVar var) {
List<SSAVar> ssaVars = var.getSsaVars();
if (ssaVars != null && !ssaVars.isEmpty()) {
// TODO: use all vars for better name generation
SSAVar ssaVar = ssaVars.get(0);
if (ssaVar != null && ssaVar.getName() == null) {
RegisterArg assignArg = ssaVar.getAssign();
InsnNode assignInsn = assignArg.getParentInsn();
if (assignInsn != null) {
String name = makeNameFromInsn(assignInsn);
if (name != null && NameMapper.isValidAndPrintable(name)) {
return name;
}
}
}
}
return makeNameForType(var.getType());
}
private String makeNameForType(ArgType type) {
if (type.isPrimitive()) {
return type.getPrimitiveType().getShortName().toLowerCase();
}
if (type.isArray()) {
return makeNameForType(type.getArrayRootElement()) + "Arr";
}
return makeNameForObject(type);
}
private String makeNameForObject(ArgType type) {
if (type.isGenericType()) {
return StringUtils.escape(type.getObject().toLowerCase());
}
if (type.isObject()) {
String alias = getAliasForObject(type.getObject());
if (alias != null) {
return alias;
}
return makeNameForCheckedClass(ClassInfo.fromType(mth.root(), type));
}
return StringUtils.escape(type.toString());
}
private String makeNameForCheckedClass(ClassInfo classInfo) {
String shortName = classInfo.getAliasShortName();
String vName = fromName(shortName);
if (vName != null) {
return vName;
}
String lower = StringUtils.escape(shortName.toLowerCase());
if (shortName.equals(lower)) {
return lower + "Var";
}
return lower;
}
private String makeNameForClass(ClassInfo classInfo) {
String alias = getAliasForObject(classInfo.getFullName());
if (alias != null) {
return alias;
}
return makeNameForCheckedClass(classInfo);
}
private static String fromName(String name) {
if (name == null || name.isEmpty()) {
return null;
}
if (name.toUpperCase().equals(name)) {
// all characters are upper case
return name.toLowerCase();
}
String v1 = Character.toLowerCase(name.charAt(0)) + name.substring(1);
if (!v1.equals(name)) {
return v1;
}
if (name.length() < 3) {
return name + "Var";
}
return null;
}
private static String getAliasForObject(String name) {
return OBJ_ALIAS.get(name);
}
private String makeNameFromInsn(InsnNode insn) {
switch (insn.getType()) {
case INVOKE:
InvokeNode inv = (InvokeNode) insn;
return makeNameFromInvoke(inv.getCallMth());
case CONSTRUCTOR:
ConstructorInsn co = (ConstructorInsn) insn;
MethodNode callMth = mth.root().getMethodUtils().resolveMethod(co);
if (callMth != null && callMth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
// don't use name of anonymous class
return null;
}
return makeNameForClass(co.getClassType());
case ARRAY_LENGTH:
return "length";
case ARITH:
case TERNARY:
case CAST:
for (InsnArg arg : insn.getArguments()) {
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
String wName = makeNameFromInsn(wrapInsn);
if (wName != null) {
return wName;
}
}
}
break;
default:
break;
}
return null;
}
private String makeNameFromInvoke(MethodInfo callMth) {
String name = callMth.getAlias();
ClassInfo declClass = callMth.getDeclClass();
if ("getInstance".equals(name)) {
// e.g. Cipher.getInstance
return makeNameForClass(declClass);
}
if (name.startsWith("get") || name.startsWith("set")) {
return fromName(name.substring(3));
}
if ("iterator".equals(name)) {
return "it";
}
if ("toString".equals(name)) {
return makeNameForClass(declClass);
}
if ("forName".equals(name) && declClass.getType().equals(ArgType.CLASS)) {
return OBJ_ALIAS.get(Consts.CLASS_CLASS);
}
if (name.startsWith("to")) {
return fromName(name.substring(2));
}
return name;
}
}
@@ -59,13 +59,11 @@ public class SSAVar implements Comparable<SSAVar> {
return version;
}
@NotNull
public RegisterArg getAssign() {
public @NotNull RegisterArg getAssign() {
return assign;
}
@Nullable
public InsnNode getAssignInsn() {
public @Nullable InsnNode getAssignInsn() {
return assign.getParentInsn();
}
@@ -0,0 +1,272 @@
package jadx.core.dex.visitors;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import jadx.core.Consts;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.instructions.InvokeNode;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.CodeVar;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.InsnWrapArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.instructions.args.SSAVar;
import jadx.core.dex.instructions.mods.ConstructorInsn;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.regions.variables.ProcessVariables;
import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
@JadxVisitor(
name = "ApplyVariableNames",
desc = "Try to guess variable name from usage",
runAfter = {
ProcessVariables.class
}
)
public class ApplyVariableNames extends AbstractVisitor {
private static final Map<String, String> OBJ_ALIAS = Utils.newConstStringMap(
Consts.CLASS_STRING, "str",
Consts.CLASS_CLASS, "cls",
Consts.CLASS_THROWABLE, "th",
Consts.CLASS_OBJECT, "obj",
"java.util.Iterator", "it",
"java.util.HashMap", "map",
"java.lang.Boolean", "bool",
"java.lang.Short", "sh",
"java.lang.Integer", "num",
"java.lang.Character", "ch",
"java.lang.Byte", "b",
"java.lang.Float", "f",
"java.lang.Long", "l",
"java.lang.Double", "d",
"java.lang.StringBuilder", "sb",
"java.lang.Exception", "exc");
private static final Set<String> GOOD_VAR_NAMES = Set.of(
"size", "length", "list", "map", "next");
private static final List<String> INVOKE_PREFIXES = List.of(
"get", "set", "to", "parse", "read", "format");
private RootNode root;
@Override
public void init(RootNode root) throws JadxException {
this.root = root;
}
@Override
public void visit(MethodNode mth) throws JadxException {
for (SSAVar ssaVar : mth.getSVars()) {
CodeVar codeVar = ssaVar.getCodeVar();
String newName = guessName(codeVar);
if (newName != null) {
codeVar.setName(newName);
}
}
}
private @Nullable String guessName(CodeVar var) {
if (var.isThis()) {
return RegisterArg.THIS_ARG_NAME;
}
if (!var.isDeclared()) {
// name is not used in code
return null;
}
if (NameMapper.isValidAndPrintable(var.getName())) {
// the current name is valid, keep it
return null;
}
List<SSAVar> ssaVars = var.getSsaVars();
if (Utils.notEmpty(ssaVars)) {
boolean mthArg = ssaVars.stream().anyMatch(ssaVar -> ssaVar.getAssign().contains(AFlag.METHOD_ARGUMENT));
if (mthArg) {
// for method args use defined type and ignore usage
return makeNameForType(var.getType());
}
for (SSAVar ssaVar : ssaVars) {
String name = makeNameForSSAVar(ssaVar);
if (name != null) {
return name;
}
}
}
return makeNameForType(var.getType());
}
private @Nullable String makeNameForSSAVar(SSAVar ssaVar) {
String ssaVarName = ssaVar.getName();
if (ssaVarName != null) {
return ssaVarName;
}
InsnNode assignInsn = ssaVar.getAssignInsn();
if (assignInsn != null) {
String name = makeNameFromInsn(ssaVar, assignInsn);
if (NameMapper.isValidAndPrintable(name)) {
return name;
}
}
return null;
}
private String makeNameFromInsn(SSAVar ssaVar, InsnNode insn) {
switch (insn.getType()) {
case INVOKE:
return makeNameFromInvoke(ssaVar, (InvokeNode) insn);
case CONSTRUCTOR:
ConstructorInsn co = (ConstructorInsn) insn;
MethodNode callMth = root.getMethodUtils().resolveMethod(co);
if (callMth != null && callMth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
// don't use name of anonymous class
return null;
}
return makeNameForClass(co.getClassType());
case ARRAY_LENGTH:
return "length";
case ARITH:
case TERNARY:
case CAST:
for (InsnArg arg : insn.getArguments()) {
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
String wName = makeNameFromInsn(ssaVar, wrapInsn);
if (wName != null) {
return wName;
}
}
}
break;
default:
break;
}
return null;
}
private String makeNameForType(ArgType type) {
if (type.isPrimitive()) {
return type.getPrimitiveType().getShortName().toLowerCase();
}
if (type.isArray()) {
return makeNameForType(type.getArrayRootElement()) + "Arr";
}
return makeNameForObject(type);
}
private String makeNameForObject(ArgType type) {
if (type.isGenericType()) {
return StringUtils.escape(type.getObject().toLowerCase());
}
if (type.isObject()) {
String alias = getAliasForObject(type.getObject());
if (alias != null) {
return alias;
}
return makeNameForCheckedClass(ClassInfo.fromType(root, type));
}
return StringUtils.escape(type.toString());
}
private String makeNameForCheckedClass(ClassInfo classInfo) {
String shortName = classInfo.getAliasShortName();
String vName = fromName(shortName);
if (vName != null) {
return vName;
}
String lower = StringUtils.escape(shortName.toLowerCase());
if (shortName.equals(lower)) {
return lower + "Var";
}
return lower;
}
private String makeNameForClass(ClassInfo classInfo) {
String alias = getAliasForObject(classInfo.getFullName());
if (alias != null) {
return alias;
}
return makeNameForCheckedClass(classInfo);
}
private static String fromName(String name) {
if (name == null || name.isEmpty()) {
return null;
}
if (name.toUpperCase().equals(name)) {
// all characters are upper case
return name.toLowerCase();
}
String v1 = Character.toLowerCase(name.charAt(0)) + name.substring(1);
if (!v1.equals(name)) {
return v1;
}
if (name.length() < 3) {
return name + "Var";
}
return null;
}
private static String getAliasForObject(String name) {
return OBJ_ALIAS.get(name);
}
private String makeNameFromInvoke(SSAVar ssaVar, InvokeNode inv) {
MethodInfo callMth = inv.getCallMth();
String name = callMth.getAlias();
ClassInfo declClass = callMth.getDeclClass();
if ("getInstance".equals(name)) {
// e.g. Cipher.getInstance
return makeNameForClass(declClass);
}
String shortName = cutPrefix(name);
if (shortName != null) {
return fromName(shortName);
}
if ("iterator".equals(name)) {
return "it";
}
if ("toString".equals(name)) {
return makeNameForClass(declClass);
}
if ("forName".equals(name) && declClass.getType().equals(ArgType.CLASS)) {
return OBJ_ALIAS.get(Consts.CLASS_CLASS);
}
// use method name as a variable name not the best idea in most cases
if (!GOOD_VAR_NAMES.contains(name)) {
String typeName = makeNameForType(ssaVar.getCodeVar().getType());
if (!typeName.equalsIgnoreCase(name)) {
return typeName + StringUtils.capitalizeFirstChar(name);
}
}
return name;
}
private @Nullable String cutPrefix(String name) {
for (String prefix : INVOKE_PREFIXES) {
if (name.startsWith(prefix)) {
return name.substring(prefix.length());
}
}
return null;
}
@Override
public String getName() {
return "ApplyVariableNames";
}
}
@@ -331,6 +331,7 @@ public class ProcessVariables extends AbstractVisitor {
return false;
}
parentInsn.add(AFlag.DECLARE_VAR);
var.getCodeVar().setDeclared(true);
return true;
}
@@ -483,4 +483,11 @@ public class StringUtils {
}
return Float.toString(f) + 'f';
}
public static String capitalizeFirstChar(String str) {
if (isEmpty(str)) {
return str;
}
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
}
}
@@ -133,11 +133,11 @@ public class TestIfCodeStyle extends SmaliTest {
.oneOf(c -> c.doesNotContain("else").countString(8, "return;"),
c -> c.countString(1, "else").countString(7, "return;"))
.containsLines(2,
"if (readInt < 0) {",
indent() + "if (dataPosition > Integer.MAX_VALUE - readInt) {",
"if (i < 0) {",
indent() + "if (iDataPosition > Integer.MAX_VALUE - i) {",
indent(2) + "throw new RuntimeException(\"Overflow in the size of parcelable\");",
indent() + "}",
indent() + "parcel.setDataPosition(dataPosition + readInt);",
indent() + "parcel.setDataPosition(iDataPosition + i);",
indent() + "return;",
"}");
}
@@ -16,7 +16,7 @@ public class TestInnerAssign3 extends SmaliTest {
disableCompilation();
assertThat(getClassNodeFromSmali())
.code()
.containsOne("(testMethod = (testClass1 = null).testMethod()) == null")
.containsOne("(testClass2TestMethod = (testClass1 = null).testMethod()) == null")
.containsOne("testClass1.testField != null");
}
}
@@ -38,8 +38,8 @@ public class TestLambdaInstance3 extends RaungTest {
assertThat(getClassNode(TestCls.class))
.code()
.doesNotContain("this::get")
.containsOne("return (TestCls) of::get;");
// TODO: type inference set type for 'of' to Memoized and cast incorrectly removed
.containsOne("return (TestCls) lazyOf::get;");
// TODO: type inference set type for 'lazyOf' to Memoized and cast incorrectly removed
// .containsOne("Memoized)");
}
@@ -49,6 +49,6 @@ public class TestLambdaInstance3 extends RaungTest {
assertThat(getClassNodeFromRaung())
.code()
.doesNotContain("this::get")
.containsOne(" of::get");
.containsOne(" lazyOf::get");
}
}
@@ -13,6 +13,6 @@ public class TestLoopRestore extends SmaliTest {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("try {")
.containsOne("for (byte b : digest) {");
.containsOne("for (byte b : bArrDigest) {");
}
}
@@ -39,6 +39,6 @@ public class TestTryCatchFinally15 extends SmaliTest {
assertThat(getClassNodeFromSmali())
.code()
.doesNotContain("parcel = Parcel.obtain();")
.containsOne("this.zza.transact(i, parcel, obtain, 0);");
.containsOne("this.zza.transact(i, parcel, parcelObtain, 0);");
}
}
@@ -17,7 +17,7 @@ public class TestPrimitiveConversion2 extends SmaliTest {
.doesNotContain("z2 == 0")
.doesNotContain("z2 | 2")
.containsOne("(z2 ? 1 : 0) | 2")
.containsOne("if (z2 && formatCurrency != null) {")
.containsOne("if (z2 && currency != null) {")
.containsOne("i = 1;");
}
}
@@ -36,6 +36,6 @@ public class TestPrimitivesInIf extends IntegrationTest {
noDebugInfo();
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("short parseShort = Short.parseShort(str);");
.containsOne("short s = Short.parseShort(str);");
}
}
@@ -44,13 +44,13 @@ public class TestTypeResolver16 extends SmaliTest {
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("(List<T>) list");
.containsOne("(List<T>) listUnion");
}
@Test
public void testSmali() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("(List<T>) list");
.containsOne("(List<T>) listUnion");
}
}
@@ -36,7 +36,7 @@ public class TestTypeResolver17 extends SmaliTest {
disableCompilation();
assertThat(getClassNodeFromSmali())
.code()
.containsOne("Cursor cursor = null;")
.containsOne("Cursor cursorQuery = null;")
.doesNotContain("(AutoCloseable autoCloseable = ");
}
}
@@ -12,8 +12,8 @@ public class TestVariablesInLoop extends SmaliTest {
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("int i;")
.countString(2, "i = 0;")
.doesNotContain("i3");
.containsOne("int iMth;")
.countString(2, "iMth = 0;")
.doesNotContain("i2");
}
}