Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ android {
}
}

tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
jvmTarget = "21"
}

dependencies {
detektPlugins(project(":detekt-rules"))
lintChecks(project(":lint-checks"))
Expand Down
30 changes: 29 additions & 1 deletion detekt-rules/src/main/kotlin/ru/otus/detekt/GlobalScopeRule.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package ru.otus.detekt

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.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression

class GlobalScopeRule(config: Config) : Rule(config) {
override val issue: Issue = Issue(
Expand All @@ -14,5 +18,29 @@ class GlobalScopeRule(config: Config) : Rule(config) {
debt = Debt.FIVE_MINS
)

// TODO
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
super.visitDotQualifiedExpression(expression)

val callExpression = expression.selectorExpression as? KtCallExpression ?: return

val functionName = callExpression.calleeExpression?.text

val isLaunchOrAsync = functionName == "launch" || functionName == "async"

if (!isLaunchOrAsync) return

val receiverText = expression.receiverExpression.text

val isGlobalScope = receiverText == "GlobalScope" || receiverText.endsWith(".GlobalScope")

if (isGlobalScope) {
report(
CodeSmell(
issue = issue,
entity = Entity.from(expression),
message = "Avoid using GlobalScope"
)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
package ru.otus.detekt

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.rules.fqNameOrNull
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
import org.jetbrains.kotlin.psi.psiUtil.hasSuspendModifier
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.types.typeUtil.makeNotNullable
import org.jetbrains.kotlin.types.typeUtil.supertypes

private const val SUPER_TYPE_NAME = "kotlinx.coroutines.CoroutineScope"

class TopLevelCoroutineInSuspendFunRule(config: Config) : Rule(config) {
override val issue: Issue = Issue(
Expand All @@ -14,5 +31,61 @@ class TopLevelCoroutineInSuspendFunRule(config: Config) : Rule(config) {
debt = Debt.FIVE_MINS
)

// TODO
override fun visitNamedFunction(function: KtNamedFunction) {
super.visitNamedFunction(function)

val isSuspend = function.modifierList?.hasSuspendModifier() ?: false

if (bindingContext == BindingContext.EMPTY || !isSuspend) return

val dotCallExpression = function.collectDescendantsOfType<KtDotQualifiedExpression>()

val dotCallExpressions = dotCallExpression.filter {

it.receiverExpression.isSubclassOfCoroutineScope()
}

val sendReport = dotCallExpressions.any { it.isExecuteAsyncOrLaunch() }

if (sendReport) {
report(
CodeSmell(
issue = issue,
entity = Entity.from(function),
message = "Avoid running top level coroutines inside suspend functions"
)
)
}
}

private fun KtQualifiedExpression.isExecuteAsyncOrLaunch(): Boolean {

val callExpression = this.selectorExpression as? KtCallExpression ?: return false

val resolvedCall = callExpression.getResolvedCall(bindingContext) ?: return false

val fullFunctionName = resolvedCall.resultingDescriptor.fqNameOrNull()?.asString()

return when (fullFunctionName) {
"kotlinx.coroutines.launch", "kotlinx.coroutines.async" -> true
else -> false
}
}

private fun KtExpression.isSubclassOfCoroutineScope(): Boolean {

val type = bindingContext.getType(this) ?: return false

val cleanType = type.makeNotNullable()

val className = cleanType.fqNameOrNull()?.asString()

if (className == SUPER_TYPE_NAME) return true

return cleanType.supertypes().any { superType ->
val superTypeName = superType.fqNameOrNull()?.asString()
superTypeName == SUPER_TYPE_NAME
}
}

}
98 changes: 98 additions & 0 deletions detekt-rules/src/test/kotlin/ru/otus/detekt/GlobalScopeRuleTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package ru.otus.detekt

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.test.compileAndLint
import io.kotest.matchers.collections.shouldHaveSize
import org.junit.jupiter.api.Test

internal class GlobalScopeRuleTest {
private val rule = GlobalScopeRule(Config.empty)

@Test
fun `reports call launch or async in GlobalScope`() {
val code = """
import androidx.compose.runtime.Composable
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

class SameEntity {

suspend fun globalScope(){
GlobalScope.async {
GlobalScope.launch{

}
}
}
}

"""
val findings = rule.compileAndLint(code)
findings shouldHaveSize 2
}

@Test
fun `reports call launch in GlobalScope`() {
val code = """
import androidx.compose.runtime.Composable
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

class SameClass {

suspend fun globalScope(){
kotlinx.coroutines.GlobalScope.launch { }
}
}

"""
val findings = rule.compileAndLint(code)
findings shouldHaveSize 1
}

@Test
fun `reports call async in GlobalScope`() {
val code = """
import androidx.compose.runtime.Composable
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

private const val PROPERTY = ""

class SameClass{

suspend fun globalScope(){
GlobalScope.async{

}
}
}

"""
val findings = rule.compileAndLint(code)
findings shouldHaveSize 1
}

@Test
fun `no reports`() {
val code = """
import androidx.compose.runtime.Composable
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

private const val PROPERTY = ""

class SameClass {

suspend fun noGlobalScope(){
GlobalScope.customMethod(launch = true)
}
}

"""
val findings = rule.compileAndLint(code)
findings shouldHaveSize 0
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package ru.otus.detekt

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest
import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext
import io.kotest.matchers.collections.shouldHaveSize
import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
import org.junit.jupiter.api.Test

@KotlinCoreEnvironmentTest
class TopLevelCoroutineInSuspendRuleTest(private val env: KotlinCoreEnvironment) {
private val rule = TopLevelCoroutineInSuspendFunRule(Config.empty)

@Test
fun `reports call coroutine scoped launch or async in suspend`() {
val content = """
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch

private const val PROPERTY = ""

class SameClass{

suspend fun executeSuspend(){
CoroutineScope(SupervisorJob()).launch{

}
}
}
"""
val findings = rule.compileAndLintWithContext(env, content)
findings shouldHaveSize 1
}

@Test
fun `reports call coroutine scoped launch in suspend with Global Scope`() {
val content = """
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.GlobalScope

private const val PROPERTY = ""

class SameClass {

suspend fun executeSuspend(){
GlobalScope.launch{

}
}
}
"""
val findings = rule.compileAndLintWithContext(env, content)
findings shouldHaveSize 1
}

@Test
fun `reports call coroutine scoped async in 2 suspend func `() {
val content = """
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.GlobalScope

private const val PROPERTY = ""

class SameClass{

suspend fun executeSuspend(){
GlobalScope.async{

}
}
suspend fun executeAnotherSuspend(){
CoroutineScope(SupervisorJob()).async{

}
}
}
"""
val findings = rule.compileAndLintWithContext(env, content)
findings shouldHaveSize 2
}

@Test
fun `no reports`() {
val content = """
package ru.otus.example

import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope

class SameClass {

suspend fun invoke(){
supervisorScope{(SupervisorJob())
launch {
println("hello")
}
}
}
}

"""
val findings = rule.compileAndLintWithContext(env, content)
findings shouldHaveSize 0
}
}
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]
kotlin = "2.1.21"
detekt = "1.23.8"
detekt = "1.23.6"
kotest = "5.9.1"
jupiter = "5.11.4"
jreleaser = "1.16.0"
Expand Down