Android自定义Lint实践(二)

为何须要自定义

  • 原生Lint没法知足咱们团队特有的需求,例如:编码规范。html

  • 原生Lint存在一些检测缺陷或者缺乏一些咱们认为有必要的检测。node

自定义方案

LinkedIn提供了一种思路 : 将jar放到一个aar中。这样咱们就能够针对工程进行自定义Lint,lint.jar只对当前工程有效。android

clipboard.png

Google指出,aar文件能够包含一个自定义的lint.jar文件express

aar虽然方便,但依然有不少问题,缘由在于,要想统一开发者的lint检查,每一个开发者都须要配置lint.xml、lintOptions。api

开发插件,统一管理lint.xml和lintOptions,自动添加aar。app

开发插件后,继承了原生lint和自定义lint的全部检查规则,内置lintOptions。maven

方案实施

建立Android项目

该项目主要用于测试规则是否正确ide

建立lint依赖项目

该项目主要用于将lint.jar转换为lint.aar文件,并提供maven库发布功能测试

建立lint的Java项目

该项目主要用于编写lint的自定义规则类,以及打包成lint.jar文件gradle

建立plugin的groovy项目

该项目主要用于编写lint的引用,lint的自定义规则,并提供maven库发布功能

自定义lint类

  • 引入lint-api和lint-checks的依赖包

  • 建立IssueRegistry类,用于注册全部的ISSUE

  • 建立相关的Detector类,继承自Detector,实现相关的接口

以CustomEquaslDetector类为例,其继承Detector类,实现JavaScanner接口。注意Detector类是一个抽象类,其包含了不少内部接口类,并实现了它们的全部方法。接口类以下所示:

  • XmlScanner

  • ResourceFolderScanner

  • OtherFileScanner

  • JavaScanner

  • GradleScanner

  • ClassScanner

  • BinaryResoucrceScanner

看类的名称就知道其相对应的做用,因此咱们实现JavaScanner类。

建立ISSUE对象,并注册到IssueRegistry类中,建立对象的方式是静态工程方法建立:

public static final Issue ISSUE = Issue.create(
        "LogUse",
        "避免使用Log/System.out.println",
        "使用Ln,防止在正式包打印log",
        Category.SECURITY, 5, Severity.ERROR,
        new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));
  • id : 惟一值,应该能简短描述当前问题。利用Java注解或者XML属性进行屏蔽时,使用的就是这个id。

  • summary : 简短的总结,一般5-6个字符,描述问题而不是修复措施。

  • explanation : 完整的问题解释和修复建议。

  • category : 问题类别。详见下文详述部分。

  • priority : 优先级。1-10的数字,10为最重要/最严重。

  • severity : 严重级别:Fatal, Error, Warning, Informational, Ignore。

  • Implementation : 为Issue和Detector提供映射关系,Detector就是当前Detector。声明扫描检测的范围Scope,Scope用来描述Detector须要分析时须要考虑的文件集,包括:Resource文件或目录、Java文件、Class文件。

相对应的,其在lint的html报告中对应的关系,以下:

clipboard.png

clipboard.png

总结下,每一个Lint检查都须要四部分:

  • Issues 一个issue对应于Android项目中的一个可能的问题或bug。

  • Detectors 一个detector用于搜寻代码潜在的Issues,一个单独的detector能够搜寻多个独立但相关的Issues。

  • implementations 一个implementation将一个Issue链接到对应的Detector类,并指定在哪儿搜寻Issue。

  • Registries 一个注册类包含一系列的Issues,默认的Registry类是BuiltinIssueRegistry类,由于咱们编写了本身的自定义Issues,因此咱们须要提供自定义Registry类。

clipboard.png

举个例子:

public class EnumDetector extends Detector implements Detector.JavaScanner {

    ... // Implementation and Issue code from above

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

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

    @Override
    public EnumSet<Scope> getApplicableFiles() {
        return Scope.JAVA_FILE_SCOPE;
    }

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

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

    private static class EnumChecker extends ForwardingAstVisitor {

        private final JavaContext mContext;

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

