diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/JavaLineMarkerProvider.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/JavaLineMarkerProvider.java index 40148d623f34..3d5a0c614961 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/JavaLineMarkerProvider.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/JavaLineMarkerProvider.java @@ -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 collectServiceProviderMethod(@NotNull PsiMethod method) { + PsiClass containingClass = method.getContainingClass(); + PsiClass resultClass = PsiUtil.resolveClassInType(method.getReturnType()); + return createJavaServiceLineMarkerInfo(method.getNameIdentifier(), containingClass, resultClass); + } + + @NotNull + private static List collectServiceImplementationClass(@NotNull PsiClass psiClass) { + return createJavaServiceLineMarkerInfo(psiClass.getNameIdentifier(), psiClass, psiClass); + } + + @NotNull + private static List 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 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 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 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 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 { 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 { + 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()); + } + } } diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/impl/ServiceLineMarkerTest.kt b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/impl/ServiceLineMarkerTest.kt new file mode 100644 index 000000000000..054de4450f84 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/impl/ServiceLineMarkerTest.kt @@ -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 MyServiceImpl implements MyService {\n" + + " @Override public void doWork() {}\n" + + "}") + + fun testProvidesWithMethod() = + doTestImplementer("public class MyServiceImpl {\n" + + "public static MyService provider() {\n" + + " return new MyService() { @Override public void doWork() {} };\n" + + "}") + + fun testLoadWithLiteral() = + doTestLoader("void foo() {\n" + + " ServiceLoader loader = ServiceLoader.load(MyService.class);\n" + + "}") + + fun testLoadWithVariable() = + doTestLoader("void foo() {\n" + + " Class service = MyService.class;\n" + + " ServiceLoader loader = ServiceLoader.load(service);\n" + + "}") + + fun testLoadWithClassForName() = + doTestLoader("void foo() throws ClassNotFoundException {\n" + + " ServiceLoader loader = \n" + + " ServiceLoader.load(Class.forName(\"foo.bar.MyService\"), Main.class.getClassLoader());\n" + + "}") + + fun testLoadWithConstant() = + doTestLoader("static final Class SERVICE = MyService.class;\n" + + "void foo() {\n" + + " ServiceLoader loader = 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 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) { + 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 }") +} \ No newline at end of file diff --git a/platform/platform-resources-en/src/messages/DaemonBundle.properties b/platform/platform-resources-en/src/messages/DaemonBundle.properties index 2b4b3458535e..36cc370a78ab 100644 --- a/platform/platform-resources-en/src/messages/DaemonBundle.properties +++ b/platform/platform-resources-en/src/messages/DaemonBundle.properties @@ -47,3 +47,6 @@ inspection.collapse.description=less... import.popup.text={0}? import.popup.multiple={0}? (multiple choices...) + +service.provides=Provides service {0}
Click to navigate
+service.uses=Uses service {0}
Click to navigate
\ No newline at end of file