[blocking calls detection] IDEA-310256: handle implicit constructor calls

GitOrigin-RevId: d550a9ad10d014116378fd4800cff7b72b7b51c1
This commit is contained in:
Aleksandr Izmailov
2023-01-12 17:22:16 +01:00
committed by intellij-monorepo-bot
parent 573f7f3268
commit 1555d664b6
8 changed files with 183 additions and 13 deletions

View File

@@ -5,7 +5,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
public @interface Blocking {
}

View File

@@ -5,6 +5,6 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
public @interface NonBlocking {}

View File

@@ -0,0 +1,35 @@
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NonBlocking;
public class TestBlockingConstructor {
static class BlockingConstructor {
@Blocking
BlockingConstructor() {
}
@Blocking
BlockingConstructor(String url) {
}
}
static class Intermediate extends BlockingConstructor {}
static class NonBlockingConstructor extends BlockingConstructor {
@NonBlocking
<warning descr="Possibly blocking call from implicit constructor call in non-blocking context could lead to thread starvation">NonBlockingConstructor</warning>() {
}
@NonBlocking
NonBlockingConstructor(String url) {
<warning descr="Possibly blocking call in non-blocking context could lead to thread starvation">super</warning>(url);
}
}
static class NonBlockingConstructorIntermediate extends Intermediate {
@NonBlocking
<warning descr="Possibly blocking call from implicit constructor call in non-blocking context could lead to thread starvation">NonBlockingConstructorIntermediate</warning>() {
}
}
}

View File

@@ -31,6 +31,8 @@ jvm.inspections.must.already.be.removed.api.earlier.version.description=API must
jvm.inspections.must.already.be.removed.api.current.version.description=API must be removed in the current version {0}
jvm.inspections.blocking.method.problem.descriptor=Possibly blocking call in non-blocking context could lead to thread starvation
jvm.inspections.blocking.method.problem.wildcard.descriptor=Possibly blocking call in {0} could lead to thread starvation
jvm.inspections.blocking.method.in.implicit.ctr.problem.descriptor=Possibly blocking call from implicit constructor call in non-blocking context could lead to thread starvation
jvm.inspections.blocking.method.in.implicit.ctr.problem.wildcard.descriptor=Possibly blocking call from implict constructor call in {0} could lead to thread starvation
jvm.inspections.blocking.method.display.name=Possibly blocking call in non-blocking context
jvm.inspections.blocking.method.annotation.blocking=Blocking annotations
jvm.inspections.blocking.method.annotation.non-blocking=Non-blocking annotations

View File

