ktlint icon indicating copy to clipboard operation
ktlint copied to clipboard

Rule suggestion: prefer `emptyList()` over `listOf()`

Open FloEdelmann opened this issue 1 year ago • 1 comments

Expected Rule behavior

// Should fail:
val foo: List<String> = listOf()
val bar = listOf<String>()

// Should pass:
val baz: List<String> = emptyList()
val boo = emptyList<String>()

val far = mutableListOf<String>()
val faz = listOf("foo")

Additional information

  • Current version of ktlint: 1.4.1
  • Styleguide section: not applicable (emptyList is not mentioned in the Kotlin Coding Conventions or Android Kotlin Style Guide)
  • https://stackoverflow.com/questions/48741473/what-is-the-function-of-emptylist-in-kotlin
  • https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/empty-list.html
  • https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/list-of.html

FloEdelmann avatar Nov 25 '24 17:11 FloEdelmann

I have some doubts whether this should be a Ktlint rule. It is not really related to lint/formatting but more an inspection kind of rule. If such a rule would be added it should be a bit more generic and also take setOf() and mapOf into account. As this is not mentioned in the style guides, it can only be added and activated by default in ktlint_official code style. For other code styles it will need to be enabled explicitly.

paul-dingemans avatar Nov 26 '24 15:11 paul-dingemans

A rule like this does not belong in Ktlint standard rules as it is not a real linting/formatting issue, but more an idiomatic Kotlin style. You can provide such a rule in a custom ruleset your. As I have used your request for a demo, you can use code below:

package com.pinterest.ktlint.ruleset.standard.rules

import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision
import com.pinterest.ktlint.rule.engine.core.api.ElementType.IDENTIFIER
import com.pinterest.ktlint.rule.engine.core.api.ElementType.LPAR
import com.pinterest.ktlint.rule.engine.core.api.ElementType.REFERENCE_EXPRESSION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.RPAR
import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT_LIST
import com.pinterest.ktlint.rule.engine.core.api.children
import com.pinterest.ktlint.rule.engine.core.api.ifAutocorrectAllowed
import com.pinterest.ktlint.rule.engine.core.api.nextSibling
import com.pinterest.ktlint.rule.engine.core.util.safeAs
import com.pinterest.ktlint.ruleset.standard.StandardRule
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement

public class EmptyCollectionInitializationRule : StandardRule("empty-collection-initialization") {
    override fun beforeVisitChildNodes(
        node: ASTNode,
        emit: (Int, String, Boolean) -> AutocorrectDecision,
    ) {
        node
            .takeIf { it.elementType == REFERENCE_EXPRESSION && it.text in COLLECTIONS_REFERENCES.keys }
            ?.nextSibling { it.elementType == VALUE_ARGUMENT_LIST }
            ?.children()
            ?.filterNot { it.elementType == LPAR || it.elementType == RPAR }
            ?.toList()
            ?.ifEmpty {
                val emptyCollectionReferenceId = COLLECTIONS_REFERENCES[node.text]!!
                emit(
                    node.startOffset,
                    "Use '$emptyCollectionReferenceId' instead of '${node.text}' to create an empty collection",
                    true,
                ).ifAutocorrectAllowed {
                    node
                        .findChildByType(IDENTIFIER)
                        .safeAs<LeafElement>()
                        ?.rawReplaceWithText(emptyCollectionReferenceId)
                }
            }
    }

    private companion object {
        val COLLECTIONS_REFERENCES =
            mapOf(
                "listOf" to "emptyList",
                "setOf" to "emptySet",
                "mapOf" to "emptyMap",
            )
    }
}

and tests:

package com.pinterest.ktlint.ruleset.standard.rules

import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule
import com.pinterest.ktlint.test.LintViolation
import org.junit.jupiter.api.Test

class EmptyCollectionInitializationRuleTest {
    private val emptyCollectionInitializationRuleAssertThat = assertThatRule { EmptyCollectionInitializationRule() }

    @Test
    fun `Given a collection initialized with listOf() then replace with emptyList()`() {
        val code =
            """
            val x = listOf<String>()
            val y: List<String> = listOf()
            val z = listOf("zzz")
            """.trimIndent()
        val formattedCode =
            """
            val x = emptyList<String>()
            val y: List<String> = emptyList()
            val z = listOf("zzz")
            """.trimIndent()
        emptyCollectionInitializationRuleAssertThat(code)
            .hasLintViolations(
                LintViolation(1, 9, "Use 'emptyList' instead of 'listOf' to create an empty collection"),
                LintViolation(2, 23, "Use 'emptyList' instead of 'listOf' to create an empty collection"),
            ).isFormattedAs(formattedCode)
    }

    @Test
    fun `Given a collection initialized with setOf() then replace with emptySet()`() {
        val code =
            """
            val x = setOf<String>()
            val y: Set<String> = setOf()
            val z = setOf("zzz")
            """.trimIndent()
        val formattedCode =
            """
            val x = emptySet<String>()
            val y: Set<String> = emptySet()
            val z = setOf("zzz")
            """.trimIndent()
        emptyCollectionInitializationRuleAssertThat(code)
            .hasLintViolations(
                LintViolation(1, 9, "Use 'emptySet' instead of 'setOf' to create an empty collection"),
                LintViolation(2, 22, "Use 'emptySet' instead of 'setOf' to create an empty collection"),
            ).isFormattedAs(formattedCode)
    }

    @Test
    fun `Given a collection initialized with mapOf() then replace with emptyMap()`() {
        val code =
            """
            val x = mapOf<String, String>()
            val y: Map<String, String> = mapOf()
            val z = mapOf("zzz")
            """.trimIndent()
        val formattedCode =
            """
            val x = emptyMap<String, String>()
            val y: Map<String, String> = emptyMap()
            val z = mapOf("zzz")
            """.trimIndent()
        emptyCollectionInitializationRuleAssertThat(code)
            .hasLintViolations(
                LintViolation(1, 9, "Use 'emptyMap' instead of 'mapOf' to create an empty collection"),
                LintViolation(2, 30, "Use 'emptyMap' instead of 'mapOf' to create an empty collection"),
            ).isFormattedAs(formattedCode)
    }
}

paul-dingemans avatar Jul 06 '25 12:07 paul-dingemans