上篇博客 中我们介绍了美团 App 如何使用自定义 Lint 进行代码检查。
在使用 Lint 的过程中,我们陆续又发现原生 Lint 的一些问题和缺陷,本文将介绍我们在实践中提出的解决方案。
上一篇博客中我们提到了对于 HashMap 检测的改进,但当时我们也在文章中提到:
代码很简单,总体就是获取变量定义的地方,将泛型值传入原先的检测逻辑。
当然这里的增强也是有局限的,比如这个变量是成员变量,向前的推断就会有问题,这点我们还在持续的优化中。
即:当时的检测解决了变量声明和变量赋值在一起的 HashMap 检测问题。但对于两者不在一起的情况,我们仍然无法检测到。
示例代码如下:
- public static void testHashMap() {
- //这种情况可以用上篇博客的检查搞定
- Map < Integer,
- String > map1 = new HashMap < >();
- map1.put(1, "name");
- //这种找不到map2的变量声明,所以用上篇博客的检查是无法判断的
- map2 = new HashMap < >();
- map2.put(2, "name2");
- }
通过我们的探索,目前已经解决了这个问题。
下面我们来详细介绍下:
- 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");
- }
- public void test1(Map<Integer, String> map) {
- map = new HashMap<>();
- map.put(1, "name");
- }
- 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 官方提供的资料: 中我们发现了如下描述:
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 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 在描述中留下当时的 ,其中提到:
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.map
出现了问题:
- sub.map
即只分析到了
所属的对象,而无法拿到
- map
的类型。
- map
显然,这个解析出来的节点不仅没有帮助我们,反而让我们偏离了我们要分析的节点。
在查看 相关代码后我们发现,除了
还有一个
- 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)来生成抽象语法树,上文代码中提到的
在 Plugin 中对应的是
- mParser
。
- EcjParser
解析时,对于
和
- case4_1.map
两个节点,
- sub.map
利用的是
- resolve
,而
- binding
调用的是
- getType
(注意:这里的
- resolvedType
是 ECJ 中的变量)。
- resolvedType
是 ECJ 一个强大的功能,有很多子类型,例如
、
- VariableBinding
等。
- TypeBinding
对于同一个节点可能还有多个 binding(例如
的
- QualifiedNameReference
会存放多个,上述例子中可以看到其实有
- otherBindings
中
- case4_1.map
类型,但在
- map
中);而
- otherBindings
是
- resolvedType
。显然,使用
- TypeBinding
可以确保我们拿到的是类型。
- resolvedType
这里还需要注意的是:虽然上述分析中,我们提到的这些是由 ECJ 提供的,且 Lint 中的 Node 也保留了拿到 ECJ Node 的能力,即: 。但并不推荐大家直接使用 ECJ。
因为 Lint 使用 的本意就是不依赖具体的 Parser( 中提到,他们曾经使用了多种 parser),上层 Detector 应尽量使用 Lombok AST。
美团 App 使用了 Retrolambda,当然为了在 Retrolambda 下 Lint 能正常运行,我们引入了 ,替换官方 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 方法的实现如下:
- @Nullable
- public 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
方法,lambda 并不会被识别到。
- test
lambda 在语句附近能识别到的是
,而不是
- lombok.ast.LambdaExpression
或者
- MethodDeclaration
,所以会一直找到
- ConstructorDeclaration
这个
- test
。
- MethodDeclaration
问题搞清楚了,解决办法也就有了:
我们加入一个
判断,提前返回,这样就可以正常识别了。
- LambdaExpression
- private static boolean isLambdaExpression(Class type) {
- return "lombok.ast.LambdaExpression".equals(type.getName());
- }
这里需要说明的是,我们用字符串比对而不是跟
一样去比对 class,这是为了更好的兼容所有使用者。
- MethodDeclaration
因为
是由 Retrolambda 的 AST 提供,并不是官方的 AST。也就是说如果我们想判断 class 就必须依赖 Retrolambda 的 AST,我们之前也提到过自定义 Lint 输出的是一个 JAR,并不包含这些依赖,运行时环境中如果没有使用 Retrolambda AST 的话就会直接 ClassNotFound。
- LambdaExpression
所以,这里我们选择了字符串比对,达成目标的同时,也让检测变得更简单。
Detector 写好了,但是与 HashMap 的增强不同,ToastDetector 这个实现只能选择替换掉系统实现。因为 HashMap 两者是增强,可以共存;而 ToastDetector 如果系统检测正常运行的话,遇到这种情况就会报错。所以我们反射修改内置 IssueRegistry(
) 完成系统 Detector 的替换。
- BuiltinIssueRegistry
本文相关示例源码已经开放,见: 。
来源: http://www.tuicool.com/articles/j6BnM32