How to write a custom rule in Lint
Tags: android, programming, testing, lint
Published: 2019-03-05 08:36:23

---
title: How to write a custom rule in Lint
published: true
description: Do you want to enforce some custom lint logic in your project? I'll explain how to do that!
tags: android, programming, testing, lint
---

At my team at Deliveroo, we have recently decided to start using Google Truth instead of Hamcrest for test assertion but considering the number of unit tests in the codebase, migrating all the tests to Truth it’s almost impossible. 
We’ve decided instead to have both libraries in the project and use Truth for new tests and do the conversion when touching an existing one. Furthermore, we also wanted to be notified about using Hamcrest when issuing a pull request in order to avoid reviewer to have to check this manually.

So we’ve decided to write a custom lint rule in order to detect an Hamcrest import and report it as a warning and because we are using Danger (https://github.com/danger/danger) it also reports on pull request which exactly what we want.

In order to write a custom lint rule, we need to create a module that contains the rule and import it in every module where you want to enforce the rule.

Let’s start with creating the lint module: in Android Studio just create a new folder called `lint-rules` and add a `build.gradle` file inside:

```Groovy
file: lint-rules/build.gradle
apply plugin: 'java-library'
apply plugin: "kotlin"

dependencies {
    compileOnly "com.android.tools.lint:lint-api:26.3.1"
    compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.21"
    testImplementation "com.android.tools.lint:lint:26.3.1"
    testImplementation "com.android.tools.lint:lint-tests:26.3.1"
    testImplementation "com.google.truth:truth:0.42"
}
```

Nothing strange here, the module is a Kotlin library and we have defined dependencies for lint, Kotlin and Google Truth (more on this later). We also need to add the module to `settings.gradle` in order to have it available for the rest of the project:

```Kotlin
file: settings.gradle
include ':app'
include 'lint-rules'
```

So now that we have defined the module, we can add code to it :) First we need to create an `IssueRegistry` class, which is how lint let you inject your own issue check to the command:

```Java
file: lint-rules/src/main/java/IssueRegistry.kt
class IssueRegistry : com.android.tools.lint.client.api.IssueRegistry() {
    override val issues: List<Issue>
        get() = listOf()

    override val api: Int = com.android.tools.lint.detector.api.CURRENT_API
}
```

An `IssueRegistry` needs to expose two things: the list of issues to check and the current api value, lint documentation about the api value suggest just to return the current value so we can just focus on the list of issues.

What is an `Issue` in Lint? The best way to look at this, is to check the create method documentation on the `Issue` class:

```
/**
 * Creates a new issue. The description strings can use some simple markup;
 * see the [TextFormat.RAW] documentation
 * for details.
 *
 * @param id the fixed id of the issue
 * @param briefDescription short summary (typically 5-6 words or less), typically
 * describing the **problem** rather than the **fix**
 * (e.g. "Missing minSdkVersion")
 * @param explanation a full explanation of the issue, with suggestions for
 * how to fix it
 * @param category the associated category, if any
 * @param priority the priority, a number from 1 to 10 with 10 being most
 * important/severe
 * @param severity the default severity of the issue
 * @param implementation the default implementation for this issue
 * @return a new [Issue]
 */
```

That’s pretty straightforward! So we need an id, some text for the description and the explanation, a category, a priority, a severity and the actual implementation. Let’s then create a new `IssueHamcrestImport`:


```
val IssueHamcrestImport = Issue.create("HamcrestImport",
        "Hamcrest is deprecated",
        "Use Google Truth instead",
        CORRECTNESS,
        5,
        Severity.WARNING,
        TODO()
)
```

And that’s our Issue! So now we can return a list containing that issue but we still need to fill the `TODO()` section with the actual implementation. 
But at least the registry is finished now:

```
file: lint-rules/src/main/java/IssueRegistry.kt
class IssueRegistry : com.android.tools.lint.client.api.IssueRegistry() {
    override val issues: List<Issue>
        get() = listOf(IssueHamcrestImport)

    override val api: Int = com.android.tools.lint.detector.api.CURRENT_API
}

val IssueHamcrestImport = Issue.create("HamcrestImport",
        "Hamcrest is deprecated",
        "Use Google Truth instead",
        CORRECTNESS,
        5,
        Severity.WARNING,
        TODO()
)
```

The last parameter of the `Issue.create` (or `TODO()`) it’s an implementation of the detector api, so let’s create a new detector:


```
file: lint-rules/src/main/java/HamcrestNamingPatternDetector.kt
class HamcrestNamingPatternDetector : Detector(), Detector.UastScanner {

}
```

But what’s exactly a `Detector`? Again, let’s have a look at the documentation:

```
/**
 * A detector is able to find a particular problem (or a set of related problems).
 * Each problem type is uniquely identified as an [Issue].
 *
 * Detectors will be called in a predefined order:
 *
 *  1.  Manifest file
 *  2.  Resource files, in alphabetical order by resource type
 *      (therefore, "layout" is checked before "values", "values-de" is checked before
 *      "values-en" but after "values", and so on.
 *  3.  Java sources
 *  4.  Java classes
 *  5.  Gradle files
 *  6.  Generic files
 *  7.  Proguard files
 *  8.  Property files
 */
```

