AndroidPub
Published in

AndroidPub

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 StackOverflowErrordoesn’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:

Built-in generic Java inspection
Android specific inspection enforced by lint

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 your Detector class, e.g. MyDetector.class, and the Scope of the Detector. The Scope 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 reportmethod, 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 ForwardingAstVisitorwhich 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!

--

--

The (retired) Pub(lication) for Android & Tech, focused on Development

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store