Repository: detekt/detekt (kotlin)

Issue:
False positive RedundantSuspendModifier when invoking a suspending function parameter
```kotlin
interface ApiCallStateModel : ViceSource<ApiCallState>, State<ApiCallState> {
  @Composable
  override fun currentState(): ApiCallState = value

  fun reset()
}

abstract class AbstractApiCallStateModel(
  private val context: Context,
) : ApiCallStateModel {
  final override var value: ApiCallState by mutableStateOf(ApiCallState.Pending)
    private set

  protected suspend fun <T> awaitApiCallResult(
    call: suspend () -> MyResult<T>,
  ): MyResult<T> {
    value = ApiCallState.InFlight

    val result = call()
    value = when(result) {
      is MyResult.Error -> ApiCallState.Error(
        error = result.message ?: "error",
      )

      is MyResult.Success -> ApiCallState.Pending
    }

    return result
  }

  override fun reset() {
    value = ApiCallState.Pending
  }
}
```

## Your Environment
* Version of detekt used: 2.0.0-alpha.0
* Version of Gradle used (if applicable): 9.0.0

Pull request:
Fixes an issue where RedundantSuspendModifier raises a false positive in a couple of cases in the new 2.0.0 alpha release:

1. a suspending lambda parameter is being invoked (fixes https://github.com/detekt/detekt/issues/8602)
2. a a `suspend operator fun` function is being called. Added tests for invoke, get and set.

We could go through [all operator overload functions](https://kotlinlang.org/docs/operator-overloading.html) and make sure a suspending variant of each doesn't trigger the rule, but that might be a bit much? Not sure where to draw the line really.

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 30239ffe4f52..707ec617d0a7 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
@@ -17,6 +17,7 @@ import org.jetbrains.kotlin.analysis.api.resolution.symbol
 import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol
 import org.jetbrains.kotlin.idea.references.mainReference
 import org.jetbrains.kotlin.lexer.KtTokens
+import org.jetbrains.kotlin.psi.KtArrayAccessExpression
 import org.jetbrains.kotlin.psi.KtCallExpression
 import org.jetbrains.kotlin.psi.KtExpression
 import org.jetbrains.kotlin.psi.KtForExpression
@@ -75,17 +76,12 @@ class RedundantSuspendModifier(config: Config) :
     }
 
     private fun KtExpression.isValidCandidateExpression(): Boolean =
-        when (this) {
-            is KtOperationReferenceExpression, is KtForExpression, is KtProperty, is KtNameReferenceExpression -> true
-            else -> {
-                val parent = parent
-                if (parent is KtCallExpression && parent.calleeExpression == this) {
-                    true
-                } else {
-                    this is KtCallExpression && this.calleeExpression is KtCallExpression
-                }
-            }
-        }
+        this is KtOperationReferenceExpression ||
+            this is KtForExpression ||
+            this is KtProperty ||
+            this is KtNameReferenceExpression ||
+            this is KtCallExpression ||
+            this is KtArrayAccessExpression // for get() operator function calls
 
     private fun KtExpression.hasSuspendCalls(): Boolean {
         if (!isValidCandidateExpression()) return false


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 ff6800ac3e4f..91596aa07b9b 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
@@ -161,4 +161,59 @@ class RedundantSuspendModifierSpec(val env: KotlinEnvironmentContainer) {
         """.trimIndent()
         assertThat(subject.lintWithContext(env, code)).isEmpty()
     }
+
+    @Test
+    fun `does not report if a suspending invoke operator function is called`() {
+        val code = """
+            class Foo {
+                suspend operator fun invoke(ms: Long): Unit = kotlinx.coroutines.delay(ms)
+            }
+
+            suspend fun bar() {
+                val foo = Foo()
+                foo(1234L)
+            }
+        """.trimIndent()
+        assertThat(subject.lintWithContext(env, code)).isEmpty()
+    }
+
+    @Test
+    fun `does not report if a suspending get operator function is called`() {
+        val code = """
+            class Foo {
+                suspend operator fun get(ms: Long): Unit = kotlinx.coroutines.delay(ms)
+            }
+
+            suspend fun bar() {
+                val foo = Foo()
+                foo[1234L]
+            }
+        """.trimIndent()
+        assertThat(subject.lintWithContext(env, code)).isEmpty()
+    }
+
+    @Test
+    fun `does not report if a suspending set operator function is called`() {
+        val code = """
+            class Foo {
+                suspend operator fun set(key: String, value: Long): Unit = kotlinx.coroutines.delay(value)
+            }
+
+            suspend fun bar() {
+                val foo = Foo()
+                foo["key"] = 1234L
+            }
+        """.trimIndent()
+        assertThat(subject.lintWithContext(env, code)).isEmpty()
+    }
+
+    @Test
+    fun `does not report if a suspending lambda is called`() {
+        val code = """
+            suspend fun bar(foo: suspend () -> Unit) {
+                foo()
+            }
+        """.trimIndent()
+        assertThat(subject.lintWithContext(env, code)).isEmpty()
+    }
 }