Skip to content

Commit

Permalink
New lint rule that checks if a Tree is planted for atleast one app …
Browse files Browse the repository at this point in the history
…variant.

* Added tests for the lint rule.
  • Loading branch information
tikurahul committed Feb 20, 2020
1 parent 10f0adc commit 35d5497
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 17 deletions.
74 changes: 74 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,74 @@
@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)
)
)

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, 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."
)
}
}
}
17 changes: 0 additions & 17 deletions timber-lint/src/main/java/timber/lint/TimberIssueRegistry.java

This file was deleted.

16 changes: 16 additions & 0 deletions timber-lint/src/main/java/timber/lint/TimberIssueRegistry.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@file:Suppress("UnstableApiUsage")

package timber.lint

import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.detector.api.Issue

class TimberIssueRegistry : IssueRegistry() {
override val api = CURRENT_API
override val minApi: Int = CURRENT_API
override val issues: List<Issue> = listOf(
*WrongTimberUsageDetector.getIssues(),
PlantATreeDetector.ISSUE
)
}
112 changes: 112 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,112 @@
package timber.lint

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() {
companion object Forest {
fun e(message: String?, vararg args: Any?) {
}
fun w(message: String?, vararg args: Any?) {
}
fun i(message: String?, vararg args: Any?) {
}
fun d(message: String?, vararg args: Any?) {
}
fun v(message: String?, vararg args: Any?) {
}
fun plant(tree: Tree) {
}
}
open class Tree {
// A Tree Stub
}
}
""").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()
}

}

0 comments on commit 35d5497

Please sign in to comment.