Skip to content

Commit

Permalink
Match functions signatures with lambdas on it (#4458)
Browse files Browse the repository at this point in the history
* Match functions signatures with lambdas on it

* Improve code
  • Loading branch information
BraisGabin committed Jan 25, 2022
1 parent 3c9f0e9 commit 576ae5f
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 13 deletions.
Expand Up @@ -302,5 +302,21 @@ class ForbiddenMethodCallSpec : Spek({
).compileAndLintWithContext(env, code)
assertThat(findings).hasSize(2)
}

it("should report functions with lambda params") {
val code = """
package org.example
fun bar(b: (String) -> String) = Unit
fun foo() {
bar { "" }
}
"""
val findings = ForbiddenMethodCall(
TestConfig(mapOf(METHODS to listOf("org.example.bar((kotlin.String) -> kotlin.String)")))
).compileAndLintWithContext(env, code)
assertThat(findings).hasSize(1)
}
}
})
Expand Up @@ -58,21 +58,84 @@ sealed class FunctionMatcher {

companion object {
fun fromFunctionSignature(methodSignature: String): FunctionMatcher {
val tokens = methodSignature.split("(", ")")
.map { it.trim() }

val methodName = tokens.first().replace("`", "")
val params = if (tokens.size > 1) {
tokens[1].split(",").map { it.trim() }.filter { it.isNotBlank() }
} else {
null
@Suppress("TooGenericExceptionCaught", "UnsafeCallOnNullableType")
try {
val result = functionSignatureRegex.matchEntire(methodSignature)!!

val methodName = result.groups[1]!!.value.replace("`", "")
val params = result.groups[2]?.value?.splitParams()
?.map { changeIfLambda(it) ?: it }

return if (params == null) {
NameOnly(methodName)
} else {
WithParameters(methodName, params)
}
} catch (ex: Exception) {
throw IllegalStateException("$methodSignature doesn't match a method signature", ex)
}
}
}
}

return if (params == null) {
NameOnly(methodName)
// Extracted from: https://stackoverflow.com/a/16108347/842697
private fun String.splitParams(): List<String> {
val split: MutableList<String> = mutableListOf()
var nestingLevel = 0
val result = StringBuilder()
this.forEach { c ->
if (c == ',' && nestingLevel == 0) {
split.add(result.toString().trim())
result.setLength(0)
} else {
if (c == '(') nestingLevel++
if (c == ')') nestingLevel--
check(nestingLevel >= 0)
result.append(c)
}
}
val lastParam = result.toString().trim()
if (lastParam.isNotEmpty()) {
split.add(lastParam)
}
return split
}

private fun changeIfLambda(param: String): String? {
val (paramsRaw, _) = splitLambda(param) ?: return null
val params = paramsRaw.splitParams()

return "kotlin.Function${params.count()}"
}

private fun splitLambda(param: String): Pair<String, String>? {
if (!param.startsWith("(")) return null

var nestingLevel = 0
val paramsRaw = StringBuilder()
val returnValue = StringBuilder()

/*
* We don't count the first `(` so as soon as the nestingLevel reaches the last `)` we know that we read all the
* params. Then we handle the rest of the String as the result.
*/
param.toCharArray()
.drop(1)
.forEach { c ->
if (nestingLevel >= 0) {
if (c == '(') nestingLevel++
if (c == ')') nestingLevel--
if (nestingLevel >= 0) {
paramsRaw.append(c)
}
} else {
WithParameters(methodName, params)
returnValue.append(c)
}
}
}

check(returnValue.trim().startsWith("->"))

return paramsRaw.toString().trim() to returnValue.toString().substringAfter("->").trim()
}

private val functionSignatureRegex = """((?:[^()`]|`.*`)*)(?:\((.*)\))?""".toRegex()
Expand Up @@ -50,7 +50,31 @@ class FunctionMatcherSpec : Spek({
"io.gitlab.arturbosch.detekt.SomeClass.some , method",
listOf("kotlin.String"),
),
)
),
TestCase(
testDescription = "should return method name and param list when it has lambdas",
functionSignature = "hello((Bar, Foo) -> Unit, (Bar) -> Bar, Foo, () -> Foo)",
expectedFunctionMatcher = FunctionMatcher.WithParameters(
"hello",
listOf(
"kotlin.Function2",
"kotlin.Function1",
"Foo",
"kotlin.Function0",
),
),
),
TestCase(
testDescription = "should return method name and param list when it has complex lambdas",
functionSignature = "hello((Bar, (Bar) -> Unit) -> (Bar) -> Foo, () -> Unit)",
expectedFunctionMatcher = FunctionMatcher.WithParameters(
"hello",
listOf(
"kotlin.Function2",
"kotlin.Function0",
),
),
),
).forEach { testCase ->
it(testCase.testDescription) {
val functionMatcher = FunctionMatcher.fromFunctionSignature(testCase.functionSignature)
Expand Down Expand Up @@ -133,6 +157,20 @@ private val matrixCase: Map<FunctionMatcher, Map<String, Boolean>> = run {
functions[4] to false, // fun compare(hello: String)
functions[5] to false, // fun compare(hello: String, world: Int)
),
FunctionMatcher.fromFunctionSignature("foo(() -> kotlin.String)") to linkedMapOf(
"fun foo(a: () -> String)" to true,
"fun foo(a: () -> Unit)" to true,
"fun foo(a: (String) -> String)" to false,
"fun foo(a: (String) -> Unit)" to false,
"fun foo(a: (Int) -> Unit)" to false,
),
FunctionMatcher.fromFunctionSignature("foo((kotlin.String) -> Unit)") to linkedMapOf(
"fun foo(a: () -> String)" to false,
"fun foo(a: () -> Unit)" to false,
"fun foo(a: (String) -> String)" to true,
"fun foo(a: (String) -> Unit)" to true,
"fun foo(a: (Int) -> Unit)" to true,
),
)
}

Expand Down

0 comments on commit 576ae5f

Please sign in to comment.