From 0b6b6dff9cabdd9b1ed57b2b37a31ec74aa69f6c Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Tue, 4 Feb 2025 16:06:32 +0100 Subject: [PATCH] GH-1348: navigation for events improved GH-1461: hierarchy of events taken into account when looking for references from listeners, too GH-1462: event listener index nodes now not created twice for type and annotation anymore --- .../boot/index/SpringMetamodelIndex.java | 19 +++++++ .../AnnotationHierarchyAwareLookup.java | 5 +- .../java/beans/ComponentSymbolProvider.java | 56 +++++++++++-------- .../java/events/EventReferenceProvider.java | 44 ++++++--------- .../test/EventsReferencesProviderTest.java | 45 +++++++++++++++ .../events/test/SpringIndexerEventsTest.java | 17 +++++- .../events/demo/CustomApplicationEvent.java | 13 +++++ .../demo/CustomApplicationEventPublisher.java | 19 +++++++ ...ventListenerPerInterfaceAndBeanMethod.java | 5 +- 9 files changed, 165 insertions(+), 58 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/CustomApplicationEvent.java create mode 100644 headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/CustomApplicationEventPublisher.java diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/SpringMetamodelIndex.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/SpringMetamodelIndex.java index d2583f1a7f..d2cc36ea12 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/SpringMetamodelIndex.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/SpringMetamodelIndex.java @@ -96,6 +96,25 @@ public DocumentElement getDocument(String docURI) { return null; } + public List getNodesOfType(Class type) { + List result = new ArrayList<>(); + + ArrayDeque elementsToVisit = new ArrayDeque<>(); + elementsToVisit.addAll(this.projectRootElements.values()); + + while (!elementsToVisit.isEmpty()) { + SpringIndexElement element = elementsToVisit.pop(); + + if (type.isInstance(element)) { + result.add(type.cast(element)); + } + + elementsToVisit.addAll(element.getChildren()); + } + + return result; + } + public Bean[] getBeans() { List result = new ArrayList<>(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchyAwareLookup.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchyAwareLookup.java index 1fdb79b896..edd0e9b708 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchyAwareLookup.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchyAwareLookup.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2018 Pivotal, Inc. + * Copyright (c) 2017, 2025 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -23,6 +23,7 @@ import org.springframework.ide.vscode.commons.util.Assert; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; /** * A Map-like utilty that allows putting and getting values associated with @@ -94,7 +95,7 @@ public Collection get(AnnotationHierarchies annotationHierarchies, IAnnotatio } public Collection getAll() { - ImmutableList.Builder found = ImmutableList.builder(); + ImmutableSet.Builder found = ImmutableSet.builder(); Collection> values = bindings.values(); values.forEach(binding -> found.add(binding.value)); return found.build(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java index c1f18182cc..4a326c6080 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java @@ -113,31 +113,41 @@ protected void createSymbol(Annotation node, ITypeBinding annotationType, Collec Bean beanDefinition = new Bean(beanName, beanType.getQualifiedName(), location, injectionPoints, supertypes, annotations, isConfiguration); // event listener - create child element, if necessary - ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(type, doc, beanType, Set.of(Annotations.APPLICATION_LISTENER)); - if (inTypeHierarchy != null) { - - MethodDeclaration handleEventMethod = findHandleEventMethod(type); - if (handleEventMethod != null) { - - IMethodBinding methodBinding = handleEventMethod.resolveBinding(); - ITypeBinding[] parameterTypes = methodBinding.getParameterTypes(); - if (parameterTypes != null && parameterTypes.length == 1) { - - ITypeBinding eventType = parameterTypes[0]; - String eventTypeFq = eventType.getQualifiedName(); - - DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, handleEventMethod.getName()); - Location handleMethodLocation = new Location(doc.getUri(), nodeRegion.asRange()); - - Collection annotationsOnHandleEventMethod = ASTUtils.getAnnotations(handleEventMethod); - AnnotationMetadata[] handleEventMethodAnnotations = ASTUtils.getAnnotationsMetadata(annotationsOnHandleEventMethod, doc); - - EventListenerIndexElement eventElement = new EventListenerIndexElement(eventTypeFq, handleMethodLocation, beanType.getQualifiedName(), handleEventMethodAnnotations); - beanDefinition.addChild(eventElement); - } - } + List alreadyCreatedEventListenerChilds = context.getBeans().stream() + .filter(cachedBean -> cachedBean.getDocURI().equals(doc.getUri())) + .filter(cachedBean -> cachedBean.getBean() instanceof EventListenerIndexElement) + .toList(); + + for (CachedBean eventListener : alreadyCreatedEventListenerChilds) { + context.getBeans().remove(eventListener); + beanDefinition.addChild(eventListener.getBean()); } +// ITypeBinding inTypeHierarchy = ASTUtils.findInTypeHierarchy(type, doc, beanType, Set.of(Annotations.APPLICATION_LISTENER)); +// if (inTypeHierarchy != null) { +// +// MethodDeclaration handleEventMethod = findHandleEventMethod(type); +// if (handleEventMethod != null) { +// +// IMethodBinding methodBinding = handleEventMethod.resolveBinding(); +// ITypeBinding[] parameterTypes = methodBinding.getParameterTypes(); +// if (parameterTypes != null && parameterTypes.length == 1) { +// +// ITypeBinding eventType = parameterTypes[0]; +// String eventTypeFq = eventType.getQualifiedName(); +// +// DocumentRegion nodeRegion = ASTUtils.nodeRegion(doc, handleEventMethod.getName()); +// Location handleMethodLocation = new Location(doc.getUri(), nodeRegion.asRange()); +// +// Collection annotationsOnHandleEventMethod = ASTUtils.getAnnotations(handleEventMethod); +// AnnotationMetadata[] handleEventMethodAnnotations = ASTUtils.getAnnotationsMetadata(annotationsOnHandleEventMethod, doc); +// +// EventListenerIndexElement eventElement = new EventListenerIndexElement(eventTypeFq, handleMethodLocation, beanType.getQualifiedName(), handleEventMethodAnnotations); +// beanDefinition.addChild(eventElement); +// } +// } +// } + // event publisher checks for (InjectionPoint injectionPoint : injectionPoints) { if (Annotations.EVENT_PUBLISHER.equals(injectionPoint.getType())) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/events/EventReferenceProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/events/EventReferenceProvider.java index 813e3ab171..45d0771640 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/events/EventReferenceProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/events/EventReferenceProvider.java @@ -10,9 +10,9 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.events; -import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.Set; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; @@ -25,7 +25,6 @@ import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider; import org.springframework.ide.vscode.commons.java.IJavaProject; -import org.springframework.ide.vscode.commons.protocol.spring.Bean; import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.text.TextDocument; @@ -53,14 +52,12 @@ public List provideReferences(CancelChecker cancelToken, IJa try { Position position = doc.toPosition(offset); - Bean[] beans = index.getBeans(); + List listeners = index.getNodesOfType(EventListenerIndexElement.class); + List publishers = index.getNodesOfType(EventPublisherIndexElement.class); // when offset is inside an event listener, find the respective event type - Optional listenerEventType = Arrays.stream(beans) - .filter(bean -> bean.getLocation().getUri().equals(doc.getUri())) - .flatMap(bean -> bean.getChildren().stream()) - .filter(element -> element instanceof EventListenerIndexElement) - .map(element -> (EventListenerIndexElement) element) + Optional listenerEventType = listeners.stream() + .filter(listener -> listener.getLocation().getUri().equals(doc.getUri())) .filter(eventListener -> isPositionInside(position, eventListener.getLocation())) .map(eventListener -> eventListener.getEventType()) .findAny(); @@ -69,10 +66,7 @@ public List provideReferences(CancelChecker cancelToken, IJa // use the listener event type to look for publishers for that type String eventType = listenerEventType.get(); - List foundLocations = Arrays.stream(beans) - .flatMap(bean -> bean.getChildren().stream()) - .filter(element -> element instanceof EventPublisherIndexElement) - .map(element -> (EventPublisherIndexElement) element) + List foundLocations = publishers.stream() .filter(publisher -> publisher.getEventType().equals(eventType) || publisher.getEventTypesFromHierarchy().contains(eventType)) .map(publisher -> publisher.getLocation()) .toList(); @@ -84,25 +78,19 @@ public List provideReferences(CancelChecker cancelToken, IJa // when offset is inside an event publisher, find the respective event type else { - Optional publisherEventType = Arrays.stream(beans) - .filter(bean -> bean.getLocation().getUri().equals(doc.getUri())) - .flatMap(bean -> bean.getChildren().stream()) - .filter(element -> element instanceof EventPublisherIndexElement) - .map(element -> (EventPublisherIndexElement) element) - .filter(eventListener -> isPositionInside(position, eventListener.getLocation())) - .map(eventListener -> eventListener.getEventType()) + Optional publisherElement = publishers.stream() + .filter(publisher -> publisher.getLocation().getUri().equals(doc.getUri())) + .filter(eventPublisher -> isPositionInside(position, eventPublisher.getLocation())) .findAny(); - if (publisherEventType.isPresent()) { - // use the listener event type to look for publishers for that type - String eventType = publisherEventType.get(); + if (publisherElement.isPresent()) { + // use the publisher event type to look for listeners for that type + String eventType = publisherElement.get().getEventType(); + Set eventTypesFromHierarchy = publisherElement.get().getEventTypesFromHierarchy(); - List foundLocations = Arrays.stream(beans) - .flatMap(bean -> bean.getChildren().stream()) - .filter(element -> element instanceof EventListenerIndexElement) - .map(element -> (EventListenerIndexElement) element) - .filter(listener -> listener.getEventType().equals(eventType)) - .map(listener-> listener.getLocation()) + List foundLocations = listeners.stream() + .filter(listener -> listener.getEventType().equals(eventType) || eventTypesFromHierarchy.contains(listener.getEventType())) + .map(listener -> listener.getLocation()) .toList(); if (foundLocations.size() > 0) { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/events/test/EventsReferencesProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/events/test/EventsReferencesProviderTest.java index 2f040a5527..ee1e22ea7b 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/events/test/EventsReferencesProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/events/test/EventsReferencesProviderTest.java @@ -131,4 +131,49 @@ public void foo() { assertTrue(references.contains(expectedLocation1)); } + @Test + public void testEventPublisherFindsAllListenersIncludingThoseFromListenersWithoutAnnotation() throws Exception { + String tempJavaDocUri = directory.toPath().resolve("src/main/java/com/example/events/demo/CustomApplicationEventPublisher.java").toUri().toString(); + + Editor editor = harness.newEditor(LanguageId.JAVA, """ + package com.example.events.demo; + + import org.springframework.context.ApplicationEventPublisher; + import org.springframework.stereotype.Component; + + @Component + public class CustomApplcationEventPublisher { + + private ApplicationEventPublisher publisher; + + public CustomApplcationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + public void foo() { + this.publisher.pub<*>lishEvent(new CustomApplicationEvent(null)); + } + }""", tempJavaDocUri); + + List references = editor.getReferences(); + assertEquals(3, references.size()); + + String expectedDefinitionUri1 = directory.toPath().resolve("src/main/java/com/example/events/demo/EventListenerPerInterface.java").toUri().toString(); + Location expectedLocation1 = new Location(expectedDefinitionUri1, new Range(new Position(10, 13), new Position(10, 31))); + + assertTrue(references.contains(expectedLocation1)); + + String expectedDefinitionUri2 = directory.toPath().resolve("src/main/java/com/example/events/demo/EventListenerPerAnnotation.java").toUri().toString(); + Location expectedLocation2 = new Location(expectedDefinitionUri2, new Range(new Position(10, 13), new Position(10, 24))); + + assertTrue(references.contains(expectedLocation2)); + + String expectedDefinitionUri3 = directory.toPath().resolve("src/main/java/com/example/events/demo/EventListenerPerInterfaceAndBeanMethod.java").toUri().toString(); + Location expectedLocation3 = new Location(expectedDefinitionUri3, new Range(new Position(9, 13), new Position(9, 24))); + + assertTrue(references.contains(expectedLocation3)); + + + } + } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/events/test/SpringIndexerEventsTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/events/test/SpringIndexerEventsTest.java index f9c8d68883..e5938f88ef 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/events/test/SpringIndexerEventsTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/events/test/SpringIndexerEventsTest.java @@ -12,6 +12,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -133,6 +134,11 @@ void testEventListenerIndexElementForEventListenerInterfaceImplementation() thro Bean[] beans = springIndex.getBeansOfDocument(docUri); assertEquals(1, beans.length); + DocumentElement document = springIndex.getDocument(docUri); + List docChildren = document.getChildren(); + assertEquals(1, docChildren.size()); + assertTrue(docChildren.get(0) instanceof Bean); + Bean listenerComponentBean = Arrays.stream(beans).filter(bean -> bean.getName().equals("eventListenerPerInterface")).findFirst().get(); assertEquals("com.example.events.demo.EventListenerPerInterface", listenerComponentBean.getType()); @@ -148,6 +154,13 @@ void testEventListenerIndexElementForEventListenerInterfaceImplementation() thro assertNotNull(location); assertEquals(docUri, location.getUri()); assertEquals(new Range(new Position(10, 13), new Position(10, 31)), location.getRange()); + + List doubleCheckEventListenerNodes = springIndex.getNodesOfType(EventListenerIndexElement.class).stream() + .filter(eventListener -> eventListener.getLocation().getUri().equals(docUri)) + .toList(); + + assertEquals(1, doubleCheckEventListenerNodes.size()); + assertSame(listenerElement, doubleCheckEventListenerNodes.get(0)); } @Test @@ -161,13 +174,13 @@ void testEventListenerIndexElementForListenerInterfaceImplementationWithoutCompo List children = document.getChildren(); EventListenerIndexElement listenerElement = children.stream().filter(element -> element instanceof EventListenerIndexElement).map(element -> (EventListenerIndexElement) element).findFirst().get(); - assertEquals("org.springframework.context.ApplicationEvent", listenerElement.getEventType()); + assertEquals("com.example.events.demo.CustomApplicationEvent", listenerElement.getEventType()); assertEquals("com.example.events.demo.EventListenerPerInterfaceAndBeanMethod", listenerElement.getContainerBeanType()); Location location = listenerElement.getLocation(); assertNotNull(location); assertEquals(docUri, location.getUri()); - assertEquals(new Range(new Position(8, 13), new Position(8, 31)), location.getRange()); + assertEquals(new Range(new Position(7, 13), new Position(7, 31)), location.getRange()); } @Test diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/CustomApplicationEvent.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/CustomApplicationEvent.java new file mode 100644 index 0000000000..6a8f7837c5 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/CustomApplicationEvent.java @@ -0,0 +1,13 @@ +package com.example.events.demo; + +import org.springframework.context.ApplicationEvent; + +public class CustomApplicationEvent extends ApplicationEvent { + + private static final long serialVersionUID = 1L; + + public CustomApplicationEvent(Object source) { + super(source); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/CustomApplicationEventPublisher.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/CustomApplicationEventPublisher.java new file mode 100644 index 0000000000..cf9809b925 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/CustomApplicationEventPublisher.java @@ -0,0 +1,19 @@ +package com.example.events.demo; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +@Component +public class CustomApplicationEventPublisher { + + private ApplicationEventPublisher publisher; + + public CustomApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + public void foo() { + this.publisher.publishEvent(new CustomApplicationEvent(null)); + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/EventListenerPerInterfaceAndBeanMethod.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/EventListenerPerInterfaceAndBeanMethod.java index fa0c0e7fde..a46a0b633c 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/EventListenerPerInterfaceAndBeanMethod.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-events-indexing/src/main/java/com/example/events/demo/EventListenerPerInterfaceAndBeanMethod.java @@ -1,12 +1,11 @@ package com.example.events.demo; -import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; -public class EventListenerPerInterfaceAndBeanMethod implements ApplicationListener { +public class EventListenerPerInterfaceAndBeanMethod implements ApplicationListener { @Override - public void onApplicationEvent(ApplicationEvent event) { + public void onApplicationEvent(CustomApplicationEvent event) { System.out.println("Event received via listener implementation and bean method: " + event); }