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  *      https://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 com.google.accompanist.permissions.lint
20 
21 import com.android.tools.lint.detector.api.Category
22 import com.android.tools.lint.detector.api.Detector
23 import com.android.tools.lint.detector.api.Implementation
24 import com.android.tools.lint.detector.api.Issue
25 import com.android.tools.lint.detector.api.JavaContext
26 import com.android.tools.lint.detector.api.Scope
27 import com.android.tools.lint.detector.api.Severity
28 import com.android.tools.lint.detector.api.SourceCodeScanner
29 import com.google.accompanist.permissions.lint.util.Name
30 import com.google.accompanist.permissions.lint.util.Package
31 import com.google.accompanist.permissions.lint.util.PackageName
32 import com.google.accompanist.permissions.lint.util.isInvokedWithinComposable
33 import com.intellij.psi.PsiJavaFile
34 import com.intellij.psi.PsiMethod
35 import org.jetbrains.uast.UCallExpression
36 import java.util.EnumSet
37 
38 /**
39  * [Detector] that checks `PermissionState.launchPermissionRequest` and
40  * `MultiplePermissionsState.launchMultiplePermissionRequest` calls to make sure they don't happen
41  * inside the body of a composable function / lambda.
42  */
43 public class PermissionsLaunchDetector : Detector(), SourceCodeScanner {
44 
getApplicableMethodNamesnull45     override fun getApplicableMethodNames(): List<String> = listOf(
46         LaunchPermissionRequest.shortName, LaunchMultiplePermissionsRequest.shortName
47     )
48 
49     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
50         if (!method.isInPackageName(PermissionsPackageName)) return
51 
52         if (node.isInvokedWithinComposable()) {
53             context.report(
54                 PermissionLaunchedDuringComposition,
55                 node,
56                 context.getNameLocation(node),
57                 "Calls to ${method.name} should happen inside a regular lambda or " +
58                     " a side-effect, but never in the Composition."
59             )
60         }
61     }
62 
63     public companion object {
64         public val PermissionLaunchedDuringComposition: Issue = Issue.create(
65             "PermissionLaunchedDuringComposition",
66             "Calls to `launchPermissionRequest` or `launchMultiplePermissionRequest` " +
67                 "should happen inside a regular lambda or a side-effect but never in the " +
68                 "Composition.",
69             "Calls to `launchPermissionRequest` or `launchMultiplePermissionRequest` " +
70                 "in the Composition throw a runtime exception. Please call them inside a regular " +
71                 "lambda or in a side-effect.",
72             Category.CORRECTNESS, 3, Severity.ERROR,
73             Implementation(
74                 PermissionsLaunchDetector::class.java,
75                 EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
76             )
77         )
78     }
79 }
80 
81 /**
82  * Returns whether [this] has [packageName] as its package name.
83  */
isInPackageNamenull84 private fun PsiMethod.isInPackageName(packageName: PackageName): Boolean =
85     packageName.javaPackageName == (containingFile as? PsiJavaFile)?.packageName
86 
87 private val PermissionsPackageName = Package("com.google.accompanist.permissions")
88 private val LaunchPermissionRequest =
89     Name(PermissionsPackageName, "launchPermissionRequest")
90 private val LaunchMultiplePermissionsRequest =
91     Name(PermissionsPackageName, "launchMultiplePermissionRequest")
92