Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error prone ExceptionSpecificity check to avoid catching too broadly #1074

Merged
merged 22 commits into from
Dec 4, 2019

Conversation

carterkozak
Copy link
Contributor

Before this PR

try {
   doesNotThrowChecked();
- } catch (Exception e) {
+ } catch (RuntimeException e) {
    log.error("failure", e);
}
try {
   doesNotThrowChecked();
- } catch (Throwable t) {
+ } catch (RuntimeException | Error t) {
    log.error("failure", e);
}
try {
   doesNotThrowChecked();
} catch (RuntimeException e) {
    throw e
- } catch (Exception e) { // unreachable
-    log.error("failure", e);
}

After this PR

==COMMIT_MSG==
Error prone ExceptionSpecificity check to avoid catching too broadly

Prefer more specific catch types than Exception and Throwable.
When methods are updated to throw new checked exceptions they expect
callers to handle failure types explicitly. Catching broad types defeats
the type system. By catching the most specific types possible we
leverage existing compiler functionality to detect unreachable code.
==COMMIT_MSG==

Possible downsides?

Possible to create a large diff in projects which don't follow best practices.
As always there's a chance the rewrite is not completely accurate.

@changelog-app
Copy link

changelog-app bot commented Nov 27, 2019

Generate changelog in changelog/@unreleased

Type

  • Feature
  • Improvement
  • Fix
  • Break
  • Deprecation
  • Manual task
  • Migration

Description

Error prone ExceptionSpecificity check to avoid catching too broadly

Prefer more specific catch types than Exception and Throwable.
When methods are updated to throw new checked exceptions they expect
callers to handle failure types explicitly. Catching broad types defeats
the type system. By catching the most specific types possible we
leverage existing compiler functionality to detect unreachable code.

Check the box to generate changelog(s)

  • Generate changelog entry

@robert3005
Copy link
Contributor

Small flag for people to be aware - scala treats all exceptions as unchecked so this autofix can break things that integrate with some scala code where you catch an exception (however, java already yells at you for catching checked exception where one is not defined)

@carterkozak carterkozak force-pushed the ckozak/ExceptionSpecificity branch from ee1c274 to 36219e5 Compare December 2, 2019 17:06
Prefer more specific catch types than Exception and Throwable.
When methods are updated to throw new checked exceptions they expect
callers to handle failure types explicitly. Catching broad types defeats
the type system. By catching the most specific types possible we
leverage existing compiler functionality to detect unreachable code.
@carterkozak carterkozak force-pushed the ckozak/ExceptionSpecificity branch from 36219e5 to 0ceeece Compare December 3, 2019 12:46

@Override
@SuppressWarnings("CyclomaticComplexity")
public Description matchMethod(MethodTree tree, VisitorState state) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if it would be clearer if this lived in a separate class? That way we'd have two error-prone checks that just do one thing, e.g. CatchSpecificity and ThrowsSpecificity?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could go either way on this, there are two manifestations of the same conceptual issue. I suppose it boils down to whether we want bug checkers mapped to concepts, or implementation/output?

I was thinking about this yesterday, specifically for the assertj cleanup checkers -- it might be easiest for consumers to understand if all assertj refactor checks were part of a single 'AssertjUsage' check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm mainly thinking about this from a maintenance perspective, as shorter classes that just do one thing are easier to understand as I can be sure each private method is somehow relevant to the one purpose of the class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point

encounteredTypes.add(catchType);
}
return Description.NO_MATCH;
}
Copy link
Contributor

@iamdanfox iamdanfox Dec 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i'm still struggling with this method - there's quite a lot of intricacy in the de-duping and selecting only exceptions&throwables that is all woven together to get this all done in one pass.

what about if we did a loop through all the possible tree.getCatches() to accumulate the encounteredTypes, then could call out to a dedicated function to process each single catch?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also the possibility of fully returning from deep inside a loop & if condition feels a bit weird - are we saying if we hit one catch(Exception) then a later catch(Throwable) might not get processed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we saying if we hit one catch(Exception) then a later catch(Throwable) might not get processed?

Yep, we have a test for that case which takes two rounds to resolve. We can do it in one loop, but I worried about the additional complexity. I can take a look at implementing it and we can decide from there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I'm fine with it just doing one thing at a time - minimum complexity = happy dan. Just wanted to check my understanding.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out it's straightforward to fix 'em all at once. I think that blocks us from splitting into multiple loops though because the changes mutate state, and we would have to keep expected handled types in sync with the code that builds suggested fixes, leading us back to the original model.

+ "new checked exceptions they expect callers to handle failure types explicitly. Catching broad "
+ "types defeats the type system. By catching the most specific types possible we leverage existing "
+ "compiler functionality to detect unreachable code.")
public final class ThrowSpecificity extends BugChecker implements BugChecker.MethodTreeMatcher {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow is state.errorProneOptions().isTestOnlyTarget() new?

I think there's a lot of overlap, ThrowSpecificExceptions.java recommends changing throw <throwable> blocks to use more specific types, where this check produces no runtime changes, only provides stronger type safety by narrowing declared throws types on methods.

This was referenced Dec 4, 2019
@robert3005 robert3005 deleted the ckozak/ExceptionSpecificity branch November 18, 2020 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants