Lint‎ > ‎

Android Lint Overview



Android Lint is a static analysis tool for Android which looks for 225 (261 as of May 2016) different types of Android bugs. The potential bugs span a number of categories:

  • Correctness (120 checks as of lint version 1.4)

  • Performance (25 checks)

  • Security (22 checks)

  • Usability (21 checks)

  • Translations & Internationalization (19)

  • Accessibility (3)


The public usage documentation for Lint is here:

http://developer.android.com/tools/debugging/improving-w-lint.html

Current State

Lint is integrated in a number of tools:

  • Android Studio and IntelliJ (details)

  • Eclipse (details)

  • The Android Gradle Plugin (details)

  • The Jenkins CI server (plugin details)

  • As a command line tool (“lint”)

    • Used to integrate it in Shipshape and Maven, and miscellaneous individual product builds at Google.

Non-Goals

  • Catching general Java programming bugs. These are already handled by existing code check tools, and they’re handled particularly well by IntelliJ’s code inspections, so performing the same checks in lint would just end up creating double warnings for the same errors in the source editor. Therefore, Android Lint is focused on Android-specific problems.

Goals

  • Deeply integrated in the IDE: highlight potential errors right in the source editor

  • Integratable in many different types of tools, not just the IDE, and in particular, must be available as part of release build checks on continuous build servers

  • High performance: must be fast enough run all checks, all the time, without noticeably slowing down the IDE

  • Should support comprehensive and sophisticated checks (e.g. checks which require consulting multiple data sources, may require multiple passes over the data, etc)

  • Should flag any and all Android-related problems, not just problems in Java code; e.g. performance issues in layout XML files, spot inconsistencies between XML declarations and Java references, and so on.

  • Should allow users to change the severity of issues (boosting warnings to errors and vice versa)

  • Should allow users to deliberately suppress issues - either turning off checks completely, or only specific occurrences of a warning

  • Allow lint rules to be shipped with libraries in an automatic way, and have lint automatically pick these up and use them to check that the code is using the library correctly

    • This also implies that allowing custom rules is a goal for lint, so writing detectors should be as easy as possible, and lint’s testing infrastructure should also be available outside the SDK source tree

Whoops, That One Slipped Through

A classic problem for error checking tools is that they’re available, but that users don’t use them. Yes, they may spot a squiggly warning line in the editor if they happen to be looking at code in the same neighborhood. But there are some checks which aren’t highlighted on the fly and require the user to explicitly check for warnings (in IntelliJ, that action is Analyze > Inspect Code..). Furthermore, even if the user sees the warning, and intend to fix it, they may get distracted and forget.


Lint has a solution for this. While of course it’s integrated with Gradle in the same way that other static analysis tools are (findbugs, checkstyle, etc), and will generate reports if you run the “check” target, it goes one vital step further:


When you generate a release build, whether you ask for it or not, lint will be run too, in a special mode where it only looks for issues with “fatal” severity. This is usually a much smaller set of checks than a normal full lint run (currently 36 our 225 lint checks have severity fatal), so it’s faster than a normal lint run (and given that release builds often perform slow Proguard shrinking and obfuscation too, the additional overhead of fatal lint checking is minimal when compared to the overall build time.)


If any fatal issues are found during a release build, the release build is aborted! Yes, users can turn off this feature, but it’s on by default, and helps Android developers avoid shipping critical problems just because they forgot to run Inspect Code one more time after that last simple innocuous edit…

Examples

