Write custom Android/Kotlin linting rules like a Psi-chic!

Leverage Ktlint & PsiViewer to supercharge your team’s linting workflow.

Jason Low
ITNEXT
Published in
10 min readFeb 27, 2021

--

Table of Contents

Preface
Chicken, or Egg?
Diving into the matrix
Get Psi-ched
· Psi-Viewer
· The Fun Part (Implementation)
Hol’ Up a Minute
Pre-setup
Setup
· The Good Stuff
· Wrap Up
References

Preface

In my previous article, I talked about how to leverage the power of git hooks and Ktlint to bring sanity to your team’s linting workflow. In this article, I’ll show you how to improve upon that by leveraging on custom rule-sets in Ktlint. Yes! Custom linting rules!

Chicken, or Egg?

During my search for the how-tos in custom linting rules, I have come across quite a few articles talking about how to write more or less the same linting rules again and again.

Now, it’s arguable that I just suck at Googling, but the thing that really grind my gears is the fact that at the end of every article, I was still lost as to how I would actually start writing a custom rule.

Ok, cool, I wrote my first “no internal import” rule, great. But how do I use that knowledge to write other rules? To begin with, how does the linter know what it is looking at, and how would I know to tell it to search for the thing I want it to check? How is it that I can write rules, when I don’t even know how the it is structured?

For that, we need to understand how the code is actually written.

Diving into the matrix

We live in a remarkable time, where we have sophisticated IDEs that is able to perform code analysis on the fly and scream at us for missing a semi-colon or writing a really redundant expression, and code compilation takes minutes or even seconds (I’d have wasted countless days and punch cards in the old days with how careless I am).

Our programming languages have evolved alongside our tools, but like the grammar in the human language, there is a foundational structure to how programming languages are written, old or new.

This grammatical structure is how an IDE such as Android Studio knows when you missed a space at the start of a function closure. Specifically in the context of Android Studio and Kotlin/Java, an “interpreter” was developed by JetBrains to look at the pattern as to how code was written, and this “interpreter” was called the UAS (Universal Abstract Syntax Tree).

I won’t pretend to understand how any of it works, I’ll instead link the reference at the end of this article, but all you should know, is that this makes it possible to use a tool to identify common code structures. This tool is a plugin called PsiViewer.

Get Psi-ched

PSI (Program Structure Interface Tree) is built on top of the aforementioned UAS specifically for Android Lint, which gives even more details on how our code written in Android Studio is structured.

PsiViewer is just a convenient GUI plugin that makes navigating this interface structure less painful. There are other ways to see the tree structure without relying on an external plugin, but the output will be printed in terminal, and I’ll show you why that is a bad time in waiting.

It was a mind-blowing revelation when I first discovered this tool and made sense of it (I still don’t understand a great deal of it mind you), and just like Keanu Reeves in The Matrix, I was able to see how the world was formed.

So, let me stop my rambling, and let’s take a look at the tool itself, shall we?

Psi-Viewer

Nobody can be told what the matrix is, you have to see it for yourself.

To give a clearer picture of how to use the plugin, let me use two pictures to illustrate my points:

Image A
Image B

A visual hierarchy of the Psi-Viewer located to the right panel of Android Studio:

  1. The panel on the top right, represents the literal PSI structure of this file I so eloquently named “SomeRandomClass”.
  2. The panel on the bottom right, is the detailed breakdown of just that highlighted portion of the code, namely the class keyword.

So, in image A, my cursor was highlighting/hovering on class , and as you can see, there’s a whole lot of words in the Psi-Viewer panel on the right, but, the thing we care about most is in the bottom right panel, under the Property column, there is a field called elementType . In the case of image A, it is highlighted as a class , of which, it is, we are highlighting a class keyword declaration after all.

What is even more interesting, is that when we look at image B, when we highlight on the TODO comment, it tells it that the elementType is an EOL_COMMENT . This shows us the entire blueprint of how the code is structured, and how we can identify the types of code there are, same way we identify Enums.

“So? How does that help us at all? What the hell Jason, this doesn’t tell us anything 😖”, well, I think this is the part of the article where we should get our hands dirty with code grease.

The Fun Part (Implementation)

Hol’ Up a Minute

The assumption here is that you already have an Android project up and running, with the main app module, along with the 2 build.gradle files; one for the app module, and the other for the entire project itself.

Also, I am writing this from the perspective of a Mac user, as well as implementing this on an ongoing project, so your mileage might vary.

Pre-setup

Download Android Studio plugin PsiViewer to assist in viewing the structure of the code. If you’re unsure how to download a plugin in Android Studio, merely press CMD + SHIFT + A on a mac (or CTRL + SHIFT + A if you’re on Windows) to open up the global action search bar, and type in “Plugins”, and search for it in the marketplace.

Setup

First, we need to include Ktlint as our primary linting engine (PsiViewer can be applied to other linting mechanisms too, but for the sake of this article, let’s just focus on Ktlint)

  • Create a separate module in your project root folder (it should live on the same level as your app module, and name it whatever you like. I named mine custom-ktlint-rules )
  • Configure the build.gradle file in this new module to be like below (very important to ensure we are using the pinterest version of ktlint)
