1 /* 2 * Copyright 2020 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 @file:Suppress("UnstableApiUsage") 18 19 package androidx.compose.runtime.lint 20 21 import androidx.compose.lint.Names 22 import androidx.compose.lint.inheritsFrom 23 import androidx.compose.lint.isNotRemembered 24 import com.android.tools.lint.client.api.UElementHandler 25 import com.android.tools.lint.detector.api.Category 26 import com.android.tools.lint.detector.api.Detector 27 import com.android.tools.lint.detector.api.Implementation 28 import com.android.tools.lint.detector.api.Issue 29 import com.android.tools.lint.detector.api.JavaContext 30 import com.android.tools.lint.detector.api.Scope 31 import com.android.tools.lint.detector.api.Severity 32 import com.android.tools.lint.detector.api.SourceCodeScanner 33 import java.util.EnumSet 34 import org.jetbrains.uast.UCallExpression 35 import org.jetbrains.uast.UElement 36 import org.jetbrains.uast.UObjectLiteralExpression 37 import org.jetbrains.uast.UastCallKind.Companion.CONSTRUCTOR_CALL 38 39 /** 40 * [Detector] that checks `derivedStateOf`, `mutableStateOf`, `mutableStateListOf`, and 41 * `mutableStateMapOf` calls to make sure that if they are called inside a Composable body, they are 42 * `remember`ed. 43 */ 44 class UnrememberedStateDetector : Detector(), SourceCodeScanner { getApplicableUastTypesnull45 override fun getApplicableUastTypes(): List<Class<out UElement>> { 46 return listOf(UCallExpression::class.java, UObjectLiteralExpression::class.java) 47 } 48 createUastHandlernull49 override fun createUastHandler(context: JavaContext): UElementHandler { 50 return object : UElementHandler() { 51 override fun visitCallExpression(node: UCallExpression) { 52 if (node.isUnrememberedStateCreation()) { 53 context.report( 54 UnrememberedState, 55 node, 56 context.getNameLocation(node), 57 "Creating a state object during composition without using `remember`" 58 ) 59 } 60 } 61 62 override fun visitObjectLiteralExpression(node: UObjectLiteralExpression) { 63 if (node.isStateObjectLiteral() && node.isNotRemembered()) { 64 context.report( 65 UnrememberedState, 66 node, 67 context.getNameLocation(node), 68 "Creating a state object during composition without using `remember`" 69 ) 70 } 71 } 72 } 73 } 74 UCallExpressionnull75 private fun UCallExpression.isUnrememberedStateCreation(): Boolean = 76 (isStateFactoryInvocation() || isStateConstructorInvocation()) && isNotRemembered() 77 78 private fun UCallExpression.isStateFactoryInvocation(): Boolean = 79 resolve()?.annotations?.any { it.hasQualifiedName(FqStateFactoryAnnotationName) } ?: false 80 isStateConstructorInvocationnull81 private fun UCallExpression.isStateConstructorInvocation(): Boolean = 82 (kind == CONSTRUCTOR_CALL) && (returnType?.inheritsFrom(Names.Runtime.State) == true) 83 84 private fun UObjectLiteralExpression.isStateObjectLiteral(): Boolean = 85 (getExpressionType()?.inheritsFrom(Names.Runtime.State) == true) 86 87 companion object { 88 private const val FqStateFactoryAnnotationName = 89 "androidx.compose.runtime.snapshots.StateFactoryMarker" 90 91 val UnrememberedState = 92 Issue.create( 93 "UnrememberedMutableState", // Left as previous id for backwards compatibility 94 "Creating a state object during composition without using `remember`", 95 "State objects created during composition need to be `remember`ed, otherwise " + 96 "they will be recreated during recomposition, and lose their state. Either hoist " + 97 "the state to an object that is not created during composition, or wrap the " + 98 "state in a call to `remember`.", 99 Category.CORRECTNESS, 100 3, 101 Severity.ERROR, 102 Implementation( 103 UnrememberedStateDetector::class.java, 104 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) 105 ) 106 ) 107 } 108 } 109