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 9ffe85805..1d6b7bef8 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 @@ -78,25 +78,7 @@ public abstract class AnnotationHierarchies { public static boolean hasTransitiveSuperAnnotationType(ITypeBinding typeBinding, String annotationType) { synchronized(lock) { - if (typeBinding != null && annotationType != null) { - String qname = typeBinding.getQualifiedName(); - -// log.info("CHECK ANNOTATION TYPE: " + qname + " / " + annotationType); - - if (annotationType.equals(qname)) { - return true; - } - else { - Collection directSuperAnnotations = getDirectSuperAnnotations(typeBinding); - for (ITypeBinding superAnnotationBinding : directSuperAnnotations) { - if (hasTransitiveSuperAnnotationType(superAnnotationBinding, annotationType)) { - return true; - } - } - } - } - - return false; + return isMetaAnnotation(typeBinding, annotationType::equals); } } 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 new file mode 100644 index 000000000..d700a9b12 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationHierarchiesTests.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom, 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: + * Broadcom, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.annotations; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.eclipse.jdt.core.dom.ASTVisitor; +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.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJava; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; + +public class AnnotationHierarchiesTests { + + private List createdFiles = new ArrayList<>(); + + private Path createFile(String projectName, String packageName, String name, String content) throws Exception { + Path projectPath = Paths.get(getClass().getResource("/test-projects/" + projectName).toURI()); + Path filePath = projectPath.resolve("src/main/java").resolve(packageName.replace('.', '/')).resolve(name); + Files.createDirectories(filePath.getParent()); + createdFiles.add(Files.createFile(filePath)); + Files.write(filePath, content.getBytes(StandardCharsets.UTF_8)); + return filePath; + } + + private void clearTestFiles() { + for (Iterator itr = createdFiles.iterator(); itr.hasNext();) { + Path path = itr.next(); + try { + Files.delete(path); + itr.remove(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @AfterEach + void tearDown() { + clearTestFiles(); + } + + @Test + void circularAnnotations() throws Exception { + String projectName = "test-spring-validations"; + IJavaProject project = ProjectsHarness.INSTANCE.mavenProject(projectName); + createFile(projectName, "test", "CustomComponent1.java", """ + package test; + + import org.springframework.stereotype.Component; + + @Component + @CustomComponent2 + public @interface CustomComponent1 { + + } + """); + createFile(projectName, "test", "CustomComponent2.java", """ + package test; + + import org.springframework.stereotype.Component; + + @Component + @CustomComponent1 + public @interface CustomComponent2 { + + } + """); + Path file = createFile(projectName, "test", "MyComponent.java", """ + package test; + + @CustomComponent1 + public class MyComponent { + + } + """); + + SpringIndexerJava.createParser(project, true).createASTs(new String[] { file.toFile().toString() }, null, new String[0], new FileASTRequestor() { + @Override + public void acceptAST(String sourceFilePath, CompilationUnit cu) { + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(MarkerAnnotation node) { + ITypeBinding binding = node.resolveTypeBinding(); + assertThat(binding).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.isMetaAnnotation(binding, "test.CustomComponent2"::equals)).isTrue(); + assertThat(AnnotationHierarchies.isMetaAnnotation(binding, "org.springframework.context.annotation.Configuration"::equals)).isFalse(); + assertThat(AnnotationHierarchies.getDirectSuperAnnotations(binding).stream().toList().size()).isEqualTo(2); + + 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); + } + + }); + } + }, null); + + } +}