diff options
| author | David Göransson <david.goransson@mullvad.net> | 2025-07-28 14:38:49 +0200 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2025-07-28 14:38:49 +0200 |
| commit | 4467d98ce03bc3057f68dc19ad17b71c44eb3501 (patch) | |
| tree | c25463ab47568fb73727646c2ce4d0414c6f7bcd /android/test | |
| parent | e2d91ff3d404d17a66c925bc5cd22dc29417550b (diff) | |
| parent | 235c1446b096d40e6ee686878391732a41c0fefb (diff) | |
| download | mullvadvpn-4467d98ce03bc3057f68dc19ad17b71c44eb3501.tar.xz mullvadvpn-4467d98ce03bc3057f68dc19ad17b71c44eb3501.zip | |
Merge branch 'detekt-named-args-droid-1528'
Diffstat (limited to 'android/test')
6 files changed, 177 insertions, 0 deletions
diff --git a/android/test/detekt/build.gradle.kts b/android/test/detekt/build.gradle.kts new file mode 100644 index 0000000000..92f34e4c6b --- /dev/null +++ b/android/test/detekt/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { kotlin("jvm") } + +dependencies { + compileOnly(libs.detekt.api) + testImplementation(libs.detekt.test) + testImplementation(libs.junit.jupiter.engine) + testRuntimeOnly(libs.junit.platform.launcher) +} + +tasks.withType<Test> { useJUnitPlatform() } diff --git a/android/test/detekt/src/main/kotlin/net/mullvad/mullvadvpn/detekt/extensions/CustomProvider.kt b/android/test/detekt/src/main/kotlin/net/mullvad/mullvadvpn/detekt/extensions/CustomProvider.kt new file mode 100644 index 0000000000..4b7c62a93f --- /dev/null +++ b/android/test/detekt/src/main/kotlin/net/mullvad/mullvadvpn/detekt/extensions/CustomProvider.kt @@ -0,0 +1,14 @@ +package net.mullvad.mullvadvpn.detekt.extensions + +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.RuleSet +import io.gitlab.arturbosch.detekt.api.RuleSetProvider +import net.mullvad.mullvadvpn.detekt.extensions.rules.ScreenAndDialogNamedArguments + +class CustomProvider : RuleSetProvider { + + override val ruleSetId: String = "custom" + + override fun instance(config: Config): RuleSet = + RuleSet(ruleSetId, listOf(ScreenAndDialogNamedArguments(config))) +} diff --git a/android/test/detekt/src/main/kotlin/net/mullvad/mullvadvpn/detekt/extensions/rules/ScreenAndDialogNamedArguments.kt b/android/test/detekt/src/main/kotlin/net/mullvad/mullvadvpn/detekt/extensions/rules/ScreenAndDialogNamedArguments.kt new file mode 100644 index 0000000000..cd458ddea9 --- /dev/null +++ b/android/test/detekt/src/main/kotlin/net/mullvad/mullvadvpn/detekt/extensions/rules/ScreenAndDialogNamedArguments.kt @@ -0,0 +1,50 @@ +package net.mullvad.mullvadvpn.detekt.extensions.rules + +import io.gitlab.arturbosch.detekt.api.CodeSmell +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.Debt +import io.gitlab.arturbosch.detekt.api.Entity +import io.gitlab.arturbosch.detekt.api.Issue +import io.gitlab.arturbosch.detekt.api.Rule +import io.gitlab.arturbosch.detekt.api.Severity +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtLambdaArgument + +class ScreenAndDialogNamedArguments(config: Config) : Rule(config) { + + override val issue = + Issue( + javaClass.simpleName, + Severity.CodeSmell, + "This rule reports Screen and Dialog composable calls that do not exclusively use named arguments", + Debt(mins = 1), + ) + + override fun visitCallExpression(expression: KtCallExpression) { + super.visitCallExpression(expression) + val name = expression.calleeExpression?.text ?: return + + if (!isProbablyScreenOrDialog(name)) return + + val args = + expression.valueArguments.let { + val skipLast = it.lastOrNull() is KtLambdaArgument + if (skipLast) it.dropLast(1) else it + } + + val hasUnnamed = args.any { !it.isNamed() } + if (hasUnnamed) { + report( + CodeSmell( + issue = issue, + entity = Entity.from(element = expression.originalElement, offset = 0), + message = "Call to composable `$name` must use only named arguments.", + ) + ) + } + } + + // We can't access the function declaration to see if this is a @Composable here. + private fun isProbablyScreenOrDialog(name: String): Boolean = + name[0].isUpperCase() && (name.endsWith("Screen") || name.endsWith("Dialog")) +} diff --git a/android/test/detekt/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider b/android/test/detekt/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider new file mode 100644 index 0000000000..bfb5c29315 --- /dev/null +++ b/android/test/detekt/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider @@ -0,0 +1 @@ +net.mullvad.mullvadvpn.detekt.extensions.CustomProvider diff --git a/android/test/detekt/src/main/resources/config/config.yml b/android/test/detekt/src/main/resources/config/config.yml new file mode 100644 index 0000000000..e81f814ba1 --- /dev/null +++ b/android/test/detekt/src/main/resources/config/config.yml @@ -0,0 +1,4 @@ +custom: + ScreenAndDialogNamedArguments: + active: true + includes: ["**/net/mullvad/mullvadvpn/compose/**"] diff --git a/android/test/detekt/src/test/kotlin/net/mullvad/mullvadvpn/detekt/extensions/ScreenAndDialogNamedArgumentsTest.kt b/android/test/detekt/src/test/kotlin/net/mullvad/mullvadvpn/detekt/extensions/ScreenAndDialogNamedArgumentsTest.kt new file mode 100644 index 0000000000..fd7250ed6b --- /dev/null +++ b/android/test/detekt/src/test/kotlin/net/mullvad/mullvadvpn/detekt/extensions/ScreenAndDialogNamedArgumentsTest.kt @@ -0,0 +1,98 @@ +package net.mullvad.mullvadvpn.detekt.extensions + +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.test.lint +import net.mullvad.mullvadvpn.detekt.extensions.rules.ScreenAndDialogNamedArguments +import org.junit.jupiter.api.Test + +class ScreenAndDialogNamedArgumentsTest { + + private val subject = ScreenAndDialogNamedArguments(Config.empty) + + @Test + fun `it should find one call that doesn't use only named arguments`() { + val findings = subject.lint(incorrectCall) + assert(findings.size == 1) + } + + @Test + fun `it should not report an error if all arguments are named`() { + val findings = subject.lint(correctCall) + assert(findings.isEmpty()) + } + + @Test + fun `it should ignore functions that do not end in Screen or Dialog`() { + val findings = subject.lint(ignoredCall) + assert(findings.isEmpty()) + } + + @Test + fun `it should ignore trailing lambda parameters`() { + val findings = subject.lint(trailingLambda) + assert(findings.isEmpty()) + } +} + +private val incorrectCall: String = + """ + @Composable + fun ExampleComposeScreen( + arg1: Int, + arg2: String = "", + ) {} + + @Composable + fun Caller() { + ExampleComposeScreen(2, args2 = "named") + } +""" + .trimIndent() + +private val correctCall: String = + """ + @Composable + fun ExampleComposeScreen( + arg1: Int, + arg2: String = "", + ) {} + + @Composable + fun Caller() { + ExampleComposeScreen(arg1 = 2, args2 = "named") + } +""" + .trimIndent() + +private val ignoredCall: String = + """ + @Composable + fun ExampleComposable( + arg1: Int, + arg2: String = "", + ) {} + + fun initScreen(arg: Int) {} + + @Composable + fun Caller() { + ExampleComposable(2, args2 = "named") + initScreen(2) + } +""" + .trimIndent() + +private val trailingLambda: String = + """ + @Composable + fun TrailingLambdaDialog(arg: Int, callback: (Int) -> Unit) { + callback(arg) + } + @Composable + fun Caller() { + TrailingLambdaDialog(arg = 2) { + println(it) + } + } +""" + .trimIndent() |
