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 index 051b73297..c8dc4238e 100644 --- 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 @@ -10,11 +10,14 @@ import jadx.api.usage.IUsageInfoData; import jadx.api.usage.IUsageInfoVisitor; import jadx.core.clsp.ClspClass; import jadx.core.clsp.ClspClassSource; +import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; +import jadx.core.dex.nodes.ICodeNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; +import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.Utils.notEmpty; @@ -71,6 +74,28 @@ public class UsageInfo implements IUsageInfoData { processType(useType, depCls -> clsUse(mth, depCls)); } + public void clsUse(ICodeNode node, ArgType useType) { + Consumer consumer; + switch (node.getAnnType()) { + case CLASS: + ClassNode cls = (ClassNode) node; + consumer = depCls -> clsUse(cls, depCls); + break; + case METHOD: + MethodNode mth = (MethodNode) node; + consumer = depCls -> clsUse(mth, depCls); + break; + case FIELD: + FieldNode fld = (FieldNode) node; + ClassNode fldCls = fld.getParentClass(); + consumer = depCls -> clsUse(fldCls, depCls); + break; + default: + throw new JadxRuntimeException("Unexpected use type: " + node.getAnnType()); + } + processType(useType, consumer); + } + public void clsUse(MethodNode mth, ClassNode useCls) { ClassNode parentClass = mth.getParentClass(); clsUse(parentClass, useCls); @@ -106,6 +131,23 @@ public class UsageInfo implements IUsageInfoData { clsUse(mth, useFld.getType()); } + public void fieldUse(ICodeNode node, FieldInfo useFld) { + FieldNode fld = root.resolveField(useFld); + if (fld == null) { + return; + } + switch (node.getAnnType()) { + case CLASS: + // TODO: support "field in class" usage? + // now use field parent class for "class in class" usage + clsUse((ClassNode) node, fld.getParentClass()); + break; + case METHOD: + fieldUse((MethodNode) node, fld); + break; + } + } + private void processType(ArgType type, Consumer consumer) { if (type == null) { return; 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 index 09be82d44..65d46ea16 100644 --- 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 @@ -8,8 +8,14 @@ import org.slf4j.LoggerFactory; import jadx.api.plugins.input.data.ICallSite; import jadx.api.plugins.input.data.ICodeReader; +import jadx.api.plugins.input.data.IFieldRef; import jadx.api.plugins.input.data.IMethodHandle; import jadx.api.plugins.input.data.IMethodRef; +import jadx.api.plugins.input.data.annotations.EncodedValue; +import jadx.api.plugins.input.data.annotations.IAnnotation; +import jadx.api.plugins.input.data.attributes.JadxAttrType; +import jadx.api.plugins.input.data.attributes.types.AnnotationMethodParamsAttr; +import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; import jadx.api.plugins.input.insns.InsnData; import jadx.api.plugins.input.insns.Opcode; import jadx.api.plugins.input.insns.custom.ICustomPayload; @@ -20,19 +26,23 @@ 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.ICodeNode; 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.OverrideMethodVisitor; +import jadx.core.dex.visitors.SignatureProcessor; import jadx.core.dex.visitors.rename.RenameVisitor; import jadx.core.utils.ListUtils; +import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.input.InsnDataUtils; @JadxVisitor( name = "UsageInfoVisitor", desc = "Scan class and methods to collect usage info and class dependencies", runAfter = { + SignatureProcessor.class, // use types with generics OverrideMethodVisitor.class, // add method override as use RenameVisitor.class // sort by alias name } @@ -80,19 +90,23 @@ public class UsageInfoVisitor extends AbstractVisitor { } for (FieldNode fieldNode : cls.getFields()) { usageInfo.clsUse(cls, fieldNode.getType()); + processAnnotations(fieldNode, usageInfo); + // TODO: process types from field 'constant value' } - // TODO: process annotations and generics + // TODO: process generics + processAnnotations(cls, usageInfo); for (MethodNode methodNode : cls.getMethods()) { 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); + processMethodAnnotations(mth, usageInfo); + usageInfo.clsUse(mth, mth.getReturnType()); + for (ArgType argType : mth.getArgTypes()) { + usageInfo.clsUse(mth, argType); } + // TODO: process exception classes from 'throws' try { processInstructions(mth, usageInfo); } catch (Exception e) { @@ -169,6 +183,65 @@ public class UsageInfoVisitor extends AbstractVisitor { } } + private static void processAnnotations(ICodeNode node, UsageInfo usageInfo) { + AnnotationsAttr annAttr = node.get(JadxAttrType.ANNOTATION_LIST); + processAnnotationAttr(node, annAttr, usageInfo); + } + + private static void processMethodAnnotations(MethodNode mth, UsageInfo usageInfo) { + processAnnotations(mth, usageInfo); + AnnotationMethodParamsAttr paramsAttr = mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS); + if (paramsAttr != null) { + for (AnnotationsAttr annAttr : paramsAttr.getParamList()) { + processAnnotationAttr(mth, annAttr, usageInfo); + } + } + } + + private static void processAnnotationAttr(ICodeNode node, AnnotationsAttr annAttr, UsageInfo usageInfo) { + if (annAttr == null || annAttr.isEmpty()) { + return; + } + for (IAnnotation ann : annAttr.getList()) { + processAnnotation(node, ann, usageInfo); + } + } + + private static void processAnnotation(ICodeNode node, IAnnotation ann, UsageInfo usageInfo) { + usageInfo.clsUse(node, ArgType.parse(ann.getAnnotationClass())); + for (EncodedValue value : ann.getValues().values()) { + processAnnotationValue(node, value, usageInfo); + } + } + + @SuppressWarnings("unchecked") + private static void processAnnotationValue(ICodeNode node, EncodedValue value, UsageInfo usageInfo) { + Object obj = value.getValue(); + switch (value.getType()) { + case ENCODED_TYPE: + usageInfo.clsUse(node, ArgType.parse((String) obj)); + break; + case ENCODED_ENUM: + case ENCODED_FIELD: + if (obj instanceof IFieldRef) { + usageInfo.fieldUse(node, FieldInfo.fromRef(node.root(), (IFieldRef) obj)); + } else if (obj instanceof FieldInfo) { + usageInfo.fieldUse(node, (FieldInfo) obj); + } else { + throw new JadxRuntimeException("Unexpected field type class: " + value.getClass()); + } + break; + case ENCODED_ARRAY: + for (EncodedValue encodedValue : (List) obj) { + processAnnotationValue(node, encodedValue, usageInfo); + } + break; + case ENCODED_ANNOTATION: + processAnnotation(node, (IAnnotation) obj, usageInfo); + break; + } + } + public static void replaceMethodUsage(MethodNode mergeIntoMth, MethodNode sourceMth) { List mergedUsage = ListUtils.distinctMergeSortedLists(mergeIntoMth.getUseIn(), sourceMth.getUseIn()); mergedUsage.remove(sourceMth); diff --git a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java index e35a24ec5..e877f3c20 100644 --- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -53,6 +53,7 @@ import jadx.api.metadata.annotations.InsnCodeOffset; import jadx.api.metadata.annotations.VarNode; import jadx.core.dex.attributes.AFlag; 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.utils.Utils; @@ -447,13 +448,23 @@ public abstract class IntegrationTest extends TestUtils { } } - protected MethodNode getMethod(ClassNode cls, String method) { + protected MethodNode getMethod(ClassNode cls, String methodName) { for (MethodNode mth : cls.getMethods()) { - if (mth.getName().equals(method)) { + if (mth.getName().equals(methodName)) { return mth; } } - fail("Method not found " + method + " in class " + cls); + fail("Method not found " + methodName + " in class " + cls); + return null; + } + + protected FieldNode getField(ClassNode cls, String fieldName) { + for (FieldNode fld : cls.getFields()) { + if (fld.getName().equals(fieldName)) { + return fld; + } + } + fail("Field not found " + fieldName + " in class " + cls); return null; } diff --git a/jadx-core/src/test/java/jadx/tests/integration/annotations/TestAnnotationsUsage.java b/jadx-core/src/test/java/jadx/tests/integration/annotations/TestAnnotationsUsage.java new file mode 100644 index 000000000..9dd93cd4e --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/annotations/TestAnnotationsUsage.java @@ -0,0 +1,62 @@ +package jadx.tests.integration.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestAnnotationsUsage extends IntegrationTest { + + public static class TestCls { + + @Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) + @Retention(RetentionPolicy.RUNTIME) + public @interface A { + Class c(); + } + + @A(c = TestCls.class) + public static class B { + } + + public static class C { + @A(c = B.class) + public String field; + } + + @A(c = B.class) + void test() { + } + + void test2(@A(c = B.class) Integer value) { + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + ClassNode annCls = searchCls(cls.getInnerClasses(), "A"); + ClassNode bCls = searchCls(cls.getInnerClasses(), "B"); + ClassNode cCls = searchCls(cls.getInnerClasses(), "C"); + MethodNode testMth = getMethod(cls, "test"); + MethodNode testMth2 = getMethod(cls, "test2"); + + assertThat(annCls.getUseIn()).contains(cls, bCls, cCls); + assertThat(annCls.getUseInMth()).contains(testMth, testMth2); + + assertThat(bCls.getUseIn()).contains(cCls); + assertThat(bCls.getUseInMth()).contains(testMth, testMth2); + + assertThat(cls) + .code() + .countString(3, "@A(c = B.class)"); + } +}