360andev matthew compton fb

Linty Fresh: How to Create Custom Lint Checks

Have you ever wanted to write your own special Lint check? Or maybe you have wondered how Lint checks work? In this 360AnDev talk, come learn everything that you have ever wanted to know about Lint and more!


Introduction (00:00)

My name is Matt Compton, I am from Atlanta, and I work for a small startup named Luma. In this post, I’m going to be talking to you all about Lint.

I know you are all Android developers and you have probably been using lints this whole time. And maybe you have realized it that it’s really well integrated into the IDE. But my goal is to show you how to utilize Lint to the fullest extent of its abilities, so that you can make your own custom Lint checks for whatever purposes you might need.

What Is Lint? (01:20)

Lint is a static analysis tool built in the 1970s out of Bell Labs and it is been ported to every major programming language. I like to think of Lint as my detail checker. It helps me keep track of small, fine-grained minutia in my codebase that otherwise might have been lost.

In Android, we have all these large pieces that we have to keep track of. You have your view layer and model layer; you have to deal with fragments and activities, Arch, Java, etc. Sometimes you can get lost when it comes to small details. That is what Lint excels at, it helps you keep track of those little things.

A great example is contentDescription. Content descriptions (you are probably familiar with this Lint check) is saying you have an image view, or an image button, and you did not give any accessibility description along with it. Accessibility is important. This is one of those details that many developers might otherwise miss or gloss over, or maybe they relegated it to the back of their backlog.

Lint has a fairly tight integration with Android Studio. It will pop up, it will yell at you and be, you forgot this, take a look, maybe you should fix it, and sometimes you can even Option+Enter or the equivalent for Windows, and it will fix it for you automatically.

Custom Lint Check (02:04)

My goal in this talk is to show you how to make your own Lint checks. I built several for my projects, and I found that there is really two big use cases for writing your own Lint checks:

  1. If you are a library or SDK developer, bundling a Lint check with your library or SDK can be really helpful when a developer’s trying to use it. It can give them some guidance on how to use it. A great example is ButterKnife by Jake. It includes several Lint checks to make sure that you are using this library correctly.
  2. For domain logic or business logic or something specific to your company, or the project that you are working on. A great example is Captain Train; Jeremie Martinez from Captain Train always prefixes their custom XML attributes with CT. This way they always avoid any naming conflicts or resolution problems when they are using libraries. Anything that is related to your core business logic or maybe your project, anything that is repetitive or detail oriented, you can write a Lint check around that to automatically make sure that your team follows those guidelines.

I would not necessarily use it for code styling because other tools do that better, but those are two good use-cases.

The example we are going to follow through this talk focuses around Enums. I came up with this idea last year after Droidcon New York when the big buzz in the community was all about whether to use enums or not. I think enums matter, so what I am going to show you is how to create a Lint check to detect and scour your codebase for all enums, then yell at you when you use them.

On slide 6, you can see an example of a custom lint check. Android Studio spits out all these different problems: I have some obsolete Gradle dependencies, etc.

That last one will show up whenever you are using a custom Lint check. It will say you have got this special warning from some Lint check that is not part of the core library. For example, mine says, “Avoid Using Enums”. I do not know what Pet.java is, but I know it is an Enum and I do not like them.

Parts of a Lint Check (06:05)

The core library has around two hundred different issues that it can scour for, and it will look and alert you whenever they are used. These are the core parts of a Lint check:

  • Issue: this is what we are looking for, it is the problem to be solved, i.e. you forgot the content description, or you have nested linear layouts with weights.
  • Detector: this is how you are solving it. It’s the big piece of your Lint check, and it is where most of your core logic and programming will go into. The detector is your controller, and it is your equivalent of an activity or a fragment. It does the heavy lifting.
  • Implementation: this is where we are looking, and this can range from Java files to XML files, anything that can be included in your project. You can hook into Gradle files, and then just generally anything, including class files. Even if you want to look at compiled code, but that is much harder.
  • Registry: The final piece is simple, it is just a listing of all of your special Lint checks. The default Lint API has a built-in custom registry, which lists all the different Lint checks, part of Android. We are going to have to provide our own registry. Ours will be simple, since we are making this enum example, we will only have a registry filled with a single issue.

Issues are what we are looking for, detectors are how we find them, implementations are where we are looking, and registry is just a nice category listing of what we care about.

