diff --git a/buildSrc/src/main/kotlin/com/datadog/gradle/config/DetektCustomConfig.kt b/buildSrc/src/main/kotlin/com/datadog/gradle/config/DetektCustomConfig.kt index f55ae6c151..f64c6077c5 100644 --- a/buildSrc/src/main/kotlin/com/datadog/gradle/config/DetektCustomConfig.kt +++ b/buildSrc/src/main/kotlin/com/datadog/gradle/config/DetektCustomConfig.kt @@ -127,27 +127,12 @@ fun Project.detektCustomConfig() { args("-ex", "**/*.kts") args("--jvm-target", "11") - val moduleDependencies = configurations - .filter { it.name == "implementation" || it.name == "api" } - .flatMap { it.dependencies.filterIsInstance() } - .map { it.path } - .toSet() - .let { - // api configurations have canBeResolved=false, so we cannot go inside them to see transitive - // module dependencies, so including common modules - if (project.path == ":dd-sdk-android-internal") { - it - } else if (project.path == ":dd-sdk-android-core") { - it + ":dd-sdk-android-internal" - } else { - it + setOf(":dd-sdk-android-core", ":dd-sdk-android-internal") - } - } + val moduleDependencies = collectTransitiveProjectDependencies(project) val externalDependencies = File("${projectDir.absolutePath}/detekt_classpath").readText() - val moduleDependenciesClasses = moduleDependencies.map { + val moduleDependenciesClasses = moduleDependencies.joinToString(":") { "${rootDir.absolutePath}${it.replace(':', '/')}/build/extracted/classes.jar" - }.joinToString(":") + } val dependencies = if (moduleDependenciesClasses.isBlank()) { externalDependencies @@ -158,3 +143,20 @@ fun Project.detektCustomConfig() { args("-cp", dependencies) } } + +private fun collectTransitiveProjectDependencies(project: Project): Set { + val rootProject = project.rootProject + val visited = mutableSetOf() + val queue = ArrayDeque() + queue.add(project) + while (queue.isNotEmpty()) { + val current = queue.removeFirst() + val depPaths = current.configurations + .filter { it.name == "implementation" || it.name == "api" } + .flatMap { it.dependencies.filterIsInstance() } + .map { it.path } + .filter { visited.add(it) } + depPaths.mapNotNull { rootProject.findProject(it) }.forEach { queue.add(it) } + } + return visited +} diff --git a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/DatadogPropagationHelper.kt b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/DatadogPropagationHelper.kt index 139228351f..381a07ba8f 100644 --- a/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/DatadogPropagationHelper.kt +++ b/features/dd-sdk-android-trace/src/main/kotlin/com/datadog/android/trace/internal/DatadogPropagationHelper.kt @@ -390,16 +390,14 @@ class DatadogPropagationHelper internal constructor() { val spanId = span.context().spanId.toString() addHeader( W3CHttpCodec.TRACE_PARENT_KEY, - // TODO RUM-11445 InvalidStringFormat false alarm - @Suppress("UnsafeThirdPartyFunctionCall", "InvalidStringFormat") // Format string is static + @Suppress("UnsafeThirdPartyFunctionCall") // Format string is static W3C_TRACE_PARENT_DROP_SAMPLING_DECISION.format( traceId.padStart(length = W3C_TRACE_ID_LENGTH, padChar = '0'), spanId.padStart(length = W3C_PARENT_ID_LENGTH, padChar = '0') ) ) // TODO RUM-2121 3rd party vendor information will be erased - // TODO RUM-11445 InvalidStringFormat false alarm - @Suppress("UnsafeThirdPartyFunctionCall", "InvalidStringFormat") // Format string is static + @Suppress("UnsafeThirdPartyFunctionCall") // Format string is static var traceStateHeader = W3C_TRACE_STATE_DROP_SAMPLING_DECISION .format(spanId.padStart(length = W3C_PARENT_ID_LENGTH, padChar = '0')) if (traceOrigin != null) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index af686c151a..717051b300 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ kover = "0.7.6" kspTesting = "1.5.0" # Tools -detekt = "1.23.0" +detekt = "1.23.4" dokka = "2.1.0" unmock = "0.9.0" robolectric = "4.4_r1-robolectric-r2" @@ -209,7 +209,6 @@ androidLintTests = { module = "com.android.tools.lint:lint-tests", version.ref = systemStubsJupiter = { module = "uk.org.webcompere:system-stubs-jupiter", version.ref = "systemStubsJupiter" } # Tools -detektCli = { module = "io.gitlab.arturbosch.detekt:detekt-cli", version.ref = "detekt" } detektApi = { module = "io.gitlab.arturbosch.detekt:detekt-api", version.ref = "detekt" } detektTest = { module = "io.gitlab.arturbosch.detekt:detekt-test", version.ref = "detekt" } okHttpMock = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okHttp" } diff --git a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracingInterceptor.kt b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracingInterceptor.kt index 572ecc278f..11b684eb49 100644 --- a/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracingInterceptor.kt +++ b/integrations/dd-sdk-android-okhttp/src/main/kotlin/com/datadog/android/okhttp/trace/TracingInterceptor.kt @@ -528,16 +528,14 @@ internal constructor( val spanId = span.context().spanId.toString() requestBuilder.addHeader( W3C_TRACEPARENT_KEY, - // TODO RUM-11445 InvalidStringFormat false alarm - @Suppress("UnsafeThirdPartyFunctionCall", "InvalidStringFormat") // Format string is static + @Suppress("UnsafeThirdPartyFunctionCall") // Format string is static W3C_TRACEPARENT_DROP_SAMPLING_DECISION.format( traceId.padStart(length = W3C_TRACE_ID_LENGTH, padChar = '0'), spanId.padStart(length = W3C_PARENT_ID_LENGTH, padChar = '0') ) ) // TODO RUM-2121 3rd party vendor information will be erased - // TODO RUM-11445 InvalidStringFormat false alarm - @Suppress("UnsafeThirdPartyFunctionCall", "InvalidStringFormat") // Format string is static + @Suppress("UnsafeThirdPartyFunctionCall") // Format string is static var traceStateHeader = W3C_TRACESTATE_DROP_SAMPLING_DECISION .format(spanId.padStart(length = W3C_PARENT_ID_LENGTH, padChar = '0')) if (traceOrigin != null) { diff --git a/local_ci.sh b/local_ci.sh index 8289f6aed0..86ac1f3b62 100755 --- a/local_ci.sh +++ b/local_ci.sh @@ -164,7 +164,7 @@ if [[ $ANALYSIS == 1 ]]; then detekt --config detekt_custom_general.yml,detekt_custom_safe_calls.yml,detekt_custom_unsafe_calls.yml --plugins tools/detekt/build/libs/detekt.jar -cp "$classpath" --jvm-target 11 -ex "**/*.kts" echo "------ Detekt test pyramid rules" - rm apiSurface.log apiUsage.log + rm -f apiSurface.log apiUsage.log detekt --config detekt_test_pyramid.yml --plugins tools/detekt/build/libs/detekt.jar -cp "$classpath" --jvm-target 11 -ex "**/*.kts" grep -v -f apiUsage.log apiSurface.log > apiCoverageMiss.log diff --git a/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/ext/ReceiverExt.kt b/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/ext/ReceiverExt.kt index 1118fe2cfa..017d78c109 100644 --- a/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/ext/ReceiverExt.kt +++ b/tools/detekt/src/main/kotlin/com/datadog/tools/detekt/ext/ReceiverExt.kt @@ -13,6 +13,7 @@ import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull import org.jetbrains.kotlin.resolve.scopes.receivers.ClassQualifier import org.jetbrains.kotlin.resolve.scopes.receivers.ExpressionReceiver import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitReceiver +import org.jetbrains.kotlin.resolve.scopes.receivers.PackageQualifier import org.jetbrains.kotlin.resolve.scopes.receivers.Receiver import org.jetbrains.kotlin.types.FlexibleType import org.jetbrains.kotlin.types.lowerIfFlexible @@ -53,6 +54,10 @@ internal fun Receiver.type( type.fqNameOrNull()?.toString() } + is PackageQualifier -> { + descriptor.fqName.asString() + } + else -> { println("DD: Unknown receiver type ${this.javaClass}") null diff --git a/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/InvalidStringFormatTest.kt b/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/InvalidStringFormatTest.kt index c956c0ca45..405cb63039 100644 --- a/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/InvalidStringFormatTest.kt +++ b/tools/detekt/src/test/kotlin/com/datadog/tools/detekt/rules/sdk/InvalidStringFormatTest.kt @@ -485,7 +485,7 @@ internal class InvalidStringFormatTest { .compileAndLintWithContext(kotlinEnv.env, code) // Then - assertThat(findings).hasSize(0) + assertThat(findings).isEmpty() } @Test @@ -509,7 +509,7 @@ internal class InvalidStringFormatTest { .compileAndLintWithContext(kotlinEnv.env, code) // Then - assertThat(findings).hasSize(0) + assertThat(findings).isEmpty() } @Test @@ -531,7 +531,7 @@ internal class InvalidStringFormatTest { .compileAndLintWithContext(kotlinEnv.env, code) // Then - assertThat(findings).hasSize(0) + assertThat(findings).isEmpty() } @Test @@ -555,7 +555,7 @@ internal class InvalidStringFormatTest { .compileAndLintWithContext(kotlinEnv.env, code) // Then - assertThat(findings).hasSize(0) + assertThat(findings).isEmpty() } @Test @@ -580,7 +580,7 @@ internal class InvalidStringFormatTest { .compileAndLintWithContext(kotlinEnv.env, code) // Then - assertThat(findings).hasSize(0) + assertThat(findings).isEmpty() } @Test @@ -598,7 +598,7 @@ internal class InvalidStringFormatTest { .compileAndLintWithContext(kotlinEnv.env, code) // Then - assertThat(findings).hasSize(0) + assertThat(findings).isEmpty() } @Test @@ -616,7 +616,83 @@ internal class InvalidStringFormatTest { .compileAndLintWithContext(kotlinEnv.env, code) // Then - assertThat(findings).hasSize(0) + assertThat(findings).isEmpty() + } + + // endregion + + // region Test same-class companion object constants + + @Test + fun `Ignores valid String format {same class companion object constant, ext}`() { + // Given + val code = + """ + class Foo { + companion object { + private const val PATTERN = "00-%s-%s-00" + } + + fun test(a: String, b: String): String { + return PATTERN.format(a, b) + } + } + """.trimIndent() + + // When + val findings = InvalidStringFormat() + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).isEmpty() + } + + @Test + fun `Warns invalid argument count {same class companion object constant, ext}`() { + // Given + val code = + """ + class Foo { + companion object { + private const val PATTERN = "00-%s-%s-00" + } + + fun test(a: String): String { + return PATTERN.format(a) + } + } + """.trimIndent() + + // When + val findings = InvalidStringFormat() + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).hasSize(1) + } + + @Test + fun `Ignores valid String format {same class companion object constant, single arg}`() { + // Given + val code = + """ + class Foo { + companion object { + private const val PATTERN = "dd=p:%s;s:0" + } + + fun test(a: String): String { + return PATTERN.format(a) + } + } + """.trimIndent() + + // When + val findings = InvalidStringFormat() + .compileAndLintWithContext(kotlinEnv.env, code) + + // Then + assertThat(findings).isEmpty() } // endregion