Repository: detekt/detekt (kotlin)

Issue:
Replace TypeUtils in detekt-psi-utils with Analysis API equivalents
<!-- Your feature request may already be reported! -->
<!-- Please search in the [issues](https://github.com/detekt/detekt/issues) before creating a new one. -->

## Expected Behavior
<!--- If you're suggesting a change/improvement, tell us how it should work -->

## Current Behavior
<!--- If suggesting a change/improvement, explain the difference from the current behavior -->

## Context
`KtExpression.getDataFlowAwareTypes` and `KtExpression.isNullable` require `BindingContext`. These functions need to be replaced with something compatible with Kotlin's Analysis API.

Pull request description:
This was the key function replacement required to close #8078

Stacks on #8175

Patch:
diff --git a/detekt-psi-utils/api/detekt-psi-utils.api b/detekt-psi-utils/api/detekt-psi-utils.api
index 46d5f4e07ab8..ed517b28615a 100644
--- a/detekt-psi-utils/api/detekt-psi-utils.api
+++ b/detekt-psi-utils/api/detekt-psi-utils.api
@@ -101,5 +101,6 @@ public final class io/gitlab/arturbosch/detekt/rules/TypeUtilsKt {
 	public static final fun getDataFlowAwareTypes (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/resolve/BindingContext;Lorg/jetbrains/kotlin/config/LanguageVersionSettings;Lorg/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory;Lorg/jetbrains/kotlin/types/KotlinType;)Ljava/util/Set;
 	public static synthetic fun getDataFlowAwareTypes$default (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/resolve/BindingContext;Lorg/jetbrains/kotlin/config/LanguageVersionSettings;Lorg/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory;Lorg/jetbrains/kotlin/types/KotlinType;ILjava/lang/Object;)Ljava/util/Set;
 	public static final fun isNullable (Lorg/jetbrains/kotlin/psi/KtExpression;Lorg/jetbrains/kotlin/resolve/BindingContext;Lorg/jetbrains/kotlin/config/LanguageVersionSettings;Lorg/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory;Z)Z
+	public static final fun isNullable (Lorg/jetbrains/kotlin/psi/KtExpression;Z)Z
 }
 
diff --git a/detekt-psi-utils/build.gradle.kts b/detekt-psi-utils/build.gradle.kts
index d9f51644bb05..dbb166c5b898 100644
--- a/detekt-psi-utils/build.gradle.kts
+++ b/detekt-psi-utils/build.gradle.kts
@@ -6,6 +6,8 @@ plugins {
 dependencies {
     api(libs.kotlin.compiler)
 
+    implementation(projects.detektKotlinAnalysisApi)
+
     testImplementation(libs.assertj.core)
     testImplementation(projects.detektTest)
     testImplementation(projects.detektTestUtils)
diff --git a/detekt-psi-utils/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/TypeUtils.kt b/detekt-psi-utils/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/TypeUtils.kt
index 43fc15a834d7..260906bfe8d1 100644
--- a/detekt-psi-utils/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/TypeUtils.kt
+++ b/detekt-psi-utils/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/TypeUtils.kt
@@ -1,6 +1,10 @@
 package io.gitlab.arturbosch.detekt.rules
 
 import com.intellij.psi.PsiElement
+import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
+import org.jetbrains.kotlin.analysis.api.analyze
+import org.jetbrains.kotlin.analysis.api.components.KaDiagnosticCheckerFilter
+import org.jetbrains.kotlin.analysis.api.fir.diagnostics.KaFirDiagnostic
 import org.jetbrains.kotlin.config.LanguageVersionSettings
 import org.jetbrains.kotlin.descriptors.CallableDescriptor
 import org.jetbrains.kotlin.diagnostics.Errors
@@ -98,3 +102,33 @@ fun KtExpression.isNullable(
 
 private fun KtExpression.descriptor(bindingContext: BindingContext): CallableDescriptor? =
     getResolvedCall(bindingContext)?.resultingDescriptor
+
+/**
+ * Return if overall expression is nullable or not nullable
+ *
+ * ```kotlin
+ * var a: Int? = null
+ * val b = a // RHS expression will be nullable
+ * val c = a!! // RHS expression will be not nullable
+ * val d = (a ?: error("null assertion message")) // RHS expression will be not nullable
+ * val c = 1?.and(2) // RHS expression will be not nullable
+ * ```
+ * [shouldConsiderPlatformTypeAsNullable] determines the behaviour of platform type. Passing true considers
+ * the platform type as nullable otherwise it is not considered nullable in case of false
+ */
+@OptIn(KaExperimentalApi::class)
+fun KtExpression.isNullable(shouldConsiderPlatformTypeAsNullable: Boolean): Boolean {
+    if (this is KtSafeQualifiedExpression) {
+        analyze(this) {
+            return diagnostics(KaDiagnosticCheckerFilter.ONLY_COMMON_CHECKERS)
+                .none { it is KaFirDiagnostic.UnnecessarySafeCall }
+        }
+    }
+    analyze(this) {
+        return if (expressionType?.hasFlexibleNullability == true && !shouldConsiderPlatformTypeAsNullable) {
+            false
+        } else {
+            expressionType?.canBeNull == true
+        }
+    }
+}


Test patch:
diff --git a/detekt-psi-utils/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/TypeUtilsSpec.kt b/detekt-psi-utils/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/TypeUtilsSpec.kt
new file mode 100644
index 000000000000..0b5c459fa155
--- /dev/null
+++ b/detekt-psi-utils/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/TypeUtilsSpec.kt
@@ -0,0 +1,71 @@
+package io.gitlab.arturbosch.detekt.rules
+
+import io.github.detekt.test.utils.KotlinAnalysisApiEngine
+import org.assertj.core.api.Assertions.assertThat
+import org.jetbrains.kotlin.psi.KtClass
+import org.jetbrains.kotlin.psi.KtProperty
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+internal class TypeUtilsSpec {
+    @Test
+    fun `TypeUtils#isNullable returns true for expression that can be null`() {
+        val code = """
+            var a: Int? = null
+            val b = a
+        """.trimIndent()
+        val file = KotlinAnalysisApiEngine.compile(code)
+        val expression = file.children.filterIsInstance<KtProperty>().last().initializer!!
+
+        assertThat(expression.isNullable(false)).isTrue()
+    }
+
+    @ParameterizedTest
+    @ValueSource(
+        strings = [
+            "val c = a!!",
+            "val d = (a ?: error(\"null assertion message\"))",
+            "val e = 1?.and(2)",
+        ]
+    )
+    fun `TypeUtils#isNullable returns false for expression that cannot be null`(codeToTest: String) {
+        val code = """
+            var a: Int? = null
+            $codeToTest
+        """.trimIndent()
+        val file = KotlinAnalysisApiEngine.compile(code)
+        val expression = file.children.filterIsInstance<KtProperty>().last().initializer!!
+
+        assertThat(expression.isNullable(false)).isFalse()
+    }
+
+    @Nested
+    inner class PlatformType {
+        val code = """
+            class Thing {
+                val f = javaClass.simpleName
+            }
+        """.trimIndent()
+        val file = KotlinAnalysisApiEngine.compile(code)
+        val expression = file
+            .children
+            .filterIsInstance<KtClass>()
+            .single()
+            .body!!
+            .properties
+            .single()
+            .initializer!!
+
+        @Test
+        fun `platform type is nullable when shouldConsiderPlatformTypeAsNullable = true`() {
+            assertThat(expression.isNullable(shouldConsiderPlatformTypeAsNullable = true)).isTrue()
+        }
+
+        @Test
+        fun `platform type is not nullable when shouldConsiderPlatformTypeAsNullable = false`() {
+            assertThat(expression.isNullable(shouldConsiderPlatformTypeAsNullable = false)).isFalse()
+        }
+    }
+}