Repository: detekt/detekt (kotlin)

Issue:
UnnecessaryParentheses false positive on FloatRange
## Expected Behavior
Parentheses are allowed for FloatRanges: `.1F..(.2F)`

## Observed Behavior
They are flagged as UnnecessaryParentheses

## Your Environment
* Version of detekt used: 1.23.7

Pull request description:
Fixes: https://github.com/detekt/detekt/issues/7640

Patch:
diff --git a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryParentheses.kt b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryParentheses.kt
index 0b55b1d7efa7..7ed0d1d9e4ac 100644
--- a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryParentheses.kt
+++ b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryParentheses.kt
@@ -6,6 +6,7 @@ import io.gitlab.arturbosch.detekt.api.Configuration
 import io.gitlab.arturbosch.detekt.api.Entity
 import io.gitlab.arturbosch.detekt.api.Rule
 import io.gitlab.arturbosch.detekt.api.config
+import org.jetbrains.kotlin.KtNodeTypes
 import org.jetbrains.kotlin.com.intellij.psi.PsiElement
 import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
 import org.jetbrains.kotlin.lexer.KtTokens
@@ -70,6 +71,8 @@ class UnnecessaryParentheses(config: Config) : Rule(
 
         if (allowForUnclearPrecedence && expression.isUnaryOperationPrecedenceUnclear()) return
 
+        if (allowForUnclearPrecedence && expression.isFloatWithOutIntegerPartAroundRange()) return
+
         val message = "Parentheses in ${expression.text} are unnecessary and can be replaced with: " +
             KtPsiUtil.deparenthesize(expression)?.text
         report(CodeSmell(Entity.from(expression), message))
@@ -175,5 +178,24 @@ class UnnecessaryParentheses(config: Config) : Rule(
                 KtTokens.MINUSMINUS,
             )
         }
+
+        /**
+         * Determines whether this is decimal without integer part present in ranges
+         */
+        @Suppress("ReturnCount")
+        private fun KtParenthesizedExpression.isFloatWithOutIntegerPartAroundRange(): Boolean {
+            val parentExpression = this.parent
+            if (parentExpression !is KtBinaryExpression) return false
+
+            if (
+                parentExpression.operationToken != KtTokens.RANGE ||
+                (this.expression as? KtConstantExpression)?.elementType != KtNodeTypes.FLOAT_CONSTANT ||
+                parentExpression.right != this
+            ) {
+                return false
+            }
+
+            return this.expression?.run { text.startsWith(".") } == true
+        }
     }
 }


Test patch:
diff --git a/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryParenthesesSpec.kt b/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryParenthesesSpec.kt
index b2bcc888670e..980dddd48b65 100644
--- a/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryParenthesesSpec.kt
+++ b/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnnecessaryParenthesesSpec.kt
@@ -153,7 +153,9 @@ class UnnecessaryParenthesesSpec {
 
     @ParameterizedTest
     @MethodSource("cases")
-    fun `should not report call to function with two lambda parameters with one as block body`(testCase: RuleTestCase) {
+    fun `should not report call to function with two lambda parameters with one as block body`(
+        testCase: RuleTestCase
+    ) {
         val code = """
             class Clazz {
                 fun test(first: (Int) -> Unit, second: (Int) -> Unit) {
@@ -364,6 +366,16 @@ class UnnecessaryParenthesesSpec {
         assertThat(testCase.rule.lint(code)).hasSize(if (testCase.allowForUnclearPrecedence) 0 else 4)
     }
 
+    @ParameterizedTest
+    @MethodSource("cases")
+    fun `integer literal range operator`(testCase: RuleTestCase) {
+        val code = """
+            val a = (1)..(2)
+            val b = (1)..<(2)
+        """.trimIndent()
+        assertThat(testCase.rule.compileAndLint(code)).hasSize(4)
+    }
+
     @ParameterizedTest
     @MethodSource("cases")
     fun `does not report unary operator with value when precedence is clear`(testCase: RuleTestCase) {
@@ -453,7 +465,9 @@ class UnnecessaryParenthesesSpec {
 
     @ParameterizedTest
     @MethodSource("cases")
-    fun `does report unnecessary parens in case of constant literal when using inc operator`(testCase: RuleTestCase) {
+    fun `does report unnecessary parens in case of constant literal when using inc operator`(
+        testCase: RuleTestCase
+    ) {
         val code = """
             class Foo(var value: Int) {
                 operator fun inc() = Foo(value + 1)
@@ -490,7 +504,9 @@ class UnnecessaryParenthesesSpec {
             }
         """.trimIndent()
 
-        assertThat(RuleTestCase(allowForUnclearPrecedence = true).rule.compileAndLint(code)).hasSize(3)
+        assertThat(RuleTestCase(allowForUnclearPrecedence = true).rule.compileAndLint(code)).hasSize(
+            3
+        )
     }
 
     @Test
@@ -511,7 +527,9 @@ class UnnecessaryParenthesesSpec {
                 val violation3 = ++((a.value))
             }
         """.trimIndent()
-        assertThat(RuleTestCase(allowForUnclearPrecedence = false).rule.compileAndLint(code)).hasSize(5)
+        assertThat(RuleTestCase(allowForUnclearPrecedence = false).rule.compileAndLint(code)).hasSize(
+            5
+        )
     }
 
     @ParameterizedTest
@@ -571,6 +589,72 @@ class UnnecessaryParenthesesSpec {
         assertThat(testCase.rule.compileAndLint(code)).hasSize(if (testCase.allowForUnclearPrecedence) 4 else 5)
     }
 
+    @ParameterizedTest
+    @MethodSource("cases")
+    fun `float literals closed range without integer part with braces on the right side - #7640`(
+        testCase: RuleTestCase
+    ) {
+        val code = """
+            val a = .1F..(.2F)
+        """.trimIndent()
+        assertThat(testCase.rule.compileAndLint(code)).hasSize(if (testCase.allowForUnclearPrecedence) 0 else 1)
+    }
+
+    @ParameterizedTest
+    @MethodSource("cases")
+    fun `float literals closed range without integer part with braces on the left side`(testCase: RuleTestCase) {
+        val code = """
+            val a = (.1F)..0.2F
+        """.trimIndent()
+        assertThat(testCase.rule.compileAndLint(code)).hasSize(1)
+    }
+
+    @ParameterizedTest
+    @MethodSource("cases")
+    fun `float literals open range`(testCase: RuleTestCase) {
+        val code = """
+            val a = .1F..<(.2F)
+            val b = .1F..<(0.2F)
+            val c = (.1F)..<.2F
+            val d = (.1F)..<.2F
+        """.trimIndent()
+        assertThat(testCase.rule.compileAndLint(code)).hasSize(4)
+    }
+
+    @ParameterizedTest
+    @MethodSource("cases")
+    fun `double literals closed range without integer part`(testCase: RuleTestCase) {
+        val code = """
+            val a = .1..(.2)
+        """.trimIndent()
+
+        assertThat(testCase.rule.compileAndLint(code)).hasSize(if (testCase.allowForUnclearPrecedence) 0 else 1)
+    }
+
+    @ParameterizedTest
+    @MethodSource("cases")
+    fun `double literals open range`(testCase: RuleTestCase) {
+        val code = """
+            val a = .1..<(.2)
+            val b = .1..<(0.2)
+            val c = (.1)..<0.2
+            val d = (0.1)..<0.2
+        """.trimIndent()
+
+        assertThat(testCase.rule.compileAndLint(code)).hasSize(4)
+    }
+
+    @ParameterizedTest
+    @MethodSource("cases")
+    fun `double variable open range `(testCase: RuleTestCase) {
+        val code = """
+            val a = 0.2
+            val b = 0.3
+            val range = (a)..(b)
+        """.trimIndent()
+        assertThat(testCase.rule.compileAndLint(code)).hasSize(2)
+    }
+
     companion object {
         class RuleTestCase(val allowForUnclearPrecedence: Boolean) {
             val rule = UnnecessaryParentheses(
@@ -582,10 +666,16 @@ class UnnecessaryParenthesesSpec {
         fun cases(): List<Arguments> =
             listOf(
                 Arguments.of(
-                    Named.of("Without allow for unclear precedence", RuleTestCase(allowForUnclearPrecedence = false))
+                    Named.of(
+                        "Without allow for unclear precedence",
+                        RuleTestCase(allowForUnclearPrecedence = false)
+                    )
                 ),
                 Arguments.of(
-                    Named.of("With allow for unclear precedence", RuleTestCase(allowForUnclearPrecedence = true))
+                    Named.of(
+                        "With allow for unclear precedence",
+                        RuleTestCase(allowForUnclearPrecedence = true)
+                    )
                 ),
             )
     }