@@ -40,10 +40,17 @@ public final class AnnotationBasedNonBlockingContextChecker implements NonBlocki
@Override
public ContextType computeContextType(@NotNull ElementContext elementContext) {
UCallExpression callExpression = UastContextKt.toUElement(elementContext.getElement(), UCallExpression.class);
if (callExpression == null) return Unsure.INSTANCE;
UMethod callingMethod;
var method = UastContextKt.toUElement(elementContext.getElement(), UMethod.class);
if (method != null) {
callingMethod = method;
}
else {
UCallExpression callExpression = UastContextKt.toUElement(elementContext.getElement(), UCallExpression.class);
if (callExpression == null) return Unsure.INSTANCE;
callingMethod = UastUtils.getParentOfType(callExpression, UMethod.class);
}
UMethod callingMethod = UastUtils.getParentOfType(callExpression, UMethod.class);
if (callingMethod == null) return Unsure.INSTANCE;
PsiMethod psiCallingMethod = callingMethod.getJavaPsi();

View File

@@ -11,13 +11,12 @@ import com.intellij.openapi.progress.ProgressIndicatorProvider;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.*;
import com.intellij.util.containers.ContainerUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.uast.*;
import java.util.*;
@@ -52,12 +51,15 @@ public final class BlockingMethodInNonBlockingContextInspection extends Abstract
@Override
public @NotNull OptPane getOptionsPane() {
return pane(
checkbox("myConsiderUnknownContextBlocking", JvmAnalysisBundle.message("jvm.inspections.blocking.method.consider.unknown.context.blocking")),
checkbox("myConsiderUnknownContextBlocking",
JvmAnalysisBundle.message("jvm.inspections.blocking.method.consider.unknown.context.blocking")),
stringSet("myBlockingAnnotations", JvmAnalysisBundle.message("jvm.inspections.blocking.method.annotation.blocking"),
new JavaClassValidator().withTitle(JvmAnalysisBundle.message("jvm.inspections.blocking.method.annotation.configure.add.blocking.title"))
new JavaClassValidator().withTitle(
JvmAnalysisBundle.message("jvm.inspections.blocking.method.annotation.configure.add.blocking.title"))
.annotationsOnly()),
stringSet("myNonBlockingAnnotations", JvmAnalysisBundle.message("jvm.inspections.blocking.method.annotation.non-blocking"),
new JavaClassValidator().withTitle(JvmAnalysisBundle.message("jvm.inspections.blocking.method.annotation.configure.add.non-blocking.title"))
new JavaClassValidator().withTitle(
JvmAnalysisBundle.message("jvm.inspections.blocking.method.annotation.configure.add.non-blocking.title"))
.annotationsOnly())
);
}
@@ -126,11 +128,18 @@ public final class BlockingMethodInNonBlockingContextInspection extends Abstract
@Override
public void visitElement(@NotNull PsiElement element) {
super.visitElement(element);
if (visitConstructor(element)) return;
UCallExpression callExpression = AnalysisUastUtil.getUCallExpression(element);
if (callExpression == null) return;
PsiElement elementToHighLight = AnalysisUastUtil.getMethodIdentifierSourcePsi(callExpression);
if (elementToHighLight == null) return;
// implicit delegating constructor, check it in visitConstructor
if (callExpression.getKind() == UastCallKind.CONSTRUCTOR_CALL && elementToHighLight.getTextRange().isEmpty()) {
return;
}
ContextType contextType = isContextNonBlockingFor(element, myNonBlockingContextCheckers, mySettings);
if (contextType instanceof ContextType.Blocking) {
return;
@@ -168,8 +177,69 @@ public final class BlockingMethodInNonBlockingContextInspection extends Abstract
}
myHolder.registerProblem(elementToHighLight, message, fixesStream.toArray(LocalQuickFix.EMPTY_ARRAY));
}
private boolean visitConstructor(@NotNull PsiElement element) {
var method = UastContextKt.toUElement(element, UMethod.class);
if (method == null || !method.isConstructor()) return false;
var anchor = method.getUastAnchor();
if (anchor == null) return false;
var elementToHighlight = anchor.getSourcePsi();
if (elementToHighlight == null) return false;
if (!(method.getUastParent() instanceof UClass containingClass)) return false;
if (containingClass.getJavaPsi().getSuperClass() == null) return false;
if (!(method.getUastBody() instanceof UBlockExpression body)) return false;
var firstExpression = ContainerUtil.getFirstItem(body.getExpressions());
if (firstExpression != null && isExplicitSuperCall(firstExpression)) return false;
ContextType contextType = isContextNonBlockingFor(element, myNonBlockingContextCheckers, mySettings);
if (contextType instanceof ContextType.Blocking) {
return true;
}
if (!(contextType instanceof ContextType.NonBlocking nonBlockingContext)) return true;
var implicitlyCalledCtr = findFirstExplicitNoArgConstructor(containingClass.getJavaPsi().getSuperClass());
if (implicitlyCalledCtr == null) return true;
if (!isMethodBlocking(implicitlyCalledCtr, myBlockingMethodCheckers, mySettings)) return true;
String message;
if (StringUtil.isNotEmpty(nonBlockingContext.getDescription())) {
String contextDescription = nonBlockingContext.getDescription();
message = JvmAnalysisBundle.message("jvm.inspections.blocking.method.in.implicit.ctr.problem.wildcard.descriptor", contextDescription);
}
else {
message = JvmAnalysisBundle.message("jvm.inspections.blocking.method.in.implicit.ctr.problem.descriptor");
}
myHolder.registerProblem(elementToHighlight, message);
return true;
}
private static @Nullable PsiMethod findFirstExplicitNoArgConstructor(@NotNull PsiClass currentClass) {
while (currentClass != null) {
var explicitEmptyArgCtr = ContainerUtil.find(currentClass.getConstructors(), ctr -> !ctr.hasParameters());
if (explicitEmptyArgCtr != null) {
return explicitEmptyArgCtr;
}
currentClass = currentClass.getSuperClass();
}
return null;
}
private static boolean isExplicitSuperCall(@NotNull UExpression expression) {
if (!(expression instanceof USuperExpression) &&
!(expression instanceof UCallExpression call && call.getKind() == UastCallKind.CONSTRUCTOR_CALL)) return true;
var sourcePsi = expression.getSourcePsi();
if (sourcePsi == null) return false;
return !sourcePsi.getTextRange().isEmpty();
}
}
private static boolean isMethodOrSupersBlocking(PsiMethod referencedMethod,
List<BlockingMethodChecker> checkers,
BlockingCallInspectionSettings settings) {

View File

@@ -84,4 +84,9 @@ public class JavaBlockingMethodInNonBlockingContextInspectionTest extends Useful
myFixture.configureByFiles("TestThrowsTypeDetection.java", "Blocking.java", "NonBlocking.java");
myFixture.testHighlighting(true, false, true, "TestThrowsTypeDetection.java");
}
public void testBlockingConstructor() {
myFixture.configureByFiles("TestBlockingConstructor.java", "Blocking.java", "NonBlocking.java");
myFixture.testHighlighting(true, false, true, "TestBlockingConstructor.java");
}
}

