Merge "Add lint rule for right lifecycle in addMenuProvider" into androidx-main
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt
index e045232..a75abfd 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt
@@ -28,6 +28,7 @@
     override val minApi = CURRENT_API
     override val issues get() = listOf(
         FragmentTagDetector.ISSUE,
+        UnsafeFragmentLifecycleObserverDetector.ADD_MENU_PROVIDER_ISSUE,
         UnsafeFragmentLifecycleObserverDetector.BACK_PRESSED_ISSUE,
         UnsafeFragmentLifecycleObserverDetector.LIVEDATA_ISSUE,
         UseRequireInsteadOfGet.ISSUE,
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeFragmentLifecycleObserverDetector.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeFragmentLifecycleObserverDetector.kt
index e38eb4e..8ed43ac 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeFragmentLifecycleObserverDetector.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/UnsafeFragmentLifecycleObserverDetector.kt
@@ -18,6 +18,7 @@
 
 package androidx.fragment.lint
 
+import androidx.fragment.lint.UnsafeFragmentLifecycleObserverDetector.Issues.ADD_MENU_PROVIDER_ISSUE
 import androidx.fragment.lint.UnsafeFragmentLifecycleObserverDetector.Issues.BACK_PRESSED_ISSUE
 import androidx.fragment.lint.UnsafeFragmentLifecycleObserverDetector.Issues.LIVEDATA_ISSUE
 import com.android.tools.lint.detector.api.Category
@@ -86,6 +87,25 @@
             ),
             androidSpecific = true
         )
