| /* |
| * Copyright (C) 2019 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.studio |
| |
| import com.android.tools.lint.detector.api.Detector |
| 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.intellij.psi.PsiMethod |
| import com.intellij.util.containers.MultiMap |
| import org.jetbrains.uast.UCallExpression |
| |
| /** |
| * Forbid methods related to Futures that run code in an implicitly chosen Executor. |
| */ |
| class ImplicitExecutorDetector : Detector(), SourceCodeScanner { |
| |
| companion object Issues { |
| private val IMPLEMENTATION = Implementation( |
| ImplicitExecutorDetector::class.java, |
| Scope.JAVA_FILE_SCOPE |
| ) |
| |
| @JvmField |
| val ISSUE = Issue.create( |
| id = "ImplicitExecutor", |
| briefDescription = "Using an implicitly chosen Executor", |
| explanation = """ |
| Not specifying an Executor for running callbacks is a common source of threading |
| issues, resulting in too much work running on the UI thread or the shared |
| ForkJoinPool used by the IDE for highlighting. |
| |
| In Android Studio, always specify the executor for running callbacks. As explained |
| in the documentation for `ListenableFuture.addCallback`, try to avoid |
| `directExecutor()`. |
| |
| For more, see `go/do-not-freeze`. |
| """, |
| category = UI_RESPONSIVENESS, |
| priority = 6, |
| severity = Severity.ERROR, |
| platforms = STUDIO_PLATFORMS, |
| implementation = IMPLEMENTATION |
| ) |
| |
| private const val COMPLETABLE_FUTURE = "java.util.concurrent.CompletableFuture" |
| private const val EXECUTOR = "java.util.concurrent.Executor" |
| |
| private val knownMethods = MultiMap<String, String>().apply { |
| // These got removed in later versions of Guava, see |
| // https://github.com/google/guava/commit/87d87f5cac5a540d46a6382683722ead7b72d1b3#diff-3fe13f15fa4a5af9b4a55b21d7db2541 |
| put( |
| "com.google.common.util.concurrent.Futures", |
| listOf( |
| "addCallback", |
| "catching", |
| "catchingAsync", |
| "transform", |
| "transformAsync" |
| ) |
| ) |
| |
| // These got removed in later versions of Guava, see |
| // https://github.com/google/guava/commit/87d87f5cac5a540d46a6382683722ead7b72d1b3#diff-3fe13f15fa4a5af9b4a55b21d7db2541 |
| put( |
| "com.google.common.util.concurrent.Futures.FutureCombiner", |
| listOf( |
| "call", |
| "callAsync" |
| ) |
| ) |
| |
| put( |
| COMPLETABLE_FUTURE, |
| Class.forName(COMPLETABLE_FUTURE) |
| .methods |
| .asSequence() |
| .map { it.name } |
| .filter { it.endsWith("Async") } |
| .toSet() |
| ) |
| } |
| } |
| |
| override fun getApplicableMethodNames(): List<String>? = knownMethods.values().toList() |
| |
| override fun visitMethodCall( |
| context: JavaContext, |
| node: UCallExpression, |
| method: PsiMethod |
| ) { |
| val evaluator = context.evaluator |
| val containingClass = method.containingClass ?: return |
| if (!knownMethods.get(containingClass.qualifiedName).contains(method.name)) return |
| |
| val lastParameter = evaluator.getParameterCount(method) - 1 |
| if (evaluator.parameterHasType(method, lastParameter, EXECUTOR)) return |
| |
| // See if this method has an overload that takes an Executor. |
| val overloads = containingClass.findMethodsByName(method.name, false) |
| if (overloads.size < 2) { |
| return |
| } |
| |
| val parametersWithExecutor = |
| method.parameterList.parameters.asSequence() |
| .map { it.type.canonicalText } |
| .plus(EXECUTOR) |
| .toList() |
| .toTypedArray() |
| |
| val overloadWithExecutor = |
| overloads.firstOrNull { evaluator.parametersMatch(it, *parametersWithExecutor) } |
| |
| if (overloadWithExecutor != null) { |
| context.report( |
| ISSUE, |
| node, |
| context.getCallLocation(node, includeReceiver = true, includeArguments = false), |
| "Use `${method.name}` overload with an explicit Executor instead. See `go/do-not-freeze`." |
| ) |
| } |
| } |
| } |