Upcoming and OnDemand Webinars View full list

Continuous Linting in Android

Jason Atwood

Matt Compton recently described how to create custom Lint detectors for Android. This is a great way to enforce your team’s code design patterns, but it can be a big undertaking.

For example, when we built our custom .jar file that included all of our custom Lint checks, we had to ensure that each member on our team built the .jar and added it to his or her local ~./android/lint directory. We also had to trust that each developer actually ran the Lint checks periodically as part of their normal development process. That’s a lot to ask, especially when we already have so much to think about. We found that the best way to enforce our custom Lint checks (as well as the built-in checks) is to run Lint as part of our continuous integration build process.

Running Lint on Travis

I should note that I’ll talk about running things on Travis CI, but because we are using Gradle, most of this should apply to Jenkins as well.

Let’s start by refining our build.gradle file to describe the behavior we want when running Lint. The Android Gradle plugin lets us specify a number of lint-related parameters. Here, we will choose to generate an HTML report, choose to abort (i.e. fail) the build when Lint detects an error, and also treat all warnings as errors.

apply plugin: 'com.android.application'

android {
   ...

   lintOptions {
       htmlReport true
       htmlOutput file("lint-report.html")
       abortOnError true
       warningsAsErrors true
   }
}

Now we can just add the “lint” task to our travis.yml file and run Lint alongside the rest of our build process.

language: android
jdk:
 - oraclejdk8
android:
 components:
...

script:
   - ./gradlew clean assembleDebug test lint

If Lint detects a single error (or warning, since we are treating warnings as errors), the entire Travis build will fail just as if we had a failing test. This is great if we are starting with a brand-new Android project because it will keep our Lint error and warning count at zero.

But what about an existing project? We can’t just fix every existing Lint warning and error, then turn on Lint checking. We need a way to benefit from Lint on a project with existing errors and warnings.

Running a Subset of Lint Checks

When we ran Lint before, it performed all 200 built-in Lint checks, but we can also ask Lint to run only a subset of checks. Then we can fix all existing instances of a specific Issue and add a Lint check just for that Issue. This will ensure that we don’t add any violations in the future.

We can utilize the check attribute in our lintOptions.

apply plugin: 'com.android.application'

android {
   ...

   lintOptions {
       htmlReport true
       htmlOutput file("lint-report.html")
       warningsAsErrors true
       abortOnError true
       check [IDs of Issues to run]
   }
}

This will ignore all Lint checks except the ones listed. As you might guess, this list will get pretty long and bloat your build.gradle file as you check more Issues. You can clean that up by extracting those IDs to a different Gradle file.

So let’s update our build.gradle file:

apply plugin: 'com.android.application'
apply from: 'lint-checks.gradle'

android {
   ...

   lintOptions {
       htmlReport true
       htmlOutput file("lint-report.html")
       warningsAsErrors true
       abortOnError true
       check lintchecks
   }
}

We can then list our Lint checks in lint-checks.gradle:

ext.lintchecks = [
       'ExportedReceiver',
       'UnusedResources',
       'GradleDeprecated',
       'OldTargetApi',
       'ShowToast',
       ...
] as String[]

Running Our Custom Lint Checks

We haven’t yet added our custom Lint checks to the CI server, so let’s do that now. Using your favorite version control tool, include the custom Lint .jar in the project repo. I’m going to name and locate our .jar as [PROJECT_ROOT]/lint_rules/lint.jar. To let Travis know about this .jar, we have to set the ANDROID_LINT_JARS environment variable.

We can reduce clutter in the build.gradle file by running Lint from within a shell script. The first thing we need to do is call that shell script from the travis.yml file:

language: android
jdk:
 - oraclejdk8
android:
 components:
...
script:
   - ./gradlew clean assembleDebug test
   - ./scripts/lint_script.sh

We can then set the environment variable and call the Lint Gradle task. Inside lint_script.sh:

# file name and relative path to custom lint rules
CUSTOM_LINT_FILE="lint_rules/lint.jar"

# set directory of custom lint .jar
export ANDROID_LINT_JARS=$(pwd)/$CUSTOM_LINT_FILE

# run lint
./gradlew clean lint

That’s all there is to it! Now all members of our team will have our custom Lint rules applied run when they trigger a Travis build. There’s no need for each team member to add the .jar file to ~./android/lint on a local machine and run Lint locally.

A Better Way to Reduce Lint Errors

Let’s be honest: This still takes a lot of time. We tried to reduce the number of existing Lint Issues on a project by running only a subset of Lint checks, but we found that we were still asking a lot from our team. Somebody had to take the time to fix all occurrences of a specific Issue and then add that Issue to the check list.

This isn’t very realistic. It would be nice if we could set the existing error (or warning) count as a threshold and bar team members from exceeding that threshold. It would be even nicer if the threshold would go down when somebody took the time to fix Lint errors or warnings. Luckily, I’ve written a script that does just that.

The script fits into an existing travis.yml file the same way we extracted Lint checks to lint_script.sh. The script will be run at every build, or wherever you choose to trigger it.

When you first add the script to a project, it will run all Lint checks, including your custom checks, and establish a “baseline” error or warning count. You will then be unable to exceed this count in future pull requests (or master merges, or wherever you call this script). Any developer who submits code that increases the error or warning count will cause the Travis build to fail. The Travis console will output:

$ ./scripts/lint-up.sh
======= starting Lint script ========
running Lint…
Ran lint on variant defaultFlavorDebug: 8 issues found
Ran lint on variant defaultFlavorRelease: 8 issues found
Wrote HTML report to file:/home/travis/build/.../lint-report.html
Wrote XML report to /home/travis/build/.../lint-results.xml
BUILD SUCCESSFUL

Total time: 1 mins 33.394 secs
found errors: 8
found warnings: 0
previous errors: 3
previous warnings: 0
FAIL: error count increased
The command "./scripts/lint-up.sh" exited with 1.

If a developer takes time to reduce the count, then a new baseline will be established. In this way, the total error and warning count will trend toward zero.

This is a great way to get Lint running on your existing project, instead of saying, “Next time we’ll add Lint from the beginning.” You can check out the script on Github. Happy Linting!

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project