Skip to content

Commit fa7c9b7

Browse files
SONARKT-594 Rule S7204: Obfuscation should be enabled for release builds
1 parent 8446cff commit fa7c9b7

File tree

8 files changed

+793
-2
lines changed

8 files changed

+793
-2
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@
1717
package org.sonarsource.kotlin.gradle
1818

1919
import org.sonarsource.kotlin.api.checks.KotlinCheck
20+
import org.sonarsource.kotlin.gradle.checks.AndroidReleaseBuildObfuscationCheck
2021
import org.sonarsource.kotlin.gradle.checks.CorePluginsShortcutUsageCheck
21-
import org.sonarsource.kotlin.gradle.checks.RootProjectNamePresentCheck
2222
import org.sonarsource.kotlin.gradle.checks.DependencyGroupedCheck
2323
import org.sonarsource.kotlin.gradle.checks.DependencyVersionHardcodedCheck
2424
import org.sonarsource.kotlin.gradle.checks.MissingSettingsCheck
2525
import org.sonarsource.kotlin.gradle.checks.MissingVerificationMetadataCheck
26+
import org.sonarsource.kotlin.gradle.checks.RootProjectNamePresentCheck
2627
import org.sonarsource.kotlin.gradle.checks.TaskDefinitionsCheck
2728
import org.sonarsource.kotlin.gradle.checks.TaskRegisterVsCreateCheck
2829

2930

3031
val KOTLIN_GRADLE_CHECKS: List<Class<out KotlinCheck>> = listOf(
32+
AndroidReleaseBuildObfuscationCheck::class.java,
3133
CorePluginsShortcutUsageCheck::class.java,
3234
RootProjectNamePresentCheck::class.java,
3335
DependencyGroupedCheck::class.java,
@@ -37,3 +39,4 @@ val KOTLIN_GRADLE_CHECKS: List<Class<out KotlinCheck>> = listOf(
3739
TaskDefinitionsCheck::class.java,
3840
TaskRegisterVsCreateCheck::class.java,
3941
)
42+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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.KtScriptInitializer
30+
import org.jetbrains.kotlin.psi.KtValueArgument
31+
import org.sonar.check.Rule
32+
import org.sonarsource.kotlin.api.checks.AbstractCheck
33+
import org.sonarsource.kotlin.api.checks.predictRuntimeBooleanValue
34+
import org.sonarsource.kotlin.api.checks.predictRuntimeStringValue
35+
import org.sonarsource.kotlin.api.frontend.KotlinFileContext
36+
import org.sonarsource.kotlin.api.reporting.KotlinTextRanges.textRange
37+
import org.sonarsource.kotlin.api.reporting.SecondaryLocation
38+
39+
private const val settingsGradleFileName = "settings.gradle.kts"
40+
private const val mainMessage = "Make sure that obfuscation is enabled in the release build configuration."
41+
private const val debuggableSetToTrueMessage = "Enabling debugging disables obfuscation for this release build. Make sure this is safe here."
42+
43+
@Rule(key = "S7204")
44+
class AndroidReleaseBuildObfuscationCheck : AbstractCheck() {
45+
46+
override fun visitScriptInitializer(initializer: KtScriptInitializer, data: KotlinFileContext) {
47+
!data.ktFile.isSettingGradleKts() || return
48+
val (androidCallee, androidLambda) = initializer.getChildCallWithLambdaOrNull("android") ?: return
49+
50+
// Ensure the project is an Android app, and not a library
51+
androidLambda
52+
.getChildCallWithLambdaOrNull("defaultConfig")
53+
?.lambda
54+
?.getPropertyAssignmentOrNull("applicationId")
55+
?: return
56+
57+
val buildTypes = androidLambda.getChildCallWithLambdaOrNull("buildTypes")
58+
if (buildTypes == null) {
59+
data.reportIssue(androidCallee, mainMessage)
60+
return
61+
}
62+
63+
val (releaseCallee, releaseLambda) = buildTypes.lambda.getChildCallWithLambdaOrNull("release")
64+
?: androidLambda.getGetByNameCallWithLambdaOrNull()
65+
?: return
66+
67+
val isDebuggableAssignment = releaseLambda.getPropertyAssignmentOrNull("isDebuggable")
68+
val isMinifiedEnabledAssignment = releaseLambda.getPropertyAssignmentOrNull("isMinifyEnabled")
69+
70+
when {
71+
isMinifiedEnabledAssignment == null ->
72+
data.reportIssue(releaseCallee, mainMessage)
73+
isMinifiedEnabledAssignment.right?.predictRuntimeBooleanValue() == false ->
74+
data.reportIssue(isMinifiedEnabledAssignment, mainMessage)
75+
isDebuggableAssignment?.right?.predictRuntimeBooleanValue() == true ->
76+
data.reportIssue(
77+
releaseCallee,
78+
debuggableSetToTrueMessage,
79+
secondaryLocations = listOf(isDebuggableAssignment).map { SecondaryLocation(data.textRange(it), "") }
80+
)
81+
releaseLambda.getChildCallOrNull("proguardFiles") == null ->
82+
data.reportIssue(releaseCallee, mainMessage)
83+
}
84+
}
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)
121+
}
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 AndroidReleaseBuildObfuscationCheckTest {
24+
private val check = AndroidReleaseBuildObfuscationCheck()
25+
private val fileNamePrefix = "AndroidReleaseBuildObfuscationCheckSample"
26+
27+
@Test
28+
fun `on 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+
}

0 commit comments

Comments
 (0)