Skip to content

Commit 80ca7c8

Browse files
SONARKT-595 Rule S7416: Android production release targets should not be debuggable (#588)
1 parent 4523e2c commit 80ca7c8

File tree

13 files changed

+643
-61
lines changed

13 files changed

+643
-61
lines changed

sonar-kotlin-gradle/src/main/java/org/sonarsource/kotlin/gradle/KotlinGradleCheckList.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.sonarsource.kotlin.gradle
1818

1919
import org.sonarsource.kotlin.api.checks.KotlinCheck
20+
import org.sonarsource.kotlin.gradle.checks.AndroidReleaseBuildDebugCheck
2021
import org.sonarsource.kotlin.gradle.checks.AndroidReleaseBuildObfuscationCheck
2122
import org.sonarsource.kotlin.gradle.checks.CorePluginsShortcutUsageCheck
2223
import org.sonarsource.kotlin.gradle.checks.DependencyGroupedCheck
@@ -29,6 +30,7 @@ import org.sonarsource.kotlin.gradle.checks.TaskRegisterVsCreateCheck
2930

3031

3132
val KOTLIN_GRADLE_CHECKS: List<Class<out KotlinCheck>> = listOf(
33+
AndroidReleaseBuildDebugCheck::class.java,
3234
AndroidReleaseBuildObfuscationCheck::class.java,
3335
CorePluginsShortcutUsageCheck::class.java,
3436
RootProjectNamePresentCheck::class.java,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* SonarSource Kotlin
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.kotlin.gradle.checks
18+
19+
import org.jetbrains.kotlin.psi.KtScriptInitializer
20+
import org.sonar.check.Rule
21+
import org.sonarsource.kotlin.api.checks.AbstractCheck
22+
import org.sonarsource.kotlin.api.checks.predictRuntimeBooleanValue
23+
import org.sonarsource.kotlin.api.frontend.KotlinFileContext
24+
25+
private const val message = "Make sure this debug feature is deactivated before delivering the code in production."
26+
27+
@Rule(key = "S7416")
28+
class AndroidReleaseBuildDebugCheck : AbstractCheck() {
29+
30+
override fun visitScriptInitializer(initializer: KtScriptInitializer, data: KotlinFileContext) {
31+
!data.ktFile.isSettingGradleKts() || return
32+
val androidLambda = initializer.getChildCallWithLambdaOrNull("android")?.lambda ?: return
33+
34+
// Ensure the project is an Android app, and not a library
35+
androidLambda.getApplicationId() ?: return
36+
37+
val buildTypes = androidLambda.getChildCallWithLambdaOrNull("buildTypes") ?: return
38+
val (_, releaseLambda) = buildTypes.lambda.getChildCallWithLambdaOrNull("release")
39+
?: androidLambda.getGetByNameCallWithLambdaOrNull()
40+
?: return
41+
42+
val isDebuggableAssignment = releaseLambda.getPropertyAssignmentOrNull("isDebuggable")
43+
44+
if (isDebuggableAssignment?.right?.predictRuntimeBooleanValue() == true) {
45+
data.reportIssue(isDebuggableAssignment, message)
46+
}
47+
}
48+
}

sonar-kotlin-gradle/src/main/java/org/sonarsource/kotlin/gradle/checks/AndroidReleaseBuildObfuscationCheck.kt

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,14 @@
1616
*/
1717
package org.sonarsource.kotlin.gradle.checks
1818

19-
import com.intellij.psi.util.childrenOfType
20-
import com.intellij.psi.util.descendantsOfType
21-
import org.jetbrains.kotlin.lexer.KtTokens
22-
import org.jetbrains.kotlin.psi.KtBinaryExpression
23-
import org.jetbrains.kotlin.psi.KtCallExpression
24-
import org.jetbrains.kotlin.psi.KtElement
25-
import org.jetbrains.kotlin.psi.KtExpression
26-
import org.jetbrains.kotlin.psi.KtFile
27-
import org.jetbrains.kotlin.psi.KtFunctionLiteral
28-
import org.jetbrains.kotlin.psi.KtLambdaExpression
2919
import org.jetbrains.kotlin.psi.KtScriptInitializer
30-
import org.jetbrains.kotlin.psi.KtValueArgument
3120
import org.sonar.check.Rule
3221
import org.sonarsource.kotlin.api.checks.AbstractCheck
3322
import org.sonarsource.kotlin.api.checks.predictRuntimeBooleanValue
34-
import org.sonarsource.kotlin.api.checks.predictRuntimeStringValue
3523
import org.sonarsource.kotlin.api.frontend.KotlinFileContext
3624
import org.sonarsource.kotlin.api.reporting.KotlinTextRanges.textRange
3725
import org.sonarsource.kotlin.api.reporting.SecondaryLocation
3826

39-
private const val settingsGradleFileName = "settings.gradle.kts"
4027
private const val mainMessage = "Make sure that obfuscation is enabled in the release build configuration."
4128
private const val debuggableSetToTrueMessage = "Enabling debugging disables obfuscation for this release build. Make sure this is safe here."
4229

@@ -48,11 +35,7 @@ class AndroidReleaseBuildObfuscationCheck : AbstractCheck() {
4835
val (androidCallee, androidLambda) = initializer.getChildCallWithLambdaOrNull("android") ?: return
4936

5037
// Ensure the project is an Android app, and not a library
51-
androidLambda
52-
.getChildCallWithLambdaOrNull("defaultConfig")
53-
?.lambda
54-
?.getPropertyAssignmentOrNull("applicationId")
55-
?: return
38+
androidLambda.getApplicationId() ?: return
5639

5740
val buildTypes = androidLambda.getChildCallWithLambdaOrNull("buildTypes")
5841
if (buildTypes == null) {
@@ -82,40 +65,4 @@ class AndroidReleaseBuildObfuscationCheck : AbstractCheck() {
8265
data.reportIssue(releaseCallee, mainMessage)
8366
}
8467
}
85-
86-
private fun KtFile.isSettingGradleKts() = name.endsWith(settingsGradleFileName, ignoreCase = true)
87-
88-
private fun KtElement.getChildCallOrNull(childCallName: String): KtCallExpression? =
89-
descendantsOfType<KtCallExpression>().firstOrNull { it.calleeExpression?.text == childCallName }
90-
91-
private fun KtElement.getChildCallWithLambdaOrNull(childCallName: String): CalleeAndLambda? {
92-
val callee = getChildCallOrNull(childCallName) ?: return null
93-
val lambda = callee.functionLiteralArgumentOrNull() ?: return null
94-
return CalleeAndLambda(callee.calleeExpression!!, lambda)
95-
}
96-
97-
private fun KtElement.getGetByNameCallWithLambdaOrNull(): CalleeAndLambda? {
98-
val callee = descendantsOfType<KtCallExpression>().firstOrNull {
99-
it.calleeExpression?.text == "getByName" &&
100-
it.valueArguments.size == 2 &&
101-
it.valueArguments[0].isReleaseBuildType()
102-
} ?: return null
103-
val lambda = callee.functionLiteralArgumentOrNull() ?: return null
104-
return CalleeAndLambda(callee.calleeExpression!!, lambda)
105-
}
106-
107-
private fun KtValueArgument.isReleaseBuildType(): Boolean =
108-
textMatches("BuildType.RELEASE") ||
109-
getArgumentExpression()?.predictRuntimeStringValue() == "release"
110-
111-
private fun KtCallExpression.functionLiteralArgumentOrNull(): KtFunctionLiteral? =
112-
valueArguments
113-
.flatMap { it.childrenOfType<KtLambdaExpression>() }
114-
.flatMap { it.childrenOfType<KtFunctionLiteral>() }
115-
.singleOrNull()
116-
117-
private fun KtElement.getPropertyAssignmentOrNull(propertyName: String): KtBinaryExpression? =
118-
descendantsOfType<KtBinaryExpression>().firstOrNull { it.operationToken == KtTokens.EQ && it.left?.text == propertyName }
119-
120-
private data class CalleeAndLambda(val callee: KtExpression, val lambda: KtFunctionLiteral)
12168
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* SonarSource Kotlin
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.kotlin.gradle.checks
18+
19+
import com.intellij.psi.util.childrenOfType
20+
import com.intellij.psi.util.descendantsOfType
21+
import org.jetbrains.kotlin.lexer.KtTokens
22+
import org.jetbrains.kotlin.psi.KtBinaryExpression
23+
import org.jetbrains.kotlin.psi.KtCallExpression
24+
import org.jetbrains.kotlin.psi.KtElement
25+
import org.jetbrains.kotlin.psi.KtExpression
26+
import org.jetbrains.kotlin.psi.KtFile
27+
import org.jetbrains.kotlin.psi.KtFunctionLiteral
28+
import org.jetbrains.kotlin.psi.KtLambdaExpression
29+
import org.jetbrains.kotlin.psi.KtValueArgument
30+
import org.sonarsource.kotlin.api.checks.predictRuntimeStringValue
31+
32+
private const val settingsGradleFileName = "settings.gradle.kts"
33+
34+
internal fun KtFile.isSettingGradleKts() = name.endsWith(settingsGradleFileName, ignoreCase = true)
35+
36+
internal fun KtFunctionLiteral.getApplicationId() =
37+
getChildCallWithLambdaOrNull("defaultConfig")
38+
?.lambda
39+
?.getPropertyAssignmentOrNull("applicationId")
40+
41+
internal fun KtElement.getChildCallOrNull(childCallName: String): KtCallExpression? =
42+
descendantsOfType<KtCallExpression>().firstOrNull { it.calleeExpression?.text == childCallName }
43+
44+
internal fun KtElement.getChildCallWithLambdaOrNull(childCallName: String): CalleeAndLambda? {
45+
val callee = getChildCallOrNull(childCallName) ?: return null
46+
val lambda = callee.functionLiteralArgumentOrNull() ?: return null
47+
return CalleeAndLambda(callee.calleeExpression!!, lambda)
48+
}
49+
50+
internal fun KtElement.getGetByNameCallWithLambdaOrNull(): CalleeAndLambda? {
51+
val callee = descendantsOfType<KtCallExpression>().firstOrNull {
52+
it.calleeExpression?.text == "getByName" &&
53+
it.valueArguments.size == 2 &&
54+
it.valueArguments[0].isReleaseBuildType()
55+
} ?: return null
56+
val lambda = callee.functionLiteralArgumentOrNull() ?: return null
57+
return CalleeAndLambda(callee.calleeExpression!!, lambda)
58+
}
59+
60+
internal fun KtValueArgument.isReleaseBuildType(): Boolean =
61+
textMatches("BuildType.RELEASE") ||
62+
getArgumentExpression()?.predictRuntimeStringValue() == "release"
63+
64+
internal fun KtCallExpression.functionLiteralArgumentOrNull(): KtFunctionLiteral? =
65+
valueArguments
66+
.flatMap { it.childrenOfType<KtLambdaExpression>() }
67+
.flatMap { it.childrenOfType<KtFunctionLiteral>() }
68+
.singleOrNull()
69+
70+
internal fun KtElement.getPropertyAssignmentOrNull(propertyName: String): KtBinaryExpression? =
71+
descendantsOfType<KtBinaryExpression>().firstOrNull { it.operationToken == KtTokens.EQ && it.left?.text == propertyName }
72+
73+
internal data class CalleeAndLambda(val callee: KtExpression, val lambda: KtFunctionLiteral)
74+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* SonarSource Kotlin
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.kotlin.gradle.checks
18+
19+
import org.junit.jupiter.api.Test
20+
import org.sonarsource.kotlin.testapi.KotlinVerifier
21+
import java.nio.file.Path
22+
23+
internal class AndroidReleaseBuildDebugCheckTest {
24+
private val check = AndroidReleaseBuildDebugCheck()
25+
private val fileNamePrefix = "AndroidReleaseBuildDebugCheckSample"
26+
27+
@Test
28+
fun `on build gradle file`() {
29+
KotlinVerifier(check) {
30+
this.fileName = Path.of(fileNamePrefix, "build.gradle.kts").toFile().path
31+
this.baseDir = SAMPLES_BASE_DIR
32+
}.verify()
33+
}
34+
35+
@Test
36+
fun `on settings gradle file`() {
37+
KotlinVerifier(check) {
38+
this.fileName = Path.of(fileNamePrefix, "settings.gradle.kts").toFile().path
39+
this.baseDir = SAMPLES_BASE_DIR
40+
}.verifyNoIssue()
41+
}
42+
}

sonar-kotlin-gradle/src/test/java/org/sonarsource/kotlin/gradle/checks/AndroidReleaseBuildObfuscationCheckTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal class AndroidReleaseBuildObfuscationCheckTest {
2525
private val fileNamePrefix = "AndroidReleaseBuildObfuscationCheckSample"
2626

2727
@Test
28-
fun `on gradle file`() {
28+
fun `on build gradle file`() {
2929
KotlinVerifier(check) {
3030
this.fileName = Path.of(fileNamePrefix, "build.gradle.kts").toFile().path
3131
this.baseDir = SAMPLES_BASE_DIR

0 commit comments

Comments
 (0)