自定义一个简单的Lint

Lint能做什么

Android Lint是Google提供给Android开发者的静态代码检查工具。使用Lint对Android工程代码进行扫描和检查,可以发现代码潜在的问题,提醒程序员及早修正。 像诸如:拼写错误、兼容性错误(NewApi等)、魔法字符串检测、不必要的对象创建、方法使用等,都是可以以错误或其他方式提醒开发者进行修改。

Lint 是如何处理源文件
微信截图_20210707082355.png
Lint是如何实现扫描分析

编译器对代码的处理流程

源文件->词语法分析-> 生成AST ->语义分析 -> 编译字节码

Lint使用了Lombok做抽象语法树的分析,根据我们给出的条件搜索出相应的node

Android Studio一些自带的lint工具

File > Settings > Edit > Inspections


16251088932169643.png
为什么需要自定义

Google提供的Lint 检查规则不能解决我们在代码中遇到的问题:
1.魔法数字、字符串
2.lateinit的使用注意
3.一些可能存在崩溃的代码调用 或 影响性能的使用(Log、e.printStackTrace())等等其他的一些规范
此时就需要自定义lint 来满足我们需求

如何自定义lint-示例
几个比较重要的概念:
Issue: 检测过程中的问题,属性中包含:id、等级、扫描范围等
Detector:问题检测器,每一个Issue都需要对应一个Detector。利用特定的Api筛选节点,并根据条件上报问题
Scanner: 扫描器。针对不同类型的目标文件,对应着不同的扫描器
IssueRegistry:自定义Lint规则加载的入口,里面包含了需要注册的问题列表

了解完定义之后,我们以对Log方法的检测为例子,简单介绍下如何自定义Lint的实现

新建Java Library-lint_checker

配置Gradle
dependencies {
compileOnly 'com.android.tools.lint:lint-api:30.1.0-alpha02'
compileOnly 'com.android.tools.lint:lint-checks:30.1.0-alpha02'
}

创建Detector-问题检测器

Detector负责的是扫描代码,发现并上报问题。一些重要的Api可以参考LintApi

/**
 * qinyafei
 * 2021-06-30
 */
public class LogDetector extends Detector implements Detector.UastScanner {
    public static final Issue ISSUE = Issue.create(
            "LogUse", // 问题id
            "避免直接使用android.util.Log", // 问题简要描述
            "避免直接使用android.util.log,请使用xxx 代替", // 问题解释,解决方案
            Category.SECURITY,// 问题所属类型-安全
            5, // 问题优先级 0 - 10
            Severity.ERROR, // 问题等级
            new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE)
    );

    /**
     * 获取方法的筛选条件
     * @return
     */
    @Nullable
    @Override
    public List getApplicableMethodNames() {
        return Arrays.asList("v", "d", "i", "w", "e");
    }

    /**
     * 搜索到方法的回调
     * @param context
     * @param node
     * @param method
     */
    @Override
    public void visitMethod(@NotNull JavaContext context, @NotNull UCallExpression node, @NotNull PsiMethod method) {
        if (context.getEvaluator().isMemberInClass(method, "android.util.Log")) {
            context.report(ISSUE, node, context.getLocation(node), "请勿直接调用Log,应该使用统一Log工具类"); // 问题上报
        }
    }
}

可以看LogDetector继承 Detector,实现了Scanner的接口,这里的UastScanner是对Java/kotlin 源文件进行扫描。根据接口实现的规则,我们自定义的LogDetector可以实现一个或者多个Scanner,具体的需要实现哪个接口,则取决于需要扫描的文件。Scanner的种类有以下这几种:

BinaryResourceScanner 扫描二进制资源文件
ClassScanner Class文件扫描器
GradleScanner 扫描Gradle脚本
ResourceFolderScanner 资源文件扫描器
XmlScanner 扫描XML文件
JavaScanner/JavaPsiScanner/UastSanner 扫描源文件
OtherFileScanner 扫描其他类型文件

代码中的getApplicableMethodNames()方法的返回值决定了哪些方法可以被检测到,visitMethod() 是方法被检测到之后的回调。检测&回调方法,都应该是成对的出现。类似的还有:


1625109118325550.png

1625109125696884.png
创建问题Issue

