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