Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Report KDoc comments that refer to non-public properties of a class (#…
…4768) * Report KDoc comments that refer to encapsulated properties of a class New rule to address the issue of breaking encapsulation principle when KDoc comments have references to encapsulated (private) properties. Clients do not need to know the implementation details. * Rename rule to 'KDocReferencesNonPublicProperty' and fix naming * Set 'protected' modifier as allowed * Improve tests * Fix rule documentation
- Loading branch information
1 parent
a066c7f
commit 4dd9ac4
Showing
5 changed files
with
241 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
...kotlin/io/gitlab/arturbosch/detekt/rules/documentation/KDocReferencesNonPublicProperty.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
* | ||
* <noncompliant> | ||
* /** | ||
* * Comment | ||
* * [prop1] - non-public property | ||
* * [prop2] - public property | ||
* */ | ||
* class Test { | ||
* private val prop1 = 0 | ||
* val prop2 = 0 | ||
* } | ||
* </noncompliant> | ||
* | ||
* <compliant> | ||
* /** | ||
* * Comment | ||
* * [prop2] - public property | ||
* */ | ||
* class Test { | ||
* private val prop1 = 0 | ||
* val prop2 = 0 | ||
* } | ||
* </compliant> | ||
* | ||
*/ | ||
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<KtClass>() | ||
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." | ||
) | ||
) | ||
} | ||
} |
134 changes: 134 additions & 0 deletions
134
...in/io/gitlab/arturbosch/detekt/rules/documentation/KDocReferencesNonPublicPropertySpec.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
} |