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

时间:2017-08-23 17:30:53

标签: android lint android-lint

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

  1. 在Android Studio中创建新的Android项目;
  2. 为在步骤1中创建的项目创建一个java模块;
  3. 在模块的build.gralde上,导入Lint API依赖项;
  4. 创建问题& IssueRegister& CustomDetector;
  5. 在模块的build.gradle;
  6. 上引用IssueRegister
  7. 创建单元测试;
  8. 我的问题是,在执行我的JUnits期间,我总是收到" 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"
    

    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相同

2 个答案:

答案 0 :(得分:3)

事实证明,在我的情况下问题出在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上添加了执行此操作的任务,则可以跳过此步骤; < / LI>
  7. 重新启动计算机 - 创建并移至~/.android/lint后,下次程序启动时应由Lint读取自定义规则。为了在Android Studio中设置警报框,使缓存无效并重新启动 IDE就足够了,但是,./gradlew check时,您的自定义规则会在Lint报告中被捕获,它可能是必须重新启动计算机;
  8. 测试探测器和检查器

    测试自定义规则并非易事 - 主要是由于缺少官方API的文档。本节将介绍处理此问题的两种方法。该项目的主要目标是创建将针对实际文件运行的自定义规则,因此,测试文件将是测试它们所必需的。它们可以放在Lint Java Library Module的src/test/resources文件夹中;

    方法01:LintDetectorTest

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

      方法02:Lint JUnit规则

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

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

答案 1 :(得分:1)

我不确定如何使用AST Api,但是我个人使用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"); } } } } } ,这是我正在为我工​​作的lint检查之一。

"SELECT LaTable.Num, LaTable.LaDate, Count(*) As C " & _
"FROM LaTable " & _
"GROUP BY LaTable.Num, LaTable.LaDate " & _
"HAVING Count(*) = 0;"

查看我写的更多here