Skip to content

Commit 5d3e729

Browse files
authored
Detect catching java.lang.Error (#15)
* Add an error catch detector * Cleanup messages * Improve error messages * Fix style
1 parent 93c55e7 commit 5d3e729

File tree

4 files changed

+231
-0
lines changed

4 files changed

+231
-0
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- This detector works best when applied to an app project with `lint.checkDependencies = true` in the app module AGP DSL.
1313
- `ForEachFunctionDetector` reports `forEach` and `forEachIndexed` use and encourages a language for loop replacement
1414
- `SkippedClassLocalOverrideDetector` warns when an explicit super method is called outside of the corresponding override.
15+
- `ErrorCatchDetector` reports an error when a catch block might catch a `java.lang.Error` type.
1516

1617
### Changed
1718
- Updated build tooling
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.faithlife.lint
2+
3+
import com.android.tools.lint.client.api.UElementHandler
4+
import com.android.tools.lint.detector.api.Category
5+
import com.android.tools.lint.detector.api.Detector
6+
import com.android.tools.lint.detector.api.Implementation
7+
import com.android.tools.lint.detector.api.Incident
8+
import com.android.tools.lint.detector.api.Issue
9+
import com.android.tools.lint.detector.api.JavaContext
10+
import com.android.tools.lint.detector.api.Scope
11+
import com.android.tools.lint.detector.api.Severity
12+
import com.android.tools.lint.detector.api.SourceCodeScanner
13+
import org.jetbrains.uast.UCatchClause
14+
15+
class ErrorCatchDetector : Detector(), SourceCodeScanner {
16+
override fun getApplicableUastTypes() = listOf(UCatchClause::class.java)
17+
18+
override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
19+
override fun visitCatchClause(node: UCatchClause) {
20+
for (typeRef in node.typeReferences) {
21+
if (context.evaluator.typeMatches(typeRef.type, "java.lang.Throwable")) {
22+
Incident(context)
23+
.issue(ISSUE_CATCH_TOO_GENERIC)
24+
.message(TOO_GENERIC_MESSAGE)
25+
.scope(node)
26+
.location(context.getLocation(typeRef))
27+
.report()
28+
}
29+
30+
val clazz = context.evaluator.getTypeClass(typeRef.type)
31+
if (context.evaluator.extendsClass(clazz, "java.lang.Error")) {
32+
Incident(context)
33+
.issue(ISSUE_ERROR_CAUGHT)
34+
.message(ERROR_CAUGHT_MESSAGE)
35+
.scope(node)
36+
.location(context.getLocation(typeRef))
37+
.report()
38+
}
39+
}
40+
}
41+
}
42+
43+
companion object {
44+
private const val TOO_GENERIC_MESSAGE = "Catching Throwable will include Errors. Be more specific."
45+
private const val ERROR_CAUGHT_MESSAGE = "Errors should not be caught."
46+
private const val DESC = "Catch blocks should not handle java.lang.Error"
47+
private fun createExplanation(message: String) = """
48+
$message
49+
50+
Catching errors can further complicate stacktraces and error investigation
51+
in general. `java.lang.Error` and subtypes should not be caught. They indicate
52+
a terminal program state that is best served by crashing quickly in order to provide
53+
the best view of application state that lead to the `Error` being thrown.
54+
"""
55+
56+
val ISSUE_CATCH_TOO_GENERIC = Issue.create(
57+
"ThrowableCatchDetector",
58+
DESC,
59+
createExplanation(TOO_GENERIC_MESSAGE),
60+
moreInfo = "https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Error.html",
61+
category = Category.CORRECTNESS,
62+
severity = Severity.ERROR,
63+
implementation = Implementation(
64+
ErrorCatchDetector::class.java,
65+
Scope.JAVA_FILE_SCOPE,
66+
),
67+
)
68+
69+
val ISSUE_ERROR_CAUGHT = Issue.create(
70+
"ErrorCatchDetector",
71+
DESC,
72+
createExplanation(ERROR_CAUGHT_MESSAGE),
73+
moreInfo = "https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Error.html",
74+
category = Category.CORRECTNESS,
75+
severity = Severity.ERROR,
76+
implementation = Implementation(
77+
ErrorCatchDetector::class.java,
78+
Scope.JAVA_FILE_SCOPE,
79+
),
80+
)
81+
}
82+
}

checks/src/main/kotlin/com/faithlife/lint/IssueRegistry.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ class IssueRegistry : ApiIssueRegistry() {
1313
)
1414

