eqtèöck
eqtèöck

Reputation: 1085

Custom Lint annotation detector is not triggered

I am trying to write my first Lint rule. For now I just want to detect the use of the annotation @AnyThread.

I have created a module to implement my custom rule. The gradle file for this module is (I use the gradle plugin version 3.6.1):

targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    compileOnly 'com.android.tools.lint:lint-api:26.6.1'
    compileOnly 'com.android.tools.lint:lint-checks:26.6.1'

    testImplementation "com.android.tools.lint:lint:26.6.1"
    testImplementation "com.android.tools.lint:lint-tests:26.6.1"
    testImplementation "com.android.tools:testutils:26.6.1"

    testImplementation "junit:junit:4.12"
}

jar {
    manifest {
        attributes("Lint-Registry-v2": "com.test.lint.MyIssueRegistry")
    }
}

My detector is:

package com.test.lint
//...
class AnyThreadAnnotationDetector: AbstractAnnotationDetector(), Detector.UastScanner {

    companion object {
        private const val AnyThreadId = "AnyThreadId"
        const val AnyThreadDescription = "This is an attempt to find AnyThread annotation in code"
        const val AnyThreadExplanation = "AnyThread annotation found!"

        val ANYTHREAD_ANNOTATION_ISSUE = Issue.create(
            id = AnyThreadId,
            briefDescription = AnyThreadDescription,
            explanation = AnyThreadExplanation,
            category = Category.CORRECTNESS,
            priority = 4,
            severity = Severity.INFORMATIONAL,
            implementation = Implementation(
                AnyThreadAnnotationDetector::class.java,
                Scope.JAVA_FILE_SCOPE
            )
        )
    }

    override fun applicableAnnotations(): List<String>? = listOf("androidx.annotation.AnyThread")

    override fun visitAnnotationUsage(
        context: JavaContext,
        usage: UElement,
        type: AnnotationUsageType,
        annotation: UAnnotation,
        qualifiedName: String,
        method: PsiMethod?,
        annotations: List<UAnnotation>,
        allMemberAnnotations: List<UAnnotation>,
        allClassAnnotations: List<UAnnotation>,
        allPackageAnnotations: List<UAnnotation>
    ) {

      context.report(
          issue = ANYTHREAD_ANNOTATION_ISSUE,
          scope = usage,
          location = context.getNameLocation(usage),
          message = "A message"
      )
  }
}

My IssueRegistry is:

class MyIssueRegistry : IssueRegistry() {
    override val issues: List<Issue>
        get() = listOf(
            AnyThreadAnnotationDetector.ANYTHREAD_ANNOTATION_ISSUE)

    override val api: Int = CURRENT_API
}

I wrote some tests:


class AnyThreadAnnotationDetectorTest  {

      @Test
      fun noAnnotatedFileKotlin() {
          TestLintTask.lint()
              .files(
                  LintDetectorTest.kotlin(
                      """
          |package foo;
          |
          |class XmlHttpRequest {
          |}""".trimMargin()
                  )
              )
              .allowMissingSdk()
              .issues(AnyThreadAnnotationDetector.ANYTHREAD_ANNOTATION_ISSUE)
              .run()
              .expectClean()
      }

      @Test
      fun annotatedKotlinMethod() {
          TestLintTask.lint()
              .files(
                  LintDetectorTest.kotlin(
                      """
          |package foo;
          |
          |import androidx.annotation.AnyThread
          |
          |class XmlHttpRequest {
          |@AnyThread
          |fun test(){}
          |}""".trimMargin()
                  )
              )
              .allowMissingSdk()
              .issues(AnyThreadAnnotationDetector.ANYTHREAD_ANNOTATION_ISSUE)
              .run()
              .expect(
                  """
                          Just a test to find annotations
                          0 errors, 0 warnings
                          """.trimIndent()
              )
      }


      @Test
      fun testNoisyDetector() {
          TestLintTask.lint().files(Stubs.ANYTHREAD_EXPERIMENT)
              .allowMissingSdk()
              .issues(AnyThreadAnnotationDetector.ANYTHREAD_ANNOTATION_ISSUE)
              .run()
              .expect(
                  """
                          Just a test to find annotations
                          0 errors, 0 warnings
                          """.trimIndent()
              )
      }
    }

Where the Stubs.ANYTHREAD_EXPERIMENT is:

object Stubs {
    val ANYTHREAD_EXPERIMENT = kotlin(
        "com/test/applicationlintdemoapp/AnythreadAnnotationStubs.kt",
        """
                package com.test.applicationlintdemoapp

                import androidx.annotation.AnyThread

                class AnythreadClassExperiment {
                    @AnyThread
                    fun setTimeToNow() {
                        TimeTravelProvider().setTime(System.currentTimeMillis())
                    }

                    @AnyThread
                    fun setTimeToEpoch() {
                        TimeTravelProvider().setTime(0)
                    }

                    fun violateTimeTravelAccords() {
                        TimeTravelProvider().setTime(-1)
                    }
                }
            """
    ).indented().within("src")
}

All my test fail (except noAnnotatedFileKotlin), actually if I put a breakpoint on the call to context.report the test made in debug mode is never paused, meaning that the annotation androidx.annotation.AnyThread is never detected.

What could go wrong ? what did I miss?

I have seen and read a some docs:

And I controlled the configuration by implementing the NoisyDetector given in the talk Coding in Style: Static Analysis with Custom Lint Rules, the result of the test are fine.

Upvotes: 3

Views: 1718

Answers (2)

spetz911
spetz911

Reputation: 48

You can add stubs for the @AnyThread annotation by adding SUPPORT_ANNOTATIONS_JAR to the lint().files(...) call or manually declaring the @AnyThread annotation class in a separate test source file.

An example of using SUPPORT_ANNOTATIONS_JAR inside of CheckResultDetectorTest can be found here.

Upvotes: 0

Gent Ahmeti
Gent Ahmeti

Reputation: 1599

I might be a little late to answer this, but it might be useful for other people who run into this question

I'm having the same problem, I need to find usages of an Annotation and report them. But for some reason the Kotlin UAST (Java works fine) doesn't record/report annotations. I'm using a sort of workaround to get through this

Workaround


Instead of visiting annotations, I'm visiting UMethod or UClass depending on what you need. Then I'm doing a manual String.contains() check on the node.sourcePsi.text to see if the annotation is there

    override fun getApplicableUastTypes() = listOf(UMethod::class.java)

    override fun createUastHandler(context: JavaContext): UElementHandler {
        return object : UElementHandler() {

            override fun visitMethod(node: UMethod) {
                if (!shouldSkip(node.sourcePsi) && node.isAnnotatedWith("AnyThread")) {
                    context.report(
                        issue = ANYTHREAD_ANNOTATION_ISSUE,
                        scope = usage,
                        location = context.getNameLocation(usage),
                        message = "A message"
                    )
                }
            }
        }

        // Skip KtClass, because it calls the `visitMethod` method since the class has the constructor method in it
        private fun shouldSkip(node: PsiElement?): Boolean = node is KtClass
    }

    fun UAnnotated.isAnnotatedWith(annotation: String) = sourcePsi?.text?.contains("@$annotation") == true

Drawbacks


The problem I see with this is that it will be called for every method instead of only when the annotation is found, and the shouldSkip() method seems like a hack to me. But other than that it works correctly and should report problems

Note: Calling node.hasAnnotation(), node.findAnnotation() or context.evaluator.hasAnnotation() will not find annotations in Kotlin

Upvotes: 3

Related Questions