Interesting, so a `Detector` is an object that can find problems in any of those files/sources. For our current use case we are interested into `imports` of sources classes but luckily we don’t have to parse manually all the files, lint is already doing that for us! We just need to specify what we are interesting in:


```
override fun getApplicableUastTypes() = listOf(UImportStatement::class.java)
```

Ok wait a moment, what’s `UAST`? Let’s have another look at the documentation:

```
* UAST is short for "Universal AST" and is an abstract syntax tree library
* which abstracts away details about Java versus Kotlin versus other similar languages
* and lets the client of the library access the AST in a unified way.
```

Wow, that’s amazing! It means that we don’t have to parse the file ourselves and we can just use the already defined `UElement`:

```
public interface UImportStatement : org.jetbrains.uast.UResolvable, org.jetbrains.uast.UElement {
```

That does provide use already all the imports. Wonderful!
OK now we need to define the actual detector implementation:

```
class HamcrestInvalidImportHandler(private val context: JavaContext) : UElementHandler() {
    override fun visitImportStatement(node: UImportStatement) {
        node.importReference?.let { importReference ->
            if (importReference.asSourceString().contains("org.hamcrest.")) {
                context.report(IssueHamcrestImport, node, context.getLocation(importReference), "Forbidden import")
            }
        }
    }
}
```

So Lint can offer us a `UElementHandler` that lets you visit different things like `Class`, `Enum`, `Field`, etc.. But here we are interested just in imports so we can override

```
override fun visitImportStatement(node: UImportStatement)
```

and then inspect the `importReference` to see what does it contains, if it contains `org.hamcrest.` then we can report the issue in the `JavaContext` object using the `IssueHamcrestImport` that we’ve created earlier. 
The full implementation of the detector is:

```
class HamcrestNamingPatternDetector : Detector, Detector.UastScanner(){
    override fun getApplicableUastTypes() = listOf(UImportStatement::class.java)

    override fun createUastHandler(context: JavaContext) = HamcrestInvalidImportHandler(context)
}

class HamcrestInvalidImportHandler(private val context: JavaContext) : UElementHandler() {
    override fun visitImportStatement(node: UImportStatement) {
        node.importReference?.let { importReference ->
            if (importReference.asSourceString().contains("org.hamcrest.")) {
                context.report(IssueHamcrestImport, node, context.getLocation(importReference), "Forbidden import")
            }
        }
    }
}
```

And now back to our `Issue` definition, we can replace the `TODO()` with the actual detector:

```
val IssueHamcrestImport = Issue.create("HamcrestImport",
        "Hamcrest is deprecated",
        "Use Google Truth instead",
        CORRECTNESS,
        5,
        Severity.WARNING,
        Implementation(HamcrestNamingPatternDetector::class.java,
                EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES))
)
```

Notice here that we can define the scope using an enumeration so you can define on which type of file you want to act on (`JAVA_FILE` also contains Kotlin files).

One thing missing is subscribing our custom registry to lint:

```
end of file: lint-rules/build.gradle
jar {
    manifest {
        attributes('Lint-Registry-v2': 'IssueRegistry')
    }
}
```

Finally we can add the custom lint check module to the module that we want to enforce the check:

```
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    ...all other dependencies...
    lintChecks project(":lint-rules")
}
```

And if you run `./gradlew lint` on a project that contains tests using Hamcrest, now you should see something like this:
![Lint output](https://thepracticaldev.s3.amazonaws.com/i/ms3czmsfbikxqrb64qwa.png)

One more thing, I’ve mentioned in the beginning the Truth dependency for the lint rules module because we can also write tests for our registry and detector:
```
class IssueRegistryTest {

    @Test
    fun `check explanation for hamcrest import is correct`() {
        val output = IssueRegistry().issues
                .joinToString(separator = "\n") { "- **${it.id}** - ${it.getExplanation(TextFormat.RAW)}" }

        assertThat("""
        - **HamcrestImport** - Use Google Truth instead
        """.trimIndent()).isEqualTo(output)
    }
}
```
and
```
class HamcrestImportDetectorTest {

    @Test
    fun `should not report imports that are not hamcrest`() {
        TestLintTask.lint()
                .files(TestFiles.java("""
            package foo;
            import foo.R;
            class Example {
            }""").indented())
                .issues(IssueHamcrestImport)
                .run()
                .expectClean()
    }

    @Test
    fun `should warning about hamcrest import`() {
        TestLintTask.lint()
                .files(TestFiles.java("""
          package foo;
          import org.hamcrest.MatcherAssert.assertThat;
          class Example {
          }""").indented())
                .issues(IssueHamcrestImport)
                .run()
                .expect("""
          |src/foo/Example.java:2: Warning: Forbidden import [HamcrestImport]
          |import org.hamcrest.MatcherAssert.assertThat;
          |       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          |0 errors, 1 warnings""".trimMargin())
    }
}
```

The `TestLintTask` class lets us test our detector creating an in-memory file so we can assert the output of the check.

That’s it! You can find a working implementation on Github: https://github.com/dbottillo/Blog/tree/hamcrest_lint