本文地址:http://www.jianshu.com/p/5991bca931e2
要做自定义的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 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> 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> 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版本。
相关代码,可访问我的github,欢迎加星:
https://github.com/IvesWang/CustomLint
https://github.com/IvesWang/lint-rules
相关参考文章:
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