Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UnnecessaryAbstractClass: fix false positive when the abstract class has properties in the primary constructor #4628

Merged
merged 2 commits into from Mar 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -5,7 +5,6 @@ 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.Finding
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
Expand All @@ -15,12 +14,11 @@ import io.gitlab.arturbosch.detekt.api.internal.Configuration
import io.gitlab.arturbosch.detekt.rules.isAbstract
import io.gitlab.arturbosch.detekt.rules.isInternal
import io.gitlab.arturbosch.detekt.rules.isProtected
import org.jetbrains.kotlin.builtins.StandardNames.FqNames.list
import org.jetbrains.kotlin.descriptors.MemberDescriptor
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.psiUtil.isAbstract
import org.jetbrains.kotlin.resolve.BindingContext

Expand Down Expand Up @@ -77,53 +75,47 @@ class UnnecessaryAbstractClass(config: Config = Config.empty) : Rule(config) {
}

override fun visitClass(klass: KtClass) {
if (!klass.isInterface() && klass.isAbstract()) {
val namedMembers = klass.body?.children.orEmpty().filterIsInstance<KtNamedDeclaration>()
if (namedMembers.isNotEmpty()) {
NamedClassMembers(klass, namedMembers).detectAbstractAndConcreteType()
} else if (!klass.hasConstructorParameter()) {
report(CodeSmell(issue, Entity.from(klass), noConcreteMember), klass)
} else {
report(CodeSmell(issue, Entity.from(klass), noAbstractMember), klass)
}
}
klass.check()
super.visitClass(klass)
}

private fun report(finding: Finding, klass: KtClass) {
if (!annotationExcluder.shouldExclude(klass.annotationEntries)) {
report(finding)
private fun KtClass.check() {
if (annotationExcluder.shouldExclude(annotationEntries) || isInterface() || !isAbstract()) return
val members = members()
when {
members.isNotEmpty() -> {
val (abstractMembers, concreteMembers) = members.partition { it.isAbstract() }
if (abstractMembers.isEmpty() && !hasInheritedMember(true)) {
report(CodeSmell(issue, Entity.from(this), noAbstractMember))
return
}
if (abstractMembers.any { it.isInternal() || it.isProtected() } || hasConstructorParameter()) {
return
}
if (concreteMembers.isEmpty() && !hasInheritedMember(false)) {
report(CodeSmell(issue, Entity.from(this), noConcreteMember))
}
Comment on lines +87 to +97
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The previous detectAbstractAndConcreteType() seems cleaner to me.

}
!hasConstructorParameter() ->
report(CodeSmell(issue, Entity.from(this), noConcreteMember))
else ->
report(CodeSmell(issue, Entity.from(this), noAbstractMember))
}
}

private fun KtClass.hasConstructorParameter() = primaryConstructor?.valueParameters?.isNotEmpty() == true

private inner class NamedClassMembers(val klass: KtClass, val members: List<KtNamedDeclaration>) {
private fun KtClass.members() = body?.children?.filterIsInstance<KtCallableDeclaration>().orEmpty() +
primaryConstructor?.valueParameters?.filter { it.hasValOrVar() }.orEmpty()
Comment on lines +106 to +107
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future readers: This line of change is the fix.


fun detectAbstractAndConcreteType() {
val (abstractMembers, concreteMembers) = members.partition { it.isAbstract() }

if (abstractMembers.isEmpty() && !hasInheritedMember(true)) {
report(CodeSmell(issue, Entity.from(klass), noAbstractMember), klass)
return
}

if (abstractMembers.any { it.isInternal() || it.isProtected() } || klass.hasConstructorParameter()) return

if (concreteMembers.isEmpty() && !hasInheritedMember(false)) {
report(CodeSmell(issue, Entity.from(klass), noConcreteMember), klass)
}
}
private fun KtClass.hasConstructorParameter() = primaryConstructor?.valueParameters?.isNotEmpty() == true

private fun hasInheritedMember(isAbstract: Boolean): Boolean {
return when {
klass.superTypeListEntries.isEmpty() -> false
bindingContext == BindingContext.EMPTY -> true
else -> {
val descriptor = bindingContext[BindingContext.CLASS, klass]
descriptor?.unsubstitutedMemberScope?.getContributedDescriptors().orEmpty().any {
(it as? MemberDescriptor)?.modality == Modality.ABSTRACT == isAbstract
}
private fun KtClass.hasInheritedMember(isAbstract: Boolean): Boolean {
return when {
superTypeListEntries.isEmpty() -> false
bindingContext == BindingContext.EMPTY -> true
else -> {
val descriptor = bindingContext[BindingContext.CLASS, this]
descriptor?.unsubstitutedMemberScope?.getContributedDescriptors().orEmpty().any {
(it as? MemberDescriptor)?.modality == Modality.ABSTRACT == isAbstract
}
}
}
Expand Down
Expand Up @@ -246,6 +246,16 @@ class UnnecessaryAbstractClassSpec : Spek({
"""
assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}

it("does not report abstract classes with properties in the primary constructor") {
val code = """
interface I {
fun test(): Int
}
abstract class Test(val x: Int) : I
"""
assertThat(subject.compileAndLintWithContext(env, code)).isEmpty()
}
}
}
})
Expand Down