diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansIndexer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansIndexer.java index 5d3597497b..34cddedbe2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansIndexer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansIndexer.java @@ -11,7 +11,6 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Set; @@ -78,8 +77,7 @@ public static void indexBeanMethod(SpringIndexElement parentNode, Annotation nod InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(method, doc); - Set supertypes = new HashSet<>(); - ASTUtils.findSupertypes(beanType, supertypes); + Set supertypes = ASTUtils.findSupertypes(beanType); Collection annotationsOnMethod = ASTUtils.getAnnotations(method); AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnMethod, doc); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java index d5abeae006..342a760957 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java @@ -11,7 +11,6 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Collection; -import java.util.HashSet; import java.util.Set; import org.eclipse.jdt.core.dom.ASTNode; @@ -101,8 +100,7 @@ private void indexFunctionBeans(TypeDeclaration typeDeclaration, SpringIndexerJa context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol)); ITypeBinding concreteBeanType = typeDeclaration.resolveBinding(); - Set supertypes = new HashSet<>(); - ASTUtils.findSupertypes(concreteBeanType, supertypes); + Set supertypes = ASTUtils.findSupertypes(concreteBeanType); Collection annotationsOnTypeDeclaration = ASTUtils.getAnnotations(typeDeclaration); AnnotationMetadata[] annotations = ASTUtils.getAnnotationsMetadata(annotationsOnTypeDeclaration, doc); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java index a7f59aa500..f7dea414f7 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ComponentSymbolProvider.java @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -45,7 +44,6 @@ import org.springframework.ide.vscode.boot.java.events.EventPublisherIndexElement; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; import org.springframework.ide.vscode.boot.java.reconcilers.NotRegisteredBeansReconciler; -import org.springframework.ide.vscode.boot.java.reconcilers.ReconcileUtils; import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException; import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingIndexer; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; @@ -114,8 +112,7 @@ private void createSymbol(TypeDeclaration type, Annotation node, ITypeBinding an InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(type, doc); - Set supertypes = new HashSet<>(); - ASTUtils.findSupertypes(beanType, supertypes); + Set supertypes = ASTUtils.findSupertypes(beanType); Collection annotationsOnType = ASTUtils.getAnnotations(type); @@ -177,8 +174,7 @@ private void createSymbol(RecordDeclaration record, Annotation node, ITypeBindin InjectionPoint[] injectionPoints = DefaultValues.EMPTY_INJECTION_POINTS; - Set supertypes = new HashSet<>(); - ASTUtils.findSupertypes(beanType, supertypes); + Set supertypes = ASTUtils.findSupertypes(beanType); Collection annotationsOnType = ASTUtils.getAnnotations(record); @@ -322,8 +318,7 @@ public boolean visit(MethodInvocation methodInvocation) { Location location; location = new Location(doc.getUri(), nodeRegion.asRange()); - Set typesFromhierarchy = new HashSet<>(); - ASTUtils.findSupertypes(eventTypeBinding, typesFromhierarchy); + Set typesFromhierarchy = ASTUtils.findSupertypes(eventTypeBinding); EventPublisherIndexElement eventPublisherIndexElement = new EventPublisherIndexElement(eventTypeBinding.getQualifiedName(), location, typesFromhierarchy); component.addChild(eventPublisherIndexElement); @@ -364,7 +359,7 @@ private void indexAotProcessors(TypeDeclaration typeDeclaration, SpringIndexerJa ITypeBinding typeBinding = typeDeclaration.resolveBinding(); if (typeBinding == null) return; - if (ReconcileUtils.implementsAnyType(NotRegisteredBeansReconciler.AOT_BEANS, typeBinding)) { + if (ASTUtils.isAnyTypeInHierarchy(typeBinding, NotRegisteredBeansReconciler.AOT_BEANS)) { String type = typeBinding.getQualifiedName(); String docUri = context.getDocURI(); @@ -594,8 +589,7 @@ public void createBean(SpringIndexElement parentNode, String beanName, String be context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), symbol)); InjectionPoint[] injectionPoints = DefaultValues.EMPTY_INJECTION_POINTS; - Set supertypes = new HashSet<>(); - ASTUtils.findSupertypes(beanTypeBinding, supertypes); + Set supertypes = ASTUtils.findSupertypes(beanTypeBinding); AnnotationMetadata[] annotations = DefaultValues.EMPTY_ANNOTATIONS; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConfigurationPropertiesSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConfigurationPropertiesSymbolProvider.java index 490768424e..71f9b9f975 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConfigurationPropertiesSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ConfigurationPropertiesSymbolProvider.java @@ -12,7 +12,6 @@ import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -96,8 +95,7 @@ protected void createSymbolForType(AbstractTypeDeclaration type, Annotation node InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(type, doc); - Set supertypes = new HashSet<>(); - ASTUtils.findSupertypes(typeBinding, supertypes); + Set supertypes = ASTUtils.findSupertypes(typeBinding); Collection annotationsOnType = ASTUtils.getAnnotations(type); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/FeignClientSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/FeignClientSymbolProvider.java index 597e9fd29a..56664ed33b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/FeignClientSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/FeignClientSymbolProvider.java @@ -12,7 +12,6 @@ import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -82,8 +81,7 @@ private Two createSymbol(Annotation node, ITypeBinding an InjectionPoint[] injectionPoints = ASTUtils.findInjectionPoints(type, doc); - Set supertypes = new HashSet<>(); - ASTUtils.findSupertypes(beanType, supertypes); + Set supertypes = ASTUtils.findSupertypes(beanType); Collection annotationsOnType = ASTUtils.getAnnotations(type); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java index 0a878c6b24..26373b8c75 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositorySymbolProvider.java @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Set; @@ -81,8 +80,7 @@ public void addSymbols(TypeDeclaration typeDeclaration, SpringIndexerJavaContext ITypeBinding concreteBeanTypeBindung = typeDeclaration.resolveBinding(); - Set supertypes = new HashSet<>(); - ASTUtils.findSupertypes(concreteBeanTypeBindung, supertypes); + Set supertypes = ASTUtils.findSupertypes(concreteBeanTypeBindung); String concreteRepoType = concreteBeanTypeBindung.getQualifiedName(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanPostProcessingIgnoreInAotReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanPostProcessingIgnoreInAotReconciler.java index f976dd9075..865f3a4f61 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanPostProcessingIgnoreInAotReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanPostProcessingIgnoreInAotReconciler.java @@ -14,6 +14,7 @@ import java.net.URI; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jdt.core.dom.ASTVisitor; @@ -23,6 +24,7 @@ import org.eclipse.jdt.core.dom.ReturnStatement; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; @@ -108,7 +110,7 @@ public boolean visit(ReturnStatement node) { } private static boolean isApplicable(ITypeBinding type) { - return ReconcileUtils.implementsType(RUNTIME_BEAN_POST_PROCESSOR, type) && ReconcileUtils.implementsType(COMPILE_BEAN_POST_PROCESSOR, type); + return ASTUtils.areAllTypesInHierarchy(type, Set.of(RUNTIME_BEAN_POST_PROCESSOR, COMPILE_BEAN_POST_PROCESSOR)); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NotRegisteredBeansReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NotRegisteredBeansReconciler.java index 46fbe7bfef..379435ce0b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NotRegisteredBeansReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NotRegisteredBeansReconciler.java @@ -30,6 +30,7 @@ import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; 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; @@ -83,7 +84,7 @@ public boolean visit(TypeDeclaration node) { if (!node.isInterface() && !Modifier.isAbstract(node.getModifiers())) { ITypeBinding type = node.resolveBinding(); - if (type != null && ReconcileUtils.implementsAnyType(AOT_BEANS, type)) { + if (type != null && ASTUtils.isAnyTypeInHierarchy(type, AOT_BEANS)) { // // reconcile AOT Proceesor itself 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 7066bec23e..273b043f93 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 @@ -148,34 +148,6 @@ public boolean visit(SimpleType node) { return typeUsed.get(); } - public static boolean implementsType(String fqName, ITypeBinding type) { - if (fqName.equals(type.getQualifiedName())) { - return true; - } else { - for (ITypeBinding t : type.getInterfaces()) { - if (implementsType(fqName, t)) { - return true; - } - } - } - return false; - } - - public static boolean implementsAnyType(Collection fqNames, ITypeBinding type) { - if (fqNames.contains(type.getQualifiedName())) { - return true; - } else { - for (ITypeBinding t : type.getInterfaces()) { - if (implementsAnyType(fqNames, t)) { - return true; - } - } - } - return false; - } - - - public static String getSimpleName(String fqName) { int idx = fqName.lastIndexOf('.'); if (idx >= 0 && idx < fqName.length() - 1) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java index 5f4c20ee5c..3371bc0bd9 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java @@ -10,13 +10,18 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.utils; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; +import java.util.Queue; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Stream; @@ -394,36 +399,14 @@ public static boolean isAbstractClass(TypeDeclaration typeDeclaration) { } public static ITypeBinding findInTypeHierarchy(ITypeBinding resolvedType, Set typesToCheck) { - ITypeBinding[] interfaces = resolvedType.getInterfaces(); - - for (ITypeBinding resolvedInterface : interfaces) { - String simplifiedType = null; - - if (resolvedInterface.isParameterizedType()) { - simplifiedType = resolvedInterface.getBinaryName(); - } - else { - simplifiedType = resolvedInterface.getQualifiedName(); + for (Iterator itr = getHierarchyTypesBreadthFirstIterator(resolvedType); itr.hasNext();) { + ITypeBinding b = itr.next(); + String fqn = b.isParameterizedType() ? b.getBinaryName() : b.getQualifiedName(); + if (typesToCheck.contains(fqn)) { + return b; } - - if (typesToCheck.contains(simplifiedType)) { - return resolvedInterface; - } - else { - ITypeBinding result = findInTypeHierarchy(resolvedInterface, typesToCheck); - if (result != null) { - return result; - } - } - } - - ITypeBinding superclass = resolvedType.getSuperclass(); - if (superclass != null) { - return findInTypeHierarchy(superclass, typesToCheck); - } - else { - return null; } + return null; } public static Optional getImportsEdit(CompilationUnit cu, Collection imprts, IDocument doc) { @@ -462,41 +445,88 @@ public static Optional getImportsEdit(CompilationUnit cu, Collect // return result; // } // - public static void findSupertypes(ITypeBinding binding, Set supertypesCollector) { - - // interfaces - ITypeBinding[] interfaces = binding.getInterfaces(); - for (ITypeBinding resolvedInterface : interfaces) { - String simplifiedType = null; - if (resolvedInterface.isParameterizedType()) { - simplifiedType = resolvedInterface.getBinaryName(); - } - else { - simplifiedType = resolvedInterface.getQualifiedName(); + /** + * Returns only the super types without the type itself. + * @param binding + * @return + */ + public static Set findSupertypes(ITypeBinding binding) { + Set supertypesCollector = new HashSet<>(); + for (Iterator itr = getHierarchyTypesFqNamesBreadthFirstIterator(binding); itr.hasNext();) { + supertypesCollector.add(itr.next()); + } + supertypesCollector.remove(binding.isParameterizedType() ? binding.getBinaryName() : binding.getQualifiedName()); + return supertypesCollector; + } + + public static boolean isAnyTypeInHierarchy(ITypeBinding binding, Collection typeFqns) { + for (Iterator itr = getHierarchyTypesFqNamesBreadthFirstIterator(binding); itr.hasNext();) { + String fqn = itr.next(); + if (typeFqns.contains(fqn)) { + return true; } - - if (simplifiedType != null) { - supertypesCollector.add(simplifiedType); - findSupertypes(resolvedInterface, supertypesCollector); + } + return false; + } + + public static boolean areAllTypesInHierarchy(ITypeBinding binding, Collection typeFqns) { + HashSet notFound = new HashSet<>(typeFqns); + for (Iterator itr = getHierarchyTypesFqNamesBreadthFirstIterator(binding); itr.hasNext() && !notFound.isEmpty();) { + notFound.remove(itr.next()); + } + return notFound.isEmpty(); + } + + private static void enqueueSuperTypes(Queue q, ITypeBinding t) { + for (ITypeBinding b : t.getInterfaces()) { + if (b != null) { + q.add(b); } } - - // superclasses - ITypeBinding superclass = binding.getSuperclass(); - if (superclass != null) { - String simplifiedType = null; - if (superclass.isParameterizedType()) { - simplifiedType = superclass.getBinaryName(); + if (t.getSuperclass() != null) { + q.add(t.getSuperclass()); + } + } + + public static Iterator getHierarchyTypesBreadthFirstIterator(ITypeBinding binding) { + final Queue q = new ArrayDeque<>(10); + q.add(binding); + return new Iterator() { + + @Override + public boolean hasNext() { + return !q.isEmpty(); } - else { - simplifiedType = superclass.getQualifiedName(); + + @Override + public ITypeBinding next() { + ITypeBinding t = q.poll(); + if (t == null) { + throw new NoSuchElementException(); + } + enqueueSuperTypes(q, t); + return t; } - if (simplifiedType != null) { - supertypesCollector.add(simplifiedType); - findSupertypes(superclass, supertypesCollector); + }; + } + + public static Iterator getHierarchyTypesFqNamesBreadthFirstIterator(ITypeBinding binding) { + Iterator itr = getHierarchyTypesBreadthFirstIterator(binding); + return new Iterator() { + + @Override + public boolean hasNext() { + return itr.hasNext(); } - } + + @Override + public String next() { + ITypeBinding b = itr.next(); + return b.isParameterizedType() ? b.getBinaryName() : b.getQualifiedName(); + } + + }; } public static InjectionPoint[] findInjectionPoints(MethodDeclaration method, TextDocument doc) throws BadLocationException { diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/ASTUtilsTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/ASTUtilsTest.java new file mode 100644 index 0000000000..ada92c546e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/ASTUtilsTest.java @@ -0,0 +1,284 @@ +/******************************************************************************* + * Copyright (c) 2025 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.utils.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +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 java.util.Set; +import java.util.function.Consumer; + +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.ITypeBinding; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; +import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJava; +import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; + +public class ASTUtilsTest { + + private List createdFiles = new ArrayList<>(); + + private final String projectName = "test-spring-validations"; + + private MavenJavaProject project; + + private Path mySimpleMain; + private Path myComponent; + + + @BeforeEach + void setup() throws Exception { + this.project = ProjectsHarness.INSTANCE.mavenProject(projectName); + createTestFiles(); + } + + @AfterEach + void tearDown() { + clearTestFiles(); + } + + @Test + void testTypeHierarchyIteratorSimpleClass() throws Exception { + runTestsAgainstTypeDeclaration(mySimpleMain, (type) -> { + Iterator iter = ASTUtils.getHierarchyTypesBreadthFirstIterator(type.resolveBinding()); + assertNotNull(iter); + + assertEquals("test.MySimpleMain", iter.next().getQualifiedName()); + assertEquals("java.lang.Object", iter.next().getQualifiedName()); + assertFalse(iter.hasNext()); + }); + } + + @Test + void testSupertypesForSimpleClass() throws Exception { + runTestsAgainstTypeDeclaration(mySimpleMain, (type) -> { + Set supertypes = ASTUtils.findSupertypes(type.resolveBinding()); + + assertEquals(1, supertypes.size()); + assertTrue(supertypes.contains("java.lang.Object")); + }); + } + + @Test + void testIsAnyTypeInHierarchyForSimpleClass() throws Exception { + runTestsAgainstTypeDeclaration(mySimpleMain, (type) -> { + assertTrue(ASTUtils.isAnyTypeInHierarchy(type.resolveBinding(), List.of("java.lang.Object"))); + assertTrue(ASTUtils.isAnyTypeInHierarchy(type.resolveBinding(), List.of("java.lang.Object", "java.io.Serializable"))); + assertFalse(ASTUtils.isAnyTypeInHierarchy(type.resolveBinding(), List.of("java.io.Serializable"))); + assertFalse(ASTUtils.isAnyTypeInHierarchy(type.resolveBinding(), List.of())); + + assertTrue(ASTUtils.isAnyTypeInHierarchy(type.resolveBinding(), List.of("test.MySimpleMain"))); + }); + } + + @Test + void testAreAllTypesInHierarchyForSimpleClass() throws Exception { + runTestsAgainstTypeDeclaration(mySimpleMain, (type) -> { + assertTrue(ASTUtils.areAllTypesInHierarchy(type.resolveBinding(), List.of("java.lang.Object"))); + assertFalse(ASTUtils.areAllTypesInHierarchy(type.resolveBinding(), List.of("java.lang.Object", "java.io.Serializable"))); + assertTrue(ASTUtils.areAllTypesInHierarchy(type.resolveBinding(), List.of())); + + assertTrue(ASTUtils.areAllTypesInHierarchy(type.resolveBinding(), List.of("test.MySimpleMain"))); + }); + } + + @Test + void testTypeHierarchyIteratorWithSuperclassesAndInterfaces() throws Exception { + runTestsAgainstTypeDeclaration(myComponent, (type) -> { + Iterator iter = ASTUtils.getHierarchyTypesBreadthFirstIterator(type.resolveBinding()); + assertNotNull(iter); + + assertEquals("test.MyComponent", iter.next().getQualifiedName()); + assertEquals("test.MyInterface", iter.next().getQualifiedName()); + assertEquals("test.MySuperclass", iter.next().getQualifiedName()); + assertEquals("test.MySuperInterface", iter.next().getQualifiedName()); + assertEquals("test.MySuperclassInterface", iter.next().getQualifiedName()); + assertEquals("java.lang.Object", iter.next().getQualifiedName()); + assertFalse(iter.hasNext()); + }); + } + + @Test + void testTypeHierarchyIteratorWithFullyQualifiedTypeNames() throws Exception { + runTestsAgainstTypeDeclaration(myComponent, (type) -> { + Iterator iter = ASTUtils.getHierarchyTypesFqNamesBreadthFirstIterator(type.resolveBinding()); + assertNotNull(iter); + + assertEquals("test.MyComponent", iter.next()); + assertEquals("test.MyInterface", iter.next()); + assertEquals("test.MySuperclass", iter.next()); + assertEquals("test.MySuperInterface", iter.next()); + assertEquals("test.MySuperclassInterface", iter.next()); + assertEquals("java.lang.Object", iter.next()); + assertFalse(iter.hasNext()); + }); + } + + @Test + void testCircularTypeHierarchy() throws Exception { + createFile(projectName, "test", "Start.java", """ + package test; + public class Start extends Third { + } + """); + + createFile(projectName, "test", "Second.java", """ + package test; + public class Second extends Start { + } + """); + + Path third = createFile(projectName, "test", "Third.java", """ + package test; + public class Third extends Second { + } + """); + + runTestsAgainstTypeDeclaration(third, (type) -> { + assertFalse(ASTUtils.isAnyTypeInHierarchy(type.resolveBinding(), List.of("java.io.Serializable"))); + assertTrue(ASTUtils.isAnyTypeInHierarchy(type.resolveBinding(), List.of("test.Start"))); + assertTrue(ASTUtils.areAllTypesInHierarchy(type.resolveBinding(), List.of("test.Start", "test.Second", "test.Third"))); + }); + + } + + @Test + void testInterfaceAppearsMultipleTimesInHierarchy() throws Exception { + createFile(projectName, "test", "Start.java", """ + package test; + public class Start implements TestInterface { + } + """); + + Path second = createFile(projectName, "test", "Second.java", """ + package test; + public class Second extends Start implements TestInterface { + } + """); + + createFile(projectName, "test", "TestInterface.java", """ + package test; + public interface TestInterface { + } + """); + + runTestsAgainstTypeDeclaration(second, (type) -> { + Iterator iter = ASTUtils.getHierarchyTypesFqNamesBreadthFirstIterator(type.resolveBinding()); + assertNotNull(iter); + + assertEquals("test.Second", iter.next()); + assertEquals("test.TestInterface", iter.next()); + assertEquals("test.Start", iter.next()); + assertEquals("test.TestInterface", iter.next()); + assertEquals("java.lang.Object", iter.next()); + assertFalse(iter.hasNext()); + }); + + } + + private void runTestsAgainstTypeDeclaration(Path file, Consumer test) throws Exception { + SpringIndexerJava.createParser(this.project, new AnnotationHierarchies(), 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(TypeDeclaration type) { + test.accept(type); + return super.visit(type); + } + + }); + } + }, null); + } + + private void createTestFiles() throws Exception { + this.mySimpleMain = createFile(projectName, "test", "MySimpleMain.java", """ + package test; + public class MySimpleMain { + } + """); + + createFile(projectName, "test", "MySuperclass.java", """ + package test; + public class MySuperclass implements MySuperclassInterface { + } + """); + + createFile(projectName, "test", "MySuperclassInterface.java", """ + package test; + public interface MySuperclassInterface { + } + """); + + createFile(projectName, "test", "MyInterface.java", """ + package test; + public interface MyInterface extends MySuperInterface { + } + """); + + createFile(projectName, "test", "MySuperInterface.java", """ + package test; + public interface MySuperInterface { + } + """); + + this.myComponent = createFile(projectName, "test", "MyComponent.java", """ + package test; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + public class MyComponent extends MySuperclass implements MyInterface { + } + """); + } + + 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(); + } + } + } + +}