Rule suggestion: prefer `emptyList()` over `listOf()`
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 (
emptyListis 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
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.
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)
}
}