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

Function generics matcher #4460

Merged
merged 2 commits into from Mar 31, 2022
Merged
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
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
Copy link
Member

Choose a reason for hiding this comment

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

Q: What is .original doing here? Why is it needed?

Copy link
Member Author

Choose a reason for hiding this comment

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

That get's the function definition instead of the function call. If I request the type of the parameter of list("hello") it will say String but if I ask for the type of the original I get T.

Copy link
Member

Choose a reason for hiding this comment

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

Gotcha 👍 Thanks for clarifying

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