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