Skip to content

Commit

Permalink
Function generics matcher (#4460)
Browse files Browse the repository at this point in the history
* Match functions with generics

* Add test to ensure that #4448 is fixed
  • Loading branch information
BraisGabin committed Mar 31, 2022
1 parent abd3851 commit e052dbe
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 4 deletions.
Expand Up @@ -334,5 +334,88 @@ class ForbiddenMethodCallSpec : Spek({
).compileAndLintWithContext(env, code)
assertThat(findings).hasSize(1)
}

context("work with generics") {
val code = """
package org.example
fun <T, U> bar(a: T, b: U, c: String) = Unit
fun foo() {
bar(1, "", "")
}
"""

it("raise the issue") {
val findings = ForbiddenMethodCall(
TestConfig(mapOf(METHODS to listOf("org.example.bar(T, U, kotlin.String)")))
).compileAndLintWithContext(env, code)
assertThat(findings).hasSize(1)
}

it("It doesn't raise any issue because the generics don't match") {
val findings = ForbiddenMethodCall(
TestConfig(mapOf(METHODS to listOf("org.example.bar(U, T, kotlin.String)")))
).compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}
}

context("work with generic extensions") {
val code = """
package org.example
fun <R> R.bar(a: String) = Unit
fun foo() {
1.bar("")
}
"""

it("raise the issue") {
val findings = ForbiddenMethodCall(
TestConfig(mapOf(METHODS to listOf("org.example.bar(R, kotlin.String)")))
).compileAndLintWithContext(env, code)
assertThat(findings).hasSize(1)
}

it("It doesn't raise any issue because the type doesn't match") {
val findings = ForbiddenMethodCall(
TestConfig(mapOf(METHODS to listOf("org.example.bar(kotlin.Int, kotlin.String)")))
).compileAndLintWithContext(env, code)
assertThat(findings).isEmpty()
}
}

context("Should distinguish between runCatching - #4448") {
val code = """
package org.example
class A {
fun foo() {
kotlin.runCatching {}
runCatching {}
}
}
"""

it("forbid the one without receiver") {
val findings = ForbiddenMethodCall(
TestConfig(mapOf(METHODS to listOf("kotlin.runCatching(() -> R)")))
).compileAndLintWithContext(env, code)
assertThat(findings)
.hasSize(1)
.hasSourceLocation(5, 16)
}

it("forbid the one with receiver") {
val findings = ForbiddenMethodCall(
TestConfig(mapOf(METHODS to listOf("kotlin.runCatching(T, (T) -> R)")))
).compileAndLintWithContext(env, code)
assertThat(findings)
.hasSize(1)
.hasSourceLocation(6, 9)
}
}
}
})
Expand Up @@ -5,6 +5,8 @@ import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.typeUtil.isTypeParameter

sealed class FunctionMatcher {

Expand Down Expand Up @@ -33,11 +35,12 @@ sealed class FunctionMatcher {
private val parameters: List<String>
) : FunctionMatcher() {
override fun match(callableDescriptor: CallableDescriptor): Boolean {
if (callableDescriptor.fqNameOrNull()?.asString() != fullyQualifiedName) return false
val descriptor = callableDescriptor.original
if (descriptor.fqNameOrNull()?.asString() != fullyQualifiedName) return false

val encounteredParamTypes =
(listOfNotNull(callableDescriptor.extensionReceiverParameter) + callableDescriptor.valueParameters)
.map { it.type.fqNameOrNull()?.asString() }
(listOfNotNull(descriptor.extensionReceiverParameter) + descriptor.valueParameters)
.map { it.type.getSignatureParameter() }

return encounteredParamTypes == parameters
}
Expand All @@ -48,7 +51,7 @@ sealed class FunctionMatcher {

val encounteredParameters =
(listOfNotNull(function.receiverTypeReference) + function.valueParameters.map { it.typeReference })
.map { bindingContext[BindingContext.TYPE, it]?.fqNameOrNull()?.toString() }
.map { bindingContext[BindingContext.TYPE, it]?.getSignatureParameter() }

return encounteredParameters == parameters
}
Expand Down Expand Up @@ -80,6 +83,14 @@ sealed class FunctionMatcher {
}
}

private fun KotlinType.getSignatureParameter(): String? {
return if (isTypeParameter()) {
toString()
} else {
fqNameOrNull()?.toString()
}
}

// Extracted from: https://stackoverflow.com/a/16108347/842697
private fun String.splitParams(): List<String> {
val split: MutableList<String> = mutableListOf()
Expand Down
Expand Up @@ -246,6 +246,21 @@ class FunctionMatcherSpec(private val env: KotlinCoreEnvironment) {
val methodSignature = FunctionMatcher.fromFunctionSignature("foo(kotlin.String, kotlin.Int)")
assertThat(methodSignature.match(function, bindingContext)).isEqualTo(result)
}

@DisplayName("When generics foo(T, U)")
@ParameterizedTest(name = "in case {0} it return {1}")
@CsvSource(
"'fun <T, U> foo(a: T, b: U)', true",
"'fun <T, U> foo(a: U, b: T)', false",
"'fun <T, U> foo(a: String, b: U)', false",
"'fun <T, U> T.foo(a: U)', true",
"'fun <T, U> U.foo(a: T)', false",
)
fun `When foo(T, U)`(code: String, result: Boolean) {
val (function, bindingContext) = buildKtFunction(env, code)
val methodSignature = FunctionMatcher.fromFunctionSignature("foo(T, U)")
assertThat(methodSignature.match(function, bindingContext)).isEqualTo(result)
}
}
}

Expand Down

0 comments on commit e052dbe

Please sign in to comment.