Repository: detekt/detekt (kotlin)

Issue:
CouldBeSequence and IntelliJ don't agree in chains with groupBy
With 2.0.0-alpha.1, the following is a `CouldBeSequence` violation (it was not in 1.23.8):

```kotlin
fun main() {
    // CouldBeSequence - [groupBy { "test" } could be .asSequence().groupBy { "test" }]
    listOf(1, 2, 3)
        .groupBy { "test" }
        .map { (a, b) -> a to b }
        .filter { (0..1).random() == 1  }
}
```

When you insert the call as instructed, an IntelliJ inspection complains that it's redundant:

<img width="580" height="156" alt="Image" src="https://github.com/user-attachments/assets/bbe2ed25-d1a4-45d5-bef6-1b1b6f07fe10" />

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

Patch:
diff --git a/detekt-rules-performance/src/main/kotlin/dev/detekt/rules/performance/CouldBeSequence.kt b/detekt-rules-performance/src/main/kotlin/dev/detekt/rules/performance/CouldBeSequence.kt
index 1dda07a7e7e..b51662d15e4 100644
--- a/detekt-rules-performance/src/main/kotlin/dev/detekt/rules/performance/CouldBeSequence.kt
+++ b/detekt-rules-performance/src/main/kotlin/dev/detekt/rules/performance/CouldBeSequence.kt
@@ -11,6 +11,7 @@ import org.jetbrains.kotlin.analysis.api.analyze
 import org.jetbrains.kotlin.analysis.api.resolution.KaCallableMemberCall
 import org.jetbrains.kotlin.analysis.api.resolution.singleCallOrNull
 import org.jetbrains.kotlin.analysis.api.resolution.symbol
+import org.jetbrains.kotlin.analysis.api.types.symbol
 import org.jetbrains.kotlin.name.StandardClassIds
 import org.jetbrains.kotlin.psi.KtCallExpression
 import org.jetbrains.kotlin.psi.KtExpression
@@ -47,13 +48,13 @@ class CouldBeSequence(config: Config) :
 
         if (visitedCallExpressions.contains(expression)) return
 
-        if (!expression.isCallingKotlinCollectionFunPresentInSequence()) return
+        if (!expression.isCallingCollectionFunPresentInSequenceReturningSequence()) return
 
         var counter = 1
         var nextCall = expression.nextChainedCall()
         while (nextCall != null) {
             visitedCallExpressions += nextCall
-            if (!nextCall.isCallingKotlinCollectionFunPresentInSequence()) {
+            if (!nextCall.isCallingCollectionFunPresentInSequenceReturningSequence()) {
                 break
             }
 
@@ -67,17 +68,22 @@ class CouldBeSequence(config: Config) :
         }
     }
 
-    private fun KtExpression.isCallingKotlinCollectionFunPresentInSequence(): Boolean = analyze(this) {
+    private fun KtExpression.isCallingCollectionFunPresentInSequenceReturningSequence(): Boolean = analyze(this) {
         val callableId = resolveToCall()
             ?.singleCallOrNull<KaCallableMemberCall<*, *>>()
             ?.symbol
             ?.callableId
         callableId?.packageName == StandardClassIds.BASE_COLLECTIONS_PACKAGE &&
-            findTopLevelCallables(StandardClassIds.BASE_SEQUENCES_PACKAGE, callableId.callableName).any()
+            findTopLevelCallables(StandardClassIds.BASE_SEQUENCES_PACKAGE, callableId.callableName)
+                .any { it.returnType.symbol?.classId?.asFqNameString() == SEQUENCE_CLASS_STR }
     }
 
     private fun KtExpression.nextChainedCall(): KtExpression? {
         val expression = this.getQualifiedExpressionForSelectorOrThis()
         return expression.getQualifiedExpressionForReceiver()?.selectorExpression
     }
+
+    companion object {
+        private const val SEQUENCE_CLASS_STR = "kotlin.sequences.Sequence"
+    }
 }


Test patch:
diff --git a/detekt-rules-performance/src/test/kotlin/dev/detekt/rules/performance/CouldBeSequenceSpec.kt b/detekt-rules-performance/src/test/kotlin/dev/detekt/rules/performance/CouldBeSequenceSpec.kt
index d10c33083ad..f1a102dae80 100644
--- a/detekt-rules-performance/src/test/kotlin/dev/detekt/rules/performance/CouldBeSequenceSpec.kt
+++ b/detekt-rules-performance/src/test/kotlin/dev/detekt/rules/performance/CouldBeSequenceSpec.kt
@@ -187,4 +187,31 @@ class CouldBeSequenceSpec(val env: KotlinEnvironmentContainer) {
         """.trimIndent()
         assertThat(subject.lintWithContext(env, code)).isEmpty()
     }
+
+    @Test
+    fun `terminating operator should not be sequence`() {
+        val code = """
+            fun main() {
+                listOf(1, 2, 3)
+                    .groupBy { "test" }
+                    .map { (a, b) -> a to b }
+                    .filter { (0..1).random() == 1  }
+            }
+        """.trimIndent()
+        assertThat(subject.lintWithContext(env, code)).isEmpty()
+    }
+
+    @Test
+    fun `long list after terminal operation should be reported`() {
+        val code = """
+            fun main() {
+                listOf(1, 2, 3)
+                    .groupBy { "test" }
+                    .map { (a, b) -> a to b }
+                    .filter { (0..1).random() == 1  }
+                    .take(2)
+            }
+        """.trimIndent()
+        assertThat(subject.lintWithContext(env, code)).hasSize(1)
+    }
 }