
io.gitlab.arturbosch.detekt.rules.style.UnusedPrivateMember.kt Maven / Gradle / Ivy
package io.gitlab.arturbosch.detekt.rules.style
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.DetektVisitor
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 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 io.gitlab.arturbosch.detekt.rules.isActual
import io.gitlab.arturbosch.detekt.rules.isExpect
import io.gitlab.arturbosch.detekt.rules.isExternal
import io.gitlab.arturbosch.detekt.rules.isMainFunction
import io.gitlab.arturbosch.detekt.rules.isOpen
import io.gitlab.arturbosch.detekt.rules.isOperator
import io.gitlab.arturbosch.detekt.rules.isOverride
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.lexer.KtSingleValueToken
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtArrayAccessExpression
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtOperationReferenceExpression
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtPrimaryConstructor
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtPropertyDelegate
import org.jetbrains.kotlin.psi.KtReferenceExpression
import org.jetbrains.kotlin.psi.KtSecondaryConstructor
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.psi.psiUtil.isPrivate
import org.jetbrains.kotlin.psi.psiUtil.isProtected
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.types.expressions.OperatorConventions
import org.jetbrains.kotlin.util.OperatorNameConventions
private const val ARRAY_GET_METHOD_NAME = "get"
/**
* Reports unused private properties, function parameters and functions.
* If these private elements are unused they should be removed. Otherwise this dead code
* can lead to confusion and potential bugs.
*/
@ActiveByDefault(since = "1.16.0")
class UnusedPrivateMember(config: Config = Config.empty) : Rule(config) {
override val defaultRuleIdAliases: Set = setOf("UNUSED_VARIABLE", "UNUSED_PARAMETER", "unused")
override val issue: Issue = Issue(
"UnusedPrivateMember",
Severity.Maintainability,
"Private member is unused.",
Debt.FIVE_MINS
)
@Configuration("unused private member names matching this regex are ignored")
private val allowedNames: Regex by config("(_|ignored|expected|serialVersionUID)", String::toRegex)
override fun visit(root: KtFile) {
super.visit(root)
root.acceptUnusedMemberVisitor(UnusedFunctionVisitor(allowedNames, bindingContext))
root.acceptUnusedMemberVisitor(UnusedParameterVisitor(allowedNames))
root.acceptUnusedMemberVisitor(UnusedPropertyVisitor(allowedNames))
}
private fun KtFile.acceptUnusedMemberVisitor(visitor: UnusedMemberVisitor) {
accept(visitor)
visitor.getUnusedReports(issue).forEach { report(it) }
}
}
private abstract class UnusedMemberVisitor(protected val allowedNames: Regex) : DetektVisitor() {
abstract fun getUnusedReports(issue: Issue): List
}
private class UnusedFunctionVisitor(
allowedNames: Regex,
private val bindingContext: BindingContext
) : UnusedMemberVisitor(allowedNames) {
private val functionDeclarations = mutableMapOf>()
private val functionReferences = mutableMapOf>()
private val propertyDelegates = mutableListOf()
override fun getUnusedReports(issue: Issue): List {
val propertyDelegateResultingDescriptors by lazy(LazyThreadSafetyMode.NONE) {
propertyDelegates.flatMap { it.resultingDescriptors() }
}
return functionDeclarations.flatMap { (functionName, functions) ->
val isOperator = functions.any { it.isOperator() }
val references = functionReferences[functionName].orEmpty()
val unusedFunctions = when {
(functions.size > 1 || isOperator) && bindingContext != BindingContext.EMPTY -> {
val functionNameAsName = Name.identifier(functionName)
val referencesViaOperator = if (isOperator) {
val operatorToken = OperatorConventions.getOperationSymbolForName(functionNameAsName)
val operatorValue = (operatorToken as? KtSingleValueToken)?.value
val directReferences = operatorValue?.let { functionReferences[it] }.orEmpty()
val assignmentReferences = when (operatorToken) {
KtTokens.PLUS,
KtTokens.MINUS,
KtTokens.MUL,
KtTokens.DIV,
KtTokens.PERC -> operatorValue?.let { functionReferences["$it="] }.orEmpty()
else -> emptyList()
}
directReferences + assignmentReferences
} else {
emptyList()
}
val referenceDescriptors = (references + referencesViaOperator)
.mapNotNull { it.getResolvedCall(bindingContext)?.resultingDescriptor }
.map { it.original }
.let {
if (functionNameAsName in OperatorNameConventions.DELEGATED_PROPERTY_OPERATORS) {
it + propertyDelegateResultingDescriptors
} else {
it
}
}
functions.filterNot {
bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, it] in referenceDescriptors
}
}
references.isEmpty() -> functions
else -> emptyList()
}
unusedFunctions.map {
CodeSmell(issue, Entity.from(it), "Private function $functionName is unused.")
}
}
}
override fun visitNamedFunction(function: KtNamedFunction) {
if (!isDeclaredInsideAnInterface(function) && function.isPrivate()) {
collectFunction(function)
}
super.visitNamedFunction(function)
}
private fun isDeclaredInsideAnInterface(function: KtNamedFunction) =
function.getStrictParentOfType()?.isInterface() == true
private fun collectFunction(function: KtNamedFunction) {
val name = function.nameAsSafeName.identifier
if (!allowedNames.matches(name)) {
functionDeclarations.getOrPut(name) { mutableListOf() }.add(function)
}
}
private fun KtPropertyDelegate.resultingDescriptors(): List {
val property = this.parent as? KtProperty ?: return emptyList()
val propertyDescriptor =
bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, property] as? PropertyDescriptor
return listOfNotNull(propertyDescriptor?.getter, propertyDescriptor?.setter).mapNotNull {
bindingContext[BindingContext.DELEGATED_PROPERTY_RESOLVED_CALL, it]?.resultingDescriptor
}
}
override fun visitPropertyDelegate(delegate: KtPropertyDelegate) {
super.visitPropertyDelegate(delegate)
propertyDelegates.add(delegate)
}
/*
* We need to collect all private function declarations and references to these functions
* for the whole file as Kotlin allows to access private and internal object declarations
* from everywhere in the file.
*/
override fun visitReferenceExpression(expression: KtReferenceExpression) {
super.visitReferenceExpression(expression)
val name = when (expression) {
is KtOperationReferenceExpression -> expression.getReferencedName()
is KtNameReferenceExpression -> expression.getReferencedName()
is KtArrayAccessExpression -> ARRAY_GET_METHOD_NAME
else -> null
} ?: return
functionReferences.getOrPut(name) { mutableListOf() }.add(expression)
}
}
private class UnusedParameterVisitor(allowedNames: Regex) : UnusedMemberVisitor(allowedNames) {
private var unusedParameters: MutableSet = mutableSetOf()
override fun getUnusedReports(issue: Issue): List {
return unusedParameters.map {
CodeSmell(issue, Entity.from(it), "Function parameter ${it.nameAsSafeName.identifier} is unused.")
}
}
override fun visitClassOrObject(klassOrObject: KtClassOrObject) {
if (klassOrObject.isExpect()) return
super.visitClassOrObject(klassOrObject)
}
override fun visitClass(klass: KtClass) {
if (klass.isInterface()) return
super.visitClass(klass)
}
override fun visitNamedFunction(function: KtNamedFunction) {
if (!function.isRelevant()) {
return
}
collectParameters(function)
super.visitNamedFunction(function)
}
private fun collectParameters(function: KtNamedFunction) {
val parameters = mutableMapOf()
function.valueParameterList?.parameters?.forEach { parameter ->
val name = parameter.nameAsSafeName.identifier
if (!allowedNames.matches(name)) {
parameters[name] = parameter
}
}
function.accept(object : DetektVisitor() {
override fun visitProperty(property: KtProperty) {
if (property.isLocal) {
val name = property.nameAsSafeName.identifier
parameters.remove(name)
}
super.visitProperty(property)
}
override fun visitReferenceExpression(expression: KtReferenceExpression) {
parameters.remove(expression.text)
super.visitReferenceExpression(expression)
}
})
unusedParameters.addAll(parameters.values)
}
private fun KtNamedFunction.isRelevant() = !isAllowedToHaveUnusedParameters()
private fun KtNamedFunction.isAllowedToHaveUnusedParameters() =
isAbstract() || isOpen() || isOverride() || isOperator() || isMainFunction() || isExternal() ||
isExpect() || isActual() || isProtected()
}
private class UnusedPropertyVisitor(allowedNames: Regex) : UnusedMemberVisitor(allowedNames) {
private val properties = mutableSetOf()
private val nameAccesses = mutableSetOf()
override fun getUnusedReports(issue: Issue): List {
return properties
.filter { it.nameAsSafeName.identifier !in nameAccesses }
.map {
CodeSmell(
issue,
Entity.from(it),
"Private property ${it.nameAsSafeName.identifier} is unused."
)
}
}
override fun visitParameter(parameter: KtParameter) {
super.visitParameter(parameter)
if (parameter.isLoopParameter) {
val destructuringDeclaration = parameter.destructuringDeclaration
if (destructuringDeclaration != null) {
for (variable in destructuringDeclaration.entries) {
maybeAddUnusedProperty(variable)
}
} else {
maybeAddUnusedProperty(parameter)
}
}
}
override fun visitPrimaryConstructor(constructor: KtPrimaryConstructor) {
super.visitPrimaryConstructor(constructor)
constructor.valueParameters
.filter {
(it.isPrivate() || (!it.hasValOrVar() && !constructor.isActual())) &&
it.containingClassOrObject?.isExpect() == false
}
.forEach { maybeAddUnusedProperty(it) }
}
override fun visitSecondaryConstructor(constructor: KtSecondaryConstructor) {
super.visitSecondaryConstructor(constructor)
constructor.valueParameters.forEach { maybeAddUnusedProperty(it) }
}
private fun maybeAddUnusedProperty(it: KtNamedDeclaration) {
if (!allowedNames.matches(it.nameAsSafeName.identifier)) {
properties.add(it)
}
}
override fun visitProperty(property: KtProperty) {
if (property.isPrivate() && property.isMemberOrTopLevel() || property.isLocal) {
maybeAddUnusedProperty(property)
}
super.visitProperty(property)
}
private fun KtProperty.isMemberOrTopLevel() = isMember || isTopLevel
override fun visitReferenceExpression(expression: KtReferenceExpression) {
nameAccesses.add(expression.text.removeSurrounding("`"))
super.visitReferenceExpression(expression)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy