diff --git a/detekt-core/src/main/resources/default-detekt-config.yml b/detekt-core/src/main/resources/default-detekt-config.yml index a5647513d665..0f356b190b93 100644 --- a/detekt-core/src/main/resources/default-detekt-config.yml +++ b/detekt-core/src/main/resources/default-detekt-config.yml @@ -433,6 +433,8 @@ potential-bugs: - '*.CheckReturnValue' ignoreReturnValueAnnotations: - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlinx.coroutines.flow.Flow' ignoreFunctionCall: [] ImplicitDefaultLocale: active: true diff --git a/detekt-rules-errorprone/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/bugs/IgnoredReturnValue.kt b/detekt-rules-errorprone/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/bugs/IgnoredReturnValue.kt index b5eb9c48df43..b6c979e2df55 100644 --- a/detekt-rules-errorprone/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/bugs/IgnoredReturnValue.kt +++ b/detekt-rules-errorprone/src/main/kotlin/io/gitlab/arturbosch/detekt/rules/bugs/IgnoredReturnValue.kt @@ -13,11 +13,14 @@ import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault import io.gitlab.arturbosch.detekt.api.internal.Configuration import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution import io.gitlab.arturbosch.detekt.api.simplePatternToRegex +import io.gitlab.arturbosch.detekt.rules.fqNameOrNull import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor +import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall +import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.typeUtil.isUnit /** @@ -61,6 +64,11 @@ class IgnoredReturnValue(config: Config = Config.empty) : Rule(config) { it.map(String::simplePatternToRegex) } + @Configuration("List of return types that should not be ignored") + private val returnValueTypes: List by config(listOf("kotlinx.coroutines.flow.Flow")) { + it.map(String::simplePatternToRegex) + } + @Configuration( "List of function signatures which should be ignored by this rule. " + "Specifying fully-qualified function signature with name only (i.e. `java.time.LocalDate.now`) will " + @@ -87,6 +95,7 @@ class IgnoredReturnValue(config: Config = Config.empty) : Rule(config) { val annotations = resultingDescriptor.annotations if (annotations.any { it in ignoreReturnValueAnnotations }) return if (restrictToAnnotatedMethods && + resultingDescriptor.returnType !in returnValueTypes && (annotations + resultingDescriptor.containingDeclaration.annotations).none { it in returnValueAnnotations } ) return @@ -100,9 +109,11 @@ class IgnoredReturnValue(config: Config = Config.empty) : Rule(config) { ) } - @Suppress("UnusedPrivateMember") - private operator fun List.contains(annotation: AnnotationDescriptor): Boolean { - val fqName = annotation.fqName?.asString() ?: return false - return any { it.matches(fqName) } + private operator fun List.contains(type: KotlinType?) = contains(type?.fqNameOrNull()) + private operator fun List.contains(annotation: AnnotationDescriptor) = contains(annotation.fqName) + + private operator fun List.contains(fqName: FqName?): Boolean { + val name = fqName?.asString() ?: return false + return any { it.matches(name) } } } diff --git a/detekt-rules-errorprone/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/bugs/IgnoredReturnValueSpec.kt b/detekt-rules-errorprone/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/bugs/IgnoredReturnValueSpec.kt index 41693016a9e3..a6a6a1340fb3 100644 --- a/detekt-rules-errorprone/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/bugs/IgnoredReturnValueSpec.kt +++ b/detekt-rules-errorprone/src/test/kotlin/io/gitlab/arturbosch/detekt/rules/bugs/IgnoredReturnValueSpec.kt @@ -881,4 +881,70 @@ class IgnoredReturnValueSpec(private val env: KotlinCoreEnvironment) { assertThat(findings).isEmpty() } } + + @Nested + inner class `return value types default config` { + private val subject = IgnoredReturnValue() + + @Test + fun `reports when result of function returning Flow is ignored`() { + val code = """ + import kotlinx.coroutines.flow.flowOf + + fun foo() { + flowOf(1, 2, 3) + } + """ + val findings = subject.compileAndLintWithContext(env, code) + + assertThat(findings) + .singleElement() + .hasSourceLocation(line = 4, column = 5) + .hasMessage("The call flowOf is returning a value that is ignored.") + } + + @Test + fun `reports when a function returned result is used in a chain that returns a Flow`() { + val code = """ + import kotlinx.coroutines.flow.* + + fun foo() { + flowOf(1, 2, 3) + .onEach { println(it) } + } + """ + val findings = subject.compileAndLintWithContext(env, code) + + assertThat(findings) + .singleElement() + .hasSourceLocation(line = 5, column = 10) + .hasMessage("The call onEach is returning a value that is ignored.") + } + + @Test + fun `does not report when a function returned value is used to be returned`() { + val code = """ + import kotlinx.coroutines.flow.flowOf + + fun foo() = flowOf(1, 2, 3) + """ + val findings = subject.compileAndLintWithContext(env, code) + assertThat(findings).isEmpty() + } + + @Test + fun `does not report when a function returned value is consumed in a chain that returns an Unit`() { + val code = """ + import kotlinx.coroutines.flow.* + + suspend fun foo() { + flowOf(1, 2, 3) + .onEach { println(it) } + .collect() + } + """ + val findings = subject.compileAndLintWithContext(env, code) + assertThat(findings).isEmpty() + } + } }