plugins {
id "kotlin"
id "java-library"
id "maven"
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:0.33.0"
compileOnly "com.pinterest.ktlint:ktlint-core:0.33.0"
testImplementation "junit:junit:4.12"
testImplementation "org.assertj:assertj-core:3.12.2"
testImplementation "com.pinterest.ktlint:ktlint-core:0.33.0"
testImplementation "com.pinterest.ktlint:ktlint-test:0.33.0"
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}

It is important to note that at the time of writing, this were the latest dependency version, and depending on the versions, there might be incompatibility issues, so keep that in mind should errors arise.

  • Next, in the custom module (the custom-ktlint-rules one we just created), create two classes that extends RuleSetProvider and Rule respectively. You should get something that looks like this (ignore the compilation error for now, we’ll get to that in a minute):
The third image is how your module directory should look like at this point.
  • Next, create a folder under the src->main directory of the custom module, and call it resources/META-INF/services , and in this directory, create a new file called com.pinterest.ktlint.core.RuleSetProvider . Now, inside this file, you need to reference to your custom RuleSetProvider classes, in this case, the CustomRuleSetProvider class we just created above.
  • Finally, go back to your app module’s build.gradle , and make sure you include ktlintRuleset project(‘:custom-ktlint-rules’) under the dependencies
  • That’s the setup process done!

Do also take note, you might have to include classpath("org.jlleitschuh.gradle:ktlint-gradle:9.3.0) into your root build.gradle ‘s dependencies block, as there are some compilation errors that might occur that I’ve yet to find to root cause for. It’s essentially the same as the Pinterest dependency from earlier, it’s just a fail-safe implementation.

The Good Stuff

Ok, thank you for sitting through all of that, it’s a lot, trust me, I know. But I swear things will start making more sense now. *coughs* ok, next:

  • Go back to your CustomRuleSetProvider class that we just wrote earlier, copy and paste the following code:
class CustomRuleSetProvider : RuleSetProvider {
override fun get() = RuleSet(
"custom-rule-set",
TodoCommentRule(),
)
}

So what this essentially does, is you are declaring this new rule-set with an id of custom-rule-set (or whatever you wanna call it really), and also declaring what is the custom rule you want. In this case, it is our TodoCommentRule !

Which, I’ll paste the entire code implementation here now:

class TodoCommentRule : Rule("todo-comment") {
override fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (
offset: Int,
errorMessage: String,
canBeAutoCorrected: Boolean
) -> Unit
) {
if (node.elementType == EOL_COMMENT) {
val commentText = node.text
if (commentText.contains("TODO")) {
val keywordIndex = commentText.indexOf("TODO")

if (keywordIndex > 0) {
val keywordCountOffset = keywordIndex + "TODO".length
val noColonAfter = commentText[keywordCountOffset].toString() != ":"

if (noColonAfter) {
emit(
node.startOffset,
"TODO should have a ':' immediately after",
true
)
}
}
}
}
}
}

Yeesh, a whole block of poorly formatted code (thanks Medium), but, the only important things to look at are:

  • The id of todo-comment that is passed into the constructor of Rule to uniquely identify this rule.
  • Overriding the visit method so that the linter knows what to look out for based on your custom implementation.

So, remember Image A and Image B that I posted awhile back? See something familiar? Yes, the EOL_COMMENT .

To the more hawk-eyed ones among you, you’d probably realised that this custom rule is a rule to lint for violations in code comments, specifically for TODOs. More specifically, it checks if there are spaces after the : symbol, meaning comments like TODO:no space after the colon lollll would fail under this rule, but TODO: This is fine would fly.

Now, I know, I know, the algorithm to check for the violation is very badly written and doesn’t check for a plethora of cases, but, I just wanted something to work at the time, so, cut me some slack? And yes, I know, it’s not the sexiest custom rule, like, I can already hear people saying “Really? All this for a measly spacing after a TODO?”, but, in my defense, this article is about how to write custom rules, not how to write great custom rules. I’m sure you’re able to do that way better than I can 😉

Anyway, because Psi-Viewer already helped us with identifying what type a particular block of code is, we can then target that type of code via the overridden visit method’s node.elementType , and emit an error to my unsuspecting colleagues via the emit(node.startOffset, “TODO should have a ‘:’ immediately after”, true)

In my case, I’d even wrote a quick method to fix the violation, as I’ll show in this image below:

Wrap Up

This is a pretty long read, I know, and there is so much still to explore on this. But, if you managed to stay until this far, and you are feeling this sense of giddiness that I felt when I discovered how to do this, then maybe it was worth it.

I’m not naive, I know the effort needed for a team to invest their time into something as niche as custom linting rules is probably not worth it considering the potential return. But, if your team’s PR comments are being plagued by comments regarding stylistic choices, or, even more functional cases like “if there is a RecyclerView present in a fragment, the class name should have a ‘listing’ suffix”, this might be a silver bullet in your team’s tech stack.

After all, automation is hardly worthwhile upfront, but in the long-term, it’ll ensure the decisions made outlive the tenancy of the development team. The matrix is eternal after all.

If you’d like to check out my previous article where I talked about leveraging on git hooks to improve your team’s linting workflow, check it out here!

Otherwise, follow me on twitter for more of my thoughts on tech, development, and life’s musings :)

References

  1. Psi-Viewer and custom linting rules (Highly recommended read)
  2. Custom Ktlint rules with Moshi
  3. Writing your first ktlint rule

--

--

Writer for

https://twitter.com/jasonlowdh Full-time Android developer. I write about anything Android/software development, tech, and life’s musings.