自定义lint

来源:互联网 发布:做淘宝赚钱吗 编辑:程序博客网 时间:2024/06/09 22:44

要做自定义的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

原创粉丝点击