Writing custom lint rules and integrating them with Android Studio inspections, or @CarefulNow @DownWithThisSortOfThing
Last week I was working on a piece of code and ended up creating a convoluted, hard-to-debug infinite loop. I managed to fix the issue, but due to the nature of the code the possibility of the infinite loop arising again in the future remained. If a specific method was called at the wrong time it would cause a StackOverflowError. In the future if another developer was to work on the codebase (which could be much much larger at that point) they would need to be made aware of this issue. To accomplish this I noted the problem in the method’s JavaDoc, here is the code from the GitHub repository.
This may be enough, however due to the nature of the bug it is not obvious it arises from calling this method. The stacktrace produced by the StackOverflowError
is not helpful in pointing you to the source of the problem. This is because the StackOverflowError
doesn’t originate from any one particular method call, it’s just the result of too many method calls executed by the infinite loop. If a future developer cannot immediately understand which method is the source of the issue it may take them a long time to even find the JavaDoc which explains the problem.
At this point I decided to use the power of wishful thinking. Coding by wishful thinking is something I do quite often. It is a method of programming by which you work backwards from what you want to where you are. Following this methodology I annotated the culprit method with a new annotation called @CarefulNow
, which does not yet exist. This annotation indicates the caller of the method should be careful when invoking it and raises a warning inside the IDE at the call site. The only similar mechanism available in the standard API is the @Deprecated annotation, but this was not suitable as @Deprecated
implies you shouldn’t use the method at all, whereas I simply wanted to warn anyone calling the method to be careful.
Now this is what I had:
As you can see a compiler error occurs because the annotation does not yet exist, so the next thing to do is to create it. Creating an annotation in and of itself is simple. It is the same as declaring a class, only instead of writing public class
we write public @interface
(don’t ask me why it’s not public annotation
).
All this does however is fix the compiler error. An annotation does nothing in and of itself, it only serves to annotate your code. You need to write more code to search for the annotations and perform some action. The standard Java API provides you with only one way to make use of annotations, which is the annotation processor. This was not useful for my purposes as the standard annotation processing mechanisms from the API are more geared towards code generation, being used in conjunction with a code generation library like JavaPoet. The ButterKnife library is a good example of this use case. For those of you not using ButterKnife I would advise you to take a look at it, although it may replaced by data binding in the future.
After I realised annotation processing wasn’t the way to go, I decided to try and emulate the functionality of the @Nullable annotation (which I discussed in my last post). From what I understood the @Nullable
annotation check was enforced by the lint tool, which is a static code analysis tool developed by the Android team to help inspect your code. However lint is only a command line tool, it is further integrated with the IntelliJ inspections via the Android plugin for IntelliJ. If you have used Android Studio you should already be familiar with these inspections. While some of them are built into the IntelliJ IDE as standard, those which are Android specific are enforced by lint. Here are some examples of such inspections:
It is possible for developers to write their own custom lint rules which lint will enforce. As I have already mentioned lint rules are automatically converted to inspections, so I realised I could write my own lint rule to seek out methods annotated with @CarefulNow
and call attention to them via the inspections. This would be a far better solution then simply adding a note to the method’s JavaDoc because now any time a new developer calls the method in question they will immediately have there attention drawn to a useful inspection bubble at the call site warning them of the issue.
Writing a custom lint rule, or Wading through undocumented waters
Writing a custom lint rule is not a task for beginners, not because it’s difficult but because there is virtually no documentation on the subject. It takes some grit and persistence to figure out. The primary source of information on writing lint rules is in the source code of Android’s built-in lint rules. The source file I studied to write my lint rule was SupportAnnoationDetector.java, which is used to analyze the @Nullable
annotation (amongst other support annotations). A Detector is exactly what it sounds like, it is used to detect annotations (or anything else you’re looking for) in your project files, e.g. source files. After much trial and error this was my final, fully-functioning Detector
class.
The structure of the class isn’t necessarily optimal or very pretty because for my first Detector
I borrowed heavily from the SupportAnnotationsDetector
class and was afraid to move too far away from it, even if that meant redundant methods and unnecessary logic. For my first go I just wanted it to work, and it does! Here is an explanation of the different parts of the class.
The first thing you’ll notice in this class is the ISSUE
constant. This is where you define the metadata for your lint rule. There’s nothing too hard to understand about this, the arguments to the create
method are:
- ID - the id of the lint rule. This will appear in the inspection bubble.
- Short description - a brief, simple description of the lint rule (six words or less).
- Long description - a longer more detailed description of your lint rule.
- Category - The category of lint rule. Here are the available categories are:
- Priority - a number between one and ten describing how important this issue is.
- Severity - the severity of the issue. The available severities are:
- Implementation - The
Implementation
. This is an object who’s constructor takes two parameters, the class object of yourDetector
class, e.g.MyDetector.class
, and theScope
of theDetector
. TheScope
is important, it tells lint what kinds of files your detector should look through, e.g. Java source files, XML resource files, gradle build files etc. here are the valid scopes:
Next you’ll see I’ve declared the fully qualified name of the the annotation in a constant called CAREFUL_NOW_ANNOTATION
. This is important to do, simply because it will prevent you from making a mistake and mistyping it anywhere else in your code.
After that we have two static helper methods, the first checks to see if the annotation passed into it is the annotation we’re looking for, and if it is it calls the second method which raises the IntelliJ inspection in Android Studio. The key to raising the inspection is the report
method, which is called on the JavaContext
object. The report method takes several arguments, the ISSUE
constant, the node (this is the part of the code which the issue is being raised on, in this case it is any call site of the getHarvestOp()
method, I’ll be covering the topic of nodes in much more detail in my next post), the location of the node (this can be found with the getLocation(Node)
method which is part of the JavaContext
class), and the string which will appear in the inspection next to the issue ID. Here’s a preview :
As you can see, the inspection bubble contains the last argument of the report
method, and the first argument of the Issue.create
method.
The four overridden methods are also relatively straight forward. The only method you may want to look into/modify for your own purposes is getApplicableNodeTypes()
. As you can see I’m specifying methods and constructors as the parts of any source file I’m interested in. I could remove ConstructorInvocation.class
from the list of applicable nodes as the @CarefulNow
annotation is currently only applicable to methods, but it’s likely I’ll change it to be applicable to constructors in the future, so I’ll leave it there for convenience.
The last bit of meat is in the CallChecker
class, a subclass of ForwardingAstVisitor
which will search the source code for places where any method annotated with @CarefulNow
is called. (Note: Remember, we’re not looking for places where methods are declared and annotated with @CarefulNow
, but where such methods are called, because that’s where issues can arise, at the call site, not at the declaration).
The CallChecker
is all about, well checking calls. The visitMethodInvocation
and visitConstructorInvocation
methods are simply called when a method or constructor call site is found. Both those methods call the checkCall
method, which uses the filterRelevantAnnotations
method to find any method calls on methods annotated with @CarefulNow
, and in turn calls the checkMethodAnnotation
helper method from the top of the class, which in turn calls the report
method to raise the inspection in Android Studio.
I’m skipping over all the code related to traversing the source code because I’ll be covering that in my next post. The code traversal is accomplished with the lombok.ast library, a library which also has no documentation, hooray for best practices!
Wiring it all together
So far I’ve neglected to explain the nuts and bolts of where this code goes and what you need to do to compile it. I’ll cover what I’ve left out here.
Where do I put my detector class?
All code related to your custom lint rule goes into a Java module (not an Android module). To create a pure Java module inside a project click File > New > New Module. Select Java Library. Click Next, fill in the package name etc. and click Finish. All you source files go where you would expect, into src/main/java/[package name]
. The automatically generated build.gradle
file should look like this (using Android Studio 2.2.2):
Change it to this
Now create a new Java class called MyIssueRegistry
in your Java module’s package (whatever it’s called, there should only be one). This class should look like this
As you can see I have added the Issue
object CAREFUL_NOW_ISSUE
from my MyAnnoationDetector
class to the IssueRegistry
. This is just a bit of setup which makes your lint rule visible to the lint tool.
That’s it for the wiring! Only one step left!
Adding the lint rule to your project
All you need to do to add your lint rule to your project is declare a dependency on your Java module in your project’s build.gradle
like this:
And that’s it! A custom lint rule!