New August 27th, 2015: There 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. 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): Here are the details from that video. Create the Custom Rules Project for Android StudioFirst, 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 DetectorWhen 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 {
@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.Create IssueThe 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:
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 RegistryThe 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 FileNow 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 $HOME . Just to clarify: Lint searches, in order, for the first of these locations that exists:
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:
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/ $ lint --show MyId MyId ---- Summary: My summary of the issue Priority: 6 / 10 Severity: Warning Category: Correctness My longer explanation of the 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 ServersIf 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 RulesThe 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 RulesThe 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. SourcesFor 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 InfoIf 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. |
Tips >