diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchies.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchies.java index aed2efe45..162cfe2ab 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchies.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchies.java @@ -10,12 +10,11 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.annotations; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; -import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.Queue; @@ -30,6 +29,7 @@ import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.internal.compiler.problem.AbortCompilation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.commons.java.JavaUtils; import com.google.common.collect.ImmutableList; @@ -124,10 +124,13 @@ public class AnnotationHierarchies { * @return iterator over annotations hierarchy */ public Iterator iterator(IBinding binding) { + return iterator(binding, new LinkedHashSet<>()); + } + + private Iterator iterator(IBinding binding, Set seen) { return new Iterator<>() { - private HashSet seen = new HashSet<>(); - private Queue queue = new LinkedList<>(); + private Queue queue = new ArrayDeque<>(10); { if (binding instanceof IAnnotationBinding ab) { @@ -154,8 +157,7 @@ public class AnnotationHierarchies { IAnnotationBinding next = queue.poll(); for (IAnnotationBinding a : getDirectSuperAnnotationBindings(next.getAnnotationType())) { String key = a.getAnnotationType().getKey(); - if (!seen.contains(key)) { - seen.add(key); + if (seen.add(key)) { queue.add(a); } } @@ -192,55 +194,48 @@ public class AnnotationHierarchies { private AnnotationTypeInformation compute(ITypeBinding typeBinding) { Set inherited = new LinkedHashSet<>(); - String fqn = typeBinding.getQualifiedName(); - // Add itself to the set to flag it as been seen - inherited.add(fqn); - collectInheritedAnnotations(typeBinding, inherited); - // remove itself from `inherited` set as we'd like to leave only inherited annotations FQNs - inherited.remove(fqn); - return new AnnotationTypeInformation(fqn, inherited); + // Iterating over all inherited annotations must fill in the `inherited` set with annotation binding keys + for (Iterator itr = iterator(typeBinding, inherited); itr.hasNext(); itr.next()) { + // nothing to do + } + // remove itself from `inherited` set as we'd like to leave only inherited annotations binding keys in the set + String key = typeBinding.getKey(); + inherited.remove(key); + return new AnnotationTypeInformation(key, inherited); } - - // recursively collect annotations of the given annotation type - private void collectInheritedAnnotations(ITypeBinding annotationType, Set inherited) { - for (IAnnotationBinding annotation : getDirectSuperAnnotationBindings(annotationType)) { - ITypeBinding type = annotation.getAnnotationType(); - boolean notSeenYet = inherited.add(type.getQualifiedName()); - if (notSeenYet) { - collectInheritedAnnotations(type, inherited); + public boolean isAnnotatedWithAnnotationByBindingKey(IBinding binding, Predicate bindingKeyTest) { + if (binding instanceof IAnnotationBinding ab) { + return annotationInfo(ab.getAnnotationType()).map(info -> info.inherits(bindingKeyTest)).orElse(false); + } else if (binding instanceof ITypeBinding tb && tb.isAnnotation()) { + return annotationInfo(tb).map(info -> info.inherits(bindingKeyTest)).orElse(false); + } else { + for (IAnnotationBinding ab : getDirectSuperAnnotationBindings(binding)) { + if (isAnnotatedWithAnnotationByBindingKey(ab.getAnnotationType(), bindingKeyTest)) { + return true; + } } } + return false; } - public boolean isAnnotatedWith(IBinding binding, Predicate annotationFqnTest) { + public boolean isAnnotatedWithAnnotationByBindingKey(IBinding binding, String annotationBindingKey) { if (binding instanceof IAnnotationBinding ab) { - return annotationInfo(ab.getAnnotationType()).map(info -> info.inherits(annotationFqnTest)).orElse(false); + return annotationInfo(ab.getAnnotationType()).map(info -> info.inherits(annotationBindingKey)).orElse(false); } else if (binding instanceof ITypeBinding tb && tb.isAnnotation()) { - return annotationInfo(tb).map(info -> info.inherits(annotationFqnTest)).orElse(false); + return annotationInfo(tb).map(info -> info.inherits(annotationBindingKey)).orElse(false); } else { for (IAnnotationBinding ab : getDirectSuperAnnotationBindings(binding)) { - if (isAnnotatedWith(ab.getAnnotationType(), annotationFqnTest)) { + if (isAnnotatedWithAnnotationByBindingKey(ab.getAnnotationType(), annotationBindingKey)) { return true; } } } return false; } - + public boolean isAnnotatedWith(IBinding binding, String annotationTypeFqn) { - if (binding instanceof IAnnotationBinding ab) { - return annotationInfo(ab.getAnnotationType()).map(info -> info.inherits(annotationTypeFqn)).orElse(false); - } else if (binding instanceof ITypeBinding tb && tb.isAnnotation()) { - return annotationInfo(tb).map(info -> info.inherits(annotationTypeFqn)).orElse(false); - } else { - for (IAnnotationBinding ab : getDirectSuperAnnotationBindings(binding)) { - if (isAnnotatedWith(ab.getAnnotationType(), annotationTypeFqn)) { - return true; - } - } - } - return false; + return isAnnotatedWithAnnotationByBindingKey(binding, JavaUtils.typeFqNameToBindingKey(annotationTypeFqn)); } } 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 95b9c029c..1fdb79b89 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 @@ -19,6 +19,7 @@ import java.util.function.Consumer; import org.eclipse.jdt.core.dom.IAnnotationBinding; import org.eclipse.jdt.core.dom.ITypeBinding; +import org.springframework.ide.vscode.commons.java.JavaUtils; import org.springframework.ide.vscode.commons.util.Assert; import com.google.common.collect.ImmutableList; @@ -51,7 +52,7 @@ public class AnnotationHierarchyAwareLookup { /** * Associates a value with a given annotation type (and all its subtypes implicitly). * - * @param fqName Fully qualified type name for the annotation. + * @param bindingKey Fully qualified type name for the annotation. * @param overrideSuperTypes Determines whether the binding has 'override' behavior. Override behavior * means that this binding stops the search for additional bindings associated * with a super type. If override behavior is disabled the search will continue @@ -59,9 +60,9 @@ public class AnnotationHierarchyAwareLookup { * in addition to the more specific binding. * @param value */ - private void put(String fqName, boolean overrideSuperTypes, T value) { - Assert.isLegal(bindings.get(fqName)==null, "Multiple bindings to the same fqName are not supported"); - bindings.put(fqName, new Binding<>(value, overrideSuperTypes)); + private void put(String bindingKey, boolean overrideSuperTypes, T value) { + Assert.isLegal(bindings.get(bindingKey)==null, "Multiple bindings to the same fqName are not supported"); + bindings.put(bindingKey, new Binding<>(value, overrideSuperTypes)); } /** @@ -100,9 +101,9 @@ public class AnnotationHierarchyAwareLookup { } private void findElements(AnnotationHierarchies annotationHierarchies, ITypeBinding annotationType, HashSet seen, Consumer requestor) { - String qname = annotationType.getQualifiedName(); - if (seen.add(qname)) { - Binding binding = bindings.get(qname); + String bindingKey = annotationType.getKey(); + if (seen.add(bindingKey)) { + Binding binding = bindings.get(bindingKey); boolean isOverriding = false; if (binding != null) { @@ -118,8 +119,8 @@ public class AnnotationHierarchyAwareLookup { } } - public void put(String annotationName, T value) { - put(annotationName, true, value); + public void put(String fqn, T value) { + put(JavaUtils.typeFqNameToBindingKey(fqn), true, value); } public boolean containsKey(String fqName) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationTypeInformation.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationTypeInformation.java index 2dc90e26e..edaf046fb 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationTypeInformation.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationTypeInformation.java @@ -3,18 +3,18 @@ package org.springframework.ide.vscode.boot.java.annotations; import java.util.Collection; import java.util.function.Predicate; -record AnnotationTypeInformation(String fqn, Collection inheritedAnnotations) { +record AnnotationTypeInformation(String bindingKey, Collection inheritedAnnotations) { - public boolean inherits(String fullyQualifiedAnnotationType) { - return fqn.equals(fullyQualifiedAnnotationType) || inheritedAnnotations.contains(fullyQualifiedAnnotationType); + public boolean inherits(String annotationBindingKey) { + return bindingKey.equals(annotationBindingKey) || inheritedAnnotations.contains(annotationBindingKey); } - public boolean inherits(Predicate annotationFqnTest) { - if (annotationFqnTest.test(fqn)) { + public boolean inherits(Predicate annotationBindingKeyTest) { + if (annotationBindingKeyTest.test(bindingKey)) { return true; } for (String fqn : inheritedAnnotations) { - if (annotationFqnTest.test(fqn)) { + if (annotationBindingKeyTest.test(fqn)) { return true; } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java index 04f9b44d6..69b2c76c5 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java @@ -753,7 +753,7 @@ public class SpringIndexerJava implements SpringIndexer { /* * If meta annotations of the current annotation is a "sub-type" of one of the annotations from symbol providers then add it to meta annotations */ - if (annotationHierarchies.isAnnotatedWith(ab, symbolProviders::containsKey)) { + if (annotationHierarchies.isAnnotatedWithAnnotationByBindingKey(ab, symbolProviders::containsKey)) { metaAnnotations.add(ab.getAnnotationType()); } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/CompilationUnitCacheTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/CompilationUnitCacheTest.java index eae4b291b..6c8fbb188 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/CompilationUnitCacheTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/CompilationUnitCacheTest.java @@ -35,6 +35,7 @@ import org.springframework.ide.vscode.boot.editor.harness.PropertyIndexHarness; import org.springframework.ide.vscode.boot.index.cache.IndexCache; import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; @@ -243,4 +244,59 @@ public class CompilationUnitCacheTest { assertNotNull(cuAnother); assertNotNull(cuAnother); } + + @Test + void annotation_hierarchies_unchanged_when_doc_changed() throws Exception { + harness.useProject(ProjectsHarness.dummyProject()); + harness.intialize(null); + + TextDocument doc = new TextDocument(harness.createTempUri(null), LanguageId.JAVA, 0, "package my.package\n" + + "\n" + + "public class SomeClass {\n" + + "\n" + + "}\n"); + + harness.newEditorFromFileUri(doc.getUri(), doc.getLanguageId()); + CompilationUnit cu = getCompilationUnit(doc); + assertNotNull(cu); + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); + + harness.changeDocument(doc.getUri(), 0, 0, " "); + CompilationUnit cuAnother = getCompilationUnit(doc); + assertNotNull(cuAnother); + AnnotationHierarchies anotherAnnotationHierarchies = AnnotationHierarchies.get(cuAnother); + assertTrue(anotherAnnotationHierarchies == annotationHierarchies); + + CompilationUnit cuYetAnother = getCompilationUnit(doc); + AnnotationHierarchies yetAnotherAnnotationHierarchies = AnnotationHierarchies.get(cuYetAnother); + assertTrue(anotherAnnotationHierarchies == yetAnotherAnnotationHierarchies); + } + + @Test + void annotation_hierarchies_reset_by_project_change() throws Exception { + File directory = new File( + ProjectsHarness.class.getResource("/test-projects/test-request-mapping-live-hover/").toURI()); + String docUri = directory.toPath().resolve("src/main/java/example/HelloWorldController.java").toUri().toString(); + MavenJavaProject project = projects.mavenProject("test-request-mapping-live-hover"); + harness.useProject(project); + harness.intialize(directory); + + URI fileUri = new URI(docUri); + Path path = Paths.get(fileUri); + String content = new String(Files.readAllBytes(path)); + + TextDocument document = new TextDocument(docUri, LanguageId.JAVA, 0, content); + + CompilationUnit cu = getCompilationUnit(document); + assertNotNull(cu); + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); + + projectObserver.doWithListeners(l -> l.changed(project)); + CompilationUnit cuAnother = getCompilationUnit(document); + AnnotationHierarchies anotherAnnotationHierarchies = AnnotationHierarchies.get(cuAnother); + assertNotNull(cuAnother); + assertTrue(anotherAnnotationHierarchies != annotationHierarchies); + } + + }