Skip to content

Commit

Permalink
Implement requiresTypeResoultion Rule
Browse files Browse the repository at this point in the history
  • Loading branch information
BraisGabin committed Aug 4, 2022
1 parent 0f8cb03 commit f6a5567
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 2 deletions.
15 changes: 15 additions & 0 deletions config/detekt/baseline.xml
Expand Up @@ -2,5 +2,20 @@
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>RequiresTypeResolution:FunctionOnlyReturningConstant.kt$FunctionOnlyReturningConstant : Rule</ID>
<ID>RequiresTypeResolution:ImplicitDefaultLocale.kt$ImplicitDefaultLocale : Rule</ID>
<ID>RequiresTypeResolution:InstanceOfCheckForException.kt$InstanceOfCheckForException : Rule</ID>
<ID>RequiresTypeResolution:LateinitUsage.kt$LateinitUsage : Rule</ID>
<ID>RequiresTypeResolution:LongParameterList.kt$LongParameterList : Rule</ID>
<ID>RequiresTypeResolution:MapGetWithNotNullAssertionOperator.kt$MapGetWithNotNullAssertionOperator : Rule</ID>
<ID>RequiresTypeResolution:MemberNameEqualsClassName.kt$MemberNameEqualsClassName : Rule</ID>
<ID>RequiresTypeResolution:OptionalUnit.kt$OptionalUnit : Rule</ID>
<ID>RequiresTypeResolution:SpreadOperator.kt$SpreadOperator : Rule</ID>
<ID>RequiresTypeResolution:UnnecessaryAbstractClass.kt$UnnecessaryAbstractClass : Rule</ID>
<ID>RequiresTypeResolution:UnusedImports.kt$UnusedImports : Rule</ID>
<ID>RequiresTypeResolution:UnusedPrivateMember.kt$UnusedPrivateMember : Rule</ID>
<ID>RequiresTypeResolution:UseCheckOrError.kt$UseCheckOrError : Rule</ID>
<ID>RequiresTypeResolution:UseDataClass.kt$UseDataClass : Rule</ID>
<ID>RequiresTypeResolution:UseRequire.kt$UseRequire : Rule</ID>
</CurrentIssues>
</SmellBaseline>
Expand Up @@ -7,6 +7,7 @@ import org.jetbrains.kotlin.psi.KtFile
* Can be used to combine different rules which do similar work like
* scanning the source code line by line to increase performance.
*/
@Suppress("RequiresTypeResolution")
abstract class MultiRule : BaseRule() {

abstract val rules: List<Rule>
Expand Down
4 changes: 4 additions & 0 deletions detekt-rules-ruleauthors/build.gradle.kts
Expand Up @@ -7,3 +7,7 @@ dependencies {
testImplementation(projects.detektTest)
testImplementation(libs.assertj)
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf("-Xcontext-receivers")
}
@@ -0,0 +1,81 @@
@file:Suppress("ForbiddenComment")

package io.gitlab.arturbosch.detekt.authors

import io.gitlab.arturbosch.detekt.api.BaseRule
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.ActiveByDefault
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import io.gitlab.arturbosch.detekt.rules.fqNameOrNull
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.types.typeUtil.supertypes
import kotlin.reflect.KClass

/**
* If a rule uses `bindingContext` should be annotated with `@RequiresTypeResolution`. And if it doesn't it shouldn't
* be annotated with it.
*/
@ActiveByDefault("1.22.0")
@RequiresTypeResolution
class RequiresTypeResolution(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
javaClass.simpleName,
Severity.Defect,
"`@RequiresTypeResolution` should be used if and only if `bindingContext` is used.",
Debt.FIVE_MINS
)

override fun visitClass(klass: KtClass) {
super.visitClass(klass)

if (klass.extendsFrom(BaseRule::class)) {
val usesBindingContext = usesBindingContext(klass)
val isAnnotatedWithRequiresTypeResolution = klass.isAnnotatedWith(RequiresTypeResolution::class)
if (usesBindingContext && !isAnnotatedWithRequiresTypeResolution) {
report(
CodeSmell(
issue,
Entity.atName(klass),
"`${klass.name}` uses `bindingContext` but is not annotated with `@RequiresTypeResolution`"
)
)
} else if (!usesBindingContext && isAnnotatedWithRequiresTypeResolution) {
report(
CodeSmell(
issue,
Entity.atName(klass),
"`${klass.name}` is annotated with `@RequiresTypeResolution` but doesn't use `bindingContext`"
)
)
}
}
}
}

private fun usesBindingContext(element: KtElement): Boolean {
return element.containingKtFile.text.contains("bindingContext", ignoreCase = false)
}

context(BaseRule) private inline fun <reified T : Any> KtClass.extendsFrom(kClass: KClass<T>): Boolean {
return bindingContext[BindingContext.CLASS, this]
?.defaultType
?.supertypes()
.orEmpty()
.any { it.fqNameOrNull()?.toString() == checkNotNull(kClass.qualifiedName) }
}

context(BaseRule) private inline fun <reified T : Any> KtClass.isAnnotatedWith(kClass: KClass<T>): Boolean {
return annotationEntries
.asSequence()
.mapNotNull { it.typeReference }
.mapNotNull { bindingContext[BindingContext.TYPE, it] }
.any { it.fqNameOrNull()?.toString() == checkNotNull(kClass.qualifiedName) }
}
Expand Up @@ -13,6 +13,10 @@ class RuleAuthorsProvider : RuleSetProvider {

override val ruleSetId: String = "detekt"

@Suppress("UseEmptyCounterpart")
override fun instance(config: Config) = RuleSet(ruleSetId, listOf())
override fun instance(config: Config) = RuleSet(
ruleSetId,
listOf(
RequiresTypeResolution(config),
)
)
}
2 changes: 2 additions & 0 deletions detekt-rules-ruleauthors/src/main/resources/config/config.yml
@@ -1,2 +1,4 @@
detekt:
active: true
RequiresTypeResolution:
active: true
@@ -0,0 +1,92 @@
package io.gitlab.arturbosch.detekt.authors

import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest
import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext
import org.assertj.core.api.Assertions.assertThat
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.junit.jupiter.api.Test

@KotlinCoreEnvironmentTest
internal class RequiresTypeResolutionSpec(private val env: KotlinCoreEnvironment) {

private val rule = RequiresTypeResolution()

@Test
fun `should not report classes that doesn't extend from BaseRule`() {
val code = """
class A {
val issue: Int = error("bindingContext")
}
"""
val findings = rule.compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}

@Test
fun `should report Rules that use bindingContext and are not annotated`() {
val code = """
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Rule
class B(config: Config) : Rule(config) {
override val issue = error("I don't care")
private fun asdf() {
bindingContext
}
}
"""
val findings = rule.compileAndLintWithContext(env, code)
assertThat(findings).hasSize(1)
}

@Test
fun `should not report Rules that doesn't use bindingContext and are not annotated`() {
val code = """
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Rule
class C(config: Config) : Rule(config) {
override val issue = error("I don't care")
}
"""
val findings = rule.compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}

@Test
fun `should not report Rules that use bindingContext and are annotated`() {
val code = """
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
@RequiresTypeResolution
class D(config: Config) : Rule(config) {
override val issue = error("I don't care")
private fun asdf() {
bindingContext
}
}
"""
val findings = rule.compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}

@Test
fun `should not report Rules that doesn't use bindingContext and are annotated`() {
val code = """
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
@RequiresTypeResolution
class E(config: Config) : Rule(config) {
override val issue = error("I don't care")
}
"""
val findings = rule.compileAndLintWithContext(env, code)
assertThat(findings).hasSize(1)
}
}

0 comments on commit f6a5567

Please sign in to comment.