
Write custom Android/Kotlin linting rules like a Psi-chic!
Leverage Ktlint & PsiViewer to supercharge your team’s linting workflow.
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:


A visual hierarchy of the Psi-Viewer located to the right panel of Android Studio:
- The panel on the top right, represents the literal PSI structure of this file I so eloquently named “SomeRandomClass”.
- 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 minecustom-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 ofktlint
)
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 extendsRuleSetProvider
andRule
respectively. You should get something that looks like this (ignore the compilation error for now, we’ll get to that in a minute):



- Next, create a folder under the
src->main
directory of the custom module, and call itresources/META-INF/services
, and in this directory, create a new file calledcom.pinterest.ktlint.core.RuleSetProvider
. Now, inside this file, you need to reference to your customRuleSetProvider
classes, in this case, theCustomRuleSetProvider
class we just created above.


- Finally, go back to your
app
module’sbuild.gradle
, and make sure you includektlintRuleset project(‘:custom-ktlint-rules’)
under thedependencies
- 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
oftodo-comment
that is passed into the constructor ofRule
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
- Psi-Viewer and custom linting rules (Highly recommended read)
- Custom Ktlint rules with Moshi
- Writing your first ktlint rule