1 /*
2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.runtime.lint
18 
19 import androidx.compose.lint.test.Stubs
20 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
21 import com.android.tools.lint.detector.api.Detector
22 import com.android.tools.lint.detector.api.Issue
23 import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly
24 import org.junit.Test
25 import org.junit.runner.RunWith
26 import org.junit.runners.Parameterized
27 
28 @RunWith(Parameterized::class)
29 class AutoboxingStateCreationDetectorTest(typeUnderTest: TypeUnderTest) : LintDetectorTest() {
30 
31     private val fqType = typeUnderTest.fqName
32     private val type = typeUnderTest.typeName
33     private val jvmType = typeUnderTest.jvmClassName
34     private val fqJvmClass = typeUnderTest.fqJvmName
35     private val stateValue = typeUnderTest.sampleValue
36 
37     private val primitiveStateStub =
38         kotlin(
39             """
40         package androidx.compose.runtime
41 
42         import kotlin.reflect.KProperty
43         import $fqType
44 
mutablenull45         fun mutable${type}StateOf(value: $type): Mutable${type}State {
46             TODO("Not implemented in lint stubs.")
47         }
48 
49         interface Mutable${type}State : State<$type> {
50             override var value: $type
51             var ${type.toLowerCaseAsciiOnly()}Value: $type
52         }
53 
54         @Suppress("NOTHING_TO_INLINE")
55         inline operator fun Mutable${type}State.getValue(
56             thisObj: Any?,
57             property: KProperty<*>
58         ): $type = ${type.toLowerCaseAsciiOnly()}Value
59 
60         @Suppress("NOTHING_TO_INLINE")
61         inline operator fun Mutable${type}State.setValue(
62             thisObj: Any?,
63             property: KProperty<*>,
64             value: $type
65         ) {
66             ${type.toLowerCaseAsciiOnly()}Value = value
67         }
68         """
69         )
70 
71     override fun getDetector(): Detector = AutoboxingStateCreationDetector()
72 
73     override fun getIssues(): MutableList<Issue> =
74         mutableListOf(AutoboxingStateCreationDetector.AutoboxingStateCreation)
75 
76     @Test
77     fun testTrivialMutableStateOf_thatCouldBeMutablePrimitiveStateOf() {
78         lint()
79             .files(
80                 primitiveStateStub,
81                 Stubs.Composable,
82                 Stubs.SnapshotState,
83                 Stubs.StateFactoryMarker,
84                 kotlin(
85                     """
86                     package androidx.compose.runtime.lint.test
87 
88                     import androidx.compose.runtime.*
89                     import $fqType
90 
91                     fun valueAssignment() {
92                         val state = mutableStateOf<$type>($stateValue)
93                         state.value = $stateValue
94                     }
95                 """
96                 )
97             )
98             .run()
99             .expect(
100                 """
101 src/androidx/compose/runtime/lint/test/test.kt:8: Information: Prefer mutable${type}StateOf instead of mutableStateOf [AutoboxingStateCreation]
102                         val state = mutableStateOf<$type>($stateValue)
103                                     ~~~~~~~~~~~~~~
104 0 errors, 0 warnings
105             """
106             )
107             .expectFixDiffs(
108                 """
109 Fix for src/androidx/compose/runtime/lint/test/test.kt line 8: Replace with mutable${type}StateOf:
110 @@ -8 +8
111 -                         val state = mutableStateOf<$type>($stateValue)
112 +                         val state = mutable${type}StateOf($stateValue)
113             """
114             )
115     }
116 
117     /**
118      * Regression test for b/314093514. Java doesn't allow specifying nullity of the generic type
119      * with either the AndroidX or JetBrains nullity annotations, so we never have enough
120      * information to know whether a mutableState created in Java is capable of being refactored
121      * into the specialized primitive version. Therefore, this inspection should never report for
122      * Java callers.
123      */
124     @Test
testTrivialMutableStateOf_notReportedInJavanull125     fun testTrivialMutableStateOf_notReportedInJava() {
126         lint()
127             .files(
128                 primitiveStateStub,
129                 Stubs.Composable,
130                 Stubs.SnapshotState,
131                 Stubs.StateFactoryMarker,
132                 java(
133                     """
134                     package androidx.compose.runtime.lint.test;
135 
136                     import static androidx.compose.runtime.SnapshotStateKt.mutableStateOf;
137                     import static androidx.compose.runtime.SnapshotStateKt.structuralEqualityPolicy;
138 
139                     import androidx.compose.runtime.*;
140                     import $fqJvmClass;
141 
142                     class Test {
143                         public void valueAssignment() {
144                             MutableState<$jvmType> state = mutableStateOf($stateValue, structuralEqualityPolicy());
145                             state.setValue($stateValue);
146                         }
147                     }
148                 """
149                 )
150             )
151             .run()
152             .expectClean()
153     }
154 
155     @Test
testInferredMutableStateOf_thatCouldBeMutablePrimitiveStateOfnull156     fun testInferredMutableStateOf_thatCouldBeMutablePrimitiveStateOf() {
157         lint()
158             .files(
159                 primitiveStateStub,
160                 Stubs.Composable,
161                 Stubs.SnapshotState,
162                 Stubs.StateFactoryMarker,
163                 kotlin(
164                     """
165                     package androidx.compose.runtime.lint.test
166 
167                     import androidx.compose.runtime.*
168                     import $fqType
169 
170                     fun valueAssignment() {
171                         val state = mutableStateOf($stateValue)
172                         state.value = $stateValue
173                     }
174                 """
175                 )
176             )
177             .run()
178             .expect(
179                 """
180 src/androidx/compose/runtime/lint/test/test.kt:8: Information: Prefer mutable${type}StateOf instead of mutableStateOf [AutoboxingStateCreation]
181                         val state = mutableStateOf($stateValue)
182                                     ~~~~~~~~~~~~~~
183 0 errors, 0 warnings
184             """
185             )
186             .expectFixDiffs(
187                 """
188 Fix for src/androidx/compose/runtime/lint/test/test.kt line 8: Replace with mutable${type}StateOf:
189 @@ -8 +8
190 -                         val state = mutableStateOf($stateValue)
191 +                         val state = mutable${type}StateOf($stateValue)
192             """
193             )
194     }
195 
196     @Test
testFqMutableStateOf_thatCouldBeMutablePrimitiveStateOfnull197     fun testFqMutableStateOf_thatCouldBeMutablePrimitiveStateOf() {
198         lint()
199             .files(
200                 primitiveStateStub,
201                 Stubs.Composable,
202                 Stubs.SnapshotState,
203                 Stubs.StateFactoryMarker,
204                 kotlin(
205                     """
206                     package androidx.compose.runtime.lint.test
207 
208                     import androidx.compose.runtime.*
209                     import $fqType
210 
211                     fun valueAssignment() {
212                         val state = mutableStateOf<$fqType>($stateValue)
213                         state.value = $stateValue
214                     }
215                 """
216                 )
217             )
218             .run()
219             .expect(
220                 """
221 src/androidx/compose/runtime/lint/test/test.kt:8: Information: Prefer mutable${type}StateOf instead of mutableStateOf [AutoboxingStateCreation]
222                         val state = mutableStateOf<$fqType>($stateValue)
223                                     ~~~~~~~~~~~~~~
224 0 errors, 0 warnings
225             """
226             )
227             .expectFixDiffs(
228                 """
229 Fix for src/androidx/compose/runtime/lint/test/test.kt line 8: Replace with mutable${type}StateOf:
230 @@ -8 +8
231 -                         val state = mutableStateOf<$fqType>($stateValue)
232 +                         val state = mutable${type}StateOf($stateValue)
233             """
234             )
235     }
236 
237     @Test
testStateDelegate_withExplicitType_thatCouldBeMutablePrimitiveStateOfnull238     fun testStateDelegate_withExplicitType_thatCouldBeMutablePrimitiveStateOf() {
239         lint()
240             .files(
241                 primitiveStateStub,
242                 Stubs.Composable,
243                 Stubs.SnapshotState,
244                 Stubs.StateFactoryMarker,
245                 kotlin(
246                     """
247                     package androidx.compose.runtime.lint.test
248 
249                     import androidx.compose.runtime.*
250                     import $fqType
251 
252                     fun propertyDelegation() {
253                         var state by mutableStateOf<$type>($stateValue)
254                         state = $stateValue
255                     }
256                 """
257                 )
258             )
259             .run()
260             .expect(
261                 """
262 src/androidx/compose/runtime/lint/test/test.kt:8: Information: Prefer mutable${type}StateOf instead of mutableStateOf [AutoboxingStateCreation]
263                         var state by mutableStateOf<$type>($stateValue)
264                                      ~~~~~~~~~~~~~~
265 0 errors, 0 warnings
266             """
267             )
268             .expectFixDiffs(
269                 """
270 Fix for src/androidx/compose/runtime/lint/test/test.kt line 8: Replace with mutable${type}StateOf:
271 @@ -8 +8
272 -                         var state by mutableStateOf<$type>($stateValue)
273 +                         var state by mutable${type}StateOf($stateValue)
274             """
275             )
276     }
277 
278     @Test
testStateDelegate_withInferredType_thatCouldBeMutablePrimitiveStateOfnull279     fun testStateDelegate_withInferredType_thatCouldBeMutablePrimitiveStateOf() {
280         lint()
281             .files(
282                 primitiveStateStub,
283                 Stubs.Composable,
284                 Stubs.SnapshotState,
285                 Stubs.StateFactoryMarker,
286                 kotlin(
287                     """
288                     package androidx.compose.runtime.lint.test
289 
290                     import androidx.compose.runtime.*
291                     import $fqType
292 
293                     fun propertyDelegation() {
294                         var state by mutableStateOf($stateValue)
295                         state = $stateValue
296                     }
297                 """
298                 )
299             )
300             .run()
301             .expect(
302                 """
303 src/androidx/compose/runtime/lint/test/test.kt:8: Information: Prefer mutable${type}StateOf instead of mutableStateOf [AutoboxingStateCreation]
304                         var state by mutableStateOf($stateValue)
305                                      ~~~~~~~~~~~~~~
306 0 errors, 0 warnings
307             """
308             )
309             .expectFixDiffs(
310                 """
311 Fix for src/androidx/compose/runtime/lint/test/test.kt line 8: Replace with mutable${type}StateOf:
312 @@ -8 +8
313 -                         var state by mutableStateOf($stateValue)
314 +                         var state by mutable${type}StateOf($stateValue)
315             """
316             )
317     }
318 
319     @Test
testStateDelegate_withInferredType_andInternalSetter_thatCouldBeMutablePrimitiveStateOfnull320     fun testStateDelegate_withInferredType_andInternalSetter_thatCouldBeMutablePrimitiveStateOf() {
321         lint()
322             .files(
323                 primitiveStateStub,
324                 Stubs.Composable,
325                 Stubs.SnapshotState,
326                 Stubs.StateFactoryMarker,
327                 kotlin(
328                     """
329                 package androidx.compose.runtime.lint.test
330 
331                 import androidx.compose.runtime.*
332                 import $fqType
333 
334                 class Test(initialValue: $type = $stateValue) {
335                     var state by mutableStateOf(initialValue)
336                         private set
337                 }
338             """
339                 )
340             )
341             .run()
342             .expect(
343                 """
344 src/androidx/compose/runtime/lint/test/Test.kt:8: Information: Prefer mutable${type}StateOf instead of mutableStateOf [AutoboxingStateCreation]
345                     var state by mutableStateOf(initialValue)
346                                  ~~~~~~~~~~~~~~
347 0 errors, 0 warnings
348             """
349             )
350             .expectFixDiffs(
351                 """
352 Fix for src/androidx/compose/runtime/lint/test/Test.kt line 8: Replace with mutable${type}StateOf:
353 @@ -8 +8
354 -                     var state by mutableStateOf(initialValue)
355 +                     var state by mutable${type}StateOf(initialValue)
356             """
357             )
358     }
359 
360     @Test
testStateDelegate_withTypeInferredFromProperty_thatCouldBeMutablePrimitiveStateOfnull361     fun testStateDelegate_withTypeInferredFromProperty_thatCouldBeMutablePrimitiveStateOf() {
362         lint()
363             .files(
364                 primitiveStateStub,
365                 Stubs.Composable,
366                 Stubs.SnapshotState,
367                 Stubs.StateFactoryMarker,
368                 kotlin(
369                     """
370                     package androidx.compose.runtime.lint.test
371 
372                     import androidx.compose.runtime.*
373                     import $fqType
374 
375                     fun propertyDelegation() {
376                         var state: $type by mutableStateOf($stateValue)
377                         state = $stateValue
378                     }
379                 """
380                 )
381             )
382             .run()
383             .expect(
384                 """
385 src/androidx/compose/runtime/lint/test/test.kt:8: Information: Prefer mutable${type}StateOf instead of mutableStateOf [AutoboxingStateCreation]
386                         var state: $type by mutableStateOf($stateValue)
387                                    ${" ".repeat(type.length)}    ~~~~~~~~~~~~~~
388 0 errors, 0 warnings
389             """
390             )
391             .expectFixDiffs(
392                 """
393 Fix for src/androidx/compose/runtime/lint/test/test.kt line 8: Replace with mutable${type}StateOf:
394 @@ -8 +8
395 -                         var state: $type by mutableStateOf($stateValue)
396 +                         var state: $type by mutable${type}StateOf($stateValue)
397             """
398             )
399     }
400 
401     @Test
testStateDelegate_withNullableInferredType_cannotBeReplacedWithMutablePrimitiveStateOfnull402     fun testStateDelegate_withNullableInferredType_cannotBeReplacedWithMutablePrimitiveStateOf() {
403         lint()
404             .files(
405                 primitiveStateStub,
406                 Stubs.Composable,
407                 Stubs.SnapshotState,
408                 Stubs.StateFactoryMarker,
409                 kotlin(
410                     """
411                     package androidx.compose.runtime.lint.test
412 
413                     import androidx.compose.runtime.*
414                     import $fqType
415 
416                     fun propertyDelegation() {
417                         var state: $type? by mutableStateOf($stateValue)
418                         state = $stateValue
419                     }
420                 """
421                 )
422             )
423             .run()
424             .expectClean()
425     }
426 
427     @Test
testInferredMutableStateOf_withExplicitEqualityPolicy_thatCouldBeMutablePrimitiveStateOfnull428     fun testInferredMutableStateOf_withExplicitEqualityPolicy_thatCouldBeMutablePrimitiveStateOf() {
429         lint()
430             .files(
431                 primitiveStateStub,
432                 Stubs.Composable,
433                 Stubs.SnapshotState,
434                 Stubs.StateFactoryMarker,
435                 kotlin(
436                     """
437                     package androidx.compose.runtime.lint.test
438 
439                     import androidx.compose.runtime.*
440                     import $fqType
441 
442                     fun valueAssignment() {
443                         val state = mutableStateOf($stateValue, structuralEqualityPolicy())
444                         state.value = $stateValue
445                     }
446                 """
447                 )
448             )
449             .run()
450             .expect(
451                 """
452 src/androidx/compose/runtime/lint/test/test.kt:8: Information: Prefer mutable${type}StateOf instead of mutableStateOf [AutoboxingStateCreation]
453                         val state = mutableStateOf($stateValue, structuralEqualityPolicy())
454                                     ~~~~~~~~~~~~~~
455 0 errors, 0 warnings
456             """
457             )
458             .expectFixDiffs(
459                 """
460 Fix for src/androidx/compose/runtime/lint/test/test.kt line 8: Replace with mutable${type}StateOf:
461 @@ -8 +8
462 -                         val state = mutableStateOf($stateValue, structuralEqualityPolicy())
463 +                         val state = mutable${type}StateOf($stateValue)
464             """
465             )
466     }
467 
468     @Test
testNonStructuralEqualityPolicy_cannotBeReplacedWithMutablePrimitiveStateOfnull469     fun testNonStructuralEqualityPolicy_cannotBeReplacedWithMutablePrimitiveStateOf() {
470         lint()
471             .files(
472                 primitiveStateStub,
473                 Stubs.Composable,
474                 Stubs.SnapshotState,
475                 Stubs.StateFactoryMarker,
476                 kotlin(
477                     """
478                     package androidx.compose.runtime.lint.test
479 
480                     import androidx.compose.runtime.*
481                     import $fqType
482 
483                     fun valueAssignment() {
484                         val state = mutableStateOf($stateValue, neverEqualPolicy())
485                         state.value = $stateValue
486                     }
487                 """
488                 )
489             )
490             .run()
491             .expectClean()
492     }
493 
494     @Test
testNullableMutableStateOf_cannotBeReplacedWithMutablePrimitiveStateOfnull495     fun testNullableMutableStateOf_cannotBeReplacedWithMutablePrimitiveStateOf() {
496         lint()
497             .files(
498                 primitiveStateStub,
499                 Stubs.Composable,
500                 Stubs.SnapshotState,
501                 Stubs.StateFactoryMarker,
502                 kotlin(
503                     """
504                     package androidx.compose.runtime.lint.test
505 
506                     import androidx.compose.runtime.*
507                     import $fqType
508 
509                     fun valueAssignment() {
510                         val state = mutableStateOf<$type?>($stateValue)
511                         state.value = $stateValue
512                     }
513                 """
514                 )
515             )
516             .run()
517             .expectClean()
518     }
519 
520     @Test
testInferredNullableMutableStateOf_cannotBeReplacedWithMutablePrimitiveStateOfnull521     fun testInferredNullableMutableStateOf_cannotBeReplacedWithMutablePrimitiveStateOf() {
522         lint()
523             .files(
524                 primitiveStateStub,
525                 Stubs.Composable,
526                 Stubs.SnapshotState,
527                 Stubs.StateFactoryMarker,
528                 kotlin(
529                     """
530                     package androidx.compose.runtime.lint.test
531 
532                     import androidx.compose.runtime.*
533                     import $fqType
534 
535                     fun valueAssignment() {
536                         val state: MutableState<$type?> = mutableStateOf($stateValue)
537                         state.value = $stateValue
538                     }
539                 """
540                 )
541             )
542             .run()
543             .expectClean()
544     }
545 
546     @Test
testInferredByCastNullableMutableStateOf_cannotBeReplacedWithMutablePrimitiveStateOfnull547     fun testInferredByCastNullableMutableStateOf_cannotBeReplacedWithMutablePrimitiveStateOf() {
548         lint()
549             .files(
550                 primitiveStateStub,
551                 Stubs.Composable,
552                 Stubs.SnapshotState,
553                 Stubs.StateFactoryMarker,
554                 kotlin(
555                     """
556                     package androidx.compose.runtime.lint.test
557 
558                     import androidx.compose.runtime.*
559                     import $fqType
560 
561                     fun valueAssignment() {
562                         val state = mutableStateOf($stateValue as $type?)
563                         state.value = $stateValue
564                     }
565                 """
566                 )
567             )
568             .run()
569             .expectClean()
570     }
571 
572     companion object {
573         @JvmStatic
574         @Parameterized.Parameters(name = "{0}")
initParametersnull575         fun initParameters() =
576             listOf(
577                 testCase("kotlin.Int", "java.lang.Integer", "42"),
578                 testCase("kotlin.Long", "java.lang.Long", "0xABCDEF1234"),
579                 testCase("kotlin.Float", "java.lang.Float", "1.5f"),
580                 testCase("kotlin.Double", "java.lang.Double", "1.024")
581             )
582 
583         private fun testCase(fqName: String, jvmFqName: String, value: String) =
584             TypeUnderTest(
585                 fqName = fqName,
586                 typeName = fqName.split('.').last(),
587                 fqJvmName = jvmFqName,
588                 jvmClassName = jvmFqName.split('.').last(),
589                 sampleValue = value
590             )
591     }
592 
593     data class TypeUnderTest(
594         val fqName: String,
595         val typeName: String,
596         val fqJvmName: String,
597         val jvmClassName: String,
598         val sampleValue: String,
599     ) {
600         // Formatting for test parameter list.
601         override fun toString() = "type = $fqName"
602     }
603 }
604