阅读 794

Android自定义Lint增量代码检查工具

背景

Lint是Google提供的一个静态代码检查工具,可以扫描出代码中潜在的问题,并且会对开发人员做出提示。而且除了Android原生提供的几百种Lint规则以外,还可以使用Lint框架的API自定义Lint规则。

自定义Lint规则可以根据项目需求制定不同的扫描规则。比如:编码规范、代码风格、特定问题检查等。

有了自定义检查规则,提交代码的时候可以规范代码编写。但是还有一个问题,新的代码使用新的规范,但如何解决项目中老代码风格?对于老代码,不可能每行代码都要修改。这个时候就要有针对性的检查,那就是使用增量检查。

增量代码检查有以下几个好处

  • 避免修改“祖传”代码的一些问题。如果全量扫描,之前老代码的问题一大堆就会暴露出来, 这样就大大增加了工作量,开发人员也没有那么多的精力全部修改;
  • 增加了一些代码规范的强制性。增量扫描代码,如果代码有问题,就会滚,可以强制开发人 员规范代码;

实现Lint增量代码检查工具

这里Lint增量代码检查工具是以Gradle插件的方式实现的。只需要在项目中引用插件便可使用Lint工具。主要实现的功能是在git提交场景下,每次提交代码都会检查新增的代码(以行为单位)。如果代码中存在不符合Lint自定义规则的代码,就回滚本次提交。

增量代码检查流程

Lint增量代码检查工具使用git hooks(post-commit) + Lint框架实现。

git hooks:是用来响应git的操作的脚本,相当于一个回调。执行特定的git操作会出发特定的git hooks脚本执行。

Lint框架:是实现Lint扫描的基础。利用Lint框架提供的API执行Lint扫描。

  1. 提交(git commit)本次修改代码(通过git diff命令找出提交的文件);
  2. 触发git hooks(post-commit)脚本执行。在脚本中执行Lint检查任务(该任务是gradle任务)开始Lint检查;
  3. 创建LintRequest(主要作用是指定Lint将要扫描的文件);
  4. 获取增量代码(通过git diff找出修改的行号);
  5. 开始Lint检查;
  6. 检查完毕输出结果,如果有不符合规则的代码,将回退本次提交。

Lint增量检查实现原理

看完上面的流程,可能会觉得不明所以。这里针对各个步骤做出详细解析。在实现Lint增量代码检查的过程中,首要的步骤就是获取将要提交的增量代码。目前的解决方案是通过git命令获取相关数据。

git hooks的执行

目前的方案是通过使用post-commit脚本触发检查流程。post-commit脚本是在git commit之后执行。

获取LInt检查的文件

获取增量文件

git diff --name-only --diff-filter=ACMRTUXB HEAD~1 HEAD~0

通过git diff命令获取本次提交的文件。

/**
 * 通过Git命令获取需要检查的文件
 *
 * @param project gradle.Project
 * @return 文件列表
 */
List<String> getCommitChange(Project project) {
    ArrayList<String> filterList = new ArrayList<>()
    try {
        //此命令获取本次提交的文件 在git commit之后执行
        String command = "git diff --name-only --diff-filter=ACMRTUXB HEAD~1 HEAD~0"
        String changeInfo = command.execute(null, project.getRootDir()).text.trim()
        if (changeInfo == null || changeInfo.empty) {
            return filterList
        }

        String[] lines = changeInfo.split("\\n")
        return lines.toList()
    } catch (Exception e) {
        e.printStackTrace()
        return filterList
    }
}
复制代码

获取增量代码

这是关键的一步,因为这一步要获取增量代码所在的具体行号,通过使用这些行号数据实现增量检查的效果。

git diff --unified=0 --ignore-blank-line --ignore-all-space HEAD~1 HEAD filepath

filepath就是增量文件的相对路径。

数据准备完毕以后,剩下的工作就要交给Lint框架了。接下来开始执行Lint检查操作。

/**
 * 通过git diff获取已提交文件的修改,包括文件的添加行的行号、删除行的行号、修改行的行号
 *
 * @param filePath 文件路径
 * @param project Project对象
 * @param startIndex 修改开始的下表数组
 * @param endIndex 修改结束的下表数组
 */
void getFileChangeStatus(String filePath, Project project, List<Integer> startIndex, List<Integer> endIndex) {
    try {
        String command = "git diff --unified=0 --ignore-blank-lines --ignore-all-space HEAD~1 HEAD " + filePath
        String changeInfo = command.execute(null, project.getRootDir()).text.trim()
        String[] changeLogs = changeInfo.split("@@")
        String[] indexArray

        for (int i = 1; i < changeLogs.size(); i += 2) {
            indexArray = changeLogs[i].trim().split(" ")
            try {
                int start, end
                String[] startArray = null
                if (indexArray.length > 1) {
                    startArray = indexArray[1].split(",")
                }

                if (startArray != null && startArray.length > 1) {
                    start = Integer.parseInt(startArray[0])
                    end = Integer.parseInt(startArray[0]) + Integer.parseInt(startArray[1])
                } else {
                    start = Integer.parseInt(startArray[0])
                    end = start + 1
                }
                startIndex.add(start)
                endIndex.add(end)
            } catch (NumberFormatException e) {
                e.printStackTrace()
                startIndex.add(0)
                endIndex.add(0)
            }

        }
    } catch (Exception e) {
        e.printStackTrace()
    }
}
复制代码

