在软件开发过程中,不规范的代码和违规组件的引入常常会导致代码质量下降和给项目引入风险。而在持续集成平台业务中,现有代码规范扫描拦截发生在代码push时候,违规组件拦截发生在生成构建产物后,发现问题时间点较晚,为了将质量卡点进一步左移、降低研发人员因代码准入失败造成的返工成本、提高代码编写质量,需要寻求上述问题场景的解决方案。
经过插件方案的调研与验证,我们基于intellij平台开发了Aladdin研发插件,插件主要提供增量代码扫描,扫描规则基于Alibaba编码规范条款,和研发中心组件使用规约,提供多种扫描方式为:实时增量代码扫描、编译时增量代码扫描、提交前增量扫描、手动全局代码扫描、手动增量代码扫描。同时提供提交问题拦截和扫描方式配置。
想要对代码进行扫描,我们需要向idea注册一个Inspection检查器,当编辑器中代码发生变动或者保存代码时,都会调用这个检查器,扫描代码,注册渲染问题描述到对应代码中。
<extensions defaultExtensionNs="com.intellij">
<localInspection language="JAVA"
shortName="AladdinCodeStyleP1Incr"
displayName="增量高危代码分析"
hasStaticDescription="true"
enabledByDefault="true"
implementationClass="com.example.PmdP1IncrInspection"
groupName="Aladdin编码规范"
level="ERROR"
runForWholeFile="true"
/>
</extensions>
插件启动的时候初始化好PMD
规则,并按照类型分类到不同的Inspection中
只分析目录src/main/java
下的有代码变动的Java文件,缩小扫描文件范围
调用PMD
完成增量文件的扫描,获取代码存在的问题
根据增量代码行来控制代码问题的注册和编辑器中的问题标注显示
初始化规则并进行等级分类
abstract class PmdBaseInspection : LocalInspectionTool(), PmdInspectionIdentify {
private val javaPathIdentify: String = "src/main/java"
//根据不同的规则类型放入到不同Inspection的ruleRests中。
private val ruleRests: RuleSets = run {
listOf(
"java-ali-comment",
"java-ali-concurrent",
"java-ali-constant",
"java-ali-exception",
"java-ali-flowcontrol",
"java-ali-naming",
"java-ali-oop",
"java-ali-orm",
"java-ali-other",
"java-ali-set",
)
.flatMap { RuleSetFactory().createRuleSet(it).rules }
.filter { it.priority == filterByPmdRulePriority() }
.map { RuleSetFactory().createSingleRuleRuleSet(it) }
.fold(RuleSets()) { rss, rs ->
rss.addRuleSet(rs)
rss
}
}
}
高危问题Inspection
继承PmdBaseInspection
重写filterByPmdRulePriority()
方法,将所有高危问题纳入PmdP1IncrInspection
中
class PmdP1IncrInspection : PmdBaseInspection() {
//过滤高危级别规则
override fun filterByPmdRulePriority(): RulePriority {
return RulePriority.HIGH
}
// 设置inspection描述
override fun getStaticDescription(): String {
return "Aladdin编码规范高危问题"
}
// interception 等级
override fun getDefaultLevel(): HighlightDisplayLevel {
return HighlightDisplayLevel.ERROR
}
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
// 实现psiElementVisitor,扫描代码,注册高危问题
}
}
建议问题Inspection
继承PmdBaseInspection
重写filterByPmdRulePriority()
方法,将所有建议问题纳入PmdP2IncrInspection
中
class PmdP2IncrInspection : PmdBaseInspection() {
// 过滤建议级别规则
override fun filterByPmdRulePriority(): RulePriority {
return RulePriority.MEDIUM_HIGH
}
// 设置inspection描述
override fun getStaticDescription(): String {
return "Aladdin编码规范建议修复"
}
// interception 等级
override fun getDefaultLevel(): HighlightDisplayLevel {
return HighlightDisplayLevel.WARNING
}
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
// 实现psiElementVisitor,扫描代码,注册建议问题
}
}
psiElementVisitor
来完成增量代码的扫描和问题注册渲染功能了,这个在下文具体介绍功能说明:编写代码时,实时对增量/变动代码进行扫描,并对有问题的代码标红提示,并提供快速修复功能和问题解释说明
目的:在存量代码不变动的情况下,希望对增量的代码进行代码问题扫描,防止新增代码引入代码问题
插件可配置开启实时高危增量代码扫描,并提供实时修复建议。
插件代码实时扫描,实时提供问题提示及示例。如图:
现在大部分代码扫描工具都是对全量代码进行扫描,但在一些情况下,如老项目的存量代码不敢轻易优化,只希望对增量代码进行扫描,防止引入新的代码问题,对此我们增加了增量代码扫描的功能,考虑到不同的场景对获取增量代码行的性能和扫描范围的要求不同,我们进行了一些探索,最终采用了以下两种获取增量代码行的方式:
增量扫描方式 | 扫描频率 | 范围 | 性能要求 | 获取方式 |
---|---|---|---|---|
实时增量代码扫描 | 高,文件变动后触发 | 小,编辑器中当前文件 | 高 | 1. 增量文件全为增量代码行 2. 增量行数据idea已经帮我们算好了,拿来用就可以 |
本次提交增量代码扫描 | 低,主动触发 | 大,git项目中变动文件 | 一般 | 插件中集成 ”git4idea插件“ 调用git diff获取增量代码行 |
这里有以下关键点:
获取当前文件代码增量行:
首先,我们观测到idea本身就帮助我们计算了增量行数据,编辑器页面就有体现,直接试用idea的api:project.getLineStatusManager().getLineStatusTracker
获取使用即可,如下图所示:
获取项目的所有增量代码行:
插件中集成 git4idea,直接调用 git diff 的方式获取增量代码行
class DiffLineContext {
companion object {
// 缓存增量代码行数据, key:文件路径,value:增量代码行号set
var DIFF_FILE_VS_NEW_LINES: Map<String, Set<Int>> = mapOf()
fun analysePatch(project: Project) {
ProgressManager.getInstance().runProcessWithProgressSynchronously({
//调用git diff,分析后获得增量代码行号
DIFF_FILE_VS_NEW_LINES = project.getGitRepository()
?.root
?.let { GitLineHandler(project, it, GitCommand.DIFF) }
?.also { it.addParameters("HEAD") }
?.exec()
?.outputAsJoinedString
?.let { PatchParseHelper.parsePatch(it) }
?.entries
?.groupingBy { it.key }
?.fold(setOf()) { _, ii -> ii.value.toSet() }
?: mapOf()
}, "Aladdin编码规范:增量代码分析", false, project)
}
}
}
核心代码:
PsiElementVisitor
抽象类扩展点visitFile()
可完成代码扫描功能PsiElementVisitor
,来完成代码问题扫描、注册、渲染的逻辑class PmdInspectionVisitor(
private val ruleSets: RuleSets,
private val holder: ProblemsHolder,
private val isOnTheFly: Boolean,
) : PsiElementVisitor() {
override fun visitFile(psiFile: PsiFile) {
//按规则扫描代码,并获取扫描结果
val violations = processFile(psiFile, ruleSets)
.takeIf { it.isNotEmpty() }
?: return
// isOnTheFly标记是否是实时扫描
val newLines = if (isOnTheFly)
// 获取idea已经计算好的增量代码行
project.getLineStatusManager().getLineStatusTracker(document)
?.getRanges()
?.filter { it.line1 != it.line2 }
?.map { it.line1 + 1..it.line2 }
?.flatten()
?.toSet()
?: setOf()
else
//通过git diff的方式获取增量行的缓存
psiFile.project.basePath
?.let { psiFile.virtualFile.path.substringAfter(it) }
?.let { DiffLineContext.DIFF_FILE_VS_NEW_LINES[it] }
?.toSet()
?: setOf()
//扫描出来的问题自带起始行和结束行信息,判断是否在增量代码行范围内
violations.filter {
newLines.contains(it.beginLine)
|| newLines.contains(it.endLine)
}
// 调用 holder.registerProblem 完成代码问题注册和渲染
// ......
}
}
功能说明:对新引入或者已修改组件进行依赖扫描(根据Aladdin已配置的违规组件),包括新引入组件的直接或者间接依赖组件。
目的:当前阶段,线上在跑带有高危组件的应用已全部完成治理,为了防止新的高危组件引入,增加了增量组件扫描功能。
高危组件实时扫描并提供修复建议,仅针对新添加或修改组件
高危组件配置与Aladdin持续集成系统保持一致,且可实时配置生效
实现增量组件扫描有以下关键点:
计算增量组件
private fun analyse(project: Project, mavenProject: MavenProject) {
// git show HEAD:pomFilePath 获取最近一次代码提交信息
val oldDeps: Set<String> = pomFileStatus
.takeIf { it == FileStatus.MODIFIED }
?.let { project.getGitRepository()?.root }
?.let { GitLineHandler(project, it, GitCommand.SHOW) }
?.also { it.addParameters("HEAD:${mavenProject.file.path.substringAfter("$pjPath/")}") }
?.exec()
?.outputAsJoinedString
?.let { pomToDepSet(it) } // 解析获取最近一次pom文件的直接依赖dependencies
?: setOf()
// 获取当前pom文件的内容,并解析获取当前pom文件的直接dependencies,排除没有发生变动的依赖,即为增量依赖
val diffDeps: Set<String> = mavenProject.file.contentsToByteArray()
?.let { String(it) }
?.let { pomToDepSet(it) }
?.let { it - oldDeps }
?.map { it.substringBefore("##") }
?.toSet()
?: setOf()
//这里直接从idea的maven依赖树种读取增量依赖对应的依赖包数据,包含增量依赖及其间接依赖,丢给Aladdin持续集成平台进行合规扫描,并缓存扫描结果
//......
}
获取增量组件及其间接依赖、风险分析、缓存分析结果
IDEA 已经将maven项目的maven依赖树解析完毕,通过上述过程获取到增量依赖后,可直接从maven依赖树中获取增量依赖包下边所有间接依赖。
// 增量组件分线问题缓存,这里缓存的目的是,当打开pom文件时候,需要对有问题的增量组件渲染问题描述,如果每次都取请求分析依赖包,成本较大,这里采用每次idea去刷新mavenProject依赖树的时候去更新缓存
val DIFF_DEP_VIOLATION_CACHE = mutableMapOf<String, Map<String, List<AladdinHttpClient.DepValResponseData>?>>()
mavenProject.dependencyTree
.filter { diffDeps.contains("${it.artifact.groupId}:${it.artifact.artifactId}") }
.filter { !(it?.artifact?.path?.startsWith(pjPath) ?: false) }
.takeIf { it.isNotEmpty() }
?.mapNotNull { assembleDepDataWrapper(it) }
// 增量组件发送Aladdin进行风险分析
?.let { AladdinHttpClient.DepValRequest(gitRepo, it) }
?.query()
?.filter { it.violations?.isNotEmpty() ?: false }
?.groupingBy { "${it.groupId}:${it.artifactId}" }
?.aggregate<AladdinHttpClient.DepValResponse, String, List<AladdinHttpClient.DepValResponseData>?> { _, _, dep, _ -> dep.violations }
?.ifEmpty { null }
// 增量组件风险问题放入缓存
?.also { DIFF_DEP_VIOLATION_CACHE[mavenProject.path] = it }
?: DIFF_DEP_VIOLATION_CACHE.remove(mavenProject.path)
实现风险组件结果页面渲染 Inspection
class MavenPomInspection : LocalInspectionTool(), DepInspectionIdentify {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
// 根据 pom文件路径,获取缓存的风险组件问题
val violationCache = DiffDependencyContext.DIFF_DEP_VIOLATION_CACHE[pf.virtualFile.path]
//重写visitXmlTag方法,根据pom文件中的标签判断是否是问题组件,这里是根据xml标签dependency中的grouId和artifactId来判定的
return object : XmlElementVisitor() {
override fun visitXmlTag(tag: XmlTag?) {
super.visitXmlTag(tag)
val dep = tag as XmlTagImpl
if (XML_TAG_DEPENDENCY != dep.name) return
val groupId = dep.findFirstSubTag(XML_TAG_GROUP)?.value?.text ?: return
val artifactId = dep.findFirstSubTag(XML_TAG_ARTIFACT)?.value?.text ?: return
// 从缓存中获取风险问题
val violations = violationCache["${groupId}:${artifactId}"]
// 向idea注册问题,并渲染到编辑页面
holder.registerProblem(dep,
violations.mapNotNull { it.failMessage }.reduce { acc, s -> acc + "\n\n" + s },
ProblemHighlightType.ERROR)
}
}
}
}
功能说明:代码push时若增量文件存在Block等级问题将被拦截提醒,并提示编译/组件依赖/代码规约等问题数量,可查看详情
目的:希望对增量代码进行编译问题/组件依赖问题/代码问题扫描拦截说明,防止高危问题遗漏
代码提交前,插件会进行一次增量代码扫描,依据Aladdin中站点设置的开关进行提醒拦截,以代替体验较差、文案不清晰的Aladdin拦截
如果存在违规代码,插件会提示问题类型和个数,其中包括:语法错误问题,P3C高危问题,高风险组件问题,点击查看详情效果,可在问题列表中查看具体问题,对应类,代码行,以及修复建议
自插件上线以来,研发中心整体有效用户安装量覆盖达到100%
代码问题拦截:Gitlab代码准入拦截向插件拦截转换情况如下
按周统计,随着插件用户使用人数增多,插件拦截问题数逐步增长,Gitlab代码准入拦截问题数逐步降低,这表明代码问题拦截逐步转移到代码开发阶段。
Aladdin研发插件提供了一系列编码规范和最佳实践,能够自动检测代码中潜在问题并给出警告或错误提示。使用插件有以下好处 :
统一代码规范:基于Alibaba编码规范条款,和研发中心组件使用规约提供一套认可的编码规范。
前置发现潜在问题:插件可检测代码中潜在问题,有助于提高代码质量和稳定性,将质量问题拦截左移,降低拦截打回的成本。
规范编码习惯:插件提供很多实用的代码提示和建议,从而写出更健壮的代码。
通过扫描新引入依赖包:拦截风险组件,防止引入新的风险组件。
在未来的规划中,我们将继续对插件进行迭代升级
开发定制化代码规范,如定制化日志规范等,逐步优化完善出适合我们自己的一套规范
增加本地的增量单元测试覆盖率功能,实现增量代码覆盖率分析等
swj,信也科技后端研发专家,主要负责基石
zhx,信也科技后端研发专家,主要负责aladdin、rubik、tcm