自定义lint

要做自定义的lint检查,首先要理解一个Abstract Syntax Tree(简称AST)抽象语法树的概念。它是一个java程序完成编译所必要的前期工作,将java代码的语法按照树结构解析存储起来。相关讲解可见
https://juejin.im/entry/576b61162e958a0078c637fb)。而我们要做的lint检查就是基于这个抽象语法树的检查。
下面,我从实现一个自定义的lint(检查变量名是否按要求命名)来讲解一下怎么实现。
首先,我们需要做的是一个准备的工作,就是引入lint包:(总不能安装studio这样的工作也要我放在一个步骤里吧,所以这些感觉不那么核心的步骤我们就不当做主要步骤来归档了,包括后面的使jar生效等步骤。但这些都是必须的,只不过于其实现原理没那么核心)
我们创建一个java library的module,在其build.gradle文件中添加:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    compile 'com.android.tools.lint:lint-api:25.3.3'
}

我用的是当前最新的版本25.3.3,我注意到以前一些版本跟这个版本的实现方法和小要求不大一样,如果不是这个版本的,我建议换成最新的这个来做吧。而且我感觉这个版本的api比以前那套确实好用一点点,虽然还不是很清晰。。。

接着,我们就可以开始主要的步骤了。

总的来说,实现过程主要有两步:
一. 使用gradle注册一个lint检查器
二. 实现这个lint检查器

注册检查器
我们可能以为直接就可以把检查器注册进构建文档里,其实不是,我们需要先添加一个“注册器”,然后才在注册器里面申明要执行的检查器。这可能是为了解耦吧,然而我觉得这个过程有点多余,完全可以直接设计成注册检查器的方式,因为build文件里面本来就是一个注册的角色了,而且最终也只是为了注册检查器,又何必再加多一个注册器呢。至少从目前的功能要求上讲,这个注册器有点多余。我们接着按照要求来做:

首先,在gradle文件中加入这样一个配置:

jar {
    manifest {
        attributes("Lint-Registry": "com.ives.lint.WordSpellRegistry")
    }
}

显然,这似乎是把一个注册器注册给了gradle,但这个注册器我们似乎还没有,那么下一步我们就是要添加一个这样的注册器。
这一步只需要在上面所写的包下添加一个相应类名的java类WordSpellRegistry,这个类继承于IssueRegistry,并且实现里面的getIssues方法:

public class WordSpellRegistryextends IssueRegistry {

    public WordSpellRegistry() {//!!必须要有无参构造器
    }

    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(//这几个元素后面接着讲
                WordSpellDetector.ISSUE,
                MessageObtainDetector.ISSUE,
                ParameterLengthConstraintDetector.ISSUE
        );
    }
}

getIssues方法里面返回的就是好几个检查器,下面,我们就是进入第二步了,就是实现检查器。

实现检查器

要实现的检查器就是我们在getIssues方法里返回的这几个东西.其实只需要一个就行了,我上面添加了三个是为了表明这可以有多个检查器而已。
我们创建一个WordSpellDetector类。这个类的继承关系及基本实现如下:

public class WordSpellDetector extends Detector implements Detector.JavaPsiScanner {

    public static final Issue ISSUE = Issue.create("WordSpellStaticFinalUpper",
            "Static final field should be all uppercase",
            "Static final field should be all uppercase as our specification",
            Category.CORRECTNESS,
            9,
            Severity.WARNING,
            new Implementation(WordSpellDetector.class,
                    Scope.JAVA_FILE_SCOPE));

    @Override
    public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
        return super.getApplicablePsiTypes();
    }

    @Override
    public JavaElementVisitor createPsiVisitor(JavaContext context) {
        return super.createPsiVisitor(context);
    }
}

ISSUE显然是创建了一个提示相关的信息的封装对象。其中里面的new Implementation()返回了所要映射的检查器。
检查器里面最重要的两个方法就是getApplicablePsiTypes和createPsiVisitor。前者表示所要检查的节点类型,以前的版本里这个方法叫做getApplicableNodeTypes,现已废弃(可能是旧的那个Note接口信息太少了,现已替换成了PsiElement接口,但以前的好名字被站了咯,但起的这个新名字有点让人傻眼)。
一个完整的检查static final型的名字必须大写的检查器如下:

public class WordSpellDetector extends Detector implements Detector.JavaPsiScanner {

    /**
     * 这是一个Issue声明
     */
    public final static Issue ISSUE = Issue.create("WordSpellStaticFinalUpper",
            "Static final field should be all uppercase",
            "Static final field should be all uppercase as our specification",
            Category.CORRECTNESS,
            9,
            Severity.WARNING,
            new Implementation(WordSpellDetector.class,
                    Scope.JAVA_FILE_SCOPE));
    @Override
    public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
        return Arrays.asList(PsiField.class);//声明为检查成员变量类型节点
    }

    @Override
    public JavaElementVisitor createPsiVisitor(JavaContext context) {
        return new WordSpellChecker(context);//返回本检查器真正的检查执行者
    }

    private class WordSpellChecker extends JavaElementVisitor {
        private JavaContext mContext;
        WordSpellChecker(JavaContext context){
            this.mContext = context;
        }

        @Override
        public void visitField(PsiField field) {//实现哪个方法,需要跟getApplicablePsiTypes()返回的类型对应
            if(field==null)return;
            PsiElement[] children = field.getChildren();
            if(children!=null){
                for (int i = 0; i < children.length; i++) {
                    if(!isAllUpper(field.getName())
                            && children[i].getText().contains("final") &&children[i].getText().contains("static")){
                        mContext.report(ISSUE, mContext.getLocation(field), "field name with static final should be all uppercase");
                    }
                }
            }
            super.visitField(field);
        }
    }
    private boolean isAllUpper(String str){
        if(str==null)return true;
        char[] chars = str.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            if(chars[i]>='a'&& chars[i]<='z')return false;//只判断有没有小写字母,不限定其它特殊字符
        }
        return true;
    }
}

至此,所有的编码已经完成。相关的api和要注意的地方我在代码注释里都提示了,具体还需要大家去探索它的api了。

最后要做的就是把java library构建出jar文件,放在系统用户路径下的.android/lint/子目录中,名称任意,只要是jar文件ok的。但这样会对整个studio的project都生效。如果想更好地管理这个lint文件的话,可以另外创建一个android library,把jar文件一起编译进去打出aar包或者直接依赖library到主module里即可,这些可以百度一下挺多讲到这里。
最后的效果是这样:
这里写图片描述

温馨提醒:
不管lint文件是直接放在.android目录下,还是用aar等形式,最后都需要重启android
studio才能生效。我现在用的studio 是2.3.1版本。

相关参考文章:
http://www.tuicool.com/articles/qaEZfiI
https://engineering.linkedin.com/android/writing-custom-lint-checks-gradle
http://blog.csdn.net/u010360371/article/details/50189171
http://tools.android.com/tips/lint-custom-rules
https://github.com/yangcheng/CustomLint
https://github.com/yangcheng/LintDemoApp
http://www.tuicool.com/articles/nY3Ary
https://juejin.im/entry/576b61162e958a0078c637fb

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值