执行Lint检查

Lint框架中的主要类说明:

  • LintCliClient:Lint客户端,作用是集成lint检查的操作、相关配置以及lint检查的入口。
  • LintCliFlags:Lint标志位管理类,提供了Lint操作的标志位。Lint代码检查工具主要使用了该类中生成日志的配置,通过加入不同实现的报告生成类可以实现不同的输出格式(比如TXT、XML、HTML等)。
  • LintRequest:执行Lint操作时的一个请求类,主要作用是存储Lint将要扫描的文件。在Lint工具中重写LintRequest初始化方法可以实现增量文件的检查。
  • LintDriver:执行Lint规则检查逻辑的类。
  • IssueRegistry:自定义Lint规则管理类。用于添加Lint自定义规则。

上述对于Lint框架中类的介绍是在实现Lint增量代码检查中主要用到的类。

创建LintRequest

class LintToolClient extends LintCliClient {

    @Override
    /**
     * 通过重写createLintRequest方法创建LintRequest
     */
    protected LintRequest createLintRequest(List<File> files) {
        LintRequest request = super.createLintRequest(files)
        for (Project project : request.getProjects()) {
            for (File file : files) {
                project.addFile(file)
            }
        }
        return new LintRequest(this, files)
    }
}
复制代码

上面的代码就是LintRequest的创建过程,通过重写LintCliClient中的createLintRequest方法。其中参数files就是将要检查的文件。

Lint检查的执行逻辑

/*LintCliClient*/
public int run(@NonNull IssueRegistry registry, @NonNull List<File> files) throws IOException {
        assert !flags.getReporters().isEmpty();
        this.registry = registry; //Lint自定义检查规则

        LintRequest lintRequest = createLintRequest(files); //创建LintRequest
        driver = createDriver(registry, lintRequest); //创建LintDriver

        addProgressPrinter();
        validateIssueIds();

        driver.analyze(); //执行Lint检查

        Collections.sort(warnings);

        int baselineErrorCount = 0;
        int baselineWarningCount = 0;
        int fixedCount = 0;

        LintBaseline baseline = driver.getBaseline();
        if (baseline != null) {
            baselineErrorCount = baseline.getFoundErrorCount();
            baselineWarningCount = baseline.getFoundWarningCount();
            fixedCount = baseline.getFixedCount();
        }

        Stats stats = new Stats(errorCount, warningCount,
                baselineErrorCount, baselineWarningCount, fixedCount);

        boolean hasConsoleOutput = false;
    	//根据LintCliFlags中的Reports打印Lint报告
        for (Reporter reporter : flags.getReporters()) {
            reporter.write(stats, warnings);
            if (reporter instanceof TextReporter && ((TextReporter)reporter).isWriteToConsole()) {
                hasConsoleOutput = true;
            }
        }
    
    //............省略部分代码..............

        return flags.isSetExitCode() ? (hasErrors ? ERRNO_ERRORS : ERRNO_SUCCESS) : ERRNO_SUCCESS;
    }
复制代码

上面代码的逻辑就是Lint执行检查的主要过程。可以看到,在代码中先传入自定义的Lint规则IssueRegistry,然后创建LintRequest,接下就开始执行Lint检查,最后将结果输出。结果输出到添加在LintCliFlags的Reports中。

增量代码检查的实现

//输出Lint检查报告
for (Reporter reporter : flags.getReporters()) {
            reporter.write(stats, warnings);
            if (reporter instanceof TextReporter && ((TextReporter)reporter).isWriteToConsole()) {
                hasConsoleOutput = true;
            }
        }
复制代码

根据上面代码的逻辑,是Lint检查输出结果的过程。增量代码检查的实现就是重写Reporter类,在重写的类中实现自定义的输出规则,这里的实现方法就是使用上文中通过git命令获取的文件修改行号进行过滤,从而实现增量检查的效果。

总结

上面描述了Lint增量代码检查工具的实现过程,实现增量代码检查的关键就是获取文件修改的精确位置,以便在输出结果是进行过滤。

增量代码检查相较于常规的Lint检查,好处就是能够避免老代码的与新规则的冲突,同时结合git使用能够在提交代码时增加一些强制性。

最后,Lint增量代码工具中使用的是Lint的自定义规则。这些还可以作为原生的Lint规则的扩展,在代码编写的阶段使用,效果跟原声Lint规则一致。

对Lint增量代码工具的实现感兴趣的同学,可以在github上获取源码,感兴趣的可以star一下。

Lint增量代码检查工具链接