Repository: detekt/detekt (kotlin)

Issue:
MaxChainedCallsOnSameLine reports fully-qualified class names
## Expected Behavior
The `MaxChainedCallsOnSameLine` should probably ignore fully-qualified class names (like this [example](https://github.com/detekt/detekt/blob/ddcf77d9d4651bd117ff622180312141f900ac23/detekt-formatting/src/main/kotlin/io/gitlab/arturbosch/detekt/formatting/FormattingRule.kt#L92) from detekt's codebase). These call chains are somewhat different in nature from its intended purpose, and the rule probably shouldn't have a second effect of restricting fully-qualified class names, which may be necessary (or at least preferred over [named imports](https://kotlinlang.org/docs/packages.html#imports) with `as`) when classes with the same name but different packages are used. See discussion in #5020.

A fix would likely require type resolution to determine the role of chained calls, but this complicates the rule and makes it harder to be applied, just to avoid a fairly rare edge case. So I wanted to open this issue for discussion/feedback on how to proceed.

## Observed Behavior
`MaxChainedCallsOnSameLine` reports fully qualified class names above its `maxChainedCalls` limit.

## Your Environment
* Version of detekt used: 1.21.0-RC2

Pull request description:
Fixes #5028

Patch:
diff --git a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/MaxChainedCallsOnSameLine.kt b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/MaxChainedCallsOnSameLine.kt
index e47a6ea033c1..f64f09a2974b 100644
--- a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/MaxChainedCallsOnSameLine.kt
+++ b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/MaxChainedCallsOnSameLine.kt
@@ -9,11 +9,15 @@ import io.gitlab.arturbosch.detekt.api.Rule
 import io.gitlab.arturbosch.detekt.api.Severity
 import io.gitlab.arturbosch.detekt.api.config
 import io.gitlab.arturbosch.detekt.api.internal.Configuration
+import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
+import org.jetbrains.kotlin.descriptors.PackageViewDescriptor
 import org.jetbrains.kotlin.psi.KtExpression
 import org.jetbrains.kotlin.psi.KtImportDirective
 import org.jetbrains.kotlin.psi.KtPackageDirective
 import org.jetbrains.kotlin.psi.KtQualifiedExpression
+import org.jetbrains.kotlin.psi.KtReferenceExpression
 import org.jetbrains.kotlin.psi.KtUnaryExpression
+import org.jetbrains.kotlin.resolve.BindingContext
 
 /**
  * Limits the number of chained calls which can be placed on a single line.
@@ -27,6 +31,7 @@ import org.jetbrains.kotlin.psi.KtUnaryExpression
  *   .d().e().f()
  * </compliant>
  */
+@RequiresTypeResolution
 class MaxChainedCallsOnSameLine(config: Config = Config.empty) : Rule(config) {
     override val issue = Issue(
         id = javaClass.simpleName,
@@ -38,9 +43,12 @@ class MaxChainedCallsOnSameLine(config: Config = Config.empty) : Rule(config) {
     @Configuration("maximum chained calls allowed on a single line")
     private val maxChainedCalls: Int by config(defaultValue = 5)
 
+    @Suppress("ReturnCount")
     override fun visitQualifiedExpression(expression: KtQualifiedExpression) {
         super.visitQualifiedExpression(expression)
 
+        if (bindingContext == BindingContext.EMPTY) return
+
         val parent = expression.parent
 
         // skip if the parent is also a call on the same line to avoid duplicated warnings
@@ -63,13 +71,21 @@ class MaxChainedCallsOnSameLine(config: Config = Config.empty) : Rule(config) {
 
     private fun KtExpression.countChainedCalls(): Int {
         return when (this) {
-            is KtQualifiedExpression ->
-                if (callOnNewLine()) 0 else receiverExpression.countChainedCalls() + 1
+            is KtQualifiedExpression -> when {
+                receiverExpression.isReferenceToPackage() || callOnNewLine() -> 0
+                else -> receiverExpression.countChainedCalls() + 1
+            }
             is KtUnaryExpression -> baseExpression?.countChainedCalls() ?: 0
             else -> 0
         }
     }
 
+    private fun KtExpression.isReferenceToPackage(): Boolean {
+        val selectorOrThis = (this as? KtQualifiedExpression)?.selectorExpression ?: this
+        if (selectorOrThis !is KtReferenceExpression) return false
+        return bindingContext[BindingContext.REFERENCE_TARGET, selectorOrThis] is PackageViewDescriptor
+    }
+
     private fun KtQualifiedExpression.callOnNewLine(): Boolean {
         val receiver = receiverExpression
         val selector = selectorExpression ?: return false


Test patch:
diff --git a/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/MaxChainedCallsOnSameLineSpec.kt b/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/MaxChainedCallsOnSameLineSpec.kt
index 84f61f6cec7e..f1240d13bfb0 100644
--- a/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/MaxChainedCallsOnSameLineSpec.kt
+++ b/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/MaxChainedCallsOnSameLineSpec.kt
@@ -1,18 +1,21 @@
 package io.gitlab.arturbosch.detekt.rules.style
 
+import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest
 import io.gitlab.arturbosch.detekt.test.TestConfig
 import io.gitlab.arturbosch.detekt.test.assertThat
-import io.gitlab.arturbosch.detekt.test.compileAndLint
+import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext
 import io.gitlab.arturbosch.detekt.test.lint
+import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
 import org.junit.jupiter.api.Test
 
-class MaxChainedCallsOnSameLineSpec {
+@KotlinCoreEnvironmentTest
+class MaxChainedCallsOnSameLineSpec(private val env: KotlinCoreEnvironment) {
     @Test
     fun `does not report 2 calls on a single line with a max of 3`() {
         val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
         val code = "val a = 0.plus(0)"
 
-        assertThat(rule.compileAndLint(code)).isEmpty()
+        assertThat(rule.compileAndLintWithContext(env, code)).isEmpty()
     }
 
     @Test
@@ -20,7 +23,7 @@ class MaxChainedCallsOnSameLineSpec {
         val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
         val code = "val a = 0.plus(0).plus(0)"
 
-        assertThat(rule.compileAndLint(code)).isEmpty()
+        assertThat(rule.compileAndLintWithContext(env, code)).isEmpty()
     }
 
     @Test
@@ -28,7 +31,7 @@ class MaxChainedCallsOnSameLineSpec {
         val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
         val code = "val a = 0.plus(0).plus(0).plus(0)"
 
-        assertThat(rule.compileAndLint(code)).hasSize(1)
+        assertThat(rule.compileAndLintWithContext(env, code)).hasSize(1)
     }
 
     @Test
@@ -36,7 +39,7 @@ class MaxChainedCallsOnSameLineSpec {
         val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
         val code = "val a = 0?.plus(0)?.plus(0)?.plus(0)"
 
-        assertThat(rule.compileAndLint(code)).hasSize(1)
+        assertThat(rule.compileAndLintWithContext(env, code)).hasSize(1)
     }
 
     @Test
@@ -44,7 +47,7 @@ class MaxChainedCallsOnSameLineSpec {
         val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
         val code = "val a = 0!!.plus(0)!!.plus(0)!!.plus(0)"
 
-        assertThat(rule.compileAndLint(code)).hasSize(1)
+        assertThat(rule.compileAndLintWithContext(env, code)).hasSize(1)
     }
 
     @Test
@@ -52,7 +55,7 @@ class MaxChainedCallsOnSameLineSpec {
         val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
         val code = "val a = 0.plus(0).plus(0).plus(0).plus(0).plus(0).plus(0)"
 
-        assertThat(rule.compileAndLint(code)).hasSize(1)
+        assertThat(rule.compileAndLintWithContext(env, code)).hasSize(1)
     }
 
     @Test
@@ -66,7 +69,7 @@ class MaxChainedCallsOnSameLineSpec {
                 .plus(0)
         """
 
-        assertThat(rule.compileAndLint(code)).isEmpty()
+        assertThat(rule.compileAndLintWithContext(env, code)).isEmpty()
     }
 
     @Test
@@ -78,7 +81,7 @@ class MaxChainedCallsOnSameLineSpec {
                 .plus(0).plus(0).plus(0)
         """
 
-        assertThat(rule.compileAndLint(code)).isEmpty()
+        assertThat(rule.compileAndLintWithContext(env, code)).isEmpty()
     }
 
     @Test
@@ -90,7 +93,7 @@ class MaxChainedCallsOnSameLineSpec {
                 .plus(0)
         """
 
-        assertThat(rule.compileAndLint(code)).hasSize(1)
+        assertThat(rule.compileAndLintWithContext(env, code)).hasSize(1)
     }
 
     @Test
@@ -103,7 +106,7 @@ class MaxChainedCallsOnSameLineSpec {
                 .plus(0)
         """
 
-        assertThat(rule.compileAndLint(code)).hasSize(1)
+        assertThat(rule.compileAndLintWithContext(env, code)).hasSize(1)
     }
 
     @Test
@@ -119,6 +122,24 @@ class MaxChainedCallsOnSameLineSpec {
         val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
         val code = "package a.b.c.d.e"
 
-        assertThat(rule.lint(code)).isEmpty()
+        assertThat(rule.compileAndLintWithContext(env, code)).isEmpty()
+    }
+
+    @Test
+    fun `does not count package references as chained calls`() {
+        val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
+        val code = """
+            val x = kotlin.math.floor(1.0).plus(1).plus(1)
+        """
+        assertThat(rule.compileAndLintWithContext(env, code)).isEmpty()
+    }
+
+    @Test
+    fun `does not count a package reference as chained calls`() {
+        val rule = MaxChainedCallsOnSameLine(TestConfig(mapOf("maxChainedCalls" to 3)))
+        val code = """
+            val x = kotlin.run { 1 }.plus(1).plus(1)
+        """
+        assertThat(rule.compileAndLintWithContext(env, code)).isEmpty()
     }
 }