Android Developer

Lint精简版

一、为什么选择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;
    }
    

当然这种使用注解的形势只适用我们工程里的代码,因为我们无法在ThreadGson里增加这个注解,所以目前我们是一起使用的

写到最后

其实 自定义lint规则 特别是注解是如何实现,因为时间关系我并没有详细讲解,而复杂的匹配规则也比自定义通用对象的要麻烦许多,而在探索和使用lint的时候我也的确遇到了很多坑 因为没有api国内也没有相应的文档,就算有也是很几年前放到现在是不行的。所以我更多是查看lint自带的规则源码,不断的尝试,过程的确比较痛苦,但是收货也很丰富