diff --git a/jadx-core/src/main/java/jadx/core/Jadx.java b/jadx-core/src/main/java/jadx/core/Jadx.java index ae1d6f394..fa8a34293 100644 --- a/jadx-core/src/main/java/jadx/core/Jadx.java +++ b/jadx-core/src/main/java/jadx/core/Jadx.java @@ -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()); diff --git a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java index 75fc28a91..0ce18c02d 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java @@ -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 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(' '); diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java index 2a5d09863..507979f11 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java @@ -33,6 +33,7 @@ public enum AFlag { SKIP_FIRST_ARG, SKIP_ARG, // skip argument in invoke call + NO_SKIP_ARGS, ANONYMOUS_CONSTRUCTOR, ANONYMOUS_CLASS, diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index f4d1c4fcf..f6b2bf055 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -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 { 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()); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/AnonymousClassVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/AnonymousClassVisitor.java new file mode 100644 index 000000000..04a91383e --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/AnonymousClassVisitor.java @@ -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 usedInsns = new ArrayList<>(); + Map argsMap = getArgsToFieldsMapping(mth, usedInsns); + if (argsMap.isEmpty()) { + mth.add(AFlag.NO_SKIP_ARGS); + } else { + for (Map.Entry 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 getArgsToFieldsMapping(MethodNode mth, List usedInsns) { + MethodInfo callMth = mth.getMethodInfo(); + ClassNode cls = mth.getParentClass(); + List 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 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; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java index 75a38bdea..c2d153f8e 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java @@ -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 usedInsns = new ArrayList<>(); + Map argsMap = getArgsToFieldsMapping(mth, usedInsns); + for (Map.Entry 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 getArgsToFieldsMapping(MethodNode mth, List usedInsns) { + MethodInfo callMth = mth.getMethodInfo(); + ClassNode cls = mth.getParentClass(); + List 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 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) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java index 2f8c12151..92ba5161d 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java @@ -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 argsMap = getArgsToFieldsMapping(callMthNode, co); - if (argsMap.isEmpty() && !callMthNode.getArgRegs().isEmpty()) { - return; - } - - for (Map.Entry 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 getArgsToFieldsMapping(MethodNode callMthNode, ConstructorInsn co) { - Map map = new LinkedHashMap<>(); - MethodInfo callMth = callMthNode.getMethodInfo(); - ClassNode cls = callMthNode.getParentClass(); - ClassNode parentClass = cls.getParentClass(); - List 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; } /** diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java index 844a3f6d5..53f9c45d3 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java @@ -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 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; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java index 981df7b74..810e0b1ab 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java @@ -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++) { diff --git a/jadx-core/src/main/java/jadx/core/utils/ListUtils.java b/jadx-core/src/main/java/jadx/core/utils/ListUtils.java index 0c9865e04..740d0a960 100644 --- a/jadx-core/src/main/java/jadx/core/utils/ListUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/ListUtils.java @@ -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 void safeRemove(List 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 filterOnlyOne(List list, Predicate 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 boolean allMatch(List list, Predicate test) { + if (list == null || list.isEmpty()) { + return false; + } + for (T element : list) { + if (!test.test(element)) { + return false; + } + } + return true; + } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass19.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass19.java new file mode 100644 index 000000000..fe8fd9685 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass19.java @@ -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);"); + } +} diff --git a/jadx-core/src/test/smali/inner/TestAnonymousClass19/ATestCls.smali b/jadx-core/src/test/smali/inner/TestAnonymousClass19/ATestCls.smali new file mode 100644 index 000000000..a93cdcb85 --- /dev/null +++ b/jadx-core/src/test/smali/inner/TestAnonymousClass19/ATestCls.smali @@ -0,0 +1,52 @@ +.class public Linner/ATestCls; +.super Ljava/lang/Object; + +.method public constructor ()V + .registers 1 + + .prologue + .line 11 + invoke-direct {p0}, Ljava/lang/Object;->()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;->(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 diff --git a/jadx-core/src/test/smali/inner/TestAnonymousClass19/Lambda$TestCls$1.smali b/jadx-core/src/test/smali/inner/TestAnonymousClass19/Lambda$TestCls$1.smali new file mode 100644 index 000000000..d90443748 --- /dev/null +++ b/jadx-core/src/test/smali/inner/TestAnonymousClass19/Lambda$TestCls$1.smali @@ -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 (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;->()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;->()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 diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java index 584b54a21..15a4ad5a0 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java @@ -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; * */ 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); + } } }