Reputation: 1085
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
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
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
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
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