Android自定义Lint实践2——改进原生Detector

上篇博客《Android自定义Lint实践》中我们介绍了美团App如何使用自定义Lint进行代码检查。

在使用Lint的过程中,我们陆续又发现原生Lint的一些问题和缺陷,本文将介绍我们在实践中提出的解决方案。

完善JDK 7泛型新写法下的HashMap检测

上一篇博客中我们提到了对于HashMap检测的改进,但当时我们也在文章中提到:

代码很简单,总体就是获取变量定义的地方,将泛型值传入原先的检测逻辑。 当然这里的增强也是有局限的,比如这个变量是成员变量,向前的推断就会有问题,这点我们还在持续的优化中。

即:当时的检测解决了变量声明和变量赋值在一起的HashMap检测问题。但对于两者不在一起的情况,我们仍然无法检测到。

示例代码如下:

public static void testHashMap() {    //这种情况可以用上篇博客的检查搞定
    Map<Integer, String> map1 = new HashMap<>();
    map1.put(1, "name");    //这种找不到map2的变量声明,所以用上篇博客的检查是无法判断的
    map2 = new HashMap<>();
    map2.put(2, "name2");
}

通过我们的探索,目前已经解决了这个问题。

下面我们来详细介绍下:

我们需要解决的情况

  1. 在同一个类中 public Map<Integer, String> map; public static Map<Integer, String> map2; public void test() { // 1: 成员变量 map = new HashMap<>(); map.put(1, "name"); // 2: 静态变量 map2 = new HashMap<>(); map2.put(1, "name"); }
  2. 方法参数 public void test1(Map<Integer, String> map) { map = new HashMap<>(); map.put(1, "name"); }
  3. 变量声明在另一个类中 public class HashMapCase4_2 { public void test() { // 1: 另一个类的静态变量 HashMapCase4_1.map2 = new HashMap<>(); HashMapCase4_1.map2.put(1, "name"); // 2: 另一个对象的成员变量 HashMapCase4_1 case4_1 = new HashMapCase4_1(); case4_1.map = new HashMap<>(); case4_1.map.put(1, "name"); // 3: 内部类静态变量 Sub.map2 = new HashMap<>(); // 4: 内部类对象的成员变量 Sub sub = new Sub(); sub.map = new HashMap<>(); } private static class Sub { public Map<Integer, String> map; public static Map<Integer, String> map2; } }

解决方案

在Google官方提供的资料:Writing a Lint Check中我们发现了如下描述:

In the next version of lint (Tools 27, Gradle plugin 0.9.2+, Android Studio 0.5.3, and ADT 27); Java AST parse tree detectors can both resolve types and declarations. This was just added to lint, and offers new APIs where you can ask for the resolved type, and the resolved declaration, of a given AST node.

这里提到了resolved type,那究竟有什么用呢?

Google在描述中留下当时的commit,其中提到:

Add type and declaration resolution to Lint's Java AST The AST used by lint, Lombok AST, does not contain type information. That means code which for example sees this code: getContext().checkPermission(name) can't find out which "checkPermission" method this is. That requires full type resolution.

根据官方描述,我们可以拿到方法属于哪个类。那resolved type是否可以帮助我们通过变量拿到变量声明呢?

在参考了commit中的代码后,我们尝试使用context.resolve来解析第一种情况中的变量map

结果证实确实帮我们解析到了变量声明的类型。

但它可以帮我们把所有情况都分析到么?我们带着怀疑的态度继续尝试,结果发现在第三种情况的case4_1.mapsub.map出现了问题:

即只分析到了map所属的对象,而无法拿到map的类型。

显然,这个解析出来的节点不仅没有帮助我们,反而让我们偏离了我们要分析的节点。

在查看JavaContext相关代码后我们发现,除了resolve还有一个getType方法,似乎从名字上看可以解决我们的问题。

@Nullable
public ResolvedNode resolve(@NonNull Node node) {
    return mParser.resolve(this, node);
}
@Nullable
public TypeDescriptor getType(@NonNull Node node) {
    return mParser.getType(this, node);
}

尝试后发现,getType适合我们列出的所有情况。

那么,两者区别是什么呢?

通过对Android Gradle Plugin(下文中称Plugin)中Lint相关代码的分析,我们发现:

在Plugin中,Lint检查依靠ECJ(Eclipse Compiler for Java)来生成抽象语法树,上文代码中提到的mParser在Plugin中对应的是EcjParser

解析时,对于case4_1.mapsub.map两个节点,resolve利用的是binding,而getType调用的是resolvedType

Bindings是ECJ一个强大的功能,有很多子类型,例如VariableBindingTypeBinding等。

对于同一个节点可能还有多个binding(例如QualifiedNameReferenceotherBindings会存放多个,上述例子中可以看到其实有case4_1.mapmap类型,但在otherBindings中);而resolvedTypeTypeBinding。显然,使用resolvedType可以确保我们拿到的是类型。

这里还需要注意的是:虽然上述分析中,我们提到的这些是由ECJ提供的,且Lint中的Node也保留了拿到ECJ Node的能力,即:getNativeNode。但并不推荐大家直接使用ECJ。

因为Lint使用tnorbye/lombok.ast的本意就是不依赖具体的Parser(Writing a Lint Check中提到,他们曾经使用了多种parser),上层Detector应尽量使用Lombok AST。

解决Retrolambda下Toast检测误报

美团App使用了Retrolambda,当然为了在Retrolambda下Lint能正常运行,我们引入了evant/android-retrolambda-lombok,替换官方AST(抽象语法树)为Retrolambda实现的AST。

但在lambda中写Toast经常会提示没有show, 示例如下:

public void test() {
    findViewById(R.id.button).setOnClickListener(view -> Toast.makeText(MainActivity.this, "xxx", Toast.LENGTH_SHORT).show());
}

Lint检查报告:Toast created but not shown: did you forget to call show() ?

从代码可以看到,虽然我们写了show,但还是检测说没有show。

这时候如果把Toast相关的代码抽离成单独的方法,检测就又会恢复正常。于是我们决定分析下究竟发生了什么?

通过gradle debug,我们发现ToastDetector在寻找包围Toast方法时出现了问题。

Node method1 = JavaContext.findSurroundingMethod(node.getParent());

而findSurroundingMethod方法的实现如下:

@Nullablepublic static Node findSurroundingMethod(Node scope) {    while (scope != null) {
        Class<? extends Node> type = scope.getClass();        // The Lombok AST uses a flat hierarchy of node type implementation classes
        // so no need to do instanceof stuff here.
        if (type == MethodDeclaration.class || type == ConstructorDeclaration.class) {            return scope;
        }
        scope = scope.getParent();
    }    return null;
}

到这里总结一下:

当ToastDetector找到Toast的时候,它会寻找外围的方法,如果是匿名内部类的方法或者其他方法时,他能够判断到并返回这个节点。

但是对于lambda来说,它只能查找到最外层的方法,也就是示例中setOnClickListener外围的test方法,lambda并不会被识别到。

lambda在语句附近能识别到的是lombok.ast.LambdaExpression,而不是MethodDeclaration或者ConstructorDeclaration,所以会一直找到test这个MethodDeclaration

问题搞清楚了,解决办法也就有了:

我们加入一个LambdaExpression判断,提前返回,这样就可以正常识别了。

private static boolean isLambdaExpression(Class type) {    return "lombok.ast.LambdaExpression".equals(type.getName());
}

这里需要说明的是,我们用字符串比对而不是跟MethodDeclaration一样去比对class,这是为了更好的兼容所有使用者。

因为LambdaExpression是由Retrolambda的AST提供,并不是官方的AST。也就是说如果我们想判断class就必须依赖Retrolambda的AST,我们之前也提到过自定义Lint输出的是一个JAR,并不包含这些依赖,运行时环境中如果没有使用Retrolambda AST的话就会直接ClassNotFound。

所以,这里我们选择了字符串比对,达成目标的同时,也让检测变得更简单。

Detector写好了,但是与HashMap的增强不同,ToastDetector这个实现只能选择替换掉系统实现。因为HashMap两者是增强,可以共存;而ToastDetector如果系统检测正常运行的话,遇到这种情况就会报错。所以我们反射修改内置IssueRegistry(BuiltinIssueRegistry) 完成系统Detector的替换。

相关代码

本文相关示例源码已经开放,见:MeituanLintDemo

参考文献

原文发布于微信公众号 - 美团点评技术团队(meituantech)

原文发表时间:2017-03-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java Web

Java 面试知识点解析(四)——版本特性篇

755
来自专栏点滴积累

Python扩展方法一二事

前言 跟着一个有强迫症的老板干活是一件极其幸福的事情(你懂的)。最近碰到一个问题,简单的说就是对一个对象做出部分修改后仍然返回此对象,于是我就写了一个方法,老板...

3326
来自专栏web前端教室

javascript设计模式 -- 工厂模式

工厂模式哈,看了半天感觉大概意思就是说,有这么个函数,它会创建什么样的实例出来, 完全是取决于你传了什么样的参数进去。 创建出来的这些实例,都拥有相同的接口,就...

1987
来自专栏博客园

深入浅出话属性

程序的本质就是“数据+算法”,或者说用算法来操作数据来得到自己想要的结果。在程序中,数据表现为各种各样的变量,算法则表现为各种各样的函数(操作符是函数的简记法)...

1273
来自专栏分布式系统和大数据处理

悟透JavaScript

这本书分为了三个部分,第一部分“JavaScript真经”主要讲解JavaScript的一些核心概念,主要是数据类型、函数、原型、对象。并通过在JavaScri...

724
来自专栏walterlv - 吕毅的博客

不再为命名而苦恼!使用 MSTestEnhancer 单元测试扩展,写契约就够了

发布于 2018-02-22 11:52 更新于 2018-08...

711
来自专栏java工会

如何优雅的设计 Java 异常

正如我们所知道的,java中的异常的超类是java.lang.Throwable(后文省略为Throwable),它有两个比较重要的子类,java.lang.E...

820
来自专栏向治洪

Swift 3.0介绍

概述 我接触swift是从2.0开始,当时出于对ios的好奇,加上官方的大力推荐,于是扎入了ios的怀抱,从1.2发展到了今天的3.0.1,这期间由于Swift...

1768
来自专栏编程

Java后台编程初学者,这些常识你都知道吗?

小编也是一位Java后台编程初学者,以后每天利用下班时间来给大家分享一下Java编程中的一些常识,希望有心学习的可以多看一眼,如果你是高手欢迎指点文中小编的不足...

1799
来自专栏软件开发 -- 分享 互助 成长

原型模式C++类的复制构造函数和赋值运算符

一、简介 1、原型模式,用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 2、为什么会用到原型模式? (1)既然可以直接new,为什么会用到原型...

1945

扫码关注云+社区