Blocking calls detection inspection created (IDEA-173248)

This commit is contained in:
Nikita Katkov
2018-06-26 15:27:53 +03:00
committed by Nicolay Mitropolsky
parent 2a877313ca
commit fea31cf421
21 changed files with 728 additions and 1 deletions

View File

@@ -132,6 +132,8 @@
<java.externalAnnotation implementation="com.intellij.codeInsight.externalAnnotation.NullableAnnotationProvider"/>
<java.externalAnnotation implementation="com.intellij.codeInsight.externalAnnotation.UnmodifiableAnnotationProvider"/>
<java.externalAnnotation implementation="com.intellij.codeInsight.externalAnnotation.UnmodifiableViewAnnotationProvider"/>
<java.externalAnnotation implementation="com.intellij.codeInsight.externalAnnotation.BlockingMethodAnnotationProvider"/>
<java.externalAnnotation implementation="com.intellij.codeInsight.externalAnnotation.NonblockingMethodAnnotationProvider"/>
<refactoring.pushDown language="JAVA" implementationClass="com.intellij.refactoring.memberPushDown.JavaPushDownDelegate" id="java"/>
<refactoring.introduceParameterObject language="JAVA"
implementationClass="com.intellij.refactoring.introduceparameterobject.JavaIntroduceParameterObjectDelegate"/>

View File

@@ -0,0 +1,25 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.externalAnnotation;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiModifierListOwner;
import org.jetbrains.annotations.NotNull;
public class BlockingMethodAnnotationProvider implements AnnotationProvider {
static final String DEFAULT_NONBLOCKING_ANNOTATION = "org.jetbrains.annotations.NonBlocking";
static final String DEFAULT_BLOCKING_ANNOTATION = "org.jetbrains.annotations.Blocking";
@NotNull
@Override
public String getName(Project project) {
return DEFAULT_BLOCKING_ANNOTATION;
}
@Override
public boolean isAvailable(PsiModifierListOwner owner) {
return owner instanceof PsiMethod &&
!owner.hasAnnotation(DEFAULT_NONBLOCKING_ANNOTATION);
}
}

View File

@@ -0,0 +1,25 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.externalAnnotation;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiModifierListOwner;
import org.jetbrains.annotations.NotNull;
import static com.intellij.codeInsight.externalAnnotation.BlockingMethodAnnotationProvider.DEFAULT_BLOCKING_ANNOTATION;
import static com.intellij.codeInsight.externalAnnotation.BlockingMethodAnnotationProvider.DEFAULT_NONBLOCKING_ANNOTATION;
public class NonblockingMethodAnnotationProvider implements AnnotationProvider {
@NotNull
@Override
public String getName(Project project) {
return DEFAULT_NONBLOCKING_ANNOTATION;
}
@Override
public boolean isAvailable(PsiModifierListOwner owner) {
return owner instanceof PsiMethod &&
!owner.hasAnnotation(DEFAULT_BLOCKING_ANNOTATION);
}
}

View File

@@ -0,0 +1,11 @@
package org.jetbrains.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.CLASS)
public @interface Blocking {
}

View File

@@ -0,0 +1,10 @@
package org.jetbrains.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.CLASS)
public @interface NonBlocking {}

View File

@@ -0,0 +1,10 @@
import org.jetbrains.annotations.Blocking;
public class TestExternalAnnotationsDetection {
@Blocking
private static void testBlocking() {}
private static void func() {
<warning descr="Inappropriate blocking method call">testBlocking</warning>();
}
}

View File

@@ -0,0 +1,16 @@
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NonBlocking;
public class TestSimpleAnnotationsDetection {
@Blocking
private static void testBlocking() {}
@NonBlocking
private static void nonBlocking() {
<warning descr="Inappropriate blocking method call">testBlocking</warning>();
}
private static void withoutAnnotation() {
testBlocking();
}
}

View File

@@ -0,0 +1,10 @@
import org.jetbrains.annotations.NonBlocking;
public class TestThrowsTypeDetection {
@NonBlocking
private static void nonBlocking() {
try {
Thread.<warning descr="Inappropriate blocking method call">sleep</warning>(1L);
} catch (InterruptedException ignored) {}
}
}

