Custom Lint annotation detector is not triggered - kotlin

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:
Writing custom lint rules
KotlinConf 2017 - Kotlin Static Analysis with Android Lint
Writing your first Lint check
Making Custom Lint for Kotlin Code
Getting the Most Out of Android Lint
Coding in Style: Static Analysis with Custom Lint Rules
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.

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

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.

Related

Gradle + Kotest + KMongo Coroutines - Could not create instance of class

I'm writing a small application in Kotlin that uses KMongo coroutines and I want to use Kotest as the testing framework.
I wrote a simple test to access a database and retrieve a document:
class KabotMultiDBClientTest : StringSpec({
val client = KabotMultiDBClient(
mapOf(
System.getenv("TEST_DB_ID")!! to MongoCredentials(
System.getenv("DB_TEST_USER")!!,
System.getenv("DB_TEST_PWD")!!,
System.getenv("TEST_DB")!!,
System.getenv("DB_TEST_IP")!!,
System.getenv("DB_TEST_PORT")!!.toInt(),
)
)
)
"dummy test" { true shouldBe true }
})
When I test it using the IntelliJ Kotest plugin it works but If I use the
./gradlew kotest
command I receive this error.
Could not create instance of class org.wagham.db.KabotMultiDBClientTest
If I remove the client instantiation the the gradle task works without problems.
This is the code of the class:
class KabotMultiDBClient(
credentials: Map<String, MongoCredentials>
) {
private val databaseCache = credentials.keys.fold(mapOf<String, CoroutineDatabase>()) { acc, guildId ->
credentials[guildId]?.let {
acc + (guildId to
KMongo.createClient("mongodb://${it.username}:${it.password}#${it.ip}:${it.port}/${it.database}").coroutine.getDatabase(it.database))
} ?: throw InvalidCredentialsExceptions(guildId)
}
suspend fun getActiveCharacter(guildId: String, playerId: String): org.wagham.db.models.Character {
return databaseCache[guildId]?.let {
val col = it.getCollection<org.wagham.db.models.Character>("characters")
col.findOne(Document(mapOf("status" to "active", "player" to playerId)))
} ?: throw InvalidGuildException(guildId)
}
}
What could be the origin of the error?
I found the error, and it was far more trivial than I imagined: apparently, gradlew launched from the Windows terminal couldn't read the environment variables.
I modified the build.gradle.kts file in this way and everything worked fine:
tasks.withType<Test> {
useJUnitPlatform()
environment("VAR1", "value 1")
environment("VAR2", "value 2")
environment("VARN", "value N")
}

Co-routines in Kotlin multiplatform project

I'm trying to use "runTest()" in Kotlin multiplatform. I'm using Jetbrains's "Getting started"-project as an example. (https://kotlinlang.org/docs/multiplatform-library.html)
The problem is that runTest() does not find a coroutine context. It gives me the following build error:
Cannot access class 'kotlin.coroutines.CoroutineContext'. Check your module classpath for missing or conflicting dependencies
Here is my test:
class Base64JvmTest {
#OptIn(ExperimentalCoroutinesApi::class)
#Test
fun testNonAsciiString() {
runTest {
val utf8String = "Gödel"
val actual = Base64Factory.createEncoder().encodeToString(utf8String.toByteArray())
assertEquals("R8O2ZGVs", actual)
}
}
}
In build.gradle.kts, I set the following in kotlin.sourceSets:
val jvmTest by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
}
}
Please help me out - what am I missing?
As it turns out, there was an issue with Idea. I added the following dependency to get rid of the error:
dependencies {
commonTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
}
It shouldn't really be needed, as the common tests are not dependent on coroutines, but an acceptable work-around.
you can run runTest following way as documentation is suggested
#Test
fun exampleTest() = runTest {
val deferred = async {
delay(1_000)
async {
delay(1_000)
}.await()
}
deferred.await() // result available immediately
}
documentation code link

Make use of web component library in Kotlin Compose for Web

