Repository: detekt/detekt (kotlin)

Issue:
False positive RedundantSuspendModifier with interface default implementation
With 2.0.0-alpha.1, there's a false positive `RedundantSuspendModifier` warning here if `foo` has a default implementation that doesn't make any suspend calls, even though the implementation needs `suspend`.

This seems to be a regression compared to 1.23.8.

```kotlin
interface Interface {
    // RedundantSuspendModifier - [Function has redundant `suspend` modifier.]
    suspend fun foo(): Int = 0
}

class Impl : Interface {
    override suspend fun foo(): Int {
        delay(1000)
        return 1
    }
}
```

Pull request:
Fixes https://github.com/detekt/detekt/issues/8800

Patch:
diff --git a/detekt-rules-coroutines/src/main/kotlin/dev/detekt/rules/coroutines/RedundantSuspendModifier.kt b/detekt-rules-coroutines/src/main/kotlin/dev/detekt/rules/coroutines/RedundantSuspendModifier.kt
index 707ec617d0a7..0b30903c8cda 100644
--- a/detekt-rules-coroutines/src/main/kotlin/dev/detekt/rules/coroutines/RedundantSuspendModifier.kt
+++ b/detekt-rules-coroutines/src/main/kotlin/dev/detekt/rules/coroutines/RedundantSuspendModifier.kt
@@ -15,6 +15,7 @@ import org.jetbrains.kotlin.analysis.api.resolution.KaVariableAccessCall
 import org.jetbrains.kotlin.analysis.api.resolution.successfulCallOrNull
 import org.jetbrains.kotlin.analysis.api.resolution.symbol
 import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaSymbolModality
 import org.jetbrains.kotlin.idea.references.mainReference
 import org.jetbrains.kotlin.lexer.KtTokens
 import org.jetbrains.kotlin.psi.KtArrayAccessExpression
@@ -65,6 +66,8 @@ class RedundantSuspendModifier(config: Config) :
 
         if (function.isOpen()) return
 
+        if (analyze(function) { function.symbol.modality } == KaSymbolModality.OPEN) return
+
         if (!function.anyDescendantOfType<KtExpression> { it.hasSuspendCalls() }) {
             report(
                 Finding(


Test patch:
diff --git a/detekt-rules-coroutines/src/test/kotlin/dev/detekt/rules/coroutines/RedundantSuspendModifierSpec.kt b/detekt-rules-coroutines/src/test/kotlin/dev/detekt/rules/coroutines/RedundantSuspendModifierSpec.kt
index 0ba517f61098..787ef2828cc6 100644
--- a/detekt-rules-coroutines/src/test/kotlin/dev/detekt/rules/coroutines/RedundantSuspendModifierSpec.kt
+++ b/detekt-rules-coroutines/src/test/kotlin/dev/detekt/rules/coroutines/RedundantSuspendModifierSpec.kt
@@ -63,7 +63,7 @@ class RedundantSuspendModifierSpec(val env: KotlinEnvironmentContainer) {
     }
 
     @Test
-    fun `does not report suspend function without body`() {
+    fun `does not report suspend function in interface without body`() {
         val code = """
             interface SuspendInterface {
                 suspend fun empty()
@@ -72,6 +72,46 @@ class RedundantSuspendModifierSpec(val env: KotlinEnvironmentContainer) {
         assertThat(subject.lintWithContext(env, code)).isEmpty()
     }
 
+    @Test
+    fun `does not report suspend function in interface with body - #8800`() {
+        val code = """
+            interface Interface {
+                suspend fun foo(): Int = 0
+            }
+            
+            class Impl : Interface {
+                override suspend fun foo(): Int {
+                    kotlinx.coroutines.delay(1000)
+                    return 1
+                }
+            }
+        """.trimIndent()
+        assertThat(subject.lintWithContext(env, code)).isEmpty()
+    }
+
+    @Test
+    fun `does not report abstract suspend function in abstract class`() {
+        val code = """
+            abstract class BaseClass {
+                abstract suspend fun bar(): Int
+            }
+        """.trimIndent()
+        assertThat(subject.lintWithContext(env, code)).isEmpty()
+    }
+
+    @Test
+    fun `does not report abstract suspend function in sealed class`() {
+        val code = """
+            sealed class SealedClass {
+                abstract suspend fun bar(): Int
+                class Subclass : SealedClass() {
+                    override suspend fun bar(): Int = 0
+                }
+            }
+        """.trimIndent()
+        assertThat(subject.lintWithContext(env, code)).isEmpty()
+    }
+
     @Test
     fun `does not report overridden suspend function`() {
         val code = """