[java] Consider @Blocking and @NonBlocking on classes in BlockingMethodInNonBlockingContextInspection

GitOrigin-RevId: d3a486b85659c78901a326f08ebadeadd8e656ab
This commit is contained in:
Yuriy Artamonov
2021-09-05 16:21:13 +00:00
committed by intellij-monorepo-bot
parent 5ec97fe500
commit 9d7c5caf91
11 changed files with 187 additions and 80 deletions
@@ -5,7 +5,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Blocking {
}
@@ -5,6 +5,6 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface NonBlocking {}
@@ -0,0 +1,29 @@
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NonBlocking;
public class TestClassAnnotationsDetection {
@NonBlocking
private static void nonBlocking(BlockingApiClass blockingApi, NonBlockingApiClass nonBlockingApi) {
blockingApi.<warning descr="Possibly blocking call in non-blocking context could lead to thread starvation">runBlocking</warning>();
blockingApi.runNonBlocking();
nonBlockingApi.<warning descr="Possibly blocking call in non-blocking context could lead to thread starvation">runBlocking</warning>();
nonBlockingApi.runNonBlocking();
}
}
@Blocking
class BlockingApiClass {
public void runBlocking() { }
@NonBlocking
public void runNonBlocking() { }
}
@NonBlocking
class NonBlockingApiClass {
@Blocking
public void runBlocking() { }
public void runNonBlocking() { }
}
@@ -6,6 +6,7 @@ import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiMethod;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
/**
@@ -21,9 +22,20 @@ public interface BlockingMethodChecker {
*/
boolean isApplicable(@NotNull PsiFile file);
boolean isMethodBlocking(@NotNull PsiMethod method);
/**
* @deprecated Override {@link #isMethodBlocking(MethodContext)} instead.
*/
@Deprecated
@ApiStatus.ScheduledForRemoval(inVersion = "2022.1")
default boolean isMethodBlocking(@NotNull PsiMethod method) {
return false;
}
default boolean isMethodNonBlocking(@NotNull PsiMethod method) {
default boolean isMethodBlocking(@NotNull MethodContext methodContext) {
return isMethodBlocking(methodContext.getMethod());
}
default boolean isMethodNonBlocking(@NotNull MethodContext methodContext) {
return false;
}
@@ -0,0 +1,33 @@
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInspection.blockingCallsDetection;
import com.intellij.psi.PsiMethod;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public final class MethodContext {
private final PsiMethod myMethod;
private final List<BlockingMethodChecker> myCheckers;
public MethodContext(@NotNull PsiMethod method, @NotNull List<BlockingMethodChecker> checkers) {
myMethod = method;
myCheckers = checkers;
}
public @NotNull PsiMethod getMethod() {
return myMethod;
}
public @NotNull List<BlockingMethodChecker> getCheckers() {
return myCheckers;
}
public boolean isMethodNonBlocking() {
for (BlockingMethodChecker checker : myCheckers) {
if (checker.isMethodNonBlocking(this)) return true;
}
return false;
}
}
@@ -3,39 +3,56 @@ package com.intellij.codeInspection.blockingCallsDetection;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiMethod;
import one.util.streamex.StreamEx;
import com.intellij.psi.search.GlobalSearchScope;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import static com.intellij.codeInspection.blockingCallsDetection.BlockingMethodInNonBlockingContextInspection.DEFAULT_BLOCKING_ANNOTATIONS;
import java.util.Collection;
public final class AnnotationBasedBlockingMethodChecker implements BlockingMethodChecker {
private final List<String> myBlockingAnnotations;
private final List<String> myNonBlockingAnnotations;
private final Collection<String> myBlockingAnnotations;
private final Collection<String> myNonBlockingAnnotations;
public AnnotationBasedBlockingMethodChecker(List<String> blockingAnnotations, List<String> nonBlockingAnnotations) {
public AnnotationBasedBlockingMethodChecker(@NotNull Collection<String> blockingAnnotations,
@NotNull Collection<String> nonBlockingAnnotations) {
myBlockingAnnotations = blockingAnnotations;
myNonBlockingAnnotations = nonBlockingAnnotations;
}
@Override
public boolean isApplicable(@NotNull PsiFile file) {
return myBlockingAnnotations != null &&
StreamEx.of(DEFAULT_BLOCKING_ANNOTATIONS)
.append(myBlockingAnnotations)
.anyMatch(annotation -> JavaPsiFacade.getInstance(file.getProject()).findClass(annotation, file.getResolveScope()) != null);
JavaPsiFacade javaPsi = JavaPsiFacade.getInstance(file.getProject());
GlobalSearchScope fileResolveScope = file.getResolveScope();
for (String annotation : myBlockingAnnotations) {
if (javaPsi.findClass(annotation, fileResolveScope) != null) return true;
}
for (String annotation : myNonBlockingAnnotations) {
if (javaPsi.findClass(annotation, fileResolveScope) != null) return true;
}
return false;
}
@Override
public boolean isMethodBlocking(@NotNull PsiMethod method) {
return AnnotationUtil.findAnnotation(method, myBlockingAnnotations, false) != null;
public boolean isMethodBlocking(@NotNull MethodContext context) {
return isMethodOrClassAnnotated(context.getMethod(), myBlockingAnnotations, myNonBlockingAnnotations);
}
@Override
public boolean isMethodNonBlocking(@NotNull PsiMethod method) {
return AnnotationUtil.findAnnotation(method, myNonBlockingAnnotations, false) != null;
public boolean isMethodNonBlocking(@NotNull MethodContext context) {
return isMethodOrClassAnnotated(context.getMethod(), myNonBlockingAnnotations, myBlockingAnnotations);
}
private static boolean isMethodOrClassAnnotated(@NotNull PsiMethod method,
@NotNull Collection<String> annotations,
@NotNull Collection<String> denyAnnotations) {
if (AnnotationUtil.findAnnotation(method, annotations, false) != null) return true;
// @NonBlocking on method overrides @Blocking on class
if (AnnotationUtil.findAnnotation(method, denyAnnotations, false) != null) return false;
PsiClass containingClass = method.getContainingClass();
return containingClass != null
&& AnnotationUtil.findAnnotation(containingClass, annotations, false) != null;
}
}
@@ -2,35 +2,33 @@
package com.intellij.codeInspection.blockingCallsDetection;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiMethod;
import one.util.streamex.StreamEx;
import com.intellij.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UMethod;
import org.jetbrains.uast.UastContextKt;
import org.jetbrains.uast.UastUtils;
import java.util.List;
import java.util.Collection;
import static com.intellij.codeInspection.blockingCallsDetection.BlockingMethodInNonBlockingContextInspection.DEFAULT_NONBLOCKING_ANNOTATIONS;
public final class AnnotationBasedNonBlockingContextChecker implements NonBlockingContextChecker {
public class AnnotationBasedNonBlockingContextChecker implements NonBlockingContextChecker {
private final Collection<String> myBlockingAnnotations;
private final Collection<String> myNonBlockingAnnotations;
private final List<String> myNonBlockingAnnotations;
public AnnotationBasedNonBlockingContextChecker(List<String> nonBlockingAnnotations) {
public AnnotationBasedNonBlockingContextChecker(@NotNull Collection<String> blockingAnnotations,
@NotNull Collection<String> nonBlockingAnnotations) {
myBlockingAnnotations = blockingAnnotations;
myNonBlockingAnnotations = nonBlockingAnnotations;
}
@Override
public boolean isApplicable(@NotNull PsiFile file) {
return myNonBlockingAnnotations != null &&
StreamEx.of(DEFAULT_NONBLOCKING_ANNOTATIONS)
.append(myNonBlockingAnnotations)
.anyMatch(annotation -> JavaPsiFacade.getInstance(file.getProject()).findClass(annotation, file.getResolveScope()) != null);
JavaPsiFacade javaPsi = JavaPsiFacade.getInstance(file.getProject());
for (String annotation : myNonBlockingAnnotations) {
if (javaPsi.findClass(annotation, file.getResolveScope()) != null) return true;
}
return false;
}
@Override
@@ -42,6 +40,17 @@ public class AnnotationBasedNonBlockingContextChecker implements NonBlockingCont
if (callingMethod == null) return false;
PsiMethod psiCallingMethod = callingMethod.getJavaPsi();
return AnnotationUtil.findAnnotation(psiCallingMethod, myNonBlockingAnnotations, false) != null;
if (AnnotationUtil.findAnnotation(psiCallingMethod, myNonBlockingAnnotations, false) != null) {
return true;
}
if (AnnotationUtil.findAnnotation(psiCallingMethod, myBlockingAnnotations, false) != null) {
// @Blocking on method overrides @NonBlocking on class
return false;
}
PsiClass containingClass = psiCallingMethod.getContainingClass();
return containingClass != null
&& AnnotationUtil.findAnnotation(containingClass, myNonBlockingAnnotations, false) != null;
}
}
@@ -9,6 +9,7 @@ import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.progress.ProgressIndicatorProvider;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.wm.IdeFocusManager;
@@ -25,8 +26,10 @@ import org.jetbrains.uast.UCallExpression;
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import static java.util.Collections.emptyList;
public final class BlockingMethodInNonBlockingContextInspection extends AbstractBaseUastLocalInspectionTool {
@@ -52,31 +55,44 @@ public final class BlockingMethodInNonBlockingContextInspection extends Abstract
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
List<NonBlockingContextChecker> nonBlockingContextCheckers = getNonBlockingContextCheckers(holder.getFile());
Collection<String> nonBlockingAnnotations = union(myNonBlockingAnnotations, DEFAULT_NONBLOCKING_ANNOTATIONS);
Collection<String> blockingAnnotations = union(myBlockingAnnotations, DEFAULT_BLOCKING_ANNOTATIONS);
List<NonBlockingContextChecker> nonBlockingContextCheckers =
getNonBlockingContextCheckers(holder.getFile(), blockingAnnotations, nonBlockingAnnotations);
if (nonBlockingContextCheckers.isEmpty()) return PsiElementVisitor.EMPTY_VISITOR;
List<BlockingMethodChecker> blockingMethodCheckers = getBlockingMethodCheckers(holder.getFile());
List<BlockingMethodChecker> blockingMethodCheckers =
getBlockingMethodCheckers(holder.getFile(), blockingAnnotations, nonBlockingAnnotations);
if (blockingMethodCheckers.isEmpty()) return PsiElementVisitor.EMPTY_VISITOR;
return new BlockingMethodInNonBlockingContextVisitor(holder, blockingMethodCheckers, nonBlockingContextCheckers);
}
@NotNull
private List<NonBlockingContextChecker> getNonBlockingContextCheckers(@NotNull PsiFile file) {
private static @NotNull List<NonBlockingContextChecker> getNonBlockingContextCheckers(@NotNull PsiFile file,
@NotNull Collection<String> blockingAnnotations,
@NotNull Collection<String> nonBlockingAnnotations) {
List<NonBlockingContextChecker> nonBlockingContextCheckers = new ArrayList<>(NonBlockingContextChecker.EP_NAME.getExtensionList());
nonBlockingContextCheckers.add(new AnnotationBasedNonBlockingContextChecker(myNonBlockingAnnotations));
nonBlockingContextCheckers.add(new AnnotationBasedNonBlockingContextChecker(blockingAnnotations, nonBlockingAnnotations));
nonBlockingContextCheckers.removeIf(checker -> !checker.isApplicable(file));
return nonBlockingContextCheckers;
}
@NotNull
private List<BlockingMethodChecker> getBlockingMethodCheckers(@NotNull PsiFile file) {
private static @NotNull List<BlockingMethodChecker> getBlockingMethodCheckers(@NotNull PsiFile file,
@NotNull Collection<String> blockingAnnotations,
@NotNull Collection<String> nonBlockingAnnotations) {
List<BlockingMethodChecker> blockingMethodCheckers = new ArrayList<>(BlockingMethodChecker.EP_NAME.getExtensionList());
blockingMethodCheckers.add(new AnnotationBasedBlockingMethodChecker(myBlockingAnnotations, myNonBlockingAnnotations));
blockingMethodCheckers.add(new AnnotationBasedBlockingMethodChecker(blockingAnnotations, nonBlockingAnnotations));
blockingMethodCheckers.removeIf(checker -> !checker.isApplicable(file));
return blockingMethodCheckers;
}
private static Collection<String> union(Collection<String> annotations, Collection<String> defaultAnnotations) {
Set<String> result = new HashSet<>(defaultAnnotations);
result.addAll(annotations != null ? annotations : emptyList());
return result;
}
private final class OptionsPanel extends JPanel {
private OptionsPanel() {
super(new BorderLayout());
@@ -123,15 +139,15 @@ public final class BlockingMethodInNonBlockingContextInspection extends Abstract
private static class BlockingMethodInNonBlockingContextVisitor extends PsiElementVisitor {
private final ProblemsHolder myHolder;
private final List<? extends BlockingMethodChecker> myBlockingMethodCheckers;
private final List<? extends NonBlockingContextChecker> myNonBlockingContextCheckers;
private final List<BlockingMethodChecker> myBlockingMethodCheckers;
private final List<NonBlockingContextChecker> myNonBlockingContextCheckers;
BlockingMethodInNonBlockingContextVisitor(@NotNull ProblemsHolder holder,
List<? extends BlockingMethodChecker> blockingMethodCheckers,
List<? extends NonBlockingContextChecker> nonBlockingContextCheckers) {
List<BlockingMethodChecker> blockingMethodCheckers,
List<NonBlockingContextChecker> nonBlockingContextCheckers) {
myHolder = holder;
this.myBlockingMethodCheckers = blockingMethodCheckers;
this.myNonBlockingContextCheckers = nonBlockingContextCheckers;
myBlockingMethodCheckers = blockingMethodCheckers;
myNonBlockingContextCheckers = nonBlockingContextCheckers;
}
@Override
@@ -145,10 +161,8 @@ public final class BlockingMethodInNonBlockingContextInspection extends Abstract
PsiMethod referencedMethod = callExpression.resolve();
if (referencedMethod == null) return;
if (!isMethodOrSupersBlocking(referencedMethod, myBlockingMethodCheckers)) return;
// if some implementation believes this method is non-blocking then we don't report it
if (isMethodNonBlocking(referencedMethod, myBlockingMethodCheckers)) return;
MethodContext methodContext = new MethodContext(referencedMethod, myBlockingMethodCheckers);
if (!isMethodOrSupersBlocking(methodContext)) return;
PsiElement elementToHighLight = AnalysisUastUtil.getMethodIdentifierSourcePsi(callExpression);
if (elementToHighLight == null) return;
@@ -162,25 +176,15 @@ public final class BlockingMethodInNonBlockingContextInspection extends Abstract
}
}
private static boolean isMethodOrSupersBlocking(PsiMethod referencedMethod,
List<? extends BlockingMethodChecker> blockingMethodCheckers) {
return StreamEx.of(referencedMethod).append(referencedMethod.findDeepestSuperMethods())
.anyMatch(method -> isMethodBlocking(method, blockingMethodCheckers));
private static boolean isMethodOrSupersBlocking(MethodContext methodContext) {
return StreamEx.of(methodContext.getMethod()).append(methodContext.getMethod().findDeepestSuperMethods())
.anyMatch(method -> isMethodBlocking(methodContext));
}
private static boolean isMethodBlocking(PsiMethod method,
List<? extends BlockingMethodChecker> blockingMethodCheckers) {
return blockingMethodCheckers.stream().anyMatch(extension -> {
ProgressIndicatorProvider.checkCanceled();
return extension.isMethodBlocking(method);
});
}
private static boolean isMethodNonBlocking(PsiMethod method,
List<? extends BlockingMethodChecker> blockingMethodCheckers) {
return blockingMethodCheckers.stream().anyMatch(extension -> {
ProgressIndicatorProvider.checkCanceled();
return extension.isMethodNonBlocking(method);
private static boolean isMethodBlocking(MethodContext methodContext) {
return methodContext.getCheckers().stream().anyMatch(extension -> {
ProgressManager.checkCanceled();
return extension.isMethodBlocking(methodContext);
});
}
@@ -2,7 +2,6 @@
package com.intellij.codeInspection.blockingCallsDetection;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.util.containers.ContainerUtil;
import one.util.streamex.StreamEx;
@@ -21,8 +20,8 @@ final class ThrowsTypeBlockingMethodChecker implements BlockingMethodChecker {
}
@Override
public boolean isMethodBlocking(@NotNull PsiMethod method) {
return StreamEx.of(method.getThrowsList().getReferencedTypes())
public boolean isMethodBlocking(@NotNull MethodContext context) {
return StreamEx.of(context.getMethod().getThrowsList().getReferencedTypes())
.cross(BLOCKING_EXCEPTION_TYPES)
.anyMatch(entry -> InheritanceUtil.isInheritor(entry.getKey(), entry.getValue()));
}
@@ -70,6 +70,11 @@ public class BlockingMethodInNonBlockingContextInspectionTest extends UsefulTest
myFixture.testHighlighting(true, false, true, "TestSimpleAnnotationsDetection.java");
}
public void testClassAnnotationDetection() {
myFixture.configureByFiles("TestClassAnnotationsDetection.java", "Blocking.java", "NonBlocking.java");
myFixture.testHighlighting(true, false, true, "TestClassAnnotationsDetection.java");
}
public void testExternalBlockingAnnotationDetection() {
myFixture.configureByFiles("TestExternalAnnotationsDetection.java", "Blocking.java", "NonBlocking.java", "annotations.xml");
myFixture.testHighlighting(true, false, true, "TestExternalAnnotationsDetection.java");
@@ -2,6 +2,7 @@
package org.jetbrains.kotlin.idea.inspections.blockingCallsDetection
import com.intellij.codeInspection.blockingCallsDetection.BlockingMethodChecker
import com.intellij.codeInspection.blockingCallsDetection.MethodContext
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiMethod
@@ -23,10 +24,8 @@ internal class CoroutineBlockingMethodChecker : BlockingMethodChecker {
return languageVersionSettings.supportsFeature(LanguageFeature.ReleaseCoroutines)
}
override fun isMethodBlocking(method: PsiMethod): Boolean = false
override fun isMethodNonBlocking(method: PsiMethod): Boolean {
val uMethod = method.toUElement()
override fun isMethodNonBlocking(context: MethodContext): Boolean {
val uMethod = context.method.toUElement()
val sourcePsi = uMethod?.sourcePsi ?: return false
return sourcePsi is KtNamedFunction && sourcePsi.modifierList?.hasSuspendModifier() == true
}