What’s better than the great set of unit tests? The great set of unit sets you don’t have to write! I’m not talking about magic, but the next best thing, i.e. static code analysis.

There are many forms of static code analysis. For Java code, the most basic checks are performed by the compiler – making sure you don’t try to pass String to a method expecting List<String> , you didn’t make a typo in a class name, you have implemented all required methods of an interface, etc. There are also specialized tools which perform more thorough checks.

Android Lint

In this post I’m going to focus on Android Lint which is shipped with Android SDK. You can perform a few dozens checks included in the SDK by running:

./gradlew lint

The most important checks are also automatically performed on the release build by the lintVitalRelease task. You can also change the default lint configuration with the following option in build.gradle :

android { lintOptions { lintConfig file ( "lint.xml" ) } }

One of the changes we applied is treating calls to APIs higher than minSdkVersion as fatal errors which should fail the release build:

<?xml version="1.0" encoding="UTF-8"?> < lint > < issue id = "NewApi" severity = "fatal" /> </ lint >

Custom checks

As awesome and extensive as they are, sometimes the built-in Lint checks are not enough. You might have project specific rules you want enforce, for example using base class for all Fragments and Activities, or using 3rd party libraries instead of standard Java classes. You might want to enforce stricter rules than the SDK defaults. For these cases you can write your own lint rules.

To demonstrate the absolute basics of writing your custom lint checks, we’ll implement a rule enforcing a sensible minSdkVersion .

The first step is defining the Issue class:

private static final int SUGGESTED_MIN_SDK_VERSION = 15 ; private static final int YEAR = Calendar.getInstance().get(Calendar.YEAR); public static final Issue ISSUE = Issue.create( "AncientMinSdk" , "Supporting ancient Android versions" , "It's " + YEAR + ", time to bump your minSdkVersion to " + SUGGESTED_MIN_SDK_VERSION, Category.CORRECTNESS, 10 , Severity.FATAL, new Implementation(AncientMinSdkDetector.class, EnumSet.noneOf(Scope.class)));

The last line points to a Detector subclass which will perform the actual checks. Depending on the scope parameter your Detector have to implement appropriate Scanner interfaces. Our lint check relies only on project properties and doesn’t need to scan any files, so our Detector doesn’t implement any additional interfaces:

public class AncientMinSdkDetector extends Detector { private static final int SUGGESTED_MIN_SDK_VERSION = 15 ; @Override public void afterCheckProject (Context context) { super .afterCheckProject(context); int minSdk = context.getProject().getMinSdk(); if (minSdk != - 1 && minSdk < SUGGESTED_MIN_SDK_VERSION) { context.report(ISSUE, null , "Ancient minSdkVersion detected" ); } } }

All issues have to be returned by an IssueRegistry subclass:

public class CustomIssueRegistry extends IssueRegistry { @Override public List<Issue> getIssues () { return Arrays.asList(AncientMinSdkDetector.ISSUE); } }

The custom lint rules are compiled to JARs, and the registry have to be registered in the JAR’s manifest:

jar { baseName 'com.getbase.lint' version '1.0' manifest { attributes 'Manifest-Version' : 1.0 attributes 'Lint-Registry' : 'com.getbase.lint.CustomIssueRegistry' } }

The output JAR should be placed in the ~/.android/lint/ directory. We’ll handle this with the following gradle task:

configurations { lintChecks } dependencies { lintChecks files(jar) } task install(type: Copy ) { from configurations .lintChecks into System.getProperty( "user.home" ) + '/.android/lint/' }

Let’s install our custom lint rule:

$ ./gradlew clean install :clean :compileJava :processResources UP-TO-DATE :classes :jar :install BUILD SUCCESSFUL

And test it on sample project:

$ ./gradlew lintVital :app:preBuild :app:preReleaseBuild … :app:compileReleaseJava :app:lintVitalRelease Error: Ancient minSdkVersion detected [AncientMinSdk] Explanation for issues of type "AncientMinSdk": It's 2015, time to bump your minSdkVersion to 15 1 errors, 0 warnings :app:lintVitalRelease FAILED

Caveats

The custom checks API is still in flux and the documentation is sorely lacking. It’s possible to write the unit tests for your lint rules, but you have write your own plumbing classes to get a basic testing framework and there is no way to debug your lint rule on production code. Configuring the lint rule to be used only for a specific project is possible but requires some gradle black magic.

Recap

Despite the issues listed above I think the custom lint rules are a good tool to have at your disposal. It works as a magic unit test that automatically covers every line you write, it can replace pre-release checklists or guidelines and help new team members adhere to your coding standards.

The source for the custom lint check from this post is available on github. In the next part I’ll dive into more complex Detectors validating Java code.