Lint Architecture (08:01)

On slide 8 we have a high-level overview of the architecture of how these pieces can fit together. These pieces can be reusable and modularized.

I can have multiple issues found by a single detector, my registry lists all of my issues, multiple issues can reuse the same implementation, and you can mix and match these as needed. If you create an implementation that is looking for Java files, then you can reuse that across multiple issues that also happen to be found in Java files.

Get more development news like this

Probably the most important piece is that detectors can look for multiple issues. That is great because we would not want one hundred detectors to all be searching through the same files over, and over, and over. Instead, we can have a single detector go through, say the Android Manifest and find multiple issues.

There is a lot that can go wrong in your Android Manifest: you could have a version mismatch, you could be missing an attribute, or maybe your build tools are out of date. We can have a single detector find multiple of those problems in a single pass.

Once we have built up our Lint check and generated it, it ends up being a JAR, and this JAR will contain your registry. It will contain your detector logic of how to find this issue and wherever you put this JAR hopefully will be included in your path. I just stick mine in the .android in my home directory in the Lint folder. It should be provided for you by default, and it will probably be empty unless you have done any Lint work before.

You could also put the JAR into continuous integration and have your entire team use it. We are going to go through the following steps:

  • We will start with updating our dependencies, which is where many great journeys in Android development begin
  • The next step will be our implementation because we have to figure out where we are looking before we determine what we are looking for.
  • Then we will create our issue
  • Our detector
  • Our registry
  • And finally, and I want to stress this, we want to test

Lint checks are really easy to test. We can provide a file that contains the error, another file that does not contain the error, and that is pretty much it.

We will run Lint on those files locally and then we can check the output. I highly recommend testing, and I am not a test-driven development advocate by any stretch, but this is actually a pretty great use case for it.

We will start by upgrading our dependencies, and we are going to use the com.android.tools.lint and the lint-api:


dependencies {
    ...
    compile 'com.android.tools.lint:lint-api:24.3.1'
}

I am using an older version of the API, 24.3.1. There are newer versions out there but Lint is constantly changing, it is a non-final API, and it hasn’t reached a point where it is finalized. You cannot count on the Lint API remaining static and remaining reliable, it is going to change, and it is going to break, and that is okay.

The core ideas of this talk will remain constant, the ideas of issues and detectors are still there, even though maybe the names or method calls or the slight details of the API might change, the central themes will remain constant throughout the API levels. But our code examples will be using this version.

We are going to start with our implementation. That is where we are searching, and we have a couple of different options that we can use. We have to define the class of our detector, which is something we will define later on in the slides, and then the scope that we care about:


private static final Class<? extends Detector> DETECTOR_CLASS = EnumDetector.class;
private static final EnumSet<Scope> DETECTOR_SCOPE = Scope.JAVA_FILE_SCOPE;

private static final Implementation IMPLEMENTATION = new Implementation(
        DETECTOR_CLASS,
        DETECTOR_SCOPE
);

This implementation is not only where we are looking for our issue, but it is also a bridge between our detector and our issue. Our detector, which will be doing the heavy lifting, will be connected to a particular issue and know where to look for it.

Here, since we are looking for Enums and we know that they will appear in Java classes, we are going to define our scope as Scope.JAVA_FILE_SCOPE. If we were looking for something in the Android Manifest, it would be an XML file scope. If we were looking at Gradle, it would be Gradle, and so on.

In sum, the implementation is just this bridge between our detector and our issue, and it is holding onto where we should look for our different issues.

Issue is the important piece, it is the problem that we are trying to solve, and it has a bunch of different fields that we can define:


private static final String ISSUE_ID = "Enum";
private static final String ISSUE_DESCRIPTION = "Avoid Using Enums";
private static final String ISSUE_EXPLANATION = "Never use enums!";
private static final Category ISSUE_CATEGORY = Category.PERFORMANCE;
private static final int ISSUE_PRIORITY = 5;
private static final Severity ISSUE_SEVERITY = Severity.WARNING;

  • The first one is the ID, this is how we uniquely identify our issue, and usually it is some camel-cased phrase. Mine is just “Enum” but typically you would give it a more descriptive ID. Later on, you can do a Lint –show with an ID, and you can learn all about the details via command line.
  • The description is that pop-up text, it is that short “this is what is going on.” For example: “You should add a content description to your image view,” or “You should avoid using enums.” It is a short, brief message to your developers to let them know what is going on.
  • The explanation is your longer, multi-paragraph explanation of why this is an issue. For brevity’s sake and for slides, I just say never use enums, but that should be a longer description to give the developer, which is your user, in this case, some context of why this is an issue.
  • Your category, of which there are many pre-defining categories, is an idea of what type of issue this is? Mine I say performance, because of the idea that enums maybe take up too much space, but you have other categories as well.
  • Priority is how important this issue is on a scale of one to ten. I say five because this is middle of the row, it is not a big deal.
  • Severity is perhaps the piece of Lint that you might have touched the most, the severity is how it interacts with your build process and your build tools. Here we say warning, as it will not fail your build, it is probably something you should fix, but go ahead and continue building and running your application.

