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