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