Skip to content

fix: generate missing throws declarations, validate exceptions (#2441)#2475

Merged
skylot merged 2 commits intoskylot:masterfrom
nitram84:issue_2441_missing_throws_declarations
May 9, 2025
Merged

fix: generate missing throws declarations, validate exceptions (#2441)#2475
skylot merged 2 commits intoskylot:masterfrom
nitram84:issue_2441_missing_throws_declarations

Conversation

@nitram84
Copy link
Copy Markdown
Contributor

@nitram84 nitram84 commented May 6, 2025

This PR calculates missing throws declarations and introduces basic validation to fix #2441.

The algorithm is a bit more complex than a simple call graph traversal: for each method thrown exceptions are calculated and throws-annotations are used as a hint. If throws-annotations are compatible with calculated exceptions, the algorithm takes the exception of the throws-annotations. So in normal cases there won't be any difference.

There are two drawbacks:

1.) Option 'no-finally' is active. In this case jadx generates code like

try {
 //...
} catch(Throwable t) {
  throws t;
}

The algorithm detects a thrown Throwable here, but in this special case where no throws declaration is required. I don't see a way to distinguish former finally clauses from other similar try-catch structures where throws declarations are required. I could only disable the pass for 'no-finally'. What is better unneeded throws declarations or possible decompilation issues?

2.) Unknown class hierarchy
With an unknown class hierarchy (e.g. decompilation of JARs, smali, ...) I can't perform a validation so assume that thrown classes are a Exception but not a RuntimeException. In this case unneeded throws declarations might be added too.

Some findings:

  • This algorithm operates on full qualified names and signatures, in other words just strings, the common "base" of ClassNode/ClspClass and MethodNode/ClspMethod. I'm aware that this types are used to express totally different things, but from a logical perspective they share common concepts:

    • names (full qualified or signatures)
    • classes can have super classes
    • classes can have methods and nested classes
    • classes are objects, should be a logical ArgType.ObjectType

    I could imagine common interfaces for both class types and method types. Some passes could be simplified if they can operate on common interfaces. For now we use even different names to access parents.

  • In my unit tests I don't use any modifiers for test classes and methods to prevent sonarqube warnings.

@skylot
Copy link
Copy Markdown
Owner

skylot commented May 6, 2025

@nitram84 thanks! Looks good, although I will need some time to review this PR, couple days I hope 🙂

What is better unneeded throws declarations or possible decompilation issues?

Hm, it is still unclear for me why rethrow Throwable from catch don't need throws, but throw new Throwable() require throws statement. Maybe it will be not hard to check if throw instruction arg is assigned in catch, I will check this.
Anyway, sure "unneeded throws declarations" is better than decompilation issues.

the common "base" of ClassNode/ClspClass and MethodNode/ClspMethod

Well there is common IMethodDetails which is implemented by MethodNode and ClspMethod.
But for classes it is harder to made similar, because ClspClass is 'unresolved' and have only ArgType, even parents is just array and not a tree like in ClassNode.
But sure, we can add common interface for just some common properties.
Also, some common methods already collected in TypeUtils and MethodUtils. Perhaps TypeUtils.visitSuperTypes will be useful for traversing parents in common way.

classes are objects, should be a logical ArgType.ObjectType

Not sure what you mean, ArgType.ObjectType already here:

private static class ObjectType extends KnownType {

and can be created using ArgType.object() (it will also convert to . form, removing L and ;)

@nitram84
Copy link
Copy Markdown
Contributor Author

nitram84 commented May 7, 2025

Thank you! This is not an easy PR. Take any time you need for a review.

I think found the answer for the special case: https://stackoverflow.com/questions/24981736/why-is-throwing-a-checked-exception-type-allowed-in-this-case

void test1() { // no throws required
	try {
	} catch (Throwable t) {
		throw t;
	}
}
	
void test2() throws Throwable {
	try {
	} catch (Throwable t) {
		Throwable t2 = t;
		throw t2;
	}
} 

This seems to be a Java 7 feature: if a caught exception is directly rethrown, it is not necessary to add a throws declaration for a more general exception type.

At the moment I don't throw exceptions of a try block, if they are handled in catch clause. To fix this I have to remember all thrown exceptions of a try block. For each catch clause I have to check whether an exception is directly rethrown. If yes I have to declare all remembered exceptions of the corresponding try block as thrown. Otherwise I have to declare all thrown exceptions of the catch clause.

Nested try catch blocks might be interesting. There might be more fun with early Java 6 apps that don't support that language feature if someone wants to recompile them with Java 6.

With my "logical ArgType.ObjectType" I just mean that both types ClassNode and ClspClass have the implicit information of representing a type that inherits from Object. So e.g. the OverrideMethodVisitor could be improved by adding override annotations for overridden methods of Object and even if the class hierarchy is incomplete.

@skylot
Copy link
Copy Markdown
Owner

skylot commented May 7, 2025

I reviewed code and have two news:

  1. Good one: instead of going through parent classes to check base exception class, we can use ClspGraph.isImplements method, it was created just for that and heavily used in type inference. And as soon as ClspGraph merged with app classes, this method can be used for all classes.
    So I refactor your PR to use that method and it works fine. Also, I added a check for rethrow Throwable and it also works fine if arg used directly from catch.
  2. Bad one: as I mentioned in [core] Missing and invalid throws declarations #2441 (comment) only first level of dependant classes are loaded, so if your instructions traversal method go deeper, method might not have basic blocks loaded and current implementation will just ignore that.
    I will try to create a test case for that, maybe it is not so bad like I think (decompilation batches pack all dependant classes close to each other for processing), but anyway it will be nice to fix this somehow.

@skylot
Copy link
Copy Markdown
Owner

skylot commented May 9, 2025

Ok, I think it is fine to merge this PR. Processing issue is still there, but to resolving it, will require to apply current instruction checks using low-level code scanner, and that might be hard.

@skylot skylot merged commit 8bbdbfc into skylot:master May 9, 2025
4 checks passed
@nitram84 nitram84 deleted the issue_2441_missing_throws_declarations branch September 10, 2025 10:18
nitram84 added a commit to nitram84/jadx that referenced this pull request Sep 13, 2025
skylot pushed a commit that referenced this pull request Sep 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[core] Missing and invalid throws declarations

2 participants