Repository: detekt/detekt (kotlin)

Issue:
False negative for companion object in UndocumentedPublicClass
## Steps to Reproduce
```kotlin
public object PublicObject

public class PublicClass {

	public companion object;
}
```

Configuration:
```gradle.kts
detekt.allRules = true
// Does not contain relevant lines, just a few rules deactivated or fine tuned.
detekt.config.from(rootProject.file("config/detekt/detekt.yml"))
dependencies { detektPlugins(libs.detekt.rules.libraries) }
```

## Observed Behavior
```console
src\main\kotlin\com\example\PublicTest.kt:21:15: PublicObject is missing required documentation. [UndocumentedPublicClass]
src\main\kotlin\com\example\PublicTest.kt:23:14: PublicClass is missing required documentation. [UndocumentedPublicClass]
```

## Expected Behavior
The 2 findings are fine, but there are 3 public classes in the above code:
 * `PublicObject`
 * `PublicClass`
 * `PublicClass.Companion`

So the false negative is the companion object.

## Context
I'm relying on detekt to ensure that all my public API surface is documented.

 * Related: https://github.com/detekt/detekt/issues/7218

## Your Environment
<!-- Include as many relevant details about the environment you experienced the bug in -->
* Version of detekt used: 1.23.5
* Version of Gradle used (if applicable): 8.7
* Gradle scan link (add `--scan` option when running the gradle task): N/A
* Operating System and version: Windows 10 / GitHub Actions Ubuntu 22.04
* Link to your project (if it's a public repository): N/A

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

Patch:
diff --git a/detekt-rules-documentation/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/UndocumentedPublicClass.kt b/detekt-rules-documentation/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/UndocumentedPublicClass.kt
index 8987d207fee4..b69c493008ec 100644
--- a/detekt-rules-documentation/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/UndocumentedPublicClass.kt
+++ b/detekt-rules-documentation/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/UndocumentedPublicClass.kt
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.psi.KtClass
 import org.jetbrains.kotlin.psi.KtClassOrObject
 import org.jetbrains.kotlin.psi.KtEnumEntry
 import org.jetbrains.kotlin.psi.KtObjectDeclaration
+import org.jetbrains.kotlin.psi.psiUtil.isPublic
 
 /**
  * This rule reports public classes, objects and interfaces which do not have the required documentation.
@@ -50,10 +51,16 @@ class UndocumentedPublicClass(config: Config) : Rule(
 
     private fun requiresDocumentation(
         klass: KtClass
-    ) = klass.isTopLevel() || klass.isInnerClass() || klass.isNestedClass() || klass.isInnerInterface()
+    ) =
+        klass.isTopLevel() || klass.isInnerClass() || klass.isNestedClass() || klass.isInnerInterface()
 
+    @Suppress("ComplexCondition")
     override fun visitObjectDeclaration(declaration: KtObjectDeclaration) {
-        if (declaration.isCompanionWithoutName() || declaration.isLocal || !searchInInnerObject) {
+        if (
+            (declaration.isCompanionWithoutName() && !declaration.isPublic) ||
+            declaration.isLocal ||
+            !searchInInnerObject
+        ) {
             return
         }
 
@@ -76,16 +83,21 @@ class UndocumentedPublicClass(config: Config) : Rule(
     }
 
     private fun isPublicAndPublicInherited(element: KtClassOrObject) =
-        element.isPublicInherited(searchInProtectedClass) && element.isPublicNotOverridden(searchInProtectedClass)
+        element.isPublicInherited(searchInProtectedClass) &&
+            element.isPublicNotOverridden(
+                searchInProtectedClass
+            )
 
     private fun KtObjectDeclaration.isCompanionWithoutName() =
         isCompanion() && nameAsSafeName.asString() == "Companion"
 
-    private fun KtClass.isNestedClass() = !isInterface() && !isTopLevel() && !isInner() && searchInNestedClass
+    private fun KtClass.isNestedClass() =
+        !isInterface() && !isTopLevel() && !isInner() && searchInNestedClass
 
     private fun KtClass.isInnerClass() = !isInterface() && isInner() && searchInInnerClass
 
-    private fun KtClass.isInnerInterface() = !isTopLevel() && isInterface() && searchInInnerInterface
+    private fun KtClass.isInnerInterface() =
+        !isTopLevel() && isInterface() && searchInInnerInterface
 
     private fun KtClassOrObject.notEnumEntry() = this !is KtEnumEntry
 }


Test patch:
diff --git a/detekt-rules-documentation/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/UndocumentedPublicClassSpec.kt b/detekt-rules-documentation/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/UndocumentedPublicClassSpec.kt
index 010b55aa7cb9..56cde6318e80 100644
--- a/detekt-rules-documentation/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/UndocumentedPublicClassSpec.kt
+++ b/detekt-rules-documentation/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/UndocumentedPublicClassSpec.kt
@@ -276,4 +276,30 @@ class UndocumentedPublicClassSpec {
         val subject = UndocumentedPublicClass(TestConfig(SEARCH_IN_PROTECTED_CLASS to "true"))
         assertThat(subject.compileAndLint(code)).hasSize(1)
     }
+
+    @Test
+    fun `should report in public companion class - #7217`() {
+        val code = """
+            public object PublicObject
+
+            public class PublicClass {
+                public companion object
+            }
+        """.trimIndent()
+        assertThat(subject.compileAndLint(code)).hasSize(3)
+    }
+
+    @Test
+    fun `should not report in private or internal companion class - #7217`() {
+        val code = """
+            public class PublicObject {
+                internal companion object
+            }
+
+            public class PublicClass {
+                private companion object
+            }
+        """.trimIndent()
+        assertThat(subject.compileAndLint(code)).hasSize(2)
+    }
 }