百臂巨人与塔尔塔罗斯
前言
一款静态代码检测工具,包含阿里java规约检测和lint检测,支持自定义pmd和lint配置,结合git在代码提交时进行增量检测插件,赫卡同克瑞斯,也就是百臂巨人,来自希腊神话,是天空神乌拉诺斯和地神盖娅的儿子,拥有50头和100个手臂,在帮助宙斯夺得神位后,成为了塔尔塔罗斯的守门人,塔尔塔罗斯就是希腊神话中的地狱。
为什么要写百臂巨人
很多时候有些bug就是因为代码不规范造成的,这种低级错误往往会造成重大损失,之前就曾经碰到过在主线程加载图片的情况,之前因为运营配置的图片较小,所以也就没什么事,直到有一天运营配置了一个大图,直接导致大面积的ANR,这是一个低级错误,后来我写了一个自定义lint检测工具,用来检测这种在主线程使用BitmapFactory的情况。之后又写了一些其他的lint检测规则,之后遇到了另外两个问题,第一,我的lint检测出来了,标注出来了,但是开发者依旧会对他进行忽视,检测出来而不修改那不就是白搞了?如何强制开发者进行检测,这是一个问题。第二,项目庞大,整体检测时间很长,而且检测出的问题都上万了,这些问题谁来改?陈芝麻烂谷子的代码,又有谁愿意改?为此我需要一个增量检测的方案,结合git,只对要提交的修改的文件进行检测,这样谁修改谁倒霉,听天由命,避免抱怨。
原理图
image核心代码
获取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检测流程图
imagelint的检测功能的编写因为缺乏文案,只能通过阅读源码来获取,这里主要是有两个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见下面链接