I’m a big fan of using autoÂmatÂed tools to improve the qualÂiÂty of code our teams write. Often this includes unit testÂing and code forÂmat checkÂers. But, for the purÂposÂes of this blog, I’m going to covÂer one kind of tool in parÂticÂuÂlar: linÂters. Though some tools conÂflate lintÂing with code style checkÂing, in realÂiÂty a linÂter is tool that anaÂlyzes code for potenÂtial bugs or undeÂsirÂable behavior.
(Quick note: While this post focusÂes specifÂiÂcalÂly on lintÂing Android source code, keep an eye out for future posts on lintÂing for othÂer frameÂworks and languages.)
ElimÂiÂnatÂing Bugs with Android Lint #
The folks on the Android DevelÂopÂer Tools team have givÂen us a great tool for lintÂing Android code, one that’s inteÂgratÂed directÂly into the build sysÂtem. In fact, you are already using this tool and probÂaÂbly don’t even know it! Some of the checks it includes run by default when you are editÂing Java or Kotlin source code or XML layÂout resources in a project. One examÂple is when you don’t use an @string
resource for user-visÂiÂble text in a layÂout resource file:
There is also a subÂset of the availÂable lint rules that check for issues which cause runÂtime crashÂes if not addressed, such as callÂing APIs that are unavailÂable on oldÂer platÂforms withÂout an SDK_INT
check or tryÂing to start an Activity
that isn’t regÂisÂtered in the AndroidManifest.xml
file. These ​“vital” checks run autoÂmatÂiÂcalÂly as part of a release build in the lintVitalRelease
graÂdle task.
It’s great getÂting all of these tools for free. Then again, humans are easÂiÂly disÂtractÂed and don’t always pay attenÂtion to approÂpriÂate warnÂings when editÂing source files. Wouldn’t it be nice if these checks ran autoÂmatÂiÂcalÂly every time we comÂmit changes to our source repos?
RunÂning Android Lint in CI #
At MichiÂganÂLabs, our Android projects run lint as part of the GitÂlab CI pipeline that is trigÂgered on every new comÂmit to our Git repo. We also retain the HTML and XML artiÂfacts from lint so the develÂopÂer doesn’t have to re-run the lint tool in order to see the forÂmatÂted results, not to menÂtion the lengthy error report:
Lint:
script:
- ./gradlew lintDebug
artifacts:
name: lint-$CI_COMMIT_SHA
paths:
- ./**/build/reports/lint-results-debug.html
- ./**/build/reports/lint-results-debug.xml
expire_in: 1 month
We also enable ​“Pipelines must sucÂceed” in the merge request checks secÂtion of the reposÂiÂtoÂry setÂtings to preÂvent merge requests with failÂing tests or lint checks from being merged.
CusÂtomizÂing Lint Checks #
By default, the linÂter will only fail if there are error
severÂiÂty levÂel issues found in your project. There are numerÂous othÂer checks that default to a levÂel of warning
(or below) which, if detectÂed, would ideÂalÂly fail a build. We can do this using a cusÂtomized lint conÂfigÂuÂraÂtion file. The lint.xml
file conÂtains a list of issues as well as the levÂel of severÂiÂty vioÂlaÂtions should be reportÂed at. To make the hardÂcodÂed text warnÂing into an error, simÂply insert <issue id="HardcodedText" severity="error" />
into the file.
Below is the lint.xml
file we curÂrentÂly use for our projects that should offer a baseÂline for your projects. The Android team mainÂtains a list of availÂable checks, also visÂiÂble in the Android StuÂdio prefÂerÂences diaÂlog under ​“EdiÂtor > InspecÂtions > Android > Lint”.
<lint>
<issue id="ObsoleteSdkInt" severity="error"/>
<issue id="PrivateResource" severity="error"/>
<issue id="UnusedIds" severity="ignore"/> <!-- does not catch uses of view IDs by Kotlin android extensions -->
<issue id="UnusedResources" severity="ignore" />
<issue id="GradleDynamicVersion" severity="error" />
<issue id="CheckResult" severity="error" />
<issue id="SimpleDateFormat" severity="error" />
<issue id="RtlEnabled" severity="error" />
<issue id="Registered" severity="error" />
<issue id="InnerclassSeparator" severity="error" />
<issue id="CommitPrefEdits" severity="error" />
<issue id="HardcodedText" severity="error" />
<issue id="UnusedAttribute" severity="error" />
<issue id="IconLocation" severity="error" />
<issue id="MergeRootFrame" severity="error"/>
<issue id="UseCompoundDrawables" severity="error"/>
<issue id="Autofill" severity="error" />
<issue id="LabelFor" severity="error" />
<!-- Change back to error when https://issuetracker.google.com/issues/110243369 is fixed -->
<issue id="SyntheticAccessor" severity="ignore" />
<issue id="MissingTranslation" severity="ignore" />
<issue id="ExtraTranslation" severity="ignore" />
<!-- No one cares. -->
<issue id="Overdraw" severity="ignore" />
<issue id="GoogleAppIndexingWarning" severity="ignore"/>
<issue id="Typos" severity="ignore" />
<!-- This is nice to have, but it makes your builds not repeatable in the future -->
<issue id="GradleDependency" severity="informational" />
</lint>
If the lint.xml
file appears in the root direcÂtoÂry of the project, it is applied autoÂmatÂiÂcalÂly. Our teams often go a step furÂther and add options that lead to more thorÂough checks, makÂing the outÂput more effecÂtive in CI logs. These options are conÂfigÂured in your module’s build.gradle
inside the android
section:
android {
lintOptions {
// Write a text report to the console (Useful for CI logs)
textReport true
textOutput 'stdout'
// HTML/XML reports always explain but this gets too verbose in console logs
explainIssues false
// Include the resources from all of its upstream modules in its analysis.
// Required to get accurate unused resource
checkDependencies true
// Also check test case code for lint issues
checkTestSources true
// We run a full lint analysis as build part in CI, so we can skip redundant checks.
checkReleaseBuilds false
}
// ...
}
GetÂting ExistÂing Projects Up To Speed #
You may find that adding this setÂup to your existÂing projects leads to hunÂdreds or even thouÂsands of errors. By baselinÂing the project, you can capÂture a snapÂshot of the existÂing errors and, more imporÂtantÂly, weed out new ones. This makes adding Android lint checks to your project virÂtuÂalÂly cost-free.
When editÂing files in Android StuÂdio, the baseÂline conÂfigÂuÂraÂtion is ignored, allowÂing you to idenÂtiÂfy and address old errors. My advice is to fix the lint errors in files that you are already touchÂing for othÂer reaÂsons, allowÂing you to address issues in the baseÂline file down the road.
ExtendÂing Lint Checks to OthÂer APIs #
Libraries, such as TimÂber and WorkÂManÂagÂer ship their own cusÂtom lint rules to avoid incorÂrect or inefÂfiÂcient usage of their APIs. The best part is that Android lint autoÂmatÂiÂcalÂly detects these new rules and applies them to your build. Note that you may still need to tweak the severÂiÂty setÂtings on the new rules so that, if vioÂlatÂed, your build will fail.
You can also proÂvide your own lint rules as the develÂopÂer of an Android library. I won’t covÂer that in this post, but Big Nerd Ranch offers insight on how to write your own cusÂtom lint checks.
TakÂing the Pain Out of the Dev Cycle #
Some of the issues that Android lint checks for can take a while to anaÂlyze (such as unused resources requirÂing checkDependencies
to be enabled); howÂevÂer, runÂning the checks in the backÂground after a comÂmit in CI is a good way to take the pain out of the local develÂopÂment cycle.
LookÂing ahead, the Android tools team has been workÂing on Project MarÂble, a way to improve the perÂforÂmance of the linÂter and othÂer build tools. We can’t wait.