Android Studio圈复杂度检测插件

语言: CN / TW / HK

什么是圈复杂度

圈复杂度,看一下百度百科的解释

圈复杂度(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插件用来进行检测提示。

截屏2021-10-08 下午5.19.04.png

如图所示,当方法的圈复杂度超过阈值的时候,会在方法名下有一条红线并给出错误提示,我们编写的插件就是这样一个代码检测(Code Inspection)插件。目前支持Kotlin和Java两种语言。阈值的设置在 Preferences|Editor|Inspections,如图所示

截屏2021-10-09 上午8.38.17.png 默认是10

设计

整个检测的原理就是利用前文提到的公式 圈复杂度=判定节点+1。

所以问题的关键就在于找到方法中判定节点的数量。整个插件的设计如下图所示

codemetrics.jpg

这里我们利用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插件,相关文档可以参考 https://plugins.jetbrains.com/docs/intellij/code-inspections.html

项目代码已经上传github https://github.com/skateboard1991/codemetrics

最后

后续会考虑在代码提交或者git pipleline中添加对应的增量圈复杂检测,这样就可以做到一个关于圈复杂度代码质量的闭环

关注我的公众号:"滑板上的老砒霜"