Here’s a (non-exhaustive) list of some of the checks lint performs:

  • It looks for typos in your resource files. Many IDEs have a “spell checking” feature where they highlight any words not found in the known dictionary, which is often missing programming terms (not so for Android Studio, I added a bunch recently). However, lint takes the opposite approach: it ships with a database of known typo’s, such as “andriod” instead of “android”. I basically grabbed all the typo dictionaries I could find on wikipedia -- which has typo dictionaries for many languages. So while lint doesn’t have a Turkish, Italian, German, Hungarian etc spelling check -- it does flag typos in all these languages.

  • When you have a button bar with Cancel and another one with an action verb -- do you remember which side the Cancel button should be on to comply with UI guidelines? (Hint: It depends on the version of Android you’re running.) Lint checks. Oh and makes sure you capitalize OK correctly. Ok?

  • Lint is doing its part to move the world to UTF-8. If it finds that you’re using any other encoding in your XML files, they’re flagged. As a fatal error. Which fails the build.

  • Lint looks for cut & paste errors, where you copy/paste code to initialize a series of fields with findViewById’s and forget to update one or more of the id’s. Yes, this has found bugs in production. Multiple times!

  • Similarly, when you look up a view with a findViewById call and cast the result to a specific view, lint looks at the actual usages of that id in layout files and checks whether the cast looks safe.

  • Lint looks at your icon files and makes sure for example that launcher icons have non-rectangular shapes, that notification icons only use shades of white and gray, that icons have consistent dip sizes across dpi folders, etc.

  • Performance checks make sure you avoid allocating objects in paint and measure calls, ensure that you’re not using Collections when you could be using the more efficient SparseArrays, etc.

  • There’s a huge list of additional checks; they are all listed and described here.

Architecture

The above goals have a number of implications for the architecture of lint:


  • To be natively integrated in multiple tools, it needs to have strong embedding API and go through it for all key services:

    • Example 1: Lint checks should never read files directly, they need to go through the lint embedding API. When lint is running in an IDE for example, the IDE can provide the current, possibly modified content of a buffer instead of the most recently saved version of a file.

    • Example 2: There’s at least one lint check which connects to a remote server (to check to see if the user is using an older version of a library when a newer one is available). The lint check shouldn’t create this HTTP connection directly; it must go via the embedding API. This allows for example lint running inside Android Studio to use the user’s configured network connection with proxy settings in the IDE.

  • To be embeddable not just in “the” IDE but in other tools like Gradle etc, means that lint cannot leverage IDE support. For example, it can’t use IntelliJ’s code index and parse trees.

    • Historical note: Lint was initially implemented as a command line tool as well as integrated with Eclipse. The fact that it wasn’t directly referencing Eclipse APIs anywhere made integrating it into IntelliJ and Android Studio much easier! And it now works in multiple IDEs, though that’s not a priority for the tools team anymore, since we’re pursuing a single-IDE strategy. But supporting lint from the command line (Gradle) continues to be a critical requirement.

  • To be able to run in the background while a user is editing a single file, it must support incremental checks. This is discussed later in the “Scopes” section (if you want to look ahead.)

  • To search for “all” Android bugs, lint cannot simply just scan Java files. It currently has special support for analyzing the following file types (and, crucially, lint checks are able to consider data from more than one of these sources):

    • Java source files, XML resource files and XML manifest files

    • Compiled bytecode. Vital when analyzing libraries where we may only have bytecode, not source files.

    • Bitmaps, like PNG files (sample check: launcher icons shouldn’t be rectangular)

    • ProGuard files (sample check: file contains bug included in older default template)

    • Property files (sample check: improperly escaped path on Windows)

    • Gradle files (sample check: dependency is obsolete, newer library exists)

    • Folders (sample check: suspicious language & region combination)

Performance, Performance, Performance

The top challenge for lint is to be highly performant, especially when running in the IDE.


