Home » Android » Custom Lint for Java / Android Report if we find a class call without implementing its interface

Custom Lint for Java / Android Report if we find a class call without implementing its interface

Posted by: admin May 14, 2020 Leave a comment

Questions:

I am writing a custom lint detector for fun. I am going off the Big Nerd Ranches lesson on creating a custom lint rule in Android (should be the same for Java)

I can detect the occurrence I want. That the class constructor was called. However due to the fact we are traversing via an Abstract Syntax Tree
I am failing to detect the implementation of the callback. I am not sure how to tell Java to check the file and only report if it could not find an occurrence. In this case the interface implementation. As I only am seeing one leaf at a time.

How do I look for two occurrences, store the locations each occurrence happened and then perform my logic and report accordingly?

package com.bignerdranch.linette.detectors;

import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.TextFormat;

import java.io.File;
import java.util.EnumSet;
import java.util.List;

import lombok.ast.AstVisitor;
import lombok.ast.Node;

/**
 * Lint check for the usage of to-do statements
 */
public class CallBackDetector extends Detector implements Detector.JavaScanner {

    private static final String FRAGMENT_MATCHER_STRING = "NoInternetDialogFragment()";
    private static final String INTERFACE_MATCHER_STRING =
            "NoInternetDialogFragment.NoInternetCallbackInterface";

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

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

    private static final String ISSUE_ID = "NoInternetDialogFragment";
    private static final String ISSUE_DESCRIPTION =
            "NoInternetDialogFragment Callback not detected";
    private static final String ISSUE_EXPLANATION =
            "When using NoInternetDialogFragment you must implement its' callback -- "
                    + "NoInternetCallbackInterface";
    private static final Category ISSUE_CATEGORY = Category.CORRECTNESS;
    private static final int ISSUE_PRIORITY = 10;
    private static final Severity ISSUE_SEVERITY = Severity.ERROR;

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

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

    @Override
    public boolean appliesTo(@NonNull Context context, @NonNull File file) {
        return true;
    }

    @Override
    public List<Class<? extends Node>> getApplicableNodeTypes() {
        return null;
    }

    @Override
    public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
        String source = context.getContents();

        // Check validity of source
        if (source == null) {
            return null;
        }

        if(source.indexOf(INTERFACE_MATCHER_STRING) >=0){
            return null;
        }
        int index = source.indexOf(FRAGMENT_MATCHER_STRING);

        for (int i = index; i >= 0; i = source.indexOf(FRAGMENT_MATCHER_STRING, i + 1)) {
            Location location = Location.create(context.file, source, i,
                    i + FRAGMENT_MATCHER_STRING.length());
            context.report(ISSUE, location, ISSUE.getBriefDescription(TextFormat.TEXT));
        }
        return null;
    }

}
How to&Answers:

It’s possible if you store the references that you find as a field and report them at the end of all syntax traversal:

Inside createJavaVisitor do the check like you have now for the use of the constructor. Instead of reporting an issue at this point create a collection field in your CallBackDetector that will hold a reference to the use of the constructor. You will then end up with a list of all the constructor uses.

Now also inside createJavaVisitor do the check for any class implementing the interface you are searching for and add this to another field collection.

You can override the method afterCheckProject (api here) in your Detector to know when its finished. In this method, iterate through your two collections and remove any items from both collections that are a matching pair (the constructor and the interface being implemented). Any that are left in either collection are your Lint errors, and you can add them as issues at this point.