问题是由Detector检测并进行上报,代码如下:

 public static final Issue ISSUE = Issue.create(
            "LogUse", // 问题id
            "避免直接使用android.util.Log", // 问题简要描述
            "避免直接使用android.util.log,请使用xxx 代替", // 问题解释,解决方案
            Category.SECURITY,// 问题所属类型-安全
            5, // 问题优先级 0 - 10
            Severity.ERROR, // 问题等级
            new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE)
    );

需要注意的是,问题需要声明为final class 且必须由静态工厂方法创建,对应的参数解释如下:
1.id : 唯一值,应该能简短描述当前问题。利用Java注解或者XML属性进行屏蔽时,使用的就是这个id
2.summary : 简短的总结,描述问题而不是修复措施。
3.explanation : 完整的问题解释和修复建议。
4.category : 问题类别(部分):

  • Security 安全
  • Performance 性能
  • Usability 可用性
  • Accessibility 可访问性
  • Correctness 正确性

5.priority : 优先级。1-10的数字,10为最重要/最严重。
6.severity : 严重级别:Fatal, Error, Warning, Informational, Ignore。
7.Implementation : 为Issue和Detector提供映射关系,Detector就是当前Detector。声明扫描检测的范围Scope,Scope用来描述Detector需要分析时需要考虑的文件集,包括:Resource文件或目录、Java文件、Class文件

各属性与报告中的对应关系如下:
16251092111545179.png

16251092215791758.png
声明问题注册器IssueRegistry

规则只有被注册,才会生效

/**
 * 问题注册
 * @author qinyafei
 * @since 2021/6/30 16:13
 */
public class IssueRegistry extends com.android.tools.lint.client.api.IssueRegistry {

    /**
     * 返回问题列表
     * @return
     */
    @NotNull
    @Override
    public List getIssues() { 
        return new ArrayList() {{
            add(LogDetector.ISSUE);
        }};
    }
    
    @Override
    public int getApi() {
        return ApiKt.CURRENT_API;
    } 
}

在build.gradle中声明 Lint-Registry

jar {
    manifest {
        attributes("Lint-Registry": "com.car300.lint_checker.IssueRegistry")
    }
}

至此,自定义Lint的编码部分 就完成了,那么项目中要如何引用呢

不同引入方案

对于Demo工程,我们可以这样引入

lintChecks project(':lint_checker')

对于其他的项目,我们可以采用多种方式引入
方案2(不建议)

将jar拷贝到~/.android/lint中(我的路径为:C:\Users\Administrator\.android\lint)

缺点:所有工程都会生效

采用LinkedIn方案

将jar包放到一个aar中,这样就可以通过引入aar的方式实现
dependencies {
    lintPublish project(':lint_checker')
}
image

这里只介绍 gradle plugin 3.4.0之后 的实现方式。新建一个aar模块,添加如下依赖

dependencies {
    lintPublish project(':lint_checker')
}

在输出的aar包中,就可以看到lint.jar。在指定模块中引入aar,自定义的Lint就可以生效了

image
Lint检查时常用的一些设置(配置在需要检测的模块中)
android {
    ...
    lintOptions {
        // 不检查的问题
        disable 'TypographyFractions','TypographyQuotes'
        
        // 只检查的问题
        checkOnly 'NewApi', 'InlinedApi'
  
        // 检测出 错误 等级的问题是否终止编译
        abortOnError false
        
        // 只报告错误
        ignoreWarnings true
    }
}
...
常用命令行
gradlew clean lint 执行检查
lint --list  列出所有的问题id
lint --show  列出所有的问题,并给出详细解释
lint --check     只检查List列表中的问题id,其中List中的id 使用逗号分开
lint -w    只检查错误,忽略警告
lint --xml  filename   创建一个xml的结果报告
lint --html  filename  创建一个html 的结果报告
TODO

1.如何实现增量检查?
2.如何将检查增加到打包流程中?

参考博客

LintApi 使用 lint 检查改进您的代码 | Android 开发者 | Android Developers (google.cn)

【我的Android进阶之旅】Android自定义Lint实践_慕课手记 (imooc.com)

Lint - 美团技术团队 (meituan.com)

(4条消息) android lint check的学习和自定义以及lint语法_徒步行走于未知,爱上呼吸空气-CSDN博客

Writing Custom Lint Checks with Gradle | LinkedIn Engineering

添加构建依赖项 | Android 开发者 | Android Developers (google.cn)

你可能感兴趣的:(自定义一个简单的Lint)