This is addressed through a number of approaches:

  • Overall philosophy: Optimize for the case that there are no warnings in the code. The assumption is that if there’s a warning, the user will see it and deal with such that we don’t have to keep paying the same price over and over.

  • Try hard to do the minimum amount of work. Obviously, when the user is editing a single file, we should only be running checks in the current file*, and similarly we should only be running checks applicable to this file type. *: this is slightly complicated by cases where there are related files. For example, in Eclipse, lint inspections are run when the file is saved, and in that case we run lint checks incrementally on both the .java and .class files (there could be many, one for each inner class) corresponding to the just edited file.

  • There are some operations which can be handled faster in the IDE. Allow the IDE integration to

    • Swap in its own implementation of lint a given lint check. This is done for the API check in Android Studio for example. (2016: not anymore)

    • Provide faster implementations of certain lint services (such as resolving super classes), where the IDE (or other embedding tool) can leverage extra information it has such as persistent code index.

  • Lint allows lint checks to request another pass through the code. When this happens, it repeats the analysis with the subset of lint checks that required another pass. This allows many lint checks to run more leanly. For example, the unused resource detector keeps track of all the declarations and references of resources, and at the end figures out if there are any unused resource. If that’s the case, only then does it request another pass, and it’s in this second pass that it gathers the much more expensive information it needs to report unused resource errors (computing locations of resource declarations etc).

  • The biggest performance gains at the infrastructure level are gained from the smart visitors.  There are dedicated visitors for .java, .class and .xml files. The purpose of for example the XML visitor is that if there are let’s say 100 visitors that consider XML content, and the document is a strings.xml file with 500 strings in it (e.g. ~500 elements and ~500 attributes, one for each name attribute), we don’t iterate through this document 100 times, once for each detector, e.g. 50,000 element checks. Instead, the lint checks declare up front which specific tag names and/or attributes they care about. This allows lint to iterate through the XML document in a single pass. For each element and attribute, it only consults the lint checks that apply to the given tag or attribute. There are some complications here, and the situation for Java checks and bytecode checks are a bit more complicated in that there are more usage scenarios, but from a performance perspective, the tricks are the same: partition the work up front to allow just a few passes through the files.

  • In addition, there’s a lot of work on performance in the individual lint checks.

    • Take the API check for example. It needs to look up the API level for every single method and field reference in every Java file. The database is a 60,000 line XML file shipped with the platform-tools. To make this fast, it performs a one-time parse of the XML file and generates a compact binary database stored in its cache directory; on subsequent runs, it can just mmap in the database file and search it quickly.

    • Another example is the plurals check, which looks for likely errors in plurals usage, such as not including a quantity string in a language where that will likely lead to a grammar error. For this, there is a special PluralsDatabase that is generated from ICU data by a unit test if necessary whenever updating to a new ICU version.

Annotation-Based Checks

The support library now ships with a number of annotations which express additional metadata about code. These are all documented here: https://developer.android.com/studio/write/annotations.html.


These annotations let you for example indicate that

  • An int parameter must be one or more of a given specific set of constants, such as Gravity.LEFT or Gravity.RIGHT.

  • A resource integer of a particular type, such as a @drawable.

  • Called on the UI thread.

  • An integer that is in the range -10 to 10.

  • ...and so on.


These annotations are also available in the framework (in the android.annotation namespace instead, but hidden). The SDK extracts these annotations, converts them to the corresponding support annotation, and ships these in an extracted form both with the platform-tools (for command line lint usage) and with Android Studio (for IDE usage).


Lint has special support for checking for these annotations.


For every single method call, it finds the corresponding method and checks whether it, or any of its super classes, are annotated with a support annotation (either directly on that code, or in the external annotations database, as discussed above). This is also done for field references, and class references.


For each support annotation constraint it finds, it then perform analysis to discover whether the annotation constraint is met.

Typedef Annotations (@IntDef) and External Annotations

The @IntDef annotations check is one of the annotation based checks, but it has some interesting special considerations.


First, @IntDef allows developers (and the framework) to use a primitive integer instead of an enum to represent that a parameter must be one of a specific set of values, offering compile-time safety to the same extent that an enum does -- but without the same overhead. (Much has been made about the number of extra bytes required by an enum in the .dex file, but another consideration of enums is that the runtime class also needs to initialize all constants as objects.)


The main benefit of an @IntDef is that you can have two unrelated constants that have the same value, and while the compiler will happily let you pass in one instead of the other, the typdef annotation knows the specific allowed constants and will flag any other constants, whether the values are the same or not.


However, annotations in .class files cannot record “references to constants” in this way; they can only contain a limit set of specific types of data: primitives, Strings, Class instances and enums. Therefore, if we look at the .class file for a typedef enum, it only contains the actual values of the constants, not the references to the fields that contained the constants -- and with only the values, lint can’t actually do its job down the line.


