mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-19 13:02:30 +07:00
Blocking calls detection inspection created (IDEA-173248)
This commit is contained in:
committed by
Nicolay Mitropolsky
parent
2a877313ca
commit
fea31cf421
@@ -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"/>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<item name="TestExternalAnnotationsDetection void func()">
|
||||
<annotation name="org.jetbrains.annotations.NonBlocking"/>
|
||||
</item>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user