什么是圈复杂度
圈复杂度,看一下百度百科的解释
圈复杂度(Cyclomatic complexity)是一种代码复杂度的衡量标准,在1976年由Thomas J. McCabe, Sr. 提出。在软件测试的概念里,圈复杂度用来衡量一个模块判定结构的复杂程度,数量上表现为线性无关的路径条数,即合理的预防错误所需测试的最少路径条数。圈复杂度大说明程序代码可能质量低且难于测试和维护,根据经验,程序的可能错误和高的圈复杂度有着很大关系。举例:如果一段源码中不包含控制流语句(条件或决策点),那么这段代码的圈复杂度为1,因为这段代码中只会有一条路径;如果一段代码中仅包含一个if语句,且if语句仅有一个条件,那么这段代码的圈复杂度为2;包含两个嵌套的if语句,或是一个if语句有两个条件的代码块的圈复杂度为3。
简单理解,圈复杂度代表的就是判定节点的数量,判定语句越多,圈复杂度越高
那么如何计算代码的圈复杂度,这里我们从定义出发,可以发现,圈复杂度代表的就是判定节点的数量,所以圈复杂度的计算就是
圈复杂度=判定节点+1
在Java中,常见的判定节点有:
- if 语句
- while 语句
- for 语句
- case 语句
- catch 语句
- and 和 or 布尔操作
- ? : 三元运算符
插件设计
例子
圈复杂度的检测,理论上是越早越好,所以需要在编码期就给出相应的检测提示,为此编写一个Android Studio插件用来进行检测提示。
如图所示,当方法的圈复杂度超过阈值的时候,会在方法名下有一条红线并给出错误提示,我们编写的插件就是这样一个代码检测(Code Inspection)插件。目前支持Kotlin和Java两种语言。阈值的设置在 Preferences|Editor|Inspections,如图所示
默认是10
设计
整个检测的原理就是利用前文提到的公式 圈复杂度=判定节点+1。
所以问题的关键就在于找到方法中判定节点的数量。整个插件的设计如下图所示
这里我们利用Intellij idea提供的插件sdk,通过psi来遍历当前文件来找到判定节点,NodeChecker是一个抽象类,其中nodeSet代表的是判定节点所对应的PsiElement集合,抽象方法abstract fun check(element: T): Int用来返回判定节点的数量,fun isNode(element: PsiElement): Boolean用来判断当前psi节点是否为判定节点。
//用来进行节点判定
abstract class NodeChecker {
protected open val nodeSet:MutableSet<Class<out PsiElement>> = mutableSetOf()
//返回判定节点数量
abstract fun check(element: PsiElement): Int
open fun isNode(element: PsiElement): Boolean {
nodeSet.forEach {
if (it.isInstance(element)) {
return true
}
}
return false
}
}
复制代码
JavaNodeChecker继承NodeChecker,用来进行对java代码的圈复杂度判断
class JavaNodeChecker : NodeChecker() {
override val nodeSet: MutableSet<Class<out PsiElement>>
get() = mutableSetOf(
PsiIfStatement::class.java,
PsiWhileStatement::class.java,
PsiDoWhileStatement::class.java,
PsiForStatement::class.java,
PsiForeachStatement::class.java,
PsiSwitchLabelStatement::class.java,
PsiCatchSection::class.java,
PsiConditionalExpression::class.java,
)
override fun check(statement: PsiElement): Int {
var nodeNum = 0
if (isNode(statement)) {
nodeNum++
}
statement.children.forEach {
if (it is PsiJavaToken && (JavaTokenType.ANDAND == it.tokenType || JavaTokenType.OROR == it.tokenType)) {
nodeNum++
}
nodeNum += check(it)
}
return nodeNum
}
}
复制代码
原理很简单,复写了nodeSet,其中包含了Java中判定节点的Psi类型,同时递归的判断每个psiElement,对于&&和||符号做了额外判断。
KotlinNodeChecker和JavaNodeChecker差不多,只不过因为Psi类型不同,nodeSet不同
class KTNodeChecker : NodeChecker() {
override val nodeSet: MutableSet<Class<out PsiElement>>
get() = mutableSetOf(
KtIfExpression::class.java,
KtWhileExpression::class.java,
KtDoWhileExpression::class.java,
KtForExpression::class.java,
KtSafeQualifiedExpression::class.java,
KtWhenConditionWithExpression::class.java,
KtCatchClause::class.java
)
override fun check(element: PsiElement): Int {
var nodeNum = 0
if (isNode(element)) {
nodeNum++
}
element.children.forEach {
if (it is KtBinaryExpression && (KtTokens.ANDAND == it.operationToken || KtTokens.OROR == it.operationToken)) {
nodeNum++
}
nodeNum+=check(it)
}
return nodeNum
}
}
复制代码
JavaCodeMetricsInspection和KTCodeMetricsInspection分别对应了plugin.xml中localInspection的实现类,并且分别使用来的JavaNodeChecker和KTNodeChecker进行对PsiMethod的检测
<localInspection
language="JAVA"
displayName="Java代码圈复杂度"
groupPath="Java"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.probable.bugs"
enabledByDefault="true"
level="ERROR"
implementationClass="com.skateboard.codemetrics.JavaCodeMetricsInspection"
/>
<localInspection
language="kotlin"
displayName="Kotlin代码圈复杂度"
groupPath="Kotlin"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.probable.bugs"
enabledByDefault="true"
level="ERROR"
implementationClass="com.skateboard.codemetrics.KTCodeMetricsInspection"/>
复制代码
关于如何编写Code Inspection插件,相关文档可以参考 plugins.jetbrains.com/docs/intell…
项目代码已经上传github github.com/skateboard1…
最后
后续会考虑在代码提交或者git pipleline中添加对应的增量圈复杂检测,这样就可以做到一个关于圈复杂度代码质量的闭环
关注我的公众号:"滑板上的老砒霜"