Repository: detekt/detekt (kotlin)

Issue:
UnusedPrivateClass false positive with callable references
<!-- The bug you're experiencing might have already be reported! -->
<!-- Please search in the [issues](https://github.com/detekt/detekt/issues) before creating one. -->

## Expected Behavior
I expect to see no `UnusedPrivateClass` warning because the private class is, in fact, used.

## Observed Behavior
The private class is marked by Detekt as an unused private class and the warning occurs.

## Steps to Reproduce
[This](https://github.com/JordanLongstaff/ArtemisAgent/security/code-scanning/286) is one of six private classes like this that are declared in my test suites. All of them are flagged as unused by Detekt, but none of them are actually unused, because there are callable references for all of them (namely, their primary constructors).

![Screen Shot 2024-09-21 at 2 22 09 PM](https://github.com/user-attachments/assets/804521d4-e4fa-4b60-b5fa-7e3cc6a03367)

## Context
These false positives are showing up as security alerts in my GitHub repo. I'm trying to clear some of them out today.

## Your Environment
<!-- Include as many relevant details about the environment you experienced the bug in -->
* Version of detekt used: 1.23.7
* Version of Gradle used (if applicable): 8.10.1
* Operating System and version: macOS Monterey 12.7.6
* Link to your project (if it's a public repository): [ArtemisAgent](https://github.com/JordanLongstaff/ArtemisAgent)

Pull request:
Fixes: https://github.com/detekt/detekt/issues/7660

Earlier we were only checking the RHS of `::` when there was empty LHS, so case when we use `::Foo` in `Parent class` like `Parent::Foo` we were skipping the usage of `Foo`

Patch:
diff --git a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnusedPrivateClass.kt b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnusedPrivateClass.kt
index ea9206ccc6c8..d5a7ffba770e 100644
--- a/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnusedPrivateClass.kt
+++ b/detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnusedPrivateClass.kt
@@ -51,7 +51,12 @@ class UnusedPrivateClass(config: Config) : Rule(
         root.accept(classVisitor)
 
         classVisitor.getUnusedClasses().forEach {
-            report(CodeSmell(Entity.from(it), "Private class ${it.nameAsSafeName.identifier} is unused."))
+            report(
+                CodeSmell(
+                    Entity.from(it),
+                    "Private class ${it.nameAsSafeName.identifier} is unused."
+                )
+            )
         }
     }
 
@@ -163,12 +168,10 @@ class UnusedPrivateClass(config: Config) : Rule(
 
         override fun visitDoubleColonExpression(expression: KtDoubleColonExpression) {
             checkReceiverForClassUsage(expression.receiverExpression)
-            if (expression.isEmptyLHS) {
-                (expression as? KtCallableReferenceExpression)
-                    ?.callableReference
-                    ?.takeIf { looksLikeAClassName(it.getReferencedName()) }
-                    ?.let { namedClasses.add(it.getReferencedName()) }
-            }
+            (expression as? KtCallableReferenceExpression)
+                ?.callableReference
+                ?.takeIf { looksLikeAClassName(it.getReferencedName()) }
+                ?.let { namedClasses.add(it.getReferencedName()) }
             super.visitDoubleColonExpression(expression)
         }
 


Test patch:
diff --git a/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnusedPrivateClassSpec.kt b/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnusedPrivateClassSpec.kt
index 8558d91a26ab..fef8604a3940 100644
--- a/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnusedPrivateClassSpec.kt
+++ b/detekt-rules-style/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/style/UnusedPrivateClassSpec.kt
@@ -457,5 +457,71 @@ class UnusedPrivateClassSpec {
             assertThat(findings).hasSize(1)
             assertThat(findings).hasStartSourceLocation(10, 5)
         }
+
+        @Test
+        fun `should not report when callable ref for constructor is used`() {
+            val code = """
+                private class A
+
+                private val listOfConstructors = listOf(::A)
+            """.trimIndent()
+            val findings = UnusedPrivateClass(Config.empty).compileAndLint(code)
+            assertThat(findings).isEmpty()
+        }
+
+        @Test
+        fun `should not report when callable ref for inner private class constructor is used with parent name`() {
+            val code = """
+                class Parent {
+                    private class Foo
+
+                    private val list = listOf(Parent::Foo)
+                }
+            """.trimIndent()
+            val findings = UnusedPrivateClass(Config.empty).compileAndLint(code)
+            assertThat(findings).isEmpty()
+        }
+
+        @Test
+        fun `should not report callable ref for inner private child class constructor is used with parent private class name`() {
+            val code = """
+                class Parent {
+                    private class Foo {
+                        private class Bar
+                        private val list = listOf(Foo::Bar)
+                    }
+                }
+            """.trimIndent()
+            val findings = UnusedPrivateClass(Config.empty).compileAndLint(code)
+            assertThat(findings).isEmpty()
+        }
+
+        @Test
+        fun `should not report when callable ref for inner private class constructor is used without parent name`() {
+            val code = """
+                class Parent {
+                    private class Foo
+
+                    private val list = listOf(::Foo)
+                }
+            """.trimIndent()
+            val findings = UnusedPrivateClass(Config.empty).compileAndLint(code)
+            assertThat(findings).isEmpty()
+        }
+
+        @Test
+        fun `should not report when callable ref for inner private class secondary constructor is used`() {
+            val code = """
+                class Parent {
+                    private class Foo {
+                        constructor(a: Int)
+                    }
+
+                    private val list = listOf(1).map(::Foo)
+                }
+            """.trimIndent()
+            val findings = UnusedPrivateClass(Config.empty).compileAndLint(code)
+            assertThat(findings).isEmpty()
+        }
     }
 }