【问题标题】:How to correctly implement and test Custom Lint Rules in Android Studio?如何在 Android Studio 中正确实施和测试自定义 Lint 规则?
【发布时间】:2026-01-05 04:00:02
【问题描述】:

我正在关注 this tutorialthis 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 执行期间,我总是收到“无警告”。当我调试测试时,我可以看到我的自定义检测器没有被调用,我做错了什么?

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";
}

问题.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"

TestVariableNameDetector.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}"))
        );
    }
}

PS:在 Java 模块上,我无权访问 assetsres 文件夹,这就是我创建 String.java 并在我的单元中使用 java(to, source) 的原因测试 - 我假设这个 java 方法与我在这个问题顶部引用的教程链接中的 xml 相同。

【问题讨论】:

    标签: android lint android-lint


    【解决方案1】:

    我不确定如何使用 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

    【讨论】:

    • 谢谢,您能否将链接添加到您编写的其他规则?我想你错过了
    【解决方案2】:

    事实证明,就我而言,问题出在 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,第二个设置为稍后将包含问题目录的类的完整路径;
      • 添加默认任务assemble;
      • [可选]:添加一个任务,将生成的 .jar 复制到~/.android/lint
    4. 检查 REF001 并选择最适合您需求的检测器,在此基础上创建并实现一个类来履行检测器的角色;
    5. 仍然基于REF0001选择的文件,创建并实现一个Checker类,稍后在Detector的createJavaVisitor()方法中引用它;
      • 为了 SRP,不要将 Checker 放在 Detector 类的同一个文件中;
    6. 将生成的 .jar 文件从 build/lib 复制到 ~/.android/lint - 如果您在 build.gradle 上添加了执行此操作的任务,则可以跳过此步骤;
    7. 重新启动计算机 - 创建并移动到 ~/.android/lint 后,Lint 应在下次程序启动时读取自定义规则。为了在 Android Studio 中包含警报框,使缓存无效并重新启动 IDE 就足够了,但是,如果要在 ./gradlew check 时在 Lint 报告中捕获您的自定义规则,则可能需要重新启动计算机;

    测试检测器和检查器

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

    方法 01:LintDetectorTest

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

    请注意,LintDetectorTest.java 派生自 TestCase.java,因此,您只能使用 JUnit 3。

    方法 02:Lint JUnit 规则

    您可能已经注意到,方法 01 可能有点过于复杂,尽管您仅限于 JUnit 3 功能。正因为如此,GitHub user a11n 创建了一个Lint JUnit Rule,它允许以更简单的方式测试自定义 Lint 规则,这对 JUnit 4 及更高版本的功能很重要。请参阅他的项目README.md,了解如何使用此方法创建测试的详细信息。

    目前,Lint JUnit Rule 没有更正测试文件的根目录,您可能无法看到从 IDE 传递的测试 - 但是当从命令行运行测试时它可以工作。为了修复此错误,创建了 issuePR

    【讨论】:

      最近更新 更多