We have this nice constructor, where we do Issue.create and we pass in all those fields we defined above, plus one extra.

As you can see, that implementation, we defined beforehand is also included. The implementation is that link between our issue and our detector when we define our detector, it will have access to these issues, and that is how we connect all the things.


public static final Issue ISSUE = Issue.create(
        ISSUE_ID,
        ISSUE_DESCRIPTION,
        ISSUE_EXPLANATION,
        ISSUE_CATEGORY,
        ISSUE_PRIORITY,
        ISSUE_SEVERITY,
        IMPLEMENTATION
);

Issue Severity Manipulation (16:34)

Issue severity, there is five of them:

  1. Ignore
  2. Informational
  3. Warning
  4. Error
  5. Fatal

Ignore means I do not care about this. I admit that it is an issue, but it is not important. That is probably what you have done to any Lint warning that annoyed you.

Hopefully, you have not done this to content descriptions, because those are important, but I know that is a common one you will see in XML and maybe you got annoyed by it, and you changed its severity from its normal warning and downgraded it to ignore. I do not recommend that, but every once in a while with Lint, you have a check that maybe is pervasive in your code, and you need to wait to change its severity.

Informational means it is a problem, but we still do not really care, but I want it to show up just I know for my sake as a developer. I do not want it to affect my build process at all, and I really just want to know about it. It is a step above ignore, which will silently go away and you will never see it again, and gives you some information and context as to what is going on with that issue.

Warning means, this is a problem, take a look, red text, red flag, I will not fail your build, but you should fix this, it is Lint shaking its hand at you angrily.

Error, we will fail your build. It will say, you can go no further. Stop here and fix this problem.

Fatal means that you actually cannot package your application, it will fail at the APK step. It will not let you build your final product. With error, it will still let you package your APK; it will just fail the build, but you could still package it if you chose. Fatal prevents that. Fatal is as severe as it gets because there is no farther blocker than not letting you package your application.

You can manipulate these severities, and that is an important part of Lint. Things ebb and flow in importance and maybe at the beginning of a project, you do not necessarily care about accessibility yet. By the end of the project, it is critical and you need to, and so you want to boost it.

Depending on the point of your project and what your timeline is and what your business priorities are, there are a couple of different ways you can change severities of Lint checks.

This first one is the in-line method; it is the idea that in our Java file, we can suppress Lint through annotations, and in XML we can use the tools namespace to ignore Lint checks if we need to, and you can see there are a few different ways:

Annotations:


@SuppressLint("InflateParams")
@SuppressLint({"InflateParams", "NewApi"})
@SuppressLint("all")

XML:


tools:ignore="RtlHardcoded"

We can either list a single Lint ID, which is what we do with InflateParams, or we could even list multiple. These are all the issue IDs for that Lint check. Which is why that ID is important, it is how we identify it.

The second way is in our build.Gradle:


app/build.gradle:
android {
  ...
  lintOptions {
    disable 'NewApi','RtlHardcoded'
  }
}

We can just say with our lintOptions tag, “I want to disable these Lint checks.” I know I am going to want them to run in my project, I no longer want to be notified about new API warnings, or worrying about right to left in coding.

And finally, you can also create your own special lint.xml file in your top level project directory, app/lint.xml, and you can list any issue, IDs, and give them your own custom severities:


app/lint.xml:
<?xml version="1.0" encoding="UTF-8"?>
<lint>
  <issue id="RedundantCast" severity="fatal"/>
</lint>

If I have a redundant cast, that is fatal, and I never want that in my project under any circumstances, and this is probably my favorite approach because it is a nice listing of everything that I am affecting in one place. I can list all the issues that I care about and their appropriate severities that I want to shift.

As opposed to the previous methods, which were either in-line or part of Gradle, which just allowed us to ignore the problem. This allows us to really change the severity at a whim.

Now we are going to move on to the detector, which is how we are going to find it.

Detector (21:00)

This is the heavy lifting of the Lint check, and it is going to be scouring through our code. In our example, we are going to be talking about Java code; we are going to be using a Java based detector mechanism, but this will vary pretty greatly depending on what type of file that you are trying to scour through, and we will see that in a moment.

Here is what our detector looks like at a simple level, I am going to call it EnumDetector because it is looking for enums, it extends the detector class as part of the Lint API, but most importantly, it implements a Scanner.

The scanners are interfaces that give you hooks into different slices of code:


public class EnumDetector extends Detector implements Detector.JavaScanner {

    /**
     * Constructs a new {@link EnumDetector} check
     */
    public EnumDetector() {
    }
    ...
}

With Java, we are going to use the JavaScanner, and it is going to be using the Lombok API for creating a tree of our Java code and then going through it step by step.

This scanner class will allow us to hook into different pieces of our code. If we want to find a Lint check that maybe, at the start of a comment, or at a field declaration, or the beginning of a method call, all of these can be represented through Lombok, all of them can be hooked into to our scanner interface.

We are in Java land right now, and that was Java specific, but the same is true with XML. We can use an XML.parser to turn our XML into something that can be represented, every piece of it, every open brace, every end tag, everything can be represented by the scanner and hooked into. The same goes for Gradle and class files.

The idea is that no matter what type of file you are looking through, Lint will have an appropriate way of converting it into an easy to hook into scanner interface. The first thing we do is create our public constructor, just an empty constructor, and the Lint API will be using that to spin up our detector for us. And this is what our code will end up parsing into.

AST - Abstract Syntax Trees (23:11)

Abstract syntax trees are just a way of representing our code in a different form. On slide 23, we have a tree graph. We are defining some variables x and y, creating a while loop that goes for a certain condition, and then writing out some variables.

We are looking at our code in a different way, instead of a very imperative line-by-line approach: we have converted it into a tree that we can hook into these slices of I need to check for my special Lint check where an enum is declared and find that exact piece, and then warn using our Lint check.

Again, if we are using XML or Gradle or class files, it would not necessarily be represented in the same way, it would have its own utilities, its own parsing mechanisms, but the idea is the same.

The first method and the first idea that we will look at with our detector is this appliesTo method:


public class EnumDetector extends Detector implements Detector.JavaScanner {
    ...
    @Override
    public boolean appliesTo(@NonNull Context context, @NonNull File file) {
        return true;
    }
    ...
}

The idea is trying to figure out if a file that has been provided to us actually matters for this detector. Here I just return true, the idea being I have defined my Java file scope with my implementation, every Java file we passed into this method.

You can see we have two parameters, we have context, and file, and just to be very clear, because it is ambiguous, that is not an Android context; that is a Lombok context, which is slightly different. It is more a representation of our tree and has nothing to do with Android.

Based on our context and our file that we are given, we can determine in this method either true or false if this file matters for what we are looking for.

In our case, we are looking for all enums; we are scouring through, and we want any Java files, because we have got to check if there is an enum inside of it. But if we knew more information, we could be more specific here. Maybe our Lint check was based on naming schemes.

We could actually just take the file handle and look at the name. We could determine based on that whether it actually should be checked or not by our detector, could do a file dot git name and then check if it ends with enum, for example. Or begins with it depending on what your naming scheme is. That is how we figure out whether a file matters to us, it is a way of narrowing down our scope even further.

With the implementation, we narrowed it to Java files; here we could narrow it even further to more specific Java files and the same would be true if it was XML or Gradle.

If we remember, our abstract syntax tree has parsed our Java code and turned it into this nice tree with all these different nodes. This is where we say what types of nodes we want to look for and any piece of our code can be represented here:


public class EnumDetector extends Detector implements Detector.JavaScanner {
    ...
    @Override
    public List<Class<? extends Node>> getApplicableNodeTypes() {
        return Arrays.<Class<? extends Node>>asList(
            EnumDeclaration.class

            );
    }

    ...
}

