Tips‎ > ‎

Writing Custom Lint Rules

New August 27th, 2015There is now an up-to-date sample project which shows how to write a custom lint check -- including unit testing:
That codebase is a better foundation for writing a lint rule than the code included in the writeup below.


Lint comes preconfigured with around 100 checks as of ADT 20. However, it can also be extended with additional rules. For example, if you are the author of a library project, and your library project has certain usage requirements, you can write additional lint rules to check that your library is used correctly, and then you can distribute those extra lint rules for users of the library. Similarly, you may have company-local rules you'd like to enforce.

This article describes how to write custom lint rules.  First, see the following talk from Google I/O 2012, "What's New in Development Tools", for a short demo of how lint rules are written and used (jump to 55 minutes, 18 seconds, if the embedded player doesn't do it automatically):

What's New in Android Development Tools


Here are the details from that video.

Create the Custom Rules Project for Android Studio

First, you'll need to create a separate Java project to compile the lint rules. Note that this is a Java project, not an Android project, since this code will be running in Android Studio, IntelliJ, Eclipse, or from Gradle or the command line lint tool -- not on a device. 

The simplest way to do this is to just start with this sample project, unzipping it, and then changing the source code as necessary. You can open it in Android Studio or IntelliJ by pointing to the build.gradle project to import it:
It's a Gradle project which has all the dependencies set up to use the Lint APIs, and to build a .jar file with the right manifest entries.

Create the Custom Rules Project in Eclipse
If you're using Eclipse, create a plain Java project using a simple library:

Next, add the lint_api.jar file to the classpath of the project. This jar file contains the Lint APIs that rules implement. You can find lint_api.jar in the SDK install directory as tools/lib/lint_api.jar.

Create Detector

When you're implementing a "lint rule", you'll really be implementing a "detector", which can identify one or more different types of "issues". This separation lets a single detector identify different types of issues that are logically related, but may have different severities, descriptions, and that the user may want to independently suppress. For example, the manifest detector looks for separate issues like incorrect registration order, missing minSdkVersion declarations, etc. For more detailed information on how to write a lint check, see the Writing a Lint Check document. 

In our example, we'll assume that we have a custom view, and we want to make a lint rule which makes sure that all uses of the custom view defines a particular custom attribute.

Here's the full detector source code:

package googleio.demo;

import java.util.Collection;
import java.util.Collections;

import org.w3c.dom.Element;

import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;

public class MyDetector extends ResourceXmlDetector {
    public static final Issue ISSUE = Issue.create(
            "MyId",
            "My brief summary of the issue",
            "My longer explanation of the issue",
            Category.CORRECTNESS, 6, Severity.WARNING,
            new Implementation(MyDetector.class, Scope.RESOURCE_FILE_SCOPE));
    
    @Override
    public Collection<String> getApplicableElements() {
        return Collections.singletonList(
                "com.google.io.demo.MyCustomView");
    }

    @Override
    public void visitElement(XmlContext context, Element element) {
        if (!element.hasAttributeNS(
                "http://schemas.android.com/apk/res/com.google.io.demo",
                "exampleString")) {
            context.report(ISSUE, element, context.getLocation(element),
                    "Missing required attribute 'exampleString'");
        }
    }
}

First you can see that this detector extends ResourceXmlDetector. This is a detector intended for use with resource XML files such as layout and string resource declarations. There are other types of detectors, for example detectors to deal with Java source code or byte code.

The getApplicableElements() method returns a set of XML tags that this detector cares about. Here we just return the tag for our custom view. The lint infrastructure will now call the visitElement() method for each occurrence of the tag -- and only for occurrences of that tag. In the visitElement() method, we simply see whether the current element defines our custom attribute ("exampleString"), and if not, we report an error.

The visitElement method passes a context object. The context provides a lot of relevant context; for example, you can look up the corresponding project (and from there, ask for the minSdkVersion or targetSdkVersion), or you can look up the path to the XML file being analyzed, or you can (as is shown here) create a Location for the error. Here we are passing in the element, which will make the error point to the start of the element, but you can for example pass in an XML attribute object instead, which would point to a particular attribute.

Again, see the Writing a Lint Check document for more details. 

Create Issue

The first parameter to the report() method is ISSUE. This is a reference to the issue being reported by this detector. This is the static field we defined at the top of the class above:
    public static final Issue ISSUE = Issue.create(
            "MyId",
            "My brief summary of the issue",
            "My longer explanation of the issue",
            Category.CORRECTNESS, 6, Severity.WARNING,
            new Implementation(MyDetector.class, Scope.RESOURCE_FILE_SCOPE));


An issue has several different attributes, defined in this order above:
  • An id. This is a constant associated with the issue, which should be short and descriptive; this id is for example used in Java suppress annotations and XML suppress attributes to identify this issue.
  • A summary. This should be a brief (single line) summary of the issue, which for example is used in the Lint Options UI in Eclipse and elsewhere to briefly describe the issue.
  • An explanation. This is a longer explanation of the issue, which should explain to the lint user what the problem is. Typically a lint error message is brief (a single line), and there are times when it's difficult to explain a subtle issue fully in a single line error message, so the explanation is used to provide more context. The explanation is shown in the full HTML report created by the lint command line tool, it's shown in the Eclipse Lint window for the currently selected issue, etc.
  • A category. There are many predefined categories, and categories can be nested (such as Usability > Icons), and this helps users filter and sort issues, or via the command line to only run issues of a certain type. The most common categories are "correctness" and "performance", but also includes internationalization, accessibility, usability, etc.
  • A priority. Priorities are an integer between 1 and 10 inclusive, with 10 being the most important, and this is used to sort issues relative to each other.
  • A severity. An issue can have a default severity of fatal, error, warning, or ignore. Note that I said "default": users can override the severities used for an issue via a lint.xml file (more info). Fatal and error are both shown as "errors" in Eclipse, but Fatal issues are also run automatically (without user intervention) whenever a user attempts to export an APK in Eclipse, and if any of them find an error, then the export will abort. Rules with severity "ignore" are not run.
  • A detector class. This is simply pointing to the detector responsible for identifying the issue. This should point to our own class. Note that a detector is instantiated automatically on our behalf by lint. This is done for each run. Therefore, you don't have to worry about clearing out instance state in your detector after a run; in Eclipse (where lint may be run multiple times) a new detector will be created for each run. Therefore, your lint rule must have a public default constructor. (If you don't specify one, the compiler will automatically create one for you, which is the case above.)
  • A scope. This determines what types of files this issue applies to. Here we just state that we apply to XML files, but the unused resource issue has a scope which includes both Java and XML files.
