存粹个人看法哦,静态扫描我觉得是一个在开发过程中就去避免掉一部分bug的重要的工具。但是对这方面的介绍的文章还是有点少,我其实写的也不怎么样,但是起码集思广益,互相提高吧。
我之前写的Lint的文章,只从实现层之类的去描述了下如何自定义一个lint扫描规则,但是也没有说清楚什么lint到底是基于什么去写的,这边进一步进行一次补充。
抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无文文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。
我们简单的从这张图来看下java的AST的过程。
一个个读取源代码,按照预定规则合并成 Token,Token 是编译过程的最小元素,关键字、变量名、字面量、运算符等都可以成为 Token。
语法树的每一个节点都代表着程序代码中的一个语法结构,如类型、修饰符、运算符等。经过这个步骤后,编译器就基本不会再对源码文件进行操作了,后续的操作都建立在抽象语法树之上。
apt的过程就是在源代码被转化成ast之后执行的对注解的一次process,所以我们能在apt的过程中获取到所有注解类以及注解的类信息相关。
java同学们熟悉的lombok库,就是基于ast语法树的一个修改,详细的可以参考下这篇文章的修改方式。
public boolean isSubType(Element element, String className) {
return element != null && isSubType(element.asType(), className);
}
复制代码
其实我最近就一直有一种奇怪的感觉,为什么Apt的element和lint的感觉非常的相似,哈哈哈。就像上面这种apt的时候的类型判断代码一样。
而对于Android Lint来说,它本质上就是AST抽象语法树,通过语法树获取到所有代码的节点,之后对其进行自定义的逻辑判断,举个例子,当前类是不是符合了特定标准,比如是不是一个构造器,是不是一个方法,方法名是什么之类的,当符合特定规则之后就会抛出一个Issue。
在Android Lint迭代过程中,扫描源代码的Scanner先后经历了三个版本的AST。
UAST是JetBrains在IDEA新版本中用于替换PSI的API。UAST更加语言无关,除了支持Java,还可以支持Kotlin。
UAST is short for "Universal AST" and is an abstract syntax tree library which abstracts away details about Java versus Kotlin versus other similar languages and lets the client of the library access the AST in a unified way. UAST isn't actually a full replacement for PSI; it augments PSI. Essentially, UAST is used for the inside of methods (e.g. method bodies), and things like field initializers. PSI continues to be used at the outer level: for packages, classes, and methods (declarations and signatures). There are also wrappers around some of these for convenience.
我仔细阅读了下官方对于uast的定义,首先正如开篇所说,UAST是一个更普遍的AST,其适用范围不仅仅局限于java代码,同时还能支持kotlin以及起来相似语言。
但是PSI也并不完全就是已经被UAST所取代的趋势,还是可以拿来做一些别的简单的java扫描工作的。
以我个人的开发经验来看,我会从Android原生提供的Lint规则中去寻找可能适合我的逻辑。举个例子,我之前在使用埋点的时候我不小心给字符串前面加了个空格,我这个时候就会反思,是不是可以通过静态扫描的方式去搞,但是这个时候api不熟悉怎么办呢??
谁家代码不是抄呀,哈哈哈。其实我之前在用TextView的时候发现当我直接设置一个字符串进去的时候lint会爆黄。有思路就可以抄代码,我去找到了SetTextDetector
,然后我就根据其中的代码,完成了这个静态扫描工具的开发。
public class EventSpaceDetector extends Detector implements Detector.UastScanner {
static final Issue ISSUE = Issue.create(
"event_space_issue", //唯一 ID
"埋点不允许出现空格", //简单描述
"你不知道有时候卵用空格会出问题的吗", //详细描述
Category.CORRECTNESS, //问题种类(正确性、安全性等)
6, //权重
Severity.WARNING, //问题严重程度(忽略、警告、错误)
new Implementation( //实现,包括处理实例和作用域
EventSpaceDetector.class,
Scope.JAVA_FILE_SCOPE));
private final String packageName = "com.kronos.sample";
@Override
public List> getApplicableUastTypes() {
List> types = new ArrayList<>();
types.add(UCallExpression.class);
return types;
}
@Override
public UElementHandler createUastHandler(@NotNull JavaContext context) {
return new UElementHandler() {
@Override
public void visitCallExpression(@NotNull UCallExpression node) {
checkIsConstructorCall(node);
}
private void checkIsConstructorCall(UCallExpression node) {
if (!UastExpressionUtils.isConstructorCall(node)) {
return;
}
UReferenceExpression classRef = node.getClassReference();
if (classRef != null) {
String className = UastUtils.getQualifiedName(classRef);
String value = packageName + ".Event";
List args = node.getValueArguments();
for (UExpression element : args) {
if (element instanceof ULiteralExpression) {
Object stringValue = ((ULiteralExpression) element).getValue();
if (stringValue instanceof String && stringValue.toString().contains(" ")) {
if (!TextUtils.isEmpty(value) && className.equals(value)) {
context.report(ISSUE, node, context.getLocation(node),
"谁给你的胆子用空格的");
}
}
}
element.getExpressionType();
}
}
}
};
}
}
复制代码
原谅我的粗鄙啊,这个文本用的是过分了点。但是实际上我在SetTextDetector
中找到了ULiteralExpression
,这个就是当前的语法树中的变量值,我将它的value取出来之后,判断了下内容是否含有空格,如果有则在当前地方直接抛出一个issue。这样我就能让项目内所有给埋点的代码加了空格的做一次提醒,起码可以避免掉一部分开发的时候的粗心大意。
我个人看法UAST的资料网上真实的是不多的,所以开发如果要想写成特别复杂的这种扫描规则就必须要靠当前系统给我们提供的那些已经定义好的lint,然后去其中分析他们是如何写的,这样就可以写出你自己想要的自定义lint了。