XTCLint

Project Url: ouyangpeng/XTCLint
Introduction: 实现 Android 自定义 Lint 实践 (Custom Lint Rules & Lint Plugin)
More: Author   ReportBugs   
Tags:

【我的 Android 进阶之旅】Android 自定义 Lint 实践 https://blog.csdn.net/ouyang_peng/article/details/80374867

目前这份代码是基于 Gradle 2.x 编写的,代码分支为 https://github.com/ouyangpeng/XTCLint/tree/feature/LintBaseOnGradel2.x

接下来准备开始基于 Gradle 3.x 重新编译自定义 Lint 插件,写新的 Lint 自定义规则。 敬请期待!

2017 年 8 月份的时候,我在公司开始推广 Lint、FindBugs 等静态代码检测工具。然后发现系统自带的 Lint 检测的 Issue 不满足我们团队内部的特定需求,因此去自定义了部分 Lint 规则。这个检测运行了大半年,运行良好,团队的代码规范也有了大幅度提升。这个是基于当时 Gradle2.x 系列写出来的自定义 Lint 实践总结,过去大半年了,现在将它搬到 CSDN 博客分享给大家一起学习学习。如果要在 Gradle3.x 系列使用该自定义规定的话,部分代码都得修改成最新的语法,因此此篇博客的内容请使用 Gralde2.x 系列编译项目中可以加入,去定义你自己的 Lint 规则吧。

当时已经实现的自定义规则大概有:

这里写图片描述

我这里只介绍如何去实现你自己的 Lint 规则,具体源代码的话不方便贴出了,所以不会去公布源代码。

一、Lint 介绍

android lint 是一个静态代码分析工具,通过 lint 工具,你可以不用边运行边调试,或者通过单元测试进行代码检查,可以检测代码中不规范、不和要求的问题,解决一些潜在的 bug。lint 工具可以在命令行上使用,也可以在 adt 中使用。

比如当想检查在 manifest.xml 中是否有 activity,activity 中是否包含了 launcher activity。如果没有进行错误的警告。

通过 lint 的这种手段,可以对代码进行规范的控制,毕竟一个团队每个人的风格不同,但是要注意的当然是代码的质量,所以 lint 可以进行代码的规范和质量控制。

在 Android studio 还没出来时,lint 和 Eclipse 并不能很好的结合在一起,只能作为一个独立的工具,通过命令行去执行 lint 检查。

在 android studio 出现之后,不再建议单独使用 lint 命令,而是结合 gradle 进行操作,命令为 ./gradlew lint 进行执行

lint 工具通过一下六个方面去检查代码中的问题 correctness, security, performance, usability, accessibility, and internationalization。检查的范围包括 java 文件,xml 文件,class 文件。

lint 工具在 sdk16 版本之后就带有了,所以在 sdk 目录/tools/可以找到 lint 工具。现在建议与 gradle 一起使用,使用./gradlew lint 进行

参考官方文档介绍

二、使用 Lint 的方法

关于 lint 的一些命令,可以参考官网,这里简单介绍一些。

  • lint path(项目目录) ——进行项目的 lint 检查
  • lint –disable id(MissingTranslation,UnusedIds,Usability:Icons)path ——id 是 lint issue(问题)的标志,检查项目,不包括指定的 issue
  • lint –check id path ——利用指定的 issue 进行项目检查
  • lint –list ——列出所有的 issue
  • lint –show id ——介绍指定的 issue
  • lint –help ——查看帮助

2.1 使用 android studio 自带的 lint 工具

点击 Analyze 的 Inspect Code 选项,即可开启 lint 检查,在 Inspection 窗口中可以看到 lint 检查的结果,lint 查询的错误类型包括:

  • Missing Translation and Unused Translation【缺少翻译或者没有】
  • Layout Peformance problems (all the issues the old layoutopt tool used to find, and more)【布局展示问题】
  • Unused resources【没有使用的资源】
  • Inconsistent array sizes (when arrays are defined in multiple configurations)【不一致的数组大小】
  • Accessibility and internationalization problems (hardcoded strings, missing contentDescription, etc)【可访问性和国际化问题,包括硬链接的字符串,缺少 contentDescription,等等】
  • Icon problems (like missing densities, duplicate icons, wrong sizes, etc)【图片问题,丢失密度,重复图片,错误尺寸等】
  • Usability problems (like not specifying an input type on a text field)【使用规范,比如没有在一个文本上指定输入的类型】
  • Manifest errors【Manifest.xml 中的错误】
  • and so on

android 自带的 lint 规则的更改可以在 Setting 的 Edit 选项下选择 Inspections(File > Settings > Project Settings),对已有的 lint 规则进行自定义选择。 参考官方文档

2.2 使用 lint.xml 定义检查规则

可以通过 lint.xml 来自定义检查规则,这里的自定义是指定义系统原有的操作,所以和第一个步骤的结果是一样的,只是可以更方便的配置。

lint.xml 生效的位置是要放在项目的根目录下面,lint.xml 的示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- 忽略指定的检查 -->
    <issue id="IconMissingDensityFolder" severity="ignore" />

    <!-- 忽略指定文件的指定检查 -->
    <issue id="ObsoleteLayoutParam">
        <ignore path="res/layout/activation.xml" />
        <ignore path="res/layout-xlarge/activation.xml" />
    </issue>

    <!-- 更改检查问题归属的严重性 -->
    <issue id="HardcodedText" severity="error" />
</lint>

参考官方文档

三、自定义 lint 检查规则结构介绍

3.1 概述

Android Lint 是 Google 提供给 Android 开发者的静态代码检查工具。使用 Lint 对 Android 工程代码进行扫描和检查,可以发现代码潜在的问题,提醒程序员及早修正。

为保证代码质量,在开发流程中加入了代码检查,如果代码检测到问题,则无法合并到正式分支中,这些检查中就包括 Lint。

3.2 为什么需要自定义

我们在实际使用 Lint 中遇到了以下问题:

