Lint代码检查工具

背景

在每个项目的开发过程中,或多或少都会存在一些属于项目自身的或通用的规定,它们的存在可能是为了规范代码,可能是为了避免某些BUG,也可能是因为特殊需求而不得不做的妥协。这些规则会被写在注释里、写在README里,亦或者被口口相传,然后很有可能在某一天被某一个新来的小伙子忽略并演变为线上BUG。

为了避免这种情况,我们需要更明显的提示。它需要覆盖范围广,在项目内编写的代码都应该能被检测到。它需要具有即时性,当代码写下的时,警告应该立刻出现。它具有需要强制性,在编译时或者代码提交时,如果检查到异常可以中断编译或禁止提交。那么,我们可以试试Lint。

Lint介绍

Lint是Android Studio提供一个代码扫描和检查工具。Android Studio本身就自带了许多的Lint规则以帮助开发者规范的进行代码编写,同时它也开放了接口,开发者可以自定义Lint规则用于管理项目中不符合预期的代码。

image

主要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文档

image

代码效果

  • 代码效果

image

  • 鼠标悬停效果

image

四、总结

Lint工具针对特定问题时有非常好的效果,例如发现一些语言或API层面比较明确的低级错误、帮助进行代码规范的约束,特别是有新人参与开发时,能减少代码规范和项目约定的沟通成本。在后续的开发中,可以增加对一些类的使用规范,如定义一个项目内唯一的Log、Taost,在检查到其它Log、Toast工具被使用时提示警告,以达到工具使用的统一。另外也可以在Git仓库增加CI/CD规则,当代码提交或创建MR时,执行一次Lint检查,如果出现异常就阻止提交。

最后,工具始终是次要的,重要的是开发者,我们要了解异常、重视异常、避免异常。

五、参考资料