一、为什么选择lint作为主要的检测工具
团队开发一个大型项目中,每一个Merge Request的时候都会进行code review,但是有些问题通过人眼去看很难发现,而且当一个比较大的需求的时候,因为代码量庞大也增加了code review的难度,如何解决这个问题,我们在参考了很多检测工具之后选择lint,来作为我们主要的检测工具
Lint功能强大
(1)检测范围广泛
-
Java
-
Kotlin
-
XML文件
-
图标
-
ProGuard 配置文件
(2)扩展性强,支持开发自定义Lint规则
(3)原生提供了几百个实用的Android相关检查规则
(3)有Google官方的支持,会和Android Sdutio一起完善
(4)Lint可以在编译时提示
(5)也可以生成Html或者xml报告
二.如何自定义lint规则
(1)介绍下Lint相关类的作用
Issue:用来声明一个Lint规则。
Detector:用于检测并报告代码中的Issue,每个Issue都要指定Detector。
Scanner:用于扫描并发现代码中的Issue,每个Detector可以实现一到多个Scanner。
IssueRegistry:Lint规则加载的入口,提供要检查的Issue列表。
(2)lint 规则实现类需要继承 Detector 并实现 Scanner 接口
Detector.UastScanner——扫描 Java 源码抽象语法树
Detector.ClassScanner——扫描 class 文件
Detector.BinaryResourceScanner——扫描二进制资源文件
Detector.ResourceFolderScanner——扫描资源文件
Detector.XmlScanner——扫描xml文件
Detector.GradleScanner——扫描gradle文件
Detector.OtherFileScanner——扫描其他类型文件
根据我们的需求实现不同的Detector
(3)这里我们简单做个示例
class ThreadDetector: Detector(), Detector.UastScanner {
companion object {
@JvmField
val ISSUE: Issue = Issue.create(
"NewThread",
"避免自己创建Thread",
"请勿直接调用new Thread(),建议使用统一的线程池管理工具类",
Category.CORRECTNESS,
7,
Severity.WARNING,
Implementation(ErrorHandleSubscriberDetector::class.java, Scope.JAVA_FILE_SCOPE)
)
}
override fun getApplicableConstructorTypes(): List<String>? {
return Collections.singletonList("java.lang.Thread")
}
override fun visitConstructor(context: JavaContext, node: UCallExpression, constructor: PsiMethod) {
context.report(ISSUE, node, context.getLocation(node), "请勿直接调用new Thread(),建议使用RXJava或统一的线程管理工具类")
}
}
三、如何做一个通用的自定义lint规则
首先在lint的规则中,主要有两种
-
要检测的内容(匹配规则,例如
java.lang.Thread
) -
提示纠错内容(命中规则后提示,例如:
请勿直接调用new Thread(),建议使用RXJava或统一的线程管理工具类
)
而在我们的代码里无论kotlin或者Java我们检测的内容大致分为如下
-
类
-
方法
-
属性字段
而lint检测匹配的内容,是根据被检测者路径来做匹配的
比如Log我们要检测的java.lang.Thread
或者com.google.gson.Gson
按照这个思路,我我可以先做一个匹配通过创建对象的一个规则
比如我们代码中,有很多可能要被替换的方法,通用的可以做一个map
class ConstructorDetector: Detector(), Detector.UastScanner {
private var constructorMap= mapOf(
"java.lang.Thread" to "请用线程池创建线程,避免直接new Thread()",
"com.google.gson.Gson" to "使用GsonUtil避免重复创建Gson对象",
)
companion object {
@JvmField
val ISSUE: Issue = Issue.create(
"NotNewUselessObject",
"避免直接创建不必要的对象",
"避免直接创建不必要的对象",
Category.CORRECTNESS,
7,
Severity.WARNING,
Implementation(ConstructorDetector::class.java, Scope.JAVA_FILE_SCOPE)
)
}
override fun getApplicableConstructorTypes(): List<String>? {
return constructorMap.keys.toList()
}
override fun visitConstructor(context: JavaContext, node: UCallExpression, constructor: PsiMethod) {
context.report(ISSUE, node, context.getLocation(node), getMessage(getConstructorClass(constructor)))
}
private fun getMessage(className: String): String {
return constructorMap[className]?:"缺少提示"
}
private fun getConstructorClass(constructor: PsiMethod):String {
var className=UastLintUtils.getQualifiedName(constructor)?:""
return if (className.isNullOrEmpty()) {
""
}else{
className.substring(0,className.lastIndexOf(".${constructor.name}"))
}
}
}
当然你可以把map配置成一个json文件,通过解析json来匹配lint规则。
我们通过配置了一个通用的创建建对象通用规则,而通用方法
与通用属性
思路大致相同
还有一种思路是放在代码里面我们不关注 我们能不能做一个不关注对象,方法的,属性,我们只关注调用的一种通用匹配规则?
答案是有的
我们借鉴了注解@Deprecated
,实现了注解@ReplaceWith
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.TYPEALIAS)
@Retention(AnnotationRetention.SOURCE)
annotation class ReplaceWith (val message: String)
messages是我们要提示的内容
我们只有需要关心有调用的地方具有@ReplaceWith
就已经命中了我们的规则,提出去message内容做为提示语
我们的项目的Java和kotlin混编的,有一些用kotlin语法糖编写的方法可能只适用于在kotlin调用,不适用与Java
而我们@ReplaceWith
目前无法区分语言,为了解决这个问题。我们又定义了两种标识过滤注解
-
@ScopeJava 表示只适用于Java代码
-
@ScopeKotlin 表示只适用于kotlin代码
用法示例
@ReplaceWith(message = "建议使用 asViewClick,asViewShown,")
@ScopeKotlin
public BaseRecord setOt(String aOt) {
params.put(Config.KEY_OT, aOt);
return this;
}
当然这种使用注解的形势只适用我们工程里的代码,因为我们无法在Thread
和Gson
里增加这个注解,所以目前我们是一起使用的
写到最后
其实 自定义lint规则 特别是注解是如何实现,因为时间关系我并没有详细讲解,而复杂的匹配规则也比自定义通用对象的要麻烦许多,而在探索和使用lint的时候我也的确遇到了很多坑 因为没有api国内也没有相应的文档,就算有也是很几年前放到现在是不行的。所以我更多是查看lint自带的规则源码,不断的尝试,过程的确比较痛苦,但是收货也很丰富