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