If we wanted to look at a comment declaration or the beginning of a field, we could find those, and those nodes will be represented, and now our detector will make sure to visit the inside of our Java file scope. Inside of the files, we have selected, and now even further narrowed down to these particular nodes it will go in and visit at that point.

Here, and for our example, we just care about enums being declared, and we are going to go into that enum declaration and node type and anywhere in our codebase that is Java files and has an enum declaration. We are going to end up visiting that piece and, as we will see in a few, triggering Lint in saying, this is a problem.

All of these are about filtering down our scope further until we have narrowed it down and found our issue, whatever it may be.

createJavaVisitor means we have narrowed it down to Java files, we have figured out the files we care about:


public class EnumDetector extends Detector implements Detector.JavaScanner {
    ...
    @Override
    public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
        return new EnumChecker(context);
    }
    ...
}

We have even narrowed it down to the particular nodes that we care about, for us being the enum being declared. This is where we visit that node, the visitor meaning we are going to go into that node and do some bit of work. The Java context is again, a Lombok context, it is a place in our Java tree.

The EnumChecker is a custom class I define next, and this Java visitor will take care of any work that we want to do while we are in that node. So for that brief period, where we have hit that node, we can check for whatever it is that we are looking for.

For us it is easy, we are just looking for enums being declared, it is tautological. We have found the enum declaration we can easily know, but with a more complex Lint check (i.e. nested layouts), your detector could be doing a lot of work. It might need to visit multiple nodes; it could even need to visit multiple files, and maybe even keep track of internal state.

This is why earlier I said your detector is your controller class, because it can keep internal state across even multiple passes through your entire application.

Your detector can be arbitrarily complex, though Lint will eventually cut you off and say, ten passes through the codebase is too far, we will limit you there to prevent infinite recursion.

Here we have our EnumChecker in your class that we have made, the idea being it is our visitor to our node and we are going to be doing some bit of work. All it needs to do is hold on to our Java context to hold onto where we are in our codebase, where we are in our Lombok tree:


public class EnumDetector extends Detector implements Detector.JavaScanner {
    ...
    private static class EnumChecker extends ForwardingAstVisitor {

        private final JavaContext mContext;

