1 /*
2  * Copyright 2021 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.Name
22 import androidx.compose.lint.Package
23 import androidx.compose.lint.inheritsFrom
24 import androidx.compose.lint.isInvokedWithinComposable
25 import com.android.tools.lint.client.api.UElementHandler
26 import com.android.tools.lint.detector.api.Category
27 import com.android.tools.lint.detector.api.Detector
28 import com.android.tools.lint.detector.api.Implementation
29 import com.android.tools.lint.detector.api.Issue
30 import com.android.tools.lint.detector.api.JavaContext
31 import com.android.tools.lint.detector.api.Scope
32 import com.android.tools.lint.detector.api.Severity
33 import com.android.tools.lint.detector.api.SourceCodeScanner
34 import com.intellij.psi.PsiMethod
35 import com.intellij.psi.util.parentOfType
36 import java.util.EnumSet
37 import org.jetbrains.kotlin.analysis.api.analyze
38 import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
39 import org.jetbrains.kotlin.name.ClassId
40 import org.jetbrains.kotlin.name.FqName
41 import org.jetbrains.kotlin.psi.KtClass
42 import org.jetbrains.kotlin.psi.KtProperty
43 import org.jetbrains.uast.USimpleNameReferenceExpression
44 import org.jetbrains.uast.tryResolve
45 
46 /**
47  * [Detector] that checks calls to StateFlow.value to make sure they don't happen inside the body of
48  * a composable function / lambda.
49  */
50 class ComposableStateFlowValueDetector : Detector(), SourceCodeScanner {
getApplicableUastTypesnull51     override fun getApplicableUastTypes() = listOf(USimpleNameReferenceExpression::class.java)
52 
53     override fun createUastHandler(context: JavaContext) =
54         object : UElementHandler() {
55             override fun visitSimpleNameReferenceExpression(node: USimpleNameReferenceExpression) {
56                 // Look for a call to .value that comes from StateFlow
57                 if (node.identifier != "value") return
58                 val psiElement = node.tryResolve()
59                 val inheritsFromStateFlow =
60                     when (psiElement) {
61                         // PsiMethod is expected in Android/JVM source sets
62                         is PsiMethod ->
63                             psiElement.containingClass?.inheritsFrom(StateFlowName) == true
64                         // KtProperty is expected in common source sets
65                         is KtProperty -> {
66                             val thisClass = psiElement.parentOfType<KtClass>() ?: return
67                             analyze(thisClass) {
68                                 val symbol = thisClass.getSymbol() as KtClassOrObjectSymbol
69                                 val baseClassId = ClassId.topLevel(FqName(StateFlowName.javaFqn))
70                                 val baseClassSymbol = getClassOrObjectSymbolByClassId(baseClassId)
71                                 symbol.isSubClassOf(baseClassSymbol ?: return@analyze false)
72                             }
73                         }
74                         else -> false
75                     }
76                 if (inheritsFromStateFlow) {
77                     if (node.isInvokedWithinComposable()) {
78                         context.report(
79                             StateFlowValueCalledInComposition,
80                             node,
81                             context.getNameLocation(node),
82                             "StateFlow.value should not be called within composition",
83                             fix()
84                                 .replace()
85                                 .text("value")
86                                 .with("collectAsState().value")
87                                 .imports("androidx.compose.runtime.collectAsState")
88                                 .build()
89                         )
90                     }
91                 }
92             }
93         }
94 
95     companion object {
96         val StateFlowValueCalledInComposition =
97             Issue.create(
98                 "StateFlowValueCalledInComposition",
99                 "StateFlow.value should not be called within composition",
100                 "Calling StateFlow.value within composition will not observe changes to the " +
101                     "StateFlow, so changes might not be reflected within the composition. Instead " +
102                     "you should use stateFlow.collectAsState() to observe changes to the StateFlow, " +
103                     "and recompose when it changes.",
104                 Category.CORRECTNESS,
105                 3,
106                 Severity.ERROR,
107                 Implementation(
108                     ComposableStateFlowValueDetector::class.java,
109                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
110                 )
111             )
112     }
113 }
114 
115 private val StateFlowPackageName = Package("kotlinx.coroutines.flow")
116 private val StateFlowName = Name(StateFlowPackageName, "StateFlow")
117