+
+        val ADD_MENU_PROVIDER_ISSUE = Issue.create(
+            id = "FragmentAddMenuProvider",
+            briefDescription = "Use getViewLifecycleOwner() as the LifecycleOwner instead of " +
+                "a Fragment instance.",
+            explanation = """The Fragment lifecycle can result in a Fragment being active \
+                longer than its view. This can lead to unexpected behavior from lifecycle aware \
+                objects remaining active longer than the Fragment's view. To solve this issue, \
+                getViewLifecycleOwner() should be used as a LifecycleOwner rather than the \
+                Fragment instance once it is safe to access the view lifecycle in a \
+                Fragment's onCreateView, onViewCreated, onActivityCreated, or \
+                onViewStateRestored methods.""",
+            category = Category.CORRECTNESS,
+            severity = Severity.ERROR,
+            implementation = Implementation(
+                UnsafeFragmentLifecycleObserverDetector::class.java, Scope.JAVA_FILE_SCOPE
+            ),
+            androidSpecific = true
+        )
     }
 
     private val lifecycleMethods = setOf(
@@ -149,8 +169,7 @@
      * @return `true` if a lint error was found and reported, `false` otherwise.
      */
     private fun checkCall(call: UCallExpression, psiMethod: PsiMethod): Boolean {
-        val method = Method(psiMethod.containingClass?.qualifiedName, psiMethod.name)
-        val issue = UNSAFE_METHODS[method] ?: return false
+        val issue = findIssueForMethod(psiMethod) ?: return false
         val argMap = context.evaluator.computeArgumentMapping(call, psiMethod)
         argMap.forEach { (arg, param) ->
             if (arg.getExpressionType().extends(context, FRAGMENT_CLASS) &&
@@ -184,6 +203,24 @@
         }
         return false
     }
+
+    /**
+     * Checks if the given [PsiMethod] should be associated with an [Issue]. It covers the case
+     * where the given [PsiMethod] is on the base class itself, or if it is on a class that
+     * extends the base class and calls the method instead.
+     *
+     * This allows us to catch the issue no matter where it may occur in the class hierarchy.
+     */
+    private fun findIssueForMethod(psiMethod: PsiMethod): Issue? {
+        UNSAFE_METHODS.keys.forEach { base ->
+            if (context.evaluator.extendsClass(psiMethod.containingClass, base.cls!!) &&
+                psiMethod.name == base.name
+            ) {
+                return UNSAFE_METHODS[base]
+            }
+        }
+        return null
+    }
 }
 
 /**
@@ -213,7 +250,9 @@
 internal val UNSAFE_METHODS = mapOf(
     Method("androidx.lifecycle.LiveData", "observe") to LIVEDATA_ISSUE,
     Method("androidx.lifecycle.LiveDataKt", "observe") to LIVEDATA_ISSUE,
-    Method("androidx.activity.OnBackPressedDispatcher", "addCallback") to BACK_PRESSED_ISSUE
+    Method("androidx.activity.OnBackPressedDispatcher", "addCallback") to BACK_PRESSED_ISSUE,
+    Method("androidx.core.view.MenuHost", "addMenuProvider") to ADD_MENU_PROVIDER_ISSUE,
+    Method("androidx.core.view.MenuHostHelper", "addMenuProvider") to ADD_MENU_PROVIDER_ISSUE,
 )
 
 private const val FRAGMENT_CLASS = "androidx.fragment.app.Fragment"
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/AddMenuProviderDetectorTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/AddMenuProviderDetectorTest.kt
new file mode 100644
index 0000000..51314e8
--- /dev/null
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/AddMenuProviderDetectorTest.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.fragment.lint
+
+import androidx.fragment.lint.stubs.ADD_MENU_PROVIDER_STUBS
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintResult
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class AddMenuProviderDetectorTest : LintDetectorTest() {
+
+    override fun getDetector(): Detector = UnsafeFragmentLifecycleObserverDetector()
+
+    override fun getIssues(): MutableList<Issue> =
+        mutableListOf(UnsafeFragmentLifecycleObserverDetector.ADD_MENU_PROVIDER_ISSUE)
+
+    private fun check(vararg files: TestFile): TestLintResult {
+        return lint().files(*files, *ADD_MENU_PROVIDER_STUBS)
+            .run()
+    }
+
+    @Test
+    fun pass() {
+        check(
+            kotlin(
+                """
+package com.example
+
+import androidx.fragment.app.Fragment
+import com.example.test.Foo
+
+class TestFragment : Fragment {
+
+    override fun onCreateView() {
+        requireActivity().addMenuProvider(this, getViewLifecycleOwner())
+        requireActivity().addMenuProvider(this, viewLifecycleOwner)
+        requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.STARTED)
+    }
+
+    override fun onViewCreated() {
+        test()
+        val foo = Foo()
+        foo.addMenuProvider(requireActivity(), this)
+        foo.menuProvider(this)
+    }
+
+    private fun test() {
+        requireActivity().addMenuProvider(this, getViewLifecycleOwner())
+        test()
+    }
+}
+            """
+            ),
+            kotlin(
+                """
+package com.example.test
+
+import androidx.activity.ComponentActivity
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.LifecycleOwner
+
+class Foo {
+    fun addMenuProvider(activity: ComponentActivity, fragment: Fragment) {
+        activity.addMenuProvider(fragment, LifecycleOwner())
+        activity.addMenuProvider(fragment, LifecycleOwner(), Lifecycle.State.STARTED)
+    }
+
+    fun menuProvider(fragment: Fragment) {}
+}
+            """
+            )
+        )
+            .expectClean()
+    }
+
+    @Test
+    fun inMethodFails() {
+        check(
+            kotlin(
+                """
+package com.example
+
+import androidx.fragment.app.Fragment
+
+class TestFragment : Fragment {
+
+    override fun onCreateView() {
+        requireActivity().addMenuProvider(this, this)
+    }
+}
+            """
+            )
+        )
+            .expect(
+                """
+src/com/example/TestFragment.kt:9: Error: Use viewLifecycleOwner as the LifecycleOwner. [FragmentAddMenuProvider]
+        requireActivity().addMenuProvider(this, this)
+                                                ~~~~
+1 errors, 0 warnings
+            """
+            )
+            .checkFix(
+                null,
+                kotlin(
+                    """
+package com.example
+
+import androidx.fragment.app.Fragment
+
+class TestFragment : Fragment {
+
+    override fun onCreateView() {
+        requireActivity().addMenuProvider(this, viewLifecycleOwner)
+    }
+}
+            """
+                )
+            )
+    }
+
+    @Test
+    fun helperMethodFails() {
+        check(
+            kotlin(
+                """
+package com.example
+
+import androidx.fragment.app.Fragment
+
+class TestFragment : Fragment {
+
+    override fun onCreateView() {
+        test()
+    }
+
+    private fun test() {
+        requireActivity().addMenuProvider(this, this)
+    }
+}
+            """
+            )
+        )
+            .expect(
+                """
+src/com/example/TestFragment.kt:13: Error: Use viewLifecycleOwner as the LifecycleOwner. [FragmentAddMenuProvider]
+        requireActivity().addMenuProvider(this, this)
+                                                ~~~~
+1 errors, 0 warnings
+            """
+            )
+            .checkFix(
+                null,
+                kotlin(
+                    """
+package com.example
+
+import androidx.fragment.app.Fragment
+
+class TestFragment : Fragment {
+
+    override fun onCreateView() {
+        test()
+    }
+
+    private fun test() {
+        requireActivity().addMenuProvider(this, viewLifecycleOwner)
+    }
+}
+            """
+                )
+            )
+    }
+
+    @Test
+    fun externalCallFails() {
+        check(
+            kotlin(
+                """
+package com.example
+
+import androidx.fragment.app.Fragment
+import com.example.test.Foo
+
+class TestFragment : Fragment {
+
+    override fun onCreateView() {
+        test()
+    }
+
+    private fun test() {
+        val foo = Foo()
+        foo.addMenuProvider(requireActivity(), this)
+    }
+}
+            """
+            ),
+            kotlin(
+                """
+package com.example.test
+
+import androidx.activity.ComponentActivity
+import androidx.fragment.app.Fragment
+
+class Foo {
+    fun addMenuProvider(activity:ComponentActivity, fragment: Fragment) {
+        activity.addMenuProvider(fragment, fragment)
+        activity.addMenuProvider(fragment, fragment, Lifecycle.State.STARTED)
+    }
+}
+            """
+            )
+        )
+            .expect(
+                """
+src/com/example/test/Foo.kt:9: Error: Unsafe call to addMenuProvider with Fragment instance as LifecycleOwner from TestFragment.onCreateView. [FragmentAddMenuProvider]
+        activity.addMenuProvider(fragment, fragment)
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+src/com/example/test/Foo.kt:10: Error: Unsafe call to addMenuProvider with Fragment instance as LifecycleOwner from TestFragment.onCreateView. [FragmentAddMenuProvider]
+        activity.addMenuProvider(fragment, fragment, Lifecycle.State.STARTED)
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+2 errors, 0 warnings
+            """
+            )
+    }
+
+    @Test
+    fun externalHelperMethodFails() {
+        check(
+            kotlin(
+                """
+package com.example
+
+import androidx.fragment.app.Fragment
+import com.example.test.Foo
+
+class TestFragment : Fragment {
+
+    override fun onCreateView() {
+        test()
+    }
+
+    private fun test() {
+        val foo = Foo()
+        foo.addMenuProvider(requireActivity(), this)
+    }
+}
+            """
+            ),
+            kotlin(
+                """
+package com.example.test
+
+import androidx.activity.ComponentActivity
+import androidx.fragment.app.Fragment
+
+class Foo {
+    private lateinit val fragment: Fragment
+
+    fun addMenuProvider(activity:ComponentActivity, fragment: Fragment) {
+        this.fragment = fragment
+        menuProvider(activity)
+    }
+
+    private fun menuProvider(activity:ComponentActivity) {
+        activity.addMenuProvider(fragment, fragment)
+        activity.addMenuProvider(fragment, fragment, Lifecycle.State.STARTED)
+    }
+}
+            """
+            )
+        )
+            .expect(
+                """
+src/com/example/test/Foo.kt:16: Error: Unsafe call to addMenuProvider with Fragment instance as LifecycleOwner from TestFragment.onCreateView. [FragmentAddMenuProvider]
+        activity.addMenuProvider(fragment, fragment)
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+src/com/example/test/Foo.kt:17: Error: Unsafe call to addMenuProvider with Fragment instance as LifecycleOwner from TestFragment.onCreateView. [FragmentAddMenuProvider]
+        activity.addMenuProvider(fragment, fragment, Lifecycle.State.STARTED)
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+2 errors, 0 warnings
+            """
+            )
+    }
+}
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentLiveDataObserveDetectorTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentLiveDataObserveDetectorTest.kt
index 85b5fef..e65b434 100644
--- a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentLiveDataObserveDetectorTest.kt
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/FragmentLiveDataObserveDetectorTest.kt
@@ -86,7 +86,7 @@
     fun observeData(fragment: Fragment) {
         val liveData = MutableLiveData<String>()
         liveData.observe(LifecycleOwner(), Observer<String> {})
-        liveData.observe(fragment, Observer<String> {}, true)
+        liveData.observe(fragment.viewLifecycleOwner, Observer<String> {}, true)
     }
 
     fun observe(fragment: Fragment) {}
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/stubs/Stubs.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/stubs/Stubs.kt
index ffdb9c4..060fab1 100644
--- a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/stubs/Stubs.kt
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/stubs/Stubs.kt
@@ -40,16 +40,41 @@
 """
 )
 
+private val COMPONENT_ACTIVITY = java(
+    """
+    package androidx.activity;
+
+    import androidx.core.view.MenuHost;
+    import androidx.core.view.MenuProvider;
+    import androidx.lifecycle.Lifecycle;
+    import androidx.lifecycle.LifecycleOwner;
+
+    public class ComponentActivity implements MenuHost {
+        public void addMenuProvider(@NonNull MenuProvider provider, @NonNull LifecycleOwner owner) {
+
+        }
+
+        public void addMenuProvider(@NonNull MenuProvider provider, @NonNull LifecycleOwner owner,
+            @NonNull Lifecycle.State state) { }
+    }
+    """
+)
+
 private val FRAGMENT = java(
     """
     package androidx.fragment.app;
 
+    import androidx.activity.ComponentActivity;
+    import androidx.core.view.MenuProvider;
     import androidx.lifecycle.Lifecycle;
     import androidx.lifecycle.LifecycleOwner;
 
-    public class Fragment implements LifecycleOwner {
+    public class Fragment implements LifecycleOwner, MenuProvider {
         public LifecycleOwner getViewLifecycleOwner() {}
         public Lifecycle getLifecycle() {}
+        public ComponentActivity requireActivity() {
+            return ComponentActivity();
+        }
     }
     """
 )
@@ -142,6 +167,33 @@
 """
 ).indented().within("src")
 
+private val MENU_PROVIDER = java(
+    """
+    package androidx.core.view;
+
+    import androidx.annotation.NonNull;
+
+    public interface MenuProvider { }
+    """
+)
+
+private val MENU_HOST = java(
+    """
+    package androidx.core.view;
+
+    import androidx.annotation.NonNull;
+    import androidx.lifecycle.Lifecycle;
+    import androidx.lifecycle.LifecycleOwner;
+
+    public interface MenuHost {
+        void addMenuProvider(@NonNull MenuProvider provider, @NonNull LifecycleOwner owner);
+
+        void addMenuProvider(@NonNull MenuProvider provider, @NonNull LifecycleOwner owner,
+            @NonNull Lifecycle.State state);
+    }
+    """
+)
+
 private val COROUTINES = TestFiles.kt(
     "kotlinx/coroutines/GlobalScope.kt",
     """
@@ -172,6 +224,7 @@
 
 // stubs for testing calls to LiveData.observe calls
 internal val LIVEDATA_STUBS = arrayOf(
+    COMPONENT_ACTIVITY,
     FRAGMENT,
     DIALOG_FRAGMENT,
     LIFECYCLE,
@@ -179,24 +232,42 @@
     LIVEDATA,
     MUTABLE_LIVEDATA,
     OBSERVER,
-    LIVEDATA_OBSERVE_EXTENSION
+    LIVEDATA_OBSERVE_EXTENSION,
+    MENU_HOST,
+    MENU_PROVIDER
 )
 
 // stubs for testing calls to OnBackPressedDispatcher.addCallback calls
 internal val BACK_CALLBACK_STUBS = arrayOf(
+    COMPONENT_ACTIVITY,
     BACK_PRESSED_CALLBACK,
     BACK_PRESSED_DISPATCHER,
     FRAGMENT,
     LIFECYCLE,
-    LIFECYCLE_OWNER
+    LIFECYCLE_OWNER,
+    MENU_HOST,
+    MENU_PROVIDER
 )
 
 // stubs for testing calls to LifecycleOwner.repeatOnLifecycle
 internal val REPEAT_ON_LIFECYCLE_STUBS = arrayOf(
+    COMPONENT_ACTIVITY,
     REPEAT_ON_LIFECYCLE,
     DIALOG_FRAGMENT,
     FRAGMENT,
     COROUTINES,
     LIFECYCLE,
-    LIFECYCLE_OWNER
+    LIFECYCLE_OWNER,
+    MENU_HOST,
+    MENU_PROVIDER
+)
+
+// stubs for testing calls to MenuHost.addMenuProvider calls
+internal val ADD_MENU_PROVIDER_STUBS = arrayOf(
+    COMPONENT_ACTIVITY,
+    FRAGMENT,
+    LIFECYCLE,
+    LIFECYCLE_OWNER,
+    MENU_HOST,
+    MENU_PROVIDER
 )