1515
override val issues = listOf(
16+
ErrorCatchDetector.ISSUE_CATCH_TOO_GENERIC,
17+
ErrorCatchDetector.ISSUE_ERROR_CAUGHT,
1618
FiniteWhenCasesDetector.ISSUE,
1719
ForEachFunctionDetector.ISSUE,
1820
IndirectSuperCallDetector.ISSUE,
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package com.faithlife.lint
2+
3+
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
4+
import org.junit.Test
5+
6+
class ErrorCatchDetectorTest : LintDetectorTest() {
7+
override fun getDetector() = ErrorCatchDetector()
8+
override fun getIssues() = listOf(
9+
ErrorCatchDetector.ISSUE_CATCH_TOO_GENERIC,
10+
ErrorCatchDetector.ISSUE_ERROR_CAUGHT,
11+
)
12+
13+
@Test
14+
fun `test clean`() {
15+
val code = """
16+
package error
17+
18+
import java.io.File
19+
import java.io.FileNotFoundException
20+
21+
class ErrorHandler {
22+
fun throwableTester() {
23+
val androidDir : File? = try {
24+
File("~/.android")
25+
} catch (e: FileNotFoundException) {
26+
System.err.println(e.message)
27+
null
28+
}
29+
}
30+
}
31+
""".trimIndent()
32+
33+
lint().files(kotlin(code))
34+
.run().expectClean()
35+
}
36+
37+
@Test
38+
fun `test throwable catch statement detected`() {
39+
val code = """
40+
package error
41+
42+
import java.io.File
43+
44+
class ErrorHandler {
45+
fun throwableTester() {
46+
val androidDir : File? = try {
47+
File("~/.android")
48+
} catch (e: Throwable) {
49+
System.err.println(e.message)
50+
null
51+
}
52+
}
53+
}
54+
""".trimIndent()
55+
56+
lint().files(kotlin(code))
57+
.run().expectErrorCount(1)
58+
}
59+
60+
@Test
61+
fun `test error catch statement detected`() {
62+
val code = """
63+
package error
64+
65+
import java.io.File
66+
67+
class ErrorHandler {
68+
fun throwableTester() {
69+
val androidDir : File? = try {
70+
File("~/.android")
71+
} catch (e: OutOfMemoryError) {
72+
System.err.println(e.message)
73+
null
74+
}
75+
}
76+
}
77+
""".trimIndent()
78+
79+
lint().files(kotlin(code))
80+
.run().expect(
81+
"""src/error/ErrorHandler.kt:9: Error: Errors should not be caught. [ErrorCatchDetector]
82+
} catch (e: OutOfMemoryError) {
83+
~~~~~~~~~~~~~~~~
84+
1 errors, 0 warnings""",
85+
)
86+
}
87+
88+
@Test
89+
fun `test consecutive catch statements detected`() {
90+
val code = """
91+
package error
92+
93+
import java.io.File
94+
95+
class ErrorHandler {
96+
fun throwableTester() {
97+
val androidDir : File? = try {
98+
File("~/.android")
99+
} catch (e: OutOfMemoryError) {
100+
System.err.println(e.message)
101+
null
102+
} catch (t: Throwable) {
103+
System.err.println(e.message + " was thrown")
104+
null
105+
}
106+
}
107+
}
108+
""".trimIndent()
109+
110+
lint().files(kotlin(code))
111+
.run().expectErrorCount(2)
112+
}
113+
114+
@Test
115+
fun `test error multi-catch java statement detected`() {
116+
val code = """
117+
package error;
118+
119+
import java.io.File;
120+
121+
class ErrorHandler {
122+
public void throwableTester() {
123+
File androidDir = null;
124+
try {
125+
androidDir = File("~/.android");
126+
} catch (OutOfMemoryError | Throwable | Exception e) {
127+
System.err.println(e.message);
128+
}
129+
130+
System.out.println(androidDir != null ? androidDir.getAbsolutePath() : "");
131+
}
132+
}
133+
""".trimIndent()
134+
135+
lint().files(java(code))
136+
.run().expect(
137+
"""src/error/ErrorHandler.java:10: Error: Errors should not be caught. [ErrorCatchDetector]
138+
} catch (OutOfMemoryError | Throwable | Exception e) {
139+
~~~~~~~~~~~~~~~~
140+
src/error/ErrorHandler.java:10: Error: Catching Throwable will include Errors. Be more specific. [ThrowableCatchDetector]
141+
} catch (OutOfMemoryError | Throwable | Exception e) {
142+
~~~~~~~~~
143+
2 errors, 0 warnings""",
144+
)
145+
}
146+
}

0 commit comments

Comments
 (0)