Writing your first Lint check

I’ll show you how you can flag class names that are not in defined camel case by writing a custom Lint check. Instead of naming a class XMLHTTPRequest it should be named XmlHttpRequest.

First we create a new module named lint using the Android Studio wizard. The build.gradle file should look like this:

apply plugin: "kotlin"
targetCompatibility = JavaVersion.VERSION_1_7
sourceCompatibility = JavaVersion.VERSION_1_7
dependencies {
compileOnly "com.android.tools.lint:lint-api:26.0.1"
compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jre7:1.2.10"
  testImplementation "com.android.tools.lint:lint:26.0.1"
testImplementation "com.android.tools.lint:lint-tests:26.0.1"
}

We’ll be using Kotlin for writing our check since it’s a nice language and also half of the Lint API is written in Kotlin, which means we’ll get some nice extra benefits like extension functions, proper @Deprecated support and all of the language goodies.

You might be wondering why the version is 26.0.1. It’s for historical reasons. The rule of thumb is, add 23.0.0 to the Android Gradle Plugin version you want to support. 3.0.1 + 23.0.1 makes 26.0.1. For the current alpha it would be 26.1.0-alpha06.

Next the Lint infrastructure needs a way to know what issues we have. There’s a Registry API which will let us provide our Lint issues. In our case we’ll name the issue ISSUE_NAMING_PATTERN.

package com.foo.bar
class MyIssueRegistry : IssueRegistry() {
override fun getIssues() = listOf(ISSUE_NAMING_PATTERN)
}

Only the registry class is not enough, we need to expose it. This is currently done by adding an attribute to the manifest file of the jar. We can do this easily in Gradle.

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

If we want to consume the Lint check from a library or application we can use the lintChecks function.

dependencies {
lintChecks project(":lint")
}

This is all that needs to be done for setting up our own Lint rules. The Android Gradle Plugin will take care of the rest.

If you are a library developer and want to include Lint checks with a library just use lintChecks in the libraries build.gradle file and they’ll automatically be bundled into the aar for the library consumers.

The missing puzzle piece now is the ISSUE_NAMING_PATTERN. This is just a variable that we instantiate like this:

val ISSUE_NAMING_PATTERN = Issue.create("NamingPattern",
"Names should be well named.",
"Some long description about this issue",
CORRECTNESS,
5,
WARNING,
Implementation(NamingPatternDetector::class.java,
EnumSet.of(JAVA_FILE, TEST_SOURCES))
)

The first parameter is the id. That one can be used to suppress it via the @SuppressLint annotation.

Then comes a title and a description.

Followed by a Category. There are a bunch defined like SECURITY, CORRECTNESS, PERFORMANCE and we can just choose whichever we’d like to.

The integer is mostly irrelevant. It describes the importance / level of the rule. Just pick anything between 1 and 10.

Next comes the severity which could be either WARNING, ERROR, INFORMATIONAL, IGNORE or FATAL. Choose what’s appropriate for you.

The last parameter is the Implementation. We create a new one, pass in our detector that we’ll write shortly and then also the scope. We’ll be running this Lint issue on Java files and test sources.

Note: This will also run on Kotlin classes, the variable just hasn’t been updated to reflect this.

Detector is another important part of the public API of Lint. This is where the logic of our Lint check will be in. You can think of Detector as a container that will get instantiated once. By implementing certain Scanner interfaces we can scan certain parts of our code in a visitor style API.

  • XmlScanner — Xml files of all sorts (e.g. layouts, drawables)
  • UastScanner — Java + Kotlin files
  • ClassScanner — Byte code
  • BinaryResourceScanner — Binary resources, for instance an image
  • ResourceFolderScanner —Resource folders like values, drawable, xml, etc.
  • GradleScanner — Gradle build scripts like build.gradle or settings.gradle
  • OtherFileScanner —Files for ProGuard, Text, YML etc.

Enough theory, let’s see how we can get our Lint check up and running.

