阅读 955
Android Studio圈复杂度检测插件

Android Studio圈复杂度检测插件

什么是圈复杂度

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

圈复杂度(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插件,相关文档可以参考 plugins.jetbrains.com/docs/intell…

项目代码已经上传github github.com/skateboard1…

最后

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

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

文章分类
Android