Lint代码检查工具
Lint代码检查工具
背景
在每个项目的开发过程中,或多或少都会存在一些属于项目自身的或通用的规定,它们的存在可能是为了规范代码,可能是为了避免某些BUG,也可能是因为特殊需求而不得不做的妥协。这些规则会被写在注释里、写在README里,亦或者被口口相传,然后很有可能在某一天被某一个新来的小伙子忽略并演变为线上BUG。
为了避免这种情况,我们需要更明显的提示。它需要覆盖范围广,在项目内编写的代码都应该能被检测到。它需要具有即时性,当代码写下的时,警告应该立刻出现。它具有需要强制性,在编译时或者代码提交时,如果检查到异常可以中断编译或禁止提交。那么,我们可以试试Lint。
Lint介绍
Lint是Android Studio提供一个代码扫描和检查工具。Android Studio本身就自带了许多的Lint规则以帮助开发者规范的进行代码编写,同时它也开放了接口,开发者可以自定义Lint规则用于管理项目中不符合预期的代码。
主要API
Issue:表示一个Lint规则。
Detector:用于检测并报告代码中的Issue,每个Issue都要指定Detector。
Scope:声明Detector要扫描的代码范围,一个Issue可包含一到多个Scope。
Scanner:用于扫描并发现代码中的Issue,每个Detector可以实现一到多个Scanner。
IssueRegistry:Lint规则加载的入口,提供要检查的Issue列表。
规则实现步骤
创建用于放置检测逻辑Java模块
创建Detector类,并实现Scanner相关接口
创建Issue类,并指定扫描范围Scope与关联的Detector
创建IssueRegistry类,注册Issue
创建META-INF配置,在其中添加IssueRegistry
在Detector类中,实现代码检查与Issue上报逻辑
使用方式
通过Jar的形式,将Java模块编译为Jar,将Jar放置在~/.android/lint目录下
通过依赖模块或者依赖AAR的形式,通过LintCheck形式集成检查模块,在此模块下,Lint规则都会生效
开发实践
目标
在使用Moshi作为Json解析库时,如果作为数据解析类的DataClass,部分变量没有设置默认值,且当解析的Json没返回该字段,解析就会出现异常。但在实际开发中,服务端会返回怎样的数据是不确定,作为客户端的我们应该秉持着防御性编程的思想,对程序逻辑进行充分考虑和完善,因此就这里而言,我们应该为每一个变量设置默认值。
那么这里就是一个小小的一个约定了,为了使开发这个项目的小伙伴都能遵守这个约定,我们现在准备编写一个简单的规则:所有带有@JsonAdapter注解的DataClass,其变量都必须设置默认值。否则Android Studio应该报错误警告。
编写规则
新建Java模块
创建java模块,并在build.gradle.kts中完成lint相关设置
plugins {
...
// 引入lint插件
alias(libs.plugins.android.lint)
}
// 设置lint输出规则
lint {
htmlReport = true
htmlOutput = file("lint-report.html")
textReport = true
absolutePaths = false
ignoreTestSources = true
}
dependencies {
// 集成lint api
compileOnly(libs.lint.api)
...
}
创建Issue
val SET_DEFAULT_VALUE: Issue = Issue.create(
// 唯一标识,利用Java注解或者XML属性进行屏蔽时,使用的就是这个id
id = "DataClassDefaultValue",
// 简短描述
briefDescription = "Data类构造函数中的变量应设置默认值",
// 详细描述
explanation = """
使用了Moshi的JsonClass注解的Data类,都被视为用于json解析的数据类,为保证\
数据解析正常,请为构造函数中的每个变量都设置默认值。
""",
// 问题分类
category = Category.CORRECTNESS,
// 问题等级
priority = 7,
// 问题严重程度
severity = Severity.ERROR,
// 实现类
implementation = Implementation(
DataClassDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
)
注册Issue
新建继承于IssueRegistry的类,实现父类的抽象方法
class DataClassIssueRegistry : IssueRegistry() {
override val issues = listOf(DataClassIssue.SET_DEFAULT_VALUE)
override val api: Int get() = CURRENT_API
override val minApi: Int get() = 8
override val vendor: Vendor = Vendor(
vendorName = "XX Project",
feedbackUrl = "https://github.com/xx/xxProject/issues",
contact = "https://github.com/xx/xxProject"
)
}
在模块内新建文件com.android.tools.lint.client.api.IssueRegistry,其在模块内的路径为src/main/resources/META-INF/services,在文件中声明自定义的注册类
com.xyoye.lint.checks.DataClassIssueRegistry
实现检测逻辑
class DataClassDetector : Detector(), Detector.UastScanner {
override fun getApplicableUastTypes(): List<Class<UClass>> = listOf(UClass::class.java)
override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
override fun visitClass(node: UClass) {
val ktClass = node.javaPsi
// 只检测Kotlin的Class
if (ktClass !is KtUltraLightClass) {
return
}
// 只检测Data Class
if (ktClass.kotlinOrigin.isData().not()) {
return
}
// 只检测JsonClass注解的Data Class
if (ktClass.annotations.none { it.qualifiedName == "com.squareup.moshi.JsonClass" }) {
return
}
// 检测构造函数
val ktConstructors = ktClass.constructors.filterIsInstance<KtLightMethod>()
if (ktConstructors.isEmpty()) {
return
}
// 检测每个构造函数
ktConstructors.onEach {
val ktParameterList = it.parameterList as? KtLightParameterList ?: return@onEach
val ktParameters = ktParameterList.parameters.filterIsInstance<KtLightParameter>()
if (ktParameters.isEmpty()) {
return
}
// 检测每个参数
ktParameters.onEach onParameterEach@{ parameter ->
val ktParameter = parameter.kotlinOrigin ?: return@onParameterEach
if (ktParameter.hasDefaultValue().not()) {
context.report(
issue = DataClassIssue.SET_DEFAULT_VALUE,
scopeClass = node,
location = context.getNameLocation(parameter),
message = "变量 ${parameter.name} 不符合规范,请为其设置默认值!"
)
}
}
}
}
}
}
使用Lint规则
新建测试数据类
在名为data的模块下新建测试的DataClass
@JsonClass(generateAdapter = true)
data class TestJsonBean(
val id: Int,
val name: String = "",
)
集成Lint规则模块
在data模块,集成Lint规则模块
dependencies {
lintChecks(project(":lint_checks"))
}
执行检查命令
./gradlew :data:lint
Lint规则效果
命令执行报错
> Task :data:lintReportDebug
Wrote HTML report to file:///xxx/data/build/reports/lint-results-debug.html
> Task :data:lintDebug FAILED
/xxx/data/src/main/kotlin/xxxx/TestJsonBean.kt:7: Error: 变量 id 不符合规范,请为其设置默认值! [DataClassDefaultValue]
val id: Int,
~~
Explanation for issues of type "DataClassDefaultValue":
使用了Moshi的JsonClass注解的Data类,都被视为用于json解析的数据类,为保证数据解析正常,请为构造函数中的每个变量都设置默认值。
Vendor: XX Project
Contact: "https://github.com/xx/xxProject"
Feedback: https://github.com/xx/xxProject/issues
1 errors, 0 warnings
FAILURE: Build failed with an exception.
html文档
代码效果
- 代码效果
- 鼠标悬停效果
四、总结
Lint工具针对特定问题时有非常好的效果,例如发现一些语言或API层面比较明确的低级错误、帮助进行代码规范的约束,特别是有新人参与开发时,能减少代码规范和项目约定的沟通成本。在后续的开发中,可以增加对一些类的使用规范,如定义一个项目内唯一的Log、Taost,在检查到其它Log、Toast工具被使用时提示警告,以达到工具使用的统一。另外也可以在Git仓库增加CI/CD规则,当代码提交或创建MR时,执行一次Lint检查,如果出现异常就阻止提交。
最后,工具始终是次要的,重要的是开发者,我们要了解异常、重视异常、避免异常。