Skip to content

Commit 957a8df

Browse files
authored
Make UnstableCollections rule opt-in (#256)
1 parent 2717680 commit 957a8df

File tree

7 files changed

+120
-58
lines changed

7 files changed

+120
-58
lines changed

docs/detekt.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Compose:
110110
RememberContentMissing:
111111
active: true
112112
UnstableCollections:
113-
active: true
113+
active: false # Opt-in, disabled by default. Turn on if you want to enforce this (e.g. you have strong skipping disabled)
114114
ViewModelForwarding:
115115
active: true
116116
# -- You can optionally use this rule on things other than types ending in "ViewModel" or "Presenter" (which are the defaults). You can add your own via a regex here:

docs/ktlint.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,15 @@ compose_disallow_material2 = true
171171
compose_allowed_from_m2 = icons.filled,Button
172172
```
173173

174+
### Enabling the unstable collections detector
175+
176+
The `unstable-collections` rule will flag any usage of any unstable collection (e.g. List/Set/Map). This rule is disabled by default, so you'll need to explicitly enable it in your `.editorconfig` file:
177+
178+
```editorconfig
179+
[*.{kt,kts}]
180+
compose_disallow_unstable_collections = true
181+
```
182+
174183
## Disabling a specific rule
175184

176185
To disable a rule you have to follow the [instructions from the ktlint documentation](https://pinterest.github.io/ktlint/0.49.1/faq/#how-do-i-suppress-errors-for-a-lineblockfile), and use the id of the rule you want to disable with the `compose` tag.
@@ -179,5 +188,5 @@ For example, to disable the `naming-check` rule, the tag you'll need to disable
179188

180189
```kotlin
181190
@Suppress("ktlint:compose:naming-check")
182-
... your code here
191+
fun YourComposableHere() { ... }
183192
```

docs/rules.md

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -30,41 +30,6 @@ More info: [Immutable docs](https://developer.android.com/reference/kotlin/andro
3030

3131
Related rule: TBD
3232

33-
### Avoid using unstable collections
34-
35-
Collections are defined as interfaces (e.g. `List<T>`, `Map<T>`, `Set<T>`) in Kotlin, which can't guarantee that they are actually immutable. For example, you could write:
36-
37-
```kotlin
38-
// ❌ The compiler won't be able to infer that the list is immutable
39-
val list: List<String> = mutableListOf<String>()
40-
```
41-
42-
The variable is constant, its declared type is not mutable but its implementation is still mutable. The Compose compiler cannot be sure of the immutability of this class as it just sees the declared type and as such declares it as unstable.
43-
44-
To force the compiler to see a collection as truly 'immutable' you have a couple of options.
45-
46-
You can use [Kotlinx Immutable Collections](https://github.com/Kotlin/kotlinx.collections.immutable):
47-
48-
```kotlin
49-
// ✅ The compiler knows that this list is immutable
50-
val list: ImmutableList<String> = persistentListOf<String>()
51-
```
52-
53-
Alternatively, you can wrap your collection in an annotated stable class to mark it as immutable for the Compose compiler.
54-
55-
```kotlin
56-
// ✅ The compiler knows that this class is immutable
57-
@Immutable
58-
data class StringList(val items: List<String>)
59-
// ...
60-
val list: StringList = StringList(yourList)
61-
```
62-
> **Note**: It is preferred to use Kotlinx Immutable Collections for this. As you can see, the wrapped case only includes the immutability promise with the annotation, but the underlying List is still mutable.
63-
64-
More info: [Jetpack Compose Stability Explained](https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8), [Kotlinx Immutable Collections](https://github.com/Kotlin/kotlinx.collections.immutable)
65-
66-
Related rule: [compose:unstable-collections](https://github.com/mrmans0n/compose-rules/blob/main/rules/common/src/main/kotlin/io/nlopez/compose/rules/UnstableCollections.kt)
67-
6833
### Use mutableStateOf type-specific variants when possible
6934

7035
`mutableIntStateOf`, `mutableLongStateOf`, `mutableDoubleStateOf`, `mutableFloatStateOf` are essentially counterparts to `mutableStateOf`, but with the added advantage of circumventing autoboxing on JVM platforms. This distinction renders them more memory efficient, making them the preferable choice when dealing with primitive types such as double, float, int, and long.
@@ -518,3 +483,40 @@ Enabling: [ktlint](https://mrmans0n.github.io/compose-rules/ktlint/#enabling-the
518483
More info: [Migration to Material 3](https://developer.android.com/develop/ui/compose/designsystems/material2-material3)
519484

520485
Related rule: [compose:material-two](https://github.com/mrmans0n/compose-rules/blob/main/rules/common/src/main/kotlin/io/nlopez/compose/rules/Material2.kt)
486+
487+
### Avoid using unstable collections
488+
489+
> **Note**: This rule will become unnecessary from the Compose version where strong skipping is enabled by default.
490+
491+
Collections are defined as interfaces (e.g. `List<T>`, `Map<T>`, `Set<T>`) in Kotlin, which can't guarantee that they are actually immutable. For example, you could write:
492+
493+
```kotlin
494+
// ❌ The compiler won't be able to infer that the list is immutable
495+
val list: List<String> = mutableListOf<String>()
496+
```
497+
498+
The variable is constant, its declared type is not mutable but its implementation is still mutable. The Compose compiler cannot be sure of the immutability of this class as it just sees the declared type and as such declares it as unstable.
499+
500+
To force the compiler to see a collection as truly 'immutable' you have a couple of options.
501+
502+
You can use [Kotlinx Immutable Collections](https://github.com/Kotlin/kotlinx.collections.immutable):
503+
504+
```kotlin
505+
// ✅ The compiler knows that this list is immutable
506+
val list: ImmutableList<String> = persistentListOf<String>()
507+
```
508+
509+
Alternatively, you can wrap your collection in an annotated stable class to mark it as immutable for the Compose compiler.
510+
511+
```kotlin
512+
// ✅ The compiler knows that this class is immutable
513+
@Immutable
514+
data class StringList(val items: List<String>)
515+
// ...
516+
val list: StringList = StringList(yourList)
517+
```
518+
> **Note**: It is preferred to use Kotlinx Immutable Collections for this. As you can see, the wrapped case only includes the immutability promise with the annotation, but the underlying List is still mutable.
519+
520+
More info: [Jetpack Compose Stability Explained](https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8), [Kotlinx Immutable Collections](https://github.com/Kotlin/kotlinx.collections.immutable)
521+
522+
Related rule: [compose:unstable-collections](https://github.com/mrmans0n/compose-rules/blob/main/rules/common/src/main/kotlin/io/nlopez/compose/rules/UnstableCollections.kt)

rules/detekt/src/main/resources/config/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Compose:
5252
RememberContentMissing:
5353
active: true
5454
UnstableCollections:
55-
active: true
55+
active: false
5656
ViewModelForwarding:
5757
active: true
5858
ViewModelInjection:

rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/EditorConfigProperties.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,14 @@ val disallowMaterial2: EditorConfigProperty<Boolean> =
255255
),
256256
defaultValue = false,
257257
)
258+
259+
val disallowUnstableCollections: EditorConfigProperty<Boolean> =
260+
EditorConfigProperty(
261+
type = PropertyType.LowerCasingPropertyType(
262+
"compose_disallow_unstable_collections",
263+
"When enabled, unstable collections (e.g. List/Set/Map) will be disallowed.",
264+
PropertyValueParser.BOOLEAN_VALUE_PARSER,
265+
setOf(true.toString(), false.toString()),
266+
),
267+
defaultValue = false,
268+
)

rules/ktlint/src/main/kotlin/io/nlopez/compose/rules/ktlint/UnstableCollectionsCheck.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,30 @@
33
package io.nlopez.compose.rules.ktlint
44

55
import io.nlopez.compose.rules.UnstableCollections
6+
import io.nlopez.rules.core.ComposeKtConfig
67
import io.nlopez.rules.core.ComposeKtVisitor
8+
import io.nlopez.rules.core.Emitter
79
import io.nlopez.rules.core.ktlint.KtlintRule
10+
import org.jetbrains.kotlin.psi.KtFunction
811

912
class UnstableCollectionsCheck :
10-
KtlintRule("compose:unstable-collections"),
11-
ComposeKtVisitor by UnstableCollections()
13+
KtlintRule(
14+
id = "compose:unstable-collections",
15+
editorConfigProperties = setOf(disallowUnstableCollections),
16+
),
17+
ComposeKtVisitor {
18+
19+
private val visitor = UnstableCollections()
20+
21+
override fun visitComposable(
22+
function: KtFunction,
23+
autoCorrect: Boolean,
24+
emitter: Emitter,
25+
config: ComposeKtConfig,
26+
) {
27+
// ktlint allows all rules by default, so we'll add an extra param to make sure it's disabled by default
28+
if (config.getBoolean("disallowUnstableCollections", false)) {
29+
visitor.visitComposable(function, autoCorrect, emitter, config)
30+
}
31+
}
32+
}

rules/ktlint/src/test/kotlin/io/nlopez/compose/rules/ktlint/UnstableCollectionsCheckTest.kt

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,40 @@ class UnstableCollectionsCheckTest {
2424
@Composable
2525
fun Something(a: Map<String, Int>) {}
2626
""".trimIndent()
27-
ruleAssertThat(code).hasLintViolationsWithoutAutoCorrect(
28-
LintViolation(
29-
line = 2,
30-
col = 18,
31-
detail = createErrorMessage("List<String>", "List", "a"),
32-
),
33-
LintViolation(
34-
line = 4,
35-
col = 18,
36-
detail = createErrorMessage("Set<String>", "Set", "a"),
37-
),
38-
LintViolation(
39-
line = 6,
40-
col = 18,
41-
detail = createErrorMessage("Map<String, Int>", "Map", "a"),
42-
),
43-
)
27+
ruleAssertThat(code)
28+
.withEditorConfigOverride(disallowUnstableCollections to true)
29+
.hasLintViolationsWithoutAutoCorrect(
30+
LintViolation(
31+
line = 2,
32+
col = 18,
33+
detail = createErrorMessage("List<String>", "List", "a"),
34+
),
35+
LintViolation(
36+
line = 4,
37+
col = 18,
38+
detail = createErrorMessage("Set<String>", "Set", "a"),
39+
),
40+
LintViolation(
41+
line = 6,
42+
col = 18,
43+
detail = createErrorMessage("Map<String, Int>", "Map", "a"),
44+
),
45+
)
46+
}
47+
48+
@Test
49+
fun `passes even if there are errors if disallowUnstableCollections is false`() {
50+
@Language("kotlin")
51+
val code =
52+
"""
53+
@Composable
54+
fun Something(a: List<String>) {}
55+
@Composable
56+
fun Something(a: Set<String>) {}
57+
@Composable
58+
fun Something(a: Map<String, Int>) {}
59+
""".trimIndent()
60+
ruleAssertThat(code).hasNoLintViolations()
4461
}
4562

4663
@Test
@@ -53,6 +70,8 @@ class UnstableCollectionsCheckTest {
5370
@Composable
5471
fun Something(a: StringList, b: StringSet, c: StringToIntMap) {}
5572
""".trimIndent()
56-
ruleAssertThat(code).hasNoLintViolations()
73+
ruleAssertThat(code)
74+
.withEditorConfigOverride(disallowUnstableCollections to true)
75+
.hasNoLintViolations()
5776
}
5877
}

0 commit comments

Comments
 (0)