diff --git a/detekt-core/src/main/resources/default-detekt-config.yml b/detekt-core/src/main/resources/default-detekt-config.yml index f67bed5e645..673c6c951f8 100644 --- a/detekt-core/src/main/resources/default-detekt-config.yml +++ b/detekt-core/src/main/resources/default-detekt-config.yml @@ -62,6 +62,9 @@ comments: EndOfSentenceFormat: active: false endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] OutdatedDocumentation: active: false matchTypeParameters: true diff --git a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/Exclusion.kt b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/Exclusion.kt index 7eca3361cb2..380dfa87258 100644 --- a/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/Exclusion.kt +++ b/detekt-generator/src/main/kotlin/io/gitlab/arturbosch/detekt/generator/printer/defaultconfig/Exclusion.kt @@ -41,6 +41,7 @@ private object TestExclusions : Exclusions() { "UndocumentedPublicFunction", "UndocumentedPublicProperty", "UnsafeCallOnNullableType", + "KDocReferencesNonPublicProperty", ) } diff --git a/detekt-rules-documentation/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/CommentSmellProvider.kt b/detekt-rules-documentation/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/CommentSmellProvider.kt index 8d93bd8e9a1..860996b59ca 100644 --- a/detekt-rules-documentation/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/CommentSmellProvider.kt +++ b/detekt-rules-documentation/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/CommentSmellProvider.kt @@ -24,7 +24,8 @@ class CommentSmellProvider : DefaultRuleSetProvider { UndocumentedPublicClass(config), UndocumentedPublicFunction(config), UndocumentedPublicProperty(config), - AbsentOrWrongFileLicense(config) + AbsentOrWrongFileLicense(config), + KDocReferencesNonPublicProperty(config) ) ) } diff --git a/detekt-rules-documentation/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/KDocReferencesNonPublicProperty.kt b/detekt-rules-documentation/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/KDocReferencesNonPublicProperty.kt new file mode 100644 index 00000000000..fe2f8312193 --- /dev/null +++ b/detekt-rules-documentation/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/KDocReferencesNonPublicProperty.kt @@ -0,0 +1,101 @@ +package io.gitlab.arturbosch.detekt.rules.documentation + +import io.gitlab.arturbosch.detekt.api.CodeSmell +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.Debt +import io.gitlab.arturbosch.detekt.api.Entity +import io.gitlab.arturbosch.detekt.api.Issue +import io.gitlab.arturbosch.detekt.api.Rule +import io.gitlab.arturbosch.detekt.api.Severity +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtNamedDeclaration +import org.jetbrains.kotlin.psi.KtObjectDeclaration +import org.jetbrains.kotlin.psi.KtProperty +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject +import org.jetbrains.kotlin.psi.psiUtil.getTopmostParentOfType +import org.jetbrains.kotlin.psi.psiUtil.isProtected +import org.jetbrains.kotlin.psi.psiUtil.isPublic + +/** + * This rule will report any KDoc comments that refer to non-public properties of a class. + * Clients do not need to know the implementation details. + * + * + * /** + * * Comment + * * [prop1] - non-public property + * * [prop2] - public property + * */ + * class Test { + * private val prop1 = 0 + * val prop2 = 0 + * } + * + * + * + * /** + * * Comment + * * [prop2] - public property + * */ + * class Test { + * private val prop1 = 0 + * val prop2 = 0 + * } + * + * + */ +class KDocReferencesNonPublicProperty(config: Config = Config.empty) : Rule(config) { + + override val issue = Issue( + javaClass.simpleName, + Severity.Maintainability, + "KDoc comments should not refer to non-public properties.", + Debt.FIVE_MINS + ) + + override fun visitProperty(property: KtProperty) { + super.visitProperty(property) + + val enclosingClass = property.getTopmostParentOfType() + val comment = enclosingClass?.docComment?.text ?: return + + if (property.isNonPublicInherited() && property.isReferencedInherited(comment)) { + report(property) + } + } + + private fun KtProperty.isNonPublicInherited(): Boolean { + if (!isPublic && !isProtected()) { + return true + } + var classOrObject = containingClassOrObject + while (classOrObject is KtObjectDeclaration) { + if (!classOrObject.isPublic) { + return true + } + classOrObject = classOrObject.containingClassOrObject + } + return false + } + + private fun KtProperty.isReferencedInherited(comment: String): Boolean { + var qualifiedName = nameAsSafeName.asString() + var classOrObject = containingClassOrObject + while (classOrObject is KtObjectDeclaration) { + qualifiedName = "${classOrObject.nameAsSafeName.asString()}.$qualifiedName" + classOrObject = classOrObject.containingClassOrObject + } + return comment.contains("[$qualifiedName]") + } + + private fun report(property: KtNamedDeclaration) { + report( + CodeSmell( + issue, + Entity.atName(property), + "The property ${property.nameAsSafeName} " + + "is non-public and should not be referenced from KDoc comments." + ) + ) + } +} diff --git a/detekt-rules-documentation/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/KDocReferencesNonPublicPropertySpec.kt b/detekt-rules-documentation/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/KDocReferencesNonPublicPropertySpec.kt new file mode 100644 index 00000000000..f877196a198 --- /dev/null +++ b/detekt-rules-documentation/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/documentation/KDocReferencesNonPublicPropertySpec.kt @@ -0,0 +1,134 @@ +package io.gitlab.arturbosch.detekt.rules.documentation + +import io.gitlab.arturbosch.detekt.test.compileAndLint +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class KDocReferencesNonPublicPropertySpec { + val subject = KDocReferencesNonPublicProperty() + + @Test + fun `reports referenced non-public properties`() { + val code = """ + /** + * Comment + * [prop1] - non-public property + * [prop2] - public property + */ + class Test { + private val nonReferencedProp = 0 + private val prop1 = 0 + val prop2 = 0 + } + """.trimIndent() + assertThat(subject.compileAndLint(code)).hasSize(1) + } + + @Test + fun `reports referenced non-public properties in private class`() { + val code = """ + /** + * Comment + * [prop1] - non-public property + * [prop2] - public property + */ + private class Test { + private val nonReferencedProp = 0 + private val prop1 = 0 + val prop2 = 0 + } + """.trimIndent() + assertThat(subject.compileAndLint(code)).hasSize(1) + } + + @Test + fun `reports referenced non-public properties in nested objects`() { + val code = """ + /** + * Comment + * [prop1] - non-public property + * [A.prop2] - non-public property + * [A.B.prop3] - non-public property + * [A.C.prop4] - non-public property + */ + class Test { + private val prop1 = 0 + + object A { + private val nonReferencedProp = 0 + private val prop2 = 0 + + private object B { + val prop3 = 0 + } + object C { + private val prop4 = 0 + } + } + } + """.trimIndent() + assertThat(subject.compileAndLint(code)).hasSize(4) + } + + @Test + fun `does not report properties with no KDoc`() { + val code = """ + class Test { + private val prop1 = 0 + val prop2 = 0 + } + """.trimIndent() + assertThat(subject.compileAndLint(code)).isEmpty() + } + + @Test + fun `does not report properties with empty comments`() { + val code = """ + /** + */ + class Test { + private val prop1 = 0 + val prop2 = 0 + } + """.trimIndent() + assertThat(subject.compileAndLint(code)).isEmpty() + } + + @Test + fun `does not report properties not enclosed in a class`() { + val code = """ + /** + * [prop1] + * [prop2] + */ + private val prop1 = 0 + val prop2 = 0 + """.trimIndent() + assertThat(subject.compileAndLint(code)).isEmpty() + } + + @Test + fun `does not report referenced public properties in nested objects`() { + val code = """ + /** + * Comment + * [prop1] - public property + * [A.B.prop2] - public property + * [C.prop3] - public property + */ + open class Test { + protected val prop1 = 0 + object A { + object B { + val nonReferencedProp = 0 + val prop2 = 0 + } + } + object C { + val prop3 = 0 + } + } + """.trimIndent() + assertThat(subject.compileAndLint(code)).isEmpty() + } +}