fix: improve anonymous class inline (#523)
This commit is contained in:
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.CommentsLevel;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.core.dex.visitors.AnonymousClassVisitor;
|
||||
import jadx.core.dex.visitors.AttachCommentsVisitor;
|
||||
import jadx.core.dex.visitors.AttachMethodDetails;
|
||||
import jadx.core.dex.visitors.AttachTryCatchVisitor;
|
||||
@@ -135,6 +136,7 @@ public class Jadx {
|
||||
passes.add(new GenericTypesVisitor());
|
||||
passes.add(new ShadowFieldVisitor());
|
||||
passes.add(new DeboxingVisitor());
|
||||
passes.add(new AnonymousClassVisitor());
|
||||
passes.add(new ModVisitor());
|
||||
passes.add(new CodeShrinkVisitor());
|
||||
passes.add(new ReSugarCode());
|
||||
|
||||
@@ -728,6 +728,19 @@ public class InsnGen {
|
||||
code.add("new ");
|
||||
useClass(code, parent);
|
||||
MethodNode callMth = mth.root().resolveMethod(insn.getCallMth());
|
||||
if (callMth != null) {
|
||||
// copy var names
|
||||
List<RegisterArg> mthArgs = callMth.getArgRegs();
|
||||
int argsCount = Math.min(insn.getArgsCount(), mthArgs.size());
|
||||
for (int i = 0; i < argsCount; i++) {
|
||||
InsnArg arg = insn.getArg(i);
|
||||
if (arg.isRegister()) {
|
||||
RegisterArg mthArg = mthArgs.get(i);
|
||||
RegisterArg insnArg = (RegisterArg) arg;
|
||||
mthArg.getSVar().setCodeVar(insnArg.getSVar().getCodeVar());
|
||||
}
|
||||
}
|
||||
}
|
||||
generateMethodArguments(code, insn, 0, callMth);
|
||||
code.add(' ');
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ public enum AFlag {
|
||||
|
||||
SKIP_FIRST_ARG,
|
||||
SKIP_ARG, // skip argument in invoke call
|
||||
NO_SKIP_ARGS,
|
||||
ANONYMOUS_CONSTRUCTOR,
|
||||
ANONYMOUS_CLASS,
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
|
||||
import static jadx.core.dex.nodes.ProcessState.LOADED;
|
||||
import static jadx.core.dex.nodes.ProcessState.NOT_LOADED;
|
||||
import static jadx.core.dex.nodes.ProcessState.PROCESS_COMPLETE;
|
||||
|
||||
public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeNode, Comparable<ClassNode> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClassNode.class);
|
||||
@@ -283,12 +282,15 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean checkProcessed() {
|
||||
return getTopParentClass().getState().isProcessComplete();
|
||||
}
|
||||
|
||||
public void ensureProcessed() {
|
||||
ClassNode topClass = getTopParentClass();
|
||||
ProcessState state = topClass.getState();
|
||||
if (state != PROCESS_COMPLETE) {
|
||||
if (!checkProcessed()) {
|
||||
ClassNode topParentClass = getTopParentClass();
|
||||
throw new JadxRuntimeException("Expected class to be processed at this point,"
|
||||
+ " class: " + topClass + ", state: " + state);
|
||||
+ " class: " + topParentClass + ", state: " + topParentClass.getState());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package jadx.core.dex.visitors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
|
||||
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
|
||||
import jadx.core.dex.info.FieldInfo;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.instructions.IndexInsnNode;
|
||||
import jadx.core.dex.instructions.InsnType;
|
||||
import jadx.core.dex.instructions.args.InsnArg;
|
||||
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.dex.visitors.shrink.CodeShrinkVisitor;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
|
||||
@JadxVisitor(
|
||||
name = "AnonymousClassVisitor",
|
||||
desc = "Prepare anonymous class for inline",
|
||||
runBefore = {
|
||||
ModVisitor.class,
|
||||
CodeShrinkVisitor.class
|
||||
}
|
||||
)
|
||||
public class AnonymousClassVisitor extends AbstractVisitor {
|
||||
|
||||
@Override
|
||||
public boolean visit(ClassNode cls) throws JadxException {
|
||||
if (cls.contains(AFlag.ANONYMOUS_CLASS)) {
|
||||
for (MethodNode mth : cls.getMethods()) {
|
||||
if (mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
|
||||
processAnonymousConstructor(mth);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void processAnonymousConstructor(MethodNode mth) {
|
||||
List<InsnNode> usedInsns = new ArrayList<>();
|
||||
Map<InsnArg, FieldNode> argsMap = getArgsToFieldsMapping(mth, usedInsns);
|
||||
if (argsMap.isEmpty()) {
|
||||
mth.add(AFlag.NO_SKIP_ARGS);
|
||||
} else {
|
||||
for (Map.Entry<InsnArg, FieldNode> entry : argsMap.entrySet()) {
|
||||
FieldNode field = entry.getValue();
|
||||
if (field == null) {
|
||||
continue;
|
||||
}
|
||||
InsnArg arg = entry.getKey();
|
||||
field.addAttr(new FieldReplaceAttr(arg));
|
||||
field.add(AFlag.DONT_GENERATE);
|
||||
if (arg.isRegister()) {
|
||||
arg.add(AFlag.SKIP_ARG);
|
||||
SkipMethodArgsAttr.skipArg(mth, ((RegisterArg) arg));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (InsnNode usedInsn : usedInsns) {
|
||||
usedInsn.add(AFlag.DONT_GENERATE);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<InsnArg, FieldNode> getArgsToFieldsMapping(MethodNode mth, List<InsnNode> usedInsns) {
|
||||
MethodInfo callMth = mth.getMethodInfo();
|
||||
ClassNode cls = mth.getParentClass();
|
||||
List<RegisterArg> argList = mth.getArgRegs();
|
||||
ClassNode outerCls = mth.getUseIn().get(0).getParentClass();
|
||||
int startArg = 0;
|
||||
if (callMth.getArgsCount() != 0 && callMth.getArgumentsTypes().get(0).equals(outerCls.getClassInfo().getType())) {
|
||||
startArg = 1;
|
||||
}
|
||||
Map<InsnArg, FieldNode> map = new LinkedHashMap<>();
|
||||
int argsCount = argList.size();
|
||||
for (int i = startArg; i < argsCount; i++) {
|
||||
RegisterArg arg = argList.get(i);
|
||||
InsnNode useInsn = getParentInsnSkipMove(arg);
|
||||
if (useInsn == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
switch (useInsn.getType()) {
|
||||
case IPUT:
|
||||
FieldNode fieldNode = cls.searchField((FieldInfo) ((IndexInsnNode) useInsn).getIndex());
|
||||
if (fieldNode == null || !fieldNode.getAccessFlags().isSynthetic()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
map.put(arg, fieldNode);
|
||||
usedInsns.add(useInsn);
|
||||
break;
|
||||
|
||||
case CONSTRUCTOR:
|
||||
ConstructorInsn superConstr = (ConstructorInsn) useInsn;
|
||||
if (!superConstr.isSuper()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
usedInsns.add(useInsn);
|
||||
break;
|
||||
|
||||
default:
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static InsnNode getParentInsnSkipMove(RegisterArg arg) {
|
||||
SSAVar sVar = arg.getSVar();
|
||||
if (sVar.getUseCount() != 1) {
|
||||
return null;
|
||||
}
|
||||
RegisterArg useArg = sVar.getUseList().get(0);
|
||||
InsnNode parentInsn = useArg.getParentInsn();
|
||||
if (parentInsn == null) {
|
||||
return null;
|
||||
}
|
||||
if (parentInsn.getType() == InsnType.MOVE) {
|
||||
return getParentInsnSkipMove(parentInsn.getResult());
|
||||
}
|
||||
return parentInsn;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package jadx.core.dex.visitors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import jadx.api.plugins.input.data.AccessFlags;
|
||||
@@ -55,7 +58,7 @@ public class ClassModifier extends AbstractVisitor {
|
||||
removeSyntheticFields(cls);
|
||||
cls.getMethods().forEach(ClassModifier::removeSyntheticMethods);
|
||||
cls.getMethods().forEach(ClassModifier::removeEmptyMethods);
|
||||
cls.getMethods().forEach(ClassModifier::cleanInsnsInAnonymousConstructor);
|
||||
cls.getMethods().forEach(ClassModifier::processAnonymousConstructor);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -326,27 +329,86 @@ public class ClassModifier extends AbstractVisitor {
|
||||
/**
|
||||
* Remove super call and put into removed fields from anonymous constructor
|
||||
*/
|
||||
private static void cleanInsnsInAnonymousConstructor(MethodNode mth) {
|
||||
private static void processAnonymousConstructor(MethodNode mth) {
|
||||
if (!mth.contains(AFlag.ANONYMOUS_CONSTRUCTOR)) {
|
||||
return;
|
||||
}
|
||||
for (BlockNode block : mth.getBasicBlocks()) {
|
||||
for (InsnNode insn : block.getInstructions()) {
|
||||
InsnType type = insn.getType();
|
||||
if (type == InsnType.CONSTRUCTOR) {
|
||||
ConstructorInsn ctorInsn = (ConstructorInsn) insn;
|
||||
if (ctorInsn.isSuper()) {
|
||||
ctorInsn.add(AFlag.DONT_GENERATE);
|
||||
}
|
||||
} else if (type == InsnType.IPUT) {
|
||||
FieldInfo fldInfo = (FieldInfo) ((IndexInsnNode) insn).getIndex();
|
||||
FieldNode fieldNode = mth.root().resolveField(fldInfo);
|
||||
if (fieldNode != null && fieldNode.contains(AFlag.DONT_GENERATE)) {
|
||||
insn.add(AFlag.DONT_GENERATE);
|
||||
}
|
||||
}
|
||||
List<InsnNode> usedInsns = new ArrayList<>();
|
||||
Map<InsnArg, FieldNode> argsMap = getArgsToFieldsMapping(mth, usedInsns);
|
||||
for (Map.Entry<InsnArg, FieldNode> entry : argsMap.entrySet()) {
|
||||
FieldNode field = entry.getValue();
|
||||
if (field == null) {
|
||||
continue;
|
||||
}
|
||||
InsnArg arg = entry.getKey();
|
||||
field.addAttr(new FieldReplaceAttr(arg));
|
||||
field.add(AFlag.DONT_GENERATE);
|
||||
if (arg.isRegister()) {
|
||||
arg.add(AFlag.SKIP_ARG);
|
||||
SkipMethodArgsAttr.skipArg(mth, ((RegisterArg) arg));
|
||||
}
|
||||
}
|
||||
for (InsnNode usedInsn : usedInsns) {
|
||||
usedInsn.add(AFlag.DONT_GENERATE);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<InsnArg, FieldNode> getArgsToFieldsMapping(MethodNode mth, List<InsnNode> usedInsns) {
|
||||
MethodInfo callMth = mth.getMethodInfo();
|
||||
ClassNode cls = mth.getParentClass();
|
||||
List<RegisterArg> argList = mth.getArgRegs();
|
||||
ClassNode outerCls = mth.getUseIn().get(0).getParentClass();
|
||||
int startArg = 0;
|
||||
if (callMth.getArgsCount() != 0 && callMth.getArgumentsTypes().get(0).equals(outerCls.getClassInfo().getType())) {
|
||||
startArg = 1;
|
||||
}
|
||||
Map<InsnArg, FieldNode> map = new LinkedHashMap<>();
|
||||
int argsCount = argList.size();
|
||||
for (int i = startArg; i < argsCount; i++) {
|
||||
RegisterArg arg = argList.get(i);
|
||||
InsnNode useInsn = getParentInsnSkipMove(arg);
|
||||
if (useInsn == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
switch (useInsn.getType()) {
|
||||
case IPUT:
|
||||
FieldNode fieldNode = cls.searchField((FieldInfo) ((IndexInsnNode) useInsn).getIndex());
|
||||
if (fieldNode == null || !fieldNode.getAccessFlags().isSynthetic()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
map.put(arg, fieldNode);
|
||||
usedInsns.add(useInsn);
|
||||
break;
|
||||
|
||||
case CONSTRUCTOR:
|
||||
ConstructorInsn superConstr = (ConstructorInsn) useInsn;
|
||||
if (!superConstr.isSuper()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
usedInsns.add(useInsn);
|
||||
break;
|
||||
|
||||
default:
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static InsnNode getParentInsnSkipMove(RegisterArg arg) {
|
||||
SSAVar sVar = arg.getSVar();
|
||||
if (sVar.getUseCount() != 1) {
|
||||
return null;
|
||||
}
|
||||
RegisterArg useArg = sVar.getUseList().get(0);
|
||||
InsnNode parentInsn = useArg.getParentInsn();
|
||||
if (parentInsn == null) {
|
||||
return null;
|
||||
}
|
||||
if (parentInsn.getType() == InsnType.MOVE) {
|
||||
return getParentInsnSkipMove(parentInsn.getResult());
|
||||
}
|
||||
return parentInsn;
|
||||
}
|
||||
|
||||
private static boolean isNonDefaultConstructorExists(MethodNode defCtor) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package jadx.core.dex.visitors;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -19,10 +17,9 @@ import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.AType;
|
||||
import jadx.core.dex.attributes.AttrNode;
|
||||
import jadx.core.dex.attributes.nodes.FieldReplaceAttr;
|
||||
import jadx.core.dex.attributes.nodes.SkipMethodArgsAttr;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.info.FieldInfo;
|
||||
import jadx.core.dex.info.MethodInfo;
|
||||
import jadx.core.dex.instructions.ArithNode;
|
||||
import jadx.core.dex.instructions.ConstClassNode;
|
||||
import jadx.core.dex.instructions.ConstStringNode;
|
||||
@@ -46,6 +43,7 @@ import jadx.core.dex.instructions.mods.TernaryInsn;
|
||||
import jadx.core.dex.nodes.BlockNode;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.IMethodDetails;
|
||||
import jadx.core.dex.nodes.InsnNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.regions.conditions.IfCondition;
|
||||
@@ -432,96 +430,39 @@ public class ModVisitor extends AbstractVisitor {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* For args in anonymous constructor invoke apply:
|
||||
* - forbid inline into constructor call
|
||||
* - make variables final (compiler require this implicitly)
|
||||
*/
|
||||
private static void processAnonymousConstructor(MethodNode mth, ConstructorInsn co) {
|
||||
MethodInfo callMth = co.getCallMth();
|
||||
MethodNode callMthNode = mth.root().resolveMethod(callMth);
|
||||
if (callMthNode == null) {
|
||||
IMethodDetails callMthDetails = mth.root().getMethodUtils().getMethodDetails(co);
|
||||
if (!(callMthDetails instanceof MethodNode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClassNode classNode = callMthNode.getParentClass();
|
||||
if (!classNode.isAnonymous()) {
|
||||
MethodNode callMth = (MethodNode) callMthDetails;
|
||||
if (!callMth.contains(AFlag.ANONYMOUS_CONSTRUCTOR) || callMth.contains(AFlag.NO_SKIP_ARGS)) {
|
||||
return;
|
||||
}
|
||||
if (!mth.getParentClass().getInnerClasses().contains(classNode)) {
|
||||
return;
|
||||
}
|
||||
Map<InsnArg, FieldNode> argsMap = getArgsToFieldsMapping(callMthNode, co);
|
||||
if (argsMap.isEmpty() && !callMthNode.getArgRegs().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Map.Entry<InsnArg, FieldNode> entry : argsMap.entrySet()) {
|
||||
FieldNode field = entry.getValue();
|
||||
if (field == null) {
|
||||
continue;
|
||||
}
|
||||
InsnArg arg = entry.getKey();
|
||||
field.addAttr(new FieldReplaceAttr(arg));
|
||||
field.add(AFlag.DONT_GENERATE);
|
||||
if (arg.isRegister()) {
|
||||
RegisterArg reg = (RegisterArg) arg;
|
||||
SSAVar sVar = reg.getSVar();
|
||||
if (sVar != null) {
|
||||
sVar.getCodeVar().setFinal(true);
|
||||
SkipMethodArgsAttr attr = callMth.get(AType.SKIP_MTH_ARGS);
|
||||
if (attr != null) {
|
||||
int argsCount = Math.min(callMth.getArgRegs().size(), co.getArgsCount());
|
||||
for (int i = 0; i < argsCount; i++) {
|
||||
if (attr.isSkip(i)) {
|
||||
anonymousCallArgMod(co.getArg(i));
|
||||
}
|
||||
reg.add(AFlag.DONT_INLINE);
|
||||
reg.add(AFlag.SKIP_ARG);
|
||||
}
|
||||
} else {
|
||||
// additional info not available apply mods to all args (the safest solution)
|
||||
co.getArguments().forEach(ModVisitor::anonymousCallArgMod);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<InsnArg, FieldNode> getArgsToFieldsMapping(MethodNode callMthNode, ConstructorInsn co) {
|
||||
Map<InsnArg, FieldNode> map = new LinkedHashMap<>();
|
||||
MethodInfo callMth = callMthNode.getMethodInfo();
|
||||
ClassNode cls = callMthNode.getParentClass();
|
||||
ClassNode parentClass = cls.getParentClass();
|
||||
List<RegisterArg> argList = callMthNode.getArgRegs();
|
||||
int startArg = 0;
|
||||
if (callMth.getArgsCount() != 0 && callMth.getArgumentsTypes().get(0).equals(parentClass.getClassInfo().getType())) {
|
||||
startArg = 1;
|
||||
private static void anonymousCallArgMod(InsnArg arg) {
|
||||
arg.add(AFlag.DONT_INLINE);
|
||||
if (arg.isRegister()) {
|
||||
((RegisterArg) arg).getSVar().getCodeVar().setFinal(true);
|
||||
}
|
||||
int argsCount = argList.size();
|
||||
for (int i = startArg; i < argsCount; i++) {
|
||||
RegisterArg arg = argList.get(i);
|
||||
InsnNode useInsn = getParentInsnSkipMove(arg);
|
||||
if (useInsn == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
FieldNode fieldNode = null;
|
||||
if (useInsn.getType() == InsnType.IPUT) {
|
||||
FieldInfo field = (FieldInfo) ((IndexInsnNode) useInsn).getIndex();
|
||||
fieldNode = cls.searchField(field);
|
||||
if (fieldNode == null || !fieldNode.getAccessFlags().isSynthetic()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
} else if (useInsn.getType() == InsnType.CONSTRUCTOR) {
|
||||
ConstructorInsn superConstr = (ConstructorInsn) useInsn;
|
||||
if (!superConstr.isSuper()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
} else {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
map.put(co.getArg(i), fieldNode);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static InsnNode getParentInsnSkipMove(RegisterArg arg) {
|
||||
SSAVar sVar = arg.getSVar();
|
||||
if (sVar.getUseCount() != 1) {
|
||||
return null;
|
||||
}
|
||||
RegisterArg useArg = sVar.getUseList().get(0);
|
||||
InsnNode parentInsn = useArg.getParentInsn();
|
||||
if (parentInsn == null) {
|
||||
return null;
|
||||
}
|
||||
if (parentInsn.getType() == InsnType.MOVE) {
|
||||
return getParentInsnSkipMove(parentInsn.getResult());
|
||||
}
|
||||
return parentInsn;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package jadx.core.dex.visitors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.attributes.nodes.AnonymousClassBaseAttr;
|
||||
import jadx.core.dex.info.AccessInfo;
|
||||
import jadx.core.dex.instructions.args.ArgType;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.MethodNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.dex.visitors.usage.UsageInfoVisitor;
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
|
||||
@JadxVisitor(
|
||||
@@ -36,89 +42,129 @@ public class ProcessAnonymous extends AbstractVisitor {
|
||||
}
|
||||
|
||||
private static void markAnonymousClass(ClassNode cls) {
|
||||
if (usedOnlyOnce(cls) || isAnonymous(cls) || isLambdaCls(cls)) {
|
||||
if (isStaticFieldUsedOutside(cls)) {
|
||||
return;
|
||||
}
|
||||
cls.add(AFlag.ANONYMOUS_CLASS);
|
||||
cls.addAttr(new AnonymousClassBaseAttr(getBaseType(cls)));
|
||||
cls.add(AFlag.DONT_GENERATE);
|
||||
boolean synthetic = cls.getAccessFlags().isSynthetic()
|
||||
|| cls.getClassInfo().getShortName().contains("$")
|
||||
|| Character.isDigit(cls.getClassInfo().getShortName().charAt(0));
|
||||
if (!synthetic) {
|
||||
return;
|
||||
}
|
||||
MethodNode anonymousConstructor = checkUsage(cls);
|
||||
if (anonymousConstructor == null) {
|
||||
return;
|
||||
}
|
||||
ArgType baseType = getBaseType(cls);
|
||||
if (baseType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (MethodNode mth : cls.getMethods()) {
|
||||
if (mth.isConstructor()) {
|
||||
mth.add(AFlag.ANONYMOUS_CONSTRUCTOR);
|
||||
cls.add(AFlag.ANONYMOUS_CLASS);
|
||||
cls.addAttr(new AnonymousClassBaseAttr(baseType));
|
||||
cls.add(AFlag.DONT_GENERATE);
|
||||
|
||||
anonymousConstructor.add(AFlag.ANONYMOUS_CONSTRUCTOR);
|
||||
// force anonymous class to be processed before outer class,
|
||||
// actual usage of outer class will be removed at anonymous class process,
|
||||
// see ModVisitor.processAnonymousConstructor method
|
||||
ClassNode outerCls = anonymousConstructor.getUseIn().get(0).getParentClass();
|
||||
ListUtils.safeRemove(cls.getDependencies(), outerCls);
|
||||
ListUtils.safeRemove(outerCls.getUseIn(), cls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks:
|
||||
* - class have only one constructor which used only once (allow common code for field init)
|
||||
* - methods or fields not used outside (allow only nested inner classes with synthetic usage)
|
||||
*
|
||||
* @return anonymous constructor method
|
||||
*/
|
||||
private static MethodNode checkUsage(ClassNode cls) {
|
||||
MethodNode ctr = ListUtils.filterOnlyOne(cls.getMethods(), MethodNode::isConstructor);
|
||||
if (ctr == null) {
|
||||
return null;
|
||||
}
|
||||
if (ctr.getUseIn().size() != 1) {
|
||||
// check if used in common field init in all constructors
|
||||
if (!checkForCommonFieldInit(ctr)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
MethodNode ctrUseMth = ctr.getUseIn().get(0);
|
||||
ClassNode ctrUseCls = ctrUseMth.getParentClass();
|
||||
if (ctrUseCls.equals(cls)) {
|
||||
// exclude self usage
|
||||
return null;
|
||||
}
|
||||
for (MethodNode mth : cls.getMethods()) {
|
||||
if (mth == ctr) {
|
||||
continue;
|
||||
}
|
||||
for (MethodNode useMth : mth.getUseIn()) {
|
||||
if (useMth.equals(ctrUseMth)) {
|
||||
continue;
|
||||
}
|
||||
if (badMethodUsage(cls, useMth, mth.getAccessFlags())) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (FieldNode field : cls.getFields()) {
|
||||
for (MethodNode useMth : field.getUseIn()) {
|
||||
if (badMethodUsage(cls, useMth, field.getAccessFlags())) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ctr;
|
||||
}
|
||||
|
||||
private static boolean badMethodUsage(ClassNode cls, MethodNode useMth, AccessInfo accessFlags) {
|
||||
ClassNode useCls = useMth.getParentClass();
|
||||
if (useCls.equals(cls)) {
|
||||
return false;
|
||||
}
|
||||
if (accessFlags.isSynthetic()) {
|
||||
// allow synthetic usage in inner class
|
||||
return !useCls.getParentClass().equals(cls);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks:
|
||||
* + all in constructors
|
||||
* + all usage in one class
|
||||
* - same field put (ignored: methods not loaded yet)
|
||||
*/
|
||||
private static boolean checkForCommonFieldInit(MethodNode ctrMth) {
|
||||
List<MethodNode> ctrUse = ctrMth.getUseIn();
|
||||
if (ctrUse.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
ClassNode firstUseCls = ctrUse.get(0).getParentClass();
|
||||
return ListUtils.allMatch(ctrUse, m -> m.isConstructor() && m.getParentClass().equals(firstUseCls));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ArgType getBaseType(ClassNode cls) {
|
||||
ArgType parent;
|
||||
if (cls.getInterfaces().size() == 1) {
|
||||
parent = cls.getInterfaces().get(0);
|
||||
} else {
|
||||
parent = cls.getSuperClass();
|
||||
int interfacesCount = cls.getInterfaces().size();
|
||||
if (interfacesCount > 1) {
|
||||
return null;
|
||||
}
|
||||
return parent != null ? parent : ArgType.OBJECT;
|
||||
}
|
||||
|
||||
private static boolean isStaticFieldUsedOutside(ClassNode cls) {
|
||||
ClassNode topCls = cls.getTopParentClass();
|
||||
for (FieldNode field : cls.getFields()) {
|
||||
if (field.isStatic()) {
|
||||
for (MethodNode useMth : field.getUseIn()) {
|
||||
ClassNode useCls = useMth.getParentClass().getTopParentClass();
|
||||
if (!useCls.equals(topCls)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
ArgType superCls = cls.getSuperClass();
|
||||
if (superCls == null || superCls.equals(ArgType.OBJECT)) {
|
||||
if (interfacesCount == 1) {
|
||||
return cls.getInterfaces().get(0);
|
||||
}
|
||||
return ArgType.OBJECT;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean usedOnlyOnce(ClassNode cls) {
|
||||
if (cls.getUseIn().size() == 1 && cls.getUseInMth().size() == 1) {
|
||||
// used only once
|
||||
boolean synthetic = cls.getAccessFlags().isSynthetic() || cls.getClassInfo().getShortName().contains("$");
|
||||
if (synthetic) {
|
||||
// must have only one constructor which used only once
|
||||
MethodNode ctr = null;
|
||||
for (MethodNode mth : cls.getMethods()) {
|
||||
if (mth.isConstructor()) {
|
||||
if (ctr != null) {
|
||||
ctr = null;
|
||||
break;
|
||||
}
|
||||
ctr = mth;
|
||||
}
|
||||
}
|
||||
return ctr != null && ctr.getUseIn().size() == 1;
|
||||
}
|
||||
if (interfacesCount == 0) {
|
||||
return superCls;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isAnonymous(ClassNode cls) {
|
||||
return cls.getClassInfo().isInner()
|
||||
&& Character.isDigit(cls.getClassInfo().getShortName().charAt(0))
|
||||
&& cls.getMethods().stream().filter(MethodNode::isConstructor).count() == 1;
|
||||
}
|
||||
|
||||
private static boolean isLambdaCls(ClassNode cls) {
|
||||
return cls.getAccessFlags().isSynthetic()
|
||||
&& cls.getAccessFlags().isFinal()
|
||||
&& cls.getClassInfo().getRawName().contains(".-$$Lambda$")
|
||||
&& countStaticFields(cls) == 0;
|
||||
}
|
||||
|
||||
private static int countStaticFields(ClassNode cls) {
|
||||
int c = 0;
|
||||
for (FieldNode field : cls.getFields()) {
|
||||
if (field.getAccessFlags().isStatic()) {
|
||||
c++;
|
||||
}
|
||||
// check if super class already implement that interface (weird case)
|
||||
ArgType interfaceType = cls.getInterfaces().get(0);
|
||||
if (cls.root().getClsp().isImplements(superCls.getObject(), interfaceType.getObject())) {
|
||||
return superCls;
|
||||
}
|
||||
return c;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ public class SignatureProcessor extends AbstractVisitor {
|
||||
return newArgTypes;
|
||||
}
|
||||
}
|
||||
mth.addWarnComment("Incorrect args count in method signature: " + sp.getSignature());
|
||||
mth.addDebugComment("Incorrect args count in method signature: " + sp.getSignature());
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < len; i++) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -66,4 +67,45 @@ public class ListUtils {
|
||||
list.add(newObj);
|
||||
return list;
|
||||
}
|
||||
|
||||
public static <T> void safeRemove(List<T> list, T obj) {
|
||||
if (list != null && !list.isEmpty()) {
|
||||
list.remove(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search exactly one element in list by filter
|
||||
*
|
||||
* @return null if found not exactly one element (zero or more than one)
|
||||
*/
|
||||
@Nullable
|
||||
public static <T> T filterOnlyOne(List<T> list, Predicate<T> filter) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
T found = null;
|
||||
for (T element : list) {
|
||||
if (filter.test(element)) {
|
||||
if (found != null) {
|
||||
// found second
|
||||
return null;
|
||||
}
|
||||
found = element;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
public static <T> boolean allMatch(List<T> list, Predicate<T> test) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (T element : list) {
|
||||
if (!test.test(element)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package jadx.tests.integration.inner;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import jadx.tests.api.SmaliTest;
|
||||
|
||||
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
|
||||
|
||||
public class TestAnonymousClass19 extends SmaliTest {
|
||||
|
||||
@SuppressWarnings({ "Convert2Lambda", "unused" })
|
||||
public static class TestCls {
|
||||
|
||||
public void test(boolean a, boolean b) {
|
||||
boolean c = a && b;
|
||||
use(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println(a + " && " + b + " = " + c);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void use(Runnable r) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
assertThat(getClassNode(TestCls.class))
|
||||
.code()
|
||||
.containsOne("System.out.println(a + \" && \" + b + \" = \" + c);");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmali() {
|
||||
assertThat(getClassNodeFromSmaliFiles("ATestCls"))
|
||||
.code()
|
||||
.containsOne("System.out.println(a + \" && \" + b + \" = \" + c);");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
.class public Linner/ATestCls;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.method public constructor <init>()V
|
||||
.registers 1
|
||||
|
||||
.prologue
|
||||
.line 11
|
||||
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
|
||||
|
||||
return-void
|
||||
.end method
|
||||
|
||||
|
||||
# virtual methods
|
||||
.method public test(ZZ)V
|
||||
.registers 5
|
||||
.param p1, "a" # Z
|
||||
.param p2, "b" # Z
|
||||
|
||||
.prologue
|
||||
.line 14
|
||||
if-eqz p1, :cond_e
|
||||
|
||||
if-eqz p2, :cond_e
|
||||
|
||||
const/4 v0, 0x1
|
||||
|
||||
.line 15
|
||||
.local v0, "c":Z
|
||||
:goto_5
|
||||
new-instance v1, Linner/Lambda$TestCls$1;
|
||||
|
||||
invoke-direct {v1, p0, p1, p2, v0}, Linner/Lambda$TestCls$1;-><init>(Linner/ATestCls;ZZZ)V
|
||||
|
||||
invoke-virtual {p0, v1}, Linner/ATestCls;->use(Ljava/lang/Runnable;)V
|
||||
|
||||
.line 21
|
||||
return-void
|
||||
|
||||
.line 14
|
||||
.end local v0 # "c":Z
|
||||
:cond_e
|
||||
const/4 v0, 0x0
|
||||
|
||||
goto :goto_5
|
||||
.end method
|
||||
|
||||
.method public use(Ljava/lang/Runnable;)V
|
||||
.registers 2
|
||||
return-void
|
||||
.end method
|
||||
@@ -0,0 +1,58 @@
|
||||
.class public final synthetic Linner/Lambda$TestCls$1;
|
||||
.super Ljava/lang/Object;
|
||||
|
||||
.implements Ljava/lang/Runnable;
|
||||
|
||||
.field final synthetic this$0:Linner/ATestCls;
|
||||
.field final synthetic val$a:Z
|
||||
.field final synthetic val$b:Z
|
||||
.field final synthetic val$c:Z
|
||||
|
||||
.method constructor <init>(Linner/ATestCls;ZZZ)V
|
||||
.registers 5
|
||||
.param p1, "this$0"
|
||||
.annotation system Ldalvik/annotation/Signature;
|
||||
value = {
|
||||
"()V"
|
||||
}
|
||||
.end annotation
|
||||
|
||||
.prologue
|
||||
.line 15
|
||||
iput-object p1, p0, Linner/Lambda$TestCls$1;->this$0:Linner/ATestCls;
|
||||
iput-boolean p2, p0, Linner/Lambda$TestCls$1;->val$a:Z
|
||||
iput-boolean p3, p0, Linner/Lambda$TestCls$1;->val$b:Z
|
||||
iput-boolean p4, p0, Linner/Lambda$TestCls$1;->val$c:Z
|
||||
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
|
||||
return-void
|
||||
.end method
|
||||
|
||||
.method public run()V
|
||||
.registers 4
|
||||
|
||||
.prologue
|
||||
.line 18
|
||||
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
|
||||
new-instance v1, Ljava/lang/StringBuilder;
|
||||
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
|
||||
iget-boolean v2, p0, Linner/Lambda$TestCls$1;->val$a:Z
|
||||
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder;
|
||||
move-result-object v1
|
||||
const-string v2, " && "
|
||||
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
|
||||
move-result-object v1
|
||||
iget-boolean v2, p0, Linner/Lambda$TestCls$1;->val$b:Z
|
||||
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder;
|
||||
move-result-object v1
|
||||
const-string v2, " = "
|
||||
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
|
||||
move-result-object v1
|
||||
iget-boolean v2, p0, Linner/Lambda$TestCls$1;->val$c:Z
|
||||
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Z)Ljava/lang/StringBuilder;
|
||||
move-result-object v1
|
||||
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
|
||||
move-result-object v1
|
||||
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
||||
.line 19
|
||||
return-void
|
||||
.end method
|
||||
@@ -8,6 +8,8 @@ import javax.swing.border.EmptyBorder;
|
||||
|
||||
import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory;
|
||||
import org.fife.ui.rsyntaxtextarea.TokenMakerFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.ui.TabbedPane;
|
||||
@@ -23,6 +25,7 @@ import jadx.gui.utils.NLS;
|
||||
* </ul>
|
||||
*/
|
||||
public final class ClassCodeContentPanel extends AbstractCodeContentPanel implements IViewStateSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClassCodeContentPanel.class);
|
||||
private static final long serialVersionUID = -7229931102504634591L;
|
||||
|
||||
private final transient CodePanel javaCodePanel;
|
||||
@@ -104,7 +107,15 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
|
||||
boolean isJava = viewState.getSubPath().equals("java");
|
||||
CodePanel activePanel = isJava ? javaCodePanel : smaliCodePanel;
|
||||
areaTabbedPane.setSelectedComponent(activePanel);
|
||||
activePanel.getCodeScrollPane().getViewport().setViewPosition(viewState.getViewPoint());
|
||||
activePanel.getCodeArea().setCaretPosition(viewState.getCaretPos());
|
||||
try {
|
||||
activePanel.getCodeScrollPane().getViewport().setViewPosition(viewState.getViewPoint());
|
||||
} catch (Exception e) {
|
||||
LOG.debug("Failed to restore view position: {}", viewState.getViewPoint(), e);
|
||||
}
|
||||
try {
|
||||
activePanel.getCodeArea().setCaretPosition(viewState.getCaretPos());
|
||||
} catch (Exception e) {
|
||||
LOG.debug("Failed to restore caret position: {}", viewState.getCaretPos(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user