Java: Show gutter icon for usages and implementations of services declared in module-info (IDEA-179178)

This commit is contained in:
Pavel Dolgov
2017-12-14 13:56:36 +03:00
parent 2b8f909eee
commit 92f9f61375
3 changed files with 306 additions and 1 deletions
@@ -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());
}
}
}
@@ -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>