Skip to content

Commit

Permalink
New Rule: ObjectLiteralToLambda (#3599)
Browse files Browse the repository at this point in the history
  • Loading branch information
zmunm committed May 12, 2021
1 parent ba264d0 commit 4d80329
Show file tree
Hide file tree
Showing 6 changed files with 625 additions and 7 deletions.
2 changes: 2 additions & 0 deletions detekt-core/src/main/resources/default-detekt-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,8 @@ style:
active: true
NoTabs:
active: false
ObjectLiteralToLambda:
active: false
OptionalAbstractKeyword:
active: true
OptionalUnit:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.gradle.api.file.FileCollection
import java.io.File
import java.net.URLClassLoader

interface ClassLoaderCache {
fun interface ClassLoaderCache {

fun getOrCreate(classpath: FileCollection): URLClassLoader
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,13 @@ import io.gitlab.arturbosch.detekt.gradle.TestFileCollection
import io.gitlab.arturbosch.detekt.internal.ClassLoaderCache
import org.assertj.core.api.Assertions.assertThatCode
import org.gradle.api.GradleException
import org.gradle.api.file.FileCollection
import org.spekframework.spek2.Spek
import java.net.URLClassLoader

internal class DefaultCliInvokerTest : Spek({

test("catches ClassCastException and fails build") {
val stubbedCache = object : ClassLoaderCache {
override fun getOrCreate(classpath: FileCollection): URLClassLoader {
return URLClassLoader(emptyArray())
}
}
val stubbedCache = ClassLoaderCache { URLClassLoader(emptyArray()) }

assertThatCode {
DefaultCliInvoker(stubbedCache)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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.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.internal.RequiresTypeResolution
import io.gitlab.arturbosch.detekt.rules.isOverride
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.jetbrains.kotlin.psi.KtObjectLiteralExpression
import org.jetbrains.kotlin.psi.KtThisExpression
import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitClassReceiver
import org.jetbrains.kotlin.resolve.scopes.receivers.ReceiverValue
import org.jetbrains.kotlin.types.KotlinType

/**
* An anonymous object that does nothing other than the implementation of a single method
* can be used as a lambda.
*
* See https://kotlinlang.org/docs/java-interop.html#sam-conversions
* See https://kotlinlang.org/docs/fun-interfaces.html
*
* <noncompliant>
* object : Foo {
* override fun bar() {
* }
* }
* </noncompliant>
*
* <compliant>
* Foo {
* }
* </compliant>
*/
@RequiresTypeResolution
class ObjectLiteralToLambda(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
javaClass.simpleName,
Severity.Style,
"Report object literals that can be changed to lambdas.",
Debt.FIVE_MINS
)

private val KotlinType.couldBeSamInterface
get() = (constructor.declarationDescriptor as ClassDescriptor)
.isDefinitelyNotSamInterface
.not()

private fun KotlinType.singleSuperTypeOrNull(): KotlinType? =
constructor.supertypes.singleOrNull()

private fun KtObjectDeclaration.singleNamedMethodOrNull(): KtNamedFunction? =
declarations.singleOrNull() as? KtNamedFunction

private fun KtExpression.containsThisReference(descriptor: DeclarationDescriptor) =
anyDescendantOfType<KtThisExpression> { thisReference ->
bindingContext[BindingContext.REFERENCE_TARGET, thisReference.instanceReference] == descriptor
}

private fun KtExpression.containsOwnMethodCall(descriptor: DeclarationDescriptor) =
anyDescendantOfType<KtExpression> {
it.getResolvedCall(bindingContext)?.let { resolvedCall ->
resolvedCall.dispatchReceiver.isImplicitClassFor(descriptor) ||
resolvedCall.extensionReceiver.isImplicitClassFor(descriptor)
} == true
}

private fun ReceiverValue?.isImplicitClassFor(descriptor: DeclarationDescriptor) =
this is ImplicitClassReceiver && classDescriptor == descriptor

private fun KtExpression.containsMethodOf(declaration: KtObjectDeclaration): Boolean {
val objectDescriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, declaration]
?: return false

return containsThisReference(objectDescriptor) ||
containsOwnMethodCall(objectDescriptor)
}

private fun KtObjectDeclaration.hasConvertibleMethod(): Boolean {
val singleNamedMethod = singleNamedMethodOrNull()
val functionBody = singleNamedMethod?.bodyExpression ?: return false

return singleNamedMethod.isOverride() &&
!functionBody.containsMethodOf(this)
}

override fun visitObjectLiteralExpression(expression: KtObjectLiteralExpression) {
super.visitObjectLiteralExpression(expression)
if (bindingContext == BindingContext.EMPTY) return
val declaration = expression.objectDeclaration

if (
declaration.name == null &&
bindingContext.getType(expression)
?.singleSuperTypeOrNull()?.couldBeSamInterface == true &&
declaration.hasConvertibleMethod()
) {
report(CodeSmell(issue, Entity.from(expression), issue.description))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class StyleGuideProvider : DefaultRuleSetProvider {
UnnecessaryParentheses(config),
UnnecessaryInheritance(config),
UtilityClassWithPublicConstructor(config),
ObjectLiteralToLambda(config),
OptionalAbstractKeyword(config),
OptionalWhenBraces(config),
OptionalUnit(config),
Expand Down

0 comments on commit 4d80329

Please sign in to comment.