Repository: skylot/jadx
Pull request #2460: fix: erroneous SignatureProcessor resolution of standard non-generic …
Author: login: nerodesu017
Created: 2025-04-13T16:08:11Z | Merged: 2025-04-14T20:36:55Z

…types with Annotation Signatures

### Description
This PR aims to fix the issue where SignatureProcessor would erroneously resolve the types of fields whose main type is not a standard generic but which also have a Signature Annotation.

Related to #2459 



Diff:
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 aac408ee8a1..8e03f80d49b 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
@@ -9,6 +9,7 @@
 
 import org.jetbrains.annotations.Nullable;
 
+import jadx.core.deobf.NameMapper;
 import jadx.core.dex.info.MethodInfo;
 import jadx.core.dex.instructions.args.ArgType;
 import jadx.core.dex.nodes.ClassNode;
@@ -167,7 +168,7 @@ private void parseFieldSignature(FieldNode field) {
 			}
 			ArgType type = root.getTypeUtils().expandTypeVariables(cls, signatureType);
 			if (!validateParsedType(type, field.getType())) {
-				cls.addWarnComment("Incorrect field signature: " + sp.getSignature());
+				field.addInfoComment("Incorrect field signature: " + sp.getSignature());
 				return;
 			}
 			field.updateType(type);
@@ -256,9 +257,29 @@ private List<ArgType> checkArgTypes(MethodNode mth, SignatureParser sp, List<Arg
 
 	private boolean validateParsedType(ArgType parsedType, ArgType currentType) {
 		TypeCompareEnum result = root.getTypeCompare().compareTypes(parsedType, currentType);
+		if (result == TypeCompareEnum.UNKNOWN
+				&& parsedType.isObject()
+				&& !validateFullClsName(parsedType.getObject())) {
+			// ignore external invalid class names: may be a reserved words or garbage
+			return false;
+		}
 		return result != TypeCompareEnum.CONFLICT;
 	}
 
+	private boolean validateFullClsName(String fullClsName) {
+		if (!NameMapper.isValidFullIdentifier(fullClsName)) {
+			return false;
+		}
+		if (fullClsName.indexOf('.') > 0) {
+			for (String namePart : fullClsName.split("\\.")) {
+				if (!NameMapper.isValidIdentifier(namePart)) {
+					return false;
+				}
+			}
+		}
+		return true;
+	}
+
 	private boolean validateInnerType(List<ArgType> types) {
 		for (ArgType type : types) {
 			if (!validateInnerType(type)) {
diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestIncorrectFieldSignature.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestIncorrectFieldSignature.java
new file mode 100644
index 00000000000..08638ecc6c4
--- /dev/null
+++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestIncorrectFieldSignature.java
@@ -0,0 +1,19 @@
+package jadx.tests.integration.others;
+
+import org.junit.jupiter.api.Test;
+
+import jadx.tests.api.SmaliTest;
+
+import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
+
+public class TestIncorrectFieldSignature extends SmaliTest {
+
+	@Test
+	public void test() {
+		assertThat(getClassNodeFromSmali())
+				.code()
+				.containsOne("public static boolean A;")
+				.containsOne("public static Boolean B;")
+				.countString(2, "/* JADX INFO: Incorrect field signature:");
+	}
+}
diff --git a/jadx-core/src/test/smali/others/TestIncorrectFieldSignature.smali b/jadx-core/src/test/smali/others/TestIncorrectFieldSignature.smali
new file mode 100644
index 00000000000..d406c2fbfea
--- /dev/null
+++ b/jadx-core/src/test/smali/others/TestIncorrectFieldSignature.smali
@@ -0,0 +1,18 @@
+.class public Lothers/TestIncorrectFieldSignature;
+.super Ljava/lang/Object;
+
+.field public static A:Z
+	.annotation system Ldalvik/annotation/Signature;
+        value = {
+            "Lint;"
+        }
+    .end annotation
+.end field
+
+.field public static B:Ljava/lang/Boolean;
+	.annotation system Ldalvik/annotation/Signature;
+        value = {
+            "Lpkg/int;"
+        }
+    .end annotation
+.end field


Reviews:
user: login: jpstotz
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T16:13:29Z
user: login: nerodesu017
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T16:19:18Z
user: login: nerodesu017
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T16:25:28Z
user: login: nerodesu017
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T16:34:23Z
user: login: nerodesu017
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T17:05:52Z
user: login: skylot
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-14T20:25:56Z

Review comments:
diff_hunk: @@ -176,6 +180,39 @@ private void parseFieldSignature(FieldNode field) {
 		}
 	}
 
+	private boolean isNotGenericType(ArgType ty) {
+		if (ty.isPrimitive()) {
+			return false;
+		} else {
+			ty.toString();
+		}
+		final String[] stdNonGenericTypes = {
+				"bool",
+				"byte",
+				"short",
+				"char",
+				"float",
+				"int",
+				"long",
+				"double",
+
+				"java.lang.String",
+				"java.lang.Byte",
+				"java.lang.Long",
+				"java.lang.Short",
+				"java.lang.Number",
+				"java.lang.CharSequence",
+				"java.lang.Double",
+				"java.lang.Float",
+		};
+		for (String genericType : stdNonGenericTypes) {
+			if (genericType.equals(ty)) {
path: jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java
user: login: jpstotz
body: Are you sure this `equals` check works?  `genericType` is a `java.lang.String` and `ty` is an `ArgType`, thus this if expression can never get `true`.

Also why does the array contains primitive types?  At the beginning of the method there is also the `isPrimitive()` check thus if it is a primitive this code should never be executed.
created_at: 2025-04-13T16:13:29Z
author_association: COLLABORATOR
diff_hunk: @@ -176,6 +180,39 @@ private void parseFieldSignature(FieldNode field) {
 		}
 	}
 
+	private boolean isNotGenericType(ArgType ty) {
+		if (ty.isPrimitive()) {
+			return false;
+		} else {
+			ty.toString();
+		}
+		final String[] stdNonGenericTypes = {
+				"bool",
+				"byte",
+				"short",
+				"char",
+				"float",
+				"int",
+				"long",
+				"double",
+
+				"java.lang.String",
+				"java.lang.Byte",
+				"java.lang.Long",
+				"java.lang.Short",
+				"java.lang.Number",
+				"java.lang.CharSequence",
+				"java.lang.Double",
+				"java.lang.Float",
+		};
+		for (String genericType : stdNonGenericTypes) {
+			if (genericType.equals(ty)) {
path: jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java
user: login: nerodesu017
body: `.equals` does work, it casts it to String automatically, but I will explicitly call `.toString` just so it is clear.

Well, the type can still be a primitive, but in case of primitives, even with annotations, they don't get erroneously resolved, so I just have a check at the beginning and I return `false`
created_at: 2025-04-13T16:19:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,6 +180,39 @@ private void parseFieldSignature(FieldNode field) {
 		}
 	}
 
+	private boolean isNotGenericType(ArgType ty) {
+		if (ty.isPrimitive()) {
+			return false;
+		} else {
+			ty.toString();
+		}
+		final String[] stdNonGenericTypes = {
+				"bool",
+				"byte",
+				"short",
+				"char",
+				"float",
+				"int",
+				"long",
+				"double",
+
+				"java.lang.String",
+				"java.lang.Byte",
+				"java.lang.Long",
+				"java.lang.Short",
+				"java.lang.Number",
+				"java.lang.CharSequence",
+				"java.lang.Double",
+				"java.lang.Float",
+		};
+		for (String genericType : stdNonGenericTypes) {
+			if (genericType.equals(ty)) {
path: jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java
user: login: nerodesu017
body: I have also modified the test so that our smali file also contains a field with a primitive type to showcase primitives are not an issue and should be skipped.
created_at: 2025-04-13T16:25:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,6 +180,39 @@ private void parseFieldSignature(FieldNode field) {
 		}
 	}
 
+	private boolean isNotGenericType(ArgType ty) {
+		if (ty.isPrimitive()) {
+			return false;
+		} else {
+			ty.toString();
+		}
+		final String[] stdNonGenericTypes = {
+				"bool",
+				"byte",
+				"short",
+				"char",
+				"float",
+				"int",
+				"long",
+				"double",
+
+				"java.lang.String",
+				"java.lang.Byte",
+				"java.lang.Long",
+				"java.lang.Short",
+				"java.lang.Number",
+				"java.lang.CharSequence",
+				"java.lang.Double",
+				"java.lang.Float",
+		};
+		for (String genericType : stdNonGenericTypes) {
+			if (genericType.equals(ty)) {
path: jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java
user: login: nerodesu017
body: I have re-read your comment and want to clarify why we have the `isPrimitive()` check at the start and we also check for "primitive" types.

If in our smali file the fields have primitive types but that are expressed using the fully qualified class name (using the `L` prefix), they are not resolved to be Primitive ArgType's, so we still have to check for them.

Example:

```smali
.field public static L:Lint;
```
created_at: 2025-04-13T16:34:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,6 +180,39 @@ private void parseFieldSignature(FieldNode field) {
 		}
 	}
 
+	private boolean isNotGenericType(ArgType ty) {
+		if (ty.isPrimitive()) {
+			return false;
+		} else {
+			ty.toString();
+		}
+		final String[] stdNonGenericTypes = {
+				"bool",
+				"byte",
+				"short",
+				"char",
+				"float",
+				"int",
+				"long",
+				"double",
+
+				"java.lang.String",
+				"java.lang.Byte",
+				"java.lang.Long",
+				"java.lang.Short",
+				"java.lang.Number",
+				"java.lang.CharSequence",
+				"java.lang.Double",
+				"java.lang.Float",
+		};
+		for (String genericType : stdNonGenericTypes) {
+			if (genericType.equals(ty)) {
path: jadx-core/src/main/java/jadx/core/dex/visitors/SignatureProcessor.java
user: login: nerodesu017
body: You were more than right. I am sorry, but I suck at java. I have managed to solve the issues, re-run the tests locally, and now it should be fine.
created_at: 2025-04-13T17:05:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: skylot
created_at: 2025-04-13T18:56:38Z
body: @nerodesu017 please check my view on the original issue in my comment: https://github.com/skylot/jadx/issues/2459#issuecomment-2800076284

As a result, I think jadx now correctly process types, but confusing class names clash with primitive type names should be fixed in `RenameVisitor` in `checkNames` method:
https://github.com/skylot/jadx/blob/c602b3d967d89cf222bd73bafa58c3e2116fc190/jadx-core/src/main/java/jadx/core/dex/visitors/rename/RenameVisitor.java#L48
author_association: OWNER
user: login: nerodesu017
created_at: 2025-04-13T20:06:54Z
body: > @nerodesu017 please check my view on the original issue in my comment: [#2459 (comment)](https://github.com/skylot/jadx/issues/2459#issuecomment-2800076284)
> 
> As a result, I think jadx now correctly process types, but confusing class names clash with primitive type names should be fixed in `RenameVisitor` in `checkNames` method:
> 
> https://github.com/skylot/jadx/blob/c602b3d967d89cf222bd73bafa58c3e2116fc190/jadx-core/src/main/java/jadx/core/dex/visitors/rename/RenameVisitor.java#L48

I am sorry, but I am not at all familiar with the codebase, as I've said above, nor am I that knowledgeable about Java. I wanted to thank both of you for helping me out, and I have read your comment which made it a bit more clear for me. Using `L` makes it so it is an actual class name following it.
Now, I wanted to ask: you've suggested I modify the `checkNames` method from `RenameVisitor`, but it seems that that runs way after the `SignatureProcessor`. I was thinking: Maybe I should modify the parsing of the annotation ArgType and throw an error in case of an invalid identifier name given as the class name?
author_association: CONTRIBUTOR
user: login: skylot
created_at: 2025-04-13T20:33:28Z
body: > Maybe I should modify the parsing of the annotation ArgType and throw an error in case of an invalid identifier name given as the class name?

Any identifier is valid in bytecode, because VM don't check names at runtime. And jadx only rename such identifiers and only if options are not forbid this.
Also, right now, jadx renames only known classes (found in app) and that's why types in your examples types not renamed. Main issue here is that renaming unknown or outside classes may break references, and this become unclear what we need to do for such cases.
I checked and jadx **will** rename such classes if they are known, so looks like there is nothing to do.
As a result, I have no idea how to correctly handle such cases, maybe we can just add a warning.
I will try to find a solution, any suggestions are welcome.

Anyway, further discussion is better to continue in original issue and I will close current PR as it not needed anymore.
author_association: OWNER