Therefore, the typedef annotation needs to either be local to the project (such that lint can look at the AST node for the typedef annotation itself), or we need to record the data in a separate mechanism: an external file. That’s what lint does. When Gradle is compiling libraries (such as the support library), it runs a special “extract annotations” task, which looks for typedef annotations, and when it finds any, it records these annotations in a special file inside the AAR file.


Lint, when analyzing projects, will look at all the AAR files of the project’s dependencies, see if any contain the special external annotations file, and if so, include it in its annotations lookup described above.

Flow Analysis

Control Flow Analysis

Lint has multiple implementations of control flow analysis: one operating at the bytecode level, for the .class file based checks, and the other for Java AST based analysis.


One obvious use for control flow analysis in lint is the WakelockDetector, which looks at wakelock acquire and release calls, and makes sure there is no escape path via exceptions that can lead to the wakelock release being skipped.


However, the most “important” control flow check in lint is related to the API check, because it avoids a lot of false positives!


Lint’s API check looks at every method call and field reference, and makes sure that the call was not introduced in an API level higher than the minSdkVersion supported by the phone.


However, what if that code is surrounded by an explicit version check? In the below, we don’t want myLollipopCall() to be flagged as invalid, since clearly it can only be executed if we’re on Lollipop:


if (Build.VERSION.SDK_INT > LOLLIPOP) {

myLollipopCall();

}


Checking this is not as simple as just looking to see if a method call is surrounded by a simple if check like this (though usually that’s where the lint check is). It’s not even enough to look at all outer if statements. We also need to handle scenarios like this:

if (Build.VERSION.SDK_INT <= LOLLIPOP) {

            return;

}

myLollipopCall();


...and so on. Hence, lint will build up a control flow graph to analyze the control flow in these cases such that it can properly be silent about API “violations” that are found to be unreachable on older platforms.


2017: There's a new global call graph for use by lint checks such as the inter-procedural thread checker. This is still experimental.

Data Flow Analysis

Lint performs data flow analysis for a number of checks.


Just as for control flow, this is implemented in two separate ways: one for bytecode analysis, and one for AST based analysis.


Examples of AST-based data flow analysis:

  • If you create a TypedArray (via a call to obtainStyledArrays), lint tracks the references to the returned value (which can pass from one variable to another), and it makes sure that the array is eventually recycled. In fact it goes one step further: it also checks to see if you recycle it more than once, which is also not valid.

  • For the String format detector, it checks whether the formatting specifies specified in an XML resource string matches the argument types supplied to the String.format call. However, to do this, it needs to be able to figure out which resource string is used for formatting. It’s simple when the call is
    getResources().getString(R.string.hello), arg) myArgument)
    but we’re not always that lucky.

  • The new Intent and ContentProvider permission lint checks in 1.4 require data flow analysis to be able to map for example a ContentProvider URI with a call.


Examples of bytecode based data flow analysis:

  • The SecureRandomDetector which looks at calls to initialize the random number generator makes sure that the value passed into the constructor is not a fixed value


Constant Folding

Lint performs “constant folding” on the AST, including for symbols referenced from other compilation units. For example, if in one class you define

public static final String LOG_TAG_PREFIX = “MyReallyLongLogTag”;

and then from some other class you do this

public static final String LOG_TAG = OtherClass.LOG_TAG_PREFIX + “_Parser”;


Lint will flag a logging call using this tag as invalid, because the LOG_TAG is too long (the max allowed is 23 characters), and it computed this on the fly in the editor by performing constant evaluation:

Implementation Details

LintClient

Lint is highly embeddable; it’s been integrated natively in Android Studio/IntelliJ, Eclipse, the Android Gradle plugin, Jenkins, as a command line tool (“lint”) distributed with the SDK -- and this tool is itself used to integrate lint in tools like Tricorder and Maven.


LintClient is the abstraction which makes this possible. When lint needs to read a file, it doesn’t do that directly, it asks the LintClient to do it. When lint is running in an IDE for example, this lets the IDE return the current, possibly modified and unsaved changes of the given file, not whatever happens to be on disk, which is vital when lint is checking code while the user is typing.