原生 Lint 无法满足我们团队特有的需求,例如:编码规范。 原生 Lint 存在一些检测缺陷或者缺少一些我们认为有必要的检测。 基于上面的考虑,我们开始调研并开发自定义 Lint。

3.3 如何加入已有的 lint 规则

Lint 规则是基于 Java 写的,是在 AST 抽象语法树上去进行一个解析。所以在写 Lint 规则的时候,要学习一下 AST 抽象语法树。才知道如何去寻找一个类方法和其参数等。以下有两种方法:

  1. 所以自定义 Lint 规则应该是一个写好的 jar 包,jar 包生效的位置是在~/.android/lint目录,这个是对于 Mac 和 Linux 来说的,对于 Windows 来说就是在C:/Users/Administrator/.android/lint下,放到这个目录下,Lint 工具会自动加载这个 jar 包作为 lint 的自定义检查规则。

  2. 放到 lint 目录下着实是一件比较麻烦的事情,即使可以用脚本来代替,但是仍然不是一个特别方便的方法。也是由于当 android 项目直接依赖于 lint.jar 包时不能起作用,而无法进行直接依赖。 而 aar 很好的解决了这个问题,aar 能够将项目中的资源、class 文件、jar 文件等都包含,所以通过将 lint.jar 放入 lintaar 中,再由项目依赖于 lintaar,这时候就可以达到自定义 lint 检查的目的。

下面就是如何使自定义 lint 生效的代码示例,使用第二个方法(第二个方法就包括了第一个方法):

这里写图片描述

主要包括了三个 Module 一个是XTCLintrRules,一个是XTCLintAAR,一个是XTCLintPlugin,还有一个是测试的app项目。

  • XTCLintrRules ,主要是用来编写自定义 Lint 规则,编译后生成 lint.jar
  • XTCLintAAR,主要是将XTCLintrRules生成的lint.jar包大包成 aar,方便引用
  • XTCLintPlugin,主要是后面统一管理lint.xml 和 lintOptions,自动添加 aar,后面再讲解。

3.3.1 XTCLintrRules 的 gradle 配置

在 XTCLintrRules 中,主要是编写 lint 规则,他是一个 Java 工程。

它的 gradle 如下:

//java 项目,该项目编译之后生成  XTCLintRules.jar

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    // 依赖于 lint 的规则的 api
    compile 'com.android.tools.lint:lint-api:25.0.0'
    compile 'com.android.tools.lint:lint-checks:25.0.0'
    testCompile 'com.android.tools.lint:lint-tests:24.5.0'
}

/**
 * Lint-Registry 是透露给 lint 工具的注册类的方法,
 * 也就是 PermissionIssueRegistry 是 lint 工具的入口,同时也通过这个方法进行打 jar 包
 */
jar{
    manifest{
        attributes('Lint-Registry': 'com.xtc.lint.rules.XTCIssueRegister')
    }
}

// 创建了一个叫“lintJarOutput”的 Gradle configuration,
// 用于输出我们生成的 jar 包。在生成 aar 的模块 "XTCLintAAR" 的 build.gradle 中会引用此 configuration。
configurations {
    lintJarOutput
}

// 指定定义方法 lintJarOutput 的作用,此处是获得调用 jar 方法后的生成的 jar 包
dependencies {
    lintJarOutput files(jar)
}

defaultTasks 'assemble'

//指定编译使用 JDK1.8
//sourceCompatibility = JavaVersion.VERSION_1_8
//targetCompatibility = JavaVersion.VERSION_1_8

//指定编译的编码
tasks.withType(JavaCompile){
    options.encoding = "UTF-8"
}
  • lint-api: 官方给出的 API,API 并不是最终版,官方提醒随时有可能会更改 API 接口。
  • lint-checks:已有的检查。

3.3.2 XTCLintAAR 的 gradle 配置

apply plugin: 'com.android.library'

