百臂巨人与塔尔塔罗斯

2,258 阅读4分钟

前言

一款静态代码检测工具,包含阿里java规约检测和lint检测,支持自定义pmd和lint配置,结合git在代码提交时进行增量检测插件,赫卡同克瑞斯,也就是百臂巨人,来自希腊神话,是天空神乌拉诺斯和地神盖娅的儿子,拥有50头和100个手臂,在帮助宙斯夺得神位后,成为了塔尔塔罗斯的守门人,塔尔塔罗斯就是希腊神话中的地狱。

为什么要写百臂巨人

很多时候有些bug就是因为代码不规范造成的,这种低级错误往往会造成重大损失,之前就曾经碰到过在主线程加载图片的情况,之前因为运营配置的图片较小,所以也就没什么事,直到有一天运营配置了一个大图,直接导致大面积的ANR,这是一个低级错误,后来我写了一个自定义lint检测工具,用来检测这种在主线程使用BitmapFactory的情况。之后又写了一些其他的lint检测规则,之后遇到了另外两个问题,第一,我的lint检测出来了,标注出来了,但是开发者依旧会对他进行忽视,检测出来而不修改那不就是白搞了?如何强制开发者进行检测,这是一个问题。第二,项目庞大,整体检测时间很长,而且检测出的问题都上万了,这些问题谁来改?陈芝麻烂谷子的代码,又有谁愿意改?为此我需要一个增量检测的方案,结合git,只对要提交的修改的文件进行检测,这样谁修改谁倒霉,听天由命,避免抱怨。

原理图

核心代码

获取git修改文件

增量检测,必然要相应的文件,这里利用了git的命令

fun getCommitFiles(): List<File> {

        val command =
            arrayOf("/bin/bash", "-c", "git diff --name-only --diff-filter=ACMRTUXB HEAD")

        val process = Runtime.getRuntime().exec(command)

        process.waitFor()

        val commitFileList = mutableListOf<File>()

        try {
            val inputReader = BufferedReader(InputStreamReader(process.inputStream))


            var fileName = inputReader.readLine()

            while (fileName != null) {

                commitFileList.add(File(fileName))
                fileName = inputReader.readLine()
            }

            inputReader.close()
        } catch (e: IOException) {
            e.printStackTrace()
        } catch (e: Exception) {
            e.printStackTrace()
        }

        return commitFileList
    }

除了删除的文件,其他文件都会作为增量文件提取出来

阿里规约检测的接入

阿里对Java代码规范做了详尽的说明,这里我们利用阿里规约中的检测插件中的检测工具进行java文件的检测 首先为项目添加p3c依赖

private fun configPmdDependency(project: Project) {
        project.plugins.apply(PMD)
        val pmdConfig = project.configurations.getByName(PMD_CONFIGURATION)
        pmdConfig.dependencies.add(project.dependencies.create(P3C_PMD_DEPENDENCY))
        pmdConfig.dependencies.add(project.dependencies.create(PMD_DEPENDENCY))
    }

这样就引用了阿里规约的pmd规则,同时由于引入pmd,支持相应的配置

private fun configPmdTask(project: Project) {
        project.afterEvaluate {
            val pmdExtension = project.extensions.findByName(PMD) as PmdExtension
            val pmdTask = project.tasks.create(PMDTASK, Pmd::class.java)
            pmdTask.targetJdk = pmdExtension.targetJdk
            pmdTask.ignoreFailures = pmdExtension.isIgnoreFailures
            ALIRULESETS.addAll(pmdExtension.ruleSets)
            pmdTask.ruleSets = ALIRULESETS
            pmdTask.ruleSetFiles = pmdExtension.ruleSetFiles
            pmdTask.source(project.rootDir)
            pmdTask.isConsoleOutput = pmdExtension.isConsoleOutput
            pmdTask.rulePriority = pmdExtension.rulePriority
            pmdTask.reports {
                it.xml.isEnabled = true
                it.xml.destination = File(pmdExtension.reportsDir, "report.xml")
                it.html.isEnabled = true
                it.html.destination = File(pmdExtension.reportsDir, "report.html")
            }
            pmdTask.group = GOUP_NAME
            pmdTask.include(GitUtil.getCommitFilesPathForPMD())
            pmdTask.exclude("**/build/**", "**/res/**", "**/*.xml", "**/*.gradle", "**/*.kt")
        }
    }

lint检测

lint检测流程图

lint的检测功能的编写因为缺乏文案,只能通过阅读源码来获取,这里主要是有两个task,一个是IncrementLintGlobalTask,这个是会检测项目中所有变体的task,另一个就是IncrementLintPerVariantTask,会根据项目中不同的变体生成不同的检测task,检测范围也局限于相应变体。

默认的linttask是对全部文件进行检测的,为了实现对增量文件的检测,需要重写LintGradleClient的createLintRequest方法,为此我写了一个IncrementLintGradleClient

class IncrementLintGradleClient(
    version: String,
    issueRegistry: IssueRegistry,
    lintFlags: LintCliFlags,
    gradleProject: org.gradle.api.Project,
    sdkHome: File?,
    variant: Variant?,
    variantInputs: VariantInputs?,
    buildToolInfo: BuildToolInfo?,
    isAndroid: Boolean
) : LintGradleClient(
    version,
    issueRegistry,
    lintFlags,
    gradleProject,
    sdkHome,
    variant,
    variantInputs,
    buildToolInfo,
    isAndroid
) {

    override fun createLintRequest(files: MutableList<File>?): LintRequest {
        val lintRequest = super.createLintRequest(files)
        val commitFiles = GitUtil.getCommitFiles()
        lintRequest.getProjects()?.forEach { project ->

            commitFiles.forEach {
                project.addFile(it)
            }
        }
        return lintRequest
    }

}

lint的api经常变化,而且修改幅度还很大,为了屏蔽相应的lint api,gradle差异以及kotlin compiler的差异,我们使用IncrementReflectiveLintRunner来调用lint检测,这个和原项目插件中ReflectiveLintRunner没什么区别,只不过使用了我们自己的IncrementLintGradleExecution来分析增量文件,同时在插件中让当前项目使用lintclasspath引用插件

private fun addLintClassPath(project: Project) {
        project.gradle.rootProject.configurations
        val classPathConfiguration = project.gradle.rootProject.buildscript.configurations.getByName("classpath")
        var hecatoncheiresDependency: Dependency? = null
        classPathConfiguration.dependencies.forEach {
            if (it.name.contains(Constants.HECATONCHEIRESEXTENSION_NAME)) {
                hecatoncheiresDependency = it
                return@forEach
            }
        }
        val lintConfiguration = project.configurations.getByName(LintBaseTask.LINT_CLASS_PATH)
        project.dependencies.add(
            lintConfiguration.name,
            hecatoncheiresDependency
        )
    }

这样插件的jar包路径就能被IncrementReflectiveLintRunner获得,并且可以使用自己的classloader用来加载IncrementLintGradleExecution

最后

这个项目感觉算是一个较为完善的静态检测方法,使用方法与demo见下面链接

项目地址