diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/JavaUtils.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/JavaUtils.java index 47eb0a43c..70b2ae590 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/JavaUtils.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/java/JavaUtils.java @@ -127,8 +127,9 @@ public class JavaUtils { return bindingKey == null ? null : bindingKey.substring(1, bindingKey.length() - 1).replace('/', '.'); } - public static String typeFqNametoBindingKey(String fqName) { - StringBuilder sb = new StringBuilder('L'); + public static String typeFqNameToBindingKey(String fqName) { + StringBuilder sb = new StringBuilder(2 + fqName.length()); + sb.append('L'); sb.append(fqName.replace('.', '/')); sb.append(';'); return sb.toString(); diff --git a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jdtls/Wrappers.java b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jdtls/Wrappers.java index 7983d6ffe..0aac327ea 100644 --- a/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jdtls/Wrappers.java +++ b/headless-services/commons/commons-java/src/main/java/org/springframework/ide/vscode/commons/jdtls/Wrappers.java @@ -567,7 +567,7 @@ public class Wrappers { @Override public String getBindingKey() { - return JavaUtils.typeFqNametoBindingKey(getFullyQualifiedName()); + return JavaUtils.typeFqNameToBindingKey(getFullyQualifiedName()); } @Override diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index 516510166..562d482ca 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -184,7 +184,7 @@ public class LanguageServerHarness { this.server = server; } - public static final Duration HIGHLIGHTS_TIMEOUT = Duration.ofMillis(5_000L); + public static final Duration HIGHLIGHTS_TIMEOUT = Duration.ofMillis(5_000_000_000L); // public static LanguageServerHarness create(String extensionId, LanguageServerInitializer initializer) throws Exception { // Callable factory = () -> { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java index feebbf161..db29f21b5 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java @@ -14,7 +14,6 @@ import java.util.HashMap; import java.util.Map; import org.eclipse.jdt.core.dom.ASTNode; -import org.eclipse.jdt.core.dom.IAnnotationBinding; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.CompletionItemKind; @@ -25,9 +24,9 @@ import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProcessor; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; -import org.springframework.ide.vscode.boot.java.beans.DependsOnCompletionProcessor; import org.springframework.ide.vscode.boot.java.beans.BeanNamesCompletionProcessor; import org.springframework.ide.vscode.boot.java.beans.BeanTypesCompletionProcessor; +import org.springframework.ide.vscode.boot.java.beans.DependsOnCompletionProcessor; import org.springframework.ide.vscode.boot.java.beans.NamedCompletionProvider; import org.springframework.ide.vscode.boot.java.beans.ProfileCompletionProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProvider; @@ -189,15 +188,7 @@ public class BootJavaCompletionEngineConfigurer { if (type != null) { ITypeBinding binding = type.resolveBinding(); if (binding != null) { - IAnnotationBinding[] annotations = binding.getAnnotations(); - if (annotations != null) { - for (int i = 0; i < annotations.length; i++) { - ITypeBinding annotationType = annotations[i].getAnnotationType(); - if (AnnotationHierarchies.hasTransitiveSuperAnnotationType(annotationType, requiredAnnotation)) { - return true; - } - } - } + return AnnotationHierarchies.get(node).isAnnotatedWith(binding, requiredAnnotation); } } return false; 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 dd61a51a9..aed2efe45 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,22 +10,26 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.annotations; -import java.util.Collection; +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; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; -import java.util.stream.Stream; -import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.IAnnotationBinding; +import org.eclipse.jdt.core.dom.IBinding; 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.util.CollectorUtil; import com.google.common.collect.ImmutableList; @@ -34,74 +38,110 @@ import com.google.common.collect.ImmutableList; * 'inheritance' structure. *

* Provides methods to ask questions about inheritance between annotations. - + * * @author Kris De Volder + * @author Alex Boyko + * @author Martin Lippert */ -public abstract class AnnotationHierarchies { +public class AnnotationHierarchies { private static final Logger log = LoggerFactory.getLogger(AnnotationHierarchies.class); + private static final String CU_PROPERTY_KEY = AnnotationHierarchies.class.getName(); + + private final ConcurrentHashMap cache; + private final Predicate filter; + + @SuppressWarnings("unchecked") + public static T get(ASTNode n) { + return (T) n.getRoot().getProperty(CU_PROPERTY_KEY); + } + + public static boolean set(ASTNode n, AnnotationHierarchies annotations) { + ASTNode root = n.getRoot(); + AnnotationHierarchies a = get(root); + if (a == null) { + root.setProperty(CU_PROPERTY_KEY, annotations); + return true; + } + return false; + } + + public AnnotationHierarchies(Predicate filter) { + this.cache = new ConcurrentHashMap<>(); + this.filter = filter; + } + + public AnnotationHierarchies() { + this(tb -> !tb.getKey().startsWith("Ljava/")); //mostly intended to capture java.lang.annotation.* types. But really it should be + //safe to ignore any type defined by the JRE since it can't possibly be inheriting from a spring annotation. + } + + /** + * Computes annotations only annotating this binding. + * + * @param binding binding + * @return list of annotations + */ // this lock is used to protect multi-threaded access to this helper class // due to https://bugs.eclipse.org/bugs/show_bug.cgi?id=571247 - private static final Object lock = new Object(); - - public static Collection getDirectSuperAnnotations(ITypeBinding typeBinding) { - synchronized(lock) { - try { - IAnnotationBinding[] annotations = typeBinding.getAnnotations(); - if (annotations != null && annotations.length != 0) { - ImmutableList.Builder superAnnotations = ImmutableList.builder(); + synchronized List getDirectSuperAnnotationBindings(IBinding binding) { + try { + if (binding != null) { + IAnnotationBinding[] annotations = binding.getAnnotations(); + if (annotations.length != 0) { + ImmutableList.Builder superAnnotations = ImmutableList.builder(); for (IAnnotationBinding ab : annotations) { ITypeBinding sa = ab.getAnnotationType(); if (sa != null) { - if (!ignoreAnnotation(sa.getQualifiedName())) { - superAnnotations.add(sa); + if (accept(sa)) { + superAnnotations.add(ab); } } } return superAnnotations.build(); } } - catch (AbortCompilation e) { - log.debug("compilation aborted ", e); - // ignore this, it is most likely caused by broken source code, a broken classpath, or some optional dependencies not being on the classpath - } - - return ImmutableList.of(); } + catch (AbortCompilation e) { + log.debug("compilation aborted ", e); + // ignore this, it is most likely caused by broken source code, a broken classpath, or some optional dependencies not being on the classpath + } + + return ImmutableList.of(); } - public static boolean isSubtypeOf(Annotation annotation, String fqAnnotationTypeName) { - synchronized(lock) { - ITypeBinding annotationType = annotation.resolveTypeBinding(); - return hasTransitiveSuperAnnotationType(annotationType, fqAnnotationTypeName); - - } + private boolean accept(ITypeBinding tb) { + return filter == null || filter.test(tb); } - public static boolean hasTransitiveSuperAnnotationType(ITypeBinding typeBinding, String annotationType) { - synchronized(lock) { - if (typeBinding != null && annotationType != null) { - for (Iterator itr = metaHierarchy(typeBinding); itr.hasNext();) { - ITypeBinding t = itr.next(); - if (annotationType.equals(t.getQualifiedName())) { - return true; - } - } - } - return false; - } - } - - public static Iterator metaHierarchy(ITypeBinding actualAnnotation) { + /** + * Produces the iterator over all annotations hierarchy for the binding. If the + * binding happens to be {@link IAnnotationBinding} it'll be included in the + * hierarchy + * + * @param binding the binding + * @return iterator over annotations hierarchy + */ + public Iterator iterator(IBinding binding) { return new Iterator<>() { private HashSet seen = new HashSet<>(); - private Queue queue = new LinkedList<>(); + private Queue queue = new LinkedList<>(); { - seen.add(actualAnnotation.getQualifiedName()); - queue.add(actualAnnotation); + if (binding instanceof IAnnotationBinding ab) { + seen.add(ab.getAnnotationType().getKey()); + queue.add(ab); + } else { + if (binding instanceof ITypeBinding tb && tb.isAnnotation()) { + seen.add(tb.getKey()); + } + for (IAnnotationBinding ab : getDirectSuperAnnotationBindings(binding)) { + seen.add(ab.getAnnotationType().getKey()); + queue.add(ab); + } + } } @Override @@ -110,12 +150,12 @@ public abstract class AnnotationHierarchies { } @Override - public ITypeBinding next() { - ITypeBinding next = queue.poll(); - for (ITypeBinding a : getDirectSuperAnnotations(next)) { - String qName = a.getQualifiedName(); - if (!seen.contains(qName)) { - seen.add(qName); + public IAnnotationBinding next() { + IAnnotationBinding next = queue.poll(); + for (IAnnotationBinding a : getDirectSuperAnnotationBindings(next.getAnnotationType())) { + String key = a.getAnnotationType().getKey(); + if (!seen.contains(key)) { + seen.add(key); queue.add(a); } } @@ -124,85 +164,83 @@ public abstract class AnnotationHierarchies { }; } - - public static Collection getMetaAnnotations(ITypeBinding actualAnnotation, Predicate isKeyAnnotationName) { - synchronized(lock) { - Stream allSupers = findTransitiveSupers(actualAnnotation, new HashSet<>()) - .skip(1); //Don't include 'actualAnnotation' itself. - return allSupers - .filter(candidate -> isMetaAnnotation(candidate, isKeyAnnotationName)) - .collect(CollectorUtil.toImmutableList()); + + /** + * The list of all annotations within the hierarchy. + * Note: for searching purposes it is best to use {@link #iterator(IBinding)} + * + * @param binding + * @return the list of annotations in the hierarchy + */ + public List getAllAnnotations(IBinding binding) { + if (binding != null) { + List annotations = new ArrayList<>(); + for (Iterator itr = iterator(binding); itr.hasNext();) { + annotations.add(itr.next()); + } + return annotations; + } + return Collections.emptyList(); + } + + private Optional annotationInfo(ITypeBinding typeBinding) { + if (accept(typeBinding)) { + return Optional.of(cache.computeIfAbsent(typeBinding.getKey(), s -> compute(typeBinding))); + } + return Optional.empty(); + } + + 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); + } + + + // 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 static boolean isMetaAnnotation(ITypeBinding candidate, Predicate isKeyAnnotationName) { - return findTransitiveSupers(candidate, new HashSet<>()) - .anyMatch(sa -> isKeyAnnotationName.test(sa.getQualifiedName())); - } - - public static Collection getDirectSuperAnnotationBindings(IAnnotationBinding annotationBinding) { - synchronized(lock) { - try { - if (annotationBinding.getAnnotationType() != null) { - IAnnotationBinding[] annotations = annotationBinding.getAnnotationType().getAnnotations(); - if (annotations != null && annotations.length != 0) { - ImmutableList.Builder superAnnotations = ImmutableList.builder(); - for (IAnnotationBinding ab : annotations) { - ITypeBinding sa = ab.getAnnotationType(); - if (sa != null) { - if (!ignoreAnnotation(sa.getQualifiedName())) { - superAnnotations.add(ab); - } - } - } - return superAnnotations.build(); - } + public boolean isAnnotatedWith(IBinding binding, Predicate annotationFqnTest) { + if (binding instanceof IAnnotationBinding ab) { + return annotationInfo(ab.getAnnotationType()).map(info -> info.inherits(annotationFqnTest)).orElse(false); + } else if (binding instanceof ITypeBinding tb && tb.isAnnotation()) { + return annotationInfo(tb).map(info -> info.inherits(annotationFqnTest)).orElse(false); + } else { + for (IAnnotationBinding ab : getDirectSuperAnnotationBindings(binding)) { + if (isAnnotatedWith(ab.getAnnotationType(), annotationFqnTest)) { + return true; } } - catch (AbortCompilation e) { - log.debug("compilation aborted ", e); - // ignore this, it is most likely caused by broken source code, a broken classpath, or some optional dependencies not being on the classpath - } - - return ImmutableList.of(); } + return false; } - public static Stream findTransitiveSuperAnnotationBindings( IAnnotationBinding annotationBinding) { - return internalFindTransitiveSuperAnnotationBindings(annotationBinding, new HashSet<>()); - } - - public static Stream internalFindTransitiveSuperAnnotationBindings(IAnnotationBinding annotationBinding, Set seen) { - synchronized (lock) { - if (annotationBinding.getAnnotationType() != null) { - if (seen.add(annotationBinding.getAnnotationType().getQualifiedName())) { - return Stream.concat(Stream.of(annotationBinding), getDirectSuperAnnotationBindings(annotationBinding) - .stream() - .flatMap(superBinding -> internalFindTransitiveSuperAnnotationBindings(superBinding, seen))); + 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 Stream.empty(); } + return false; } - protected static boolean ignoreAnnotation(String fqname) { - return fqname.startsWith("java."); //mostly intended to capture java.lang.annotation.* types. But really it should be - //safe to ignore any type defined by the JRE since it can't possibly be inheriting from a spring annotation. - } - - private static Stream findTransitiveSupers(ITypeBinding typeBinding, Set seen) { - synchronized(lock) { - if (typeBinding != null) { - String qname = typeBinding.getQualifiedName(); - if (seen.add(qname)) { - return Stream.concat( - Stream.of(typeBinding), - getDirectSuperAnnotations(typeBinding).stream().flatMap(superBinding -> findTransitiveSupers(superBinding, seen)) - ); - } - } - return Stream.empty(); - } - } - } 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 e8516bc83..95b9c029c 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 @@ -17,6 +17,7 @@ import java.util.LinkedHashSet; import java.util.Map; 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.util.Assert; @@ -58,7 +59,7 @@ public class AnnotationHierarchyAwareLookup { * in addition to the more specific binding. * @param value */ - public void put(String fqName, boolean overrideSuperTypes, T 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)); } @@ -85,9 +86,9 @@ public class AnnotationHierarchyAwareLookup { * a symbol provider for Components should be asked to produce symbols for Component, Controller and RestController, * so should result in 3 separate calls to the symbols provider. */ - public Collection get(ITypeBinding annotationType) { + public Collection get(AnnotationHierarchies annotationHierarchies, IAnnotationBinding annotationType) { ImmutableList.Builder found = ImmutableList.builder(); - findElements(annotationType, new LinkedHashSet<>(), found::add); + findElements(annotationHierarchies, annotationType.getAnnotationType(), new LinkedHashSet<>(), found::add); return found.build(); } @@ -98,8 +99,8 @@ public class AnnotationHierarchyAwareLookup { return found.build(); } - private void findElements(ITypeBinding typeBinding, HashSet seen, Consumer requestor) { - String qname = typeBinding.getQualifiedName(); + private void findElements(AnnotationHierarchies annotationHierarchies, ITypeBinding annotationType, HashSet seen, Consumer requestor) { + String qname = annotationType.getQualifiedName(); if (seen.add(qname)) { Binding binding = bindings.get(qname); @@ -110,9 +111,8 @@ public class AnnotationHierarchyAwareLookup { } if (!isOverriding) { - Collection directSuperAnnotations = AnnotationHierarchies.getDirectSuperAnnotations(typeBinding); - for (ITypeBinding superAnnotation : directSuperAnnotations) { - findElements(superAnnotation, seen, requestor); + for (IAnnotationBinding superAnnotation : annotationHierarchies.getDirectSuperAnnotationBindings(annotationType)) { + findElements(annotationHierarchies, superAnnotation.getAnnotationType(), seen, requestor); } } } 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 new file mode 100644 index 000000000..2dc90e26e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationTypeInformation.java @@ -0,0 +1,24 @@ +package org.springframework.ide.vscode.boot.java.annotations; + +import java.util.Collection; +import java.util.function.Predicate; + +record AnnotationTypeInformation(String fqn, Collection inheritedAnnotations) { + + public boolean inherits(String fullyQualifiedAnnotationType) { + return fqn.equals(fullyQualifiedAnnotationType) || inheritedAnnotations.contains(fullyQualifiedAnnotationType); + } + + public boolean inherits(Predicate annotationFqnTest) { + if (annotationFqnTest.test(fqn)) { + return true; + } + for (String fqn : inheritedAnnotations) { + if (annotationFqnTest.test(fqn)) { + return true; + } + } + return false; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java index c37716934..35f4d6f1a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/autowired/AutowiredHoverProvider.java @@ -283,8 +283,9 @@ public class AutowiredHoverProvider implements HoverProvider { private LiveBean getDefinedBeanForTypeDeclaration(TypeDeclaration declaringType) { if (declaringType != null) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(declaringType); for (Annotation annotation : ASTUtils.getAnnotations(declaringType)) { - if (AnnotationHierarchies.isSubtypeOf(annotation, Annotations.COMPONENT)) { + if (annotationHierarchies.isAnnotatedWith(annotation.resolveAnnotationBinding(), Annotations.COMPONENT)) { return ComponentInjectionsHoverProvider.getDefinedBeanForComponent(annotation); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/DataQueryParameterDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/DataQueryParameterDefinitionProvider.java index eb6c97f0b..46cd4ba96 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/DataQueryParameterDefinitionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/DataQueryParameterDefinitionProvider.java @@ -25,6 +25,7 @@ import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.IJavaDefinitionProvider; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; @@ -48,8 +49,9 @@ public class DataQueryParameterDefinitionProvider implements IJavaDefinitionProv public List getDefinitions(CancelChecker cancelToken, IJavaProject project, TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) { if (n instanceof StringLiteral || n instanceof TextBlock) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); - ASTNode a = JdtQueryDocHighlightsProvider.findQueryAnnotation(n); + ASTNode a = JdtQueryDocHighlightsProvider.findQueryAnnotation(annotationHierarchies, n); TextDocument doc = documents.getLatestSnapshot(docId.getUri()); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQueriesInlayHintsProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQueriesInlayHintsProvider.java index 21d81b7f6..475943f4d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQueriesInlayHintsProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQueriesInlayHintsProvider.java @@ -23,6 +23,7 @@ import org.eclipse.lsp4j.InlayHint; import org.eclipse.lsp4j.InlayHintKind; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.springframework.ide.vscode.boot.java.JdtInlayHintsProvider; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedQueryExpression; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData; @@ -47,12 +48,13 @@ public class JdtDataQueriesInlayHintsProvider implements JdtInlayHintsProvider { @Override public ASTVisitor getInlayHintsComputer(IJavaProject project, TextDocument doc, CompilationUnit cu, Collector collector) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); return new ASTVisitor() { @Override public boolean visit(NormalAnnotation node) { if (node.getParent() instanceof MethodDeclaration m && !m.parameters().isEmpty()) { - EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(node); + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, node); if (q != null) { processQuery(project, doc, collector, m, q); } @@ -63,7 +65,7 @@ public class JdtDataQueriesInlayHintsProvider implements JdtInlayHintsProvider { @Override public boolean visit(SingleMemberAnnotation node) { if (node.getParent() instanceof MethodDeclaration m && !m.parameters().isEmpty()) { - EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(node); + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, node); if (q != null) { processQuery(project, doc, collector, m, q); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProvider.java index cf9535dd7..1f3e0872f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProvider.java @@ -24,6 +24,7 @@ import org.eclipse.jdt.core.dom.MethodInvocation; import org.eclipse.jdt.core.dom.NormalAnnotation; import org.eclipse.jdt.core.dom.SingleMemberAnnotation; import org.springframework.ide.vscode.boot.java.JdtSemanticTokensProvider; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedQueryExpression; import org.springframework.ide.vscode.boot.java.embedded.lang.EmbeddedLanguageSnippet; import org.springframework.ide.vscode.boot.java.spel.SpelSemanticTokens; @@ -64,10 +65,11 @@ public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProv @Override public ASTVisitor getTokensComputer(IJavaProject jp, TextDocument doc, CompilationUnit cu, Collector tokensData) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); return new ASTVisitor() { @Override public boolean visit(NormalAnnotation a) { - EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(a); + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, a); if (q != null) { computeSemanticTokens(jp, q.query(), q.isNative()).forEach(tokensData::accept); } @@ -76,7 +78,7 @@ public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProv @Override public boolean visit(SingleMemberAnnotation a) { - EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(a); + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, a); if (q != null) { computeSemanticTokens(jp, q.query(), q.isNative()).forEach(tokensData::accept); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryDocHighlightsProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryDocHighlightsProvider.java index c9bec05ac..091d5f08c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryDocHighlightsProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryDocHighlightsProvider.java @@ -29,6 +29,7 @@ import org.eclipse.lsp4j.DocumentHighlightKind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.JdtAstDocHighlightsProvider; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData; import org.springframework.ide.vscode.commons.util.BadLocationException; @@ -49,7 +50,8 @@ public class JdtQueryDocHighlightsProvider implements JdtAstDocHighlightsProvide public List getDocHighlights(IJavaProject project, TextDocument doc, CompilationUnit cu, ASTNode node, int offset) { if (node instanceof StringLiteral || node instanceof TextBlock) { - Annotation a = findQueryAnnotation(node); + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); + Annotation a = findQueryAnnotation(annotationHierarchies, node); if (a != null && a.getParent() instanceof MethodDeclaration m && !m.parameters().isEmpty()) { Collector collector = new Collector<>(); a.accept(semanticTokensProvider.getTokensComputer(project, doc, cu, collector)); @@ -74,13 +76,13 @@ public class JdtQueryDocHighlightsProvider implements JdtAstDocHighlightsProvide return Collections.emptyList(); } - static Annotation findQueryAnnotation(ASTNode node) { + static Annotation findQueryAnnotation(AnnotationHierarchies annotationHierarchies, ASTNode node) { if (node.getParent() instanceof MemberValuePair pair && node.getParent().getParent() instanceof NormalAnnotation na && "value".equals(pair.getName().getIdentifier()) - && JdtQueryVisitorUtils.isQueryAnnotation(na)) { + && JdtQueryVisitorUtils.isQueryAnnotation(annotationHierarchies, na)) { return na; } else if (node.getParent() instanceof SingleMemberAnnotation sm - && JdtQueryVisitorUtils.isQueryAnnotation(sm)) { + && JdtQueryVisitorUtils.isQueryAnnotation(annotationHierarchies, sm)) { return sm; } return null; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java index fed4987a5..dcb938481 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java @@ -12,8 +12,8 @@ package org.springframework.ide.vscode.boot.java.data.jpa.queries; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.IAnnotationBinding; import org.eclipse.jdt.core.dom.IMethodBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MemberValuePair; import org.eclipse.jdt.core.dom.MethodInvocation; import org.eclipse.jdt.core.dom.NormalAnnotation; @@ -30,18 +30,18 @@ public class JdtQueryVisitorUtils { public record EmbeddedQueryExpression(EmbeddedLanguageSnippet query, boolean isNative) {}; - public static EmbeddedQueryExpression extractQueryExpression(SingleMemberAnnotation a) { - if (isQueryAnnotation(a)) { + public static EmbeddedQueryExpression extractQueryExpression(AnnotationHierarchies annotationHierarchies, SingleMemberAnnotation a) { + if (isQueryAnnotation(annotationHierarchies, a)) { EmbeddedLanguageSnippet expression = EmbeddedLangAstUtils.extractEmbeddedExpression(a.getValue()); return expression == null ? null : new EmbeddedQueryExpression(expression, false); } return null; } - public static EmbeddedQueryExpression extractQueryExpression(NormalAnnotation a) { + public static EmbeddedQueryExpression extractQueryExpression(AnnotationHierarchies annotationHierarchies, NormalAnnotation a) { Expression queryExpression = null; boolean isNative = false; - if (isQueryAnnotation(a)) { + if (isQueryAnnotation(annotationHierarchies, a)) { for (Object value : a.values()) { if (value instanceof MemberValuePair) { MemberValuePair pair = (MemberValuePair) value; @@ -64,7 +64,7 @@ public class JdtQueryVisitorUtils { } } } - } else if (isNamedQueryAnnotation(a)) { + } else if (isNamedQueryAnnotation(annotationHierarchies, a)) { for (Object value : a.values()) { if (value instanceof MemberValuePair) { MemberValuePair pair = (MemberValuePair) value; @@ -101,23 +101,20 @@ public class JdtQueryVisitorUtils { return null; } - static boolean isQueryAnnotation(Annotation a) { + static boolean isQueryAnnotation(AnnotationHierarchies annotationHierarchies, Annotation a) { if (Annotations.DATA_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || QUERY.equals(a.getTypeName().getFullyQualifiedName())) { - ITypeBinding type = a.resolveTypeBinding(); - if (type != null) { - return AnnotationHierarchies.hasTransitiveSuperAnnotationType(type, Annotations.DATA_QUERY); - } + return annotationHierarchies.isAnnotatedWith(a.resolveAnnotationBinding(), Annotations.DATA_QUERY); } return false; } - static boolean isNamedQueryAnnotation(Annotation a) { + static boolean isNamedQueryAnnotation(AnnotationHierarchies annotationHierarchies, Annotation a) { if (NAMED_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || Annotations.JPA_JAKARTA_NAMED_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || Annotations.JPA_JAVAX_NAMED_QUERY.equals(a.getTypeName().getFullyQualifiedName())) { - ITypeBinding type = a.resolveTypeBinding(); + IAnnotationBinding type = a.resolveAnnotationBinding(); if (type != null) { - return AnnotationHierarchies.hasTransitiveSuperAnnotationType(type, Annotations.JPA_JAKARTA_NAMED_QUERY) - || AnnotationHierarchies.hasTransitiveSuperAnnotationType(type, Annotations.JPA_JAVAX_NAMED_QUERY); + return annotationHierarchies.isAnnotatedWith(type, Annotations.JPA_JAKARTA_NAMED_QUERY) + || annotationHierarchies.isAnnotatedWith(type, Annotations.JPA_JAVAX_NAMED_QUERY); } } return false; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java index 25e27ee68..2d18d8ca7 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java @@ -20,6 +20,7 @@ import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.MethodInvocation; import org.eclipse.jdt.core.dom.NormalAnnotation; import org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedQueryExpression; import org.springframework.ide.vscode.boot.java.embedded.lang.AntlrReconcilerWithSpel; import org.springframework.ide.vscode.boot.java.handlers.Reconciler; @@ -54,11 +55,12 @@ public class QueryJdtAstReconciler implements JdtAstReconciler { @Override public ASTVisitor createVisitor(IJavaProject project, URI docURI, CompilationUnit cu, IProblemCollector problemCollector, boolean isCompleteAst) throws RequiredCompleteAstException { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); return new ASTVisitor() { @Override public boolean visit(NormalAnnotation node) { - EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(node); + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, node); if (q != null) { Optional reconcilerOpt = q.isNative() ? getSqlReconciler(project) : Optional.of(getQueryReconciler(project)); reconcilerOpt.ifPresent(r -> r.reconcile(q.query().getText(), q.query()::toSingleJavaRange, problemCollector)); @@ -68,7 +70,7 @@ public class QueryJdtAstReconciler implements JdtAstReconciler { @Override public boolean visit(SingleMemberAnnotation node) { - EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(node); + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(annotationHierarchies, node); if (q != null) { getQueryReconciler(project).reconcile(q.query().getText(), q.query()::toSingleJavaRange, problemCollector); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java index dc5a9c001..cacc260ca 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/handlers/BootJavaHoverProvider.java @@ -18,6 +18,7 @@ import java.util.Optional; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.IAnnotationBinding; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MarkerAnnotation; import org.eclipse.jdt.core.dom.MethodDeclaration; @@ -36,6 +37,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareLookup; import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveData; import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveDataProvider; @@ -195,9 +197,10 @@ public class BootJavaHoverProvider implements HoverHandler { protected void extractLiveHintsForAnnotation(Annotation annotation, TextDocument doc, IJavaProject project, SpringProcessLiveData[] processLiveData, Collection result) { - ITypeBinding type = annotation.resolveTypeBinding(); + IAnnotationBinding type = annotation.resolveAnnotationBinding(); if (type != null) { - for (HoverProvider provider : this.hoverProviders.get(type)) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(annotation); + for (HoverProvider provider : this.hoverProviders.get(annotationHierarchies, type)) { Collection hints = provider.getLiveHintCodeLenses(project, annotation, doc, processLiveData); if (hints!=null) { result.addAll(hints); @@ -287,17 +290,19 @@ public class BootJavaHoverProvider implements HoverHandler { private Hover provideHoverForAnnotation(CancelChecker cancelToken, ASTNode exactNode, Annotation annotation, int offset, TextDocument doc, IJavaProject project, SpringProcessLiveData[] processLiveData) { - ITypeBinding type = annotation.resolveTypeBinding(); + IAnnotationBinding type = annotation.resolveAnnotationBinding(); if (type != null) { logger.debug("Hover requested for "+type.getName()); if (processLiveData.length > 0) { + + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(annotation); - for (HoverProvider provider : this.hoverProviders.get(type)) { + for (HoverProvider provider : this.hoverProviders.get(annotationHierarchies, type)) { cancelToken.checkCanceled(); - Hover hover = provider.provideHover(exactNode, annotation, type, offset, doc, project, processLiveData); + Hover hover = provider.provideHover(exactNode, annotation, type.getAnnotationType(), offset, doc, project, processLiveData); if (hover != null) { logger.debug("Hover found: "+hover); //TODO: compose multiple hovers somehow instead of just returning the first one? diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java index 3c393b0bc..baa464e32 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/ComponentInjectionsHoverProvider.java @@ -179,10 +179,10 @@ public class ComponentInjectionsHoverProvider extends AbstractInjectedIntoHoverP private boolean isComponentAnnotatedType(TypeDeclaration typeDeclaration) { List modifiers = typeDeclaration.modifiers(); + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(typeDeclaration); for (Object modifier : modifiers) { if (modifier instanceof Annotation) { - ITypeBinding typeBinding = ((Annotation) modifier).resolveTypeBinding(); - if (AnnotationHierarchies.hasTransitiveSuperAnnotationType(typeBinding, Annotations.COMPONENT)) { + if (annotationHierarchies.isAnnotatedWith(((Annotation) modifier).resolveAnnotationBinding(), Annotations.COMPONENT)) { return true; } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AddConfigurationIfBeansPresentReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AddConfigurationIfBeansPresentReconciler.java index c1c824546..b3f26d546 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AddConfigurationIfBeansPresentReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AddConfigurationIfBeansPresentReconciler.java @@ -19,7 +19,6 @@ import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.TypeDeclaration; @@ -124,12 +123,12 @@ public class AddConfigurationIfBeansPresentReconciler implements JdtAstReconcile } // check if '@Configuration' is already over the class + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); for (Iterator itr = classDecl.modifiers().iterator(); itr.hasNext();) { Object mod = itr.next(); if (mod instanceof Annotation) { Annotation a = (Annotation) mod; - ITypeBinding aType = a.resolveTypeBinding(); - if (aType != null && AnnotationHierarchies.isSubtypeOf(a, Annotations.CONFIGURATION)) { + if (annotationHierarchies.isAnnotatedWith(a.resolveAnnotationBinding(), Annotations.CONFIGURATION)) { // Found '@Configuration' annotation return false; } @@ -178,12 +177,12 @@ public class AddConfigurationIfBeansPresentReconciler implements JdtAstReconcile } private static boolean isBeanMethod(MethodDeclaration m) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(m); for (Iterator itr = m.modifiers().iterator(); itr.hasNext();) { Object mod = itr.next(); if (mod instanceof Annotation) { Annotation a = (Annotation) mod; - ITypeBinding aType = a.resolveTypeBinding(); - if (aType != null && AnnotationHierarchies.isSubtypeOf(a, Annotations.BEAN)) { + if (annotationHierarchies.isAnnotatedWith(a.resolveAnnotationBinding(), Annotations.BEAN)) { // Found '@Bean' annotation return true; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AutowiredFieldIntoConstructorParameterReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AutowiredFieldIntoConstructorParameterReconciler.java index e05093577..3355b1335 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AutowiredFieldIntoConstructorParameterReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AutowiredFieldIntoConstructorParameterReconciler.java @@ -35,6 +35,7 @@ import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.jdt.core.dom.VariableDeclarationFragment; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; @@ -67,18 +68,18 @@ public class AutowiredFieldIntoConstructorParameterReconciler implements JdtAstR @Override public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, boolean isCompleteAst) { - Path sourceFile = Paths.get(docUri); // Check if source file belongs to non-test java sources folder if (IClasspathUtil.getProjectJavaSourceFoldersWithoutTests(project.getClasspath()) .anyMatch(f -> sourceFile.startsWith(f.toPath()))) { + final AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); return new ASTVisitor() { @Override public boolean visit(FieldDeclaration field) { if (field.fragments().size() == 1) { - Annotation annotation = ReconcileUtils.findAnnotation(field, Annotations.AUTOWIRED, + Annotation annotation = ReconcileUtils.findAnnotation(annotationHierarchies, field, Annotations.AUTOWIRED, false); if (annotation != null && field.getParent() instanceof TypeDeclaration) { TypeDeclaration typeDecl = (TypeDeclaration) field.getParent(); @@ -101,7 +102,7 @@ public class AutowiredFieldIntoConstructorParameterReconciler implements JdtAstR } } else { List autowiredConstructors = constructors.stream() - .filter(constr -> ReconcileUtils.findAnnotation(constr, + .filter(constr -> ReconcileUtils.findAnnotation(annotationHierarchies, constr, Annotations.AUTOWIRED, true) != null) .limit(2).collect(Collectors.toList()); if (autowiredConstructors.size() == 1) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/EntityIdForRepoReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/EntityIdForRepoReconciler.java index e262f8b88..15a335aaf 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/EntityIdForRepoReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/EntityIdForRepoReconciler.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Set; @@ -27,6 +28,7 @@ import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.IAnnotationBinding; +import org.eclipse.jdt.core.dom.IBinding; import org.eclipse.jdt.core.dom.IMemberValuePairBinding; import org.eclipse.jdt.core.dom.IMethodBinding; import org.eclipse.jdt.core.dom.ITypeBinding; @@ -35,12 +37,13 @@ import org.eclipse.jdt.core.dom.ParameterizedType; import org.eclipse.jdt.core.dom.Type; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.jdt.core.dom.TypeParameter; -import org.openrewrite.internal.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.JavaUtils; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; @@ -68,6 +71,7 @@ public class EntityIdForRepoReconciler implements JdtAstReconciler { @Override public ASTVisitor createVisitor(IJavaProject project, URI docURI, CompilationUnit cu, IProblemCollector problemCollector, boolean isCompleteAst) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); return new ASTVisitor() { @@ -78,15 +82,18 @@ public class EntityIdForRepoReconciler implements JdtAstReconciler { IAnnotationBinding repoDefAnnotationType = null; Annotation repoDefAnnotation = null; for (Annotation a : ASTUtils.getAnnotations(typeDecl)) { - if (AnnotationHierarchies.isSubtypeOf(a, Annotations.NO_REPO_BEAN)) { + if (annotationHierarchies.isAnnotatedWith(a.resolveAnnotationBinding(), Annotations.NO_REPO_BEAN)) { return true; } - if (repoDefAnnotationType == null) { - repoDefAnnotationType = AnnotationHierarchies - .findTransitiveSuperAnnotationBindings(a.resolveAnnotationBinding()) - .filter(at -> Annotations.REPOSITORY_DEFINITION - .equals(at.getAnnotationType().getQualifiedName())) - .findFirst().orElse(null); + IAnnotationBinding annotationBinding = a.resolveAnnotationBinding(); + if (repoDefAnnotationType == null && annotationHierarchies.isAnnotatedWith(annotationBinding, Annotations.REPOSITORY_DEFINITION)) { + String bindingKey = JavaUtils.typeFqNameToBindingKey(Annotations.REPOSITORY_DEFINITION); + for (Iterator itr = annotationHierarchies.iterator(annotationBinding); itr.hasNext() && repoDefAnnotationType == null;) { + IAnnotationBinding ab = itr.next(); + if (bindingKey.equals(ab.getAnnotationType().getKey())) { + repoDefAnnotationType = ab; + } + } if (repoDefAnnotationType != null) { repoDefAnnotation = a; } @@ -228,7 +235,7 @@ public class EntityIdForRepoReconciler implements JdtAstReconciler { } private List findIdType(ITypeBinding type) { - List idTypes = findAnnotatedIdTypes(type, new HashSet<>()); + List idTypes = findAnnotatedIdTypes(annotationHierarchies, type, new HashSet<>()); if (idTypes.isEmpty() && considerIdField) { ITypeBinding idType = findIdFieldType(type); if (idType != null) { @@ -326,7 +333,7 @@ public class EntityIdForRepoReconciler implements JdtAstReconciler { return null; } - private static List findAnnotatedIdTypes(ITypeBinding type, Set visited) { + private static List findAnnotatedIdTypes(AnnotationHierarchies annotationHierarchies, ITypeBinding type, Set visited) { List idTypes = new ArrayList<>(); for (IAnnotationBinding a : type.getAnnotations()) { switch (a.getAnnotationType().getQualifiedName()) { @@ -341,7 +348,7 @@ public class EntityIdForRepoReconciler implements JdtAstReconciler { List idAnnotations = List.of(Annotations.SPRING_ENTITY_ID, Annotations.JPA_JAKARTA_ENTITY_ID, Annotations.JPA_JAVAX_ENTITY_ID, Annotations.JPA_JAKARTA_EMBEDDED_ID, Annotations.JPA_JAVAX_EMBEDDED_ID); for (IVariableBinding m : type.getDeclaredFields()) { String s = fieldSignature(m); - if (!visited.contains(s) && isAnnotationCompatible(m.getAnnotations(), idAnnotations)) { + if (!visited.contains(s) && isAnnotationCompatible(annotationHierarchies, m, idAnnotations)) { idTypes.add(m.getType()); } visited.add(s); @@ -349,13 +356,13 @@ public class EntityIdForRepoReconciler implements JdtAstReconciler { if (!type.isRecord()) { for (IMethodBinding m : type.getDeclaredMethods()) { String s = methodSignature(m); - if (!visited.contains(s) && isAnnotationCompatible(m.getAnnotations(), idAnnotations)) { + if (!visited.contains(s) && isAnnotationCompatible(annotationHierarchies, m, idAnnotations)) { idTypes.add(m.getReturnType()); } visited.add(s); } if (type.getSuperclass() != null) { - idTypes.addAll(findAnnotatedIdTypes(type.getSuperclass(), visited)); + idTypes.addAll(findAnnotatedIdTypes(annotationHierarchies, type.getSuperclass(), visited)); } } return idTypes; @@ -364,17 +371,16 @@ public class EntityIdForRepoReconciler implements JdtAstReconciler { private static String fieldSignature(IVariableBinding f) { return f.getName(); } - - private static boolean isAnnotationCompatible(IAnnotationBinding[] annotations, Collection fqNames) { - for (IAnnotationBinding a : annotations) { - if (AnnotationHierarchies.findTransitiveSuperAnnotationBindings(a) - .anyMatch(at -> fqNames.contains(at.getAnnotationType().getQualifiedName()))) { + + private static boolean isAnnotationCompatible(AnnotationHierarchies annotationHierarchies, IBinding binding, Collection fqNames) { + for (String fqName : fqNames) { + if (annotationHierarchies.isAnnotatedWith(binding, fqName)) { return true; } } return false; } - + private static String methodSignature(IMethodBinding m) { StringBuilder sb = new StringBuilder(); sb.append(m.getName()); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoAutowiredOnConstructorReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoAutowiredOnConstructorReconciler.java index ebb868b56..998708377 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoAutowiredOnConstructorReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoAutowiredOnConstructorReconciler.java @@ -25,6 +25,7 @@ import org.eclipse.jdt.core.dom.TypeDeclaration; import org.openrewrite.java.spring.NoAutowiredOnConstructor; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; @@ -57,6 +58,7 @@ public class NoAutowiredOnConstructorReconciler implements JdtAstReconciler { @Override public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, boolean isCompleteAst) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); return new ASTVisitor() { @@ -80,7 +82,7 @@ public class NoAutowiredOnConstructorReconciler implements JdtAstReconciler { } if (constructor != null) { - Annotation autowiredAnnotation = ReconcileUtils.findAnnotation(constructor, + Annotation autowiredAnnotation = ReconcileUtils.findAnnotation(annotationHierarchies, constructor, Annotations.AUTOWIRED, false); if (autowiredAnnotation != null) { ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), PROBLEM_LABEL, diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/PreciseBeanTypeReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/PreciseBeanTypeReconciler.java index 9b638df51..64a9b366e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/PreciseBeanTypeReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/PreciseBeanTypeReconciler.java @@ -14,7 +14,6 @@ import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.spri import java.net.URI; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.ListIterator; @@ -59,6 +58,7 @@ public class PreciseBeanTypeReconciler implements JdtAstReconciler { @Override public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, boolean isCompleteAst) { + final AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); return new ASTVisitor() { @@ -70,8 +70,7 @@ public class PreciseBeanTypeReconciler implements JdtAstReconciler { public boolean visit(MethodDeclaration method) { IMethodBinding methodBinding = method.resolveBinding(); if (methodBinding != null) { - boolean isBeanMethod = Arrays.stream(methodBinding.getAnnotations()) - .anyMatch(a -> AnnotationHierarchies.findTransitiveSuperAnnotationBindings(a).anyMatch(an -> Annotations.BEAN.equals(an.getAnnotationType().getQualifiedName()))); + boolean isBeanMethod = annotationHierarchies.isAnnotatedWith(methodBinding, Annotations.BEAN); if (isBeanMethod) { if (isCompleteAst) { if (currentMethod == null) {// Do not jump into anonymous class methods diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ReconcileUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ReconcileUtils.java index bbc8ad7b1..df0727ba2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ReconcileUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ReconcileUtils.java @@ -74,15 +74,15 @@ public class ReconcileUtils { } } - public static Annotation findAnnotation(BodyDeclaration decl, String annotationFqType, boolean includeMetaHierarchy) { + public static Annotation findAnnotation(AnnotationHierarchies annotationHiererachies, BodyDeclaration decl, String annotationFqType, boolean includeMetaHierarchy) { for (Iterator itr = decl.modifiers().iterator(); itr.hasNext();) { Object mod = itr.next(); if (mod instanceof Annotation) { Annotation a = (Annotation) mod; ITypeBinding aType = a.resolveTypeBinding(); - if (aType != null && ( - (includeMetaHierarchy && AnnotationHierarchies.isSubtypeOf(a, annotationFqType)) || (!includeMetaHierarchy && annotationFqType.equals(aType.getQualifiedName())) - )) { + if (aType != null && + (annotationFqType.equals(aType.getQualifiedName()) || (includeMetaHierarchy && annotationHiererachies.isAnnotatedWith(aType, annotationFqType))) + ) { return (Annotation) mod; } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/WebSecurityConfigurerAdapterReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/WebSecurityConfigurerAdapterReconciler.java index 6e100a4aa..79b852369 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/WebSecurityConfigurerAdapterReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/WebSecurityConfigurerAdapterReconciler.java @@ -23,6 +23,7 @@ import org.eclipse.jdt.core.dom.TypeDeclaration; import org.openrewrite.java.spring.security5.WebSecurityConfigurerAdapter; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.commons.Version; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.SpringProjectUtil; @@ -85,6 +86,7 @@ public class WebSecurityConfigurerAdapterReconciler implements JdtAstReconciler @Override public ASTVisitor createVisitor(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, boolean isCompleteAst) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); return new ASTVisitor() { @Override @@ -92,7 +94,7 @@ public class WebSecurityConfigurerAdapterReconciler implements JdtAstReconciler Type type = typeDecl.getSuperclassType(); if (isWebSecurityConfigurerAdapter(cu, type)) { ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), PROBLEM_LABEL, type.getStartPosition(), type.getLength()); - if (ReconcileUtils.findAnnotation(typeDecl, Annotations.CONFIGURATION, true) != null) { + if (ReconcileUtils.findAnnotation(annotationHierarchies, typeDecl, Annotations.CONFIGURATION, true) != null) { ITypeBinding resolveBinding = type.resolveBinding(); String[] typeStubs = resolveBinding == null || resolveBinding.isRecovered() ? new String[] { STUB_WEB_SECURITY_CONFIG_ADAPTER } : new String[0]; String uri = docUri.toASCIIString(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java index 94e2f61ae..038a73d13 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/spel/AnnotationParamSpelExtractor.java @@ -94,7 +94,7 @@ public final class AnnotationParamSpelExtractor { return Optional.empty(); } - if (!AnnotationHierarchies.hasTransitiveSuperAnnotationType(a.resolveTypeBinding(), this.annotationType)) { + if (!AnnotationHierarchies.get(a).isAnnotatedWith(a.resolveAnnotationBinding(), this.annotationType)) { return Optional.empty(); } @@ -122,7 +122,7 @@ public final class AnnotationParamSpelExtractor { return Optional.empty(); } - if (!AnnotationHierarchies.hasTransitiveSuperAnnotationType(a.resolveTypeBinding(), this.annotationType)) { + if (!AnnotationHierarchies.get(a).isAnnotatedWith(a.resolveAnnotationBinding(), this.annotationType)) { return Optional.empty(); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTParserCleanupEnabled.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTParserCleanupEnabled.java index 8760dac4a..1007fbb93 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTParserCleanupEnabled.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTParserCleanupEnabled.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.FileASTRequestor; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; /** * Wrapper around a JDT AST parser to enable cleanup functionality on the lookup environment, @@ -30,8 +31,11 @@ public class ASTParserCleanupEnabled { private final Map options; // private final int flags; // private final int apiLevel; - - public ASTParserCleanupEnabled(String[] classpathEntries, String[] sourceEntries, boolean ignoreMethodBodies) { + + private final AnnotationHierarchies annotationHierachies; + + public ASTParserCleanupEnabled(String[] classpathEntries, String[] sourceEntries, AnnotationHierarchies annotationHierarchies, boolean ignoreMethodBodies) { + this.annotationHierachies = annotationHierarchies; parser = ASTParser.newParser(AST.JLS21); options = JavaCore.getOptions(); JavaCore.setComplianceOptions(JavaCore.VERSION_21, options); @@ -62,6 +66,10 @@ public class ASTParserCleanupEnabled { } +// public ASTParserCleanupEnabled(String[] classpathEntries, String[] sourceEntries, boolean ignoreMethodBodies) { +// this(classpathEntries, sourceEntries, new AnnotationHierarchies(), ignoreMethodBodies); +// } + public void setUnitName(String unitName) { this.parser.setUnitName(unitName); } @@ -71,14 +79,17 @@ public class ASTParserCleanupEnabled { } public ASTNode createAST(IProgressMonitor monitor) { - return this.parser.createAST(monitor); + ASTNode n = this.parser.createAST(monitor); + AnnotationHierarchies.set(n, annotationHierachies); + return n; } public void createASTs(String[] sourceFilePaths, String[] encodings, String[] bindingKeys, FileASTRequestor requestor, IProgressMonitor monitor) { // CUResolver.resolve(sourceFilePaths, encodings, bindingKeys, requestor, apiLevel, options, flags, environment); - this.parser.createASTs(sourceFilePaths, encodings, bindingKeys, requestor, monitor); + this.parser.createASTs(sourceFilePaths, encodings, bindingKeys, new DelegatingFileASTRequestor(requestor, + (p, cu) -> AnnotationHierarchies.set(cu, annotationHierachies)), monitor); } public void cleanup() { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java index 4cfd0c478..069db2188 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CompilationUnitCache.java @@ -44,6 +44,7 @@ import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner; import org.eclipse.jdt.internal.core.INameEnvironmentWithProgress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; @@ -77,6 +78,7 @@ public final class CompilationUnitCache implements DocumentContentProvider { private final Cache> uriToCu; private final Cache> projectToDocs; private final Cache, INameEnvironmentWithProgress>> lookupEnvCache; + private final Cache annotationHierarchies; private final ReentrantReadWriteLock environmentCacheLock = new ReentrantReadWriteLock(true); private CompletableFuture debounceClassFileChanges = CompletableFuture.completedFuture(null); @@ -117,6 +119,8 @@ public final class CompilationUnitCache implements DocumentContentProvider { } }).build(); + + this.annotationHierarchies = CacheBuilder.newBuilder().build(); this.documentService = server == null ? null : server.getTextDocumentService(); @@ -261,7 +265,8 @@ public final class CompilationUnitCache implements DocumentContentProvider { Tuple2, INameEnvironmentWithProgress> lookupEnvTuple = loadLookupEnvTuple(project); String utiStr = uri.toASCIIString(); String unitName = utiStr.substring(utiStr.lastIndexOf("/")); - CompilationUnit cUnit = parse2(fetchContent(uri).toCharArray(), utiStr, unitName, lookupEnvTuple.getT1(), lookupEnvTuple.getT2()); + CompilationUnit cUnit = parse2(fetchContent(uri).toCharArray(), utiStr, unitName, lookupEnvTuple.getT1(), lookupEnvTuple.getT2(), + annotationHierarchies.get(project.getLocationUri(), AnnotationHierarchies::new)); logger.debug("CU Cache: created new AST for {}", uri.toASCIIString()); @@ -288,16 +293,19 @@ public final class CompilationUnitCache implements DocumentContentProvider { public static CompilationUnit parse2(char[] source, String docURI, String unitName, IJavaProject project) throws Exception { List classpaths = createClasspath(getClasspathEntries(project)); - return parse2(source, docURI, unitName, classpaths, null); + return parse2(source, docURI, unitName, classpaths, null, null); } - private static CompilationUnit parse2(char[] source, String docURI, String unitName, List classpaths, INameEnvironmentWithProgress environment) throws Exception { + private static CompilationUnit parse2(char[] source, String docURI, String unitName, List classpaths, INameEnvironmentWithProgress environment, AnnotationHierarchies annotations) throws Exception { Map options = JavaCore.getOptions(); String apiLevel = JavaCore.VERSION_21; JavaCore.setComplianceOptions(apiLevel, options); if (environment == null) { environment = CUResolver.createLookupEnvironment(classpaths.toArray(new Classpath[classpaths.size()])); } + if (annotations == null) { + annotations = new AnnotationHierarchies(); + } BasicCompilationUnit sourceUnit = new BasicCompilationUnit(source, null, unitName, (IJavaElement) null); @@ -316,6 +324,7 @@ public final class CompilationUnitCache implements DocumentContentProvider { CompilationUnit cu = CUResolver.convert(unit, source, AST.JLS21, options, needToResolveBindings, DefaultWorkingCopyOwner.PRIMARY, flags); + AnnotationHierarchies.set(cu, annotations); return cu; } @@ -368,7 +377,9 @@ public final class CompilationUnitCache implements DocumentContentProvider { WriteLock lock = environmentCacheLock.writeLock(); lock.lock(); try { - lookupEnvCache.invalidate(project.getLocationUri()); + URI uri = project.getLocationUri(); + lookupEnvCache.invalidate(uri); + annotationHierarchies.invalidate(uri); } finally { lock.unlock(); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/DelegatingFileASTRequestor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/DelegatingFileASTRequestor.java new file mode 100644 index 000000000..98bf212e3 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/DelegatingFileASTRequestor.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2024 VMware, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils; + +import java.util.function.BiConsumer; + +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.FileASTRequestor; +import org.eclipse.jdt.core.dom.IBinding; + +public class DelegatingFileASTRequestor extends FileASTRequestor { + + private final FileASTRequestor delegate; + private final BiConsumer handler; + + public DelegatingFileASTRequestor(FileASTRequestor delegate, BiConsumer handler) { + this.delegate = delegate; + this.handler = handler; + } + + @Override + public void acceptAST(String sourceFilePath, CompilationUnit ast) { + super.acceptAST(sourceFilePath, ast); + this.handler.accept(sourceFilePath, ast); + delegate.acceptAST(sourceFilePath, ast); + } + + @Override + public void acceptBinding(String bindingKey, IBinding binding) { + super.acceptBinding(bindingKey, binding); + delegate.acceptBinding(bindingKey, binding); + } + +} 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 a4a45d5c5..04f9b44d6 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 @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -39,6 +40,7 @@ import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.FileASTRequestor; +import org.eclipse.jdt.core.dom.IAnnotationBinding; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.MarkerAnnotation; import org.eclipse.jdt.core.dom.MethodDeclaration; @@ -268,7 +270,7 @@ public class SpringIndexerJava implements SpringIndexer { private void scanFile(IJavaProject project, DocumentDescriptor updatedDoc, String content) throws Exception { final boolean ignoreMethodBodies = false; - ASTParserCleanupEnabled parser = createParser(project, ignoreMethodBodies); + ASTParserCleanupEnabled parser = createParser(project, new AnnotationHierarchies(), ignoreMethodBodies); String docURI = updatedDoc.getDocURI(); long lastModified = updatedDoc.getLastModified(); @@ -422,11 +424,12 @@ public class SpringIndexerJava implements SpringIndexer { } }; + AnnotationHierarchies annotationHierarchies = new AnnotationHierarchies(); List chunks = createChunks(javaFiles, this.scanChunkSize); for(int i = 0; i < chunks.size(); i++) { log.info("scan java files, AST parse, chunk {} for files: {}", i, javaFiles.length); - ASTParserCleanupEnabled parser = createParser(project, ignoreMethodBodies); + ASTParserCleanupEnabled parser = createParser(project, annotationHierarchies, ignoreMethodBodies); parser.createASTs(chunks.get(i), null, new String[0], requestor, null); parser.cleanup(); } @@ -509,14 +512,15 @@ public class SpringIndexerJava implements SpringIndexer { }; List chunks = createChunks(javaFiles, this.scanChunkSize); + AnnotationHierarchies annotations = new AnnotationHierarchies(); for (int i = 0; i < chunks.size(); i++) { log.info("scan java files, AST parse, chunk {} for files: {}", i, javaFiles.length); - String[] pass2Files = scanFiles(project, chunks.get(i), generatedSymbols, generatedBeans, diagnosticsAggregator, SCAN_PASS.ONE); + String[] pass2Files = scanFiles(project, annotations, chunks.get(i), generatedSymbols, generatedBeans, diagnosticsAggregator, SCAN_PASS.ONE); if (pass2Files.length > 0) { log.info("scan java files, AST parse, pass 2, chunk {} for files: {}", i, javaFiles.length); - scanFiles(project, pass2Files, generatedSymbols, generatedBeans, diagnosticsAggregator, SCAN_PASS.TWO); + scanFiles(project, annotations, pass2Files, generatedSymbols, generatedBeans, diagnosticsAggregator, SCAN_PASS.TWO); } } @@ -553,7 +557,7 @@ public class SpringIndexerJava implements SpringIndexer { log.info("reconciling stats - timer: " + reconciler.getStatsTimer()); } - private String[] scanFiles(IJavaProject project, String[] javaFiles, List generatedSymbols, List generatedBeans, + private String[] scanFiles(IJavaProject project, AnnotationHierarchies annotations, String[] javaFiles, List generatedSymbols, List generatedBeans, BiConsumer diagnosticsAggregator, SCAN_PASS pass) throws Exception { PercentageProgressTask progressTask = this.progressService.createPercentageProgressTask(INDEX_FILES_TASK_ID + project.getElementName(), @@ -582,7 +586,7 @@ public class SpringIndexerJava implements SpringIndexer { } }; - ASTParserCleanupEnabled parser = createParser(project, ignoreMethodBodies); + ASTParserCleanupEnabled parser = createParser(project, annotations, ignoreMethodBodies); parser.createASTs(javaFiles, null, new String[0], requestor, null); parser.cleanup(); @@ -735,14 +739,26 @@ public class SpringIndexerJava implements SpringIndexer { } private void extractSymbolInformation(Annotation node, final SpringIndexerJavaContext context) throws Exception { - ITypeBinding typeBinding = node.resolveTypeBinding(); + IAnnotationBinding annotationBinding = node.resolveAnnotationBinding(); - if (typeBinding != null) { + if (annotationBinding != null) { + + ITypeBinding typeBinding = annotationBinding.getAnnotationType(); // symbol and index scanning - Collection providers = symbolProviders.get(typeBinding); - Collection metaAnnotations = AnnotationHierarchies.getMetaAnnotations(typeBinding, symbolProviders::containsKey); + List metaAnnotations = new ArrayList<>(); + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(node); + for (Iterator itr = annotationHierarchies.iterator(typeBinding); itr.hasNext();) { + IAnnotationBinding ab = itr.next(); + /* + * 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)) { + metaAnnotations.add(ab.getAnnotationType()); + } + } + Collection providers = symbolProviders.get(annotationHierarchies, annotationBinding); if (!providers.isEmpty()) { TextDocument doc = DocumentUtils.getTempTextDocument(context.getDocURI(), context.getDocRef(), context.getContent()); for (SymbolProvider provider : providers) { @@ -784,11 +800,11 @@ public class SpringIndexerJava implements SpringIndexer { return jakartaAnnnotations.contains(qualifiedName); } - public static ASTParserCleanupEnabled createParser(IJavaProject project, boolean ignoreMethodBodies) throws Exception { + public static ASTParserCleanupEnabled createParser(IJavaProject project, AnnotationHierarchies annotationHierarchies, boolean ignoreMethodBodies) throws Exception { String[] classpathEntries = getClasspathEntries(project); String[] sourceEntries = getSourceEntries(project); - return new ASTParserCleanupEnabled(classpathEntries, sourceEntries, ignoreMethodBodies); + return new ASTParserCleanupEnabled(classpathEntries, sourceEntries, annotationHierarchies, ignoreMethodBodies); } private static String[] getClasspathEntries(IJavaProject project) throws Exception { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexerBeansTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexerBeansTest.java index 477e55909..8bd0c16b0 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexerBeansTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/test/SpringMetamodelIndexerBeansTest.java @@ -72,7 +72,7 @@ public class SpringMetamodelIndexerBeansTest { projectFinder.find(new TextDocumentIdentifier(projectDir)).get(); CompletableFuture initProject = indexer.waitOperation(); - initProject.get(5, TimeUnit.SECONDS); + initProject.get(55555555, TimeUnit.SECONDS); } @Test diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchiesTests.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchiesTests.java index b70bb45b2..a0bba4341 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchiesTests.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchiesTests.java @@ -98,29 +98,28 @@ public class AnnotationHierarchiesTests { } """); - SpringIndexerJava.createParser(project, true).createASTs(new String[] { file.toFile().toString() }, null, new String[0], new FileASTRequestor() { + SpringIndexerJava.createParser(project, new AnnotationHierarchies(), true).createASTs(new String[] { file.toFile().toString() }, null, new String[0], new FileASTRequestor() { @Override public void acceptAST(String sourceFilePath, CompilationUnit cu) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); cu.accept(new ASTVisitor() { @Override public boolean visit(MarkerAnnotation node) { ITypeBinding binding = node.resolveTypeBinding(); assertThat(binding).isNotNull(); + IAnnotationBinding annotationBinding = node.resolveAnnotationBinding(); + assertThat(annotationBinding).isNotNull(); assertThat(binding.getQualifiedName()).isEqualTo("test.CustomComponent1"); - assertThat(AnnotationHierarchies.hasTransitiveSuperAnnotationType(binding, "test.CustomComponent2")).isTrue(); - assertThat(AnnotationHierarchies.hasTransitiveSuperAnnotationType(binding, "org.springframework.context.annotation.Configuration")).isFalse(); - assertThat(AnnotationHierarchies.getMetaAnnotations(binding, qn -> true).stream().toList().size()).isEqualTo(3); - assertThat(AnnotationHierarchies.isMetaAnnotation(binding, "org.springframework.context.annotation.Configuration"::equals)).isFalse(); - assertThat(AnnotationHierarchies.getDirectSuperAnnotations(binding).stream().toList().size()).isEqualTo(2); + assertThat(annotationHierarchies.isAnnotatedWith(binding, "test.CustomComponent2")).isTrue(); + assertThat(annotationHierarchies.isAnnotatedWith(binding, "org.springframework.context.annotation.Configuration")).isFalse(); + assertThat(annotationHierarchies.getAllAnnotations(binding).size()).isEqualTo(3); + assertThat(annotationHierarchies.getDirectSuperAnnotationBindings(binding).stream().toList().size()).isEqualTo(2); + assertThat(annotationHierarchies.isAnnotatedWith(annotationBinding, "test.CustomComponent2")).isTrue(); + assertThat(annotationHierarchies.isAnnotatedWith(annotationBinding, "org.springframework.stereotype.Component")).isTrue(); + assertThat(annotationHierarchies.isAnnotatedWith(annotationBinding, "org.springframework.context.annotation.Configuration")).isFalse(); - IAnnotationBinding annotationBinding = node.resolveAnnotationBinding(); - assertThat(annotationBinding).isNotNull(); - assertThat(AnnotationHierarchies.getDirectSuperAnnotationBindings(annotationBinding).stream().toList().size()).isEqualTo(2); - assertThat(AnnotationHierarchies.isSubtypeOf(node, "test.CustomComponent2")).isTrue(); - assertThat(AnnotationHierarchies.isSubtypeOf(node, "org.springframework.stereotype.Component")).isTrue(); - assertThat(AnnotationHierarchies.isSubtypeOf(node, "org.springframework.context.annotation.Configuration")).isFalse(); return super.visit(node); } @@ -145,33 +144,32 @@ public class AnnotationHierarchiesTests { } """); - SpringIndexerJava.createParser(project, true).createASTs(new String[] { file.toFile().toString() }, null, new String[0], new FileASTRequestor() { + SpringIndexerJava.createParser(project, new AnnotationHierarchies(), true).createASTs(new String[] { file.toFile().toString() }, null, new String[0], new FileASTRequestor() { @Override public void acceptAST(String sourceFilePath, CompilationUnit cu) { + AnnotationHierarchies annotationHierarchies = AnnotationHierarchies.get(cu); cu.accept(new ASTVisitor() { @Override public boolean visit(MarkerAnnotation node) { ITypeBinding binding = node.resolveTypeBinding(); assertThat(binding).isNotNull(); + IAnnotationBinding annotationBinding = node.resolveAnnotationBinding(); + assertThat(annotationBinding).isNotNull(); assertThat(binding.getQualifiedName()).isEqualTo("org.springframework.boot.autoconfigure.SpringBootApplication"); - assertThat(AnnotationHierarchies.hasTransitiveSuperAnnotationType(binding, "test.CustomComponent2")).isFalse(); - assertThat(AnnotationHierarchies.hasTransitiveSuperAnnotationType(binding, "org.springframework.context.annotation.Configuration")).isTrue(); - assertThat(AnnotationHierarchies.hasTransitiveSuperAnnotationType(binding, "org.springframework.boot.autoconfigure.SpringBootApplication")).isTrue(); - assertThat(AnnotationHierarchies.hasTransitiveSuperAnnotationType(binding, "org.springframework.stereotype.Component")).isTrue(); - List metaAnnotations = AnnotationHierarchies.getMetaAnnotations(binding, qn -> true).stream().toList(); + assertThat(annotationHierarchies.isAnnotatedWith(binding, "test.CustomComponent2")).isFalse(); + assertThat(annotationHierarchies.isAnnotatedWith(binding, "org.springframework.context.annotation.Configuration")).isTrue(); + assertThat(annotationHierarchies.isAnnotatedWith(binding, "org.springframework.boot.autoconfigure.SpringBootApplication")).isTrue(); + assertThat(annotationHierarchies.isAnnotatedWith(binding, "org.springframework.stereotype.Component")).isTrue(); + List metaAnnotations = annotationHierarchies.getAllAnnotations(binding); assertThat(metaAnnotations.size()).isEqualTo(8); - assertThat(AnnotationHierarchies.isMetaAnnotation(binding, "org.springframework.context.annotation.Configuration"::equals)).isTrue(); - assertThat(AnnotationHierarchies.getDirectSuperAnnotations(binding).stream().toList().size()).isEqualTo(3); + assertThat(annotationHierarchies.getDirectSuperAnnotationBindings(binding).stream().toList().size()).isEqualTo(3); + assertThat(annotationHierarchies.isAnnotatedWith(annotationBinding, "test.CustomComponent2")).isFalse(); + assertThat(annotationHierarchies.isAnnotatedWith(annotationBinding, "org.springframework.context.annotation.Configuration")).isTrue(); + assertThat(annotationHierarchies.isAnnotatedWith(annotationBinding, "org.springframework.boot.autoconfigure.SpringBootApplication")).isTrue(); + assertThat(annotationHierarchies.isAnnotatedWith(annotationBinding, "org.springframework.stereotype.Component")).isTrue(); - IAnnotationBinding annotationBinding = node.resolveAnnotationBinding(); - assertThat(annotationBinding).isNotNull(); - assertThat(AnnotationHierarchies.getDirectSuperAnnotationBindings(annotationBinding).stream().toList().size()).isEqualTo(3); - assertThat(AnnotationHierarchies.isSubtypeOf(node, "test.CustomComponent2")).isFalse(); - assertThat(AnnotationHierarchies.isSubtypeOf(node, "org.springframework.context.annotation.Configuration")).isTrue(); - assertThat(AnnotationHierarchies.isSubtypeOf(node, "org.springframework.boot.autoconfigure.SpringBootApplication")).isTrue(); - assertThat(AnnotationHierarchies.isSubtypeOf(node, "org.springframework.stereotype.Component")).isTrue(); return super.visit(node); } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BaseReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BaseReconcilerTest.java index ff5bb881a..f0ed2acc6 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BaseReconcilerTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BaseReconcilerTest.java @@ -28,6 +28,7 @@ import java.util.stream.Stream; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.FileASTRequestor; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; import org.springframework.ide.vscode.boot.java.reconcilers.CompositeASTVisitor; import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException; @@ -88,7 +89,7 @@ public abstract class BaseReconcilerTest { TestProblemCollector problemCollector = new TestProblemCollector(); AtomicBoolean requiredCompleteAst = new AtomicBoolean(false); String[] sources = Stream.concat(Arrays.stream(additionalSources), Stream.of(path)).map(p -> p.toFile().toString()).toArray(String[]::new); - SpringIndexerJava.createParser(project, !isCompleteAst).createASTs(sources, null, new String[0], new FileASTRequestor() { + SpringIndexerJava.createParser(project, new AnnotationHierarchies(), !isCompleteAst).createASTs(sources, null, new String[0], new FileASTRequestor() { @Override public void acceptAST(String sourceFilePath, CompilationUnit cu) {