apply from: 'maven_upload.gradle'
apply from: '../jenkins.gradle'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.3"

    defaultConfig {
        minSdkVersion 9
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        debug {
            buildConfigField 'String', 'JenkinsName', "\"" + jenkinsName + "\""
            buildConfigField 'String', 'JenkinsRevision', "\"" + jenkinsRevision + "\""
            buildConfigField 'String', 'GitSHA', "\"" + gitSHA  + "\""
            buildConfigField 'String', 'GitBranch', "\"" + gitBranch + "\""
            buildConfigField 'String', 'GitTag', "\"" + gitTag + "\""

            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        release {
            buildConfigField 'String', 'JenkinsName', "\"" + jenkinsName + "\""
            buildConfigField 'String', 'JenkinsRevision', "\"" + jenkinsRevision + "\""
            buildConfigField 'String', 'GitSHA', "\"" + gitSHA  + "\""
            buildConfigField 'String', 'GitBranch', "\"" + gitBranch + "\""
            buildConfigField 'String', 'GitTag', "\"" + gitTag + "\""

            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}


/**
 * 定义方法 lintJarImport,引入 XTCLintRules.jar
 *
 * rules for including "lint.jar" in aar
 */
configurations {
    lintJarImport
}

// 链接到 lintJar 中的 lintJarOutput 方法,调用 jar 方法,并获得 jar 包
dependencies {
    //其引用了模块 “:XTCLintRules”的 Gradle configuration “lintJarOutput”。
    lintJarImport project(path: ':XTCLintrRules', configuration: "lintJarOutput")
}

// 将得到的 JAR 包复制到目录 build/intermediates/lint/下,并且重命名为 lint.jar
task copyLintJar(type: Copy) {
    from(configurations.lintJarImport) {
        rename {
            String fileName ->
                'lint.jar'
        }
    }
    into 'build/intermediates/lint/'
}

// 当项目 build 到 compileLint 这一步时执行 copyLintJar 方法
project.afterEvaluate {
    def compileLintTask = project.tasks.find { it.name == 'compileLint' }
    //对内置的 Gradle task “compileLint”做了修改,让其依赖于我们定义的一个 task “copyLintJar”。
    compileLintTask.dependsOn(copyLintJar)
}

该 Module 的作用就是:

  1. 链接到 lintJar 中的 lintJarOutput 方法,调用 jar 方法,并获得 jar 包
  2. 将得到的 JAR 包复制到目录 build/intermediates/lint/下,并且重命名为 lint.jar
  3. 当项目 build 到 compileLint 这一步时执行 copyLintJar 方法,这样的话就可以调用到我们自定义的 Lint 规则
  4. 生成 AAR 方便项目调用

3.3.3 XTCLintPlugin 介绍

关于 XTCLintPlugin 的介绍,等我们先将自定义规则讲解完后再介绍,这里先不介绍。

四、自定义 lint 检查规则的编写

上面大致讲解了下 XTCLintrRules 和 XTCLintAAR 两个 Module 的 gradle 配置和作用,下面我们来针对 XTCLintrRules 这个 Module 来编写我们自定义的 Lint 检查规则

这里写图片描述

4.1 创建 Detector

Detector 负责扫描代码,发现问题并报告。 我们通过一个 XTCCustomLogDetector 这个类来学习下 Detector 怎么实现,

XTCCustomLogDetector 类主要功能是:针对代码中直接使用android.util.Log的方法 { v,d,i,w,e,wtf }或者直接使用了 System.out.print/System.err.print进行日志打印的一个判断,然后提示各位开发人员使用我们自定义好的com.xtc.log.LogUtil类进行日志打印。


package com.xtc.lint.rules.detectors.java;

import com.android.tools.lint.client.api.JavaParser;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.xtc.lint.rules.JavaPackageRelativePersonUtil;

import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;

import lombok.ast.AstVisitor;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
/**
 * 定义代码检查规则
 * 这个是针对代码中直接使用 android.util.Log 的方法 { v,d,i,w,e,wtf } 进行日志打印的一个判断
 * </p>
 * created by OuyangPeng at 2017/8/31 9:55
 */
public class XTCCustomLogDetector extends Detector implements Detector.JavaScanner {
    private static final Class<? extends Detector> DETECTOR_CLASS = XTCCustomLogDetector.class;
    private static final EnumSet<Scope> DETECTOR_SCOPE = Scope.JAVA_FILE_SCOPE;
    private static final Implementation IMPLEMENTATION = new Implementation(
            DETECTOR_CLASS,
            DETECTOR_SCOPE
    );

    private static final String ISSUE_ID = "XTC_LogUseError";
    private static final String ISSUE_DESCRIPTION = "警告:你应该使用我们团队自定义的 Log 打印工具类工具类{com.xtc.log.LogUtil}";
    private static final String ISSUE_EXPLANATION = "为了能够更好的控制 Log 打印的开关,你不能直接使用{android.util.Log}或者{System.out.println}直接打印日志,你应该使用我们团队自定义的 Log 打印工具类工具类{com.xtc.log.LogUtil}";

    private static final Category ISSUE_CATEGORY = Category.CORRECTNESS;
    private static final int ISSUE_PRIORITY = 9;
    private static final Severity ISSUE_SEVERITY = Severity.WARNING;

    private static final String SYSTEM_OUT_PRINT = "System.out.print";
    private static final String SYSTEM_OUT_PRINTLN = " System.out.println";
    private static final String SYSTEM_ERR_PRINT = "System.err.print";
    private static final String SYSTEM_ERR_PRINTLN = " System.err.println";


    private static final String CHECK_PACKAGE = "android.util.Log";

    public static final Issue ISSUE = Issue.create(
            ISSUE_ID,
            ISSUE_DESCRIPTION,
            ISSUE_EXPLANATION,
            ISSUE_CATEGORY,
            ISSUE_PRIORITY,
            ISSUE_SEVERITY,
            IMPLEMENTATION
    );

    @Override
    public List<String> getApplicableMethodNames() {
        return Arrays.asList("v", "d", "i", "w", "e", "wtf");
    }

    @Override
    public List<Class<? extends Node>> getApplicableNodeTypes() {
        return Collections.singletonList(MethodInvocation.class);
    }

    @Override
    public AstVisitor createJavaVisitor(final JavaContext context) {
        return new LogVisit(context);
    }

    private class LogVisit extends ForwardingAstVisitor {
        private final JavaContext javaContext;

        private LogVisit(JavaContext context) {
            javaContext = context;
        }

        @Override
        public boolean visitMethodInvocation(MethodInvocation node) {
            String nodeString = node.toString();
            if (nodeString.startsWith(SYSTEM_OUT_PRINT)
                    || nodeString.startsWith(SYSTEM_OUT_PRINTLN)
                    || nodeString.startsWith(SYSTEM_ERR_PRINT)
                    || nodeString.startsWith(SYSTEM_ERR_PRINTLN)) {
                String relativePersonName = JavaPackageRelativePersonUtil.getPackageRelativePerson(javaContext,node);
//                System.out.println("LogVisit visitMethodInvocation() 出现 lint 检测项,对应的责任人为: " + relativePersonName);
                String message = ISSUE_DESCRIPTION + " ,请 【" + relativePersonName + "】速度修改";
                javaContext.report(ISSUE, node, javaContext.getLocation(node), message);
                return true;
            }

            JavaParser.ResolvedNode resolve = javaContext.resolve(node);
            if (resolve instanceof JavaParser.ResolvedMethod) {
                JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolve;
                JavaParser.ResolvedClass containingClass = method.getContainingClass();

                if (resolve.getName().equals("v")
                        ||resolve.getName().equals("d")
                        ||resolve.getName().equals("i")
                        ||resolve.getName().equals("w")
                        ||resolve.getName().equals("e")
                        ||resolve.getName().equals("wtf")){
//                    System.out.println("XTCCustomLogDetector  called method  one of { v,d,i,w,e,wtf }");
                    if (containingClass.matches(CHECK_PACKAGE)) {
//                      System.out.println("XTCCustomLogDetector  called method  one of { v,d,i,w,e,wtf } , and the className is : android.util.Log");

                        String relativePersonName = JavaPackageRelativePersonUtil.getPackageRelativePerson(javaContext,node);
//                        System.out.println("LogVisit visitMethodInvocation() 出现 lint 检测项,对应的责任人为: " + relativePersonName);
                        String message = ISSUE_DESCRIPTION + " ,请 【" + relativePersonName + "】速度修改";
                        javaContext.report(ISSUE, node, javaContext.getLocation(node),
                                message);
                        return true;
                    }
                }
            }

            return super.visitMethodInvocation(node);
        }
    }
}

4.2 Detector 介绍

可以看到这个 Detector 继承 Detector 类,然后实现 Scanner 接口。

自定义 Detector 可以实现一个或多个 Scanner 接口,选择实现哪种接口取决于你想要的扫描范围

  • Detector.XmlScanner
  • Detector.JavaScanner
  • Detector.ClassScanner
  • Detector.BinaryResourceScanner
  • Detector.ResourceFolderScanner
  • Detector.GradleScanner
  • Detector.OtherFileScanner

这里写图片描述

这里因为我们是要针对 Java 代码扫描,所以选择使用 JavaScanner。

这里写图片描述

代码中getApplicableNodeTypes()方法决定了什么样的类型能够被检测到。这里我们想看 Log 以及 println 的方法调用,选取MethodInvocation

对应的,我们在createJavaVisitor()创建一个ForwardingAstVisitor通过visitMethodInvocation方法来接收被检测到的 Node。

可以看到getApplicableNodeTypes()返回值是一个 List,也就是说可以同时检测多种类型的节点来帮助精确定位到代码,对应的ForwardingAstVisitor接受返回值进行逻辑判断就可以了。

可以看到 JavaScanner 中还有其他很多方法,getApplicableMethodNames(指定方法名)、visitMethod(接收检测到的方法),这种对于直接找寻方法名的场景会更方便。

当然这种场景我们用最基础的方式也可以完成,只是比较繁琐。

那么其他 Scanner 如何去写呢? 可以去查看各接口中的方法去实现,一般都是有这两种对应:什么样的类型需要返回、接收发现的类型。

这里插一句,Lint 是如何实现 Java 扫描分析的呢?Lint 使用了Lombok做抽象语法树的分析。所以在我们告诉它需要什么类型后,它就会把相应的 Node 返回给我们。

回到示例,当接收到返回的 Node 之后需要进行判断,如果调用方法是android.util.Log的方法 { v,d,i,w,e,wtf }或者直接使用了 System.out.print/System.err.print,则调用 context.report 上报。

  javaContext.report(ISSUE, node, javaContext.getLocation(node), message);

第一个参数是 Issue,这个之后会讲到; 第二个参数是当前节点; 第三个参数 location 会返回当前的位置信息,便于在报告中显示定位; 最后的字符串用来为警告添加解释。

对应报告中的位置如下图:

这里写图片描述

4.2.1 Issue 介绍

Issue 由 Detector 发现并报告,是 Android 程序代码可能存在的 bug。


    private static final Implementation IMPLEMENTATION = new Implementation(
            DETECTOR_CLASS,
            DETECTOR_SCOPE
    );

    private static final String ISSUE_ID = "XTC_LogUseError";
    private static final String ISSUE_DESCRIPTION = "警告:你应该使用我们团队自定义的 Log 打印工具类工具类{com.xtc.log.LogUtil}";
    private static final String ISSUE_EXPLANATION = "为了能够更好的控制 Log 打印的开关,你不能直接使用{android.util.Log}或者{System.out.println}直接打印日志,你应该使用我们团队自定义的 Log 打印工具类工具类{com.xtc.log.LogUtil}";

    private static final Category ISSUE_CATEGORY = Category.CORRECTNESS;
    private static final int ISSUE_PRIORITY = 9;
    private static final Severity ISSUE_SEVERITY = Severity.WARNING;

    public static final Issue ISSUE = Issue.create(
            ISSUE_ID,
            ISSUE_DESCRIPTION,
            ISSUE_EXPLANATION,
            ISSUE_CATEGORY,
            ISSUE_PRIORITY,
            ISSUE_SEVERITY,
            IMPLEMENTATION
    );

声明为 final class,由静态工厂方法创建。对应参数解释如下:

这里写图片描述

  • id : 唯一值,应该能简短描述当前问题。利用 Java 注解或者 XML 属性进行屏蔽时,使用的就是这个 id。
  • summary : 简短的总结,通常 5-6 个字符,描述问题而不是修复措施。
  • explanation : 完整的问题解释和修复建议。
  • category : 问题类别。详见下文详述部分。
  • priority : 优先级。1-10 的数字,10 为最重要/最严重。
  • severity : 严重级别:Fatal, Error, Warning, Informational, Ignore。
  • Implementation : 为 Issue 和 Detector 提供映射关系,Detector 就是当前 Detector。声明扫描检测的范围+ + + Scope,Scope 用来描述 Detector 需要分析时需要考虑的文件集,包括:Resource 文件或目录、Java 文件、Class 文件。

4.2.2 Issue 与 Lint HTML 报告对应关系

对应着 id 和 summary 这里写图片描述

对应着 explanation 、category 、severity 、priority

这里写图片描述

4.2.2 Category 详述

系统现在已有的类别如下:

  • Lint
  • Correctness (incl. Messages)
  • Security
  • Performance
  • Usability (incl. Icons, Typography)
  • Accessibility
  • Internationalization
  • Icons
  • Typography
  • Messages

这里写图片描述

Category 类的部分代码

 /** Issues related to running lint itself */
    public static final Category LINT = create("Lint", 110);

    /** Issues related to correctness */
    public static final Category CORRECTNESS = create("Correctness", 100);

    /** Issues related to security */
    public static final Category SECURITY = create("Security", 90);

    /** Issues related to performance */
    public static final Category PERFORMANCE = create("Performance", 80);

    /** Issues related to usability */
    public static final Category USABILITY = create("Usability", 70);

    /** Issues related to accessibility */
    public static final Category A11Y = create("Accessibility", 60);

    /** Issues related to internationalization */
    public static final Category I18N = create("Internationalization", 50);

    // Sub categories

    /** Issues related to icons */
    public static final Category ICONS = create(USABILITY, "Icons", 73);

    /** Issues related to typography */
    public static final Category TYPOGRAPHY = create(USABILITY, "Typography", 76);

    /** Issues related to messages/strings */
    public static final Category MESSAGES = create(CORRECTNESS, "Messages", 95);

    /** Issues related to right to left and bidirectional text support */
    public static final Category RTL = create(I18N, "Bidirectional Text", 40);

4.2.3 自定义 Category

public class XTCCategory {
    public static final Category NAMING_CONVENTION = Category.create("小天才命名规范", 101);
}

使用

public static final Issue ISSUE = Issue.create(
        "IntentExtraKey",
        "intent extra key 命名不规范",
        "请在接受此参数中的 Activity 中定义一个按照 EXTRA_<name>格式命名的常量",
        XTCCategory.NAMING_CONVENTION , 5, Severity.ERROR,
        new Implementation(IntentExtraKeyDetector.class, Scope.JAVA_FILE_SCOPE));

4.3 IssueRegistry

IssueRegistry 就是注册类,继承他,并重写 getIssues 的方法即可,提供需要被检测的 Issue 列表

例如我们的项目工程中的XTCIssueRegister.java代码如下


package com.xtc.lint.rules;

import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.detector.api.Issue;
import com.xtc.lint.rules.detectors.binaryResource.XTCImageFileSizeDetector;
import com.xtc.lint.rules.detectors.java.XTCActivityFragmentLayoutNameDetector;
import com.xtc.lint.rules.detectors.java.XTCChineseStringDetector;
import com.xtc.lint.rules.detectors.java.XTCCustomLogDetector;
import com.xtc.lint.rules.detectors.java.XTCCloseDetector;
import com.xtc.lint.rules.detectors.java.XTCCustomToastDetector;
import com.xtc.lint.rules.detectors.java.XTCEnumDetector;
import com.xtc.lint.rules.detectors.java.XTCHardcodedValuesDetector;
import com.xtc.lint.rules.detectors.java.XTCHashMapForJDK7Detector;
import com.xtc.lint.rules.detectors.java.XTCMessageObtainDetector;
import com.xtc.lint.rules.detectors.java.XTCViewHolderItemNameDetector;
import com.xtc.lint.rules.detectors.xml.XTCViewIdNameDetector;

import java.util.Arrays;
import java.util.List;

public class XTCIssueRegister extends IssueRegistry {
    static {
        System.out.println("***************************************************");
        System.out.println("**************** lint 读取配置文件 *****************");
        System.out.println("***************************************************");
        LoadPropertiesFile.loadPropertiesFile();
    }

    @Override
    public List<Issue> getIssues() {
        System.out.println("***************************************************");
        System.out.println("**************** lint 开始静态分析代码 *****************");
        System.out.println("***************************************************");
        return Arrays.asList(
                XTCChineseStringDetector.ISSUE,
                XTCActivityFragmentLayoutNameDetector.ACTIVITY_LAYOUT_NAME_ISSUE,
                XTCActivityFragmentLayoutNameDetector.FRAGMENT_LAYOUT_NAME_ISSUE,
                XTCMessageObtainDetector.ISSUE,
                XTCCustomToastDetector.ISSUE,
                XTCCustomLogDetector.ISSUE,
                XTCViewIdNameDetector.ISSUE,
                XTCViewHolderItemNameDetector.ISSUE,
                XTCCloseDetector.ISSUE,
                XTCImageFileSizeDetector.ISSUE,
                XTCHashMapForJDK7Detector.ISSUE,
                XTCHardcodedValuesDetector.ISSUE,
                XTCEnumDetector.ISSUE
        );
    }
}

在 getIssues()方法中返回需要被检测的 Issue List,我们刚才编写的 XTCCustomLogDetector.ISSUE 也被注册进去了。

在 build.grade 中声明 Lint-Registry 属性


/**
 * Lint-Registry 是透露给 lint 工具的注册类的方法,
 * 也就是 PermissionIssueRegistry 是 lint 工具的入口,同时也通过这个方法进行打 jar 包
 */
jar{
    manifest{
        attributes('Lint-Registry': 'com.xtc.lint.rules.XTCIssueRegister')
    }
}

这里写图片描述

至此,自定义 Lint 的编码部分就完成了。

五、为自定义 Lint 开发 plugin

5.1 为自定义 Lint 开发 plugin 的目的

aar 虽然很方便,但是在团队内部推广中我们遇到了以下问题:

  • 配置繁琐,不易推广。每个库都需要自行配置 lint.xml、lintOptions,并且 compile aar。
  • 不易统一。各库之间需要使用相同的配置,保证代码质量。但现在手动来回拷贝规则,且配置文件可以自己修改。

于是我想到开发一个 plugin,统一管理 lint.xml 和 lintOptions,自动添加 aar。下图就是我们的工程XTCLintPlugin

这里写图片描述

编写自定义插件需要 实现 Plugin 接口,然后将插件的作用在apply(Project project)方法中实现即可。

这里写图片描述

class XTCLintPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        applyTask(project, getAndroidVariants(project))
    }
    ...
}

