summaryrefslogtreecommitdiffhomepage
path: root/android/test
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson@mullvad.net>2025-07-28 14:38:49 +0200
committerDavid Göransson <david.goransson@mullvad.net>2025-07-28 14:38:49 +0200
commit4467d98ce03bc3057f68dc19ad17b71c44eb3501 (patch)
treec25463ab47568fb73727646c2ce4d0414c6f7bcd /android/test
parente2d91ff3d404d17a66c925bc5cd22dc29417550b (diff)
parent235c1446b096d40e6ee686878391732a41c0fefb (diff)
downloadmullvadvpn-4467d98ce03bc3057f68dc19ad17b71c44eb3501.tar.xz
mullvadvpn-4467d98ce03bc3057f68dc19ad17b71c44eb3501.zip
Merge branch 'detekt-named-args-droid-1528'
Diffstat (limited to 'android/test')
-rw-r--r--android/test/detekt/build.gradle.kts10
-rw-r--r--android/test/detekt/src/main/kotlin/net/mullvad/mullvadvpn/detekt/extensions/CustomProvider.kt14
-rw-r--r--android/test/detekt/src/main/kotlin/net/mullvad/mullvadvpn/detekt/extensions/rules/ScreenAndDialogNamedArguments.kt50
-rw-r--r--android/test/detekt/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider1
-rw-r--r--android/test/detekt/src/main/resources/config/config.yml4
-rw-r--r--android/test/detekt/src/test/kotlin/net/mullvad/mullvadvpn/detekt/extensions/ScreenAndDialogNamedArgumentsTest.kt98
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()