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