5.2 自定义 Lint 开发 plugin 需要实现的功能

5.2.1 统一 lint.xml

我们在 plugin 中内置 lint.xml,执行前拷贝过去,执行完成后删除。

//==========================  统一  lint.xml  开始=============================================//
            //lint 任务执行前,先复制 lint.xml
            lintTask.doFirst {
                //如果 lint.xml 存在,则改名为 lintOld.xml
                if (lintFile.exists()) {
                    lintOldFile = project.file("lintOld.xml")
                    lintFile.renameTo(lintOldFile)
                }

                //进行 将 plugin 内置的 lint.xml 文件和项目下面的 lint.xml 进行复制合并操作
                def isLintXmlReady = copyLintXml(project, lintFile)
                //合并完毕后,将 lintOld.xml 文件改名为 lint.xml
                if (!isLintXmlReady) {
                    if (lintOldFile != null) {
                        lintOldFile.renameTo(lintFile)
                    }
                    throw new GradleException("lint.xml 不存在")
                }
            }

            //lint 任务执行后,删除 lint.xml
            project.gradle.taskGraph.afterTask { task, TaskState state ->
                if (task == lintTask) {
                    lintFile.delete()
                    if (lintOldFile != null) {
                        lintOldFile.renameTo(lintFile)
                    }
                }
            }
            //==========================  统一  lint.xml  结束=============================================//

