如何在 Android Studio 中正确实施和测试自定义 Lint 规则?

How to correctly implement and test Custom Lint Rules in Android Studio?

我正在关注 this tutorial and this Custom Detector Example 以实施自定义 Lint 规则。基本上我所做的是:

  1. 在 Android Studio 中创建一个新的 Android 项目;
  2. 为步骤 1 中创建的项目创建一个 java 模块;
  3. 在模块的 build.gradle 上,导入 Lint API 依赖项;
  4. 创建一个 Issue & IssueRegistry & CustomDetector;
  5. 引用模块 build.gradle 上的 IssueRegistry
  6. 创建单元测试;

我的问题是,在我的 JUnit 执行期间,我总是收到 "No Warning"。当我调试测试时,我可以看到我的自定义检测器没有被调用,我做错了什么?

Strings.java

public class Strings {

    public static final String STR_ISSUE_001_ID = "VarsMustHaveMoreThanOneCharacter";
    public static final String STR_ISSUE_001_DESCRIPTION = "Avoid naming variables with only one character";
    public static final String STR_ISSUE_001_EXPLANATION = "Variables named with only one character do not pass any meaning to the reader. " +
        "Variables name should clear indicate the meaning of the value it is holding";
}

Issues.java

public class Issues {

    public static final
    Issue ISSUE_001 = Issue.create(
            STR_ISSUE_001_ID,
            STR_ISSUE_001_DESCRIPTION,
            STR_ISSUE_001_EXPLANATION,
            SECURITY,
            // Priority ranging from 0 to 10 in severeness
            6,
            WARNING,
            new Implementation(VariableNameDetector.class, ALL_RESOURCES_SCOPE)
    );
}

IssuesRegistry.java

public class IssueRegistry extends com.android.tools.lint.client.api.IssueRegistry {
    @Override
    public List<Issue> getIssues() {
        List<Issue> issues = new ArrayList<>();
        issues.add(ISSUE_001);
        return issues;
    }
}

VariableNameDetector.java

public class VariableNameDetector extends Detector implements Detector.JavaScanner {

    public VariableNameDetector() {

    }

    @Override
    public boolean appliesToResourceRefs() {
        return false;
    }

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

    @Override
    @Nullable
    public AstVisitor createJavaVisitor(JavaContext context) {
        return new NamingConventionVisitor(context);
    }

    @Override
    public List<String> getApplicableMethodNames() {
        return null;
    }

    @Override
    public List<Class<? extends Node>> getApplicableNodeTypes() {
        List<Class<? extends Node>> types = new ArrayList<>(1);
        types.add(lombok.ast.VariableDeclaration.class);
        return types;
    }

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

    @Override
    public void visitResourceReference(
            JavaContext context,
            AstVisitor visitor,
            Node node,
            String type,
            String name,
            boolean isFramework
    ) {
    }

    private class NamingConventionVisitor extends ForwardingAstVisitor {

        private final JavaContext context;

        NamingConventionVisitor(JavaContext context) {
            this.context = context;
        }

        @Override
        public boolean visitVariableDeclaration(VariableDeclaration node) {
            StrictListAccessor<VariableDefinitionEntry, VariableDeclaration> varDefinitions =
                    node.getVariableDefinitionEntries();

            for (VariableDefinitionEntry varDefinition : varDefinitions) {
                String name = varDefinition.astName().astValue();
                if (name.length() == 1) {
                    context.report(
                            ISSUE_001,
                            context.getLocation(node),
                            STR_ISSUE_001_DESCRIPTION
                    );
                    return true;
                }
            }
            return false;
        }
    }
}

build.gradle

apply plugin: 'java'

configurations {
    lintChecks
}

ext {
    VERSION_LINT_API = '24.3.1'
    VERSION_LINT_API_TESTS = '24.3.1'
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "com.android.tools.lint:lint-api:$VERSION_LINT_API"
    implementation "com.android.tools.lint:lint-checks:$VERSION_LINT_API"
    testImplementation "com.android.tools.lint:lint-tests:$VERSION_LINT_API_TESTS"
}

jar {
    manifest {
        attributes('Lint-Registry': 'br.com.edsilfer.lint_rules.resources.IssueRegistry')
    }
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

测试VariableNameDetector.java

private static final String ARG_DEFAULT_LINT_SUCCESS_LOG = "No warnings.";

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

    @Override
    protected List<Issue> getIssues() {
        return Collections.singletonList(Issues.ISSUE_001);
    }

    public void test_file_with_no_variables_with_length_equals_01() throws Exception {
        assertEquals(
                ARG_DEFAULT_LINT_SUCCESS_LOG,
                lintProject(java("assets/Test.java", "public class Test {public String sampleVariable;}"))
        );
    }

    public void test_file_with_variables_with_length_equals_01() throws Exception {
        assertEquals(
                ARG_DEFAULT_LINT_SUCCESS_LOG,
                lintProject(java("assets/Test3.java", "public class Test {public String a;bnvhgvhj}"))
        );
    }
}

P.S.: 在 Java 的模块上我无法访问 assetsres 文件夹,这就是为什么我创建了一个 String.java 并在我的单元测试中使用 java(to, source) - 我假设这个 java 方法与教程 [=] 中的 xml 相同89=] 我在这个问题的顶部引用了。

我不确定如何使用 AST Api,但我个人正在使用 Psi,这是对我有用的 lint 检查之一。

