Check if CVF fix method already exists before adding it

Test: Added test with existing fix method
Bug: 243402089
Change-Id: Ib8c094b3915cf4c140d0029a2660e4c5c2ebd32d
diff --git a/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeReferenceWithExistingFix.java b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeReferenceWithExistingFix.java
new file mode 100644
index 0000000..efa821b
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/AutofixUnsafeReferenceWithExistingFix.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 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.
+ */
+
+package androidx;
+
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.RequiresApi;
+
+/**
+ * Test class containing unsafe method references.
+ */
+@SuppressWarnings("unused")
+public class AutofixUnsafeReferenceWithExistingFix {
+
+    /**
+     * Unsafe reference to a new API with an already existing fix method in Api21Impl.
+     */
+    @RequiresApi(21)
+    void unsafeReferenceFixMethodExists(View view) {
+        view.setBackgroundTintList(new ColorStateList(null, null));
+    }
+
+    /**
+     * Unsafe reference to a new API without an existing fix method, but requiring API 21.
+     */
+    @RequiresApi(21)
+    void unsafeReferenceFixClassExists(Drawable drawable) {
+        drawable.getOutline(null);
+    }
+
+    @RequiresApi(21)
+    static class Api21Impl {
+        private Api21Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static void setBackgroundTintList(View view, ColorStateList tint) {
+            view.setBackgroundTintList(tint);
+        }
+    }
+}
diff --git a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
index e46c90b..4bc5ab0 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
@@ -509,15 +509,18 @@
             // The host class should never be null if we're looking at Java code.
             val callContainingClass = call.getContainingUClass() ?: return null
 
-            val (wrapperMethodName, methodForInsertion) = generateWrapperMethod(
-                method,
-                // Find what type the result of this call is used as
-                getExpectedTypeByParent(callPsi)
-            ) ?: return null
+            val (wrapperMethodName, wrapperMethodParams, methodForInsertion) =
+                generateWrapperMethod(
+                    method,
+                    // Find what type the result of this call is used as
+                    getExpectedTypeByParent(callPsi)
+                ) ?: return null
 
             val (wrapperClassName, insertionPoint, insertionSource) = generateInsertionSource(
                 api,
                 callContainingClass,
+                wrapperMethodName,
+                wrapperMethodParams,
                 methodForInsertion
             )
 
@@ -529,22 +532,29 @@
                 wrapperMethodName
             )
 