I want to tinker with Kotlin Compose for Web a bit.
In some of my past web projects, I made use of some web components of the Clarity Design System (CDS).
In a JavaScript or TypeScript project,
you first need to install both npm packages#cds/core and #cds/city.
Secondly, you have to include some global stylesheets, e.g. via HTML or sass-import.
For each component you want to use, you need to import the corresponding register.js.
Lastly, you can include the component in your HTML like any other tag:
<cds-button>Click me!</cds-button>
I tried to replicate the steps with Kotlin Compose for Web, but wasn't able to get it to work.
Any help appreciated!
Okay, I've got it to work now, which included several steps.
Install npm dependencies
kotlin {
...
sourceSets {
val jsMain by getting {
dependencies {
// dependencies for Compose for Web
implementation(compose.web.core)
implementation(compose.runtime)
// dependencies for Clarity Design System
implementation(npm("#cds/core", "5.6.0"))
implementation(npm("#cds/city", "1.1.0"))
// dependency for webpack - see step 3
implementation(npm("file-loader", "6.2.0"))
}
}
...
}
}
Enable css support
This seems to be required, in order to include the global stylesheets.
kotlin {
js(IR) {
browser {
...
commonWebpackConfig {
cssSupport.enabled = true
}
}
...
}
...
}
Add support for .woff2 files included in stylesheet of Clarity
The stylesheet of CDS include font files of type .woff2, whose support in webpack must be configured.
This can be achieved by creating the file webpack.config.d/support-fonts.js
at the project root with the following content:
config.module.rules.push({
test: /\.(woff(2)?|ttf|eot|svg|gif|png|jpe?g)(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/'
}
}]
});
Include global stylesheets
external fun require(module: String): dynamic
fun main() {
require("modern-normalize/modern-normalize.css")
require("#cds/core/global.min.css")
require("#cds/core/styles/module.shims.min.css")
require("#cds/city/css/bundles/default.min.css")
...
}
Import register.js for desired web component
external fun require(module: String): dynamic
fun main() {
...
require("#cds/core/button/register.js")
...
}
Create #Composable for the web component
Sadly this solution makes use of APIs marked as #OptIn(ComposeWebInternalApi::class),
which stands for "This API is internal and is likely to change in the future".
Any hints on how this may be implemented without relying on internal APIs are appreciated.
#Composable
fun CdsButton(
status: CdsButtonStatus = CdsButtonStatus.Primary,
attrs: AttrBuilderContext<HTMLElement>? = null,
content: ContentBuilder<HTMLElement>? = null
) = TagElement(
elementBuilder = CdsElementBuilder("cds-button"),
applyAttrs = {
if (attrs != null) apply(attrs)
attr("status", status.attributeValue)
},
content = content
)
/**
* This is a copy of the private class org.jetbrains.compose.web.dom.ElementBuilderImplementation
*/
internal class CdsElementBuilder<TElement : Element>(private val tagName: String) : ElementBuilder<TElement> {
private val element: Element by lazy {
document.createElement(tagName)
}
override fun create(): TElement = element.cloneNode() as TElement
}
sealed interface CdsButtonStatus {
object Primary : CdsButtonStatus
...
}
internal val CdsButtonStatus.attributeValue
get() = when (this) {
CdsButtonStatus.Primary -> "primary"
...
}
Make us of your #Composable!
fun main() {
...
renderComposable(rootElementId = "root") {
CdsButton(
status = CdsButtonStatus.Success
) {
Text("It works! :-)")
}
}
}

Kotlin annotations defined in tests not present in reflection info in integration tests

We have a fairly standard Kotlin DSL Gradle build. We've added an integrationTest sourceSet and task:
plugins {
kotlin("jvm") version "1.3.72"
application
}
sourceSets {
create("integrationTest") {
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
compileClasspath += sourceSets.test.get().output
compileClasspath += sourceSets.main.get().runtimeClasspath
runtimeClasspath += sourceSets.test.get().output
resources.srcDir(sourceSets.test.get().resources.srcDirs)
}
}
val integrationTest = task<Test>("integrationTest") {
description = "Runs the integration tests."
group = "verification"
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
classpath = sourceSets["integrationTest"].runtimeClasspath
mustRunAfter(tasks.test)
useJUnitPlatform()
}
Classes in src/integrationTest/kotlin can use classes from src/test/kotlin just fine, but annotations defined in src/test/kotlin do not show up in reflection data for classes in src/integrationTest/kotlin. When used on classes in src/test/kotlin, the annotations are present in reflection data as expected.
The annotations are very simple:
#Target(FUNCTION, CLASS)
// NB: Default Retention is RUNTIME (Annotation is stored in binary output and visible for reflection)
annotation class SystemProperty(val key: String, val value: String)
// Kotlin does not yet support repeatable annotations https://youtrack.jetbrains.com/issue/KT-12794
#Target(FUNCTION, CLASS)
annotation class SystemProperties(vararg val systemProperties: SystemProperty)
This is how the annotations are used, in a JUnit 5 Extension:
class SystemPropertyExtension : BeforeAllCallback {
override fun beforeAll(extensionContext: ExtensionContext) {
val clazz = extensionContext.requiredTestClass
clazz.getAnnotation(SystemProperty::class.java)?.let {
System.setProperty(it.key, it.value)
}
clazz.getAnnotation(SystemProperties::class.java)?.let {
it.systemProperties.forEach { prop -> System.setProperty(prop.key, prop.value) }
}
}
}
And typical use on the test itself:
#SystemProperty(key = "aws.s3.endpoint", value = "http://localstack:4566")
#ExtendWith(SystemPropertyExtension::class)
class SomeIntegrationTest {
//
}
Setting breakpoints while running tests shows System.setProperty(it.key, it.value) getting called. However while debugging integration tests, the breakpoint is not hit.
Any ideas on what might be wrong/missing here?
We could add a "testing" module to the project and export the test-jar, but would like to avoid that if possible.
The annotations were simply missing #Inherited. They were found on classes, but without #Inherited, they weren't found via superclass.

