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  *      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.navigation.compose.lint
20 
21 import androidx.compose.lint.Name
22 import androidx.compose.lint.Package
23 import androidx.compose.lint.isInPackageName
24 import androidx.compose.lint.isInvokedWithinComposable
25 import com.android.tools.lint.detector.api.Category
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 
37 /**
38  * [Detector] that checks `composable` calls to make sure that they are not called inside a
39  * Composable body.
40  */
41 class ComposableDestinationInComposeScopeDetector : Detector(), SourceCodeScanner {
getApplicableMethodNamesnull42     override fun getApplicableMethodNames(): List<String> =
43         listOf(Composable.shortName, Navigation.shortName)
44 
45     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
46         if (!method.isInPackageName(PackageName)) return
47 
48         if (node.isInvokedWithinComposable()) {
49             if (method.name == Composable.shortName) {
50                 context.report(
51                     ComposableDestinationInComposeScope,
52                     node,
53                     context.getNameLocation(node),
54                     "Using composable inside of a compose scope"
55                 )
56             } else {
57                 context.report(
58                     ComposableNavGraphInComposeScope,
59                     node,
60                     context.getNameLocation(node),
61                     "Using navigation inside of a compose scope"
62                 )
63             }
64         }
65     }
66 
67     companion object {
68         val ComposableDestinationInComposeScope =
69             Issue.create(
70                 "ComposableDestinationInComposeScope",
71                 "Building composable destination in compose scope",
72                 "Composable destinations should only be constructed directly within a " +
73                     "NavGraphBuilder scope. Composable destinations cannot be nested, and you " +
74                     "should use the `navigation` function to create a nested graph instead.",
75                 Category.CORRECTNESS,
76                 3,
77                 Severity.ERROR,
78                 Implementation(
79                     ComposableDestinationInComposeScopeDetector::class.java,
80                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
81                 )
82             )
83         val ComposableNavGraphInComposeScope =
84             Issue.create(
85                 "ComposableNavGraphInComposeScope",
86                 "Building navigation graph in compose scope",
87                 "Composable destinations should only be constructed directly within a " +
88                     "NavGraphBuilder scope.",
89                 Category.CORRECTNESS,
90                 3,
91                 Severity.ERROR,
92                 Implementation(
93                     ComposableDestinationInComposeScopeDetector::class.java,
94                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
95                 )
96             )
97     }
98 }
99 
100 private val PackageName = Package("androidx.navigation.compose")
101 private val Composable = Name(PackageName, "composable")
102 private val Navigation = Name(PackageName, "navigation")
103