Repository: detekt/detekt (kotlin)

Issue:
RedundantSuspendModifier false positive with expect/actual class
## Expected Behavior

If an `expect class` has functions with `suspend` in the signature, I shouldn't get a violation for having `suspend` in the `actual class` even if no suspending functions are called.

## Observed Behavior

I do get a violation.

## Steps to Reproduce

```kotlin
public expect class ThemeSettingStorage {
  public suspend fun load(): ThemeSetting?
  public suspend fun store(setting: ThemeSetting)
}

public actual class ThemeSettingStorage {
  public actual suspend fun load(): ThemeSetting? = ThemeSetting.System

  public actual suspend fun store(setting: ThemeSetting) {
  }
}
```

## Your Environment
* Version of detekt used: 1.23.5
* Version of Kotlin used: 1.9.22
* Version of Gradle used (if applicable): 8.6
* Operating System and version: Ubuntu 22.04

Pull request:
Fixes #6949

Patch:
diff --git a/detekt-rules-coroutines/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/coroutines/RedundantSuspendModifier.kt b/detekt-rules-coroutines/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/coroutines/RedundantSuspendModifier.kt
index b8da4f385a12..ef438f0fd28d 100644
--- a/detekt-rules-coroutines/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/coroutines/RedundantSuspendModifier.kt
+++ b/detekt-rules-coroutines/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/coroutines/RedundantSuspendModifier.kt
@@ -62,7 +62,7 @@ class RedundantSuspendModifier(config: Config) : Rule(
     override fun visitNamedFunction(function: KtNamedFunction) {
         val suspendModifier = function.modifierList?.getModifier(KtTokens.SUSPEND_KEYWORD) ?: return
         if (!function.hasBody()) return
-        if (function.hasModifier(KtTokens.OVERRIDE_KEYWORD)) return
+        if (function.hasModifier(KtTokens.OVERRIDE_KEYWORD) || function.hasModifier(KtTokens.ACTUAL_KEYWORD)) return
 
         val descriptor = bindingContext[BindingContext.FUNCTION, function] ?: return
         if (descriptor.modality == Modality.OPEN) return


Test patch:
diff --git a/detekt-rules-coroutines/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/coroutines/RedundantSuspendModifierSpec.kt b/detekt-rules-coroutines/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/coroutines/RedundantSuspendModifierSpec.kt
index 6b103c2d6b2a..79d49c4f7019 100644
--- a/detekt-rules-coroutines/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/coroutines/RedundantSuspendModifierSpec.kt
+++ b/detekt-rules-coroutines/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/coroutines/RedundantSuspendModifierSpec.kt
@@ -3,6 +3,7 @@ package io.gitlab.arturbosch.detekt.rules.coroutines
 import io.gitlab.arturbosch.detekt.api.Config
 import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest
 import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext
+import io.gitlab.arturbosch.detekt.test.lintWithContext
 import org.assertj.core.api.Assertions.assertThat
 import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
 import org.junit.jupiter.api.Test
@@ -10,7 +11,7 @@ import org.junit.jupiter.api.Test
 @KotlinCoreEnvironmentTest
 class RedundantSuspendModifierSpec(val env: KotlinCoreEnvironment) {
 
-    val subject = RedundantSuspendModifier(Config.empty)
+    private val subject = RedundantSuspendModifier(Config.empty)
 
     @Test
     fun `reports when public function returns expression of platform type`() {
@@ -88,6 +89,20 @@ class RedundantSuspendModifierSpec(val env: KotlinCoreEnvironment) {
         assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
     }
 
+    @Test
+    fun `does not report actual suspend function`() {
+        val code = """
+            expect class Foo {
+                suspend fun bar()
+            }
+            
+            actual class Foo {
+                actual suspend fun bar() {}
+            }
+        """.trimIndent()
+        assertThat(subject.lintWithContext(env, code)).isEmpty()
+    }
+
     @Test
     fun `ignores when iterator is suspending`() {
         val code = """