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