自从ADT 16第一次引入Android Lint(以下简称:Lint)以来,Lint便成为Android平台上最重要的静态代码扫描工具。与早期基于XPath的静态扫描工具不同,Lint基于AST(Abstract Syntax Tree)进行分析,可以用来定制很复杂的扫描规则。
关于Lint的介绍,网上已有很多文章可参考。关于Lint规则的定制,也是个复杂的话题,这里不展开。本文着眼点在于Lint使用的AST Parser,这是整个静态扫描最核心的部分。
Lint从第一个版本就选择了lombok-ast作为自己的AST Parser,并且用了很久。但是Java语言本身在不断更新,Android也在不断迭代出新,lombok-ast慢慢跟不上发展,所以Lint在25.2.0版增加了IntelliJ的PSI(Program Structure Interface)作为新的AST Parser。但是PSI于IntelliJ、于Lint也只是个过渡性方案,事实上IntelliJ早已开始了新一代AST Parser,UAST(Unified AST)的开发,而Lint也将于即将发布的25.4.0版中将PSI更新为UAST。
因为Lint发布时间较久,已有很多研发人员基于lombok-ast开发了大量定制规则,出于兼容目的,Lint一直到现在都还保留着lombok-ast,也就是说,从25.2.0之后,一直是新旧两套AST Parser并存。我们现在就来看一看这个变迁的过程。
1、lombok-ast
说起Project lombok,这也是Java世界里一个很经典的项目了。通过一组强大的annotation,它可以在编译时修改编译器前端生成的AST,最终生成符合JVM规范的字节码。可以简单地理解为,lombok的annotation,实际上创造了一个语法简化版的Java语言,帮助提升开发效率,lombok会在编译阶段将这种简化的语言翻译成真正的Java,保证其兼容性。
lombok从一开始就同时支持两大主流Java编译器,javac和ECJ(Eclipse Compiler for Java)。这两个编译器虽然都可以生成符合JVM规范的字节码,但它们的实现是完全不同的。这使得lombok团队不得不建立了一个子工程lombok-ast,专门用于封装特定编译器的AST实现细节。也正是由于它的编译器无关特性,Lint选择了这个模块。
lombok-ast虽然功能很强,但是到今天它越来越不够用了,具体地说,有这么几个缺点:
1.1、对Java语言只支持到Java 6
lombok-ast的更新维护太慢了,它对Java语言的支持只能完美支持到Java 6 …… 对于Java 7、Java 8的新特性,根本不支持 …… 可是我们都看到,Android从4.4就开始支持Java 7了,从6.0开始支持Java 8,如果你装了Android Studio >= 2.1和SDK >= 24,开发目标版本较低的应用一样可以用某些Java 8特性。眼看Java 9就要发布了,lombok-ast还只支持到Java 6 ……
例如Java 8里一个大家喜闻乐见的新特性,lambda表达式
public void demoThreadUseLambda() {
new Thread(() -> {
System.out.println("Thread running ......");
}).start();
}
lombok-ast是不支持的,如果你试图自己写个Lint规则,检测其中的内联类,语法上是根本做不到的。
于是,有人自己扩充了lombok-ast的语法支持,让其能支持到Java 8,这其中做的比较好的例如 android-retrolambda-lombok 。但这样也有问题,Lint官方版不认啊 …… 你只能自己本地改代码。
但是这个还算是可以自己修改的,更不能忍的是下面这点。
1.2、不支持类型解析
现代编译器的特性里,Type Resolver / Declaration Resolver已经是标配,毕竟高级语言普遍支持多态,判断一个接口或抽象类型的实例到底是什么类型是很必要的。
但是lombok-ast却不支持这点 …… 当然说不支持并不完全准确,它里面其实是有一个
lombok.ast.resolve.Resolver
的,但是其能力基本可以让人放弃。只能说,lombok这个项目,从它最初的目的来看,它还真不需要多么高级的类型解析。因为它本来的用途就不是代码静态分析,光是修改AST这点,还真的只需要简单的符号分析就够了。
于是我们在Lint里就看到了Google做的一个有点奇葩的设计,ResolvedNode机制。
package com.android.tools.lint.client.api;
public abstract class JavaParser {
public abstract ResolvedNode resolve(@NonNull JavaContext context, @NonNull Node node);
/** A resolved declaration from an AST Node reference */
public abstract static class ResolvedNode {
......
}
/** A resolved class declaration (class, interface, enumeration or annotation) */
public abstract static class ResolvedClass extends ResolvedNode {
......
}
/** A method or constructor declaration */
public abstract static class ResolvedMethod extends ResolvedNode {
......
}
/** A field declaration */
public abstract static class ResolvedField extends ResolvedNode {
......
}
/**
* An annotation <b>reference</b>. Note that this refers to a usage of an annotation,
* not a declaraton of an annotation. You can call {@link #getClassType()} to
* find the declaration for the annotation.
*/
public abstract static class ResolvedAnnotation extends ResolvedNode {
......
}
/** A package declaration */
public abstract static class ResolvedPackage extends ResolvedNode {
......
}
/** A local variable or parameter declaration */
public abstract static class ResolvedVariable extends ResolvedNode {
......
}
}
ResolvedNode机制是对lombok-ast的一个扩展,对于一个lombok-ast封装的AST节点
lombok.ast.Node
,Lint允许你通过
com.android.tools.lint.client.api.JavaParser#resolve()
将其解析成一个
com.android.tools.lint.client.api.JavaParser.ResolvedNode
节点,ResolvedNode就包含了AST节点的类型解析信息。
所以当我们基于lombok-ast定制规则时,只要规则稍复杂点,或者我们需要更精确的实现,我们就不得不面临一个尴尬的局面,就是我们必须在lombok-ast提供的AST节点树和Lint提供的ResolvedNode类型解析树之间来回切换,这两个树的结构是一一对应的,但是却是分离的 …… 想必Google自己也很受不了这点,所以后面才要换成PSI / UAST。
这里多说一句,可能有人感到好奇,Lint又是如何实现类型解析的呢?原因是Lint使用了ECJ作为lombok-ast的编译器实现,而ECJ在编译阶段其实已经为每个AST节点生成了类型解析信息并附加在节点上,也就是ECJ牛逼的binding机制。
package org.eclipse.jdt.internal.compiler;
public class Compiler implements ITypeRequestor, ProblemSeverities {
protected void internalBeginToCompile(ICompilationUnit[] sourceUnits, int maxUnits) {
......
try {
......
// diet parsing for large collection of units
CompilationUnitDeclaration parsedUnit;
......
if (this.totalUnits < this.parseThreshold) {
parsedUnit = this.parser.parse(sourceUnits[i], unitResult);
} else {
parsedUnit = this.parser.dietParse(sourceUnits[i], unitResult);
}
......
// initial type binding creation
this.lookupEnvironment.buildTypeBindings(parsedUnit, null /*no access restriction*/);
......
addCompilationUnit(sourceUnits[i], parsedUnit);
......
// binding resolution
this.lookupEnvironment.completeTypeBindings();
}
}
在Lint里,前述JavaParser的实现类是EcjParser,在其中读取lombok-ast提供的ECJ AST节点(也即lombok-ast所谓的Native Node),获取其binding信息,完成了最终的类型解析。
package com.android.tools.lint;
public class EcjParser extends JavaParser {
public ResolvedNode resolve(@NonNull JavaContext context, @NonNull Node node) {
Object nativeNode = getNativeNode(node);
if (nativeNode == null) {
return null;
}
if (nativeNode instanceof NameReference) {
return resolve(((NameReference) nativeNode).binding);
......
}
private ResolvedNode resolve(@Nullable Binding binding) {
if (binding == null || binding instanceof ProblemBinding) {
return null;
}
if (binding instanceof TypeBinding) {
TypeBinding tb = (TypeBinding) binding;
return new EcjResolvedClass(tb);
......
}
}