From Punched Cards to Prompts
AndroidIntroduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
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.
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.
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[]
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.
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!
Introduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
Jetpack Compose is a declarative framework for building native Android UI recommended by Google. To simplify and accelerate UI development, the framework turns the...
Big Nerd Ranch is chock-full of incredibly talented people. Today, we’re starting a series, Tell Our BNR Story, where folks within our industry share...