1 /* 2 * Copyright 2022 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.ui.lint 20 21 import androidx.compose.lint.Names 22 import androidx.compose.lint.isInPackageName 23 import com.android.tools.lint.detector.api.Category 24 import com.android.tools.lint.detector.api.Detector 25 import com.android.tools.lint.detector.api.Implementation 26 import com.android.tools.lint.detector.api.Issue 27 import com.android.tools.lint.detector.api.JavaContext 28 import com.android.tools.lint.detector.api.Scope 29 import com.android.tools.lint.detector.api.Severity 30 import com.android.tools.lint.detector.api.SourceCodeScanner 31 import com.intellij.psi.PsiMethod 32 import java.util.EnumSet 33 import org.jetbrains.kotlin.analysis.api.analyze 34 import org.jetbrains.kotlin.analysis.api.types.KtFunctionalType 35 import org.jetbrains.kotlin.psi.KtLambdaExpression 36 import org.jetbrains.uast.UCallExpression 37 import org.jetbrains.uast.UElement 38 import org.jetbrains.uast.UExpression 39 import org.jetbrains.uast.ULambdaExpression 40 import org.jetbrains.uast.ULocalVariable 41 import org.jetbrains.uast.UReturnExpression 42 import org.jetbrains.uast.UVariable 43 import org.jetbrains.uast.skipParenthesizedExprUp 44 45 class ReturnFromAwaitPointerEventScopeDetector : Detector(), SourceCodeScanner { getApplicableMethodNamesnull46 override fun getApplicableMethodNames(): List<String> = 47 listOf(Names.Ui.Pointer.AwaitPointerEventScope.shortName) 48 49 override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) { 50 if (!method.isInPackageName(Names.Ui.Pointer.PackageName)) return 51 52 val methodParent = skipParenthesizedExprUp(node.uastParent) 53 val isAssignedToVariable = methodParent is ULocalVariable 54 55 val isReturnExpression = methodParent is UReturnExpression 56 57 val invalidUseOfAwaitPointerEventScopeWithReturn = 58 isReturnExpression && !validUseOfAwaitPointerEventScopeWithReturn(node) 59 60 if (isAssignedToVariable || invalidUseOfAwaitPointerEventScopeWithReturn) { 61 context.report( 62 ExitAwaitPointerEventScope, 63 node, 64 context.getNameLocation(node), 65 ErrorMessage 66 ) 67 } 68 } 69 validUseOfAwaitPointerEventScopeWithReturnnull70 private fun validUseOfAwaitPointerEventScopeWithReturn( 71 awaitPointerEventScopeNode: UCallExpression 72 ): Boolean { 73 // Traverse up the UAST tree 74 var currentNode: UElement? = awaitPointerEventScopeNode.uastParent 75 while (currentNode != null) { 76 // Check if awaitPointerEventScope is within a PointerInputEventHandler or a 77 // pointerInput method call (making it a valid use of return). 78 if ( 79 currentNode is UCallExpression && 80 (currentNode.methodName == POINTER_INPUT_HANDLER || 81 currentNode.methodName == POINTER_INPUT_METHOD || 82 currentNode.methodName == COROUTINE_METHOD) 83 ) { 84 return true 85 } 86 87 // For backward compatibility, checks if awaitPointerEventScopeNode is returned to a 88 // "suspend PointerInputScope.() -> Unit" type variable (see test 89 // awaitPointerEventScope_assignedFromContainingLambdaMethod_shouldNotWarn() ). 90 if (currentNode is UVariable) { 91 val variable = currentNode 92 val lambda: UExpression? = variable.uastInitializer 93 94 // Check if the initializer is a suspend lambda with the correct type 95 if (lambda is ULambdaExpression) { 96 val ktLambdaExpression = lambda.sourcePsi 97 if ( 98 ktLambdaExpression is KtLambdaExpression && 99 isSuspendPointerInputLambda(ktLambdaExpression) 100 ) { 101 return true 102 } 103 } 104 } 105 currentNode = currentNode.uastParent 106 } 107 return false 108 } 109 110 // Helper function for lambda type check isSuspendPointerInputLambdanull111 private fun isSuspendPointerInputLambda(ktLambdaExpression: KtLambdaExpression): Boolean { 112 return analyze(ktLambdaExpression) { 113 val type = ktLambdaExpression.getExpectedType() as? KtFunctionalType ?: return false 114 type.isSuspendFunctionType && 115 type.receiverType?.expandedClassSymbol?.classIdIfNonLocal?.asFqNameString() == 116 POINTER_INPUT_SCOPE 117 } 118 } 119 120 companion object { 121 private const val POINTER_INPUT_SCOPE = 122 "androidx.compose.ui.input.pointer.PointerInputScope" 123 private const val POINTER_INPUT_HANDLER = "PointerInputEventHandler" 124 private const val POINTER_INPUT_METHOD = "pointerInput" 125 private const val COROUTINE_METHOD = "coroutineScope" 126 127 const val IssueId: String = "ReturnFromAwaitPointerEventScope" 128 const val ErrorMessage = 129 "Returning from awaitPointerEventScope may cause some input " + "events to be dropped" 130 val ExitAwaitPointerEventScope = 131 Issue.create( 132 IssueId, 133 ErrorMessage, 134 "Pointer Input events are queued inside awaitPointerEventScope. " + 135 "By using the return value of awaitPointerEventScope one might unexpectedly lose " + 136 "events. If another awaitPointerEventScope is restarted " + 137 "there is no guarantee that the events will persist between those calls. In this " + 138 "case you should keep all events inside the awaitPointerEventScope block", 139 Category.CORRECTNESS, 140 3, 141 Severity.WARNING, 142 Implementation( 143 ReturnFromAwaitPointerEventScopeDetector::class.java, 144 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) 145 ) 146 ) 147 } 148 } 149