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