java实现一个语法检查器_自定义lint与抽象语法树(AST)检查器

要做自定义的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里即可,这些可以百度一下挺多讲到这里。

最后的效果是这样:

5991bca931e2

温馨提醒:

不管lint文件是直接放在.android目录下,还是用aar等形式,最后都需要重启android

studio才能生效。我现在用的studio 是2.3.1版本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值