1 /*
<lambda>null2  * Copyright 2024 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 package androidx.build.lint
18 
19 import com.android.tools.lint.client.api.UElementHandler
20 import com.android.tools.lint.detector.api.Category
21 import com.android.tools.lint.detector.api.Detector
22 import com.android.tools.lint.detector.api.Implementation
23 import com.android.tools.lint.detector.api.Incident
24 import com.android.tools.lint.detector.api.Issue
25 import com.android.tools.lint.detector.api.JavaContext
26 import com.android.tools.lint.detector.api.LintFix
27 import com.android.tools.lint.detector.api.Location
28 import com.android.tools.lint.detector.api.Scope
29 import com.android.tools.lint.detector.api.Severity
30 import com.android.tools.lint.detector.api.SourceCodeScanner
31 import com.intellij.lang.jvm.JvmModifier
32 import com.intellij.psi.PsiElement
33 import com.intellij.psi.PsiModifier
34 import com.intellij.psi.impl.source.PsiClassReferenceType
35 import org.jetbrains.kotlin.idea.KotlinFileType
36 import org.jetbrains.uast.UAnonymousClass
37 import org.jetbrains.uast.UBlockExpression
38 import org.jetbrains.uast.UCallExpression
39 import org.jetbrains.uast.UClass
40 import org.jetbrains.uast.UExpression
41 import org.jetbrains.uast.UMethod
42 import org.jetbrains.uast.UQualifiedReferenceExpression
43 import org.jetbrains.uast.UResolvable
44 import org.jetbrains.uast.UReturnExpression
45 import org.jetbrains.uast.USimpleNameReferenceExpression
46 import org.jetbrains.uast.getContainingUClass
47 import org.jetbrains.uast.skipParenthesizedExprDown
48 
49 class ObsoleteCompatDetector : Detector(), SourceCodeScanner {
50 
51     override fun getApplicableUastTypes() = listOf(UMethod::class.java)
52 
53     override fun createUastHandler(context: JavaContext): UElementHandler =
54         CompatMethodHandler(context)
55 
56     companion object {
57         val ISSUE =
58             Issue.create(
59                 "ObsoleteCompatMethod",
60                 "Obsolete compatibility method can be deprecated with replacement",
61                 "Compatibility methods that consist of a single call to the platform SDK " +
62                     "should be deprecated and provide a suggestion to replace with a direct call.",
63                 Category.CORRECTNESS,
64                 5,
65                 Severity.ERROR,
66                 Implementation(ObsoleteCompatDetector::class.java, Scope.JAVA_FILE_SCOPE)
67             )
68     }
69 }
70 
71 private class CompatMethodHandler(val context: JavaContext) : UElementHandler() {
72 
visitMethodnull73     override fun visitMethod(node: UMethod) {
74         // If this method probably a compat method?
75         if (!node.isMaybeJetpackUtilityMethod()) return
76 
77         // Is this method probably contained in a Jetpack utility class?
78         if (node.getContainingUClass()?.isMaybeJetpackUtilityClass() != true) return
79 
80         // Does it already have @Deprecated and @ReplaceWith annotations?
81         val hasDeprecated = node.hasAnnotation("java.lang.Deprecated")
82         val hasReplaceWith = node.hasAnnotation("androidx.annotation.ReplaceWith")
83         val hasDeprecatedDoc =
84             node.comments.any { comment -> comment.text.contains("@deprecated ") }
85         if (hasDeprecated && hasReplaceWith && hasDeprecatedDoc) return
86 
87         // Compat methods take the wrapped class as the first parameter.
88         val firstParameter = node.javaPsi.parameterList.parameters.firstOrNull() ?: return
89 
90         // Ensure that we're dealing with a single-line method that operates on the wrapped class.
91         val expression =
92             (node.uastBody as? UBlockExpression)
93                 ?.expressions
94                 ?.singleOrNull()
95                 ?.unwrapReturnExpression()
96                 ?.skipParenthesizedExprDown() as? UQualifiedReferenceExpression
97         val receiver = expression?.unwrapReceiver()
98         if (firstParameter != receiver) return
99 
100         val lintFix = LintFix.create().composite().name("Replace obsolete compat method")
101 
102         if (!hasDeprecatedDoc) {
103             val docLink =
104                 when (expression.selector) {
105                     is UCallExpression -> {
106                         val methodCall = expression.selector as UCallExpression
107                         val className = (methodCall.receiverType as PsiClassReferenceType).className
108                         val methodName = methodCall.methodName
109                         val argTypes =
110                             methodCall.typeArguments.map { psiType ->
111                                 (psiType as PsiClassReferenceType).className
112                             }
113                         "$className#$methodName(${argTypes.joinToString(", ")})"
114                     }
115                     is USimpleNameReferenceExpression -> {
116                         val fieldName =
117                             (expression.selector as USimpleNameReferenceExpression).resolvedName
118                         val className =
119                             (expression.receiver.getExpressionType() as PsiClassReferenceType)
120                                 .className
121                         "$className#$fieldName"
122                     }
123                     else -> {
124                         // We don't know how to handle this type of qualified reference.
125                         return
126                     }
127                 }
128             val docText = "@deprecated Call {@link $docLink} directly."
129             val javadocFix =
130                 buildInsertJavadocFix(context, node, docText)
131                     .autoFix()
132                     .shortenNames()
133                     .reformat(true)
134                     .build()
135             lintFix.add(javadocFix)
136         }
137 
138         if (!hasReplaceWith) {
139             val replacement =
140                 expression.javaPsi!!.text!!.replace("\"", "\\\"").replace(Regex("\n\\s*"), "")
141             lintFix.add(
142                 LintFix.create()
143                     .name("Annotate with @ReplaceWith")
144                     .annotate(
145                         source = "androidx.annotation.ReplaceWith(expression = \"$replacement\")",
146                         context = context,
147                         element = node,
148                         replace = false
149                     )
150                     .autoFix()
151                     .build()
152             )
153         }
154 
155         if (!hasDeprecated) {
156             lintFix.add(
157                 LintFix.create()
158                     .name("Annotate with @Deprecated")
159                     .annotate(
160                         source = "java.lang.Deprecated",
161                         context = context,
162                         element = node,
163                         replace = false
164                     )
165                     .autoFix()
166                     .build()
167             )
168         }
169 
170         val incident =
171             Incident(context)
172                 .issue(ObsoleteCompatDetector.ISSUE)
173                 .location(context.getNameLocation(node))
174                 .message("Obsolete compat method should provide replacement")
175                 .scope(node)
176                 .fix(lintFix.build())
177         context.report(incident)
178     }
179 }
180 
buildInsertJavadocFixnull181 fun buildInsertJavadocFix(
182     context: JavaContext,
183     node: UMethod,
184     docText: String
185 ): LintFix.ReplaceStringBuilder {
186     val javadocNode = node.comments.lastOrNull { it.text.startsWith("/**") }
187     val javadocFix = LintFix.create().name("Add @deprecated Javadoc annotation").replace()
188     if (javadocNode != null) {
189         // Append to the existing block comment before the close.
190         val docEndOffset = javadocNode.text.lastIndexOf("*/")
191         val insertAt = context.getRangeLocation(javadocNode, docEndOffset, 2)
192         val replacement = applyIndentToInsertion(context, insertAt, "* $docText")
193         javadocFix.range(insertAt).beginning().with(replacement)
194     } else {
195         // Insert a new comment before the declaration or any annotations.
196         val insertAt = context.getLocation(node.annotations.firstOrNull() ?: node.modifierList)
197         val replacement = applyIndentToInsertion(context, insertAt, "/** $docText */")
198         javadocFix.range(insertAt).beginning().with(replacement)
199     }
200     return javadocFix
201 }
202 
applyIndentToInsertionnull203 fun applyIndentToInsertion(context: JavaContext, insertAt: Location, replacement: String): String {
204     val contents = context.getContents()!!
205     val start = insertAt.start!!
206     val startOffset = start.offset
207     var lineBegin = startOffset
208     while (lineBegin > 0) {
209         val c = contents[lineBegin - 1]
210         if (!Character.isWhitespace(c)) {
211             break
212         } else if (c == '\n' || lineBegin == 1) {
213             if (startOffset > lineBegin) {
214                 val indent = contents.substring(lineBegin, startOffset)
215                 return replacement + "\n" + indent
216             }
217             break
218         } else lineBegin--
219     }
220     return replacement
221 }
222 
UExpressionnull223 fun UExpression.unwrapReceiver(): PsiElement? =
224     ((this as? UQualifiedReferenceExpression)?.receiver?.skipParenthesizedExprDown()
225             as? UResolvable)
226         ?.resolve()
227 
228 fun UExpression.unwrapReturnExpression(): UExpression =
229     (this as? UReturnExpression)?.returnExpression ?: this
230 
231 @Suppress("UnstableApiUsage") // hasModifier, JvmModifier.PUBLIC
232 fun UMethod.isMaybeJetpackUtilityMethod(): Boolean {
233     return isStatic && !isConstructor && hasModifier(JvmModifier.PUBLIC)
234 }
235 
isMaybeJetpackUtilityClassnull236 fun UClass.isMaybeJetpackUtilityClass(): Boolean {
237     return !(isInterface ||
238         isEnum ||
239         hasModifierProperty(PsiModifier.ABSTRACT) ||
240         this is UAnonymousClass ||
241         // If this is a subclass, then don't flag it.
242         supers.any { !it.qualifiedName.equals("java.lang.Object") } ||
243         // Don't run for Kotlin, for now at least
244         containingFile.fileType == KotlinFileType.INSTANCE)
245 }
246