From 69bc6d8f0c6b2130c61d27cc9295cb6cdd306c62 Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 16:10:20 +0200 Subject: [PATCH 01/17] Scaffold created for Dart --- build.gradle.kts | 1 + .../core/ComplexityInfoProvider.kt | 2 +- .../dart/DartComplexityInfoProvider.kt | 31 ++++++++++++++++ .../dart/DartLanguageVisitor.kt | 35 +++++++++++++++++++ .../META-INF/codecomplexity-dart.xml | 6 ++++ src/main/resources/META-INF/plugin.xml | 1 + .../DartComplexityCalculationTest.kt | 22 ++++++++++++ 7 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt create mode 100644 src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt create mode 100644 src/main/resources/META-INF/codecomplexity-dart.xml create mode 100644 src/test/kotlin/com/github/nikolaikopernik/codecomplexity/DartComplexityCalculationTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 0cd4337..719257a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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() diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/core/ComplexityInfoProvider.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/core/ComplexityInfoProvider.kt index 1a73212..0ba3455 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/core/ComplexityInfoProvider.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/core/ComplexityInfoProvider.kt @@ -11,7 +11,7 @@ import com.intellij.psi.PsiElement val PLUGIN_EP_NAME: ExtensionPointName = ExtensionPointName("com.github.nikolaikopernik.codecomplexity.languageInfoProvider") val PLUGIN_HINT_KEY = SettingsKey("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. diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt new file mode 100644 index 0000000..636f4dd --- /dev/null +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt @@ -0,0 +1,31 @@ +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.jetbrains.lang.dart.DartLanguage + +class DartComplexityInfoProvider(override val language: Language = DartLanguage.INSTANCE) : ComplexityInfoProvider { + + override fun isComplexitySuitableMember(element: PsiElement): Boolean { + // TODO: return true for top-level functions and class members. Candidates: + // DartMethodDeclaration, DartFunctionDeclarationWithBody, + // DartFunctionDeclarationWithBodyOrNative, DartGetterDeclaration, + // DartSetterDeclaration, DartFactoryConstructorDeclaration. + return false + } + + override fun isClassWithBody(element: PsiElement): Boolean { + // TODO: return true if element is a DartClassDefinition with at least one member. + return false + } + + override fun getVisitor(sink: ComplexitySink): ElementVisitor = DartLanguageVisitor(sink) + + override fun getNameElementFor(element: PsiElement): PsiElement { + // TODO: resolve the name identifier for the Dart declaration node. + return element + } +} diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt new file mode 100644 index 0000000..34dd30e --- /dev/null +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt @@ -0,0 +1,35 @@ +package com.github.nikolaikopernik.codecomplexity.dart + +import com.github.nikolaikopernik.codecomplexity.core.ComplexitySink +import com.github.nikolaikopernik.codecomplexity.core.ElementVisitor +import com.intellij.psi.PsiElement + +internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVisitor() { + + override fun processElement(element: PsiElement) { + // TODO: map Dart PSI nodes to complexity points. Reference Campbell rules + // and the existing visitors (JavaLanguageVisitor, PythonLanguageVisitor). + // + // Core nodes to handle (from com.jetbrains.lang.dart.psi.*): + // DartIfStatement, DartWhileStatement, DartDoWhileStatement, + // DartForStatement, DartSwitchStatement, DartTryStatement, + // DartCatchPart, DartBreakStatement, DartContinueStatement, + // DartTernaryExpression, DartFunctionExpression, + // DartLogicAndExpression, DartLogicOrExpression, + // DartCallExpression (recursion). + // + // Dart-specific cases worth checking against the Campbell paper: + // - null-coalescing `??` and `??=` (DartIfNullExpression) — treat as logical op + // - cascade `..` (DartCascadeReferenceExpression) — probably not complexity + // - async/sync generators (`async*`, `sync*`) — yield points + // - switch expressions (Dart 3) — distinct from DartSwitchStatement + // - pattern matching in switch (Dart 3) + // - extension/mixin declarations — likely not complexity-bearing themselves + } + + override fun postProcess(element: PsiElement) { + // TODO: decrease nesting for any element that increased it in processElement. + } + + override fun shouldVisitElement(element: PsiElement): Boolean = true +} diff --git a/src/main/resources/META-INF/codecomplexity-dart.xml b/src/main/resources/META-INF/codecomplexity-dart.xml new file mode 100644 index 0000000..bb99ecf --- /dev/null +++ b/src/main/resources/META-INF/codecomplexity-dart.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index a7e71a4..e60a148 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -8,6 +8,7 @@ com.intellij.java org.jetbrains.kotlin com.intellij.modules.python + Dart > = + TODO("Extract Dart methods and their expected @complexity(N) annotation values") +} From 2af4968ae1ded49baaef032e4ac4a9ac60cfb173 Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 16:18:42 +0200 Subject: [PATCH 02/17] Upgrade Gradle to 3.14.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit org.jetbrains.intellij.platform 2.10.5 requires ≥ 8.13 --- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 470f82a..d10126b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1af9e09..d4081da 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 From 11e7859c1203b008c3dbd44bff8cab5ff2032d58 Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 16:25:35 +0200 Subject: [PATCH 03/17] upgrade IntelliJ Platform plugin version to latest <9.0.0 gradle >2.11.0 requires Gradle 9.0.0 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 719257a..f1d1ebc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 } From 9e2e1c65029f112f63bb2255fd2f4a858e63aee1 Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 20:39:44 +0200 Subject: [PATCH 04/17] Add Dart member and class-body detection to ComplexityInfoProvider --- .../dart/DartComplexityInfoProvider.kt | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt index 636f4dd..c1aa3ae 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt @@ -5,27 +5,51 @@ 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 { - // TODO: return true for top-level functions and class members. Candidates: - // DartMethodDeclaration, DartFunctionDeclarationWithBody, - // DartFunctionDeclarationWithBodyOrNative, DartGetterDeclaration, - // DartSetterDeclaration, DartFactoryConstructorDeclaration. - return false + 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 { - // TODO: return true if element is a DartClassDefinition with at least one member. - return false + if (element !is DartClassDefinition) return false + return PsiTreeUtil.findChildOfAnyType( + element, + DartMethodDeclaration::class.java, + DartGetterDeclaration::class.java, + DartSetterDeclaration::class.java, + DartFactoryConstructorDeclaration::class.java, + DartNamedConstructorDeclaration::class.java + ) != null } override fun getVisitor(sink: ComplexitySink): ElementVisitor = DartLanguageVisitor(sink) - override fun getNameElementFor(element: PsiElement): PsiElement { - // TODO: resolve the name identifier for the Dart declaration node. - return element - } + 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() } From 60c34909a2f52aee86cea82c20e0fff613d0c69a Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 20:44:53 +0200 Subject: [PATCH 05/17] Add Dart control-flow scoring (if / for / while / do-while) --- .../dart/DartLanguageVisitor.kt | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt index 34dd30e..ffe6d2f 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt @@ -2,34 +2,47 @@ 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.IF +import com.github.nikolaikopernik.codecomplexity.core.PointType.LOOP_FOR +import com.github.nikolaikopernik.codecomplexity.core.PointType.LOOP_WHILE import com.intellij.psi.PsiElement +import com.intellij.psi.PsiWhiteSpace +import com.jetbrains.lang.dart.psi.DartDoWhileStatement +import com.jetbrains.lang.dart.psi.DartForStatement +import com.jetbrains.lang.dart.psi.DartIfStatement +import com.jetbrains.lang.dart.psi.DartWhileStatement internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVisitor() { override fun processElement(element: PsiElement) { - // TODO: map Dart PSI nodes to complexity points. Reference Campbell rules - // and the existing visitors (JavaLanguageVisitor, PythonLanguageVisitor). - // - // Core nodes to handle (from com.jetbrains.lang.dart.psi.*): - // DartIfStatement, DartWhileStatement, DartDoWhileStatement, - // DartForStatement, DartSwitchStatement, DartTryStatement, - // DartCatchPart, DartBreakStatement, DartContinueStatement, - // DartTernaryExpression, DartFunctionExpression, - // DartLogicAndExpression, DartLogicOrExpression, - // DartCallExpression (recursion). - // - // Dart-specific cases worth checking against the Campbell paper: - // - null-coalescing `??` and `??=` (DartIfNullExpression) — treat as logical op - // - cascade `..` (DartCascadeReferenceExpression) — probably not complexity - // - async/sync generators (`async*`, `sync*`) — yield points - // - switch expressions (Dart 3) — distinct from DartSwitchStatement - // - pattern matching in switch (Dart 3) - // - extension/mixin declarations — likely not complexity-bearing themselves + 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) + } } override fun postProcess(element: PsiElement) { - // TODO: decrease nesting for any element that increased it in processElement. + when (element) { + is DartIfStatement -> if (!element.isElseIf()) sink.decreaseNesting() + is DartForStatement, + is DartWhileStatement, + is DartDoWhileStatement -> sink.decreaseNesting() + } } override fun shouldVisitElement(element: PsiElement): Boolean = true } + +private fun DartIfStatement.isElseIf(): Boolean = + prevNotWhitespace()?.text == "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 +} From 6520d06f2ae66772cfa19040515b13cd5636f0da Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 20:48:04 +0200 Subject: [PATCH 06/17] Score Dart `else` clauses via DartTokenTypes.ELSE --- .../codecomplexity/dart/DartLanguageVisitor.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt index ffe6d2f..7f6d141 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt @@ -2,11 +2,13 @@ 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.ELSE import com.github.nikolaikopernik.codecomplexity.core.PointType.IF import com.github.nikolaikopernik.codecomplexity.core.PointType.LOOP_FOR import com.github.nikolaikopernik.codecomplexity.core.PointType.LOOP_WHILE import com.intellij.psi.PsiElement import com.intellij.psi.PsiWhiteSpace +import com.jetbrains.lang.dart.DartTokenTypes import com.jetbrains.lang.dart.psi.DartDoWhileStatement import com.jetbrains.lang.dart.psi.DartForStatement import com.jetbrains.lang.dart.psi.DartIfStatement @@ -21,6 +23,9 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi is DartWhileStatement -> sink.increaseComplexityAndNesting(LOOP_WHILE) is DartDoWhileStatement -> sink.increaseComplexityAndNesting(LOOP_WHILE) } + if (element.isElseKeyword()) { + sink.increaseComplexity(ELSE) + } } override fun postProcess(element: PsiElement) { @@ -35,8 +40,11 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi override fun shouldVisitElement(element: PsiElement): Boolean = true } +private fun PsiElement.isElseKeyword(): Boolean = + node?.elementType == DartTokenTypes.ELSE && parent is DartIfStatement + private fun DartIfStatement.isElseIf(): Boolean = - prevNotWhitespace()?.text == "else" + prevNotWhitespace()?.node?.elementType == DartTokenTypes.ELSE private fun DartIfStatement.prevNotWhitespace(): PsiElement? { var prev: PsiElement = this From 9f7b57ef997f438c198e8c62b0ee310afe565e8d Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 20:51:33 +0200 Subject: [PATCH 07/17] Add Dart switch-statement and switch-expression scoring --- .../codecomplexity/dart/DartLanguageVisitor.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt index 7f6d141..94d1271 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt @@ -6,12 +6,15 @@ import com.github.nikolaikopernik.codecomplexity.core.PointType.ELSE import com.github.nikolaikopernik.codecomplexity.core.PointType.IF 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.SWITCH import com.intellij.psi.PsiElement import com.intellij.psi.PsiWhiteSpace import com.jetbrains.lang.dart.DartTokenTypes import com.jetbrains.lang.dart.psi.DartDoWhileStatement import com.jetbrains.lang.dart.psi.DartForStatement import com.jetbrains.lang.dart.psi.DartIfStatement +import com.jetbrains.lang.dart.psi.DartSwitchExpression +import com.jetbrains.lang.dart.psi.DartSwitchStatement import com.jetbrains.lang.dart.psi.DartWhileStatement internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVisitor() { @@ -22,6 +25,8 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi 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) } if (element.isElseKeyword()) { sink.increaseComplexity(ELSE) @@ -33,7 +38,9 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi is DartIfStatement -> if (!element.isElseIf()) sink.decreaseNesting() is DartForStatement, is DartWhileStatement, - is DartDoWhileStatement -> sink.decreaseNesting() + is DartDoWhileStatement, + is DartSwitchStatement, + is DartSwitchExpression -> sink.decreaseNesting() } } From 601848fd7bfe1a7080d110a7f5df564fc768be5e Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 20:54:11 +0200 Subject: [PATCH 08/17] Score Dart catch clauses via DartOnPart --- .../codecomplexity/dart/DartLanguageVisitor.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt index 94d1271..8d83e52 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt @@ -2,6 +2,7 @@ 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.CATCH import com.github.nikolaikopernik.codecomplexity.core.PointType.ELSE import com.github.nikolaikopernik.codecomplexity.core.PointType.IF import com.github.nikolaikopernik.codecomplexity.core.PointType.LOOP_FOR @@ -13,6 +14,7 @@ import com.jetbrains.lang.dart.DartTokenTypes import com.jetbrains.lang.dart.psi.DartDoWhileStatement import com.jetbrains.lang.dart.psi.DartForStatement import com.jetbrains.lang.dart.psi.DartIfStatement +import com.jetbrains.lang.dart.psi.DartOnPart import com.jetbrains.lang.dart.psi.DartSwitchExpression import com.jetbrains.lang.dart.psi.DartSwitchStatement import com.jetbrains.lang.dart.psi.DartWhileStatement @@ -27,6 +29,7 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi is DartDoWhileStatement -> sink.increaseComplexityAndNesting(LOOP_WHILE) is DartSwitchStatement -> sink.increaseComplexityAndNesting(SWITCH) is DartSwitchExpression -> sink.increaseComplexityAndNesting(SWITCH) + is DartOnPart -> sink.increaseComplexityAndNesting(CATCH) } if (element.isElseKeyword()) { sink.increaseComplexity(ELSE) @@ -40,7 +43,8 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi is DartWhileStatement, is DartDoWhileStatement, is DartSwitchStatement, - is DartSwitchExpression -> sink.decreaseNesting() + is DartSwitchExpression, + is DartOnPart -> sink.decreaseNesting() } } From a4c6289121fcae2f6b7960b3111434f1e694dc8d Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 20:56:19 +0200 Subject: [PATCH 09/17] Score labeled break/continue statements in Dart --- .../codecomplexity/dart/DartLanguageVisitor.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt index 8d83e52..b261b94 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt @@ -2,7 +2,9 @@ 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.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.LOOP_FOR @@ -11,6 +13,8 @@ import com.github.nikolaikopernik.codecomplexity.core.PointType.SWITCH import com.intellij.psi.PsiElement import com.intellij.psi.PsiWhiteSpace import com.jetbrains.lang.dart.DartTokenTypes +import com.jetbrains.lang.dart.psi.DartBreakStatement +import com.jetbrains.lang.dart.psi.DartContinueStatement import com.jetbrains.lang.dart.psi.DartDoWhileStatement import com.jetbrains.lang.dart.psi.DartForStatement import com.jetbrains.lang.dart.psi.DartIfStatement @@ -30,6 +34,8 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi 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) } if (element.isElseKeyword()) { sink.increaseComplexity(ELSE) From e2a2cdb8d9a163a7395a4249315fb75a69ca734b Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 21:08:58 +0200 Subject: [PATCH 10/17] Score Dart logical operators, null-coalescing, and ternary expressions --- .../dart/DartLanguageVisitor.kt | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt index b261b94..1cfcd3e 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt @@ -2,25 +2,37 @@ 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.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.DartContinueStatement import com.jetbrains.lang.dart.psi.DartDoWhileStatement +import com.jetbrains.lang.dart.psi.DartExpression import com.jetbrains.lang.dart.psi.DartForStatement +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.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() { @@ -36,6 +48,20 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi is DartOnPart -> sink.increaseComplexityAndNesting(CATCH) is DartBreakStatement -> if (element.referenceExpression != null) sink.increaseComplexity(BREAK) is DartContinueStatement -> if (element.referenceExpression != null) sink.increaseComplexity(CONTINUE) + 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() + } + } } if (element.isElseKeyword()) { sink.increaseComplexity(ELSE) @@ -50,11 +76,35 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi is DartDoWhileStatement, is DartSwitchStatement, is DartSwitchExpression, - is DartOnPart -> sink.decreaseNesting() + is DartOnPart, + is DartTernaryExpression -> sink.decreaseNesting() } } override fun shouldVisitElement(element: PsiElement): Boolean = true + + private fun PsiElement.calculateBinaryComplexity(operands: MutableList = mutableListOf()) { + this.children.forEach { child -> + 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) + } + } + } + } } private fun PsiElement.isElseKeyword(): Boolean = @@ -71,3 +121,20 @@ private fun DartIfStatement.prevNotWhitespace(): PsiElement? { } 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 +} From f7e1e1df3b1db263b655f28fc7cfbffb0e54911e Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 21:11:56 +0200 Subject: [PATCH 11/17] Add +nesting for Dart function expressions (closures) --- .../codecomplexity/dart/DartLanguageVisitor.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt index 1cfcd3e..40a9524 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt @@ -23,6 +23,7 @@ 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.DartForStatement +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 @@ -48,6 +49,7 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi 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() @@ -77,7 +79,8 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi is DartSwitchStatement, is DartSwitchExpression, is DartOnPart, - is DartTernaryExpression -> sink.decreaseNesting() + is DartTernaryExpression, + is DartFunctionExpression -> sink.decreaseNesting() } } From baf9a8feb43d776bae2ca19f2fce61d202e94c73 Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 21:19:42 +0200 Subject: [PATCH 12/17] Add recursion detection for Dart call expressions --- .../dart/DartLanguageVisitor.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt index 40a9524..27c4d3b 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt @@ -12,6 +12,7 @@ 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 @@ -19,15 +20,20 @@ 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 @@ -64,6 +70,7 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi element.calculateBinaryComplexity() } } + is DartCallExpression -> if (element.isRecursion()) sink.increaseComplexity(RECURSION) } if (element.isElseKeyword()) { sink.increaseComplexity(ELSE) @@ -141,3 +148,36 @@ private fun IElementType.toPointType(): PointType = when (this) { 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() ?: 0) + } + is DartFunctionDeclarationWithBody -> return p.componentName?.text?.let { + EnclosingFunction(it, p.formalParameterList?.countParams() ?: 0) + } + is DartFunctionDeclarationWithBodyOrNative -> return p.componentName?.text?.let { + EnclosingFunction(it, p.formalParameterList?.countParams() ?: 0) + } + } + 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) From 27b1d78b098d4f3ff155c83ca288156e5416a691 Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 21:30:48 +0200 Subject: [PATCH 13/17] parseTestFile for Dart complexity tests --- .../DartComplexityCalculationTest.kt | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/test/kotlin/com/github/nikolaikopernik/codecomplexity/DartComplexityCalculationTest.kt b/src/test/kotlin/com/github/nikolaikopernik/codecomplexity/DartComplexityCalculationTest.kt index ac054e6..f77daae 100644 --- a/src/test/kotlin/com/github/nikolaikopernik/codecomplexity/DartComplexityCalculationTest.kt +++ b/src/test/kotlin/com/github/nikolaikopernik/codecomplexity/DartComplexityCalculationTest.kt @@ -5,11 +5,21 @@ 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 private const val DART_TEST_FILES_PATH = "src/test/testData/dart" class DartComplexityCalculationTest : BaseComplexityTest() { - // TODO: enable once DartLanguageVisitor is implemented and testData/dart/ has fixtures. + // TODO: enable once testData/dart/ has fixtures (Step 13). // @Test fun testDartFiles() = checkAllFilesInFolder(DART_TEST_FILES_PATH, ".dart") override fun getTestDataPath() = DART_TEST_FILES_PATH @@ -17,6 +27,29 @@ class DartComplexityCalculationTest : BaseComplexityTest() { override fun createLanguageElementVisitor(sink: ComplexitySink): ElementVisitor = DartComplexityInfoProvider().getVisitor(sink) - override fun parseTestFile(file: PsiFile): List> = - TODO("Extract Dart methods and their expected @complexity(N) annotation values") + override fun parseTestFile(file: PsiFile): List> { + 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() + } } From 1f59ed8552ea94813792bda5ed48df584ed69f34 Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 21:43:07 +0200 Subject: [PATCH 14/17] Add Dart complexity test fixtures (ports + Dart-specific) --- src/test/testData/dart/AsyncGenerators.dart | 46 ++++ .../testData/dart/ExamplesFromArticle.dart | 199 ++++++++++++++++++ .../testData/dart/ExceptionStatements.dart | 37 ++++ src/test/testData/dart/LogicalOperations.dart | 51 +++++ src/test/testData/dart/LoopStatements.dart | 50 +++++ src/test/testData/dart/NullSafety.dart | 42 ++++ src/test/testData/dart/PatternMatching.dart | 62 ++++++ src/test/testData/dart/Recursion.dart | 11 + src/test/testData/dart/SwitchExpressions.dart | 40 ++++ 9 files changed, 538 insertions(+) create mode 100644 src/test/testData/dart/AsyncGenerators.dart create mode 100644 src/test/testData/dart/ExamplesFromArticle.dart create mode 100644 src/test/testData/dart/ExceptionStatements.dart create mode 100644 src/test/testData/dart/LogicalOperations.dart create mode 100644 src/test/testData/dart/LoopStatements.dart create mode 100644 src/test/testData/dart/NullSafety.dart create mode 100644 src/test/testData/dart/PatternMatching.dart create mode 100644 src/test/testData/dart/Recursion.dart create mode 100644 src/test/testData/dart/SwitchExpressions.dart diff --git a/src/test/testData/dart/AsyncGenerators.dart b/src/test/testData/dart/AsyncGenerators.dart new file mode 100644 index 0000000..bf94941 --- /dev/null +++ b/src/test/testData/dart/AsyncGenerators.dart @@ -0,0 +1,46 @@ +// Dart async / sync generators — verify modifiers do NOT add complexity. +// Plan decision: async, async*, sync*, await, yield, yield* score 0 themselves. + +@complexity(0) +Future simpleAsync() async { + await Future.delayed(Duration.zero); // await scores 0 + return 42; +} + +@complexity(1) +Stream asyncGenerator() async* { + for (int i = 0; i < 3; i++) { // +1 LOOP_FOR + yield i; // yield scores 0 + await Future.delayed(Duration.zero); // await scores 0 + } +} + +@complexity(2) +Stream conditionalAsync(bool flag) async* { + if (flag) { // +1 IF + yield 1; + } else { // +1 ELSE + yield 2; + } +} + +@complexity(1) +Iterable syncGenerator() sync* { + for (int i = 0; i < 3; i++) { // +1 LOOP_FOR + yield i; + } +} + +@complexity(0) +Iterable simpleSyncGen() sync* { + yield 1; // yield scores 0 + yield 2; + yield* [3, 4]; // yield* scores 0 +} + +@complexity(0) +Future awaitChain(Future a, Future b) async { + final x = await a; // scores 0 + final y = await b; // scores 0 + return x + y; +} diff --git a/src/test/testData/dart/ExamplesFromArticle.dart b/src/test/testData/dart/ExamplesFromArticle.dart new file mode 100644 index 0000000..5d65d0f --- /dev/null +++ b/src/test/testData/dart/ExamplesFromArticle.dart @@ -0,0 +1,199 @@ +class Tests { + @complexity(7) + void exampleOne(bool a, bool b, bool c, bool d, bool e, bool f) { + if (a // +1 for `if` + && b && c // +1 + || d || e // +1 + && f // +1 + ) {} + + if (a // +1 for `if` + && // +1 + !(b && c)) { // +1 + } + } + + dynamic frst; + dynamic ti; + + @complexity(35) + void exampleTwo(dynamic entry, dynamic txn) { + const ABORTED = 1; + while (true) { // +1 + try { + if (frst != null) { // +2 (nesting = 1) + if (frst.version > entry.version) { // +3 (nesting = 2) + throw StateError('Rollback'); + } + if (txn.isActive) { // +3 (nesting = 2) + var e = frst; + while (e != null) { // +4 (nesting = 3) + final version = e.version; + final depends = ti.wwDependency(version, txn.status, 0); + if (depends == 'TIMED_OUT') { // +5 (nesting = 4) + throw StateError('WWRetry'); + } + if (depends != 0 // +5 (nesting = 4) + && depends != ABORTED // +1 + ) { + throw StateError('Rollback'); + } + e = e.previous; + } + } + entry.previous = frst; + frst = entry; + break; + } + } on StateError catch (re) { // +2 (nesting = 1) + try { + final depends = ti.wwDependency(re.message, txn.status, 0); + if (depends != 0 // +3 (nesting = 2) + && depends != ABORTED // +1 + ) { + throw StateError('Rollback'); + } + } on FormatException catch (ie) { // +3 (nesting = 2) + throw StateError('Interrupted'); + } + } on FormatException catch (ie) { // +2 (nesting = 1) + throw StateError('Interrupted'); + } + } + } // total complexity == 35 + + @complexity(9) + void exampleThree(bool a, bool b, bool c) { + try { + if (a) { // +1 + for (int i = 1; i < 10; i++) { // +2 (nesting=1) + while (b) { // +3 (nesting=2) + } + } + } + } on Exception { // +1 + if (c) { // +2 (nesting=1) + } + } + } // Cognitive Complexity 9 + + @complexity(2) + void exampleFour(bool a) { + Function r = () { // +0 (nesting becomes 1) + if (a) { // +2 (nesting=1) + } + }; + } // Cognitive Complexity 2 + + @complexity(7) + int exampleFive(int max) { + int total = 0; + OUT: + for (int i = 1; i < max; i++) { // +1 + for (int j = 2; j < i; j++) { // +2 + if (i % j == 0) { // +3 + continue OUT; // +1 (labeled) + } + } + total += i; + } + return total; + } // Cognitive Complexity 7 + + @complexity(1) + String exampleSix(int number) { + switch (number) { // +1 + case 1: + return "one"; + case 2: + return "a couple"; + case 3: + return "a few"; + default: + return "lots"; + } + } // Cognitive Complexity 1 + + @complexity(19) + dynamic exampleSeven(dynamic classType, String name) { + final unknownMethodSymbol = Object(); + if (classType.isUnknown) { // +1 + return unknownMethodSymbol; + } + bool unknownFound = false; + final symbols = classType.symbol.members.lookup(name) as List; + for (final overrideSymbol in symbols) { // +1 + if (overrideSymbol.isMethod // +2 (nesting=1) + && !overrideSymbol.isStatic) { // +1 + final methodJavaSymbol = overrideSymbol; + if (canOverride(methodJavaSymbol)) { // +3 (nesting=2) + final overriding = + checkOverridingParameters(methodJavaSymbol, classType); + if (overriding == null) { // +4 (nesting=3) + if (!unknownFound) { // +5 (nesting=4) + unknownFound = true; + } + } else if (overriding) { // +1 ELSE + return methodJavaSymbol; + } + } + } + } + return unknownFound ? unknownMethodSymbol : null; // +1 ternary + } // total complexity == 19 + + bool canOverride(dynamic s) => true; + dynamic checkOverridingParameters(dynamic m, dynamic c) => null; + + @complexity(20) + String exampleEight(String antPattern, String directorySeparator) { + const SPECIAL_CHARS = ".+"; + final escapedDirectorySeparator = '\\' + directorySeparator; + final sb = StringBuffer(); + sb.write('^'); + int i = (antPattern.startsWith("/") + || antPattern.startsWith("\\")) // +1 OR + ? 1 + : 0; // +1 ternary + while (i < antPattern.length) { // +1 + final ch = antPattern[i]; + if (SPECIAL_CHARS.indexOf(ch) != -1) { // +2 (nesting = 1) + sb.write('\\'); + sb.write(ch); + } else if (ch == '*') { // +1 + if (i + 1 < antPattern.length // +3 (nesting = 2) + && antPattern[i + 1] == '*' // +1 + ) { + if (i + 2 < antPattern.length // +4 (nesting = 3) + && isSlash(antPattern[i + 2]) // +1 + ) { + sb.write("(?:.*"); + sb.write(escapedDirectorySeparator); + sb.write("|)"); + i += 2; + } else { // +1 + sb.write(".*"); + i += 1; + } + } else { // +1 + sb.write("[^"); + sb.write(escapedDirectorySeparator); + sb.write("]*?"); + } + } else if (ch == '?') { // +1 + sb.write("[^"); + sb.write(escapedDirectorySeparator); + sb.write("]"); + } else if (isSlash(ch)) { // +1 + sb.write(escapedDirectorySeparator); + } else { // +1 + sb.write(ch); + } + i++; + } + sb.write('\$'); + return sb.toString(); + } // total complexity = 20 + + bool isSlash(String c) => c == '/' || c == '\\'; +} diff --git a/src/test/testData/dart/ExceptionStatements.dart b/src/test/testData/dart/ExceptionStatements.dart new file mode 100644 index 0000000..0cb9b53 --- /dev/null +++ b/src/test/testData/dart/ExceptionStatements.dart @@ -0,0 +1,37 @@ +class Tests { + @complexity(1) + void tryDoesNotAddToComplexityNorNesting() { + try { + if (true) { // +1 + parseFile("salary.txt"); + } + } on Exception { + // empty - no points if no actual handler body branching + } + } + + @complexity(4) + void catchAddsToBoth(dynamic log) { + try { + parseFile("salary.txt"); + } on RangeError catch (e) { // +1 catch + if (e.toString() == "Not found") { // +2 (nesting=1) + log.warn(e.toString()); + } + } on Exception catch (e) { // +1 catch + log.error(e.toString()); + } + } + + @complexity(1) + void catchWithGenericException() { + try { + rethrowSomething("abc"); + } on Object catch (e) { // +1 catch + print(e); + } + } +} + +void parseFile(String path) {} +void rethrowSomething(String s) {} diff --git a/src/test/testData/dart/LogicalOperations.dart b/src/test/testData/dart/LogicalOperations.dart new file mode 100644 index 0000000..5cfb974 --- /dev/null +++ b/src/test/testData/dart/LogicalOperations.dart @@ -0,0 +1,51 @@ +@complexity(4) +String simpleStatements(int a, int b, int d) { + if (a != b) // +1 if + return "no conditions"; + if (a == b) { // +1 if + return "simple equal"; + } + if (a == b && b == d) { // +1 if, +1 AND group + return "still simple"; + } + return "exit"; +} + +@complexity(2) +void simpleAnd(bool a, bool b) { + if (a && b) return; // +1 if, +1 AND +} + +@complexity(2) +void simpleOr(bool a, bool b) { + if (a || b) return; // +1 if, +1 OR +} + +@complexity(2) +void singleLongGroup(bool a, bool b, bool c, bool d) { + if (a || b || c || d) return; // +1 if, +1 OR group +} + +@complexity(3) +void twoGroups(bool a, bool b, bool c, bool d) { + if (a || b || c && d) return; // +1 if, +1 OR, +1 AND +} + +@complexity(3) +void parenthesisCreateNewGroupAnyway(bool a, bool b, bool c, bool d) { + if (a || b || (c || d)) return; // +1 if, +1 OR, +1 OR separate +} + +@complexity(4) +void parenthesisInCenterSplitTheGroup( + bool a, bool b, bool c, bool d, bool e, bool f) { + if (a || b || !(c || d) || e || f) // +1 if, +1 OR, +1 OR separate, +1 OR new + return; +} + +@complexity(1) +bool doesSupportOperation(bool exists, dynamic op) { + return exists && support(op); // +1 AND +} + +bool support(dynamic op) => true; diff --git a/src/test/testData/dart/LoopStatements.dart b/src/test/testData/dart/LoopStatements.dart new file mode 100644 index 0000000..6ca96c8 --- /dev/null +++ b/src/test/testData/dart/LoopStatements.dart @@ -0,0 +1,50 @@ +class Tests { + int a = 0; + List counts = []; + + @complexity(4) + void allPossibleLoops() { + while (true) { // +1 + a++; + } + + do { // +1 + a++; + } while (true); + + for (int i = 0; i < 10; i++) { // +1 + a++; + } + + for (final i in counts) { // +1 + a++; + } + + final tokens = []; + tokens.forEach((it) => a++); // closure: +nesting only, no +complexity + } + + @complexity(5) + void loopsCreateNesting() { + while (true) { // +1 + if (true) { // +2 (nesting = 1) + a++; + } else { // +1 + } + } + + for (final i in counts) { // +1 + a++; + } + } + + @complexity(2) + void lambdaAddsNestingOnly() { + final tokens = []; + tokens.where((it) => it.isNotEmpty).forEach((it) { // nesting = 1 + if (true) { // +2 (nesting = 1) + a++; + } + }); + } +} diff --git a/src/test/testData/dart/NullSafety.dart b/src/test/testData/dart/NullSafety.dart new file mode 100644 index 0000000..0404463 --- /dev/null +++ b/src/test/testData/dart/NullSafety.dart @@ -0,0 +1,42 @@ +// Dart-specific: ??, ??=, !, ?. — verify scoring matches plan decisions. + +@complexity(1) +int? singleNullCoalesce(int? a, int? b) { + return a ?? b; // +1 (?? scores like an OR sequence) +} + +@complexity(1) +int? chainedNullCoalesce(int? a, int? b, int? c) { + return a ?? b ?? c; // +1 (consecutive ?? = single sequence) +} + +@complexity(2) +int? mixedOrAndCoalesce(int? a, int? b, bool? cond) { + if (cond ?? false) { // +1 IF, +1 ?? sequence + return a; + } + return b; +} + +@complexity(0) +void compoundNullAssign(Map m) { + m["x"] ??= 0; // ??= is assignment, not a binary expression +} + +@complexity(0) +void bangOperator(int? a) { + print(a!); // ! null-assertion, not a decision +} + +@complexity(0) +String? questionDotChain(String? s) { + return s?.toLowerCase(); // ?. null-aware access, not a decision +} + +@complexity(3) +int? guardWithCoalesce(int? a, int? b, int? c) { + if (a == null && b == null) { // +1 IF, +1 AND + return c; + } + return a ?? b ?? c; // +1 ?? sequence +} diff --git a/src/test/testData/dart/PatternMatching.dart b/src/test/testData/dart/PatternMatching.dart new file mode 100644 index 0000000..4feea95 --- /dev/null +++ b/src/test/testData/dart/PatternMatching.dart @@ -0,0 +1,62 @@ +// Dart 3 pattern matching: destructuring, `when` guards, `||` patterns. +// Plan decision: when guards and logical-or patterns inside cases score 0. +// The switch itself is the only decision point. + +sealed class Shape {} +class Circle extends Shape { + final double r; + Circle(this.r); +} +class Square extends Shape { + final double side; + Square(this.side); +} + +@complexity(1) +String destructurePair(Object o) { + switch (o) { // +1 SWITCH + case (int x, int y) when x > 0: // when guard scores 0 + return 'pair pos'; + case (int x, _): // record pattern, no extra + return 'pair any'; + default: + return 'unknown'; + } +} + +@complexity(1) +String orPatternCase(int x) { + switch (x) { // +1 SWITCH + case 1 || 2 || 3: // logical-OR pattern scores 0 + return 'small'; + case int n when n < 0: // type pattern + when, scores 0 + return 'neg'; + default: + return 'big'; + } +} + +@complexity(1) +String objectPattern(Shape s) { + switch (s) { // +1 SWITCH + case Circle(r: var r) when r > 0: // object pattern + when, scores 0 + return 'circle'; + case Square(side: _): // object pattern, no extra + return 'square'; + default: + return 'unknown'; + } +} + +@complexity(2) +String switchInsideIf(Object o, bool flag) { + if (flag) { // +1 IF + switch (o) { // +2 SWITCH (nesting=1) + case int(): + return 'int'; + default: + return 'other'; + } + } + return 'none'; +} diff --git a/src/test/testData/dart/Recursion.dart b/src/test/testData/dart/Recursion.dart new file mode 100644 index 0000000..a34ed52 --- /dev/null +++ b/src/test/testData/dart/Recursion.dart @@ -0,0 +1,11 @@ +class Tests { + @complexity(5) + int fibonacci(int n) { + if (n == 1) // +1 if + return 1; + else if (n == 0) // +1 ELSE + return 0; + else // +1 ELSE + return fibonacci(n - 1) + fibonacci(n - 2); // +1 RECURSION, +1 RECURSION + } +} diff --git a/src/test/testData/dart/SwitchExpressions.dart b/src/test/testData/dart/SwitchExpressions.dart new file mode 100644 index 0000000..df39a75 --- /dev/null +++ b/src/test/testData/dart/SwitchExpressions.dart @@ -0,0 +1,40 @@ +// Dart 3 switch expressions — distinct PSI from switch statements. +// Plan decision: score same as switch statement (+1 + nesting). + +@complexity(1) +String basicSwitchExpression(int n) { + return switch (n) { // +1 SWITCH + 1 => 'one', + 2 => 'two', + _ => 'other', + }; +} + +@complexity(3) +String switchExpressionWithCondition(int n, bool flag) { + if (flag) { // +1 IF (nesting becomes 1) + return switch (n) { // +2 SWITCH (nesting=1, point=2) + _ => 'fallback', + }; + } + return 'none'; +} + +@complexity(1) +String switchExpressionWithGuards(int n) { + return switch (n) { // +1 SWITCH (when guards score 0) + int x when x > 0 => 'pos', + int x when x < 0 => 'neg', + _ => 'zero', + }; +} + +@complexity(3) +String nestedSwitchExpressions(int n, int m) { + return switch (n) { // +1 SWITCH (nesting=0, point=1) + 0 => switch (m) { // +2 SWITCH (nesting=1, point=2) + _ => 'inner', + }, + _ => 'outer', + }; +} From 497c3ba69dc8d9bb97c7ebad39767c8984656888 Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 21:49:33 +0200 Subject: [PATCH 15/17] Enable Dart tests; fix children iteration to include leaf tokens --- .../codecomplexity/dart/DartComplexityInfoProvider.kt | 4 ++-- .../codecomplexity/dart/DartLanguageVisitor.kt | 7 ++++++- .../codecomplexity/DartComplexityCalculationTest.kt | 5 +++-- src/test/testData/dart/ExceptionStatements.dart | 4 ++-- src/test/testData/dart/PatternMatching.dart | 2 +- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt index c1aa3ae..0dd042a 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt @@ -32,14 +32,14 @@ class DartComplexityInfoProvider(override val language: Language = DartLanguage. override fun isClassWithBody(element: PsiElement): Boolean { if (element !is DartClassDefinition) return false - return PsiTreeUtil.findChildOfAnyType( + return PsiTreeUtil.findChildrenOfAnyType( element, DartMethodDeclaration::class.java, DartGetterDeclaration::class.java, DartSetterDeclaration::class.java, DartFactoryConstructorDeclaration::class.java, DartNamedConstructorDeclaration::class.java - ) != null + ).any { isComplexitySuitableMember(it) } } override fun getVisitor(sink: ComplexitySink): ElementVisitor = DartLanguageVisitor(sink) diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt index 27c4d3b..f2cdae6 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt @@ -94,7 +94,11 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi override fun shouldVisitElement(element: PsiElement): Boolean = true private fun PsiElement.calculateBinaryComplexity(operands: MutableList = mutableListOf()) { - this.children.forEach { child -> + // 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 -> { @@ -113,6 +117,7 @@ internal class DartLanguageVisitor(private val sink: ComplexitySink) : ElementVi operands.add(tt) } } + child = child.nextSibling } } } diff --git a/src/test/kotlin/com/github/nikolaikopernik/codecomplexity/DartComplexityCalculationTest.kt b/src/test/kotlin/com/github/nikolaikopernik/codecomplexity/DartComplexityCalculationTest.kt index f77daae..85186cf 100644 --- a/src/test/kotlin/com/github/nikolaikopernik/codecomplexity/DartComplexityCalculationTest.kt +++ b/src/test/kotlin/com/github/nikolaikopernik/codecomplexity/DartComplexityCalculationTest.kt @@ -15,12 +15,13 @@ 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() { - // TODO: enable once testData/dart/ has fixtures (Step 13). - // @Test fun testDartFiles() = checkAllFilesInFolder(DART_TEST_FILES_PATH, ".dart") + @Test + fun testDartFiles() = checkAllFilesInFolder(DART_TEST_FILES_PATH, ".dart") override fun getTestDataPath() = DART_TEST_FILES_PATH diff --git a/src/test/testData/dart/ExceptionStatements.dart b/src/test/testData/dart/ExceptionStatements.dart index 0cb9b53..a2c3b4c 100644 --- a/src/test/testData/dart/ExceptionStatements.dart +++ b/src/test/testData/dart/ExceptionStatements.dart @@ -5,8 +5,8 @@ class Tests { if (true) { // +1 parseFile("salary.txt"); } - } on Exception { - // empty - no points if no actual handler body branching + } finally { + // try/finally — no CATCH; finally is unconditional, not a decision point. } } diff --git a/src/test/testData/dart/PatternMatching.dart b/src/test/testData/dart/PatternMatching.dart index 4feea95..2087caf 100644 --- a/src/test/testData/dart/PatternMatching.dart +++ b/src/test/testData/dart/PatternMatching.dart @@ -48,7 +48,7 @@ String objectPattern(Shape s) { } } -@complexity(2) +@complexity(3) String switchInsideIf(Object o, bool flag) { if (flag) { // +1 IF switch (o) { // +2 SWITCH (nesting=1) From d549e8bb2c3ec063d2e01b90408c82cc260d0d01 Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Wed, 27 May 2026 22:09:44 +0200 Subject: [PATCH 16/17] Update lang list in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 69b1c12..55f8193 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ 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. --- From 57b9b4ec206539c886f8dd9c6e4451073d70533e Mon Sep 17 00:00:00 2001 From: LahaLuhem Date: Thu, 28 May 2026 12:28:27 +0200 Subject: [PATCH 17/17] Remove some redundants --- .../codecomplexity/dart/DartLanguageVisitor.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt index f2cdae6..73bd6f9 100644 --- a/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt @@ -167,13 +167,13 @@ private fun PsiElement.findEnclosingFunction(): EnclosingFunction? { while (p != null) { when (p) { is DartMethodDeclaration -> return p.componentName?.text?.let { - EnclosingFunction(it, p.formalParameterList?.countParams() ?: 0) + EnclosingFunction(it, p.formalParameterList.countParams()) } - is DartFunctionDeclarationWithBody -> return p.componentName?.text?.let { - EnclosingFunction(it, p.formalParameterList?.countParams() ?: 0) + is DartFunctionDeclarationWithBody -> return p.componentName.text?.let { + EnclosingFunction(it, p.formalParameterList.countParams()) } - is DartFunctionDeclarationWithBodyOrNative -> return p.componentName?.text?.let { - EnclosingFunction(it, p.formalParameterList?.countParams() ?: 0) + is DartFunctionDeclarationWithBodyOrNative -> return p.componentName.text?.let { + EnclosingFunction(it, p.formalParameterList.countParams()) } } p = p.parent