JavaScanner适用于扫描源码的,如源码中流的close操作是否在finally块中,成员的命名规范等,另外也可以选择在对象创建(构建方法调用)时,方法触发时进行检查。
使用JavaScanner需要使用JAVA_FILE或JAVA_FILE_SCOPE的scope,先来看看JavaScanner的接口声明:
1 | public interface JavaScanner { |
使用JavaScanner出现得最多的就是Node跟Visitor,Node是一相对于抽象语法树(Abstract Syntax Tree,AST)的一个概念,一段代码可以对应一棵语法树,将上下嵌套映射到树上的父子结果,使得在语法树上可以在任一结点获取到其上下文结点的关系,而Visitor就是用来实现各种检查的;lint的扫描就是基于语法树来进行的,我们进行检查也是基于同样的抽象来操作的。
如下代码:
1 | if (1 == 1 && 2 == 2) { |
按节点遍历后,可以得到以下的结点关系图:
关于AST我了解得不够深入,需要再了解清楚再细说。
一个一个来,getApplicableNodeTypes返回的是需要监控的节点(Node),返回的一个class对象的列表,这些class对象都是属于Node的子类,实际上我们较常用的是AbstractNode的子类,这些类都定义在lombok-ast的库里(/lombok/ast路径下),示例中我们取了几个代表:
1 | public List<Class<? extends Node>> getApplicableNodeTypes() { |
Try,If,For,Constructorinvocation都是继承于AbstractNode的子类,可以在lombok.ast中找到其它的定义;对应于源码中的操作也是很明显的,就是在源码中存在try,if,for,以及对象构建时,对应的visitor方法就会被触发。而这些触发的方法就是在createJavaVisitor方法里定义的。
createJavaVisitor方法需要返回一个自定义的AstVisitor子类的实例,这个子类里需要实现我们需要监控的各个方法,每一个方法里,如果返回true则表示这个事件已经处理了,反之没有。如果返回false,当前这个没有处理的节点的子节点都会自动传递到这个类的实现中;另外,当所有的子结节都已经遍历完了,endVisit(lombok.ast.Node)会被调用以通知所有孩子结点都已经被访问。
这里我们使用ForwardingAstVisitor来处理处理这种访问。
1 | public AstVisitor createJavaVisitor(@NonNull JavaContext javaContext) { |
如此,当代码中遇到if,try,for或者对象创建时,就会触发对应的方法,我们就可以根据对应传入的Node参数来进行一些检查操作。
比方说,我们在使用for时,规定要使用括号扩上,可以这么操作
1 | public boolean visitFor(For node) { |
通过检查for节点的最后一个孩子,获取到body内容,然后根据内容是否前后有”{“和”}”来判断是否符合规则,不符合,则抛出错误。
如果我们需要在特定的方法触发时进行检查,可以使用getApplicableMethodNames来设定哪些方法名需要拦截。它返回的是一个字符串的list,假如我们需要捕获”put”方法的调用,则可以这样:
1 | public List<String> getApplicableMethodNames() { |
它对应的检查方法是visitMethod
1 | public void visitMethod(JavaContext javaContext, AstVisitor astVisitor, MethodInvocation methodInvocation) |
JavaContext: 上下文
AstVisiitor: 假如有实现createJavaVisitor方法,则返回里面的实例
MethodInvocation: 方法执行时的Node
其中,methodInvocation实例中包含了当前方法的一些信息,如获取方法名字可以使用
1 | methodInvocation.astName().astValue() |
然而MethodInvocation属于ast中的类,在lint中使用时可以使用JavaContext将其转换成lint的对象
1 | JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod)javaContext.resolve(methodInvocation); |
使用JavaContext的resolve方法可以将Node转换成JavaParser.ResolvedXXX的对象,其中,JavaParser提供了几种Resolved类型
1 | JavaParser.ResolvedAnnotation |
关键在合适的时候将Node转换成JavaParser.ResolvedXXX对应类型。
在demo中,我想检查 PersistHelper.put(PersistKey.INIT_DATA, “Data”);执行时第一个参数的注解信息。
1 | public void visitMethod(JavaContext javaContext, AstVisitor astVisitor, MethodInvocation methodInvocation) { |
开始时通过astName().astValue()获取到执行方法的名字,并将node节点转换成JavaParser.ResolvedMethod对象,并执行到这个方法的所属类(method.getContainingClass()),获取到的是一个JavaParser.ResolvedClass对象。
注意,在上下文类中有一个静态方法 findSurroundingClass ,同样也可以找到一个类,但这个类返回的是一个ClassDeclaration (extends Node)对象,对应的是拦截到node是被类一个类包裹,如demo中,它是被MainActivity包裹的,则这里返回的就是MainActivity的Node信息,同样也可以使用JavaContext的resolve转换成JavaParser.ResolvedClass来操作。
接下来的操作就是从methodInvocation中获取参数列表,每一个参数都是一个Expression对象,同样也可以将其转换成JavaParser.ResolvedField对象,从而可以获取到这个field的Annotation信息(这点跟Java的反射类似),获取到JavaParser.ResolvedAnnotation信息后,进一步分解,使用getValue可以获取到注解上的参数对应的值,从而进行对比。
从两个例子来看,JavaScanner着重的是静态代码本身的信息的扫描,如编码规范是否符合,节点上下文是否与预期一致,等等。
[link]