Skip to content

Commit

Permalink
Ensure a Tree is planted for atleast one app variant.
Browse files Browse the repository at this point in the history
* Added tests for the lint rule.
  • Loading branch information
tikurahul committed Aug 11, 2021
1 parent adae9b8 commit e550f77
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 2 deletions.
80 changes: 80 additions & 0 deletions timber-lint/src/main/java/timber/lint/PlantATreeDetector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
@file:Suppress("UnstableApiUsage")

package timber.lint

import com.android.tools.lint.detector.api.*
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
import java.util.*

/**
* A [Detector] which makes sure than anytime Timer APIs are used, there is at-least a single tree
* planted.
*/
class PlantATreeDetector : Detector(), SourceCodeScanner {
companion object {
val ISSUE = Issue.create(
id = "MustPlantATimberTree",
briefDescription = "A Timber tree needs to be planted",
explanation = """
When using Timber's logging APIs, a `Tree` must be planted on at least a single \
variant of the app.
""",
androidSpecific = true,
category = Category.CORRECTNESS,
severity = Severity.ERROR,
implementation = Implementation(
PlantATreeDetector::class.java,
EnumSet.of(Scope.JAVA_FILE)
)
)

// plant() is a @JvmStatic
// It can be resolved via both Timber & Forest.
private const val TIMBER = "timber.log.Timber"
private const val FOREST = "timber.log.Timber.Forest"
}

// Do we need to check if a Tree is planted
private var checkForPlantedTrees = false
private var hasPlantedTree = false
private var location: Location? = null

override fun getApplicableMethodNames() = listOf("v", "d", "i", "w", "e", "wtf", "plant")

override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
val methodName = method.name
when (context.driver.phase) {
1 -> {
if (methodName.matches(Regex("(v|d|i|w|e|wtf)"))
&& context.evaluator.isMemberInClass(method, FOREST)
) {
if (!checkForPlantedTrees) {
location = context.getLocation(node)
checkForPlantedTrees = true
// Request a second scan with the same scope
context.driver.requestRepeat(this, null)
}
}
}
else -> {
if (methodName.matches(Regex("plant")) &&
(context.evaluator.isMemberInClass(method, TIMBER) ||
context.evaluator.isMemberInClass(method, FOREST))
) {
hasPlantedTree = true
}
}
}
}

override fun afterCheckRootProject(context: Context) {
if (checkForPlantedTrees && !hasPlantedTree && context.driver.phase > 1) {
context.report(
issue = ISSUE,
location = location ?: Location.create(context.file),
message = "A `Tree` must be planted for at least a single variant of the application."
)
}
}
}
4 changes: 2 additions & 2 deletions timber-lint/src/main/java/timber/lint/TimberIssueRegistry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import com.google.auto.service.AutoService
@AutoService(value = [IssueRegistry::class])
class TimberIssueRegistry : IssueRegistry() {
override val issues: List<Issue>
get() = WrongTimberUsageDetector.issues
get() = WrongTimberUsageDetector.issues + PlantATreeDetector.ISSUE

override val api: Int
get() = CURRENT_API
Expand All @@ -27,4 +27,4 @@ class TimberIssueRegistry : IssueRegistry() {
identifier = "com.jakewharton.timber:timber:{version}",
feedbackUrl = "https://github.com/JakeWharton/timber/issues",
)
}
}
145 changes: 145 additions & 0 deletions timber-lint/src/test/java/timber/lint/PlantATreeDetectorTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package timber.lint

import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import org.junit.Test

class PlantATreeDetectorTest {

private val timber = kotlin("timber/log/Timber.kt", """
package timber.log
class Timber private constructor() {
abstract class Tree {
}
companion object Forest: Tree() {
@JvmStatic
fun e(message: String?, vararg args: Any?) {
}
@JvmStatic
fun w(message: String?, vararg args: Any?) {
}
@JvmStatic
fun i(message: String?, vararg args: Any?) {
}
@JvmStatic
fun d(message: String?, vararg args: Any?) {
}
@JvmStatic
fun v(message: String?, vararg args: Any?) {
}
@JvmStatic
fun plant(tree: Tree) {
}
}
}
""").indented().within("src")

@Test
fun testNoTimberLoggingApisAreUsed() {
val application = kotlin("com/example/App.kt", """
package com.example
import timber.log.Timber
class App {
fun onCreate() {
}
}
""").indented().within("src")

lint()
.files(timber, application)
.issues(PlantATreeDetector.ISSUE)
.run()
.expectClean()
}

@Test
fun testWhenTimberApisAreUsed() {
val application = kotlin("com/example/App.kt", """
package com.example
import timber.log.Timber
class App {
fun onCreate() {
Timber.d("Log something")
}
}
""").indented().within("src")

lint()
.files(timber, application)
.issues(PlantATreeDetector.ISSUE)
.run()
.expect("""
src/com/example/App.kt:7: Error: A Tree must be planted for at least a single variant of the application. [MustPlantATimberTree]
Timber.d("Log something")
~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""".trimIndent())
}

@Test
fun testWhenTimberApisAreUsedAndTreeIsPlanted() {
val application = kotlin("com/example/App.kt", """
package com.example
import timber.log.Timber
class App {
fun onCreate() {
plantTree()
Timber.d("Log something")
}
private fun plantTree() {
val tree = Timber.Tree()
Timber.plant(tree)
}
}
""").indented().within("src")

lint()
.files(timber, application)
.issues(PlantATreeDetector.ISSUE)
.run()
.expectClean()
}

@Test
fun testWhenTimberApisAreUsedAndTreeIsPlanted_java() {
val application = java("com/example/App.java", """
package com.example;
import timber.log.Timber;
class App {
void onCreate() {
plantTree();
Timber.d("Log something");
}
void plantTree() {
val tree = Timber.Tree();
Timber.plant(tree);
}
}
""").indented().within("src")

lint()
.files(timber, application)
.issues(PlantATreeDetector.ISSUE)
.run()
.expectClean()
}

}

0 comments on commit e550f77

Please sign in to comment.