Custom lint checks in Android | By Gopal

Gopal
Powerplay
Published in
7 min readNov 30, 2023

What is Lint

Lint in Android is a very powerful tool. The Android lint is a static code
analysis tool that checks your project source files for potential bugs and optimization improvements for correctness, security, performance, usability, accessibility, and internationalization.

The Android Lint API allows users to create custom lint checks. For example, if you are the author of an Android library project and your library project has certain usage requirements, you can write
additional lint rules to check that your library is used correctly, and then you can distribute those extra lint rules for users of the library. Similarly, you may have company-local rules you’d like to enforce.

Note: That while Android Lint has the name “Android” in it, it is no longer an Android-specific static analysis tool; it’s a general static analysis tool, and inside Google for example it is run to analyze server-side Java and Kotlin code.

Ways to use Lint

Let's dive into some ways to use Android Lint.

Using Android Studio

  1. On-the-fly: It is one of the easiest and simplest ways to see your lint on Android. It will show the error message as you start typing in Android Studio. For Eg: When you write the incorrect variable name or wrong ID in XML.

2. Inspect Code: This is an action in Android Studio, where we can run a batch analysis of our project. It's a great way to audit our project for common errors. Eg: There is an action called Run Inspection by Name which will check the whole project for the specific error we want.
To try: Just press Shift twice and enter the Run inspection name and then your inspection name.

Using the Command line

We can also run lint in the command line with gradle. For eg: ./gradlew lintDebug will also do the batch analysis for the enitre project and it will also create an HTML and XML report where we can see our errors.

Getting the most out of Android Lint

We are now going to talk about some ways to set up lint, use cases, and understand how lint works under the hood.
We will learn how to write custom lint rules in Android in Part 2, but I suggest reading and understanding Part 1 before reading Part 2. I will try to make it as simple as I can.

The topic we are going to cover in this Android lint series

Configuration

Android Studio comes with a great tool ( Inspection ) to configure your lint in Android. You can perform several tasks using this option in Android Studio. Let's explore one by one. Path to open Inspection tool in Android Studio.

Android Studio Setting > Editor > Inspection

Android Inspection option comes with around 400+ inbuild checks. Most of them are enabled by default. I would like you guys to take a look at the disabled lint because some of these might apply to your project. With this Inspection option, you can perform several tasks over lint such as.

  1. Change the Scope of the lint
  2. Change Severity of the lint
  3. Change the Highlighting option of your lint

Apart from these basic functions inspection tools come with. You can also add a new check from the inspection tool itself. Enable/Disable your lints and much more.

All you custom lint that you will be writing will appear here also.

Annotations

Annotations let you provide hints to code inspection tools, such as lint, to help detect more subtle code problems. Annotations are added as metadata tags that you attach to variables, parameters, and return values to inspect method return values, passed parameters, local variables, and fields. When used with code inspection tools, annotations can help you detect problems such as null pointer exceptions and resource type conflicts.

For Ex: Android resource IDs, for example, use an int to identify strings, graphics, colors, and other resource types, so inspection tools can't tell when you have specified a string resource where you should have specified a color resource.

Some Annotation examples are

  1. @NonNull & @Nullable These are specific to Java
  2. @RequirePermission(Manifest.Permission.SET_WALLPAPER)
  3. @MainThread , @UiThread , @WorkerThread, etc.

Let's understand some above Thread annotation.

If we have to do any Network request, we do it in a background thread because if we do it in the UI thread, the UI thread will be blocked it will freeze the the UI. Users will be frustrated. We might not even see a crash report. It is also one of the causes of ANRs.

So to tackle these problems we have these thread annotations, which tell that this method should only be called on the background thread or UI thread. If we try to call any method that is annotated with @WorkerThread on UI thread, it will throw an error. For Ex:

So in the above example, we can clearly see that any method that is annotation @WorkerThread can not be called from any UI thread. So this annotation helps us to avoid such laggy apps.

Now you must be thinking that in real-world apps we usually don't call network requests directly. There are many layers between the UI and the network. Will it work in this scenario? See the below photo for clarity.

Android Lint can not directly find the error in this case. But what we can do is run an inspection by name to find wrong thread (Interprocedural) problems in the app. So after running this inspection, all wrong thread calls in our get identified.

Lint Internals

So the way Lint works is it starts out parsing the source file, like Java and Kotlin codes in our project. Then the Lint Wrap them around UAST ( Universal Abstract syntax tree )
This step is really important, because due to this UAST wrapping of our Java and Kotlin code, whenever we write Lint in our code. We have to write it only once and It works in both Java and Kotlin.

Now whenever we write any Lint Analysis that means we are directly operating in UAST. We write checks on source code, method calls we are interested in UAST.

Lint works on All types of files. We can write our custom lints on XML files like manifest, resources, layouts, or Gradle files.

Lint check doesn't simply mean that we are directly looking at any plain text ( java, kotlin code ), but we also have type information available for each file like file type, class hierarchy, etc. It is really important to prevent false positives and just write powerful lint checks.

How Android Lint and other static code analysis tools work

Originally, Android Lint was developed with the primary purpose of identifying issues specific to Android within Java code. To analyze Java code, Android Lint creates a Java-specific Abstract Syntax Tree (AST), a hierarchical/tree representation of the source code.

Similar static code analysis tools adopt language-specific ASTs for their analyses; for instance, Checkstyle for Java, ktlint for Kotlin. These tools navigate through the AST, identifying errors by examining its nodes and associated attributes.

Bonus Read

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 for Java. For this, they introduced the so-called Universal Abstract Syntax Tree 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.
Both UAST and AST give high-level details about the source code. They don’t contain information about whitespaces or braces, but it is usually enough for the Android-specific rules.

PSI Tree (Program Structure Interface Tree)

The PSI tree is built on the top of UAST for Android Lint by JetBrains. In the case of other linting tools, it is built on the language-specific AST. PSI contains more details about the structure of the code, such as whitespaces, braces, and other similar elements.

In Android Lint, PSI Expressions are included in the implementation of the UAST Expressions, which are the nodes of the UAST.

There is even a handy intelliJ plugin: PsiViewer that can ease the debugging of PSI tree-based code analysis rules.

We need both UAST and PSI Tree to create our linting formatting rule because UAST is great for filtering and visiting nodes that we are interested in for both languages at once, but as I mentioned it doesn’t provide formatting information, eg. whitespace information. UAST rather focuses on a higher abstraction level, so primarily it isn’t for creating formatting rules.

We will be writing custom lint checks for our project in Part 2 of this article. Where we will have practical examples with source code.
I hope you like Part 1 where we discussed some basics about custom lint check in Android.

Join me on LinkedIn, for a rollercoaster of insights, laughs, and maybe a meme or two.

--

--