1 /*
<lambda>null2  * Copyright 2019 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.build.lint
20 
21 import com.android.tools.lint.detector.api.AnnotationInfo
22 import com.android.tools.lint.detector.api.AnnotationUsageInfo
23 import com.android.tools.lint.detector.api.AnnotationUsageType
24 import com.android.tools.lint.detector.api.Category
25 import com.android.tools.lint.detector.api.ConstantEvaluator
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.LintFix
31 import com.android.tools.lint.detector.api.Location
32 import com.android.tools.lint.detector.api.Scope
33 import com.android.tools.lint.detector.api.Severity
34 import com.android.tools.lint.detector.api.SourceCodeScanner
35 import com.android.tools.lint.detector.api.isKotlin
36 import com.intellij.psi.PsiField
37 import com.intellij.psi.PsiJavaFile
38 import com.intellij.psi.PsiMethod
39 import com.intellij.psi.PsiNewExpression
40 import com.intellij.psi.impl.source.tree.TreeElement
41 import org.jetbrains.kotlin.asJava.elements.KtLightMethod
42 import org.jetbrains.kotlin.lexer.KtTokens
43 import org.jetbrains.kotlin.psi.KtFile
44 import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
45 import org.jetbrains.kotlin.psi.KtProperty
46 import org.jetbrains.kotlin.psi.psiUtil.endOffset
47 import org.jetbrains.uast.UAnnotation
48 import org.jetbrains.uast.UCallExpression
49 import org.jetbrains.uast.UElement
50 import org.jetbrains.uast.UExpression
51 import org.jetbrains.uast.ULiteralExpression
52 import org.jetbrains.uast.UParenthesizedExpression
53 import org.jetbrains.uast.UPolyadicExpression
54 import org.jetbrains.uast.UQualifiedReferenceExpression
55 import org.jetbrains.uast.USimpleNameReferenceExpression
56 
57 class ReplaceWithDetector : Detector(), SourceCodeScanner {
58 
59     override fun applicableAnnotations(): List<String> =
60         listOf(
61             JAVA_REPLACE_WITH_ANNOTATION,
62             KOTLIN_DEPRECATED_ANNOTATION,
63         )
64 
65     override fun visitAnnotationUsage(
66         context: JavaContext,
67         element: UElement,
68         annotationInfo: AnnotationInfo,
69         usageInfo: AnnotationUsageInfo
70     ) {
71         val qualifiedName = annotationInfo.qualifiedName
72         val annotation = annotationInfo.annotation
73         val referenced = usageInfo.referenced
74         val usage = usageInfo.usage
75         val type = usageInfo.type
76 
77         // Ignore callbacks for assignment on the original declaration of an annotated field.
78         if (type == AnnotationUsageType.ASSIGNMENT_RHS && usage.uastParent == referenced) return
79 
80         // [b/323214452] Don't replace property usages since we don't handle property accessors.
81         if ((referenced as? KtLightMethod)?.kotlinOrigin is KtProperty) return
82 
83         // Don't warn for Kotlin replacement in Kotlin files -- that's the Kotlin Compiler's job.
84         if (qualifiedName == KOTLIN_DEPRECATED_ANNOTATION && isKotlin(usage.lang)) return
85 
86         var (expression, imports) =
87             when (qualifiedName) {
88                 KOTLIN_DEPRECATED_ANNOTATION -> {
89                     val replaceWith =
90                         annotation.findAttributeValue("replaceWith")?.unwrap() as? UCallExpression
91                             ?: return
92                     val expression =
93                         replaceWith.valueArguments.getOrNull(0)?.parseLiteral() ?: return
94                     val imports =
95                         replaceWith.valueArguments.getOrNull(1)?.parseVarargLiteral() ?: emptyList()
96                     Pair(expression, imports)
97                 }
98                 JAVA_REPLACE_WITH_ANNOTATION -> {
99                     val expression =
100                         annotation.findAttributeValue("expression")?.let { expr ->
101                             ConstantEvaluator.evaluate(context, expr)
102                         } as? String ?: return
103                     val imports = annotation.getAttributeValueVarargLiteral("imports")
104                     Pair(expression, imports)
105                 }
106                 else -> return
107             }
108 
109         var location = context.getLocation(usage)
110         val includeReceiver = expressionWithReceiverRegex.matches(expression)
111         val includeArguments =
112             expressionWithArgumentRegex.matches(expression) ||
113                 expressionWithAssignmentRegex.matches(expression)
114 
115         if (referenced is PsiMethod && usage is UCallExpression) {
116             // Per Kotlin documentation for ReplaceWith: For function calls, the replacement
117             // expression may contain argument names of the deprecated function, which will
118             // be substituted with actual parameters used in the call being updated.
119             val argsToParams =
120                 referenced.parameters
121                     .mapIndexed { index, param ->
122                         param.name to usage.getArgumentForParameter(index)?.asSourceString()
123                     }
124                     .toMap()
125 
126             // Tokenize the replacement expression using a regex, replacing as we go. This
127             // isn't the most efficient approach (e.g. trie) but it's easy to write.
128             val search = Regex("\\w+")
129             var index = 0
130             do {
131                 val matchResult = search.find(expression, index) ?: break
132                 val replacement = argsToParams[matchResult.value]
133                 if (replacement != null) {
134                     expression = expression.replaceRange(matchResult.range, replacement)
135                     index += replacement.length
136                 } else {
137                     index += matchResult.value.length
138                 }
139             } while (index < expression.length)
140 
141             location =
142                 when (val sourcePsi = usage.sourcePsi) {
143                     is PsiNewExpression -> {
144                         // The expression should never specify "new", but if it specifies a
145                         // receiver then we should replace the call to "new". For example, if
146                         // we're replacing `new Clazz("arg")` with `ClazzCompat.create("arg")`.
147                         context.getConstructorLocation(
148                             usage,
149                             sourcePsi,
150                             includeReceiver,
151                             includeArguments
152                         )
153                     }
154                     else -> {
155                         // The expression may optionally specify a receiver or arguments, in
156                         // which case we should include the originals in the replacement range.
157                         context.getCallLocation(usage, includeReceiver, includeArguments)
158                     }
159                 }
160         } else if (referenced is PsiField && usage is USimpleNameReferenceExpression) {
161             // The expression may optionally specify a receiver, in which case we should
162             // include the original in the replacement range.
163             if (includeReceiver) {
164                 // If this is a qualified reference and we're including the "receiver" then
165                 // we should replace the fully-qualified expression.
166                 (usage.uastParent as? UQualifiedReferenceExpression)?.let { reference ->
167                     location = context.getLocation(reference)
168                 }
169             }
170         }
171 
172         reportLintFix(context, usage, location, expression, imports)
173     }
174 
175     private fun reportLintFix(
176         context: JavaContext,
177         usage: UElement,
178         location: Location,
179         expression: String,
180         imports: List<String>,
181     ) {
182         context.report(
183             ISSUE,
184             usage,
185             location,
186             "Replacement available",
187             createLintFix(context, location, expression, imports)
188         )
189     }
190 
191     private fun createLintFix(
192         context: JavaContext,
193         location: Location,
194         expression: String,
195         imports: List<String>
196     ): LintFix {
197         val name = "Replace with `$expression`"
198         val lintFixBuilder = fix().composite().name(name)
199         lintFixBuilder.add(fix().replace().range(location).name(name).with(expression).build())
200         if (imports.isNotEmpty()) {
201             lintFixBuilder.add(fix().import(context, add = imports).build())
202         }
203         return lintFixBuilder.build()
204     }
205 
206     /**
207      * Add imports.
208      *
209      * @return a string replace builder
210      */
211     fun LintFix.Builder.import(
212         context: JavaContext,
213         add: List<String>
214     ): LintFix.ReplaceStringBuilder {
215         val isKotlin = isKotlin(context.uastFile!!.lang)
216         val lastImport = context.uastFile?.imports?.lastOrNull()
217         val packageElem =
218             when (val psiFile = context.psiFile) {
219                 is PsiJavaFile -> psiFile.packageStatement
220                 is KtFile -> psiFile.packageDirective?.psiOrParent
221                 else -> null
222             }
223 
224         // Build the imports block. Leave any ordering or formatting up to the client.
225         val prependImports =
226             when {
227                 lastImport != null -> "\n"
228                 packageElem != null -> "\n\n"
229                 else -> ""
230             }
231         val appendImports =
232             when {
233                 lastImport != null -> ""
234                 packageElem != null -> ""
235                 else -> "\n"
236             }
237         val formattedImports = add.joinToString("\n") { "import " + if (isKotlin) it else "$it;" }
238         val importsText = prependImports + formattedImports + appendImports
239 
240         // Append after any existing imports, after the package declaration, or at the beginning of
241         // the file if there are no imports and no package declaration.
242         val location =
243             (lastImport ?: packageElem)
244                 ?.let { context.getLocation(it).end }
245                 ?.let { Location.create(context.file, it, it) }
246                 ?: Location.create(context.file, context.getContents(), 0, 0)
247         return replace().range(location).with(importsText).autoFix()
248     }
249 
250     companion object {
251         private val IMPLEMENTATION =
252             Implementation(
253                 ReplaceWithDetector::class.java,
254                 Scope.JAVA_FILE_SCOPE,
255             )
256 
257         private val expressionWithReceiverRegex = Regex("^\\w+\\.\\w+.*$")
258         private val expressionWithArgumentRegex = Regex("^.*\\w+\\(.*\\)$")
259         private val expressionWithAssignmentRegex = Regex("^.*\\w+\\s*=\\s*.*$")
260 
261         const val KOTLIN_DEPRECATED_ANNOTATION = "kotlin.Deprecated"
262         const val JAVA_REPLACE_WITH_ANNOTATION = "androidx.annotation.ReplaceWith"
263 
264         val ISSUE =
265             Issue.create(
266                 id = "ReplaceWith",
267                 briefDescription = "Replacement available",
268                 explanation = "A recommended replacement is available for this API usage.",
269                 category = Category.CORRECTNESS,
270                 priority = 4,
271                 severity = Severity.INFORMATIONAL,
272                 implementation = IMPLEMENTATION,
273             )
274     }
275 }
276 
277 /**
278  * Modified version of [JavaContext.getRangeLocation] that uses the `classReference` instead of the
279  * `receiver` to handle trimming the `new` keyword from the start of a Java constructor call.
280  */
JavaContextnull281 fun JavaContext.getConstructorLocation(
282     call: UCallExpression,
283     newExpression: PsiNewExpression,
284     includeNew: Boolean,
285     includeArguments: Boolean
286 ): Location {
287     if (includeArguments) {
288         call.valueArguments.lastOrNull()?.let { lastArgument ->
289             val argumentsEnd = lastArgument.sourcePsi?.endOffset
290             val callEnds = newExpression.endOffset
291             if (argumentsEnd != null && argumentsEnd > callEnds) {
292                 // The call element has arguments that are outside of its own range.
293                 // This typically means users are making a function call using
294                 // assignment syntax, e.g. key = value instead of setKey(value);
295                 // here the call range is just "key" and the arguments range is "value".
296                 // Create a range which merges these two.
297                 val startElement = if (!includeNew) call.classReference ?: call else call
298                 // Work around UAST bug where the value argument list points directly to the
299                 // string content node instead of a node containing the opening and closing
300                 // tokens as well. We need to include the closing tags in the range as well!
301                 val next =
302                     (lastArgument.sourcePsi as? KtLiteralStringTemplateEntry)?.nextSibling
303                         as? TreeElement
304                 val delta =
305                     if (next != null && next.elementType == KtTokens.CLOSING_QUOTE) {
306                         next.textLength
307                     } else {
308                         0
309                     }
310                 return getRangeLocation(startElement, 0, lastArgument, delta)
311             }
312         }
313     }
314 
315     val classReference = call.classReference
316     if (includeNew || classReference == null) {
317         if (includeArguments) {
318             // Method with arguments but no receiver is the default range for UCallExpressions
319             // modulo the scenario with arguments outside the call, handled at the beginning
320             // of this method
321             return getLocation(call)
322         }
323         // Just the method name
324         val methodIdentifier = call.methodIdentifier
325         if (methodIdentifier != null) {
326             return getLocation(methodIdentifier)
327         }
328     } else {
329         if (!includeArguments) {
330             val methodIdentifier = call.methodIdentifier
331             if (methodIdentifier != null) {
332                 return getRangeLocation(classReference, 0, methodIdentifier, 0)
333             }
334         }
335 
336         // Use PsiElement variant of getRangeLocation because UElement variant returns wrong results
337         // when the `from` argument starts after the `to` argument, as it does for the constructor
338         // class reference.
339         return getRangeLocation(classReference.javaPsi!!, 0, call.javaPsi!!, 0)
340     }
341 
342     return getLocation(call)
343 }
344 
345 /**
346  * @return the value of the specified vararg attribute as a list of String literals, or an empty
347  *   list if not specified
348  */
UAnnotationnull349 fun UAnnotation.getAttributeValueVarargLiteral(name: String): List<String> =
350     findDeclaredAttributeValue(name)?.parseVarargLiteral() ?: emptyList()
351 
352 fun UExpression.parseVarargLiteral(): List<String> =
353     when (val expr = this.unwrap()) {
354         is ULiteralExpression -> listOfNotNull(expr.parseLiteral())
355         is UCallExpression -> expr.valueArguments.mapNotNull { it.parseLiteral() }
356         else -> emptyList()
357     }
358 
UExpressionnull359 fun UExpression.parseLiteral(): String? =
360     when (val expr = this.unwrap()) {
361         is ULiteralExpression -> expr.value.toString()
362         else -> null
363     }
364 
UExpressionnull365 fun UExpression.unwrap(): UExpression =
366     when (this) {
367         is UParenthesizedExpression -> expression.unwrap()
368         is UPolyadicExpression -> operands.singleOrNull()?.unwrap() ?: this
369         is UQualifiedReferenceExpression -> selector.unwrap()
370         else -> this
371     }
372