Writing a custom Lint rule

Writing a custom Lint rule sounds like a complicated thing, but actually this is a very easy thing to do, and can benefit your cadebase a great deal.

The best way to do that is to create a separate module, and use the Java plugin instead of the Application/Library plugin that we usually use. 

So, in order to start, create a new module (you can name it customlints), and add this line to the module build.gradle:

apply plugin: 'java-library'
dependencies {
    def lint_version = "26.2.1" //this correspond to gradle plugin version 3.2.1. The rule is to add 23 to the first number
    compileOnly "com.android.tools.lint:lint-api:$lint_version"
    compileOnly "com.android.tools.lint:lint-checks:$lint_version"
}

The next thing we need to do is to create a Detector. The detector is the place where you will put the logic for your lint rule. There is a lot of resources about what you can do there, and for doing complicated things you can look at the source code for the built-in Android Lint rules (here).

Lets use a common example, where you wrote your own Logger class, and you want to have a Lint Check to warn for any use of Log.d instead of your desired SmartLogger.d

So we will create a class named SmartLoggerDetector.java for that purpose:

public class SmartLoggerDetector extends Detector implements Detector.UastScanner {

    @Override 
    public List<String> getApplicableMethodNames() {
        return Arrays.asList("v", "d", "i", "w", "e", "wtf");
    }

    @Override 
    public void visitMethod(@NotNull JavaContext context,@NotNull UCallExpression call,@NotNull PsiMethod method) {
        JavaEvaluator evaluator = context.getEvaluator();

        if (evaluator.isMemberInClass(method, "android.util.Log")) {
            LintFix fix = quickFixIssueLog(call);
            context.report(ISSUE_LOG, call, context.getLocation(call), "Using 'Log' instead of 'SmartLogger'", fix);
        }
    }

    private LintFix quickFixIssueLog(UCallExpression logCall) {
        List<UExpression> arguments = logCall.getValueArguments();
        String methodName = logCall.getMethodName();
        UExpression tag = arguments.get(0);
        String fixSource1 = "SmartLogger." + methodName +  "(" + tag.asSourceString();

        int numArguments = arguments.size();
        if (numArguments == 2) {
            UExpression msgOrThrowable = arguments.get(1);
            fixSource1 +=  ", " + msgOrThrowable.asSourceString() + ")";
        } else if (numArguments == 3) {
            UExpression msg = arguments.get(1);
            UExpression throwable = arguments.get(2);
            fixSource1 += ", " + throwable.asSourceString() + ", " + msg.asSourceString() + ")";
        } else {
            throw new IllegalStateException("android.util.Log overloads should have 2 or 3 arguments");
        }
        String logCallSource = logCall.asSourceString();
        LintFix.GroupBuilder fixGrouper = fix().group();
        fixGrouper.add(fix().replace().text(logCallSource).shortenNames().reformat(true).with(fixSource1).build());
        return fixGrouper.build();
    }

    static Issue[] getIssues() {
        return new Issue[] {
                ISSUE_LOG
        };
    }

    public static final Issue ISSUE_LOG =
            Issue.create("LogNotSmartLogger", "Logging call to Log instead of SmartLogger",
                    "Since SmartLogger is included in the project, it is likely that calls to Log should instead"
                            + " be going to SmartLogger.", Category.CORRECTNESS, 6, Severity.WARNING,
                    new Implementation(SmartLoggerDetector.class, Scope.JAVA_FILE_SCOPE));

}

The method quickFixIssueLog gives you the possibility to integrate a quick fix into Android Studio, but you can skip this part and just put null instead:

 @Override 
    public void visitMethod(@NotNull JavaContext context,@NotNull UCallExpression call,@NotNull PsiMethod method) {
        JavaEvaluator evaluator = context.getEvaluator();
        if (evaluator.isMemberInClass(method, "android.util.Log")) {
            context.report(ISSUE_LOG, call, context.getLocation(call), "Using 'Log' instead of 'Loggy'", null);
        }
    }

Now that our custom Lint check is in place, we need to notify the IDE about it, this is done by adding this rule to Lint registry. For that we will create a class named SmartLoggerIssueRegistry.java:

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

import org.jetbrains.annotations.NotNull;

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

public class SmartLoggerIssueRegistry extends IssueRegistry {

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

    @Override public int getApi() {
        return ApiKt.CURRENT_API;
    }
}

Now we will use this class in our build.gradle file:

apply plugin: 'java-library'

dependencies {

    def lint_version = "26.2.1" //this correspond to gradle plugin version 3.2.1. The rule is to add 23 to the first number
    compileOnly "com.android.tools.lint:lint-api:$lint_version"
    compileOnly "com.android.tools.lint:lint-checks:$lint_version"
}

jar {
    manifest {
        attributes("Lint-Registry-v2": "com.thedroidboy.customlint.SmartLoggerIssueRegistry")
    }
}

That’s all. In order to consume this rule in another module, we need to hook this module in the client module build.gradle :

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    ...
    lintChecks project(':customlints')
}

And this is how it will look in your client code:

That’s all!

(Or at least this is how it should look like after Android Studio will fix the bug mention here:
In the meanwhile you can use the same workaround mention in the tracker, and put the generated customlint.jar file into ~/.android/lint )

A demo source code can be found here

Additional useful resources:
A video from android-dev-summit-18
Google sample project
Timber lint rules

Leave a Reply

Your email address will not be published. Required fields are marked *