summaryrefslogtreecommitdiff
path: root/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/CheckResultDetector.kt
blob: ad72c0bcbd55c998ce292a35b162fc11bf20571d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
/*
 * Copyright (C) 2017 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 com.android.tools.lint.checks

import com.android.tools.lint.detector.api.AnnotationInfo
import com.android.tools.lint.detector.api.AnnotationOrigin
import com.android.tools.lint.detector.api.AnnotationUsageInfo
import com.android.tools.lint.detector.api.AnnotationUsageType
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Implementation
import com.android.tools.lint.detector.api.Issue
import com.android.tools.lint.detector.api.JavaContext
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.android.tools.lint.detector.api.UImplicitCallExpression
import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationStringValue
import com.android.tools.lint.detector.api.findSelector
import com.android.tools.lint.detector.api.isJava
import com.android.tools.lint.detector.api.isKotlin
import com.android.tools.lint.detector.api.nextStatement
import com.android.tools.lint.detector.api.previousStatement
import com.intellij.psi.PsiClassType
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiParameter
import com.intellij.psi.PsiSynchronizedStatement
import com.intellij.psi.PsiType
import com.intellij.psi.PsiWildcardType
import org.jetbrains.uast.UAnnotationMethod
import org.jetbrains.uast.UAnonymousClass
import org.jetbrains.uast.UBlockExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClassInitializer
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UIfExpression
import org.jetbrains.uast.ULambdaExpression
import org.jetbrains.uast.UMethod
import org.jetbrains.uast.UParenthesizedExpression
import org.jetbrains.uast.UQualifiedReferenceExpression
import org.jetbrains.uast.UResolvable
import org.jetbrains.uast.USwitchClauseExpressionWithBody
import org.jetbrains.uast.USwitchExpression
import org.jetbrains.uast.UYieldExpression
import org.jetbrains.uast.getParentOfType
import org.jetbrains.uast.skipParenthesizedExprUp
import java.util.EnumSet

class CheckResultDetector : AbstractAnnotationDetector(), SourceCodeScanner {
    override fun applicableAnnotations(): List<String> = listOf(
        // Match all annotations named *.CheckResult or *.CheckReturnValue; there are many:
        // android.support.annotation.CheckResult
        // androidx.annotation.CheckResult
        // edu.umd.cs.findbugs.annotations.CheckReturnValue
        // javax.annotation.CheckReturnValue
        // io.reactivex.annotations.CheckReturnValue
        // io.reactivex.rxjava3.annotations.CheckReturnValue
        // com.google.errorprone.annotations.CheckReturnValue
        // com.google.protobuf.CheckReturnValue
        // org.mockito.CheckReturnValue
        // as well as com.google.errorprone.annotations.CanIgnoreReturnValue
        "CheckResult",
        "CheckReturnValue",
        "CanIgnoreReturnValue"
    )

    override fun isApplicableAnnotationUsage(type: AnnotationUsageType): Boolean {
        return type != AnnotationUsageType.METHOD_OVERRIDE && super.isApplicableAnnotationUsage(type)
    }

    override fun visitAnnotationUsage(
        context: JavaContext,
        element: UElement,
        annotationInfo: AnnotationInfo,
        usageInfo: AnnotationUsageInfo
    ) {
        if (annotationInfo.qualifiedName.endsWith(".CanIgnoreReturnValue")) {
            return
        }

        val method = usageInfo.referenced as? PsiMethod ?: return
        val returnType = method.returnType ?: return
        if (returnType == PsiType.VOID || method.isConstructor || returnType.canonicalText == "kotlin.Unit") {
            return
        }

        // @CheckReturnValue, unlike @CheckResult, can be applied not just on methods,
        // but on classes and packages too, implying a "default" for all non-void methods.
        // However, in this case we don't want to also make it apply for all *subtypes*.
        // So for an annotation specified on a specific method, we'll apply it to overrides
        // as well, but for "outer" annotations, we'll only apply them within the inner context,
        // not in subclasses.
        if (annotationInfo.origin != AnnotationOrigin.METHOD && annotationInfo.isInherited()) {
            return
        }

        if (isExpressionValueUnused(element)) {
            if (context.evaluator.isSuspend(method)) {
                // For coroutines the suspend methods return context rather than the intended return type,
                // which is encoded in a continuation parameter at the end of the parameter list
                val classReference = method.parameterList.parameters.lastOrNull()?.type as? PsiClassType ?: return
                val wildcard = classReference.parameters.singleOrNull() as? PsiWildcardType ?: return
                val bound = wildcard.bound ?: return
                if (bound == PsiType.VOID || bound.canonicalText == "kotlin.Unit") {
                    return
                }
            }

            // Type parameter which resolves to Void?
            if (element is UExpression && element.getExpressionType()?.canonicalText == "java.lang.Void") {
                return
            }

            // If this CheckResult annotation is from a class, check to see
            // if it's been reversed with @CanIgnoreReturnValue
            if (usageInfo.anyCloser { it.qualifiedName.endsWith(".CanIgnoreReturnValue") }) {
                return
            }
            if (context.isTestSource && expectsSideEffect(context, element)) {
                return
            }

            val methodName = JavaContext.getMethodName(element)
            val annotation = annotationInfo.annotation
            val suggested = getAnnotationStringValue(
                annotation,
                AnnotationDetector.ATTR_SUGGEST
            )

            // Failing to check permissions is a potential security issue (and had an existing
            // dedicated issue id before which people may already have configured with a
            // custom severity in their LintOptions etc) so continue to use that issue
            // (which also has category Security rather than Correctness) for these:
            var issue = CHECK_RESULT
            if (methodName != null && methodName.startsWith("check") &&
                methodName.contains("Permission")
            ) {
                issue = CHECK_PERMISSION
            }

            var message = String.format(
                "The result of `%1\$s` is not used",
                methodName
            )
            if (suggested != null) {
                // TODO: Resolve suggest attribute (e.g. prefix annotation class if it starts
                // with "#" etc?
                message = String.format(
                    "The result of `%1\$s` is not used; did you mean to call `%2\$s`?",
                    methodName, suggested
                )
            } else if ("intersect" == methodName && context.evaluator.isMemberInClass(
                    method,
                    "android.graphics.Rect"
                )
            ) {
                message += ". If the rectangles do not intersect, no change is made and the " +
                    "original rectangle is not modified. These methods return false to " +
                    "indicate that this has happened."
            }

            val fix = if (suggested != null) {
                fix().data(KEY_SUGGESTION, suggested)
            } else {
                null
            }

            val location = context.getLocation(element)
            report(context, issue, element, location, message, fix)
        }
    }

    companion object {
        /**
         * In unit tests it's often acceptable to ignore the return
         * value because you're either describing a mock of checking for
         * exceptions being thrown.
         */
        fun expectsSideEffect(context: JavaContext, element: UElement): Boolean {
            val containingMethod = element.getParentOfType(UMethod::class.java)

            // (1) try { annotated(); fail()/error()/throw X } catch { }
            val nextStatement = element.nextStatement()?.findSelector()
            if (nextStatement is UCallExpression) {
                val methodName = nextStatement.methodName
                // (Ideally we'd look for the Kotlin type `Nothing` here instead of checking
                // for methods named error and TODO, but UAST does not expose Kotlin types,
                // only the mapped types (e.g. both Unit and Nothing maps to void).
                if (methodName == "fail" || methodName == "error" || methodName == "TODO") {
                    return true
                }
            }

            // (2) @Test(expect=Exception.class) method() { ...; annotated(); ... }
            //noinspection ExternalAnnotations
            val annotations = containingMethod?.uAnnotations
            if (annotations != null && annotations.any {
                it.qualifiedName == "org.junit.Test" && it.findDeclaredAttributeValue("expected") != null
            }
            ) {
                return true
            }

            // (3) Within the context of a ThrowingRunnable/Executable, which includes
            //     assertThrows(Throwable.class, () => { me(); })
            if (nextStatement == null) {
                val lambda = skipParenthesizedExprUp(element.getParentOfType(ULambdaExpression::class.java))
                if (lambda is UExpression) {
                    val call = lambda.uastParent
                    if (call is UCallExpression) {
                        val resolved = call.resolve()
                        if (resolved != null) {
                            val methodName = resolved.name
                            // kotlin.test/common/src/main/kotlin/kotlin/test/Assertions.kt
                            if (methodName == "assertFails" || methodName == "assertFailsWith") {
                                return true
                            }
                            val parameter: PsiParameter? =
                                context.evaluator.computeArgumentMapping(call, resolved)[lambda]
                            if (parameter != null && isThrowingRunnable(parameter.type.canonicalText)) {
                                return true
                            }
                        }
                    }
                } else if (containingMethod != null) {
                    // Anonymous inner class?
                    //  assertThrows(Throwable.class, new ThrowingRunable() { ... me(); });
                    val containingClass = containingMethod.uastParent
                    if (containingClass is UAnonymousClass) {
                        for (type in containingClass.superTypes) {
                            if (isThrowingRunnable(type.canonicalText)) {
                                return true
                            }
                        }
                    }
                }
            }

            // (4) expectedException.expect(Foo.class); me();
            val previousStatement = element.previousStatement()?.findSelector()
            if (previousStatement is UCallExpression) {
                previousStatement.resolve()?.let { calledMethod ->
                    val containingClass = calledMethod.containingClass?.qualifiedName
                    if (containingClass == "org.junit.rules.ExpectedException") {
                        return true
                    }
                }
            }

            // (5) Mockito invocation
            if (element is UCallExpression) {
                val receiver = element.receiver
                if (receiver is UResolvable) {
                    val resolved = receiver.resolve()
                    if (resolved is PsiMethod) {
                        val containingClass = resolved.containingClass?.qualifiedName
                        if (containingClass != null && containingClass.startsWith("org.mockito.")) {
                            return true
                        }
                    }
                }
            }

            return false
        }

        private fun isThrowingRunnable(s: String): Boolean {
            // See Matchers.CLASSES_CONSIDERED_THROWING in errorprone
            return when (s) {
                "org.junit.function.ThrowingRunnable",
                "org.junit.jupiter.api.function.Executable",
                "org.assertj.core.api.ThrowableAssert\$ThrowingCallable",
                "com.google.devtools.build.lib.testutil.MoreAsserts\$ThrowingRunnable",
                "com.google.truth.ExpectFailure.AssertionCallback",
                "com.google.truth.ExpectFailure.DelegatedAssertionCallback",
                "com.google.truth.ExpectFailure.StandardSubjectBuilderCallback",
                "com.google.truth.ExpectFailure.SimpleSubjectBuilderCallback" -> true
                else -> false
            }
        }

        fun isExpressionValueUnused(element: UElement): Boolean {
            if (element is UParenthesizedExpression) {
                return isExpressionValueUnused(element.expression)
            }

            var prev: UElement = element.getParentOfType(UExpression::class.java, false) ?: return true

            if (prev is UImplicitCallExpression) {
                // Wrapped overloaded operator call: we need to point to the original element
                // such that the identity check below (for example in the UIfExpression handling)
                // recognizes it.
                prev = prev.expression
            }

            var curr: UElement = prev.uastParent ?: return true
            while (curr is UQualifiedReferenceExpression && curr.selector === prev || curr is UParenthesizedExpression) {
                prev = curr
                curr = curr.uastParent ?: return true
            }

            @Suppress("RedundantIf")
            if (curr is UBlockExpression) {
                if (curr.sourcePsi is PsiSynchronizedStatement) {
                    return false
                }
                // In Java, it's apparent when an expression is unused:
                // the parent is a block expression. However, in Kotlin it's
                // much trickier: values can flow through blocks and up through
                // if statements, try statements.
                //
                // In Kotlin, we consider an expression unused if its parent
                // is not a block, OR, the expression is not the last statement
                // in the block, OR, recursively the parent expression is not
                // used (e.g. you're in an if, but that if statement is itself
                // not doing anything with the value.)
                val block = curr
                val expression = prev
                val index = block.expressions.indexOf(expression)
                if (index == -1) {
                    return true
                }

                if (index < block.expressions.size - 1) {
                    // Not last child
                    return true
                }

                // It's the last child: see if the parent is unused
                val parent = skipParenthesizedExprUp(curr.uastParent)
                if (parent is ULambdaExpression && isKotlin(curr.sourcePsi)) {
                    val expressionType = parent.getExpressionType()?.canonicalText
                    if (expressionType != null &&
                        expressionType.startsWith("kotlin.jvm.functions.Function") &&
                        expressionType.endsWith("kotlin.Unit>")
                    ) {
                        // We know that this lambda does not return anything so the value is unused
                        return true
                    }
                    // Lambda block: for now assume used (e.g. parameter
                    // in call. Later consider recursing here to
                    // detect if the lambda itself is unused.
                    return false
                }

                if (isJava(curr.sourcePsi)) {
                    // In Java there's no implicit passing to the parent
                    return true
                }

                // It's the last child: see if the parent is unused
                parent ?: return true
                if (parent is UMethod || parent is UClassInitializer) {
                    return true
                }
                return isExpressionValueUnused(parent)
            } else if (curr is UMethod && curr.isConstructor) {
                return true
            } else if (curr is UIfExpression) {
                if (curr.condition === prev) {
                    return false
                } else if (curr.isTernary) {
                    // Ternary expressions can only be used as expressions, not statements,
                    // so we know that the value is used
                    return false
                }
                val parent = skipParenthesizedExprUp(curr.uastParent) ?: return true
                if (parent is UMethod || parent is UClassInitializer) {
                    return true
                }
                return isExpressionValueUnused(curr)
            } else if (curr is UMethod || curr is UClassInitializer) {
                if (curr is UAnnotationMethod) {
                    return false
                }
                return true
            } else {
                @Suppress("UnstableApiUsage")
                if (curr is UYieldExpression) {
                    val p2 = skipParenthesizedExprUp((skipParenthesizedExprUp(curr.uastParent))?.uastParent)
                    val body = p2 as? USwitchClauseExpressionWithBody ?: return false
                    val switch = body.getParentOfType(USwitchExpression::class.java) ?: return true
                    return isExpressionValueUnused(switch)
                }
                // Some other non block node type, such as assignment,
                // method declaration etc: not unused
                // TODO: Make sure that a void/unit method inline declaration
                // works correctly
                return false
            }
        }

        const val KEY_SUGGESTION = "suggestion"

        private val IMPLEMENTATION = Implementation(
            CheckResultDetector::class.java,
            EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES),
            Scope.JAVA_FILE_SCOPE
        )

        /** Method result should be used. */
        @JvmField
        val CHECK_RESULT = Issue.create(
            id = "CheckResult",
            briefDescription = "Ignoring results",
            explanation = """
                Some methods have no side effects, and calling them without doing something \
                without the result is suspicious.""",
            category = Category.CORRECTNESS,
            priority = 6,
            severity = Severity.WARNING,
            implementation = IMPLEMENTATION
        )

        /**
         * Failing to enforce security by just calling check permission.
         */
        @JvmField
        val CHECK_PERMISSION = Issue.create(
            id = "UseCheckPermission",
            briefDescription = "Using the result of check permission calls",
            explanation = """
                You normally want to use the result of checking a permission; these methods \
                return whether the permission is held; they do not throw an error if the \
                permission is not granted. Code which does not do anything with the return \
                value probably meant to be calling the enforce methods instead, e.g. rather \
                than `Context#checkCallingPermission` it should call \
                `Context#enforceCallingPermission`.""",
            category = Category.SECURITY,
            priority = 6,
            severity = Severity.WARNING,
            androidSpecific = true,
            implementation = IMPLEMENTATION
        )
    }
}