-            return fix().name("Extract to static inner class")
-                .composite(
-                    fix()
-                        .replace()
-                        .range(insertionPoint)
-                        .beginning()
-                        .with(insertionSource)
-                        .shortenNames()
-                        .build(),
-                    fix()
-                        .replace()
-                        .range(context.getLocation(call))
-                        .with(replacementCall)
-                        .shortenNames()
-                        .build(),
+            val fix = fix()
+                .name("Extract to static inner class")
+                .composite()
+                .add(fix()
+                    .replace()
+                    .range(context.getLocation(call))
+                    .with(replacementCall)
+                    .shortenNames()
+                    .build()
                 )
+
+            if (insertionPoint != null) {
+                fix.add(fix()
+                    .replace()
+                    .range(insertionPoint)
+                    .beginning()
+                    .with(insertionSource)
+                    .shortenNames()
+                    .build()
+                )
+            }
+
+            return fix.build()
         }
 
         /**
@@ -587,6 +597,8 @@
          * Generates source code for a wrapper method and class (where applicable) and calculates
          * the insertion point. If the wrapper class already exists, returns source code for the
          * method body only with an insertion point at the end of the existing wrapper class body.
+         * If the wrapper class and method both already exists, just returns the name of the
+         * wrapper class.
          *
          * Source code follows the general format:
          *
@@ -600,19 +612,26 @@
          *
          * @param api API level at which the platform method can be safely called
          * @param callContainingClass Class containing the call to the platform method
+         * @param wrapperMethodName The name of the wrapper method, used to check if the wrapper
+         * method already exists
+         * @param wrapperMethodParams List of the types of the wrapper method's parameters, used to
+         * check if the wrapper method already exists
          * @param wrapperMethodBody Source code for the wrapper method
          * @return Triple containing (1) the name of the static wrapper class, (2) the insertion
-         * point for the generated source code, and (3) generated source code for a static wrapper
-         * method, including a static wrapper class if necessary
+         * point for the generated source code (or null if the wrapper method already exists), and
+         * (3) generated source code for a static wrapper method, including a static wrapper class
+         * if necessary (or null if the wrapper method already exists)
          */
         private fun generateInsertionSource(
             api: Int,
             callContainingClass: UClass,
+            wrapperMethodName: String,
+            wrapperMethodParams: List<PsiType>,
             wrapperMethodBody: String,
-        ): Triple<String, Location, String> {
+        ): Triple<String, Location?, String?> {
             val wrapperClassName = "Api${api}Impl"
-            val implInsertionPoint: Location
-            val implForInsertion: String
+            val implInsertionPoint: Location?
+            val implForInsertion: String?
 
             val existingWrapperClass = callContainingClass.innerClasses.find { innerClass ->
                 innerClass.name == wrapperClassName
@@ -631,8 +650,18 @@
 
                 """.trimIndent()
             } else {
-                implInsertionPoint = context.getLocation(existingWrapperClass.lastChild)
-                implForInsertion = wrapperMethodBody.trimIndent()
+                val existingWrapperMethod = existingWrapperClass.methods.find { method ->
+                    method.name == wrapperMethodName &&
+                        wrapperMethodParams == getParameterTypes(method)
+                }
+                if (existingWrapperMethod == null) {
+                    implInsertionPoint = context.getLocation(existingWrapperClass.lastChild)
+                    // Add a newline to force the `}`s for the class and method onto different lines
+                    implForInsertion = wrapperMethodBody.trimIndent() + "\n"
+                } else {
+                    implInsertionPoint = null
+                    implForInsertion = null
+                }
             }
 
             return Triple(
@@ -727,7 +756,7 @@
         private fun generateWrapperMethod(
             method: PsiMethod,
             expectedReturnType: PsiType?
-        ): Pair<String, String>? {
+        ): Triple<String, List<PsiType>, String>? {
             val evaluator = context.evaluator
             val isStatic = evaluator.isStatic(method)
             val isConstructor = method.isConstructor
@@ -753,6 +782,9 @@
             }
             val typedParamsStr = (listOfNotNull(hostParam) + typedParams).joinToString(", ")
 
+            val paramTypes = listOf(PsiTypesUtil.getClassType(containingClass)) +
+                getParameterTypes(method)
+
             val namedParamsStr = method.parameters.joinToString(separator = ", ") { param ->
                 "${param.name}"
             }
@@ -797,8 +829,9 @@
 
             val returnStmtStr = if ("void" == returnTypeStr) "" else "return "
 
-            return Pair(
+            return Triple(
                 wrapperMethodName,
+                paramTypes,
                 """
                     @androidx.annotation.DoNotInline
                     static $typeParamsStr$returnTypeStr $wrapperMethodName($typedParamsStr) {
@@ -809,6 +842,12 @@
         }
 
         /**
+         * Returns a list of the method's parameter types.
+         */
+        private fun getParameterTypes(method: PsiMethod): List<PsiType> =
+            method.parameterList.parameters.map { it.type }
+
+        /**
          * Check if the specified class is available at the min SDK.
          */
         private fun classAvailableAtMinSdk(className: String): Boolean {
diff --git a/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
index 945254e..efdf27c 100644
--- a/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
@@ -18,6 +18,7 @@
 
 package androidx.build.lint
 
+import androidx.build.lint.Stubs.Companion.DoNotInline
 import androidx.build.lint.Stubs.Companion.RequiresApi
 import androidx.build.lint.Stubs.Companion.IntRange
 import org.junit.Ignore
@@ -347,6 +348,48 @@
         check(*input).expectFixDiffs(expectedFix)
     }
 
+    @Test
+    fun `Auto-fix unsafe reference in Java source when the fix code already exists`() {
+        val input = arrayOf(
+            javaSample("androidx.AutofixUnsafeReferenceWithExistingFix"),
+            RequiresApi,
+            DoNotInline
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/androidx/AutofixUnsafeReferenceWithExistingFix.java:37: Error: This call references a method added in API level 21; however, the containing class androidx.AutofixUnsafeReferenceWithExistingFix is reachable from earlier API levels and will fail run-time class verification. [ClassVerificationFailure]
+        view.setBackgroundTintList(new ColorStateList(null, null));
+             ~~~~~~~~~~~~~~~~~~~~~
+src/androidx/AutofixUnsafeReferenceWithExistingFix.java:45: Error: This call references a method added in API level 21; however, the containing class androidx.AutofixUnsafeReferenceWithExistingFix is reachable from earlier API levels and will fail run-time class verification. [ClassVerificationFailure]
+        drawable.getOutline(null);
+                 ~~~~~~~~~~
+2 errors, 0 warnings
+        """
+
+        val expectedFix = """
+Fix for src/androidx/AutofixUnsafeReferenceWithExistingFix.java line 37: Extract to static inner class:
+@@ -37 +37
+-         view.setBackgroundTintList(new ColorStateList(null, null));
++         Api21Impl.setBackgroundTintList(view, new ColorStateList(null, null));
+Fix for src/androidx/AutofixUnsafeReferenceWithExistingFix.java line 45: Extract to static inner class:
+@@ -45 +45
+-         drawable.getOutline(null);
++         Api21Impl.getOutline(drawable, null);
+@@ -58 +58
+-     }
++     @annotation.DoNotInline
++ static void getOutline(Drawable drawable, android.graphics.Outline outline) {
++     drawable.getOutline(outline);
+@@ -60 +62
++ }
++ }
+        """
+        /* ktlint-enable max-line-length */
+
+        check(*input).expect(expected).expectFixDiffs(expectedFix)
+    }
+
     @Ignore("Ignored until the fix for b/241573146 is in the current version of studio")
     @Test
     fun `Detection and auto-fix for qualified expressions (issue 205026874)`() {
diff --git a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
index b48f7d9..3feb926 100644
--- a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
@@ -261,6 +261,23 @@
 annotation class Ignore
             """
         )
+
+        val DoNotInline = TestFiles.java(
+            """
+package androidx.annotation;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(CLASS)
+@Target({METHOD})
+public @interface DoNotInline {
+}
+            """
+        )
         /* ktlint-enable max-line-length */
     }
 }