View File

@@ -54,4 +54,55 @@ public class KotlinBlockingCallDetectionTest extends JavaCodeInsightFixtureTestC
myFixture.checkHighlighting(true, false, true);
}
public void testDelegatingConstructor() {
myFixture.addClass("package org.jetbrains.annotations;\n" +
"public @interface Blocking {}");
myFixture.addClass("package org.jetbrains.annotations;\n" +
"public @interface NonBlocking {}");
myFixture.addClass("""
import org.jetbrains.annotations.*;
public class BlockingCtrClass {
@Blocking
BlockingCtrClass() {
}
public static class Intermediate extends BlockingCtrClass {}
}
"""
);
myFixture.configureByText("file.kt",
"""
import org.jetbrains.annotations.*
class NonBlockingCtrClass : BlockingCtrClass {
@NonBlocking
<warning descr="Possibly blocking call from implicit constructor call in non-blocking context could lead to thread starvation">constructor</warning>() {}
}
class NonBlockingCtrClassWithIntermediate : BlockingCtrClass.Intermediate {
@NonBlocking
<warning descr="Possibly blocking call from implicit constructor call in non-blocking context could lead to thread starvation">constructor</warning>() {}
}
class NonBlockingCtrClassExplicit @NonBlocking constructor() : <warning descr="Possibly blocking call in non-blocking context could lead to thread starvation">BlockingCtrClass</warning>()
class NonBlockingCtrClassExplicit2 : BlockingCtrClass {
@NonBlocking
constructor() : <warning descr="Possibly blocking call in non-blocking context could lead to thread starvation">super</warning>()
}
open class KotlinBlockingCtr @Blocking constructor()
class NonBlockingCtr : KotlinBlockingCtr {
@NonBlocking
<warning descr="Possibly blocking call from implicit constructor call in non-blocking context could lead to thread starvation">constructor</warning>() {}
}
""");
myFixture.testHighlighting(true, false, true, "file.kt");
}
}