1.背景
在软件开发过程中,不规范的代码和违规组件的引⼊常常会导致代码质量下降和给项⽬引⼊⻛险。⽽在持续集成业务中,现有代码规范扫描拦截发⽣在代码push时候,违规组件拦截发⽣在⽣成构建产物后,发现问题时间点较晚,为了将质量卡点进⼀步左移、降低研发⼈员因代码准⼊失败造成的返⼯成本、提⾼代码编写质量,需要寻求上述问题场景的解决⽅案。
2.⽅案
经过插件⽅案的调研与验证,我们基于intellij平台开发了Aladdin研发插件,插件主要提供增量代码扫描,扫描规则基于Alibaba编码规范条款,和研发中⼼组件使⽤规约,提供多种扫描⽅式为:实时增量代码扫描、编译时增量代码扫描、提交前增量扫描、⼿动全局代码扫描、⼿动增量代码扫描。同时提供提交问题拦截和扫描⽅式配置。
3.插件功能实现原理
3.1注册Inspection
想要对代码进⾏扫描,我们需要向idea注册⼀个Inspection检查器,当编辑器中代码发⽣变动或者保存代码时,都会调⽤这个检查器,扫描代码,注册渲染问题描述到对应代码中。
- 开发⼀个⾃定义inspection , ⾸先要注册我们的增量inspection, 在插件项⽬中
\src\main\resources\META-INF\plugin.xml 插件配置⽂件中配置我们的增量⾼危代码分析inspection
XML
11 /> 12 </extensions> |
- 注册好Inspection后,接下来实现我们的增量代码分析功能。当idea编辑器中⽂件发⽣变动或主动保存时候,都会触发inspection的扫描,⽽Inspection可能会有很多,每⼀个都会触发⼀次,因此实时检测对性能要求⽐较⾼。明确增量代码Inspection主要有以下⼏件事情要做:
1.插件启动的时候初始化好 PMD规则,并按照类型分类到不同的Inspection中
2.只分析⽬录src/main/java下的有代码变动的Java⽂件,缩⼩扫描⽂件范围
3.调⽤PMD完成增量⽂件的扫描,获取代码存在的问题
4.根据增量代码⾏来控制代码问题的注册和编辑器中的问题标注显示
初始化规则并进⾏等级分类
Kotlin
-
- abstract class PmdBaseInspection : LocalInspectionTool(), PmdInspectionIde ntify {
- 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",
16 )
17 .flatMap { RuleSetFactory().createRuleSet(it).rules }
18 .filter { it.priority == filterByPmdRulePriority() }
19 .map { RuleSetFactory().createSingleRuleRuleSet(it) }
20 .fold(RuleSets()) { rss, rs ->
21 rss.addRuleSet(rs)
22 rss
23 }
24 }
25 }
⾼危问题Inspection
PdP1IncrInspection中
Kotlin |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 } |
18 19 20 |
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean ): PsiElementVisitor { // 实现psiElementVisitor,扫描代码,注册⾼危问题 } } |
继承PmdBaseInspection重写filterByPmdRulePriority()⽅法,将所有建议问题纳⼊
PdP2IncrInspection 中
Kotlin
- class PmdP2IncrInspection : PmdBaseInspection() {
- // 过滤建议级别规则
- override fun filterByPmdRulePriority(): RulePriority {
- return RulePriority.MEDIUM_HIGH
5 }
6
- // 设置inspection描述
- override fun getStaticDescription(): String {
- return "Aladdin编码规范建议修复"
10 }
11
- // interception 等级
- override fun getDefaultLevel(): HighlightDisplayLevel {
- return HighlightDisplayLevel.WARNING
15 }
16
- override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean
): PsiElementVisitor {
- // 实现psiElementVisitor,扫描代码,注册建议问题
19 }
20 }
- 接下来就是实现psiElementVisitor来完成增量代码的扫描和问题注册渲染功能了,这个在下文具体介绍
3.2增量代码实时扫描
3.2.1增量代码实时扫描原理图
功能说明:编写代码时,实时对增量/变动代码进⾏扫描,并对有问题的代码标红提示,并提供快速修复功能和问题解释说明
⽬的:在存量代码不变动的情况下,希望对增量的代码进⾏代码问题扫描,防⽌新增代码引⼊代码问题
- 原理图
3.2.2增量代码实时扫描功能说明及效果图
- 插件可配置开启实时⾼危增量代码扫描,并提供实时修复建议。
- 插件代码实时扫描,实时提供问题提示及示例。如图:
3.2.3实时增量代码扫描原理
现在⼤部分代码扫描⼯具都是对全量代码进⾏扫描,但在⼀些情况下,如⽼项⽬的存量代码不敢轻易优化,只希望对增量代码进⾏扫描,防⽌引⼊新的代码问题,对此我们增加了增量代码扫描的功能,考虑到不同的场景对获取增量代码⾏的性能和扫描范围的要求不同,我们进⾏了⼀些探索,最终采⽤了以下两种获取增量代码⾏的⽅式:
增量扫描⽅式 | 扫描频率 | 范围 | 性能要求 | 获取⽅式 |
实时增量代码扫描 | ⾼,⽂件变动后触发 | ⼩,编辑器中当前 ⽂件 | ⾼ |
|
本次提交增量代码扫描 | 低,主动触发 | ⼤,git项⽬中变动⽂件 | ⼀般 | 插件中集成 ” “ git4idea插件 调⽤git diff获取增量代码⾏ |
这⾥有以下关键点:
- 实时扫描是对当前⽂件的扫描,对实时性要求⾼,应最⼩成本的实现(采⽤读取idea已有的增量⾏数据)
- 主动扫描,扫描范围为整个git项⽬中增量⽂件和变更⽂件的增量代码⾏,需要我们⾃⼰实现(这⾥加⼊扫描缓存来降低扫描增量代码⾏成本)
获取当前⽂件代码增量⾏:
⾸先,我们观测到idea本身就帮助我们计算了增量⾏数据,编辑器⻚⾯就有体现,直接试⽤idea的api :project.getLineStatusManager().getLineStatusTracker 获取使⽤即可,如下图所示:
获取项⽬的所有增量代码⾏:
插件中集成 git4idea,直接调⽤ git diff 的⽅式获取增量代码⾏
1 | class DiffLineContext { companion | ||||
2 | object { | ||||
3 | // 缓存增量代码⾏数据, key:⽂件路径,value:增量代码⾏号set | ||||
4 | var DIFF_FILE_VS_NEW_LINES: Map<String, Set<Int>> = mapOf() | ||||
5 | |||||
6 | fun analysePatch(project: Project) { | ||||
7 | ProgressManager.getInstance().runProcessWithProgressSynchronou | ||||
sly({ | |||||
8 | //调⽤git diff,分析后获得增量代码⾏号 | ||||
9 | DIFF_FILE_VS_NEW_LINES = project.getGitRepository() | ||||
10 | ?.root | ||||
11 | ?.let { GitLineHandler(project, it, GitCommand.DIF | ||||
F) | } | ||||
12 | ?.also { it.addParameters("HEAD") } | ||||
13 | ?.exec() | ||||
14 | ?.outputAsJoinedString | ||||
15 | ?.let { PatchParseHelper.parsePatch(it) } | ||||
16 | ?.entries | ||||
17 | ?.groupingBy { it.key } | ||||
18 | ?.fold(setOf()) { _, ii -> ii.value.toSet() } | ||||
19 | ?: mapOf() | ||||
20 | |||||
21 | }, "Aladdin编码规范:增量代码分析", false, project) | ||||
22 | } | ||||
23 | } | ||||
24 | } |
1. idea提供了PsiElementVisitor抽象类扩展点visitFile()可完成代码扫描功能
2. 我们实现⾃⼰的PsiElementVisitor,来完成代码问题扫描、注册、渲染的逻辑
Kotlin
-
- 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
11
- // 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()
29
- //扫描出来的问题⾃带起始⾏和结束⾏信息,判断是否在增量代码⾏范围内
- violations.filter {
- newLines.contains(it.beginLine)
- || newLines.contains(it.endLine)
34 }
- // 调⽤ holder.registerProblem 完成代码问题注册和渲染
- // ......
37 }
38 }
3.3⾼危组件实时扫描
3.3.1⾼危组件实时扫描原理图
功能说明:对新引⼊或者已修改组件进⾏依赖扫描(根据Aladdin已配置的违规组件),包括新引⼊组件的直接或者间接依赖组件。
⽬的:当前阶段,线上在跑带有⾼危组件的应⽤已全部完成治理,为了防⽌新的⾼危组件引⼊,增加了增量组件扫描功能。
- 原理图:
3.3.2⾼危组件实时扫描功能说明及效果图
- ⾼危组件实时扫描并提供修复建议,仅针对新添加或修改组件
- ⾼危组件配置与Aladdin持续集成系统保持⼀致,且可实时配置⽣效
3.3.3增量组件扫描原理
实现增量组件扫描有以下关键点:
- 获取增量组件(git show⽐对pom⽂件变化)
- 获取增量组件和其依赖组件的依赖关系(idea已经帮我们分析好了,我们直接读取idea依赖树上的依赖关系)
- 将增量组件信息传给Aladdin持续化集成平台进⾏组件规范扫描并缓存扫描结果
计算增量组件
Kotlin
-
- 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.subst ringAfter("$pjPath/")}") }
- ?.exec()
- ?.outputAsJoinedString
- ?.let { pomToDepSet(it) } // 解析获取最近⼀次pom⽂件的直接依赖depen dencies
- ?: setOf()
12
- // 获取当前pom⽂件的内容,并解析获取当前pom⽂件的直接dependencies,排除没有发⽣变动的依赖,即为增量依赖
- val diffDeps: Set<String> = mavenProject.file.contentsToByteArray()
- ?.let { String(it) }
- ?.let { pomToDepSet(it) }
- ?.let { it - oldDeps }
- ?.map { it.substringBefore("##") }
- ?.toSet()
- ?: setOf()
21
- //这⾥直接从idea的maven依赖树种读取增量依赖对应的依赖包数据,包含增量依赖及其间接依赖,丢给Aladdin持续集成平台进⾏合规扫描,并缓存扫描结果
- //......
24 }
获取增 量 组件及其 间 接 依 赖、 ⻛险 分 析、 缓存分析结 果IDEA 已经将maven项⽬的maven依赖树解析完毕,通过上述过程获取到增量依赖后,可直接从maven依赖树中获取增量依赖包下边所有间接依赖。
Kotlin
-
- // 增量组件分线问题缓存,这⾥缓存的⽬的是,当打开pom⽂件时候,需要对有问题的增量组件渲染问题描述,如果每次都取请求分析依赖包,成本较⼤,这⾥采⽤每次idea去刷新mavenProject依赖树的时候去更新缓存
- val DIFF_DEP_VIOLATION_CACHE = mutableMapOf<String, Map<String, List<Aladd inHttpClient.DepValResponseData>?>>()
3
- mavenProject.dependencyTree
- .filter { diffDeps.contains("${it.artifact.groupI d}:${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.violati ons }
- ?.ifEmpty { null }
- // 增量组件⻛险问题放⼊缓存
- ?.also { DIFF_DEP_VIOLATION_CACHE[mavenProject.path] = it }
- ?: DIFF_DEP_VIOLATION_CACHE.remove(mavenProject.path)
实现⻛险组件结果⻚⾯渲染 Inspection
Kotlin
-
- class MavenPomInspection : LocalInspectionTool(), DepInspectionIdentify {
- override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean
): PsiElementVisitor {
-
- // 根据 pom⽂件路径,获取缓存的⻛险组件问题
- val violationCache = DiffDependencyContext.DIFF_DEP_VIOLATION_CACH E[pf.virtualFile.path]
- //重写visitXmlTag⽅法,根据pom⽂件中的标签判断是否是问题组件,这⾥是根据xml
标签dependency中的grouId和artifactId来判定的
-
- return object : XmlElementVisitor() {
- override fun visitXmlTag(tag: XmlTag?) {
- super.visitXmlTag(tag)
9
10 val dep = tag as XmlTagImpl
11 if (XML_TAG_DEPENDENCY != dep.name) return
12 val groupId = dep.findFirstSubTag(XML_TAG_GROUP)?.value?.t ext ?: return
13 val artifactId = dep.findFirstSubTag(XML_TAG_ARTIFACT)?.va lue?.text ?: return
14 // 从缓存中获取⻛险问题
15 val violations = violationCache["${groupId}:${artifactId}"
]
16 // 向idea注册问题,并渲染到编辑⻚⾯
17 holder.registerProblem(dep,
18 violations.mapNotNull { it.failMessage }.reduce { acc, s -> acc + "\n\n" + s },
19 ProblemHighlightType.ERROR)
20 }
21 }
22 }
23 }
3.4代码提交⾃动扫描
3.4.1代码提交拦截扫描原理图
功能说明: 代码push时若增量⽂件存在Block等级问题将被拦截提醒,并提示编译/组件依赖/代码规约等问题数量,可查看详情
⽬的: 希望对增量代码进⾏编译问题/组件依赖问题/代码问题扫描拦截说明,防⽌⾼危问题遗漏
- 原理图:
3.4.2代码提交拦截核⼼功能说明及效果图
- 代码提交前,插件会进⾏⼀次增量代码扫描,依据Aladdin中站点设置的开关进⾏提醒拦截,以代替体验较差、⽂案不清晰的Aladdin拦截
- 如果存在违规代码,插件会提示问题类型和个数,其中包括:语法错误问题,P3C⾼危问题,⾼⻛险组件问题,点击查看详情效果,可在问题列表中查看具体问题,对应类,代码⾏,以及修复建议
4.插件⽤户安装综合使⽤情况
⾃插件上线以来,研发中⼼整体有效⽤户安装量覆盖达到100%
代码问题拦截:Gitlab代码准⼊拦截向插件拦截转换情况如下
按周统计,随着插件⽤户使⽤⼈数增多,插件拦截问题数逐步增⻓,Gitlab代码准⼊拦截问题数逐步降低,这表明代码问题拦截逐步转移到代码开发阶段。
5.总结&展望
Aladdin研发插件提供了⼀系列编码规范和最佳实践,能够⾃动检测代码中潜在问题并给出警告或错误提示。使⽤插件有以下好处 :
- 统⼀代码规范:基于Alibaba编码规范条款,和研发中⼼组件使⽤规约提供⼀套认可的编码规范。
- 前置发现潜在问题:插件可检测代码中潜在问题,有助于提⾼代码质量和稳定性,将质量问题拦截左移,降低拦截打回的成本。
- 规范编码习惯:插件提供很多实⽤的代码提示和建议,从⽽写出更健壮的代码。
- 通过扫描新引⼊依赖包:拦截⻛险组件,防⽌引⼊新的⻛险组件。
在未来的规划中,我们将继续对插件进⾏迭代升级
- 开发定制化代码规范,如定制化⽇志规范等,逐步优化完善出适合我们⾃⼰的⼀套规范
- 增加本地的增量单元测试覆盖率功能,实现增量代码覆盖率分析等
作者介绍
- swj,信也科技后端研发专家,主要负责基⽯
- zhx,信也科技后端研发专家,主要负责aladdin、rubik、tcm