When lint finds errors, it also reports these back to the LintClient. This lets for example an IDE highlight text ranges in a source file, whereas for something like the Gradle plugin, it can instead aggregate and sort the errors for a command line report, an HTML report, or an XML file to be analyzed by something like a Jenkins plugin.

Issues versus Detectors

A particular class of bug is called an Issue. An issue provides information about this class of bug, such as

  • An issue id. This is id is used to uniquely identify an error, in command line output, referencing the id in a @SuppressWarning annotation, and so on.

  • A brief summary of the issue

  • A full explanation of the problem (typically multiple paragraphs); the average explanation length is 360 characters

  • A category, such as security, or performance, or correctness, or usability, or internationalization, and so on

  • Default severity (fatal, error, warning, information).  This is “default” severity rather than just “severity” because users, and projects, can change the severity of issues.

  • Default priority. This is just used for sorting purposes in lint reports to list the most important issues first.

  • Optional additional links to point to more information (particularly useful for security bugs where they for example point to vulnerability disclosures)


Note that an issue is just data; it’s not the class responsible for finding that problem. An issue is found by a Detector. The advantage of separating out the process of looking for an issue from the issue itself is that a detector can often look for related issues at the same time, using the same underlying scan and data structures. As an example, the detector which analyzes formatting strings and reports mismatched parameter types with string formatting argument types, also finds formatting strings that should probably be using plurals instead. These two issues are completely unrelated from a bug perspective, and have different categories and severities, but they’re both identified by a single detector which has identified a given string in a resource file as used for formatting.


The issues are all aggregated in an IssueRegistry. Out of the box, there is one special issue registry: BuiltinIssueRegistry, which provides all the built-in lint issues, currently 225. However, lint also supports “custom lint rules”: additional lint rules that are not part of the base distribution. There are two primary use-cases for this:

  • Lint rules enforcing specific requirements of a given library. For example, the timber logging library provides a WrongTimberUsageDetector class which checks for 7 different types of issues around the usage of the library.

  • Company-local rules, not specific to a library.


When lint runs, it doesn’t just include the built-in rules; it also looks at all the libraries used by the project, transitively, and it finds the custom lint rules used by those libraries (which are included inside the AAR files, the Android ARchive files used with the Gradle build system for Android.).  Therefore, by simply depending on a library such as the timber logging library, your client code using the library will also be checked with timber’s custom lint rules, with no extra work on your part to enable it.


Lint will also look in some known locations (such as ~/.android/lint and $ANDROID_LINT_JARS) for additional custom rules. This can be used for company or personal lint rules across any project that is loaded.


To provide a custom rule you basically place your Detector class as well as an IssueRegistry which points to the issues into the .jar, and point to it with a manifest attribute.

At runtime, lint will find all the issue registries, it will use JarIssueRegistry to load issues from a jar file, and it will merge them all together with a CompositeIssueRegistry.

Rich Text

