Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve binding context management #5130

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion build-logic/src/main/kotlin/releasing.gradle.kts
Expand Up @@ -38,7 +38,8 @@ project.afterEvaluate {
files(
cliBuildDir.resolve("libs/detekt-cli-${project.version}-all.jar"),
cliBuildDir.resolve("distributions/detekt-cli-${project.version}.zip"),
project(":detekt-formatting").buildDir.resolve("libs/detekt-formatting-${project.version}.jar")
project(":detekt-formatting").buildDir.resolve("libs/detekt-formatting-${project.version}.jar"),
project(":detekt-authors").buildDir.resolve("libs/detekt-authors-${project.version}.jar")
)
)
}
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Expand Up @@ -27,6 +27,7 @@ allprojects {
dependencies {
detekt(project(":detekt-cli"))
detektPlugins(project(":detekt-formatting"))
detektPlugins(project(":detekt-authors"))
}

tasks.withType<Detekt>().configureEach {
Expand Down
9 changes: 9 additions & 0 deletions detekt-authors/build.gradle.kts
@@ -0,0 +1,9 @@
plugins {
id("module")
}

dependencies {
compileOnly(projects.detektApi)
testImplementation(projects.detektTest)
testImplementation(libs.assertj)
}
@@ -0,0 +1,22 @@
package io.gitlab.arturbosch.detekt.authors

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.RuleSetProvider
import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault

/**
* The authors ruleset provides rules that ensures good practices when writing detekt rules
*/
@ActiveByDefault("1.22.0")
class AuthorsProvider : RuleSetProvider {

override val ruleSetId: String = "detekt"

override fun instance(config: Config) = RuleSet(
ruleSetId,
listOf(
RequiresTypeResolutionRulesDoesNotRunWithoutAContext(config),
)
)
}
@@ -0,0 +1,79 @@
@file:Suppress("ForbiddenComment")

package io.gitlab.arturbosch.detekt.authors

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 org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.psiUtil.findFunctionByName

/**
* A rule annotated with RequiresTypeResolution should override `visitCondition` and return false if the provided
* `bindingContext` is empty.
*
* <noncompliant>
* @@RequiresTypeResolution
* class MyRule(config: Config = Config.empty) : Rule(config) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My 2 cents here: shouldn't we just instead have a Rule subclass or a parameter to the Rule constructor, or a property inside Rule that users can override?

While a generic visitCondition works, it does not prevent from rules accidentally using TR and forgetting to add the @RequiresTypeResolution annotation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While a generic visitCondition works, it does not prevent from rules accidentally using TR and forgetting to add the @RequiresTypeResolution annotation.

You are right. That is why #5148 is still needed

* override fun visitKtFile(file: KtFile) =
* if (bindingContext == BindingContext.EMPTY) return
* ...
* }
* }
* </noncompliant>
*
* <compliant>
* @@RequiresTypeResolution
* class MyRule(config: Config = Config.empty) : Rule(config) {
* override fun visitCondition(root: KtFile) =
* bindingContext != BindingContext.EMPTY && super.visitCondition(root)
* }
* }
* </compliant>
Fixed Show fixed Hide fixed
*/
@ActiveByDefault("1.22.0")
class RequiresTypeResolutionRulesDoesNotRunWithoutAContext(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
javaClass.simpleName,
Severity.Style,
"Rules marked as RequiresTypeResolution shouldn't run without `bindingContext`.",
Debt.FIVE_MINS
)

override fun visitClass(klass: KtClass) {
if (klass.annotationEntries.map { it.text }.contains("@RequiresTypeResolution")) {
val function: KtNamedFunction? = klass.findFunctionByName("visitCondition") as? KtNamedFunction

if (function == null) {
report(
CodeSmell(
issue,
Entity.atName(klass),
message
)
)
return
}

if (function.bodyExpression?.text != requiredImplementation) {
report(
CodeSmell(
issue,
Entity.atName(function),
message
)
)
}
}
}
}

private const val requiredImplementation = "bindingContext != BindingContext.EMPTY && super.visitCondition(root)"
private const val message = "This rule is annotated with `RequiresTypeResolution` but `visitCondition` " +
"implementation is not `$requiredImplementation`"
@@ -0,0 +1 @@
io.gitlab.arturbosch.detekt.authors.AuthorsProvider
4 changes: 4 additions & 0 deletions detekt-authors/src/main/resources/config/config.yml
@@ -0,0 +1,4 @@
detekt:
active: true
RequiresTypeResolutionRulesDoesNotRunWithoutAContext:
active: true
@@ -0,0 +1,80 @@
package io.gitlab.arturbosch.detekt.authors

import io.gitlab.arturbosch.detekt.test.assertThat
import io.gitlab.arturbosch.detekt.test.compileAndLint
import org.junit.jupiter.api.Test

class RequiresTypeResolutionRulesDoesNotRunWithoutAContextSpec {

private val rule = RequiresTypeResolutionRulesDoesNotRunWithoutAContext()

@Test
fun `should not report no annotated classes`() {
val code = """
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Rule

class A(config: Config = Config.empty) : Rule(config) {
override val issue = error("I don't care")
}
"""
val findings = rule.compileAndLint(code)
assertThat(findings).isEmpty()
}

@Test
fun `should report annotated classes without visitCondition()`() {
val code = """
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Rule

@RequiresTypeResolution
class A(config: Config = Config.empty) : Rule(config) {
override val issue = error("I don't care")
}
"""
val findings = rule.compileAndLint(code)
assertThat(findings).hasSize(1)
}

@Test
fun `should report annotated classes with visitCondition() but no check for bindingContext`() {
val code = """
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Rule
import org.jetbrains.kotlin.psi.KtFile

@RequiresTypeResolution
class A(config: Config = Config.empty) : Rule(config) {
override val issue = error("I don't care")

override fun visitCondition(root: KtFile) = super.visitCondition(root)
}
"""
val findings = rule.compileAndLint(code)
assertThat(findings).hasSize(1)
}

@Test
fun `should not report annotated classes with visitCondition() and check for bindingContext first line`() {
val code = """
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Rule
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.BindingContext

@RequiresTypeResolution
class A(config: Config = Config.empty) : Rule(config) {
override val issue = error("I don't care")

override fun visitCondition(root: KtFile) =
bindingContext != BindingContext.EMPTY && super.visitCondition(root)
}
"""
val findings = rule.compileAndLint(code)
assertThat(findings).isEmpty()
}
}
10 changes: 9 additions & 1 deletion detekt-generator/build.gradle.kts
Expand Up @@ -22,6 +22,8 @@ val configDir = "${rootProject.rootDir}/detekt-core/src/main/resources"
val cliOptionsFile = "${rootProject.rootDir}/website/docs/gettingstarted/_cli-options.md"
val defaultConfigFile = "$configDir/default-detekt-config.yml"
val deprecationFile = "$configDir/deprecation.properties"
val formattingConfigFile = "${rootProject.rootDir}/detekt-formatting/src/main/resources/config/config.yml"
val authorsConfigFile = "${rootProject.rootDir}/detekt-authors/src/main/resources/config/config.yml"

val ruleModules = rootProject.subprojects
.filter { "rules" in it.name }
Expand All @@ -36,13 +38,16 @@ val generateDocumentation by tasks.registering(JavaExec::class) {

inputs.files(
ruleModules.map { fileTree(it) },
fileTree("${rootProject.rootDir}/detekt-authors/src/main/kotlin"),
fileTree("${rootProject.rootDir}/detekt-formatting/src/main/kotlin"),
file("${rootProject.rootDir}/detekt-generator/build/libs/detekt-generator-${Versions.DETEKT}-all.jar"),
)

outputs.files(
fileTree(documentationDir),
file(defaultConfigFile),
file(formattingConfigFile),
file(authorsConfigFile),
file(deprecationFile),
file(cliOptionsFile),
)
Expand All @@ -55,7 +60,10 @@ val generateDocumentation by tasks.registering(JavaExec::class) {
mainClass.set("io.gitlab.arturbosch.detekt.generator.Main")
args = listOf(
"--input",
ruleModules.plus("${rootProject.rootDir}/detekt-formatting/src/main/kotlin").joinToString(","),
ruleModules
.plus("${rootProject.rootDir}/detekt-authors/src/main/kotlin")
.plus("${rootProject.rootDir}/detekt-formatting/src/main/kotlin")
.joinToString(","),
"--documentation",
documentationDir,
"--config",
Expand Down
Expand Up @@ -24,7 +24,7 @@ class DetektPrinter(private val arguments: GeneratorArgs) {
}
}
yamlWriter.write(arguments.configPath, "default-detekt-config") {
ConfigPrinter.print(pages.filterNot { it.ruleSet.name == "formatting" })
ConfigPrinter.print(pages.filterNot { it.ruleSet.name == "formatting" || it.ruleSet.name == "detekt" })
}
propertiesWriter.write(arguments.configPath, "deprecation") {
// We intentionally not filter for "formatting" as we want to be able to deprecate
Expand All @@ -36,6 +36,11 @@ class DetektPrinter(private val arguments: GeneratorArgs) {
printRuleSetPage(pages.first { it.ruleSet.name == "formatting" })
}
}
yamlWriter.write(Paths.get("../detekt-authors/src/main/resources/config"), "config") {
yaml {
printRuleSetPage(pages.first { it.ruleSet.name == "detekt" })
}
}
}

private fun markdownHeader(ruleSet: String): String {
Expand Down
Expand Up @@ -11,6 +11,7 @@ import io.gitlab.arturbosch.detekt.api.config
import io.gitlab.arturbosch.detekt.api.internal.Configuration
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtLambdaArgument
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getParameterForArgument
Expand Down Expand Up @@ -48,8 +49,9 @@ class NamedArguments(config: Config = Config.empty) : Rule(config) {
@Configuration("ignores when argument values are the same as the parameter names")
private val ignoreArgumentsMatchingNames: Boolean by config(defaultValue = false)

override fun visitCondition(root: KtFile) = bindingContext != BindingContext.EMPTY && super.visitCondition(root)

override fun visitCallExpression(expression: KtCallExpression) {
if (bindingContext == BindingContext.EMPTY) return
val valueArguments = expression.valueArguments.filterNot { it is KtLambdaArgument }
if (valueArguments.size > threshold && expression.canNameArguments()) {
val message = "This function call has ${valueArguments.size} arguments. To call a function with more " +
Expand Down
Expand Up @@ -15,6 +15,7 @@ import io.gitlab.arturbosch.detekt.api.internal.Configuration
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
Expand Down Expand Up @@ -65,8 +66,9 @@ class NestedScopeFunctions(config: Config = Config.empty) : Rule(config) {
it.toSet().map(FunctionMatcher::fromFunctionSignature)
}

override fun visitCondition(root: KtFile) = bindingContext != BindingContext.EMPTY && super.visitCondition(root)

override fun visitNamedFunction(function: KtNamedFunction) {
if (bindingContext == BindingContext.EMPTY) return
function.accept(FunctionDepthVisitor())
}

Expand Down
Expand Up @@ -8,6 +8,7 @@ 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 org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
Expand Down Expand Up @@ -49,11 +50,11 @@ class ReplaceSafeCallChainWithRun(config: Config = Config.empty) : Rule(config)
Debt.TEN_MINS
)

override fun visitCondition(root: KtFile) = bindingContext != BindingContext.EMPTY && super.visitCondition(root)

override fun visitSafeQualifiedExpression(expression: KtSafeQualifiedExpression) {
super.visitSafeQualifiedExpression(expression)

if (bindingContext == BindingContext.EMPTY) return

/* We want the last safe qualified expression in the chain, so if there are more in this chain then there's no
need to run checks on this one */
if (expression.parent is KtSafeQualifiedExpression) return
Expand Down
Expand Up @@ -13,10 +13,11 @@ import io.gitlab.arturbosch.detekt.api.internal.Configuration
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import io.gitlab.arturbosch.detekt.rules.fqNameOrNull
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.resolve.BindingContext.EMPTY
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getType
import org.jetbrains.kotlin.types.typeUtil.supertypes

Expand Down Expand Up @@ -54,9 +55,11 @@ class InjectDispatcher(config: Config) : Rule(config) {
Debt.FIVE_MINS
)

override fun visitCondition(root: KtFile) = bindingContext != BindingContext.EMPTY && super.visitCondition(root)

override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) {
super.visitSimpleNameExpression(expression)
if (bindingContext == EMPTY || expression.getReferencedName() !in dispatcherNames) return
if (expression.getReferencedName() !in dispatcherNames) return
val type = expression.getType(bindingContext) ?: return
val isCoroutineDispatcher = type.fqNameOrNull() == COROUTINE_DISPATCHER_FQCN ||
type.supertypes().any { it.fqNameOrNull() == COROUTINE_DISPATCHER_FQCN }
Expand Down
Expand Up @@ -17,6 +17,7 @@ import org.jetbrains.kotlin.descriptors.accessors
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
Expand Down Expand Up @@ -65,8 +66,9 @@ class RedundantSuspendModifier(config: Config) : Rule(config) {
Debt.FIVE_MINS
)

override fun visitCondition(root: KtFile) = bindingContext != BindingContext.EMPTY && super.visitCondition(root)

override fun visitNamedFunction(function: KtNamedFunction) {
if (bindingContext == BindingContext.EMPTY) return
val suspendModifier = function.modifierList?.getModifier(KtTokens.SUSPEND_KEYWORD) ?: return
if (!function.hasBody()) return
if (function.hasModifier(KtTokens.OVERRIDE_KEYWORD)) return
Expand Down