这里写图片描述

5.2.2 统一 lintOptions

Android plugin 在 1.3 以后允许我们替换 Lint Task 的 lintOptions


//==========================  统一  lintOptions  开始=============================================//
            /*
            lintOptions {
               lintConfig file("lint.xml")
               warningsAsErrors true
               abortOnError true
               htmlReport true
               htmlOutput file("lint-report/lint-report.html")
               xmlReport false
            }
            */

            def newLintOptions = new LintOptions()

            //配置 lintConfig 的配置文件路径
            newLintOptions.lintConfig = lintFile

            //是否将所有的 warnings 视为 errors
//            newLintOptions.warningsAsErrors = true

            //是否 lint 检测出错则停止编译
            newLintOptions.abortOnError = true

            //htmlReport 打开
            newLintOptions.htmlReport = true
            newLintOptions.htmlOutput = project.file("${project.buildDir}/reports/lint/lint-result.html")

            //xmlReport 打开 因为 Jenkins 上的插件需要 xml 文件
            newLintOptions.xmlReport = true
            newLintOptions.xmlOutput = project.file("${project.buildDir}/reports/lint/lint-result.xml")

            //配置 lint 任务的配置为  newLintOptions
            lintTask.lintOptions = newLintOptions
            //==========================  统一  lintOptions  结束=============================================//

这里写图片描述

5.2.3 自动添加最新 aar

   //========================== 统一  自动添加 AAR  开始=============================================//
        //配置 project 的 dependencies 配置,默认都自动加上 自定义 lint 检测的 AAR 包
        project.dependencies {
            //如果是 android application 项目
            if (project.getPlugins().hasPlugin('com.android.application')) {
                compile('com.xtc.lint:lint-check:1.1.1') {
                    force = true
                }
            } else {
                provided('com.xtc.lint:lint-check:1.1.1') {
                    force = true
                }
            }
        }

        //去除 gradle 缓存的配置
        project.configurations.all {
            resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
            resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
        }
        //========================== 统一  自动添加 AAR  结束=============================================//

这里写图片描述

本来这段代码

//配置 project 的 dependencies 配置,默认都自动加上 自定义 lint 检测的 AAR 包
        project.dependencies {
            //如果是 android application 项目
            if (project.getPlugins().hasPlugin('com.android.application')) {
                compile('com.xtc.lint:lint-check:1.1.1') {
                    force = true
                }
            } else {
                provided('com.xtc.lint:lint-check:1.1.1') {
                    force = true
                }
            }
        }

中的 lint-check 版本号 我们写的是 compile('com.xtc.lint:lint-check:+') ,但是张亚州的依赖管理库不准我们使用+号,因此我们这里写的是指定好的版本。至此我们的插件功能介绍完毕了,下面是我们的 Lint 插件 XTCLintPlugin 的具体实现逻辑代码

package com.xtc.lint.plugin

import com.android.build.gradle.AppPlugin
import com.android.build.gradle.LibraryPlugin
import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.internal.dsl.LintOptions
import com.android.build.gradle.tasks.Lint
import org.gradle.api.*
import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency
import org.gradle.api.tasks.TaskState

/**
 * aar 虽然很方便,但是在团队内部推广中我们遇到了以下问题:

 配置繁琐,不易推广。每个库都需要自行配置 lint.xml、lintOptions,并且 compile aar。
 不易统一。各库之间需要使用相同的配置,保证代码质量。但现在手动来回拷贝规则,且配置文件可以自己修改。

 于是我们想到开发一个 plugin,统一管理 lint.xml 和 lintOptions,自动添加 aar。
 */
class XTCLintPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        applyTask(project, getAndroidVariants(project))
    }

    private static final String sPluginMissConfiguredErrorMessage = "Plugin requires the 'android' or 'android-library' plugin to be configured."

    /**
     * 获取 project 项目 中 android 项目 或者 library 项目 的 variant 列表
     * @param project 要编译的项目
     * @return variants 列表
     */
    private static DomainObjectCollection<BaseVariant> getAndroidVariants(Project project) {

        if (project.getPlugins().hasPlugin(AppPlugin)) {
            return project.getPlugins().getPlugin(AppPlugin).extension.applicationVariants
        }

        if (project.getPlugins().hasPlugin(LibraryPlugin)) {
            return project.getPlugins().getPlugin(LibraryPlugin).extension.libraryVariants
        }

        throw new ProjectConfigurationException(sPluginMissConfiguredErrorMessage, null)
    }

    /**
     *  插件的实际应用:统一管理 lint.xml 和 lintOptions,自动添加 aar。
     * @param project 项目
     * @param variants 项目的 variants
     */
    private void applyTask(Project project, DomainObjectCollection<BaseVariant> variants) {
        //========================== 统一  自动添加 AAR  开始=============================================//
        //配置 project 的 dependencies 配置,默认都自动加上 自定义 lint 检测的 AAR 包
        project.dependencies {
            //如果是 android application 项目
            if (project.getPlugins().hasPlugin('com.android.application')) {
                compile('com.xtc.lint:lint-check:1.1.1') {
                    force = true
                }
            } else {
                provided('com.xtc.lint:lint-check:1.1.1') {
                    force = true
                }
            }
        }

        //去除 gradle 缓存的配置
        project.configurations.all {
            resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
            resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
        }
        //========================== 统一  自动添加 AAR  结束=============================================//


        def xtcLintTaskExists = false

        variants.all { variant ->
            //获取 Lint Task
            def variantName = variant.name.capitalize()
            Lint lintTask = project.tasks.getByName("lint" + variantName) as Lint

            //Lint 会把 project 下的 lint.xml 和 lintConfig 指定的 lint.xml 进行合并,为了确保只执行插件中的规则,采取此策略
            File lintFile = project.file("lint.xml")
            File lintOldFile = null

            //==========================  统一  lintOptions  开始=============================================//
            /*
            lintOptions {
               lintConfig file("lint.xml")
               warningsAsErrors true
               abortOnError true
               htmlReport true
               htmlOutput file("lint-report/lint-report.html")
               xmlReport false
            }
            */

            def newLintOptions = new LintOptions()

            //配置 lintConfig 的配置文件路径
            newLintOptions.lintConfig = lintFile

            //是否将所有的 warnings 视为 errors
//            newLintOptions.warningsAsErrors = true

            //是否 lint 检测出错则停止编译
            newLintOptions.abortOnError = true

            //htmlReport 打开
            newLintOptions.htmlReport = true
            newLintOptions.htmlOutput = project.file("${project.buildDir}/reports/lint/lint-result.html")

            //xmlReport 打开 因为 Jenkins 上的插件需要 xml 文件
            newLintOptions.xmlReport = true
            newLintOptions.xmlOutput = project.file("${project.buildDir}/reports/lint/lint-result.xml")

            //配置 lint 任务的配置为  newLintOptions
            lintTask.lintOptions = newLintOptions
            //==========================  统一  lintOptions  结束=============================================//

            //==========================  统一  lint.xml  开始=============================================//
            //lint 任务执行前,先复制 lint.xml
            lintTask.doFirst {
                //如果 lint.xml 存在,则改名为 lintOld.xml
                if (lintFile.exists()) {
                    lintOldFile = project.file("lintOld.xml")
                    lintFile.renameTo(lintOldFile)
                }

                //进行 将 plugin 内置的 lint.xml 文件和项目下面的 lint.xml 进行复制合并操作
                def isLintXmlReady = copyLintXml(project, lintFile)
                //合并完毕后,将 lintOld.xml 文件改名为 lint.xml
                if (!isLintXmlReady) {
                    if (lintOldFile != null) {
                        lintOldFile.renameTo(lintFile)
                    }
                    throw new GradleException("lint.xml 不存在")
                }
            }

            //lint 任务执行后,删除 lint.xml
            project.gradle.taskGraph.afterTask { task, TaskState state ->
                if (task == lintTask) {
                    lintFile.delete()
                    if (lintOldFile != null) {
                        lintOldFile.renameTo(lintFile)
                    }
                }
            }
            //==========================  统一  lint.xml  结束=============================================//


            //==========================  在终端 执行命令 gradlew lintForXTC  的配置  开始=============================================//
            // 在终端 执行命令 gradlew lintForXTC 的时候,则会应用  lintTask
            if (!xtcLintTaskExists) {
                xtcLintTaskExists = true
                //创建一个 task 名为  lintForXTC
                project.task("lintForXTC").dependsOn lintTask
            }
            //==========================  在终端 执行命令 gradlew lintForXTC  的配置  结束=============================================//
        }
    }

    /**
     * 复制 lint.xml 到 targetFile
     * @param project 项目
     * @param targetFile 复制到的目标文件
     * @return 是否复制成功
     */
    boolean copyLintXml(Project project, File targetFile) {
        //创建目录
        targetFile.parentFile.mkdirs()

        //目标文件为  resources/config/lint.xml 文件
        InputStream lintIns = this.class.getResourceAsStream("/config/lint.xml")
        OutputStream outputStream = new FileOutputStream(targetFile)

        int retroLambdaPluginVersion = getRetroLambdaPluginVersion(project)

        if (retroLambdaPluginVersion >= 180) {
            // 加入屏蔽 try with resource 检测  1.8.0 版本引入此功能
            InputStream retroLambdaLintIns = this.class.getResourceAsStream("/config/retrolambda_lint.xml")
            XMLMergeUtil.merge(outputStream, "/lint", lintIns, retroLambdaLintIns)
        } else {
            // 未使用 或 使用了不支持 try with resource 的版本
            IOUtils.copy(lintIns, outputStream)
            IOUtils.closeQuietly(outputStream)
            IOUtils.closeQuietly(lintIns)
        }

        //如果复制操作完成后,目标文件存在
        if (targetFile.exists()) {
            return true
        }
        return false
    }
    /**
     * 获取 使用的 RetroLambda Plugin 插件的版本
     * @param project 项目
     * @return 没找到时返回 -1 ,找到返回正常 version
     */
    def static int getRetroLambdaPluginVersion(Project project) {
        DefaultExternalModuleDependency retroLambdaPlugin = findClassPathDependencyVersion(project, 'me.tatarka', 'gradle-retrolambda') as DefaultExternalModuleDependency
        if (retroLambdaPlugin == null) {
            retroLambdaPlugin = findClassPathDependencyVersion(project.getRootProject(), 'me.tatarka', 'gradle-retrolambda') as DefaultExternalModuleDependency
        }
        if (retroLambdaPlugin == null) {
            return -1
        }
        return retroLambdaPlugin.version.split("-")[0].replaceAll("\\.", "").toInteger()
    }

    /**
     * 查找 Dependency 的 Version 信息
     * @param project
     * @param group
     * @param attributeId
     * @return
     */
   def static findClassPathDependencyVersion(Project project, group, attributeId) {
        return project.buildscript.configurations.classpath.dependencies.find {
            it.group != null && it.group.equals(group) && it.name.equals(attributeId)
        }
    }
}

关于 Android 团队使用内部 Lint 检测的指导文档 2 ---> 集成 Lint 插件自动添加 Lint 检测的 AAR.md

六、应用自定义 Lint 到项目中去

我们刚才说了,我们有自定义 Lint 的 AAR,你可以直接加入到项目中,当然也可以通过自定义的插件来知己添加到项目中去。

6.1 添加自定义 Lint 检测的 AAR

6.1.1 添加自定义 Lint 检测的 AAR

