Skip to content

Commit

Permalink
Match functions signatures with lambdas on it
Browse files Browse the repository at this point in the history
  • Loading branch information
BraisGabin committed Jan 6, 2022
1 parent a9c38b8 commit 1576eeb
Show file tree
Hide file tree
Showing 3 changed files with 123 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,77 @@ 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)
}
}
}
}

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

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

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

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

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

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.contains("->"))

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 1576eeb

Please sign in to comment.