View File

@@ -0,0 +1,3 @@
<item name="TestExternalAnnotationsDetection void func()">
<annotation name="org.jetbrains.annotations.NonBlocking"/>
</item>

View File

@@ -9,5 +9,6 @@
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.jvm.analysis" exported="" />
<orderEntry type="module" module-name="intellij.java" />
</component>
</module>

View File

@@ -1,8 +1,20 @@
<idea-plugin>
<extensionPoints>
<extensionPoint qualifiedName="com.intellij.blockingMethodChecker"
interface="com.intellij.codeInspection.blockingCallsDetection.BlockingMethodChecker"/>
<extensionPoint qualifiedName="com.intellij.nonblockingMethodChecker"
interface="com.intellij.codeInspection.blockingCallsDetection.NonblockingContextChecker"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<localInspection language="UAST" enabledByDefault="true" level="WARNING" shortName="UnstableApiUsage"
groupBundle="com.intellij.jvm.analysis.JvmAnalysisBundle" bundle="com.intellij.jvm.analysis.JvmAnalysisBundle"
groupKey="jvm.inspections.group.name" key="jvm.inspections.unstable.api.usage.display.name"
implementationClass="com.intellij.codeInspection.UnstableApiUsageInspection"/>
<localInspection language="UAST" shortName="BlockingMethodInNonBlockingContext"
groupBundle="com.intellij.jvm.analysis.JvmAnalysisBundle" groupKey="jvm.inspections.group.name"
enabledByDefault="true" level="WARNING"
key="method.name.contains.blocking.word.display.name" bundle="com.intellij.jvm.analysis.JvmAnalysisBundle"
implementationClass="com.intellij.codeInspection.blockingCallsDetection.BlockingMethodInNonBlockingContextInspection"/>
<blockingMethodChecker implementation="com.intellij.codeInspection.blockingCallsDetection.ThrowsTypeBlockingMethodChecker"/>
</extensions>
</idea-plugin>

View File

@@ -5,5 +5,15 @@ jvm.inspections.unstable.api.usage.annotations.list=Unstable API annotations
jvm.inspections.unstable.api.usage.ignore.inside.imports=Ignore inside imports
jvm.inspections.unstable.api.usage.description=''{0}'' is marked unstable
method.name.contains.blocking.word.problem.descriptor=Inappropriate blocking method call
method.name.contains.blocking.word.display.name=Inappropriate thread-blocking method call found
inspection.blocking.method.annotation.blocking=Blocking Annotations
inspection.blocking.method.annotation.nonblocking=Non-blocking Annotations
inspection.blocking.method.annotation.configure.add.blocking.title=Add Blocking Annotation
inspection.blocking.method.annotation.configure.add.nonblocking.title=Add Non-Blocking Annotation
inspection.blocking.method.annotation.configure.empty.text=No annotations added.
inspection.blocking.method.annotation.configure.add.text=Add annotation
jvm.inspections.string.touppercase.tolowercase.without.locale.display.name=Call to 'String.toUpperCase()' or 'toLowerCase()' without a Locale
jvm.inspections.string.touppercase.tolowercase.without.locale.description=<code>String.{0}()</code> called without specifying a Locale using internationalized strings #loc
jvm.inspections.string.touppercase.tolowercase.without.locale.description=<code>String.{0}()</code> called without specifying a Locale using internationalized strings #loc

View File