在项目的 build.gradle 里面添加如下代码

compile 'com.xtc.lint:lint-check:1.0.1'

这里写图片描述

目前版本为 1.0.1,之后随着自定义规则越来越多,版本再逐步升高。

6.2 执行 lint 检测命令

6.2.1 本地执行 lint 命令

执行如下 lint 检测命令

gradlew clean lint

开始执行命令

这里写图片描述

开始 Lint 检测

这里写图片描述

检测完毕

这里写图片描述

6.2.2 Android Studio 执行 Inspect Code

如果上面的命令你用的不习惯,你可以使用 Android Studio 自带的代码检测

打开【Analyze】—>【Inspect Code】 这里写图片描述

弹出如下所示的 Scope 对话框,你可以选择检测的范围 这里写图片描述

选择完后,点击【OK】按钮即开始进行检测。

APP 项目比较大,可能检测得几分钟,请等待。

这里写图片描述

检测完后,会出现上图所示的选项框,在 Android Lint 下面可以看到我们自定义的 Lint 检测规则

这里写图片描述

这里写代码片

6.2.3 Jenkins 执行 lint 命令

修改 Jenkins 的编译命令为

clean lint build --stacktrace

这里写图片描述

编译完后,将 lint 报告归档到 FTP

这里写图片描述

Source files 填写

watch/build/outputs/lint-results-debug.html,lint-results-debug.xml 改目录要根据自己项目实际输出的 lint 报告来填写,可以自己查看 Jenkins 的工作区生成的目录

这里写图片描述

这里写图片描述

Remote directory 填写

'Android/Common/${JOB_NAME}/'yyyy-MM-dd-HH-mm-ss-'build-${BUILD_NUMBER}-git-${GIT_COMMIT}'

然后点击【高级】选项,勾选上【Flatten files】和【Remote directory】

这里写图片描述

这样编译完后,在 FTP 服务器对应的目录上就会有 lint 检测报告了 这里写图片描述

6.3 查看 Lint 检测报告

6.3.1 lint 报告输出目录

lint 命令执行完毕之后,会输出相应的 lint 报告,不同的 gradle 版本输出的文档地址可能不同,APP 的输出目录为:\watch\build\outputs

这里写图片描述

gradle 版本高的,输出目录可能为 \app\build\reports,如下所示

这里写图片描述

6.3.2 报告详情

打开 \watch\build\outputs\lint-results-debug.html 网页即可看到输出的 lint 检测报告。 这里写图片描述

  • 控件命名

这里写图片描述

  • 图片太大 这里写图片描述

  • Log 打印 这里写图片描述

  • Toast 这里写图片描述

如上图所示,我们就可以看到自定义规则出来的代码问题。请大家逐步修改检测出来的问题,后期的代码编译会将 lint 检测出来的问题作为一个指标。

6.4 添加自定义的 Lint 检测插件

上一个方法是要自己手动的添加 Lint 检测的 AAR 包到项目中,但是 aar 虽然很方便,但是在团队内部推广中我们遇到了以下问题:

  1. 配置繁琐,不易推广。每个库都需要自行配置 lint.xml、lintOptions,并且 compile aar。
  2. 不易统一。各库之间需要使用相同的配置,保证代码质量。但现在手动来回拷贝规则,且配置文件可以自己修改。

于是我开发一个 plugin,统一管理 lint.xml 和 lintOptions,自动添加 aar,下面是介绍如何添加该 Plugin

这里写图片描述

6.4.1 在根目录下的在 build.gradle 中,添加 Lint 检测插件

这里写图片描述

如上图所示,在 buildscript 中的 dependencies 块中,添加 Lint 检测的插件地址,

//lint 检测插件
classpath 'com.xtc.lint:lint-check-plugin:1.0.7-Dev'

6.4.2 在 module 的 build.gradle 中,应用 Lint 检测插件

这里写图片描述

如上图所示,添加如下代码即可

apply plugin: 'XTCLintPlugin'

并且把之前已经添加过的 lint 检测 aar 包代码去掉,

这里写图片描述

6.4.3 运行命令 gradlew lintForXTC 即可正常应用插件统一配置的 lint

6.4.3.1 在 Android Studio 中

gradlew clean lintForXTC

这里写图片描述

这样就可以使用 插件统一配置的 lint,进行静态代码分析了。

这里写图片描述

编译完后的 lint 报告,位置位于:lint-report 目录下,如下所示:

这里写图片描述

6.4.3.1 在 Jenkins 中

构建 在 Jenkins 中,修改下 构建的 任务 为

clean lintForXTC build --stacktrace

这里写图片描述

侯建后的操作

1、修改 Publish Android Lint results 这里写图片描述

修改为 watch/lint-report/lint-results*.xml

2、修改 FTP Publishers 增加一项,将 lint 检测的 html 报告保存到 ftp 路径 这里写图片描述

Source files 路径填写

 watch/lint-report/lint-results.html

Remote directory 远程保存路径填写

'Android/Common/${JOB_NAME}/'yyyy-MM-dd-HH-mm-ss-'build-${BUILD_NUMBER}-git-${GIT_COMMIT}'

构建过程,lint 检测过程中

这里写图片描述

构建后的结果 编译完后,位置位于:lint-report 目录下,如下所示:

这里写图片描述

保存到 FTP 的报告如下

这里写图片描述

可以将 html 文件 下载到本地然后使用浏览器打开,如下所示:

这里写图片描述

点击【Lint Issues】可以查看 Jenkins 插件的 Lint 报告

这里写图片描述

查看具体细节的 issue 和上面介绍的一样。

七、参考文献


这里写图片描述

作者:欧阳鹏 欢迎转载,与人分享是进步的源泉! 转载请保留原文地址:https://blog.csdn.net/ouyang_peng/article/details/80374867

如果觉得本文对您有所帮助,欢迎您扫码下图所示的支付宝和微信支付二维码对本文进行随意打赏。您的支持将鼓励我继续创作!

这里写图片描述

Support Me
Apps
About Me
Google+: Trinea trinea
GitHub: Trinea