Kotlin script eval and Java 11 with inline function in the DSL

Our project decided to make the "jump" and move to open implementations, and grabbing the opportunity to move the Kotlin Java compatibility to version 11.
It introduced several 3rd-party library compatibility challenges, but I was able to solve them all.
Now I use Kotlin 1.3.50 (with the new JSR223 engine) with Java 11 target and builds and runs correctly.
plugins {
kotlin("jvm") version "1.3.50"
}
dependencies {
compile(kotlin("stdlib"))
implementation(kotlin("scripting-jsr223-embeddable"))
}
configure<JavaPluginConvention> {
sourceCompatibility = JavaVersion.VERSION_11
}
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "11"
}
The application uses complex DSL scripts in the form of kts. They worked perfectly prior upgrading to 11, but fails to evaluate since switching to version 11.
When the code calls the eval:
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
val building = engine.eval(src) as Building
it throws class version incompatibility exception:
javax.script.ScriptException: Cannot inline bytecode built
with JVM target 11 into bytecode that is being built with
JVM target 1.8. Please specify proper '-jvm-target' option
...
From this error, I can't determine whether the script engine compiles the script into Java 8 class and then tries to add the result to the core code; or the script tries to use a class which is not in Java 11 format.
How can fix this incompatibility or how could I debug all the project (including 3rd party) classes of their class file version?
Note: The DSL is too complex to give a clear and simple example, however I was able to eval the script of "2+2" without problem.
EDIT 1: A simplified example
After a long lets-comment-out session, I was able to locate the problem: it fails when I use a reified inline function in the script. This discovery made it possible to create a simple example:
package hu.app
import javax.script.ScriptEngineManager
import kotlin.reflect.KClass
/**
* A class representing a type-value pair
*/
class TypedProperty<T : Any>(val type: KClass<T>, initialValue: T? = null) {
var value: T? = initialValue
companion object {
inline fun <reified T : Any> create(initialValue: T? = null) = TypedProperty(T::class, initialValue)
}
}
/**
* The root object the DSL creates. Body omitted for simplicity.
*/
class Building {
// Class body is omitted for simplicity
}
/**
* The simple DSL
*/
#DslMarker
annotation class MyDsl
#MyDsl
class PropertyBuilder constructor() {
#MyDsl
val properties: Map<String, TypedProperty<*>> = mutableMapOf()
/**
* An extension function for String allowing the creation of a freely typed, but typed properties.
* This function causes the problem!
*/
#MyDsl
inline infix fun <reified T : Any> String.set(value: T?) {
val p = (properties as MutableMap).getOrPut(this, { TypedProperty.create(value) })
if (T::class == p.type)
(p as TypedProperty<T>).apply { this.value = value }
else
throw RuntimeException("Property '$this' is reassigned with different type. Original: ${p.type.simpleName}, new: ${T::class.simpleName}")
}
}
// Root of the DSL
#MyDsl
fun building(id: String = "Unnamed", op: BuildingBuilder.() -> Unit) = BuildingBuilder(id).apply(op).build()
// An interface for DSL classes with property support
#MyDsl
interface HasPropertiesBuilder {
#MyDsl
val properties: PropertyBuilder
#MyDsl
fun properties(op: PropertyBuilder.() -> Unit) = properties.op()
}
// The builder of the root object: it has properties
class BuildingBuilder(val id: String) : HasPropertiesBuilder {
override val properties = PropertyBuilder()
// This function body is omitted for simplification
fun build() : Building = Building()
}
fun main() {
// Initialize the engine (new 1.3.50 engine is used)
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
// Evaluation
val res = engine.eval("""
import hu.pmi.autoparking.app.*
building {
properties {
"X" set 42
}
}
""".trimIndent()) as Building
}
In the above example, there is an extension function on String letting to create typed properties. It has reified type parameter, so it should be inline. This method cause the exception: if the "X" set 42 line is removed, the script compiles.
Digging deeper, now it is clear, that the script engine compiles into Java 8 and therefor, when it tries to inject the inline function which is in Java 11 class format, it fails.
So the new question: how could I tell the JSR223 kotlin engine to compile to version 11?