        public EnumChecker(JavaContext context) {
            mContext = context;
        }
        ...
}

The most important method is next: how we visit, where we are, this visitEnumDeclaration.

Since earlier we defined the node that we care about as an enum being declared, this is the only method that will override in this class, but our forwarding AST visitor has all sorts of methods that we could override depending on what we care about.

For us, it is enum declaration, but again, you can look for really any concept that can be represented in Java code, but have a corresponding method here that we could override.


public class EnumDetector extends Detector implements Detector.JavaScanner {
  ...
    private static class EnumChecker extends ForwardingAstVisitor {
        ...
        @Override
        public boolean visitEnumDeclaration(EnumDeclaration node) {
            mContext.report(ISSUE, Location.create(mContext.file), ISSUE.
getBriefDescription(TextFormat.TEXT));
            return super.visitEnumDeclaration(node);
        }
    }
}
          

With us, we have this visit enum declaration. Based on this context that we saved on the previous slide, that Java context that we held onto, based on where we are, I am going to use the report method.

The report method is you saying to Lint, I found it, I found the issue, here it is, and we are going to pass in the issue that we found, whatever it may be, a location for that issue. Which can be specific, here I am passing in just the file as a whole, I found it in this file, which is probably enough for looking for enums. But you can be more specific with that location by giving line numbers or even multiple line numbers if you chose.

We also give a brief description, which I just copy over from my issue description that we defined previously. We give the issue we found and a short description, and this is what Lint is using. This is the IDE integration; it will anytime you have a Lint check yell at you, this is what is happening under the hood. It is being reported in a specific context in your abstract syntax tree.

That was the detector, the most complex piece; it is scouring through your code, sometimes recursively, and it finds the problem that we are trying to solve. Now we will talk about the simplest piece, which is the registry.

Registry (31:04)

The registry is just a listing of all the issues that we have created. There is a default registry that is built into the Lint API and part of the Android Open Source Project that lists out some two hundred or issues, but we cannot hook into that since we are creating our custom Lint checks. We have to provide our own custom, special registry. As such, I just call it CustomIssueRegistry, and I extend from the Lint API’s issue registry class. This is how the Lint API will know what issues we have defined that are new, and what issues it should now start reporting with our special detectors. And what we will do here is a few method overrides, first being git issues, which will just be a list of issues for us. It is arbitrary, just a single one, but you could imagine having a long list of domain specific issues you have created for your company:


public class CustomIssueRegistry extends IssueRegistry` {
...
    private List<Issue> mIssues = Arrays.asList(EnumDetector.ISSUE);

    @Override
    public List<Issue> getIssues() {
        return mIssues;
    }
...
}

The next piece, in our Gradle file:


jar {
    baseName 'com.getluma.lint'
    version '1.0'

    manifest {
        attributes 'Manifest-Version': 1.0
        attributes('Lint-Registry': 'com.getluma.lint.CustomIssueRegistry')
    }
}

We have to define what our JAR: we need to provide some package name for our JAR, here I use com.getluma.lint because that is where I keep all my Lint checks for my company.

Give it a version name because it might change over time, and most importantly, we have this manifest block, and there we define our attributes for what our Lint registry is. That will be information packaged with your JAR that Lint can look at your JAR and know how to use it.

Without that, you just have a useless JAR file. It might have all your Lint checks and your detectors and all that, but Lint as a utility will have no idea what to do with it. With this Lint registry attribute, we are pointing to our custom issue registry that we have defined the Lint utility will find it and then have that listing of issues that we have created.

Testing (33:57)

Testing is an important step of hooking into the Lint utility. We have seen how to update our dependencies, creating an issue, detector, the implementation, and the registry - all those pieces together are how you create a Lint check.

But testing is an important part of this process because it is useful to have verification that your Lint check works as you believe it does, and it’s really easy.

We are going to have to add some more dependencies, some test resources, which will be our positive and negative test case files that we will be looking at:


dependencies {
    ...
    testCompile 'junit:junit:4.11'
    testCompile 'com.android.tools.lint:lint-tests:24.3.1'
    testCompile 'com.android.tools:testutils:24.3.1'
}

And test the registry as a sanity check of making sure it is what we expect:


public class CustomIssueRegistryTest {
    private CustomIssueRegistry mCustomIssueRegistry;
    @Before
    public void setUp() throws Exception {
        mCustomIssueRegistry = new CustomIssueRegistry();
    }

}


@Test
public void testNumberOfIssues() throws Exception {
    int size = mCustomIssueRegistry.getIssues().size();
    assertThat(size).isEqualTo(1);
}

@Test
public void testGetIssues() throws Exception {
    List<Issue> actual = mCustomIssueRegistry.getIssues();
    assertThat(actual).contains(EnumDetector.ISSUE);

Then we will test the detector public class EnumDetectorTest extends LintDetectorTest, which is the interesting part of making sure our Lint check behaves the way that we want it to:


public class EnumDetectorTest extends LintDetectorTest {
    ...
}

The last thing that we would want is a static analysis tool that tells us wrong information or flags incorrectly. I am using JUnit 4.11; you could update to 4.12, or use a different version. The Lint API provides us with this Lint test package, and we have their test utils that we will also be using to run Lint explicitly.

Testing Output (38:50)

The important part is creating your test cases. I usually keep mine in a resources folder inside of the test package. I create it on an issue by issue basis. I name it in my enum package since it is named after the ID for my Lint issue that I have created. I provide an empty test case, a case that will always just fail in the sense of the issue will not be found there. Then I provide a positive test case of the enum being there, and it should trigger my Lint check, and obviously you could have many, many more test cases as would be appropriate, but at the simplest level, we have a positive-negative case.

The first thing we will check is our registry, and testing it is pretty straight forward. We have to define our custom issue registry, we will instantiate it, and the next step will be to make sure it contains what we expect, which for us is two things.

We want to make sure it has the number of issues that we expect that we are not caught off guard if we only added one issue. We found four, that would be surprising, and we want to make sure that it has all the issues that we actually defined. It is a sanity check of did I create my registry correctly?

More interesting: it is our detector, which we have a nice handy Lint detector test provided by the Lint API that we can hook into, which will make it easier for us. I call the enum detector test, and we have a few methods to override.


@Override
protected Detector getDetector() {
    return new EnumDetector();
}

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

First is getDetector, which is the detector to be used for this test case, for us it is our new enum detector, and you would pass in whatever you want.

The second are the issues we want to test with that detector. For us, it is just going to be our single issue that we would find inside of our enum detector. If you had a more complex use case, your detector might have multiple issues associated with it, and you could split them out into their own classes.

Here I only have one issue and only one detector, so I bundled them all together inside of the detector.

To actually run our test case, we are going to have this test empty case:


public void testEmptyCase() throws Exception {
    String file = "EmptyTestCase.java";
    assertEquals(
        "No warnings.",
        lintFiles(file)
    );
}

We are testing the simple one that should have no Lint warnings associated with it, and I will define the file name, and then I will insert equals, and that no warnings is just the default message Lint gives whenever it does not find anything wrong with a file.

It is not often that I get to see no warnings told to me by Lint, but this is one of the rare occasions. The second parameter is that Lint files, taking in the file name. I actually run Lint as a utility on that particular file and then give you the text output. We are just checking that Lint being run on our particular test case file has no warnings.

For the empty case is easy, it is going to say no warnings if nothing is found, but the other case is a bit harder.

It is ugly, but we have to construct what that error message might look like:


public void testEnumCase() throws Exception {
    String file = "EnumTestCase.java";
    String warningMessage = file
            + ": Warning: "
            + EnumDetector.ISSUE.getBriefDescription(TextFormat.TEXT)
            + " ["
            + EnumDetector.ISSUE.getId()
            + "]\n"
            + "0 errors, 1 warnings\n";
    assertEquals(
        warningMessage,
        lintFiles(file)
    );
}

You do not really have any great way of knowing beforehand. You have to run it once, let it fail, and use that message for your test case: the general idea is the same. We have our file, and we have to construct our warning message instead of having that default no warning message, and then we still call Lint files onto that file name.

Here we are constructing it; again, some of these pieces we have defined elsewhere in our codebase, I know what they are and I will use them. Other pieces i.e. the braces and the new line and the warning, I had to run and figure it out. Sadly there is not a better way of doing that (that I know of).

If we want to run these, we just do good old, Gradle wrapper clean build test and it will give us some output hopefully, and be a build successful:


> ./gradlew clean build test
:compileJava
...
:test
:check
:build
BUILD SUCCESSFUL
Total time: 6.373 secs

That will be running in our nice little Lint project, our test suite that we have created for it. I highly recommend doing that, because it gives you some level of certainty that your Lint check, again, does what you think it will.

Building (39:15)

I created a nice little utility because, in the end, our Lint checks are going to be packaged into a JAR file. I want an easy way to move that JAR file from my build outputs to my home directory, where I am keeping all of my custom Lint checks:


task install(type: Copy) {
    from configurations.lintChecks
    into System.getProperty('user.home') + '/.android/lint/'
}

>  ./gradlew clean build test install

I call it install, and I will copy it from our output into my home directory’s Android Lint. Using that would be a nice, good old Gradle wrapper clean build, and throw in a test there for good measure, and then install to copy over that JAR output.

You could use that JAR locally, or you could throw it into continuous integration so your whole team can use it.

If we want to look at a particular Lint issue and figure out what it is about, what it is doing, it goes to a nice lint --show Enum and it will tell you all about it, give you the ID, a summary of it, the explanation, priority, everything that we defined earlier on our issue. And I do not actually believe the advice I say there. Enums matter.

Finally, if you actually just want to run Lint as a whole, you do a good old > ./gradlew lint, and it will output in your build/outputs/lint-results.html, which is a horribly styled HTML page. It will just say Lint report and then list everything that you care about.

Mine will say, a nice little warning side, and then enum, avoid using them. That is how you just run it stand alone, and of course, you could alt+right click in Android Studio and do like an analyze code if you just want to use your IDE integrations.

In the end, once you have bundled your new JAR inside your home directory, it will start running for all of your projects. In my sample code, I created this Lint check, and now it always shows up in all of my projects. But, as we learned, I can suppress it at will.

You can find all source code in this GitHub repository.

Q&A (41:36)

Q: Can you ignore per package? Matt: Yes, that would be using the Lint dot XML at your topical file, I believe you can specify which files that it should care about. I did not show a very complex example there, but look at the documentation, it does a good job of how to suppress and ignore Lint checks, but not good a job on how to create them.

Resources

About the content

This talk was delivered live in July 2016 at 360 AnDev. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Matthew Compton

Matt Compton is an Android Developer and Instructor for Big Nerd Ranch, where he works both to solve challenging problems and teach others how to. From his home in Atlanta, he enjoys playing board games, solving puzzles, and trying out new technology.