@@ -0,0 +1,183 @@
// Copyright 2000-2018 JetBrains s.r.o. 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.codeInspection.ui.ListWrappingTableModel;
import com.intellij.icons.AllIcons;
import com.intellij.ide.util.ClassFilter;
import com.intellij.ide.util.TreeClassChooser;
import com.intellij.ide.util.TreeClassChooserFactory;
import com.intellij.openapi.actionSystem.Shortcut;
import com.intellij.openapi.actionSystem.ShortcutSet;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiClass;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.ui.*;
import com.intellij.ui.table.JBTable;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ui.EmptyIcon;
import com.intellij.util.ui.JBDimension;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.StatusText;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.TableColumn;
import java.awt.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class BlockingAnnotationsPanel {
private final Project myProject;
private String myDefaultAnnotation;
private final Set<String> myDefaultAnnotations;
private final JBTable myTable;
private final JPanel myComponent;
protected final ListWrappingTableModel myTableModel;
private final String myCustomEmptyText;
private final String myCustomAddLinkText;
BlockingAnnotationsPanel(Project project,
String name,
String defaultAnnotation,
List<String> annotations,
String[] defaultAnnotations,
String customEmptyText,
String customAddLinkText) {
myProject = project;
myDefaultAnnotation = defaultAnnotation;
myDefaultAnnotations = new HashSet<>(Arrays.asList(defaultAnnotations));
myCustomEmptyText = customEmptyText;
myCustomAddLinkText = customAddLinkText;
myTableModel = new ListWrappingTableModel(annotations, name) {
@Override
public boolean isCellEditable(int row, int column) {
return column == 1;
}
};
DefaultTableColumnModel columnModel = new DefaultTableColumnModel();
columnModel.addColumn(new TableColumn(0, 100, new ColoredTableCellRenderer() {
@Override
public void acquireState(JTable table, boolean isSelected, boolean hasFocus, int row, int column) {
super.acquireState(table, isSelected, false, row, column);
}
@Override
protected void customizeCellRenderer(JTable table,
Object value,
boolean selected,
boolean hasFocus,
int row,
int column) {
append((String)value, SimpleTextAttributes.REGULAR_ATTRIBUTES);
if (value.equals(myDefaultAnnotation)) {
setIcon(AllIcons.Diff.CurrentLine);
}
else {
setIcon(EmptyIcon.ICON_16);
}
}
}, null));
myTable = new JBTable(myTableModel, columnModel) {
@NotNull
@Override
public StatusText getEmptyText() {
StatusText emptyText = super.getEmptyText();
if (myCustomEmptyText != null && myCustomAddLinkText != null) {
emptyText.setText(myCustomEmptyText)
.appendSecondaryText(myCustomAddLinkText, SimpleTextAttributes.LINK_ATTRIBUTES, e -> chooseAnnotation(name));
ShortcutSet shortcutSet = CommonActionsPanel.getCommonShortcut(CommonActionsPanel.Buttons.ADD);
Shortcut shortcut = ArrayUtil.getFirstElement(shortcutSet.getShortcuts());
if (shortcut != null) {
emptyText.appendSecondaryText(" (" + KeymapUtil.getShortcutText(shortcut) + ")", StatusText.DEFAULT_ATTRIBUTES, null);
}
}
return emptyText;
}
};
final ToolbarDecorator toolbarDecorator = ToolbarDecorator.createDecorator(myTable).disableUpDownActions()
.setAddAction(b -> chooseAnnotation(name))
.setRemoveAction(new AnActionButtonRunnable() {
@Override
public void run(AnActionButton anActionButton) {
String selectedValue = getSelectedAnnotation();
if (selectedValue == null) return;
if (myDefaultAnnotation.equals(selectedValue)) myDefaultAnnotation = (String)myTable.getValueAt(0, 0);
myTableModel.removeRow(myTable.getSelectedRow());
}
})
.setRemoveActionUpdater(e -> !myDefaultAnnotations.contains(getSelectedAnnotation()));
final JPanel panel = toolbarDecorator.createPanel();
myComponent = new JPanel(new BorderLayout());
myComponent.setBorder(IdeBorderFactory.createEmptyBorder(JBUI.insetsTop(10)));
myComponent.add(new JLabel(name), BorderLayout.NORTH);
myComponent.add(panel, BorderLayout.CENTER);
myComponent.setPreferredSize(new JBDimension(myComponent.getPreferredSize().width, 200));
myTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
myTable.setRowSelectionAllowed(true);
myTable.setShowGrid(false);
selectAnnotation(myDefaultAnnotation);
}
private void addRow(String annotation) {
myTableModel.addRow(annotation);
}
private Integer selectAnnotation(String annotation) {
for (int i = 0; i < myTable.getRowCount(); i++) {
if (annotation.equals(myTable.getValueAt(i, 0))) {
myTable.setRowSelectionInterval(i, i);
return i;
}
}
return null;
}
private String getSelectedAnnotation() {
int selectedRow = myTable.getSelectedRow();
return selectedRow < 0 ? null : (String)myTable.getValueAt(selectedRow, 0);
}
private void chooseAnnotation(String title) {
final TreeClassChooser chooser = TreeClassChooserFactory.getInstance(myProject)
.createNoInnerClassesScopeChooser("Choose " + title, GlobalSearchScope.allScope(myProject), new ClassFilter() {
@Override
public boolean isAccepted(PsiClass aClass) {
return aClass.isAnnotationType();
}
}, null);
chooser.showDialog();
final PsiClass selected = chooser.getSelected();
if (selected == null) {
return;
}
final String qualifiedName = selected.getQualifiedName();
if (selectAnnotation(qualifiedName) == null) {
addRow(qualifiedName);
}
}
public JComponent getComponent() {
return myComponent;
}
public String[] getAnnotations() {
int size = myTable.getRowCount();
String[] result = new String[size];
for (int i = 0; i < size; i++) {
result[i] = (String)myTable.getValueAt(i, 0);
}
return result;
}
}