        @Override
        public boolean visitEnumDeclaration(EnumDeclaration node) {
            mContext.report(ISSUE, Location.create(mContext.file),                ISSUE.getBriefDescription(TextFormat.TEXT));
            return super.visitEnumDeclaration(node);
        }
    }
}
  • appliesTo方法 决定是否给定的文件可用并可被扫描,咱们return true来检查给定的范围

  • getApplicableFiles方法定义了Detector的范围,该例是全部的Java文件。

  • getApplicableNodeTypes方法,注意其中的node,指的是一段代码。一个node能够是一个类的申明,一个方法的调用,或者一个注释,由于咱们只关心Enum的申明,全部返回 EnumDeclaration.class。

  • createJavaVisitor方法是Lombok遍历Java树的方法。咱们建立一个EnumChecker内部类来表示检查node树的过程。

  • 由于只有一个node类型须要被检查,全部覆写visitEnumDeclaration方法。每当有一个Enum的申明,该方法就会被执行一次。

  • mContext.report方法用于问题的报告。ISSUE为哪种Issue,location为问题的发现地,以及Issue的简要描述。

在看下IntentExtraKeyDetector类:

public class IntentExtraKeyDetector extends Detector implements JavaScanner {
    public static final Issue ISSUE = Issue.create(
            "extraKey",
            "please avoid use hardcode defined intent extra key",
            "defined in another activity",
            Category.SECURITY, 5, Severity.ERROR,
            new Implementation(IntentExtraKeyDetector.class,             Scope.JAVA_FILE_SCOPE));

    public IntentExtraKeyDetector() {}

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

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

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

    @Override
    public List<String> getApplicableMethodNames() {
        return Collections.singletonList("putExtra");
    }

    @Override
    public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
                            @NonNull MethodInvocation node) {
        ResolvedNode resolved = context.resolve(node);
        if (resolved instanceof ResolvedMethod) {
            ResolvedMethod method = (ResolvedMethod) resolved;

            if (method.getContainingClass().isSubclassOf("android.content.Intent", false)
                    && method.getArgumentCount() == 2) {
                ensureExtraKey(context, node);
            }
        }
    }

    private static void ensureExtraKey(JavaContext context, @NonNull MethodInvocation node) {
        //获取method的参数值
        StrictListAccessor<Expression, MethodInvocation> accessor = node.astArguments();

        if (accessor.size() != 2) {
            return;
        }
        Expression expression = accessor.first();
        //当第一个参数值类型为String,这样是硬编码
        if (expression instanceof StringLiteral){
            context.report(ISSUE, node, context.getLocation(node), "please avoid use hardcode defined Intent.putExtra key");
            return;
        }
        //当第一个参数值类型为变量
        //ConstantEvaluator.evaluate(context, expression);
        if (expression instanceof VariableReference){
            //获取该变量的定义name
            String targetName = ((VariableReference)expression).astIdentifier().astValue();
            if (!targetName.startsWith("EXTRA_")){
                context.report(ISSUE, node, context.getLocation(node), "please defined intent extra key start with EXTRA_");
            }
        }
        //当第一个参数值是其余类的变量时
        if (expression instanceof Select){
            String targetName = ((Select)expression).astIdentifier().astValue();
            if (!targetName.startsWith("EXTRA_")){
                context.report(ISSUE, node, context.getLocation(node), "please defined intent extra key start with EXTRA_");
            }
        }
    }

使用指南

  • 在project中的build.gradle文件中的dependencies中添加

classpath 'com.mucfc.muna.lint:plugin:latest.integration'
  • 在module app中的build.gradle文件中,添加:

apply plugin: 'MuLintPlugin'

融合项目后

使用系统Toast,而没有使用muna中的自定义toast

clipboard.png

使用Bundle.putXXX("key","value")

注意key不该该这样定义

clipboard.png

使用Intent.putExtra(key,value);

注意key不能直接硬编码,且key定义的String引用必须为EXTRA_开头

clipboard.png

定义Activity类必须继承BaseActivity(Fragment类同)

clipboard.png

注意由于本身编写的BaseActivity,可使用@Suppressint("activityUse")去除错误

clipboard.png

使用equals方法

在equals(value)中,value不能为硬编码或定义在该类中的static final字符串,由于当为指定字符串的时候,须要value.equals(),防止空指针

clipboard.png

使用原始的Log.d()方法

由于有MuLog,因此不该该再次使用Log.d,防止敏感信息泄露。

clipboard.png

相关文章
相关标签/搜索