You can also call additional methods on the issue to for example define an issue as disabled by default, or to set a "more info" URL.

We've created an instance of the issue. The builtin lint issues are all registered in the BuiltinIssueRegistry class. However, for custom rules, we need to provide our own registry. Each custom jar file provides its own issue registry, where each issue registry can contain one or more issues identified by the given jar file.

Here's our custom issue registry:

package googleio.demo;

import java.util.Arrays;
import java.util.List;

import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.detector.api.Issue;

public class MyIssueRegistry extends IssueRegistry {
    public MyIssueRegistry() {
    }

    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(
            MyDetector.ISSUE
        );
    }
}

The getIssues() method returns a list of issues provided by this registry. We could obviously use Collections.singletonList() in this specific case instead of Arrays.asList, but there would typically be more than one issue. Note that the registry must have a public default constructor, such that it can be instantiated by lint. 

Register Registry

The last thing we need to do is register the issue registry, such that it can be found by lint. We do that by editing the manifest file for the jar file.

If you're using the Gradle / Android Studio project, this is handled by Gradle; just open the build.gradle file and update the path to the registry in the jar entry as necessary.

In Eclipse, create a manifest file like this:
Manifest-Version: 1.0
Lint-Registry: googleio.demo.MyIssueRegistry
and then export a Jar file from your project where you use the above manifest file.  (I had some difficulties doing this with Eclipse; there are clearly options in the Export Jar dialog to do it, but when I looked in my exported .jar file and opened the manifest file, it didn't include my above line, so I created the jar file manually: jar cvfm customrule.jar META-INF/MANIFEST.MF googleio/).
NOTE: that the exporter in Eclipse expects a newline (\n) after the second line. So, make sure there is a blank line at the end, and then export directly from Eclipse should work properly.

Register Custom Jar File

Now we have our customrule.jar file. When lint runs, it will look for custom rule jar files in the ~/.android/lint/ folder, so we need to place it there:

$ mkdir ~/.android/lint/
$ cp customrule.jar ~/.android/lint/

(Somebody asked about this; Lint basically calls the general Android tools method to find the "tools settings directory", used for emulator snapshots, ddms configuration, etc. You can find the relevant code for that here: https://android.googlesource.com/platform/tools/base/+/master/common/src/main/java/com/android/prefs/AndroidLocation.java )

Search Locations

The location of the .android directory is typically the home directory; lint will search in $ANDROID_SDK_HOME, in ${user.home} (the Java property), and in $HOMEJust to clarify: Lint searches, in order, for the first of these locations that exists:
  • ANDROID_SDK_HOME    (system prop or environment variable)
  • user.home    (system prop)
  • HOME    (environment variable)
So, if ANDROID_SDK_HOME exists then user.home and HOME will be ignored! This may seem counter intuitive, but the reason for this is that ANDROID_SDK_HOME is not intended to be pointed to your SDK installation; the environment variable for that is ANDROID_SDK. More recently the code also looks for ANDROID_HOME.  So, ANDROID_SDK_HOME is really meant to be an alternative home directory location, used on build servers when you want to point to specific emulator AVDs etc for unit testing. This naming is very unfortunate, and has led to various bugs, but it's hard to change without breaking users already relying on the past documented behavior. For now, make sure you don't use the environment variable ANDROID_SDK_HOME to point to your SDK installation!

Following that, it looks for the sub-path /.android/lint/, i.e.
whichever one of these corresponds to the one and only location just found:
  • ANDROID_SDK_HOME/.android/lint/
  • user.home/.android/lint/
  • HOME/.android/lint/
and loads any JAR files it finds there.

Consequently, if you have Lint jar files in ~/.android/lint/ and ANDROID_SDK_HOME exists, your JAR files will not be read!
If you have Lint JAR files in ~/.android/lint/ and ANDROID_SDK_HOME does not exist, your JAR files will be read.
If ANDROID_SDK_HOME exists, put your Lint JAR files in ANDROID_SDK_HOME/.android/lint/

Running the Custom Check

Finally, let's run lint to make sure it knows about our rule:
$ lint --show MyId
MyId
----
Summary: My summary of the issue

Priority: 6 / 10
Severity: Warning
Category: Correctness

My longer explanation of the issue

If this does not work, troubleshoot it by making sure your jar file is in the right place, that it contains the right registration line in the manifest file, that the full package name and class name is correct, that it is instantiatable (public default constructor), and that it registers your issue. 

Finally, let's test the rule. Here's a sample layout file:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res/com.google.io.demo"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <com.google.io.demo.MyCustomView
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="#ccc"
        android:paddingBottom="40dp"
        android:paddingLeft="20dp" 
        app:exampleDimension="24sp"
        app:exampleColor="#ff0000"
        app:exampleDrawable="@drawable/io_logo"
                        
    />

</FrameLayout>

Now let's run lint on the project which contains the above layout (and with the --check MyId argument we'll only check for our issue):

$ lint --check MyId /demo/workspace/Demo

Scanning Demo: ...............
res/layout/sample_my_custom_view.xml:20: Warning: Missing required attribute 'exampleString' [MyId]
    <com.google.io.demo.MyCustomView
    ^
0 errors, 1 warnings

Using Jenkins / Other Build Servers

If you're running lint as part of a continuous build, instead of placing the custom lint rules in the home directory of the account used by the build server, you can also use the environment variable $ANDROID_LINT_JARS to point to a path of jar files (separated with File.pathSeparator, e.g. ; on Windows and : everywhere else) containing the lint rules to be used. For more info about using Jenkins+Lint, see https://wiki.jenkins-ci.org/display/JENKINS/Android+Lint+Plugin.

More Complex Rules

The above check was extremely simple. A lot more is possible. A really useful resource to figure out how to do more is to look at the existing 100 or so issues. Look in the SDK git repository, at sdk/lint/, and in particular sdk/lint/libs/lint_checks/src/com/android/tools/lint/checks/.

You should also read this document: Writing a Lint Check which contains more background information on how lint works, how the different detectors should implemented, etc.

Contributing Rules

The custom lint rule support in lint is primarily intended for writing truly local rules. If you've thought of some lint check of general interest, you can use the above approach to develop and check the rule. However, please consider contributing the rule to AOSP such that all Android developers can benefit from the check! See the Contributing page for details.

Sources

For Gradle/Android Studio/IntelliJ, use this sample project, and for Eclipse, use this .zip of the above custom lint rules project and copy lint_api.jar into the root of the project before opening it in Eclipse.

More Info

If you have a Google+ account, you can comment or ask questions on this post here: https://plus.google.com/u/0/116539451797396019960/posts/iyH3ER3LJF7
You can also write to adt-dev (the Google Group) for more info.

ċ
CustomLintRule.zip
(92k)
Tor Norbye,
Jul 18, 2012, 1:02 PM
ċ
customlint.zip
(5k)
Tor Norbye,
Apr 28, 2015, 7:39 AM
Comments