View File

@@ -0,0 +1,15 @@
// Copyright 2000-2018 JetBrains s.r.o. 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.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiMethod;
import org.jetbrains.annotations.NotNull;
public interface BlockingMethodChecker {
ExtensionPointName<BlockingMethodChecker> EP_NAME = ExtensionPointName.create("com.intellij.blockingMethodChecker");
boolean isActive(Project project);
boolean isMethodBlocking(@NotNull PsiMethod method);
}

View File

@@ -0,0 +1,176 @@
// Copyright 2000-2018 JetBrains s.r.o. 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.analysis.JvmAnalysisBundle;
import com.intellij.codeInspection.AbstractBaseUastLocalInspectionTool;
import com.intellij.codeInspection.AnalysisUastUtil;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.IdeFrame;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiModificationTracker;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.uast.UCallExpression;
import javax.swing.*;
import java.awt.*;
import java.util.List;
public class BlockingMethodInNonBlockingContextInspection extends AbstractBaseUastLocalInspectionTool {
public static final String DEFAULT_BLOCKING_ANNOTATION = "org.jetbrains.annotations.Blocking";
public static final String DEFAULT_NONBLOCKING_ANNOTATION = "org.jetbrains.annotations.NonBlocking";
public List<String> myBlockingAnnotations = new SmartList<>();
public List<String> myNonblockingAnnotations = new SmartList<>();
@Nullable
@Override
public JComponent createOptionsPanel() {
return new OptionsPanel();
}
@Nls
@NotNull
@Override
public String getDisplayName() {
return JvmAnalysisBundle.message("method.name.contains.blocking.word.display.name");
}
@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
List<BlockingMethodChecker> blockingMethodCheckers =
ContainerUtil.append(BlockingMethodChecker.EP_NAME.getExtensionList(),
new DefaultBlockingMethodChecker(myBlockingAnnotations));
List<NonblockingContextChecker> nonblockingContextCheckers =
ContainerUtil.append(NonblockingContextChecker.EP_NAME.getExtensionList(),
new DefaultNonblockingContextChecker(myNonblockingAnnotations));
if (!isInspectionActive(holder.getProject(), blockingMethodCheckers, nonblockingContextCheckers)) {
return PsiElementVisitor.EMPTY_VISITOR;
}
return new BlockingMethodInNonBlockingContextVisitor(holder, blockingMethodCheckers, nonblockingContextCheckers);
}
private static boolean isInspectionActive(Project project,
List<BlockingMethodChecker> myBlockingMethodCheckers,
List<NonblockingContextChecker> myNonblockingContextCheckers) {
return myBlockingMethodCheckers.stream().anyMatch(extension -> extension.isActive(project)) &&
myNonblockingContextCheckers.stream().anyMatch(extension -> extension.isActive(project));
}
private class OptionsPanel extends JPanel {
private OptionsPanel() {
super(new BorderLayout());
final Splitter mainPanel = new Splitter(true);
Project project = getCurrentProjectOrDefault(this);
BlockingAnnotationsPanel blockingAnnotationsPanel =
new BlockingAnnotationsPanel(
project,
JvmAnalysisBundle
.message("inspection.blocking.method.annotation.blocking"),
DEFAULT_BLOCKING_ANNOTATION,
myBlockingAnnotations,
new String[]{DEFAULT_BLOCKING_ANNOTATION},
JvmAnalysisBundle.message("inspection.blocking.method.annotation.configure.empty.text"),
JvmAnalysisBundle.message("inspection.blocking.method.annotation.configure.add.blocking.title"));
BlockingAnnotationsPanel nonblockingAnnotationsPanel =
new BlockingAnnotationsPanel(
project,
JvmAnalysisBundle.message(
"inspection.blocking.method.annotation.nonblocking"),
DEFAULT_NONBLOCKING_ANNOTATION,
myNonblockingAnnotations,
new String[]{DEFAULT_NONBLOCKING_ANNOTATION},
JvmAnalysisBundle.message("inspection.blocking.method.annotation.configure.add.nonblocking.title"),
JvmAnalysisBundle.message("inspection.blocking.method.annotation.configure.add.nonblocking.title"));
mainPanel.setFirstComponent(blockingAnnotationsPanel.getComponent());
mainPanel.setSecondComponent(nonblockingAnnotationsPanel.getComponent());
add(mainPanel, BorderLayout.CENTER);
}
}
@NotNull
private static Project getCurrentProjectOrDefault(Component context) {
Project project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(context));
if (project == null) {
IdeFrame lastFocusedFrame = IdeFocusManager.getGlobalInstance().getLastFocusedFrame();
if (lastFocusedFrame != null) project = lastFocusedFrame.getProject();
if (project == null) project = ProjectManager.getInstance().getDefaultProject();
}
return project;
}
private static class BlockingMethodInNonBlockingContextVisitor extends PsiElementVisitor {
private final ProblemsHolder myHolder;
private final List<BlockingMethodChecker> myBlockingMethodCheckers;
private final List<NonblockingContextChecker> myNonblockingContextCheckers;
public BlockingMethodInNonBlockingContextVisitor(@NotNull ProblemsHolder holder,
List<BlockingMethodChecker> blockingMethodCheckers,
List<NonblockingContextChecker> nonblockingContextCheckers) {
myHolder = holder;
this.myBlockingMethodCheckers = blockingMethodCheckers;
this.myNonblockingContextCheckers = nonblockingContextCheckers;
}
@Override
public void visitElement(PsiElement element) {
UCallExpression callExpression = AnalysisUastUtil.getUCallExpression(element);
if (callExpression == null) return;
if (!isContextNonBlockingFor(element)) return;
PsiMethod referencedMethod = callExpression.resolve();
if (referencedMethod == null) return;
boolean isReferencedMethodBlocking = CachedValuesManager.getCachedValue(referencedMethod, () -> {
boolean isBlocking =
StreamEx.of(referencedMethod).append(referencedMethod.findDeepestSuperMethods()).anyMatch(method -> isMethodBlocking(method));
return CachedValueProvider.Result.create(isBlocking, PsiModificationTracker.MODIFICATION_COUNT);
});
if (!isReferencedMethodBlocking) return;
PsiElement elementToHighLight = AnalysisUastUtil.getMethodIdentifierSourcePsi(callExpression);
if (elementToHighLight == null) return;
myHolder.registerProblem(elementToHighLight,
JvmAnalysisBundle.message("method.name.contains.blocking.word.problem.descriptor"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
}
private boolean isContextNonBlockingFor(PsiElement element) {
return myNonblockingContextCheckers.stream().anyMatch(extension -> extension.isContextNonBlockingFor(element));
}
private boolean isMethodBlocking(PsiMethod method) {
return myBlockingMethodCheckers.stream().anyMatch(extension -> extension.isMethodBlocking(method));
}
}
}

View File

@@ -0,0 +1,36 @@
// Copyright 2000-2018 JetBrains s.r.o. 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.codeInsight.AnnotationUtil;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiMethod;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
public class DefaultBlockingMethodChecker implements BlockingMethodChecker {
private final List<String> myBlockingAnnotations;
public DefaultBlockingMethodChecker(List<String> blockingAnnotations) {
myBlockingAnnotations = blockingAnnotations;
}
@Override
public boolean isActive(Project project) {
return !myBlockingAnnotations.isEmpty();
}
@Override
public boolean isMethodBlocking(@NotNull PsiMethod method) {
HashSet<String> setOfAnnotations = Arrays.stream(AnnotationUtil.getAllAnnotations(method, true, null))
.map(PsiAnnotation::getQualifiedName).collect(Collectors.toCollection(HashSet::new));
return myBlockingAnnotations.stream()
.anyMatch(annotation -> setOfAnnotations.contains(annotation));
}
}

View File

@@ -0,0 +1,54 @@
// Copyright 2000-2018 JetBrains s.r.o. 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.codeInsight.AnnotationUtil;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.search.ProjectScope;
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.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
public class DefaultNonblockingContextChecker implements NonblockingContextChecker {
private final List<String> myNonblockingAnnotations;
public DefaultNonblockingContextChecker(List<String> nonblockingAnnotations) {
myNonblockingAnnotations = nonblockingAnnotations;
}
@Override
public boolean isActive(Project project) {
if (myNonblockingAnnotations.isEmpty()) return false;
PsiClass annotationClass = JavaPsiFacade.getInstance(project)
.findClass(BlockingMethodInNonBlockingContextInspection.DEFAULT_NONBLOCKING_ANNOTATION, ProjectScope.getProjectScope(project));
return annotationClass != null;
}
@Override
public boolean isContextNonBlockingFor(@NotNull PsiElement element) {
UCallExpression callExpression = UastContextKt.toUElement(element, UCallExpression.class);
if (callExpression == null) return false;
UMethod callingMethod = UastUtils.getParentOfType(callExpression, UMethod.class);
if (callingMethod == null) return false;
PsiMethod psiCallingMethod = callingMethod.getJavaPsi();
return hasNonblockingAnnotation(psiCallingMethod);
}
private boolean hasNonblockingAnnotation(PsiMethod method) {
HashSet<String> setOfAnnotations = Arrays.stream(AnnotationUtil.getAllAnnotations(method, true, null))
.map(PsiAnnotation::getQualifiedName).collect(Collectors.toCollection(HashSet::new));
return myNonblockingAnnotations.stream()
.anyMatch(annotation -> setOfAnnotations.contains(annotation));
}
}

View File

@@ -0,0 +1,15 @@
// Copyright 2000-2018 JetBrains s.r.o. 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.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;
public interface NonblockingContextChecker {
ExtensionPointName<NonblockingContextChecker> EP_NAME = ExtensionPointName.create("com.intellij.nonblockingMethodChecker");
boolean isActive(Project project);
boolean isContextNonBlockingFor(@NotNull PsiElement element);
}

View File

@@ -0,0 +1,26 @@
// Copyright 2000-2018 JetBrains s.r.o. 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.openapi.project.Project;
import com.intellij.psi.PsiMethod;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.HashSet;
public class ThrowsTypeBlockingMethodChecker implements BlockingMethodChecker {
private static final HashSet<String> BLOCKING_EXCEPTION_TYPES = new HashSet<>(Arrays.asList(
"java.lang.InterruptedException",
"java.io.IOException"));
@Override
public boolean isActive(Project project) {
return !BLOCKING_EXCEPTION_TYPES.isEmpty();
}
@Override
public boolean isMethodBlocking(@NotNull PsiMethod method) {
return Arrays.stream(method.getThrowsList().getReferenceElements())
.anyMatch(thrownException -> BLOCKING_EXCEPTION_TYPES.contains(thrownException.getQualifiedName()));
}
}

View File

@@ -0,0 +1,80 @@
// Copyright 2000-2018 JetBrains s.r.o. 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;
import com.intellij.codeInspection.blockingCallsDetection.BlockingMethodInNonBlockingContextInspection;
import com.intellij.openapi.application.ex.PathManagerEx;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.JavaModuleExternalPaths;
import com.intellij.openapi.roots.ModuleRootModificationUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.psi.codeStyle.JavaCodeStyleSettings;
import com.intellij.testFramework.UsefulTestCase;
import com.intellij.testFramework.builders.JavaModuleFixtureBuilder;
import com.intellij.testFramework.fixtures.*;
import java.util.Collections;
public class BlockingMethodInNonBlockingContextInspectionTest extends UsefulTestCase {
private CodeInsightTestFixture myFixture;
@Override
protected void tearDown() throws Exception {
try {
myFixture.tearDown();
}
finally {
myFixture = null;
super.tearDown();
}
}
@Override
public void setUp() throws Exception {
super.setUp();
final TestFixtureBuilder<IdeaProjectTestFixture>
projectBuilder = IdeaTestFixtureFactory.getFixtureFactory().createFixtureBuilder(getName());
myFixture = JavaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(projectBuilder.getFixture());
final String dataPath = PathManagerEx.getTestDataPath() + "/codeInspection/blockingCallsDetection";
myFixture.setTestDataPath(dataPath);
final JavaModuleFixtureBuilder builder = projectBuilder.addModule(JavaModuleFixtureBuilder.class);
builder.setMockJdkLevel(JavaModuleFixtureBuilder.MockJdkLevel.jdk15);
myFixture.setUp();
Module module = builder.getFixture().getModule();
ModuleRootModificationUtil.updateModel(module, model -> {
String contentUrl = VfsUtilCore.pathToUrl(myFixture.getTempDirPath());
model.addContentEntry(contentUrl).addSourceFolder(contentUrl, false);
final JavaModuleExternalPaths extension = model.getModuleExtension(JavaModuleExternalPaths.class);
extension.setExternalAnnotationUrls(new String[]{VfsUtilCore.pathToUrl(myFixture.getTempDirPath())});
});
Project project = myFixture.getProject();
JavaCodeStyleSettings.getInstance(project).USE_EXTERNAL_ANNOTATIONS = true;
BlockingMethodInNonBlockingContextInspection myInspection = new BlockingMethodInNonBlockingContextInspection();
myInspection.myBlockingAnnotations =
Collections.singletonList(BlockingMethodInNonBlockingContextInspection.DEFAULT_BLOCKING_ANNOTATION);
myInspection.myNonblockingAnnotations =
Collections.singletonList(BlockingMethodInNonBlockingContextInspection.DEFAULT_NONBLOCKING_ANNOTATION);
myFixture.enableInspections(myInspection);
}
public void testSimpleAnnotationDetection() {
myFixture.configureByFiles("TestSimpleAnnotationsDetection.java", "Blocking.java", "NonBlocking.java");
myFixture.testHighlighting(true, false, true, "TestSimpleAnnotationsDetection.java");
}
public void testExternalBlockingAnnotationDetection() {
myFixture.configureByFiles("TestExternalAnnotationsDetection.java", "Blocking.java", "NonBlocking.java", "annotations.xml");
myFixture.testHighlighting(true, false, true, "TestExternalAnnotationsDetection.java");
}
public void testThrowsTypeDetection() {
myFixture.configureByFiles("TestThrowsTypeDetection.java", "Blocking.java", "NonBlocking.java");
myFixture.testHighlighting(true, false, true, "TestThrowsTypeDetection.java");
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
Reports thread-blocking method calls found in a code fragment where a thread should not be blocked
<!-- tooltip end -->
(e.g. Reactive frameworks, Kotlin coroutines)
</body>
</html>