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