How to check usage of 'it' parameter in lambda with Detekt custom rule?

351 views Asked by At

I want to write a Detekt rule which disallows usage of implicit "it" variable in a multi-line lambda. I've written method override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) but I don't know how

  1. Check if the lambda contains 'it' variable. lambdaExpression.valueParameters contains nothing in that case.
  2. Check if 'it' is used in the lambda.

And I've not found any documentation.

1

There are 1 answers

0
Fox On

Try this

import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFunctionLiteral
import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.psiUtil.allChildren

class NoItInMultilineLambda(config: Config = Config.empty) : Rule(config) {
    override val issue = Issue(
        id = "NoItInMultilineLambda",
        severity = Severity.Style,
        description = "Do not use 'it' in multiline lambda. Use an explicit parameter instead",
        debt = Debt(mins = 1),
    )

    override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) {
        super.visitLambdaExpression(lambdaExpression)
        val referenceExpression: KtNameReferenceExpression? = lambdaExpression.run {
            when {
                isMultiline() -> {
                    bodyExpression?.statements?.findKtNameReferenceExpression { ktNameReferenceExpression ->
                        ktNameReferenceExpression.getReferencedName() == "it"
                    }
                }
                else -> null
            }
        }

        if (referenceExpression != null) {
            report(
                CodeSmell(
                    issue = issue,
                    entity = Entity.from(referenceExpression),
                    message = "Usage of 'it' in multiline lambda",
                )
            )
        }
    }

    private fun KtLambdaExpression.isMultiline(): Boolean {
        return children.filterIsInstance<KtFunctionLiteral>().firstOrNull()?.allChildren?.any { psiElement ->
            psiElement is PsiWhiteSpace && psiElement.textContains('\n')
        } ?: false
    }

    @Suppress("ReturnCount")
    private fun List<KtElement>.findKtNameReferenceExpression(
        predicate: (KtNameReferenceExpression) -> Boolean,
    ): KtNameReferenceExpression? {
        forEach { ktElement: KtElement ->
            when {
                ktElement is KtNameReferenceExpression && predicate(ktElement) -> return ktElement
                ktElement is KtLambdaExpression -> return null // Nested lambdas are handled automatically
                else -> {
                    val foundInChidren = ktElement.children.filterIsInstance<KtElement>()
                        .findKtNameReferenceExpression(predicate)

                    if (foundInChidren != null) {
                        return foundInChidren
                    }
                }
            }
        }

        return null
    }
}

And the test:

import com.google.common.truth.Truth
import io.gitlab.arturbosch.detekt.test.lint
import org.junit.jupiter.api.Test

internal class NoItInMultilineLambdaTest {

    @Test
    fun noItInMultilineLambdaTest0() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test() {
                    funWithLambda { 
                        it
                    }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(1)
        Truth.assertThat(findings[0].message).isEqualTo(
            "Usage of 'it' in multiline lambda",
        )
    }

    @Test
    fun noItInMultilineLambdaTest1() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test() {
                    funWithLambda {
                        // Some good statement
                        val x = 0
                        test() 
                        it
                        it
                    }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(1)
        Truth.assertThat(findings[0].message).isEqualTo(
            "Usage of 'it' in multiline lambda",
        )
    }

    @Test
    fun noItInMultilineLambdaTest2() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test() {
                    funWithLambda {
                        // Some good statement
                        val x = 0
                        test() 
                        it.dec()
                        // Some good statement
                        val y = 0
                        test() 
                    }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(1)
        Truth.assertThat(findings[0].message).isEqualTo(
            "Usage of 'it' in multiline lambda",
        )
    }

    @Test
    fun noItInMultilineLambdaTest3() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test() {
                    funWithLambda {
                        // Some good statement
                        val x = 0
                        test() 
                        it += 1
                    }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(1)
        Truth.assertThat(findings[0].message).isEqualTo(
            "Usage of 'it' in multiline lambda",
        )
    }

    @Test
    fun noItInMultilineLambdaTest4() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test() {
                    funWithLambda {
                        // Some good statement
                        val x = 0
                        test() 
                        it.and(1)
                    }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(1)
        Truth.assertThat(findings[0].message).isEqualTo(
            "Usage of 'it' in multiline lambda",
        )
    }

    @Test
    fun noItInMultilineLambdaTest6() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test() {
                    funWithLambda {
                        // Some good statement
                        val x = 0
                        test() 
                        it == 0
                    }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(1)
        Truth.assertThat(findings[0].message).isEqualTo(
            "Usage of 'it' in multiline lambda",
        )
    }

    @Test
    fun noItInMultilineLambdaTest5() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test() {
                    funWithLambda {
                        funWithLambda {
                            it == 0
                        }           
                    }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings.map { it.message }).contains(
            "Usage of 'it' in multiline lambda",
        )
    }

    @Test
    fun noItInMultilineLambdaTest7() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test() {
                    funWithLambda {
                        funWithLambda {
                            // Some good statement
                            val x = 0
                            test() 
                            it == 0
                            // Some good statement
                            val x = 0
                            test() 
                        }           
                    }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings.map { it.message }).contains(
            "Usage of 'it' in multiline lambda",
        )
    }

    @Test
    fun noItInMultilineLambdaTest8() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test(i: Int) {
                    funWithLambda {
                        test(it) 
                    }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(1)
        Truth.assertThat(findings[0].message).isEqualTo(
            "Usage of 'it' in multiline lambda",
        )
    }

    @Test
    fun noItInMultilineLambdaTest9() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test(a: Boolean, i: Int, c: Float) {
                    funWithLambda {
                        test(false, it, 1.0f) 
                    }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(1)
        Truth.assertThat(findings[0].message).isEqualTo(
            "Usage of 'it' in multiline lambda",
        )
    }

    @Test
    fun noItInMultilineLambdaTest15() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test() {
                    var x: Int = 0

                    funWithLambda {
                        x = it
                    }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(1)
        Truth.assertThat(findings[0].message).isEqualTo(
            "Usage of 'it' in multiline lambda",
        )
    }

    @Test
    fun noItInMultilineLambdaTest10() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test(a: Boolean, i: Int, c: Float) {
                    funWithLambda { it }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(0)
    }

    @Test
    fun noItInMultilineLambdaTest11() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test(a: Boolean, i: Int, c: Float) {
                    funWithLambda { funWithLambda { it } }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(0)
    }

    @Test
    fun noItInMultilineLambdaTest12() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test(a: Boolean, i: Int, c: Float) {
                    funWithLambda { funWithLambda { it == 0 } }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(0)
    }

    @Test
    fun noItInMultilineLambdaTest13() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test(a: Boolean, i: Int, c: Float) {
                    funWithLambda { funWithLambda { it += 1 } }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(0)
    }

    @Test
    fun noItInMultilineLambdaTest16() {
        val findings = NoItInMultilineLambda().lint(
            """
                fun funWithLambda1(lambda: () -> Unit) { }
                fun funWithLambda(lambda: (param: Int) -> Unit) { }
                
                fun test() {
                    funWithLambda1 {
                        funWithLambda { it += 1 }
                    }
                }
            """.trimIndent()
        )
        Truth.assertThat(findings).hasSize(0)
    }
}