mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-10-01 15:15:45 +07:00
Java: Show gutter icon for usages and implementations of services declared in module-info (IDEA-179178)
This commit is contained in:
+187
-1
@@ -17,8 +17,10 @@ package com.intellij.codeInsight.daemon.impl;
|
||||
|
||||
import com.intellij.codeHighlighting.Pass;
|
||||
import com.intellij.codeInsight.daemon.*;
|
||||
import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil;
|
||||
import com.intellij.concurrency.JobLauncher;
|
||||
import com.intellij.icons.AllIcons;
|
||||
import com.intellij.navigation.NavigationItem;
|
||||
import com.intellij.openapi.actionSystem.IdeActions;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.editor.Document;
|
||||
@@ -35,22 +37,28 @@ import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.*;
|
||||
import com.intellij.psi.impl.FindSuperElementsHelper;
|
||||
import com.intellij.psi.impl.source.resolve.reference.impl.JavaReflectionReferenceUtil;
|
||||
import com.intellij.psi.search.searches.AllOverridingMethodsSearch;
|
||||
import com.intellij.psi.search.searches.DirectClassInheritorsSearch;
|
||||
import com.intellij.psi.search.searches.FunctionalExpressionSearch;
|
||||
import com.intellij.psi.search.searches.SuperMethodsSearch;
|
||||
import com.intellij.psi.util.InheritanceUtil;
|
||||
import com.intellij.psi.util.MethodSignatureBackedByPsiMethod;
|
||||
import com.intellij.psi.util.PsiExpressionTrimRenderer;
|
||||
import com.intellij.psi.util.PsiUtil;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.FunctionUtil;
|
||||
import com.intellij.util.ObjectUtils;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.containers.HashSet;
|
||||
import com.intellij.util.containers.MultiMap;
|
||||
import com.siyeh.ig.callMatcher.CallMatcher;
|
||||
import one.util.streamex.StreamEx;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.*;
|
||||
|
||||
public class JavaLineMarkerProvider extends LineMarkerProviderDescriptor {
|
||||
@@ -61,6 +69,9 @@ public class JavaLineMarkerProvider extends LineMarkerProviderDescriptor {
|
||||
private final Option myImplementedOption = new Option("java.implemented", "Implemented method", AllIcons.Gutter.ImplementedMethod);
|
||||
private final Option myOverridingOption = new Option("java.overriding", "Overriding method", AllIcons.Gutter.OverridingMethod);
|
||||
private final Option myImplementingOption = new Option("java.implementing", "Implementing method", AllIcons.Gutter.ImplementingMethod);
|
||||
private final Option myServiceOption = new Option("java.service", "Service", AllIcons.Gutter.Java9Service);
|
||||
|
||||
private static final CallMatcher SERVICE_LOADER_LOAD = CallMatcher.staticCall("java.util.ServiceLoader", "load", "loadInstalled");
|
||||
|
||||
public JavaLineMarkerProvider(DaemonCodeAnalyzerSettings daemonSettings, EditorColorsManager colorsManager) {
|
||||
myDaemonSettings = daemonSettings;
|
||||
@@ -187,9 +198,19 @@ public class JavaLineMarkerProvider extends LineMarkerProviderDescriptor {
|
||||
if (PsiUtil.canBeOverridden(method) && psiClass != null) {
|
||||
byClass.putValue(psiClass, method);
|
||||
}
|
||||
if (isServiceProviderMethod(method)) {
|
||||
tasks.add(() -> collectServiceProviderMethod(method));
|
||||
}
|
||||
}
|
||||
else if (parent instanceof PsiClass && !(parent instanceof PsiTypeParameter)) {
|
||||
tasks.add(() -> collectInheritingClasses((PsiClass)parent));
|
||||
tasks.add(() -> collectServiceImplementationClass((PsiClass)parent));
|
||||
}
|
||||
else if (parent instanceof PsiReferenceExpression && parent.getParent() instanceof PsiMethodCallExpression) {
|
||||
PsiMethodCallExpression grandParent = (PsiMethodCallExpression)parent.getParent();
|
||||
if (SERVICE_LOADER_LOAD.test(grandParent)) {
|
||||
tasks.add(() -> collectServiceLoaderLoadCall((PsiIdentifier)element, grandParent));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (PsiClass psiClass : byClass.keySet()) {
|
||||
@@ -328,9 +349,104 @@ public class JavaLineMarkerProvider extends LineMarkerProviderDescriptor {
|
||||
@NotNull
|
||||
@Override
|
||||
public Option[] getOptions() {
|
||||
return new Option[] {myLambdaOption, myOverriddenOption, myImplementedOption, myOverridingOption, myImplementingOption};
|
||||
return new Option[]{myLambdaOption, myOverriddenOption, myImplementedOption, myOverridingOption, myImplementingOption, myServiceOption};
|
||||
}
|
||||
|
||||
private static boolean isServiceProviderMethod(@NotNull PsiMethod method) {
|
||||
return "provider".equals(method.getName()) &&
|
||||
method.getParameterList().getParametersCount() == 0 &&
|
||||
method.hasModifierProperty(PsiModifier.PUBLIC) &&
|
||||
method.hasModifierProperty(PsiModifier.STATIC);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<LineMarkerInfo> collectServiceProviderMethod(@NotNull PsiMethod method) {
|
||||
PsiClass containingClass = method.getContainingClass();
|
||||
PsiClass resultClass = PsiUtil.resolveClassInType(method.getReturnType());
|
||||
return createJavaServiceLineMarkerInfo(method.getNameIdentifier(), containingClass, resultClass);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<LineMarkerInfo> collectServiceImplementationClass(@NotNull PsiClass psiClass) {
|
||||
return createJavaServiceLineMarkerInfo(psiClass.getNameIdentifier(), psiClass, psiClass);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<LineMarkerInfo> createJavaServiceLineMarkerInfo(@Nullable PsiIdentifier identifier,
|
||||
@Nullable PsiClass implementerClass,
|
||||
@Nullable PsiClass resultClass) {
|
||||
if (identifier != null && implementerClass != null && resultClass != null) {
|
||||
String implementerClassName = implementerClass.getQualifiedName();
|
||||
if (implementerClassName != null && PsiUtil.isLanguageLevel9OrHigher(identifier)) {
|
||||
PsiJavaModule javaModule = JavaModuleGraphUtil.findDescriptorByElement(identifier);
|
||||
if (javaModule != null) {
|
||||
Iterable<PsiProvidesStatement> provides = javaModule.getProvides();
|
||||
for (PsiProvidesStatement providesStatement : provides) {
|
||||
PsiJavaCodeReferenceElement interfaceReference = providesStatement.getInterfaceReference();
|
||||
PsiReferenceList implementationList = providesStatement.getImplementationList();
|
||||
if (interfaceReference != null && implementationList != null) {
|
||||
PsiReference[] implementationReferences = implementationList.getReferenceElements();
|
||||
for (PsiReference implementationReference : implementationReferences) {
|
||||
if (implementationReference.isReferenceTo(implementerClass)) {
|
||||
PsiClass interfaceClass = ObjectUtils.tryCast(interfaceReference.resolve(), PsiClass.class);
|
||||
if (InheritanceUtil.isInheritorOrSelf(resultClass, interfaceClass, true)) {
|
||||
String interfaceClassName = interfaceClass.getQualifiedName();
|
||||
if (interfaceClassName != null) {
|
||||
LineMarkerInfo<PsiElement> info =
|
||||
new LineMarkerInfo<>(identifier, identifier.getTextRange(), AllIcons.Gutter.Java9Service, Pass.LINE_MARKERS,
|
||||
e -> DaemonBundle.message("service.provides", interfaceClassName),
|
||||
new ServiceProvidesNavigationHandler(interfaceClassName, implementerClassName),
|
||||
GutterIconRenderer.Alignment.LEFT);
|
||||
return Collections.singletonList(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private static List<LineMarkerInfo> collectServiceLoaderLoadCall(@NotNull PsiIdentifier identifier,
|
||||
@NotNull PsiMethodCallExpression methodCall) {
|
||||
if (PsiUtil.isLanguageLevel9OrHigher(methodCall)) {
|
||||
PsiExpression[] arguments = methodCall.getArgumentList().getExpressions();
|
||||
|
||||
JavaReflectionReferenceUtil.ReflectiveType serviceType = null;
|
||||
for (int i = 0; i < arguments.length && serviceType == null; i++) {
|
||||
serviceType = JavaReflectionReferenceUtil.getReflectiveType(arguments[i]);
|
||||
}
|
||||
|
||||
if (serviceType != null && serviceType.isExact()) {
|
||||
PsiClass psiClass = serviceType.getPsiClass();
|
||||
if (psiClass != null) {
|
||||
String qualifiedName = psiClass.getQualifiedName();
|
||||
if (qualifiedName != null) {
|
||||
PsiJavaModule javaModule = JavaModuleGraphUtil.findDescriptorByElement(methodCall);
|
||||
if (javaModule != null) {
|
||||
for (PsiUsesStatement statement : javaModule.getUses()) {
|
||||
PsiJavaCodeReferenceElement reference = statement.getClassReference();
|
||||
if (reference != null && reference.isReferenceTo(psiClass)) {
|
||||
LineMarkerInfo<PsiElement> info =
|
||||
new LineMarkerInfo<>(identifier, identifier.getTextRange(), AllIcons.Gutter.Java9Service, Pass.LINE_MARKERS,
|
||||
e -> DaemonBundle.message("service.uses", qualifiedName),
|
||||
new ServiceUsesNavigationHandler(qualifiedName),
|
||||
GutterIconRenderer.Alignment.LEFT);
|
||||
return Collections.singletonList(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
private static class ArrowUpLineMarkerInfo extends MergeableLineMarkerInfo<PsiElement> {
|
||||
private ArrowUpLineMarkerInfo(@NotNull PsiElement element, @NotNull Icon icon, @NotNull MarkerType markerType, int passId) {
|
||||
super(element, element.getTextRange(), icon, passId, markerType.getTooltip(),
|
||||
@@ -366,4 +482,74 @@ public class JavaLineMarkerProvider extends LineMarkerProviderDescriptor {
|
||||
return super.getElementPresentation(element);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static abstract class ServiceNavigationHandler implements GutterIconNavigationHandler<PsiElement> {
|
||||
protected final String myInterfaceClassName;
|
||||
|
||||
protected ServiceNavigationHandler(@NotNull String interfaceClassName) {myInterfaceClassName = interfaceClassName;}
|
||||
|
||||
@Override
|
||||
public void navigate(MouseEvent e, PsiElement element) {
|
||||
Optional.ofNullable(JavaModuleGraphUtil.findDescriptorByElement(element))
|
||||
.map(this::findTargetReference)
|
||||
.filter(NavigationItem.class::isInstance)
|
||||
.map(NavigationItem.class::cast)
|
||||
.ifPresent(item -> item.navigate(true));
|
||||
}
|
||||
|
||||
public abstract PsiJavaCodeReferenceElement findTargetReference(@NotNull PsiJavaModule module);
|
||||
|
||||
@NotNull
|
||||
protected String getTargetFQN() {
|
||||
return myInterfaceClassName;
|
||||
}
|
||||
|
||||
protected boolean isTargetReference(PsiJavaCodeReferenceElement reference) {
|
||||
return reference != null && getTargetFQN().equals(reference.getQualifiedName());
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServiceUsesNavigationHandler extends ServiceNavigationHandler {
|
||||
public ServiceUsesNavigationHandler(String interfaceClassName) {
|
||||
super(interfaceClassName);
|
||||
}
|
||||
|
||||
public PsiJavaCodeReferenceElement findTargetReference(@NotNull PsiJavaModule module) {
|
||||
return StreamEx.of(module.getUses().iterator())
|
||||
.map(PsiUsesStatement::getClassReference)
|
||||
.findAny(this::isTargetReference)
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServiceProvidesNavigationHandler extends ServiceNavigationHandler {
|
||||
private final String myImplementerClassName;
|
||||
|
||||
public ServiceProvidesNavigationHandler(@NotNull String interfaceClassName, @NotNull String implementerClassName) {
|
||||
super(interfaceClassName);
|
||||
myImplementerClassName = implementerClassName;
|
||||
}
|
||||
|
||||
public PsiJavaCodeReferenceElement findTargetReference(@NotNull PsiJavaModule module) {
|
||||
PsiJavaCodeReferenceElement[] references =
|
||||
StreamEx.of(module.getProvides().iterator())
|
||||
.findAny(this::isTargetStatement)
|
||||
.map(PsiProvidesStatement::getImplementationList)
|
||||
.map(PsiReferenceList::getReferenceElements)
|
||||
.orElse(PsiJavaCodeReferenceElement.EMPTY_ARRAY);
|
||||
|
||||
return ContainerUtil.find(references, this::isTargetReference);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected String getTargetFQN() {
|
||||
return myImplementerClassName;
|
||||
}
|
||||
|
||||
private boolean isTargetStatement(@NotNull PsiProvidesStatement statement) {
|
||||
PsiJavaCodeReferenceElement reference = statement.getInterfaceReference();
|
||||
return reference != null && myInterfaceClassName.equals(reference.getQualifiedName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2000-2017 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.java.codeInsight.daemon.impl
|
||||
|
||||
import com.intellij.codeInsight.daemon.DaemonBundle
|
||||
import com.intellij.codeInsight.daemon.LineMarkerInfo
|
||||
import com.intellij.codeInsight.daemon.impl.JavaLineMarkerProvider.ServiceNavigationHandler
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.psi.*
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.intellij.testFramework.LightProjectDescriptor
|
||||
import com.intellij.testFramework.UsefulTestCase
|
||||
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
|
||||
|
||||
/**
|
||||
* @author Pavel.Dolgov
|
||||
*/
|
||||
class ServiceLineMarkerTest : LightCodeInsightFixtureTestCase() {
|
||||
override fun getProjectDescriptor(): LightProjectDescriptor = JAVA_9
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
|
||||
myFixture.addFileToProject("foo/bar/MyService.java",
|
||||
"package foo.bar; public class MyService { void doWork(); }")
|
||||
}
|
||||
|
||||
fun testProvidesAsSubclass() =
|
||||
doTestImplementer("public class <caret>MyServiceImpl implements MyService {\n" +
|
||||
" @Override public void doWork() {}\n" +
|
||||
"}")
|
||||
|
||||
fun testProvidesWithMethod() =
|
||||
doTestImplementer("public class MyServiceImpl {\n" +
|
||||
"public static MyService <caret>provider() {\n" +
|
||||
" return new MyService() { @Override public void doWork() {} };\n" +
|
||||
"}")
|
||||
|
||||
fun testLoadWithLiteral() =
|
||||
doTestLoader("void foo() {\n" +
|
||||
" ServiceLoader<MyService> loader = <caret>ServiceLoader.load(MyService.class);\n" +
|
||||
"}")
|
||||
|
||||
fun testLoadWithVariable() =
|
||||
doTestLoader("void foo() {\n" +
|
||||
" Class<MyService> service = MyService.class;\n" +
|
||||
" ServiceLoader<MyService> loader = <caret>ServiceLoader.load(service);\n" +
|
||||
"}")
|
||||
|
||||
fun testLoadWithClassForName() =
|
||||
doTestLoader("void foo() throws ClassNotFoundException {\n" +
|
||||
" ServiceLoader<MyService> loader = \n" +
|
||||
" <caret>ServiceLoader.load(Class.forName(\"foo.bar.MyService\"), Main.class.getClassLoader());\n" +
|
||||
"}")
|
||||
|
||||
fun testLoadWithConstant() =
|
||||
doTestLoader("static final Class<MyService> SERVICE = MyService.class;\n" +
|
||||
"void foo() {\n" +
|
||||
" ServiceLoader<MyService> loader = <caret>ServiceLoader.load(SERVICE);\n" +
|
||||
"}")
|
||||
|
||||
private fun doTestLoader(text: String) {
|
||||
val module = addModule("module foo.bar { uses foo.bar.MyService; provides foo.bar.MyService with foo.bar.impl.MyServiceImpl; }")
|
||||
|
||||
addImplementer("public class <caret>MyServiceImpl implements MyService {\n" +
|
||||
" @Override public void doWork() {}\n" +
|
||||
"}")
|
||||
|
||||
val file = addMain(text)
|
||||
doTest(file, module, DaemonBundle.message("service.uses", "foo.bar.MyService"),
|
||||
"foo.bar.MyService", PsiUsesStatement::class.java)
|
||||
}
|
||||
|
||||
private fun doTestImplementer(text: String) {
|
||||
val module = addModule("module foo.bar { provides foo.bar.MyService with foo.bar.impl.MyServiceImpl; }")
|
||||
|
||||
val file = addImplementer(text)
|
||||
doTest(file, module, DaemonBundle.message("service.provides", "foo.bar.MyService"),
|
||||
"foo.bar.impl.MyServiceImpl", PsiProvidesStatement::class.java)
|
||||
}
|
||||
|
||||
private fun doTest(file: PsiFile, module: PsiJavaModule, message: String, fqn: String, parentType: Class<out PsiElement>) {
|
||||
myFixture.configureFromExistingVirtualFile(file.virtualFile!!)
|
||||
|
||||
val atCaret = myFixture.findGuttersAtCaret().filter { it.icon === AllIcons.Gutter.Java9Service }
|
||||
assertEquals("atCaret", 1, atCaret.size)
|
||||
val all = myFixture.findAllGutters().filter { it.icon === AllIcons.Gutter.Java9Service }
|
||||
assertEquals("all", atCaret, all)
|
||||
|
||||
val mark = atCaret[0]
|
||||
assertEquals(message, mark.tooltipText)
|
||||
|
||||
val handler = (mark as LineMarkerInfo.LineMarkerGutterIconRenderer<*>).lineMarkerInfo.navigationHandler as ServiceNavigationHandler
|
||||
val targetReference = handler.findTargetReference(module)
|
||||
assertNotNull("targetReference", targetReference)
|
||||
|
||||
val parent = PsiTreeUtil.getParentOfType(targetReference, parentType)
|
||||
UsefulTestCase.assertInstanceOf(parent, parentType)
|
||||
|
||||
assertTrue("isAncestor", PsiTreeUtil.isAncestor(module, targetReference, true))
|
||||
assertEquals(fqn, targetReference.qualifiedName)
|
||||
}
|
||||
|
||||
private fun addModule(text: String): PsiJavaModule =
|
||||
(myFixture.addFileToProject("module-info.java", text) as PsiJavaFile).moduleDeclaration!!
|
||||
|
||||
private fun addImplementer(text: String) = myFixture.addFileToProject("foo/bar/impl/MyServiceImpl.java",
|
||||
"package foo.bar.impl; import foo.bar.MyService; $text")
|
||||
|
||||
private fun addMain(method: String) = myFixture.addFileToProject("foo/bar/main/Main.java",
|
||||
"package foo.bar.main;\n" +
|
||||
"import foo.bar.MyService;\n" +
|
||||
"import java.util.ServiceLoader;\n" +
|
||||
"public class Main { $method }")
|
||||
}
|
||||
@@ -47,3 +47,6 @@ inspection.collapse.description=less...
|
||||
|
||||
import.popup.text={0}?
|
||||
import.popup.multiple={0}? (multiple choices...)
|
||||
|
||||
service.provides=Provides service <a href="#javaClass/{0}">{0}</a><br><div style='margin-top: 5px'><font size='2'>Click to navigate</font></div>
|
||||
service.uses=Uses service <a href="#javaClass/{0}">{0}</a><br><div style='margin-top: 5px'><font size='2'>Click to navigate</font></div>
|
||||
Reference in New Issue
Block a user