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.isNotRememberedWithKeys
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 `getBackStackEntry` calls to make sure that if they are called inside a
39  * Composable body, they are `remember`ed with a `NavBackStackEntry` as a key.
40  */
41 class UnrememberedGetBackStackEntryDetector : Detector(), SourceCodeScanner {
getApplicableMethodNamesnull42     override fun getApplicableMethodNames(): List<String> = listOf(GetBackStackEntry.shortName)
43 
44     override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
45         if (!method.isInPackageName(PackageName)) return
46 
47         if (node.isNotRememberedWithKeys(NavBackStackEntry)) {
48             context.report(
49                 UnrememberedGetBackStackEntry,
50                 node,
51                 context.getNameLocation(node),
52                 "Calling getBackStackEntry during composition without using `remember` " +
53                     "with a NavBackStackEntry key"
54             )
55         }
56     }
57 
58     companion object {
59         val UnrememberedGetBackStackEntry =
60             Issue.create(
61                 "UnrememberedGetBackStackEntry",
62                 "Calling getBackStackEntry during composition without using `remember`" +
63                     "with a NavBackStackEntry key",
64                 "Backstack entries retrieved during composition need to be `remember`ed, otherwise " +
65                     "they will be retrieved from the navController again, and be changed. You also " +
66                     "need to pass in a key of a NavBackStackEntry to the remember call or they will " +
67                     "not be updated properly. If this is in a `NavGraphBuilder.composable` scope, " +
68                     "you should pass in the lambda's given entry as the key. Either hoist the state " +
69                     "to an object that is not created during composition, or wrap the state in a " +
70                     "call to `remember` with a `NavBackStackEntry` as a key.",
71                 Category.CORRECTNESS,
72                 3,
73                 Severity.ERROR,
74                 Implementation(
75                     UnrememberedGetBackStackEntryDetector::class.java,
76                     EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
77                 )
78             )
79     }
80 }
81 
82 private val PackageName = Package("androidx.navigation")
83 private val GetBackStackEntry = Name(PackageName, "getBackStackEntry")
84 private val NavBackStackEntry = Name(PackageName, "NavBackStackEntry")
85