class NamingPatternDetector : Detector(), Detector.UastScanner {
override fun getApplicableUastTypes() = listOf(UClass::class.java)
  override fun createUastHandler(context: JavaContext) =
NamingPatternHandler(context)
class NamingPatternHandler(private val context: JavaContext) :
UElementHandler() {
override fun visitClass(clazz: UClass) {
if (clazz.name?.isDefinedCamelCase() == false) {
context.report(ISSUE_NAMING_PATTERN, clazz,
context.getNameLocation(clazz),
"Not named in defined camel case.")
}
}
}
}

We extend the Detector class and implement the UastScanner.

With getApplicableUastTypes() we tell Lint what we’re interested in UClass. UClass is a representation of a Kotlin or Java class.

We create our UastHandler and pass in the context, which we need for reporting.

The NamingPatternHandler implementation is straight forward. We visit the class, check that the name is well defined and if it’s not we can report an issue.

First we pass in the issue, next comes the scope, which is the class itself. Third comes the location, which is the part Lint will scribble in Android Studio or in the reports. We can use the context for getting the exact location for the name of the class. Neat. Next we define a custom message for this particular case.

The implementation of isDefinedCamelCase dissects the string and then pairs the current character and the next one. To know whether it’s well defined or not we just need to check that there’s no combination where two uppercase letters are next to each other.

private fun String.isDefinedCamelCase(): Boolean {
val charArray = toCharArray()
return charArray
.mapIndexed { index, current ->
current to charArray.getOrNull(index + 1)
}
.none {
it.first.isUpperCase() && it.second?.isUpperCase() ?: false
}
}

Now we’ve written all of this but how can we test this easily? Lint comes with tooling to make this a breeze.

@Test fun correctClassName() {
lint()
.files(java("""
|package foo;
|
|class XmlHttpRequest {
|}""".trimMargin()))
.issues(ISSUE_NAMING_PATTERN)
.run()
.expectClean()
}

lint() will set up a testing infrastructure. We’ll feed it with our class file, run it with our issue and exceptClean. Nothing gets reported. It’s that easy.

Now let’s look how it would look like if we would name our class XMLHTTPRequest.

@Test fun incorrectClassName() {
lint()
.files(java("""
|package foo;
|
|class XMLHTTPRequest {
|}""".trimMargin()))
.issues(ISSUE_NAMING_PATTERN)
.run()
.expect("""
|src/foo/XMLHTTPRequest.java:3: Warning: Not named in defined camel case. [NamingPattern]
|class XMLHTTPRequest {
| ~~~~~~~~~~~~~~
|0 errors, 1 warnings""".trimMargin())
}

This time we don’t use expectClean() instead we use expect(). The trick is there to let the test run once, then copy and paste the output and verify that it’s correct. We have all of the information that we need in this small string. We have the reported file, the line number, the severity, the issue id, the exact location as well as a preview.

We can also test this on a Kotlin class when running on 26.1.0-beta4. The check will already work on Kotlin code when running from Android Studio 3.0.0. 3.1.0 will add support for the command line as well as for the testing support when writing custom Lint checks.

@Test fun incorrectClassNameKotlin() {
lint()
.files(kt("""
|package foo;
|
|class XMLHTTPRequest""".trimMargin()))
.issues(ISSUE_NAMING_PATTERN)
.run()
.expect("""
|src/foo/XMLHTTPRequest.kt:3: Warning: Not named in defined camel case. [NamingPattern]
|class XMLHTTPRequest
| ~~~~~~~~~~~~~~
|0 errors, 1 warnings""".trimMargin())
}

Conclusion

In no time we’ve set up a new module for our Lint checks, wrote one, tested it and integrated it into our app. This is super easy and really convenient.

If you are looking for more inspiration about other Lint checks check out my open source lint rules or the ones that are bundled with Android.

If you have any further question or need a review on your Lint check, let me know!

You can check out this Lint rule in a slightly modified and broader here.