public final class RxJava2MethodCheckReturnValueDetector extends Detector implements Detector.JavaPsiScanner {
  static final Issue ISSUE_METHOD_MISSING_CHECK_RETURN_VALUE =
      Issue.create("MethodMissingCheckReturnValue", "Method is missing the @CheckReturnValue annotation",
          "Methods returning RxJava Reactive Types should be annotated with the @CheckReturnValue annotation.",
              MESSAGES, 8, WARNING,
          new Implementation(RxJava2MethodCheckReturnValueDetector.class, EnumSet.of(JAVA_FILE, TEST_SOURCES)));

  @Override public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
    return Collections.<Class<? extends PsiElement>>singletonList(PsiMethod.class);
  }

  @Override public JavaElementVisitor createPsiVisitor(@NonNull final JavaContext context) {
    return new CheckReturnValueVisitor(context);
  }

  static class CheckReturnValueVisitor extends JavaElementVisitor {
    private final JavaContext context;

    CheckReturnValueVisitor(final JavaContext context) {
      this.context = context;
    }

    @Override public void visitMethod(final PsiMethod method) {
      final PsiType returnType = method.getReturnType();

      if (returnType != null && Utils.isRxJava2TypeThatRequiresCheckReturnValueAnnotation(returnType)) {
        final PsiAnnotation[] annotations = method.getModifierList().getAnnotations();

        for (final PsiAnnotation annotation : annotations) {
          if ("io.reactivex.annotations.CheckReturnValue".equals(annotation.getQualifiedName())) {
            return;
          }
        }

        final boolean isMethodMissingCheckReturnValueSuppressed = context.getDriver().isSuppressed(context, ISSUE_METHOD_MISSING_CHECK_RETURN_VALUE, method);

        if (!isMethodMissingCheckReturnValueSuppressed) {
          context.report(ISSUE_METHOD_MISSING_CHECK_RETURN_VALUE, context.getLocation(method.getNameIdentifier()), "Method should have @CheckReturnValue annotation");
        }
      }
    }
  }
}

查看我写的更多内容 here

事实证明,在我的案例中,问题出在 JUnit 本身。我认为我尝试模拟文件的方式是错误的。下面的文字是我创建的 README.md of a sample project 的一部分,目的是记录我从这个 API 中学到的东西,并回答标题中的问题:


创建

  1. 创建一个新的 Android 项目;
  2. 创建一个新的 Java 库模块 - 自定义 Lint 规则一旦准备就绪就会打包到 .jar 库中,因此使用它们实现它们的最简单方法是在 Java 模块库;
  3. 在模块的 build.gradle 上:
    • 将目标和源兼容性添加到 Java 1.7;
    • 为lint-api、lint-checks添加依赖并测试依赖;
    • 添加包含两个属性的 jar 打包任务:Manifest-VersionLint-Registry,将第一个设置为 1.0,将第二个设置为稍后将包含的 class 的完整路径问题目录;
    • 添加默认任务assemble;
    • [可选]: 添加一个任务,将生成的 .jar 复制到 ~/.android/lint;
  4. 检查 REF001 并选择最适合您需求的检测器,在此基础上创建并实施 class 以履行检测器的作用;
  5. 仍然基于REF0001选择的文件,创建并实现一个Checker class,稍后在Detector的createJavaVisitor()方法中引用它;
    • 为了SRP,不要将Checker和Detector的class;
    • 放在同一个文件中
  6. 将生成的 .jar 文件从 build/lib 复制到 ~/.android/lint - 如果您在 build.gradle 上添加了执行此操作的任务,则可以跳过此步骤;
  7. 重新启动计算机 - 创建并移至 ~/.android/lint 后,下次程序启动时,Lint 应该会读取自定义规则。为了在 Android Studio 中设置警告框, 使缓存无效并重新启动 IDE 就足够了,但是,让您的自定义规则在 Lint 上被捕获./gradlew check时报告,可能需要重启电脑;

测试检测器和检查器

测试自定义规则不是一件容易的事 - 主要是因为缺少官方 API 的文档。本节将介绍两种处理此问题的方法。该项目的主要目标是创建针对真实文件 运行 的自定义规则,因此需要测试文件来测试它们。它们可以放在 Lint Java 库模块的 src/test/resources 文件夹中;

方法 01:LintDetectorTest

  1. 确保您已添加所有测试依赖项 - 检查 sample project's build.gradle;
  2. 复制EnhancedLintDetectorTest.java and FileUtils.java到你项目的测试目录;
    • Android Studio 存在一个已知错误,导致它无法查看 src/test/resources 文件夹中的文件,这些文件是解决该问题的方法;
    • EnhancedLintDetectorTest.java 应该 return 所有将要测试的问题。一个很好的方法是从 Issue Registry 获取它们;
  3. 创建一个扩展自 EnhancedLintDetectorTest.java 的测试 class;
  4. 实施 getDetector() 方法 return 要测试的检测器实例;
  5. 使用lintFiles("test file path taking resources dir as root")执行自定义规则的检查并使用其结果object断言测试;

请注意,LintDetectorTest.java 派生自 TestCase.java,因此,您仅限于 JUnit 3。

方法 02:Lint JUnit 规则

您可能已经注意到方法 01 可能有点过于复杂,尽管您仅限于使用 JUnit 3 功能。因此 GitHub user a11n created a Lint JUnit Rule that allows the test of Custom Lint Rules in a easier way that counts with JUnit 4 and up features. Please, refer to his project README.md 有关如何使用此方法创建测试的详细信息。

目前,已创建 Lint JUnit Rule do not correct the root dir for test files and you might no be able to see the tests passing from the IDE - however it works when test are run from command line. An issue and PR 以修复此错误。