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.inheritsFrom
23 import androidx.compose.lint.isInPackageName
24 import com.android.tools.lint.detector.api.Category
25 import com.android.tools.lint.detector.api.Context
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 com.intellij.psi.PsiMethod
34 import java.util.EnumSet
35 import org.jetbrains.uast.UCallExpression
36 import org.jetbrains.uast.UElement
37 import org.jetbrains.uast.UExpression
38 import org.jetbrains.uast.getContainingDeclaration
39 import org.jetbrains.uast.tryResolve
40 import org.jetbrains.uast.visitor.AbstractUastVisitor
41 import org.jetbrains.uast.withContainingElements
42 
43 class MultipleAwaitPointerEventScopesDetector : Detector(), SourceCodeScanner {
44 
getApplicableMethodNamesnull45     override fun getApplicableMethodNames(): List<String> =
46         listOf(Names.Ui.Pointer.AwaitPointerEventScope.shortName)
47 
48     // Our approach is to go up the file tree and find all awaitPointerEventScopes under
49     // a given parent. We might report the same node more than once, so we keep track of reported
50     // nodes to avoid duplicated reporting.
51     private val reportedNodes = mutableSetOf<UElement>()
52 
53     override fun afterCheckFile(context: Context) {
54         reportedNodes.clear()
55     }
56 
visitMethodCallnull57     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
58         // Not defined in UI Pointer Input
59         if (!method.isInPackageName(Names.Ui.Pointer.PackageName)) return
60         val containingDeclaration = node.getContainingDeclaration()
61 
62         // The element we will look inside
63         val boundaryElement =
64             node.withContainingElements.first {
65                 // Reached the containing function / lambda
66                 // or Found a modifier expression - don't look outside the scope of this modifier
67                 it == containingDeclaration ||
68                     (it is UExpression) &&
69                         it.getExpressionType()?.inheritsFrom(Names.Ui.Modifier) == true
70             }
71 
72         val awaitPointerEventCalls = searchAwaitPointerScopeCalls(boundaryElement)
73 
74         // loop block contains the correct amount of awaitPointerEventScope calls (1 or none)
75         if (awaitPointerEventCalls <= 1) return
76 
77         // If loop contains more than one awaitPointerEventScope we should report if we haven't done
78         // so before.
79         if (!reportedNodes.contains(node)) {
80             context.report(
81                 MultipleAwaitPointerEventScopes,
82                 node,
83                 context.getNameLocation(node),
84                 ErrorMessage
85             )
86             reportedNodes.add(node)
87         }
88     }
89 
searchAwaitPointerScopeCallsnull90     private fun searchAwaitPointerScopeCalls(parent: UElement): Int {
91         var awaitPointerEventCallsCount = 0
92         parent.accept(
93             object : AbstractUastVisitor() {
94                 override fun visitCallExpression(node: UCallExpression): Boolean {
95                     val method = node.tryResolve() as? PsiMethod ?: return false
96                     if (!method.isInPackageName(Names.Ui.Pointer.PackageName)) return false
97 
98                     if (method.name == Names.Ui.Pointer.AwaitPointerEventScope.shortName) {
99                         awaitPointerEventCallsCount++
100                     }
101                     return false
102                 }
103             }
104         )
105 
106         return awaitPointerEventCallsCount
107     }
108 
109     companion object {
110         const val IssueId: String = "MultipleAwaitPointerEventScopes"
111         const val ErrorMessage =
112             "Suspicious use of multiple awaitPointerEventScope blocks. Using " +
113                 "multiple awaitPointerEventScope blocks may cause some input events to be dropped."
114         val MultipleAwaitPointerEventScopes =
115             Issue.create(
116                 IssueId,
117                 ErrorMessage,
118                 "Pointer Input events are queued inside awaitPointerEventScope. Multiple " +
119                     "calls to awaitPointerEventScope may exit the scope. During this time " +
120                     "there is no guarantee that the events will be queued and some " +
121                     "events may be dropped. It is recommended to use a single top-level block and " +
122                     "perform the pointer events processing within such block.",
123                 Category.CORRECTNESS,
124                 3,
125                 Severity.WARNING,
126                 Implementation(
127                     MultipleAwaitPointerEventScopesDetector::class.java,
128                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
129                 )
130             )
131     }
132 }
133