diff --git a/jadx-core/src/main/java/jadx/core/Consts.java b/jadx-core/src/main/java/jadx/core/Consts.java index efe038512..f76c23b93 100644 --- a/jadx-core/src/main/java/jadx/core/Consts.java +++ b/jadx-core/src/main/java/jadx/core/Consts.java @@ -2,6 +2,7 @@ package jadx.core; public class Consts { public static final boolean DEBUG = false; + public static final boolean DEBUG_USAGE = false; public static final String CLASS_OBJECT = "java.lang.Object"; public static final String CLASS_STRING = "java.lang.String"; diff --git a/jadx-core/src/main/java/jadx/core/Jadx.java b/jadx-core/src/main/java/jadx/core/Jadx.java index 84889304f..83cbfa589 100644 --- a/jadx-core/src/main/java/jadx/core/Jadx.java +++ b/jadx-core/src/main/java/jadx/core/Jadx.java @@ -17,7 +17,6 @@ import jadx.core.dex.visitors.ClassModifier; import jadx.core.dex.visitors.ConstInlineVisitor; import jadx.core.dex.visitors.ConstructorVisitor; import jadx.core.dex.visitors.DeboxingVisitor; -import jadx.core.dex.visitors.DependencyCollector; import jadx.core.dex.visitors.DotGraphVisitor; import jadx.core.dex.visitors.EnumVisitor; import jadx.core.dex.visitors.ExtractFieldInit; @@ -55,6 +54,7 @@ import jadx.core.dex.visitors.regions.variables.ProcessVariables; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.dex.visitors.ssa.SSATransform; import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; +import jadx.core.dex.visitors.usage.UsageInfoVisitor; public class Jadx { private static final Logger LOG = LoggerFactory.getLogger(Jadx.class); @@ -79,7 +79,7 @@ public class Jadx { public static List getPreDecompilePassesList() { List passes = new ArrayList<>(); passes.add(new RenameVisitor()); - passes.add(new DependencyCollector()); + passes.add(new UsageInfoVisitor()); return passes; } diff --git a/jadx-core/src/main/java/jadx/core/ProcessClass.java b/jadx-core/src/main/java/jadx/core/ProcessClass.java index 5e0bad411..9ff97eb29 100644 --- a/jadx-core/src/main/java/jadx/core/ProcessClass.java +++ b/jadx-core/src/main/java/jadx/core/ProcessClass.java @@ -9,6 +9,7 @@ import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.utils.exceptions.JadxRuntimeException; +import static jadx.core.dex.nodes.ProcessState.GENERATED; 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; @@ -53,11 +54,16 @@ public final class ProcessClass { if (topParentClass != cls) { return generateCode(topParentClass); } + if (cls.getState() == GENERATED) { + // allow to run code generation again + cls.setState(NOT_LOADED); + } try { cls.getDependencies().forEach(ProcessClass::process); process(cls); ICodeInfo code = CodeGen.generate(cls); + cls.setState(GENERATED); cls.unload(); return code; } catch (Throwable e) { diff --git a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java index f8f6f5b59..86542eb1d 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java @@ -18,6 +18,7 @@ import jadx.api.JadxArgs; import jadx.api.plugins.input.data.AccessFlags; import jadx.api.plugins.input.data.annotations.EncodedType; import jadx.api.plugins.input.data.annotations.EncodedValue; +import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AttrNode; @@ -114,6 +115,9 @@ public class ClassGen { if (cls.contains(AFlag.DONT_GENERATE)) { return; } + if (Consts.DEBUG_USAGE) { + addClassUsageInfo(code, cls); + } CodeGenUtils.addComments(code, cls); insertDecompilationProblems(code, cls); addClassDeclaration(code); @@ -378,6 +382,9 @@ public class ClassGen { if (f.contains(AFlag.DONT_GENERATE)) { return; } + if (Consts.DEBUG_USAGE) { + addFieldUsageInfo(code, f); + } CodeGenUtils.addComments(code, f); annotationGen.addForField(code, f); @@ -574,11 +581,6 @@ public class ClassGen { if (extClsInfo.getPackage().equals(useCls.getPackage()) && !extClsInfo.isInner()) { return shortName; } - // don't add import if class not public (must be accessed using inheritance) - ClassNode classNode = cls.root().resolveClass(extClsInfo); - if (classNode != null && !classNode.getAccessFlags().isPublic()) { - return shortName; - } if (searchCollision(cls.root(), useCls, extClsInfo)) { return fullName; } @@ -685,6 +687,40 @@ public class ClassGen { } } + private static void addClassUsageInfo(CodeWriter code, ClassNode cls) { + List deps = cls.getDependencies(); + code.startLine("// deps - ").add(Integer.toString(deps.size())); + for (ClassNode depCls : deps) { + code.startLine("// ").add(depCls.getFullName()); + } + List useIn = cls.getUseIn(); + code.startLine("// use in - ").add(Integer.toString(useIn.size())); + for (ClassNode useCls : useIn) { + code.startLine("// ").add(useCls.getFullName()); + } + List useInMths = cls.getUseInMth(); + code.startLine("// use in methods - ").add(Integer.toString(useInMths.size())); + for (MethodNode useMth : useInMths) { + code.startLine("// ").add(useMth.toString()); + } + } + + static void addMthUsageInfo(CodeWriter code, MethodNode mth) { + List useInMths = mth.getUseIn(); + code.startLine("// use in methods - ").add(Integer.toString(useInMths.size())); + for (MethodNode useMth : useInMths) { + code.startLine("// ").add(useMth.toString()); + } + } + + private static void addFieldUsageInfo(CodeWriter code, FieldNode fieldNode) { + List useInMths = fieldNode.getUseIn(); + code.startLine("// use in methods - ").add(Integer.toString(useInMths.size())); + for (MethodNode useMth : useInMths) { + code.startLine("// ").add(useMth.toString()); + } + } + public ClassGen getParentGen() { return parentGen == null ? this : parentGen; } diff --git a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java index fd7456754..415f402fa 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java @@ -80,6 +80,9 @@ public class MethodGen { code.attachDefinition(mth); return false; } + if (Consts.DEBUG_USAGE) { + ClassGen.addMthUsageInfo(code, mth); + } addOverrideAnnotation(code, mth); annotationGen.addForMethod(code, mth); 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 0a7ad5321..8525106f1 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 @@ -42,7 +42,6 @@ import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.dex.nodes.ProcessState.LOADED; -import static jadx.core.dex.nodes.ProcessState.NOT_LOADED; public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeNode, Comparable { private static final Logger LOG = LoggerFactory.getLogger(ClassNode.class); @@ -69,8 +68,13 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN private ClassNode parentClass; private volatile ProcessState state = ProcessState.NOT_LOADED; + + /** Top level classes used in this class (only for top level classes, empty for inners) */ private List dependencies = Collections.emptyList(); - private List usedIn = Collections.emptyList(); + /** Classes which uses this class */ + private List useIn = Collections.emptyList(); + /** Methods which uses this class (by instructions only, definition is excluded) */ + private List useInMth = Collections.emptyList(); // cache maps private Map mthInfoMap = Collections.emptyMap(); @@ -305,7 +309,6 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN innerClasses.forEach(ClassNode::unload); fields.forEach(FieldNode::unloadAttributes); unloadAttributes(); - setState(NOT_LOADED); } private void buildCache() { @@ -585,12 +588,20 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN this.dependencies = dependencies; } - public List getUsedIn() { - return usedIn; + public List getUseIn() { + return useIn; } - public void setUsedIn(List usedIn) { - this.usedIn = usedIn; + public void setUseIn(List useIn) { + this.useIn = useIn; + } + + public List getUseInMth() { + return useInMth; + } + + public void setUseInMth(List useInMth) { + this.useInMth = useInMth; } @Override diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java index 305a38e54..7a0a60513 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java @@ -1,6 +1,8 @@ package jadx.core.dex.nodes; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; import jadx.api.plugins.input.data.IFieldData; import jadx.core.dex.attributes.annotations.AnnotationsList; @@ -18,6 +20,8 @@ public class FieldNode extends LineAttrNode implements ICodeNode { private ArgType type; + private List useIn = Collections.emptyList(); + public static FieldNode build(ClassNode cls, IFieldData fieldData) { FieldInfo fieldInfo = FieldInfo.fromData(cls.root(), fieldData); FieldNode fieldNode = new FieldNode(cls, fieldInfo, fieldData.getAccessFlags()); @@ -70,6 +74,14 @@ public class FieldNode extends LineAttrNode implements ICodeNode { return parentClass; } + public List getUseIn() { + return useIn; + } + + public void setUseIn(List useIn) { + this.useIn = useIn; + } + @Override public String typeName() { return "field"; diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java index 7b3e2bffb..c52ebeb6a 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java @@ -40,7 +40,7 @@ import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.Utils.lockList; -public class MethodNode extends NotificationAttrNode implements IMethodDetails, ILoadable, ICodeNode { +public class MethodNode extends NotificationAttrNode implements IMethodDetails, ILoadable, ICodeNode, Comparable { private static final Logger LOG = LoggerFactory.getLogger(MethodNode.class); private final MethodInfo mthInfo; @@ -72,6 +72,8 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails, private List loops; private Region region; + private List useIn = Collections.emptyList(); + public static MethodNode build(ClassNode classNode, IMethodData methodData) { MethodNode methodNode = new MethodNode(classNode, methodData); AnnotationsList.attach(methodNode, methodData.getAnnotations()); @@ -620,6 +622,14 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails, return codeReader; } + public List getUseIn() { + return useIn; + } + + public void setUseIn(List useIn) { + this.useIn = useIn; + } + @Override public int hashCode() { return mthInfo.hashCode(); @@ -637,6 +647,11 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails, return mthInfo.equals(other.mthInfo); } + @Override + public int compareTo(@NotNull MethodNode o) { + return mthInfo.compareTo(o.mthInfo); + } + @Override public String toString() { return parentClass + "." + mthInfo.getName() diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ProcessState.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ProcessState.java index 4cce135b8..847f7cdaf 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ProcessState.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ProcessState.java @@ -4,13 +4,10 @@ public enum ProcessState { NOT_LOADED, LOADED, PROCESS_STARTED, - PROCESS_COMPLETE; - - public boolean isLoaded() { - return this != NOT_LOADED; - } + PROCESS_COMPLETE, + GENERATED; public boolean isProcessed() { - return this == PROCESS_COMPLETE; + return this == PROCESS_COMPLETE || this == GENERATED; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/DependencyCollector.java b/jadx-core/src/main/java/jadx/core/dex/visitors/DependencyCollector.java deleted file mode 100644 index 9c39c31f5..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/DependencyCollector.java +++ /dev/null @@ -1,186 +0,0 @@ -package jadx.core.dex.visitors; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import jadx.api.plugins.input.data.ICodeReader; -import jadx.api.plugins.input.data.IFieldData; -import jadx.api.plugins.input.data.IMethodData; -import jadx.api.plugins.input.insns.InsnData; -import jadx.api.plugins.input.insns.Opcode; -import jadx.core.dex.attributes.AType; -import jadx.core.dex.info.ClassInfo; -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; - -@JadxVisitor( - name = "DependencyCollector", - desc = "Scan class and methods and collect dependant classes", - runAfter = { - RenameVisitor.class // sort by alias name - } -) -// TODO: store usage info for fields, methods and inner classes -public class DependencyCollector extends AbstractVisitor { - - @Override - public void init(RootNode root) { - List clsList = root.getClassesWithoutInner(); - for (ClassNode cls : clsList) { - collectClassDeps(cls); - } - buildUsageList(clsList); - } - - private void buildUsageList(List clsList) { - clsList.forEach(cls -> cls.setUsedIn(new ArrayList<>())); - for (ClassNode cls : clsList) { - for (ClassNode depCls : cls.getDependencies()) { - depCls.getUsedIn().add(cls); - } - } - for (ClassNode cls : clsList) { - List usedIn = cls.getUsedIn(); - if (usedIn.isEmpty()) { - cls.setUsedIn(Collections.emptyList()); - } else { - Collections.sort(usedIn); - } - } - } - - public void collectClassDeps(ClassNode cls) { - RootNode root = cls.root(); - Set depSet = new HashSet<>(); - processClass(cls, root, depSet); - for (ClassNode inner : cls.getInnerClasses()) { - processClass(inner, root, depSet); - } - depSet.remove(cls); - - if (depSet.isEmpty()) { - cls.setDependencies(Collections.emptyList()); - } else { - List depList = new ArrayList<>(depSet); - Collections.sort(depList); - cls.setDependencies(depList); - } - } - - private static void processClass(ClassNode cls, RootNode root, Set depList) { - addDep(root, depList, cls.getSuperClass()); - for (ArgType iType : cls.getInterfaces()) { - addDep(root, depList, iType); - } - for (FieldNode fieldNode : cls.getFields()) { - addDep(root, depList, fieldNode.getType()); - } - // TODO: process annotations and generics - for (MethodNode methodNode : cls.getMethods()) { - if (methodNode.isNoCode() || methodNode.contains(AType.JADX_ERROR)) { - continue; - } - processMethod(root, depList, methodNode); - } - } - - private static void processMethod(RootNode root, Set depList, MethodNode methodNode) { - addDep(root, depList, methodNode.getParentClass()); - addDep(root, depList, methodNode.getReturnType()); - for (ArgType arg : methodNode.getMethodInfo().getArgumentsTypes()) { - addDep(root, depList, arg); - } - try { - processInstructions(methodNode, depList); - } catch (Exception e) { - methodNode.getCodeReader().visitInstructions(insnData -> { - insnData.decode(); - System.out.println(insnData); - }); - methodNode.addError("Dependency scan failed", e); - } - } - - private static void processInstructions(MethodNode mth, Set deps) { - ICodeReader codeReader = mth.getCodeReader(); - if (codeReader == null) { - return; - } - RootNode root = mth.root(); - codeReader.visitInstructions(insnData -> { - try { - processInsn(root, insnData, deps); - } catch (Exception e) { - mth.addError("Dependency scan failed at insn: " + insnData, e); - } - }); - } - - private static void processInsn(RootNode root, InsnData insnData, Set deps) { - if (insnData.getOpcode() == Opcode.UNKNOWN) { - return; - } - switch (insnData.getIndexType()) { - case TYPE_REF: - insnData.decode(); - resolveType(root, deps, insnData.getIndexAsType()); - break; - case FIELD_REF: - insnData.decode(); - resolveField(root, deps, insnData.getIndexAsField()); - break; - case METHOD_REF: - insnData.decode(); - resolveMethod(root, deps, insnData.getIndexAsMethod()); - break; - } - } - - private static void resolveType(RootNode root, Set deps, String type) { - addDep(root, deps, ArgType.parse(type)); - } - - private static void resolveMethod(RootNode root, Set deps, IMethodData method) { - resolveType(root, deps, method.getParentClassType()); - } - - private static void resolveField(RootNode root, Set deps, IFieldData field) { - resolveType(root, deps, field.getParentClassType()); - } - - private static void addDep(RootNode root, Set depList, ArgType type) { - if (type != null) { - if (type.isObject() && !type.isGenericType()) { - addDep(root, depList, ClassInfo.fromType(root, type)); - ArgType[] genericTypes = type.getGenericTypes(); - if (type.isGeneric() && genericTypes != null) { - for (ArgType argType : genericTypes) { - addDep(root, depList, argType); - } - } - } else if (type.isArray()) { - addDep(root, depList, type.getArrayRootElement()); - } - } - } - - private static void addDep(RootNode root, Set depList, ClassInfo clsInfo) { - if (clsInfo != null) { - ClassNode node = root.resolveClass(clsInfo); - addDep(root, depList, node); - } - } - - private static void addDep(RootNode root, Set depList, ClassNode clsNode) { - if (clsNode != null) { - // add only top classes - depList.add(clsNode.getTopParentClass()); - } - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/FixAccessModifiers.java b/jadx-core/src/main/java/jadx/core/dex/visitors/FixAccessModifiers.java index a7166be8a..04d1c87a0 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/FixAccessModifiers.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/FixAccessModifiers.java @@ -56,7 +56,7 @@ public class FixAccessModifiers extends AbstractVisitor { } private int fixClassVisibility(ClassNode cls) { - if (cls.getUsedIn().isEmpty()) { + if (cls.getUseIn().isEmpty()) { return -1; } AccessInfo accessFlags = cls.getAccessFlags(); @@ -66,7 +66,7 @@ public class FixAccessModifiers extends AbstractVisitor { } // check if private inner class is used outside ClassNode topParentClass = cls.getTopParentClass(); - for (ClassNode useCls : cls.getUsedIn()) { + for (ClassNode useCls : cls.getUseIn()) { if (useCls.getTopParentClass() != topParentClass) { return AccessFlags.PUBLIC; } @@ -74,12 +74,21 @@ public class FixAccessModifiers extends AbstractVisitor { } if (accessFlags.isPackagePrivate()) { String pkg = cls.getPackage(); - for (ClassNode useCls : cls.getUsedIn()) { + for (ClassNode useCls : cls.getUseIn()) { if (!useCls.getPackage().equals(pkg)) { return AccessFlags.PUBLIC; } } } + if (!accessFlags.isPublic()) { + // if class is used in inlinable method => make it public + for (MethodNode useMth : cls.getUseInMth()) { + boolean canInline = MethodInlineVisitor.canInline(useMth) || useMth.contains(AType.METHOD_INLINE); + if (canInline && !useMth.getUseIn().isEmpty()) { + return AccessFlags.PUBLIC; + } + } + } return -1; } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/MethodInlineVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/MethodInlineVisitor.java index aecd0b4a8..a5f49af96 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/MethodInlineVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/MethodInlineVisitor.java @@ -34,12 +34,7 @@ public class MethodInlineVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) throws JadxException { - if (mth.isNoCode() || mth.contains(AFlag.DONT_GENERATE)) { - return; - } - AccessInfo accessFlags = mth.getAccessFlags(); - if (accessFlags.isSynthetic() && accessFlags.isStatic() - && mth.getBasicBlocks().size() == 2) { + if (canInline(mth) && mth.getBasicBlocks().size() == 2) { BlockNode returnBlock = mth.getBasicBlocks().get(1); if (returnBlock.contains(AFlag.RETURN) || returnBlock.getInstructions().isEmpty()) { BlockNode firstBlock = mth.getBasicBlocks().get(0); @@ -48,6 +43,14 @@ public class MethodInlineVisitor extends AbstractVisitor { } } + public static boolean canInline(MethodNode mth) { + if (mth.isNoCode() || mth.contains(AFlag.DONT_GENERATE)) { + return false; + } + AccessInfo accessFlags = mth.getAccessFlags(); + return accessFlags.isSynthetic() && accessFlags.isStatic(); + } + private static void inlineMth(MethodNode mth, BlockNode firstBlock, BlockNode returnBlock) { List insnList = firstBlock.getInstructions(); if (insnList.isEmpty()) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java new file mode 100644 index 000000000..edc071199 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java @@ -0,0 +1,101 @@ +package jadx.core.dex.visitors.usage; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +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; + +public class UsageInfo { + private final RootNode root; + + private final UseSet clsDeps = new UseSet<>(); + private final UseSet clsUsage = new UseSet<>(); + private final UseSet clsUseInMth = new UseSet<>(); + private final UseSet fieldUsage = new UseSet<>(); + private final UseSet mthUsage = new UseSet<>(); + + public UsageInfo(RootNode root) { + this.root = root; + } + + public void apply() { + clsDeps.visit((cls, deps) -> cls.setDependencies(sortedList(deps))); + clsUsage.visit((cls, deps) -> cls.setUseIn(sortedList(deps))); + clsUseInMth.visit((cls, methods) -> cls.setUseInMth(sortedList(methods))); + fieldUsage.visit((field, methods) -> field.setUseIn(sortedList(methods))); + mthUsage.visit((mth, methods) -> mth.setUseIn(sortedList(methods))); + } + + public void clsUse(ClassNode cls, ArgType useType) { + processType(useType, depCls -> clsUse(cls, depCls)); + } + + public void clsUse(MethodNode mth, ArgType useType) { + processType(useType, depCls -> clsUse(mth, depCls)); + } + + public void clsUse(MethodNode mth, ClassNode useCls) { + ClassNode parentClass = mth.getParentClass(); + clsUse(parentClass, useCls); + if (parentClass != useCls) { + // exclude class usage in self methods + clsUseInMth.add(useCls, mth); + } + } + + public void clsUse(ClassNode cls, ClassNode depCls) { + ClassNode topParentClass = cls.getTopParentClass(); + clsDeps.add(topParentClass, depCls.getTopParentClass()); + + clsUsage.add(depCls, cls); + clsUsage.add(depCls, topParentClass); + } + + /** + * Add method usage: {@code useMth} occurrence found in {@code mth} code + */ + public void methodUse(MethodNode mth, MethodNode useMth) { + clsUse(mth, useMth.getParentClass()); + mthUsage.add(useMth, mth); + } + + public void fieldUse(MethodNode mth, FieldNode useFld) { + clsUse(mth, useFld.getParentClass()); + fieldUsage.add(useFld, mth); + } + + private void processType(ArgType type, Consumer consumer) { + if (type == null) { + return; + } + if (type.isArray()) { + processType(type.getArrayRootElement(), consumer); + return; + } + if (type.isObject() && !type.isGenericType()) { + ClassNode clsNode = root.resolveClass(type); + if (clsNode != null) { + consumer.accept(clsNode); + } + ArgType[] genericTypes = type.getGenericTypes(); + if (type.isGeneric() && genericTypes != null) { + for (ArgType argType : genericTypes) { + processType(argType, consumer); + } + } + } + } + + private static > List sortedList(Set deps) { + List list = new ArrayList<>(deps); + Collections.sort(list); + return list; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java new file mode 100644 index 000000000..c191100e6 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java @@ -0,0 +1,115 @@ +package jadx.core.dex.visitors.usage; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.plugins.input.data.ICodeReader; +import jadx.api.plugins.input.insns.InsnData; +import jadx.api.plugins.input.insns.Opcode; +import jadx.core.dex.attributes.AType; +import jadx.core.dex.info.FieldInfo; +import jadx.core.dex.info.MethodInfo; +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.AbstractVisitor; +import jadx.core.dex.visitors.JadxVisitor; +import jadx.core.dex.visitors.RenameVisitor; + +@JadxVisitor( + name = "UsageInfoVisitor", + desc = "Scan class and methods to collect usage info and class dependencies", + runAfter = { + RenameVisitor.class // sort by alias name + } +) +public class UsageInfoVisitor extends AbstractVisitor { + private static final Logger LOG = LoggerFactory.getLogger(UsageInfoVisitor.class); + + @Override + public void init(RootNode root) { + long startTime = System.currentTimeMillis(); + UsageInfo usageInfo = new UsageInfo(root); + for (ClassNode cls : root.getClasses()) { + processClass(cls, usageInfo); + } + usageInfo.apply(); + LOG.debug("Dependency collection done in {}ms", System.currentTimeMillis() - startTime); + } + + private static void processClass(ClassNode cls, UsageInfo usageInfo) { + usageInfo.clsUse(cls, cls.getSuperClass()); + for (ArgType interfaceType : cls.getInterfaces()) { + usageInfo.clsUse(cls, interfaceType); + } + for (FieldNode fieldNode : cls.getFields()) { + usageInfo.clsUse(cls, fieldNode.getType()); + } + // TODO: process annotations and generics + for (MethodNode methodNode : cls.getMethods()) { + if (methodNode.isNoCode() || methodNode.contains(AType.JADX_ERROR)) { + continue; + } + processMethod(methodNode, usageInfo); + } + } + + private static void processMethod(MethodNode mth, UsageInfo usageInfo) { + ClassNode cls = mth.getParentClass(); + usageInfo.clsUse(cls, mth.getReturnType()); + for (ArgType argType : mth.getMethodInfo().getArgumentsTypes()) { + usageInfo.clsUse(cls, argType); + } + try { + processInstructions(mth, usageInfo); + } catch (Exception e) { + mth.addError("Dependency scan failed", e); + } + } + + private static void processInstructions(MethodNode mth, UsageInfo usageInfo) { + ICodeReader codeReader = mth.getCodeReader(); + if (codeReader == null) { + return; + } + RootNode root = mth.root(); + codeReader.visitInstructions(insnData -> { + try { + processInsn(root, mth, insnData, usageInfo); + } catch (Exception e) { + mth.addError("Dependency scan failed at insn: " + insnData, e); + } + }); + } + + private static void processInsn(RootNode root, MethodNode mth, InsnData insnData, UsageInfo usageInfo) { + if (insnData.getOpcode() == Opcode.UNKNOWN) { + return; + } + switch (insnData.getIndexType()) { + case TYPE_REF: + insnData.decode(); + ArgType usedType = ArgType.parse(insnData.getIndexAsType()); + usageInfo.clsUse(mth, usedType); + break; + + case FIELD_REF: + insnData.decode(); + FieldNode fieldNode = root.resolveField(FieldInfo.fromData(root, insnData.getIndexAsField())); + if (fieldNode != null) { + usageInfo.fieldUse(mth, fieldNode); + } + break; + + case METHOD_REF: + insnData.decode(); + MethodNode methodNode = root.resolveMethod(MethodInfo.fromData(root, insnData.getIndexAsMethod())); + if (methodNode != null) { + usageInfo.methodUse(mth, methodNode); + } + break; + } + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UseSet.java b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UseSet.java new file mode 100644 index 000000000..8a6cee12e --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UseSet.java @@ -0,0 +1,30 @@ +package jadx.core.dex.visitors.usage; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +public class UseSet { + private final Map> useMap = new HashMap<>(); + + public void add(K obj, V use) { + if (obj == use) { + // self excluded + return; + } + Set set = useMap.computeIfAbsent(obj, k -> new HashSet<>()); + set.add(use); + } + + public Set get(K obj) { + return useMap.get(obj); + } + + public void visit(BiConsumer> consumer) { + for (Map.Entry> entry : useMap.entrySet()) { + consumer.accept(entry.getKey(), entry.getValue()); + } + } +} diff --git a/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java b/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java index e41160696..55b21b897 100644 --- a/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java @@ -107,6 +107,7 @@ public abstract class SmaliTest extends IntegrationTest { try { SmaliOptions options = new SmaliOptions(); options.outputDexFile = output.getAbsolutePath(); + options.verboseErrors = true; List inputFileNames = inputFiles.stream().map(File::getAbsolutePath).collect(Collectors.toList()); Smali.assemble(options, inputFileNames); } catch (Exception e) { diff --git a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxAssertions.java b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxAssertions.java index a5a20421e..41913303d 100644 --- a/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxAssertions.java +++ b/jadx-core/src/test/java/jadx/tests/api/utils/assertj/JadxAssertions.java @@ -8,10 +8,12 @@ import jadx.core.dex.nodes.ClassNode; public class JadxAssertions extends Assertions { public static JadxClassNodeAssertions assertThat(ClassNode actual) { + Assertions.assertThat(actual).isNotNull(); return new JadxClassNodeAssertions(actual); } public static JadxCodeAssertions assertThat(ICodeInfo actual) { + Assertions.assertThat(actual).isNotNull(); return new JadxCodeAssertions(actual.getCodeStr()); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/inline/TestMethodInline.java b/jadx-core/src/test/java/jadx/tests/integration/inline/TestMethodInline.java new file mode 100644 index 000000000..d8c30035d --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/inline/TestMethodInline.java @@ -0,0 +1,55 @@ +package jadx.tests.integration.inline; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestMethodInline extends SmaliTest { + // @formatter:off + /* + package inline; + + public class A { + public static void useMth() { + inline.other.B.bridgeMth(); // after inline 'inline.other.C.test()' is not accessible + } + } + ----------------------------------------------------------- + package inline.other; + + public class B { + public static bridge synthetic void bridgeMth() { + inline.other.C.test(); + } + } + ---------------------------------------------------------- + package inline.other; + + class C { + public static void test() { + } + } + */ + // @formatter:on + + @Test + public void test() { + List classes = loadFromSmaliFiles(); + ClassNode aCls = searchCls(classes, "inline.A"); + ClassNode bCls = searchCls(classes, "inline.other.B"); + ClassNode cCls = searchCls(classes, "inline.other.C"); + + assertThat(bCls).code().doesNotContain("bridgeMth()"); + assertThat(aCls).code().containsOne("C.test()"); + assertThat(cCls).code().containsOne("public class C {"); + + // TODO: update dependencies? + // assertThat(aCls.getDependencies()).contains(cCls); + // assertThat(cCls.getUsedIn()).contains(aCls); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestFixClassAccessModifiers.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestFixClassAccessModifiers.java new file mode 100644 index 000000000..437554a13 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestFixClassAccessModifiers.java @@ -0,0 +1,31 @@ +package jadx.tests.integration.others; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestFixClassAccessModifiers extends SmaliTest { + // @formatter:off + /* + public Cls.InnerCls field; + + public static class Cls { + private static class InnerCls { + } + } + */ + // @formatter:on + + @Test + public void test() { + List classes = loadFromSmaliFiles(); + assertThat(searchCls(classes, "others.Cls")) + .code() + .containsOne("public static class InnerCls {"); + } +} diff --git a/jadx-core/src/test/smali/inline/TestMethodInline/A.smali b/jadx-core/src/test/smali/inline/TestMethodInline/A.smali new file mode 100644 index 000000000..fa4b8c4ac --- /dev/null +++ b/jadx-core/src/test/smali/inline/TestMethodInline/A.smali @@ -0,0 +1,10 @@ +.class public Linline/A; +.super Ljava/lang/Object; + +.method public static useMth()V + .locals 1 + + invoke-static {}, Linline/other/B;->bridgeMth()V + + return-void +.end method diff --git a/jadx-core/src/test/smali/inline/TestMethodInline/B.smali b/jadx-core/src/test/smali/inline/TestMethodInline/B.smali new file mode 100644 index 000000000..5162d9408 --- /dev/null +++ b/jadx-core/src/test/smali/inline/TestMethodInline/B.smali @@ -0,0 +1,10 @@ +.class public Linline/other/B; +.super Ljava/lang/Object; + +.method public static bridge synthetic bridgeMth()V + .locals 1 + + invoke-static {}, Linline/other/C;->test()V + + return-void +.end method diff --git a/jadx-core/src/test/smali/inline/TestMethodInline/C.smali b/jadx-core/src/test/smali/inline/TestMethodInline/C.smali new file mode 100644 index 000000000..3d6a6995c --- /dev/null +++ b/jadx-core/src/test/smali/inline/TestMethodInline/C.smali @@ -0,0 +1,8 @@ +.class Linline/other/C; # package-private +.super Ljava/lang/Object; + +.method public static test()V + .locals 1 + + return-void +.end method diff --git a/jadx-core/src/test/smali/others/TestFixClassAccessModifiers/Cls.smali b/jadx-core/src/test/smali/others/TestFixClassAccessModifiers/Cls.smali new file mode 100644 index 000000000..5b6da374e --- /dev/null +++ b/jadx-core/src/test/smali/others/TestFixClassAccessModifiers/Cls.smali @@ -0,0 +1,8 @@ +.class public Lothers/Cls; +.super Ljava/lang/Object; + +.annotation system Ldalvik/annotation/MemberClasses; + value = { + Lothers/Cls$InnerCls; + } +.end annotation diff --git a/jadx-core/src/test/smali/others/TestFixClassAccessModifiers/InnerCls.smali b/jadx-core/src/test/smali/others/TestFixClassAccessModifiers/InnerCls.smali new file mode 100644 index 000000000..437cd50b0 --- /dev/null +++ b/jadx-core/src/test/smali/others/TestFixClassAccessModifiers/InnerCls.smali @@ -0,0 +1,11 @@ +.class private Lothers/Cls$InnerCls; +.super Ljava/lang/Object; + +.annotation system Ldalvik/annotation/EnclosingClass; + value = Lothers/Cls; +.end annotation + +.annotation system Ldalvik/annotation/InnerClass; + accessFlags = 0xA + name = "InnerCls" +.end annotation diff --git a/jadx-core/src/test/smali/others/TestFixClassAccessModifiers/TestCls.smali b/jadx-core/src/test/smali/others/TestFixClassAccessModifiers/TestCls.smali new file mode 100644 index 000000000..abf3cc950 --- /dev/null +++ b/jadx-core/src/test/smali/others/TestFixClassAccessModifiers/TestCls.smali @@ -0,0 +1,4 @@ +.class public Lothers/TestCls; +.super Ljava/lang/Object; + +.field public field:Lothers/Cls$InnerCls;