Skip to content

Commit 35d5497

Browse files
committed
New lint rule that checks if a Tree is planted for atleast one app variant.
* Added tests for the lint rule.
1 parent 10f0adc commit 35d5497

4 files changed

Lines changed: 202 additions & 17 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
@file:Suppress("UnstableApiUsage")
2+
3+
package timber.lint
4+
5+
import com.android.tools.lint.detector.api.*
6+
import com.intellij.psi.PsiMethod
7+
import org.jetbrains.uast.UCallExpression
8+
import java.util.*
9+
10+
/**
11+
* A [Detector] which makes sure than anytime Timer APIs are used, there is at-least a single tree
12+
* planted.
13+
*/
14+
class PlantATreeDetector : Detector(), SourceCodeScanner {
15+
companion object {
16+
val ISSUE = Issue.create(
17+
id = "MustPlantATimberTree",
18+
briefDescription = "A Timber tree needs to be planted",
19+
explanation = """
20+
When using Timber's logging APIs, a `Tree` must be planted on at least a single \
21+
variant of the app.
22+
""",
23+
androidSpecific = true,
24+
category = Category.CORRECTNESS,
25+
severity = Severity.ERROR,
26+
implementation = Implementation(
27+
PlantATreeDetector::class.java,
28+
EnumSet.of(Scope.JAVA_FILE)
29+
)
30+
)
31+
32+
private const val FOREST = "timber.log.Timber.Forest"
33+
}
34+
35+
// Do we need to check if a Tree is planted
36+
private var checkForPlantedTrees = false
37+
private var hasPlantedTree = false
38+
private var location: Location? = null
39+
40+
override fun getApplicableMethodNames() = listOf("v", "d", "i", "w", "e", "wtf", "plant")
41+
42+
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
43+
val methodName = method.name
44+
when (context.driver.phase) {
45+
1 -> {
46+
if (methodName.matches(Regex("(v|d|i|w|e|wtf)"))
47+
&& context.evaluator.isMemberInClass(method, FOREST)) {
48+
if (!checkForPlantedTrees) {
49+
location = context.getLocation(node)
50+
checkForPlantedTrees = true
51+
// Request a second scan with the same scope
52+
context.driver.requestRepeat(this, null)
53+
}
54+
}
55+
}
56+
else -> {
57+
if (methodName.matches(Regex("plant"))
58+
&& context.evaluator.isMemberInClass(method, FOREST)) {
59+
hasPlantedTree = true
60+
}
61+
}
62+
}
63+
}
64+
65+
override fun afterCheckRootProject(context: Context) {
66+
if (checkForPlantedTrees && !hasPlantedTree && context.driver.phase > 1) {
67+
context.report(
68+
issue = ISSUE,
69+
location = location ?: Location.create(context.file),
70+
message = "A `Tree` must be planted for at least a single variant of the application."
71+
)
72+
}
73+
}
74+
}

timber-lint/src/main/java/timber/lint/TimberIssueRegistry.java

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@file:Suppress("UnstableApiUsage")
2+
3+
package timber.lint
4+
5+
import com.android.tools.lint.client.api.IssueRegistry
6+
import com.android.tools.lint.detector.api.CURRENT_API
7+
import com.android.tools.lint.detector.api.Issue
8+
9+
class TimberIssueRegistry : IssueRegistry() {
10+
override val api = CURRENT_API
11+
override val minApi: Int = CURRENT_API
12+
override val issues: List<Issue> = listOf(
13+
*WrongTimberUsageDetector.getIssues(),
14+
PlantATreeDetector.ISSUE
15+
)
16+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package timber.lint
2+
3+
import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin
4+
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
5+
import org.junit.Test
6+
7+
class PlantATreeDetectorTest {
8+
9+
private val timber = kotlin("timber/log/Timber.kt", """
10+
package timber.log
11+
class Timber private constructor() {
12+
companion object Forest {
13+
fun e(message: String?, vararg args: Any?) {
14+
15+
}
16+
fun w(message: String?, vararg args: Any?) {
17+
18+
}
19+
fun i(message: String?, vararg args: Any?) {
20+
21+
}
22+
fun d(message: String?, vararg args: Any?) {
23+
24+
}
25+
fun v(message: String?, vararg args: Any?) {
26+
27+
}
28+
fun plant(tree: Tree) {
29+
30+
}
31+
}
32+
33+
open class Tree {
34+
// A Tree Stub
35+
}
36+
}
37+
""").indented().within("src")
38+
39+
@Test
40+
fun testNoTimberLoggingApisAreUsed() {
41+
val application = kotlin("com/example/App.kt", """
42+
package com.example
43+
44+
import timber.log.Timber
45+
46+
class App {
47+
fun onCreate() {
48+
}
49+
}
50+
""").indented().within("src")
51+
52+
lint()
53+
.files(timber, application)
54+
.issues(PlantATreeDetector.ISSUE)
55+
.run()
56+
.expectClean()
57+
}
58+
59+
@Test
60+
fun testWhenTimberApisAreUsed() {
61+
val application = kotlin("com/example/App.kt", """
62+
package com.example
63+
64+
import timber.log.Timber
65+
66+
class App {
67+
fun onCreate() {
68+
Timber.d("Log something")
69+
}
70+
}
71+
""").indented().within("src")
72+
73+
lint()
74+
.files(timber, application)
75+
.issues(PlantATreeDetector.ISSUE)
76+
.run()
77+
.expect("""
78+
src/com/example/App.kt:7: Error: A Tree must be planted for at least a single variant of the application. [MustPlantATimberTree]
79+
Timber.d("Log something")
80+
~~~~~~~~~~~~~~~~~~~~~~~~~
81+
1 errors, 0 warnings
82+
""".trimIndent())
83+
}
84+
85+
@Test
86+
fun testWhenTimberApisAreUsedAndTreeIsPlanted() {
87+
val application = kotlin("com/example/App.kt", """
88+
package com.example
89+
90+
import timber.log.Timber
91+
92+
class App {
93+
fun onCreate() {
94+
plantTree()
95+
Timber.d("Log something")
96+
}
97+
98+
private fun plantTree() {
99+
val tree = Timber.Tree()
100+
Timber.plant(tree)
101+
}
102+
}
103+
""").indented().within("src")
104+
105+
lint()
106+
.files(timber, application)
107+
.issues(PlantATreeDetector.ISSUE)
108+
.run()
109+
.expectClean()
110+
}
111+
112+
}

0 commit comments

Comments
 (0)