Writing custom lint warning to check for custom annotation

时间:2016-10-19 13:42:02

标签: android android-studio android-lint java-annotations

I have written the following annotation:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.METHOD})
public @interface Warning {

}

Which is intended to annotate methods which can cause problems if called carelessly. I added an annotation processor to my project, but this only provides the warning in the log output of the javac command. I want this warning to appear in Android Studio along with the other lint warnings anywhere a method with this annotation is called. This is why I am trying to write a custom lint rule. I have the basic skeleton of the lint rule:

import com.android.tools.lint.detector.api.Category;
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.Scope;
import com.android.tools.lint.detector.api.Severity;

public class CaimitoDetector extends Detector implements Detector.JavaScanner {

  public static final Issue ISSUE = Issue.create(
      "WarningAnnotation",
      "This method has been annotated with @Warning",
      "This method has special conditions surrounding it's use, be careful when using it and refer to its documentation.",
      Category.USABILITY, 7, Severity.WARNING,
      new Implementation(CaimitoDetector.class, Scope.JAVA_FILE_SCOPE));

  @Override
  public void visitMethod(JavaContext context, AstVisitor visitor, MethodInvocation node) {

  }

}

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

import java.util.Collections;
import java.util.List;

public class CaimitoIssueRegistry extends IssueRegistry {

  @Override
  public List<Issue> getIssues() {
    return Collections.singletonList(CaimitoDetector.ISSUE);
  }

}

But I do not know how to proceed from here. How can I check if an annoation exists on a method, and raise a warning such that it will be visible in Android Studio?

UPDATE

Here is my Detector class for anyone looking to do the same:

import com.android.annotations.NonNull;
import com.android.tools.lint.client.api.JavaParser.ResolvedAnnotation;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.JavaParser.ResolvedNode;
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.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import lombok.ast.AstVisitor;
import lombok.ast.ConstructorInvocation;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;

public class CaimitoAnnotationDetector extends Detector implements Detector.JavaScanner {

  private static final String WARNING_ANNOTATION = "com.treemetrics.caimito.annotations.Warning";

  public static final Issue ISSUE = Issue.create(
      "Waqrning.",
      "Be careful when using this method.",
      "This method has special conditions surrounding it's use," +
          " be careful when calling it and refer to its documentation.",
      Category.USABILITY,
      7,
      Severity.WARNING,
      new Implementation(
          CaimitoAnnotationDetector.class,
          Scope.JAVA_FILE_SCOPE));

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

  @NonNull
  @Override
  public Speed getSpeed() {
    return Speed.FAST;
  }

  private static void checkMethodAnnotation(@NonNull JavaContext context,
                                            @NonNull ResolvedMethod method,
                                            @NonNull Node node,
                                            @NonNull ResolvedAnnotation annotation) {
    String signature = annotation.getSignature();
    if(WARNING_ANNOTATION.equals(signature) || signature.endsWith(".Warning")) {
      checkWarning(context, node, annotation);
    }
  }

  private static void checkWarning(@NonNull JavaContext context,
                                      @NonNull Node node,
                                      @NonNull ResolvedAnnotation annotation) {
    context.report(ISSUE, node, context.getLocation(node), "Warning");
  }

  // ---- Implements JavaScanner ----

  @Override
  public List<Class<? extends Node>> getApplicableNodeTypes() {
    return Arrays.asList(
        MethodInvocation.class,
        ConstructorInvocation.class);
  }

  @Override
  public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
    return new CallChecker(context);
  }

  private static class CallChecker extends ForwardingAstVisitor {

    private final JavaContext mContext;

    public CallChecker(JavaContext context) {
      mContext = context;
    }

    @Override
    public boolean visitMethodInvocation(@NonNull MethodInvocation call) {
      ResolvedNode resolved = mContext.resolve(call);
      if(resolved instanceof ResolvedMethod) {
        ResolvedMethod method = (ResolvedMethod) resolved;
        checkCall(call, method);
      }

      return false;
    }

    @Override
    public boolean visitConstructorInvocation(@NonNull ConstructorInvocation call) {
      ResolvedNode resolved = mContext.resolve(call);
      if(resolved instanceof ResolvedMethod) {
        ResolvedMethod method = (ResolvedMethod) resolved;
        checkCall(call, method);
      }

      return false;
    }

    private void checkCall(@NonNull Node call, ResolvedMethod method) {
      Iterable<ResolvedAnnotation> annotations = method.getAnnotations();
      annotations = filterRelevantAnnotations(annotations);
      for(ResolvedAnnotation annotation : annotations) {
        checkMethodAnnotation(mContext, method, call, annotation);
      }
    }

    private Iterable<ResolvedAnnotation> filterRelevantAnnotations(Iterable<ResolvedAnnotation> resolvedAnnotationsIn) {
      List<ResolvedAnnotation> resolvedAnnotationsOut = new ArrayList<>();
      for(ResolvedAnnotation resolvedAnnotation : resolvedAnnotationsIn) {
        if(resolvedAnnotation.matches(WARNING_ANNOTATION)) {
          resolvedAnnotationsOut.add(resolvedAnnotation);
        }
      }

      return resolvedAnnotationsOut;
    }

  }

}

UPDATE 2

You can integrate your custom lint check with Android Studio inspections by creating a lint.xml file in the root of your project and adding you custom lint rule there like this:

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <issue id="Warning" severity="warning"/>
</lint>

Notice the id of the issue tag is the id provided at the first argument of the Issue.create() method in the CaimitoDetector class. You will also have to copy the jar file outputted by building your lint rule into your /home/{user}/.android/lint folder to make it work. I wrote a custom gradle task for this. Here is my lint rule's build.gradle file

apply plugin: 'java'

targetCompatibility = '1.7'
sourceCompatibility = '1.7'

repositories {
    jcenter()
}

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

jar {
    manifest {
        attributes 'Manifest-Version': 1.0
        attributes 'Lint-Registry': 'com.treemetrics.caimito.lint.CaimitoIssueRegistry'
    }
}

defaultTasks 'assemble'

task copyLintJar(type: Copy) {
    description = 'Copies the caimito-lint jar file into the {user.home}/.android/lint folder.'
    from('build/libs/')
    into(System.getProperty("user.home") + '/.android/lint')
    include("*.jar")
}

// Runs the copyLintJar task after build has completed.
build.finalizedBy(copyLintJar)

UPDATE 3

You can also add your Java lint project as a dependency on other projects to get the same effect as update 2.

enter image description here

UPDATE 4

I have since written a blog post on this topic https://medium.com/@mosesJay/writing-custom-lint-rules-and-integrating-them-with-android-studio-inspections-or-carefulnow-c54d72f00d30#.3hm576b4f .

1 个答案:

答案 0 :(得分:2)

  

但我不知道如何从这里开始

我建议先为Detector编写测试。这是一个示例项目,演示如何编写Detector测试[1]。这样您就可以根据需要尝试调整Detector

  

如何检查方法上是否存在annoation

我建议看看Android的默认探测器[2]。在那里你很可能会找到一个好的开始。例如。 AnnotationDetector

  

并发出警告,使其在Android Studio中可见?

如果您将自定义规则正确地集成到项目中,那么Lint将为您发出警告。有关如何在项目中集成自定义规则的不同选项,请查看此处[3]。注意:只有在运行相应的Gradle任务时才会报告自定义规则的AFAIK警告。 Android Studio的“自动突出显示”不适用于自定义规则。

  1. https://github.com/a11n/CustomLintRules
  2. https://android.googlesource.com/platform/tools/base/+/master/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks
  3. https://github.com/a11n/android-lint/tree/master/6_application