Formatting Code Analysis Rule with Android Lint (part 1/2)
For Kotlin and Java at once
In this first part of the article, primarily, I want to show that it’s worth the time to write a custom code analysis rule to enforce code conventions. Secondly, I want to demonstrate that in addition to Checkstyle and ktlint, Android Lint should also be considered when creating a formatting-related code analysis rule, despite that not being its basic purpose. I’ll explain its main advantage in my case against the other tools, and describe the inner workings that provide this benefit. For inspiration, I’ll also show the steps I took to make my formatting rule work with Android Lint.
I get frustrated when I get formatting-related comments on my merge requests or when I have to add such comments to others’. Sometimes, we have to change branches in order to fix a single formatting error in our MR, which is quite demotivating. That’s why I started thinking about ways we could automate fixing this minor (yet important) problem, so we could focus on more complex issues during code reviews.
We use several lint tools, such as ktlint or Checkstyle, for enforcing global formatting conventions (variable order, line length, etc.), but these don’t include certain company-wide rules by default. For those cases, the above-mentioned tools support adding custom rules as well. I decided to write one for the most common formatting mistakes we make:
We always require double line breaks before and after “block statements”, which can be if, switch (when in Kotlin), for, try, or while blocks, unless they are exactly at the beginning or the end of a method or another block. We believe this makes our code more readable, that’s why we always leave a comment during code review if someone violates this rule (and if we happen to notice it).
What is the best tool for this problem
We can say that ktlint for Kotlin and Checkstyle for Java are the main formatting code analysis tools, but now I’ll pick another tool yet: Android Lint (not only for Android projects), because it also supports writing custom rules that can be applied to Kotlin and Java at the same time. It seems a more logical choice, because we use both languages in our Android projects, and I wanted neither to write the same rule twice nor maintain them concurrently. Not to mention the integration of the rule, which would also need to be done two times.
Why Java is still important at all
As an Android developer, I have to write code in both Java and Kotlin. However, we can say that it’s not worth the time to focus on Java anymore, because as Kotlin is coming up, we use it more and more instead of Java in our codebase. On the other hand, I believe the transition isn’t happening that quickly in big projects already in production. So, as we want to see nicely formatted code in Java as well, it’s important to have this rule for both languages.
How to write custom lint rules
There are many tutorials and articles about writing custom code analysis rules, so I won’t detail it too much in this post. But if you’re interested, here are a couple of links I can recommend for the different tools: To Checkstyle, the official doc, to ktlint and Android Lint, Niklas Baudy’s great medium articles.
How Android Lint and other static code analysis tools work
Initially, Android Lint was created for finding mainly Android specific issues in Java code. In order to analyze the Java code, Android Lint used to create a Java-specific Abstract Syntax Tree (or just AST, learn more on Wikipedia), which is a tree representation of the source code. Other static code analysis tools also use a language specific AST for analysis;
Checkstyle: Java-, ktlint: Kotlin-, detekt: Kotlin-specific. The tools traverse this AST and find errors by checking its nodes and their attributes.
How Android Lint can check two languages at once
The difference between Android Lint and other tools is that as Kotlin also became a supported language for Android, Android Lint started to support Kotlin as well regarding the rules we already had to Java. For this, they introduced the so called Universal Abstract Syntax Tree (developed by JetBrains) which provides a tree representation of the code that can be applied for both Kotlin and Java languages at the same time, so it’s on a higher abstraction level than a language specific AST. For more information, I recommend this talk part from Tor Norbye — the creator and maintainer of Android Lint.
Both UAST and AST provide high level details about the source code. They don’t contain information about whitespaces or braces, but it’s usually enough for the Android specific rules. See a UAST example below:
The UAST library includes all the language-specific expressions, but also provides common interfaces for them. Example for the if expression:
PSI Tree (Program Structure Interface Tree)
The PSI tree is built on the UAST in case of Android Lint (in case of other tools, it’s built on the language specific AST), and this is the tree that contains more details about the structure of the code, such as whitespaces, braces, and similar elements.
In Android Lint, PSI Expressions are included in the implementation of the UAST Expressions, which are the nodes of the UAST. Eg. org.jetbrains.kotlin.psi.KtIfExpression can be accessed from KotlinUIfExpression.
There is even a handy intelliJ plugin: PsiViewer that can ease the debugging of PSI tree-based code analysis rules. See the example below with the same code snippet, where we can see that the two languages have different language-specific tokens in the trees:
Using both UAST and PSI Tree
I need both UAST and PSI Tree to create my formatting rule, because UAST is great to filter and visit nodes that I’m interested in for both languages at once, but as I mentioned it doesn’t provide information about formatting, eg. whitespace information that is essential for me. UAST rather focuses on a higher abstraction level, so primarily it isn’t for creating formatting rules.
Since the Gradle plugin 3.4 and the related Android Lint version 26.4.0, we can get the PSI tree representation of each node and its surroundings, not only in Java, but Kotlin as well. This makes it possible to use Android Lint for my formatting rule.
How the rule is implemented
First, I need to create my issue, where I set the scope to JAVA_FILE, which means both Java and Kotlin in Android Lint currently. (Here is where we can set other kinds of files as well such as XML or Gradle files, because Android Lint can check them as well.)
Then, in my detector class, I enumerate the nodes I’m interested in inside getApplicableUastTypes function, so Android Lint will know that it should call the related overridden visit methods such as visitForEachExpression. In each visit method, I just call my checker method.
In my checker method, the forward parameter describes if I want to check the line break before or after the “block statement”. In the method body, I investigate the line break number of whitespaces around the block statement I’m visiting. For that, I do three main steps:
- First, by firstBlockLevelNode method, I check what’s the first block level node, around which I want to check the whitespaces, because if I use an if block for assigning a value to a variable like this,
Lint would investigate the whitespace just before the if keyword, but I am interested in the whitespace before the val keyword. So, in this case, the first block level statement is the value assignment that wraps my if statement.
- Second, in the firstRelevantWhiteSpaceNode method, I check what the first relevant whitespace node is, where we should count the line breaks. Sometimes there is no relevant whitespace to check, because if my block is at the beginning or end of a method or any other blocks, then that’s fine and I can forego further investigation. See:
At this point I’m already using the PSI nodes, because I want to check whitespace information that is not provided in UAST. For that, I need to get the sourcePsi property of the block level UAST node.
An edge case is if there’s a comment just above my block statement. Here, I want to check the whitespace above the comment, so the first relevant whitespace is above the comment statement.
- Finally, I count the number of line breaks in the relevant whitespace; If it’s not bigger than 1, I report an issue.
As a result, I expect the following warnings in the IDE, so it’ll be clear for each developer that an additional line has to be added. I also can get these warnings in the Lint report if I run lint gradle task. Furthermore, I can even raise the severity of the issue to error, if I want to block the merge request job.
After integrating the custom rule, we don’t have to focus on this frustrating issue anymore; instead, we can spend our brain power on finding more complex problems when we review each other’s code. Even better, we can 100% guarantee that this mistake won’t get in our codebase accidentally, because we can configure our MR job to fail when someone misses to fix this issue.
Though Android Lint isn’t primarily for formatting rules, in my case it was quite practical, because it can be used for both Java and Kotlin: no double rule writing, maintaining and integration needed.
On the other hand, we have to notice that this rule is a very simple one in that I only have to check whitespaces and curly braces on PSI level, which are the same in the two languages. That’s why I don’t have to write any language specific code. However, if I should write some language specific code yet (eg. handling Elvis operator in Kotlin or Ternary operator in Java), I would still consider to prefer Android Lint against writing one-one ktlint and a Checkstyle rules, because I would still have much less work probably.
If you liked the first part of the article, please check the second part as well, where I’ll detail the integration of my rule in Android (and non-Android) projects, how to fix the already existing issues in our codebase, and how we can unit test and debug our rules.
If you’re interested in my code more thoroughly, please use this Github link: https://github.com/team-supercharge/lint-checks.