Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<!-- Plugin description -->
This plugin calculates code complexity metric right in the editor and shows the complexity in the hint next to the method/class. It's based on the **Cognitive Complexity** metric proposed by G. Ann Campbell in [Cognitive Complexity - A new way of measuring understandability](https://www.sonarsource.com/docs/CognitiveComplexity.pdf).

Works with Java, Kotlin, and Python.
Works with Java, Kotlin, Python, and Dart.
<!-- Plugin description end -->

---
Expand Down
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import org.jetbrains.intellij.platform.gradle.TestFrameworkType

plugins {
id("java") // Java support
id("org.jetbrains.intellij.platform") version "2.10.5" // Gradle IntelliJ Plugin
id("org.jetbrains.intellij.platform") version "2.11.0" // Gradle IntelliJ Plugin
alias(libs.plugins.kotlin) version "2.2.0" // Kotlin support
alias(libs.plugins.changelog) // Gradle Changelog Plugin
}
Expand Down Expand Up @@ -40,6 +40,7 @@ dependencies {
bundledPlugin("com.intellij.java")
bundledPlugin("org.jetbrains.kotlin")
plugin("PythonCore", "253.28294.334")
plugin("Dart", "253.28294.51")

pluginVerifier()
zipSigner()
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pluginSinceBuild = 253
pluginUntilBuild = 261.*

# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion = 8.9
gradleVersion = 8.14.3

# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib
kotlin.stdlib.default.dependency = false
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import com.intellij.psi.PsiElement
val PLUGIN_EP_NAME: ExtensionPointName<ComplexityInfoProvider> = ExtensionPointName("com.github.nikolaikopernik.codecomplexity.languageInfoProvider")
val PLUGIN_HINT_KEY = SettingsKey<NoSettings>("code.complexity.hint")

val SUPPORTED_LANGUAGES = setOf("java", "kotlin", "python")
val SUPPORTED_LANGUAGES = setOf("java", "kotlin", "python", "dart")

/**
* Main interface to calculate complexity for different languages.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.github.nikolaikopernik.codecomplexity.dart

import com.github.nikolaikopernik.codecomplexity.core.ComplexityInfoProvider
import com.github.nikolaikopernik.codecomplexity.core.ComplexitySink
import com.github.nikolaikopernik.codecomplexity.core.ElementVisitor
import com.intellij.lang.Language
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.jetbrains.lang.dart.DartLanguage
import com.jetbrains.lang.dart.psi.DartClassDefinition
import com.jetbrains.lang.dart.psi.DartComponent
import com.jetbrains.lang.dart.psi.DartFactoryConstructorDeclaration
import com.jetbrains.lang.dart.psi.DartFunctionDeclarationWithBody
import com.jetbrains.lang.dart.psi.DartFunctionDeclarationWithBodyOrNative
import com.jetbrains.lang.dart.psi.DartGetterDeclaration
import com.jetbrains.lang.dart.psi.DartMethodDeclaration
import com.jetbrains.lang.dart.psi.DartNamedConstructorDeclaration
import com.jetbrains.lang.dart.psi.DartSetterDeclaration

class DartComplexityInfoProvider(override val language: Language = DartLanguage.INSTANCE) : ComplexityInfoProvider {

override fun isComplexitySuitableMember(element: PsiElement): Boolean = when (element) {
is DartFunctionDeclarationWithBody,
is DartFunctionDeclarationWithBodyOrNative,
is DartGetterDeclaration,
is DartSetterDeclaration,
is DartFactoryConstructorDeclaration -> true
is DartMethodDeclaration -> !element.isConstructor
is DartNamedConstructorDeclaration -> element.hasNonTrivialBody()
else -> false
}

override fun isClassWithBody(element: PsiElement): Boolean {
if (element !is DartClassDefinition) return false
return PsiTreeUtil.findChildrenOfAnyType(
element,
DartMethodDeclaration::class.java,
DartGetterDeclaration::class.java,
DartSetterDeclaration::class.java,
DartFactoryConstructorDeclaration::class.java,
DartNamedConstructorDeclaration::class.java
).any { isComplexitySuitableMember(it) }
}

override fun getVisitor(sink: ComplexitySink): ElementVisitor = DartLanguageVisitor(sink)

override fun getNameElementFor(element: PsiElement): PsiElement =
(element as? DartComponent)?.componentName ?: element
}

private fun DartNamedConstructorDeclaration.hasNonTrivialBody(): Boolean {
val block = functionBody?.block ?: return false
val statements = block.statements ?: return false
return statements.children.isNotEmpty()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package com.github.nikolaikopernik.codecomplexity.dart

import com.github.nikolaikopernik.codecomplexity.core.ComplexitySink
import com.github.nikolaikopernik.codecomplexity.core.ElementVisitor
import com.github.nikolaikopernik.codecomplexity.core.PointType
import com.github.nikolaikopernik.codecomplexity.core.PointType.BREAK
import com.github.nikolaikopernik.codecomplexity.core.PointType.CATCH
import com.github.nikolaikopernik.codecomplexity.core.PointType.CONTINUE
import com.github.nikolaikopernik.codecomplexity.core.PointType.ELSE
import com.github.nikolaikopernik.codecomplexity.core.PointType.IF
import com.github.nikolaikopernik.codecomplexity.core.PointType.LOGICAL_AND
import com.github.nikolaikopernik.codecomplexity.core.PointType.LOGICAL_OR
import com.github.nikolaikopernik.codecomplexity.core.PointType.LOOP_FOR
import com.github.nikolaikopernik.codecomplexity.core.PointType.LOOP_WHILE
import com.github.nikolaikopernik.codecomplexity.core.PointType.RECURSION
import com.github.nikolaikopernik.codecomplexity.core.PointType.SWITCH
import com.github.nikolaikopernik.codecomplexity.core.PointType.UNKNOWN
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.tree.IElementType
import com.jetbrains.lang.dart.DartTokenTypes
import com.jetbrains.lang.dart.psi.DartBreakStatement
import com.jetbrains.lang.dart.psi.DartCallExpression
import com.jetbrains.lang.dart.psi.DartContinueStatement
import com.jetbrains.lang.dart.psi.DartDoWhileStatement
import com.jetbrains.lang.dart.psi.DartExpression
import com.jetbrains.lang.dart.psi.DartFormalParameterList
import com.jetbrains.lang.dart.psi.DartForStatement
import com.jetbrains.lang.dart.psi.DartFunctionDeclarationWithBody
import com.jetbrains.lang.dart.psi.DartFunctionDeclarationWithBodyOrNative
import com.jetbrains.lang.dart.psi.DartFunctionExpression
import com.jetbrains.lang.dart.psi.DartIfNullExpression
import com.jetbrains.lang.dart.psi.DartIfStatement
import com.jetbrains.lang.dart.psi.DartLogicAndExpression
import com.jetbrains.lang.dart.psi.DartLogicOrExpression
import com.jetbrains.lang.dart.psi.DartMethodDeclaration
import com.jetbrains.lang.dart.psi.DartOnPart
import com.jetbrains.lang.dart.psi.DartParenthesizedExpression
import com.jetbrains.lang.dart.psi.DartPrefixExpression
import com.jetbrains.lang.dart.psi.DartSwitchExpression
import com.jetbrains.lang.dart.psi.DartSwitchStatement
import com.jetbrains.lang.dart.psi.DartTernaryExpression
import com.jetbrains.lang.dart.psi.DartWhileStatement

internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVisitor() {

override fun processElement(element: PsiElement) {
when (element) {
is DartIfStatement -> if (!element.isElseIf()) sink.increaseComplexityAndNesting(IF)
is DartForStatement -> sink.increaseComplexityAndNesting(LOOP_FOR)
is DartWhileStatement -> sink.increaseComplexityAndNesting(LOOP_WHILE)
is DartDoWhileStatement -> sink.increaseComplexityAndNesting(LOOP_WHILE)
is DartSwitchStatement -> sink.increaseComplexityAndNesting(SWITCH)
is DartSwitchExpression -> sink.increaseComplexityAndNesting(SWITCH)
is DartOnPart -> sink.increaseComplexityAndNesting(CATCH)
is DartBreakStatement -> if (element.referenceExpression != null) sink.increaseComplexity(BREAK)
is DartContinueStatement -> if (element.referenceExpression != null) sink.increaseComplexity(CONTINUE)
is DartFunctionExpression -> sink.increaseNesting()
is DartTernaryExpression -> {
sink.increaseComplexityAndNesting(IF)
element.calculateBinaryComplexity()
}
is DartLogicAndExpression,
is DartLogicOrExpression,
is DartIfNullExpression -> {
// Accept only top-level binary expressions; nested ones are walked
// by the outer's calculateBinaryComplexity with a shared operand list
// so consecutive same-kind operators score once.
if (element.parent !is DartExpression) {
element.calculateBinaryComplexity()
}
}
is DartCallExpression -> if (element.isRecursion()) sink.increaseComplexity(RECURSION)
}
if (element.isElseKeyword()) {
sink.increaseComplexity(ELSE)
}
}

override fun postProcess(element: PsiElement) {
when (element) {
is DartIfStatement -> if (!element.isElseIf()) sink.decreaseNesting()
is DartForStatement,
is DartWhileStatement,
is DartDoWhileStatement,
is DartSwitchStatement,
is DartSwitchExpression,
is DartOnPart,
is DartTernaryExpression,
is DartFunctionExpression -> sink.decreaseNesting()
}
}

override fun shouldVisitElement(element: PsiElement): Boolean = true

private fun PsiElement.calculateBinaryComplexity(operands: MutableList<IElementType> = mutableListOf()) {
// Iterate via firstChild/nextSibling — Dart's BNF-generated PSI uses
// ASTDelegatePsiElement.getChildren() which only returns composite children,
// omitting leaf tokens like the && / || / ?? operators we need to inspect.
var child: PsiElement? = firstChild
while (child != null) {
when {
child.isBinaryLogicExpression() -> child.calculateBinaryComplexity(operands)
child is DartParenthesizedExpression -> {
child.calculateBinaryComplexity()
operands.clear()
}
child is DartPrefixExpression -> {
child.calculateBinaryComplexity()
operands.clear()
}
child.isLogicOperatorToken() -> {
val tt = child.node.elementType
if (operands.lastOrNull() != tt) {
sink.increaseComplexity(tt.toPointType())
}
operands.add(tt)
}
}
child = child.nextSibling
}
}
}

private fun PsiElement.isElseKeyword(): Boolean =
node?.elementType == DartTokenTypes.ELSE && parent is DartIfStatement

private fun DartIfStatement.isElseIf(): Boolean =
prevNotWhitespace()?.node?.elementType == DartTokenTypes.ELSE

private fun DartIfStatement.prevNotWhitespace(): PsiElement? {
var prev: PsiElement = this
while (prev.prevSibling != null) {
prev = prev.prevSibling
if (prev !is PsiWhiteSpace) return prev
}
return null
}

private fun PsiElement.isBinaryLogicExpression(): Boolean =
this is DartLogicAndExpression ||
this is DartLogicOrExpression ||
this is DartIfNullExpression

private fun PsiElement.isLogicOperatorToken(): Boolean {
val tt = node?.elementType ?: return false
return tt == DartTokenTypes.AND_AND || tt == DartTokenTypes.OR_OR || tt == DartTokenTypes.QUEST_QUEST
}

private fun IElementType.toPointType(): PointType = when (this) {
DartTokenTypes.AND_AND -> LOGICAL_AND
DartTokenTypes.OR_OR -> LOGICAL_OR
DartTokenTypes.QUEST_QUEST -> LOGICAL_OR
else -> UNKNOWN
}

private data class EnclosingFunction(val name: String, val paramCount: Int)

private fun DartCallExpression.isRecursion(): Boolean {
val enclosing = findEnclosingFunction() ?: return false
if (this.expression?.text != enclosing.name) return false
return this.argCount() == enclosing.paramCount
}

private fun PsiElement.findEnclosingFunction(): EnclosingFunction? {
var p: PsiElement? = this.parent
while (p != null) {
when (p) {
is DartMethodDeclaration -> return p.componentName?.text?.let {
EnclosingFunction(it, p.formalParameterList.countParams())
}
is DartFunctionDeclarationWithBody -> return p.componentName.text?.let {
EnclosingFunction(it, p.formalParameterList.countParams())
}
is DartFunctionDeclarationWithBodyOrNative -> return p.componentName.text?.let {
EnclosingFunction(it, p.formalParameterList.countParams())
}
}
p = p.parent
}
return null
}

private fun DartCallExpression.argCount(): Int =
arguments?.argumentList?.let { it.expressionList.size + it.namedArgumentList.size } ?: 0

private fun DartFormalParameterList.countParams(): Int =
normalFormalParameterList.size + (optionalFormalParameters?.defaultFormalNamedParameterList?.size ?: 0)
6 changes: 6 additions & 0 deletions src/main/resources/META-INF/codecomplexity-dart.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<idea-plugin>
<extensions defaultExtensionNs="com.github.nikolaikopernik.codecomplexity">
<languageInfoProvider id="DartComplexityInfoProvider"
implementation="com.github.nikolaikopernik.codecomplexity.dart.DartComplexityInfoProvider"/>
</extensions>
</idea-plugin>
1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<depends optional="true" config-file="codecomplexity-java.xml">com.intellij.java</depends>
<depends optional="true" config-file="codecomplexity-kotlin.xml">org.jetbrains.kotlin</depends>
<depends optional="true" config-file="codecomplexity-python.xml">com.intellij.modules.python</depends>
<depends optional="true" config-file="codecomplexity-dart.xml">Dart</depends>

<extensionPoints>
<extensionPoint name="languageInfoProvider"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.github.nikolaikopernik.codecomplexity

import com.github.nikolaikopernik.codecomplexity.core.ComplexitySink
import com.github.nikolaikopernik.codecomplexity.core.ElementVisitor
import com.github.nikolaikopernik.codecomplexity.dart.DartComplexityInfoProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.jetbrains.lang.dart.psi.DartArguments
import com.jetbrains.lang.dart.psi.DartComponent
import com.jetbrains.lang.dart.psi.DartFactoryConstructorDeclaration
import com.jetbrains.lang.dart.psi.DartFunctionDeclarationWithBody
import com.jetbrains.lang.dart.psi.DartFunctionDeclarationWithBodyOrNative
import com.jetbrains.lang.dart.psi.DartGetterDeclaration
import com.jetbrains.lang.dart.psi.DartMethodDeclaration
import com.jetbrains.lang.dart.psi.DartNamedConstructorDeclaration
import com.jetbrains.lang.dart.psi.DartSetterDeclaration
import org.junit.Test

private const val DART_TEST_FILES_PATH = "src/test/testData/dart"

class DartComplexityCalculationTest : BaseComplexityTest() {
@Test
fun testDartFiles() = checkAllFilesInFolder(DART_TEST_FILES_PATH, ".dart")

override fun getTestDataPath() = DART_TEST_FILES_PATH

override fun createLanguageElementVisitor(sink: ComplexitySink): ElementVisitor =
DartComplexityInfoProvider().getVisitor(sink)

override fun parseTestFile(file: PsiFile): List<Triple<PsiElement, String, Int>> {
val declarations = PsiTreeUtil.findChildrenOfAnyType(
file,
DartMethodDeclaration::class.java,
DartFunctionDeclarationWithBody::class.java,
DartFunctionDeclarationWithBodyOrNative::class.java,
DartGetterDeclaration::class.java,
DartSetterDeclaration::class.java,
DartFactoryConstructorDeclaration::class.java,
DartNamedConstructorDeclaration::class.java
)
return declarations.mapNotNull { decl ->
val component = decl as DartComponent
val complexity = component.complexityAnnotationValue() ?: return@mapNotNull null
val name = component.componentName?.text ?: return@mapNotNull null
Triple(decl as PsiElement, name, complexity)
}
}

private fun DartComponent.complexityAnnotationValue(): Int? {
val metadata = metadataList.firstOrNull { it.referenceExpression?.text == "complexity" } ?: return null
val args = PsiTreeUtil.findChildOfType(metadata, DartArguments::class.java) ?: return null
val firstArg = args.argumentList?.expressionList?.firstOrNull() ?: return null
return firstArg.text.toIntOrNull()
}
}
Loading