All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.gitlab.arturbosch.detekt.rules.style.UnnecessaryAbstractClass.kt Maven / Gradle / Ivy

package io.gitlab.arturbosch.detekt.rules.style

import io.gitlab.arturbosch.detekt.api.AnnotationExcluder
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
import io.gitlab.arturbosch.detekt.api.config
import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault
import io.gitlab.arturbosch.detekt.api.internal.Configuration
import io.gitlab.arturbosch.detekt.rules.isAbstract
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.MemberDescriptor
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.psiUtil.isAbstract
import org.jetbrains.kotlin.resolve.BindingContext

/**
 * This rule inspects `abstract` classes. In case an `abstract class` does not have any concrete members it should be
 * refactored into an interface. Abstract classes which do not define any `abstract` members should instead be
 * refactored into concrete classes.
 *
 * 
 * abstract class OnlyAbstractMembersInAbstractClass { // violation: no concrete members
 *
 *     abstract val i: Int
 *     abstract fun f()
 * }
 *
 * abstract class OnlyConcreteMembersInAbstractClass { // violation: no abstract members
 *
 *     val i: Int = 0
 *     fun f() { }
 * }
 * 
 */
@ActiveByDefault(since = "1.2.0")
class UnnecessaryAbstractClass(config: Config = Config.empty) : Rule(config) {

    private val noConcreteMember = "An abstract class without a concrete member can be refactored to an interface."
    private val noAbstractMember = "An abstract class without an abstract member can be refactored to a concrete class."

    override val issue =
        Issue(
            "UnnecessaryAbstractClass",
            Severity.Style,
            "An abstract class is unnecessary and can be refactored. " +
                "An abstract class should have both abstract and concrete properties or functions. " +
                noConcreteMember + " " + noAbstractMember,
            Debt.FIVE_MINS
        )

    @Configuration("Allows you to provide a list of annotations that disable this check.")
    private val excludeAnnotatedClasses: List by config(listOf("dagger.Module")) { classes ->
        classes.map { it.removePrefix("*").removeSuffix("*") }
    }

    private lateinit var annotationExcluder: AnnotationExcluder

    override fun visitKtFile(file: KtFile) {
        annotationExcluder = AnnotationExcluder(file, excludeAnnotatedClasses)
        super.visitKtFile(file)
    }

    override fun visitClass(klass: KtClass) {
        if (!klass.isInterface() && klass.isAbstract()) {
            val body = klass.body
            if (body != null) {
                val namedMembers = body.children.filter { it is KtProperty || it is KtNamedFunction }
                val namedClassMembers = NamedClassMembers(klass, namedMembers)
                namedClassMembers.detectAbstractAndConcreteType()
            } else if (klass.superTypeListEntries.isEmpty() && !hasNoConstructorParameter(klass)) {
                report(CodeSmell(issue, Entity.from(klass), noAbstractMember), klass)
            }
        }
        super.visitClass(klass)
    }

    private fun report(finding: Finding, klass: KtClass) {
        if (!annotationExcluder.shouldExclude(klass.annotationEntries)) {
            report(finding)
        }
    }

    private fun hasNoConstructorParameter(klass: KtClass): Boolean {
        val primaryConstructor = klass.primaryConstructor
        return primaryConstructor == null || !primaryConstructor.valueParameters.any()
    }

    private inner class NamedClassMembers(val klass: KtClass, val namedMembers: List) {

        fun detectAbstractAndConcreteType() {
            val firstAbstractMemberIndex = indexOfFirstMember(true)
            if (firstAbstractMemberIndex == -1 && !hasInheritedMember(true)) {
                report(CodeSmell(issue, Entity.from(klass), noAbstractMember), klass)
            } else if (isAbstractClassWithoutConcreteMembers(firstAbstractMemberIndex) && !hasInheritedMember(false)) {
                report(CodeSmell(issue, Entity.from(klass), noConcreteMember), klass)
            }
        }

        private fun indexOfFirstMember(isAbstract: Boolean, members: List = this.namedMembers) =
            members.indexOfFirst { it is KtNamedDeclaration && it.isAbstract() == isAbstract }

        private fun isAbstractClassWithoutConcreteMembers(indexOfFirstAbstractMember: Int) =
            indexOfFirstAbstractMember == 0 && hasNoConcreteMemberLeft() && hasNoConstructorParameter(klass)

        private fun hasNoConcreteMemberLeft() = indexOfFirstMember(false, namedMembers.drop(1)) == -1

        private fun hasInheritedMember(isAbstract: Boolean): Boolean {
            return when {
                klass.superTypeListEntries.isEmpty() -> false
                bindingContext == BindingContext.EMPTY -> true
                else -> {
                    val descriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, klass] as? ClassDescriptor
                    descriptor?.unsubstitutedMemberScope?.getContributedDescriptors().orEmpty().any {
                        (it as? MemberDescriptor)?.modality == Modality.ABSTRACT == isAbstract
                    }
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy