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. --- diff --git a/build.gradle.kts b/build.gradle.kts index 0cd4337..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 } @@ -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/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 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..0dd042a --- /dev/null +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartComplexityInfoProvider.kt @@ -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() +} 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..73bd6f9 --- /dev/null +++ b/src/main/kotlin/com/github/nikolaikopernik/codecomplexity/dart/DartLanguageVisitor.kt @@ -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 = 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) 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 > { + 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() + } +} 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..a2c3b4c --- /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"); + } + } finally { + // try/finally — no CATCH; finally is unconditional, not a decision point. + } + } + + @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..2087caf --- /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(3) +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', + }; +}