Creating Android Lint Checks: A Survival Guide

September 12, 2023 , , ,

At Wealthfront, we’ve observed that the cost of resolving an issue becomes exponentially higher the later it is discovered. For example, a defect discovered at design time is far easier to resolve than one which has already reached our clients. And between those extremes, we can discover issues at code authoring, compilation, or automated test time. 

The positive relationship between an issue's resolution cost and the stage of discovery, from Design, Development, Testing, and Production

Static analysis tools like Android Lint allow us to scan our code for issues at coding time, but before that code even reaches the compiler. Android Lint supports checking Java/Kotlin code as well as Android-specific sources like string resources and layouts. It’s also extensible – and after writing three dozen of our own Android Lint checks, we’d like to share some of the best practices (and “gotchas”!) that we’ve acquired along the way.

Do you need Android Lint?

Before implementing a solution in static analysis, you should recall the relationship between development stage and issue cost, and ask whether your problem could instead be solved by revisiting an earlier stage.

Also, Android Lint is just one of many static analysis tools available to Android engineers. For enforcing idiomatic styles and catching common Kotlin-level issues, consider detekt or ktlint. These tools offer support for custom Rules, so your usage of Android Lint should arguably be confined to Android-specific concerns. However, you may still decide to employ Android Lint for all of your custom check needs, to avoid the overhead of learning and maintaining multiple frameworks.

Understanding the Unified Abstract Syntax Tree API

When we include the Android Gradle plugin in our application, we get a :lint Gradle task. This checks our code for an extensive (and ever-increasing) set of general Android development issues. But for concerns specific to our app, we must create our own checks – or Issues, as they are called within Android Lint.

First, let’s understand the APIs available to us for creating custom Issues. Android Lint’s scanners traverse our code using Jetbrains’ Unified Abstract Syntax Tree (UAST). The UAST exposes our code’s structure – things like classes, expressions, and method calls – in a way that is agnostic across JVM languages. This is invaluable for Android codebases, which typically contain a mix of Java and Kotlin. 

The UAST is built on top of Program Structure Interfaces (PSIs), which are specific to each JVM language. While UAST APIs expose these underlying PSIs, using them naturally risks tying your Issue to a single JVM language, so be cautious.

Writing your own Issue with SourceCodeScanner

Built atop these low-level concepts are Android Lint’s own APIs for defining custom checks. This blog post will focus on SourceCodeScanner – an interface which allows us to define the applicable scope of our Issue, and how to execute it. You are not expected to implement all of SourceCodeScanner’s many methods. Instead, you must decide which of its several scoping and visitation methods apply to your rule.

Let’s look at an example SourceCodeScanner, which reminds developers that DateTime.toLocalDate is a lossy operation:

In this example, we’ve implemented getApplicableMethodNames and visitMethodCall. The former restricts which call sites visitMethodCall should be invoked on. visitMethodCall inspects the particulars of the call site, and reports our issue, if present.

Integrated Development Environment screenshot showing in-context highlighting of a detected Android Lint issue

SourceCodeScanner’s methods must be implemented in correct pairs. The best documentation of these scope/visit method pairings is the code comments of SourceCodeScanner. In this example, only getApplicableMethodNames, and not other scoping methods such as applicableSuperClasses or getApplicableUastTypes, will limit how often visitMethodCall is executed. Detector methods such as getApplicableCallOwners and getApplicableCallNames also won’t affect visitMethodCall.

By the way, it’s possible to implement multiple pairs – for example, both visitReference / getApplicableReferenceNames and visitMethod / getApplicableMethodNames – in a single Issue.

Testing your Issue with TestLintTask

Setting up a test harness for our Issues allows us to develop them more quickly compared to running them against our app code. However, the TestLintTaskAPI has quite a few pitfalls to be aware of.

In the simplest case, we can write a test by defining which Issue(s) we’d like to run, and providing sample source code. Here is an example which performs some BigDecimal operations:

Providing dependencies to Lint tests

Let’s return to our toLocalDate example above. Since our Issue depends on library code, we can provide the necessary binary artifacts:

Or, we can write our own stubs for those same dependencies:

I generally recommend using stubs over artifacts as the latter forces you to manage transitive dependencies manually. In the above example, you’d need to provide all of Joda-Time’s dependencies alongside Joda-Time in order for compilation to succeed. Be sure that your stubs mimic their bases accurately – especially when Issues depend on implementation details like package or class name.

You can customize the strictness of TestLintTask source compilation with calls like allowCompilationErrors and allowSystemErrors. These can be useful if you’d prefer to use binary resources but are blocked by the dependency challenges that come with them.

Specifying test file paths

If you are using the JavaContext.isTestSource method, be aware that its value is determined by your sources’ file path. Use an expression like this to define a source with a leading “test/”, rather than the default of “src/”: TestFile.KotlinTestFile.create(“test/foo/BaseTest.kt”, “…”)

General advice and best practices

If you are unable to use targeted SourceCodeScanner methods, such as visitMethod, consider using the getApplicableUastTypes / createUastHandler pair. This general-purpose approach is more verbose but is also more powerful, and can overcome many of the visit methods’ frustrations.

Speaking of, pairing a debugger with createUastHandler is a good way to learn more about the UAST and its various UElements. Try placing breakpoints inside your UElementHandler and triggering them via your Issue’s unit tests – you’ll have a framework for exploring how code is interpreted as UElements.

My final recommendation is to start small, and proceed with small steps, when creating and testing Android Lint rules. There are so many variables in play – scoping methods, Issue scoping, testing pitfalls – that it is wise to tackle them incrementally. For this reason, existing custom Issue code can serve as a great starting point for new ones. 

If you ever find yourself completely stuck getting your Issue to trigger, consider starting over with a new approach. You can also consult Google’s built-in checks, or custom Issue sets written by other teams.

Conclusion

Android Lint Issues allow us to prevent certain types of concerns from proliferating in our code base. While it’s always preferable to solve development problems earlier – at design or compilation time – good Android Lint rules prevent production issues and reduce the amount of testing required.

Whether you are a beginner or expert of Android Lint, I hope this post provides you with some useful tips and time-savers. And if you’re an advocate for automated solutions, like static analysis, check out our careers page!

Disclosures

The information contained in this communication is provided for general informational purposes only, and should not be construed as investment or tax advice. Nothing in this communication should be construed as a solicitation or offer, or recommendation, to buy or sell any security. Any links provided to other server sites are offered as a matter of convenience and are not intended to imply that Wealthfront Advisers or its affiliates endorses, sponsors, promotes and/or is affiliated with the owners of or participants in those sites, or endorses any information contained on those sites, unless expressly stated otherwise. All investing involves risk, including the possible loss of money you invest, and past performance does not guarantee future performance. Please see our Full Disclosure for important details. Wealthfront offers a free software-based financial advice engine that delivers automated financial planning tools to help users achieve better outcomes. Investment management and advisory services are provided by Wealthfront Advisers LLC, an SEC registered investment adviser, and brokerage related products are provided by Wealthfront Brokerage LLC, a member of FINRA/SIPC. Wealthfront, Wealthfront Advisers and Wealthfront Brokerage are wholly owned subsidiaries of FINRA/SIPC.

© 2023 Wealthfront Corporation. All rights reserved.