Lint’s messages (not just issue explanations, but the error messages themselves, etc) are all in a special “markdown-like” format, where you can surround characters with asterisks (*) to make words bold, with backticks (`) to format the text as a symbol, etc. Internally, this is referred to as TextFormat.RAW, but it can also be formatted to TextFormat.TEXT or TextFormat.HTML. It’s typically formatted to plain text in command line output, but in the IDE, it’s formatted to HTML, which allows the error message to look like this:


Here the hyperlink is highlighted as a link, and (though it’s hard to spot) the “contentDescription” attribute mentioned in the error message is highlighted with a code font instead of a normal text font.

Binding Issues and Detectors

Each issue is mapped to a specific detector. When lint rules, it checks which issues are enabled (which can be controlled for example by IDE metadata such as inspection settings, as well as project metadata, such as a Gradle lintOptions section, as well as by a config file named “lint.xml”).  From this it finds out which detectors need to be run. If none of the issues associated with a detector are enabled, that detector doesn’t have to run at all.


There is actually one extra level of indirection in the mapping from issues to detectors: an Implementation. When lint initializes the detectors to run, it asks an issue’s implementation for the associated detector. This indirection allows a particular tool integrating lint to substitute out the detector used to find a specific issue. For example, the API Check needs to resolve every single call and field reference, and it does this via bytecode analysis. However, when running inside IntelliJ, we swap out the implementation for a custom detector which can take advantage of certain persistent data structures to do the job much more efficiently.

Scopes & Analysis Scopes

A scope indicates what files are required for a given detector to do its job. For example, for a lint check which looks for performance problems in onDraw and onMeasure calls, the scope would simply be Scope.JAVA_FILE.


There are two purposes for scopes:

  1. Quickly determine what types of files a given detector applies to. This lets it partition up its set of detectors that it needs to look at for each file in the project as it’s working its way through. For example, less than half of the lint checks need to look at .xml resource files.

  2. Let the lint infrastructure figure out whether an issue can be run incrementally within the IDE while the user is editing.


For example, consider the Eclipse integration. When the user edits a file, and then saves it, Eclipse will compile the file immediately and create a corresponding .class file. When lint is integrated in Eclipse, like the other tools it operates on the save action and runs lint. It can now provide both the .java and the .class versions of this file, so it passes in the set of scopes [JAVA, CLASS]. Now lint knows that it can run all the lint checks that require either JAVA, or CLASS, or both.


However, just because a lint check applies to for example both XML resource files and Java

files, doesn’t mean that it needs to look at both in order to report errors. In some cases, it can independently check both. The PrivateResourcesDetector for example can independently look at R.type.name references in Java files and @type/name resource references in XML files and indepedently warn if it finds that a given resource usage is problematic.


To solve this, the Implementation (which maps issues to detectors and specifies the scope required for the detector to handle a given issue) can also specify “analysis scopes”; this is a list of scopes that the detector can independently handle.


The LintClient has several lookup methods which are useful for incremental operation. For example, it has a facility to look up resource references, which in the IDE is already a persistent data structure used for example for layout rendering, and with that facility, lint’s “wrong cast” detector, which makes sure that after a findViewById lookup the cast to a class is compatible with the types actually found for the id in the layout, can operate incrementally when the user is editing the Java class. As soon as lint sees the cast, it consults the resource repository, finds the associated places the ID is referenced in layouts and makes sure they’re compatible with the cast.


Currently, 188 out of lint’s 255 lint checks can run incrementally, when taking analysis scopes into account.

Projects

Lint uses the term “project” for what we now call a “module” in Android Studio; its usage is similar to the terminology in Eclipse and in gradle itself. A project can depend on other projects. When you depend on a library, that library is also considered a project by lint -- and lint will look at it (it has to, to for example correctly compute unused resources accurately). However, lint keeps track of whether it’s your code or upstream code you’re just depending on, and it won’t flag warnings in code that you’re not responsible for.


There are different types of projects: Android app projects, Android library projects, and non-Android projects (e.g. plain Java code; Guava would be an example of this type of library.) Many lint detectors make a distinction between these; for example, there’s a check to make sure you specify whether you want to allow automatic backups or not, but this is only required for an app project, not a library project.


A project can also provide a “subset”: a specific set of files (typically 1) to analyze. This is used when lint is run incrementally in the IDE; it still needs the full project context surrounding the file being edited (such that it can consult the minSdkVersion and targetSdkVersion in the manifest, dependencies and so on).

Finding Issues

A LintClient can perform lint by creating a LintDriver, and providing it with a LintRequest. The LintRequest basically identifies the root projects to analyze, the available scopes, and the lint client to use.


It then invokes the lint driver, which does all the heavy lifting in lint:

  • Figuring out which detectors apply, and partitioning them up by scope

  • Figuring out the project dependency graph

  • For each project running lint across the various types of detectors. For performance (speed and memory) reasons this has specialized handling for

    • XML resource files: a special ResourceVisitor is computed up front such that we can perform a single visit of the XML file and only consult detectors that apply to specific XML elements or attribute

    • Java files: a special JavaVisitor is used to allow detectors to only be invoked for certain parse tree types, or for certain call names, or when they are subclasses of certain classes, etc.

    • Class files: Special processing up front to skim class signatures be able to respond to inheritance queries by the lint rules, and to be able to associate inner classes with outer classes, since on disk these are separate .class files; then as with XML and Java a special AsmVisitor which attempts to perform a single visit through the ASM DOM.

  • Handling multiple passes, if requested by individual detectors


The individual detectors will report issues back to the driver, which performs extra checking, such as filtering out reported issues if they are suppressed via annotations, XML metadata, or comments in the source code, as well as ignoring reports for issues that are suppressed (which could be caused by a detector which reports multiple issues and is enabled because at least one issue is enabled, but reported a problem for a disabled issue).

Embedding Lint

As described earlier, the most important aspect of integrating lint is providing a custom implementation of LintClient. It provides a number of services used by lint:

  • Reporting an error: the lint client can show these in the IDE error, or add it to an Inspections window, or dump it out to the CLI, etc

  • Reading files: allows lint to handle not just files on disk, but files read from virtual file systems (e.g. inside Jar files), files that have not been saved yet

  • Providing parsers for XML and Java. This abstraction lets lint efficiently reuse the parsing data structures of the surrounding IDE (in Android Studio for example, the LintClient’s getParser() implementation is a class which maps the IDE’s native PSI data structure over to the generic AST abstraction used by lint.)  Similarly, when run from the command line, the command line runner’s getParser() implementation uses the Eclipse compiler, ECJ, and has a bridge which maps the ECJ parse tree into the same AST abstraction.

    • The parser APIs also provide lifecycle events around parsing units; this is important in IDE integration where in both Eclipse and IntelliJ, locks have to be grabbed and release around parse tree access.

  • SDK lookup methods (finding the SDK install configured for this project in the tool (IDE, Gradle etc)

  • etc.


The LintCliClient is a useful subclass of LintClient for command-line oriented integrations of lint; it handles aggregating and sorting errors, CLI progress reporting, flag parsing, etc.

Quick Fixes

Quick-fixes are automated refactoring operations, typically in the IDE, which can automatically fix the issue identified by lint. These come in two variants:

  • Fully automated. These can be applied en masse. For example, for unused resources, a quickfix can remove all unused resource.

  • Interactive. In some cases lint has identified a problem, and it partially knows how to fix it, but not fully. For example, when you are targeting API 23 you’re encouraged to set the android:fullBacupContent attribute. The quickfix can open the manifest file and add the attribute, but it can’t write the descriptor file content for you; that’s something you’ll need to do based on your application’s needs.


Note that quick fix operations are not implemented in Lint itself; these are typically handled on the IDE integration side, since as refactoring they typically integrate with the IDE’s native quickfix facility, utilize core IDE services like the ability to lock & modify documents, creating “Undoable” actions, etc.   There are currently quickfixes for 52 of lint’s checks in Android Studio.

Testing Infrastructure

Lint has comprehensive unit tests, and a good unit testing infrastructure. It also distributes its testing infrastructure as a separate Maven artifact your custom rules project can depend on for its tests.


Each unit tests builds up a test project by describing source files, and then asserting that the output should the report lint would emit when run on the command line.


In many of the older tests, the source files passed to lint were stored in separate files and then referenced from the test, but in newer tests I’m including the files inline instead. If you look on the command line this may not look great, but when opened in the IDE, IntelliJ can handle nested syntax highlighting, so it looks like this:



In this example, the XML source file, passed to the test is constructed with the xml(source, contents) call; there are others to construct Java source files, etc. For manifest files there are special facilities such that instead of inlining the whole manifest, you can just ask for a manifest with minSdkVersion=14 for example, or with a given set of permissions, and so on.


Creating source inputs this way also makes it easy to create synthetic tests. For example, I recently added a lint check to validate App Restrictions, and one of the things the lint check enforces is that the maximum number of nested children is 1000 (this matches the Play Store validator). I don’t have to create a large file with 1000 restrictions and check it into the source tree; I added a loop which added 1001 repetitions in the middle of the file, and then I could check that the lint output complained about the 1001’th element.

Lint 2.0

We’ve started making plans for lint 2.0 to clean up the APIs and address a number of issues (improving issue registration etc).


Comments