Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Android自定义Lint实践2——改进原生Detector

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

作者头像
美团技术团队
发布于 2018-03-12 12:33:28
发布于 2018-03-12 12:33:28
93100
代码可运行
举报
文章被收录于专栏:美团技术团队美团技术团队
运行总次数:0
代码可运行

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

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

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

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

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

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

示例代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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方法,似乎从名字上看可以解决我们的问题。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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, 示例如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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方法时出现了问题。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Node method1 = JavaContext.findSurroundingMethod(node.getParent());

而findSurroundingMethod方法的实现如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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判断,提前返回,这样就可以正常识别了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

参考文献

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-03-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 美团点评技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【技术博客】Android自定义Lint实践
概述 Android Lint是Google提供给Android开发者的静态代码检查工具。使用Lint对Android工程代码进行扫描和检查,可以发现代码潜在的问题,提醒程序员及早修正。 为保证代码质量,美团在开发流程中加入了代码检查,如果代码检测到问题,则无法合并到正式分支中,这些检查中就包括Lint。 为什么需要自定义 我们在实际使用Lint中遇到了以下问题: 原生Lint无法满足我们团队特有的需求,例如:编码规范。 原生Lint存在一些检测缺陷或者缺少一些我们认为有必要的检测。 基于上面的考虑,我们开
美团技术团队
2018/03/12
1.5K0
【技术博客】Android自定义Lint实践
android 自定义Lint
概述 Android Lint是Google提供给Android开发者的静态代码检查工具。使用Lint对Android工程代码进行扫描和检查,可以发现代码潜在的问题,提醒程序员及早修正。 为什么要自定义 我们在实际使用Lint中遇到了以下问题: 原生Lint无法满足我们团队特有的需求,例如:编码规范。 原生Lint存在一些检测缺陷或者缺少一些我们认为有必要的检测。 对于正式发布包来说,debug和verbose的日志会自动不显示。 基于上面的考虑,我们开始调研并开发自定义Lint。开发中我们希望开发者使用R
xiangzhihong
2018/02/06
1.5K0
android 自定义Lint
Lint Tool Analysis (3)
本系列的几篇源码分析文档意义不大,如果你正好也在研究lint源码,或者你想知道前面自定义lint规则中提出的那几个问题,抑或你只是想大致了解下lint的源码都有些什么内容的话,这些文章可能有还些作用,否则看了和没看差不多的,因为这几篇文章只是我在读源码的过程中记录下来的一些零碎的片段,方便以后看的时候能够迅速上手。
宅男潇涧
2018/08/01
1.1K0
Android自定义lint开发
Android Lint 是 SDK Tools 16(ADT 16)开始引入的一个代码扫描工具,通过对代码进行静态分析,可以帮助开发者发现代码质量问题和提出一些改进建议。除了检查 Android 项目源码中潜在的错误,对于代码的正确性、安全性、性能、易用性、便利性和国际化方面也会作出检查。
逮虾户
2020/10/15
9630
美团外卖Android Lint代码检查实践
总第237篇 2018年 第29篇 概述 Lint是Google提供的Android静态代码检查工具,可以扫描并发现代码中潜在的问题,提醒开发人员及早修正,提高代码质量。除了Android原生提供的几百个Lint规则,还可以开发自定义Lint规则以满足实际需要。 为什么要使用Lint 在美团外卖Android App的迭代过程中,线上问题频繁发生。开发时很容易写出一些问题代码,例如Serializable的使用:实现了Serializable接口的类,如果其成员变量引用的对象没有实现Serializab
美团技术团队
2018/06/07
2.3K3
再谈Android Lint
存粹个人看法哦,静态扫描我觉得是一个在开发过程中就去避免掉一部分bug的重要的工具。但是对这方面的介绍的文章还是有点少,我其实写的也不怎么样,但是起码集思广益,互相提高吧。
逮虾户
2020/10/15
1.7K0
再谈Android Lint
再谈Android Lint
存粹个人看法哦,静态扫描我觉得是一个在开发过程中就去避免掉一部分bug的重要的工具。但是对这方面的介绍的文章还是有点少,我其实写的也不怎么样,但是起码集思广益,互相提高吧。
逮虾户
2024/01/27
2591
再谈Android Lint
自定义枚举 --- Swagger文档展示
在其它两篇文章中,已经解决的自定义枚举在MyBatis以及Rest接口的转换,但是在Springfox中还存在问题,不能使用code来作为api。本文通过扩展Springfox,实现了对自定义枚举的良
十毛
2019/03/27
2.6K0
自定义枚举 --- Swagger文档展示
深入浅出 Eslint,告别 Lint 恐惧症
对于 Lint 配置的不了解导致项目中总是会莫名其妙的提示报错,这应该是大多数同学面临的窘境。
19组清风
2022/09/27
2K0
深入浅出 Eslint,告别 Lint 恐惧症
编译原理工程实践—04处理语义分析实现简易脚本解释器
上一章实现的简易语法分析器能够解析简单的表达式、变量声明和初始化语句、赋值语句,生成简化的AST。但距离一门真正的语言还相差甚远,例如未处理作用域、面向对象等等特性,这些往往是在语义分析阶段来处理的,本章将讲述语义分析的实现。
CS逍遥剑仙
2025/05/12
1110
Builtin Lint Detectors (1)
本文主要介绍的是Lint工具中自带的与Android开发相关的lint检查项,通过查看lint检查项的描述及其代码实现,我发现这里面存在不少应用开发编码的Best Practice,有些是平常编码中非常常见的错误(这类问题建议看下对应的参考资料),有些却有点隐晦(这类问题我们会仔细研究下),这些lint检查项对于我们在平常编码过程中都会有不少启发。
宅男潇涧
2018/08/01
7540
Lint Tool Analysis (2)
本系列的几篇源码分析文档意义不大,如果你正好也在研究lint源码,或者你想知道前面自定义lint规则中提出的那几个问题,抑或你只是想大致了解下lint的源码都有些什么内容的话,这些文章可能有还些作用,否则看了和没看差不多的,因为这几篇文章只是我在读源码的过程中记录下来的一些零碎的片段,方便以后看的时候能够迅速上手。
宅男潇涧
2018/08/01
2.4K1
Lombok使用指南
Lombok 是一款 Java 开发插件,使得 Java 开发者可以通过其定义的一些注解来消除业务工程中冗长和繁琐的代码,尤其对于简单的 Java 模型对象(POJO)。在开发环境中使用 Lombok 插件后,Java 开发人员可以节省出重复构建,诸如 hashCode 和 equals 这样的方法以及各种业务对象模型的 accessor 和 toString 等方法的大量时间。对于这些方法,Lombok 能够在编译源代码期间自动帮我们生成这些方法,但并不会像反射那样降低程序的性能。
一个正经的程序员
2022/04/11
1.2K0
Lombok使用指南
Android 自定义View实战系列 :时间轴
本文采用 自定义View & RecyclerView 实现时间轴,所以必须先了解相关知识:
Carson.Ho
2019/02/22
1.7K0
揭秘 Rollup Tree Shaking
Next-generation ES module bundler官网定义此为下一代ES模块捆绑器。
Careteen
2022/02/14
2.9K0
揭秘 Rollup Tree Shaking
AndroidLintWatchDog Custom-Lint 自定义Lint检查的实现
Android自定义Lint检查有效提升代码质量、避免人工的低级失误、规范代码,属于程序自动化的内容,这部分内容涉及的资料较少,但是实际意义重大,尤其是对有规模的团队而言。
open
2020/03/19
9430
AndroidLintWatchDog Custom-Lint 自定义Lint检查的实现
Lint Tool Analysis (1)
本系列的几篇源码分析文档意义不大,如果你正好也在研究lint源码,或者你想知道前面自定义lint规则中提出的那几个问题,抑或你只是想大致了解下lint的源码都有些什么内容的话,这些文章可能有还些作用,否则看了和没看差不多的,因为这几篇文章只是我在读源码的过程中记录下来的一些零碎的片段,方便以后看的时候能够迅速上手。
宅男潇涧
2018/08/01
1.2K0
Android 微信登录授权、微信分享
1.先去微信开放平台注册账号,然后创建应用,签名工具下载(在页面最下面),不细说。
yechaoa
2022/06/10
5.3K0
Android 微信登录授权、微信分享
Java词法树与自定义关键字 发布于
Java词法树(语法树)这种树形结构是Java源码的一种抽象表示,它以图形化的方式反映出代码的语法结构,从而帮助开发者更好地理解和解析程序的语义。
DioxideCN
2023/10/21
2590
Java词法树与自定义关键字
                        
                            发布于
历经14天自定义3个注解解决项目的3个Swagger难题
点开此文章的小伙伴们请注意了,本片文章针对的是对Swagger有一定基础的小伙伴;
手撕代码八百里
2020/10/26
1.2K0
相关推荐
【技术博客】Android自定义Lint实践
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验