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

io.github.mkohm.detekt.hint.rules.UseCompositionInsteadOfInheritance.kt Maven / Gradle / Ivy

package io.github.mkohm.detekt.hint.rules

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.js.resolve.diagnostics.findPsi
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.psiUtil.getSuperNames
import org.jetbrains.kotlin.psi.psiUtil.isPublic
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes.ENUM_ENTRY
import org.jetbrains.kotlin.psi.stubs.elements.KtStubElementTypes.SUPER_TYPE_CALL_ENTRY
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe

/**
 * A rule suggesting the use of composition instead of inheritance. It will help you test for Liskov Substitution.
 *
 * The rule will fire every time inheritance is introduced, unless you derive from a class that exists in another package.
 * This will reduce the amount of warnings created where the framework or library you are working with are forcing you to introduce inheritance.
 *
 * Remember to configure this rule correctly by adding:
 * "yourUniquePackageName" : "io.github.mkohm"
 * replacing "io.github.com" with your unique package name.
 */
class UseCompositionInsteadOfInheritance(config: Config = Config.empty) : Rule(config) {
    override val issue = Issue(
        javaClass.simpleName,
        Severity.CodeSmell,
        "This rule reports a file using inheritance.",
        Debt.TWENTY_MINS
    )

    override fun visitClass(klass: KtClass) {
        super.visitClass(klass)
        val uniquePackageName = valueOrNull("yourUniquePackageName") ?: error("You must specify your unique package name in the configuration for rule 'UseCompositionInsteadOfInheritance'")

        if (klass.getSuperNames().isEmpty() || noSuperTypeCallEntry(klass) || isEnumEntry(klass)) return

        val superClass =
            klass.superTypeListEntries[0].getResolvedCall(bindingContext)?.resultingDescriptor?.containingDeclaration
                ?: return

        val superClassFqName = superClass.fqNameSafe.toString()
        val isLocalInheritanceUsed = superClassFqName.contains(uniquePackageName)

        if (isLocalInheritanceUsed) {

            val functions = (superClass.findPsi() as KtClass).body?.functions
            val publicFunctions = functions?.filter { it.isPublic }

            val toPrint = if (publicFunctions.isNullOrEmpty()) {
                "empty public interface"
            } else {
                publicFunctions.map { it.name }.reduceRight { ktNamedFunction, acc -> "$acc, $ktNamedFunction" }
            }

            val typeA = superClass.name
            val typeB = klass.name
            val message =
                "The class `${klass.name}` is using inheritance, consider using composition instead.\n\nDoes `${typeB}` want to expose the complete interface (`$toPrint`) of `${typeA}` such that `${typeB}` can be used where `${typeA}` is expected? Indicates __inheritance__.\n\nDoes `${typeB}` want only some/part of the behavior exposed by `${typeA}`? Indicates __Composition__."

            report(CodeSmell(issue, Entity.from(klass), message))
        }
    }

    private fun isEnumEntry(klass: KtClass) = klass.elementType == ENUM_ENTRY

    private fun noSuperTypeCallEntry(klass: KtClass) =
        (klass.superTypeListEntries[0].elementType != SUPER_TYPE_CALL_ENTRY)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy