阅读 841

再谈Android Lint

先把上一篇自定义lint的文章链接放在开头

自定义lintDemo项目

存粹个人看法哦,静态扫描我觉得是一个在开发过程中就去避免掉一部分bug的重要的工具。但是对这方面的介绍的文章还是有点少,我其实写的也不怎么样,但是起码集思广益,互相提高吧。

我之前写的Lint的文章,只从实现层之类的去描述了下如何自定义一个lint扫描规则,但是也没有说清楚什么lint到底是基于什么去写的,这边进一步进行一次补充。

AST(Abstract Syntax Tree)抽象语法树

抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无文文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。

我们简单的从这张图来看下java的AST的过程。

步骤一:词法分析,将源代码的字符流转变为 Token 列表。

一个个读取源代码,按照预定规则合并成 Token,Token 是编译过程的最小元素,关键字、变量名、字面量、运算符等都可以成为 Token。

步骤二:语法分析,根据 Token 流来构造树形表达式也就是 AST。

语法树的每一个节点都代表着程序代码中的一个语法结构,如类型、修饰符、运算符等。经过这个步骤后,编译器就基本不会再对源码文件进行操作了,后续的操作都建立在抽象语法树之上。

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

而对于Android Lint来说,它本质上就是AST抽象语法树,通过语法树获取到所有代码的节点,之后对其进行自定义的逻辑判断,举个例子,当前类是不是符合了特定标准,比如是不是一个构造器,是不是一个方法,方法名是什么之类的,当符合特定规则之后就会抛出一个Issue。

在Android Lint迭代过程中,扫描源代码的Scanner先后经历了三个版本的AST。

  1. 最开始使用的是JavaScanner,Lint通过Lombok库将Java源码解析成AST(抽象语法树),然后由JavaScanner扫描。

  2. 在Android Studio 2.2和lint-api 25.2.0版本中,Lint工具将Lombok AST替换为PSI,同时弃用JavaScanner,推荐使用JavaPsiScanner。

    PSI是JetBrains在IDEA中解析Java源码生成语法树后提供的API。相比之前的Lombok AST,可以支持Java 1.8、类型解析等。使用JavaPsiScanner实现的自定义Lint规则,可以被加载到Android Studio 2.2+版本中,在编写Android代码时实时执行。

  3. 在Android Studio 3.0和lint-api 25.4.0版本中,Lint工具将PSI替换为UAST,同时推荐使用新的UastScanner。

UAST

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扫描工作的。

在不熟悉API的情况下如何更好的写一个Lint呢?

以我个人的开发经验来看,我会从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<Class<? extends UElement>> getApplicableUastTypes() {
        List<Class<? extends UElement>> 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<UExpression> 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了。