From 061f07ca9341814fd61961c038877b1371a38c0a Mon Sep 17 00:00:00 2001 From: aboyko Date: Wed, 9 Aug 2023 02:14:01 -0400 Subject: [PATCH] Next batch of JDT reconcilers --- .../boot/app/BootLanguageServerBootApp.java | 9 +- .../ide/vscode/boot/app/JdtConfig.java | 80 ++++ ...ConfigurationIfBeansPresentReconciler.java | 1 - .../boot/java/reconcilers/JdtReconciler.java | 22 +- ...ulithTypeReferenceViolationReconciler.java | 122 ++++++ .../NoRepoAnnotationReconciler.java | 126 +++++++ .../NoRequestMappingAnnotationReconciler.java | 116 ++++++ .../PreciseBeanTypeReconciler.java | 146 ++++++++ .../reconcilers/RewriteQuickFixUtils.java | 17 + .../UnnecessarySpringExtensionReconciler.java | 144 ++++++++ .../boot/java/utils/SpringIndexerJava.java | 34 +- .../vscode/boot/modulith/ModulithService.java | 32 +- .../test/NoRepoAnnotationReconcilerTest.java | 134 +++++++ ...equestMappingAnnotationReconcilerTest.java | 116 ++++++ .../test/PreciseBeanTypeReconcilerTest.java | 347 ++++++++++++++++++ ...ecessarySpringExtensionReconcilerTest.java | 120 ++++++ .../ValueSpelExpressionValidationTest.java | 6 +- .../test-spring-validations/pom.xml | 4 + 18 files changed, 1537 insertions(+), 39 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ModulithTypeReferenceViolationReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoRepoAnnotationReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoRequestMappingAnnotationReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/PreciseBeanTypeReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/UnnecessarySpringExtensionReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoRepoAnnotationReconcilerTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoRequestMappingAnnotationReconcilerTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/PreciseBeanTypeReconcilerTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/UnnecessarySpringExtensionReconcilerTest.java diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java index e55d5a5a3..c9c99edf8 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java @@ -62,6 +62,7 @@ import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessConnec import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessConnectorService; import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveDataProvider; import org.springframework.ide.vscode.boot.java.reconcilers.JavaReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler; import org.springframework.ide.vscode.boot.java.rewrite.RewriteRecipeRepository; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; @@ -241,8 +242,8 @@ public class BootLanguageServerBootApp { return new CompilationUnitCache(params.projectFinder, server, params.projectObserver); } - @Bean JdtReconciler jdtReconciler(CompilationUnitCache cuCache, BootJavaConfig config, SimpleLanguageServer server) { - return new JdtReconciler(cuCache, server.getQuickfixRegistry(), config); + @Bean JdtReconciler jdtReconciler(CompilationUnitCache cuCache, BootJavaConfig config, SimpleLanguageServer server, JdtAstReconciler[] reconcilers) { + return new JdtReconciler(cuCache, config, reconcilers); } @Bean SpringXMLCompletionEngine xmlCompletionEngine(SimpleLanguageServer server, JavaProjectFinder projectFinder, SpringSymbolIndex symbolIndex, BootJavaConfig config) { @@ -378,9 +379,9 @@ public class BootLanguageServerBootApp { @Bean ModulithService modulithService(SimpleLanguageServer server, JavaProjectFinder projectFinder, ProjectObserver projectObserver, SpringSymbolIndex springIndex, - Optional projectReconcileScheduler, BootJavaReconcileEngine reconciler, + BootJavaReconcileEngine reconciler, BootJavaConfig config) { - return new ModulithService(server, projectFinder, projectObserver, springIndex, reconciler, projectReconcileScheduler, config); + return new ModulithService(server, projectFinder, projectObserver, springIndex, reconciler, config); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java new file mode 100644 index 000000000..0264ec611 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2023 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.app; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.ide.vscode.boot.java.reconcilers.AddConfigurationIfBeansPresentReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.AnnotationNodeReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.AutowiredFieldIntoConstructorParameterReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.BeanMethodNotPublicReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.Boot3NotSupportedTypeReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.ModulithTypeReferenceViolationReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.NoAutowiredOnConstructorReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.NoRepoAnnotationReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.NoRequestMappingAnnotationReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.PreciseBeanTypeReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.UnnecessarySpringExtensionReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.WebSecurityConfigurerAdapterReconciler; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; + +@Configuration(proxyBeanMethods = false) +public class JdtConfig { + + @Bean AnnotationNodeReconciler annotationNodeReconciler(BootJavaConfig config) { + return new AnnotationNodeReconciler(config); + } + + @Bean BeanMethodNotPublicReconciler beanMethodNotPublicReconciler(SimpleLanguageServer server) { + return new BeanMethodNotPublicReconciler(server.getQuickfixRegistry()); + } + + @Bean AddConfigurationIfBeansPresentReconciler addConfigurationIfBeansPresentReconciler(SimpleLanguageServer server) { + return new AddConfigurationIfBeansPresentReconciler(server.getQuickfixRegistry()); + } + + @Bean AutowiredFieldIntoConstructorParameterReconciler autowiredFieldIntoConstructorParameterReconciler(SimpleLanguageServer server) { + return new AutowiredFieldIntoConstructorParameterReconciler(server.getQuickfixRegistry()); + } + + @Bean Boot3NotSupportedTypeReconciler boot3NotSupportedTypeReconciler() { + return new Boot3NotSupportedTypeReconciler(); + } + + @Bean NoAutowiredOnConstructorReconciler noAutowiredOnConstructorReconciler(SimpleLanguageServer server) { + return new NoAutowiredOnConstructorReconciler(server.getQuickfixRegistry()); + } + + @Bean WebSecurityConfigurerAdapterReconciler webSecurityConfigurerAdapterReconciler(SimpleLanguageServer server) { + return new WebSecurityConfigurerAdapterReconciler(server.getQuickfixRegistry()); + } + + @Bean PreciseBeanTypeReconciler preciseBeanTypeReconciler(SimpleLanguageServer server) { + return new PreciseBeanTypeReconciler(server.getQuickfixRegistry()); + } + + @Bean NoRequestMappingAnnotationReconciler noRequestMappingAnnotationReconciler(SimpleLanguageServer server) { + return new NoRequestMappingAnnotationReconciler(server.getQuickfixRegistry()); + } + + @Bean ModulithTypeReferenceViolationReconciler modulithTypeReferenceViolationReconciler() { + return new ModulithTypeReferenceViolationReconciler(); + } + + @Bean NoRepoAnnotationReconciler noRepoAnnotationReconciler(SimpleLanguageServer server) { + return new NoRepoAnnotationReconciler(server.getQuickfixRegistry()); + } + + @Bean UnnecessarySpringExtensionReconciler unnecessarySpringExtensionReconciler(SimpleLanguageServer server) { + return new UnnecessarySpringExtensionReconciler(server.getQuickfixRegistry()); + } + +} 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 9f0a03094..863697d35 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 @@ -59,7 +59,6 @@ public class AddConfigurationIfBeansPresentReconciler implements JdtAstReconcile @Override public boolean visit(TypeDeclaration classDecl) { if (isApplicableClass(cu, classDecl)) { - SimpleName nameAst = classDecl.getName(); ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), PROBLEM_LABEL, nameAst.getStartPosition(), nameAst.getLength()); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java index 586f04a53..9f2dd60de 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java @@ -21,11 +21,9 @@ import org.eclipse.jdt.core.dom.CompilationUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.app.BootJavaConfig; -import org.springframework.ide.vscode.boot.java.handlers.SpelExpressionReconciler; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.SpringProjectUtil; -import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; import org.springframework.ide.vscode.commons.util.text.IDocument; @@ -50,28 +48,12 @@ public class JdtReconciler implements JavaReconciler { private final CompilationUnitCache compilationUnitCache; private final JdtAstReconciler[] reconcilers; - private final SpelExpressionReconciler spelExpressionReconciler; private BootJavaConfig config; - public JdtReconciler(CompilationUnitCache compilationUnitCache, QuickfixRegistry quickfixRegistry, BootJavaConfig config) { + public JdtReconciler(CompilationUnitCache compilationUnitCache, BootJavaConfig config, JdtAstReconciler[] reconcilers) { this.compilationUnitCache = compilationUnitCache; this.config = config; - config.addListener(evt -> setSpelExpressionSyntaxValidationEnabled(config.isSpelExpressionValidationEnabled())); - this.spelExpressionReconciler = new SpelExpressionReconciler(); - - this.reconcilers = new JdtAstReconciler[] { - new AnnotationNodeReconciler(config), - new BeanMethodNotPublicReconciler(quickfixRegistry), - new AddConfigurationIfBeansPresentReconciler(quickfixRegistry), - new AutowiredFieldIntoConstructorParameterReconciler(quickfixRegistry), - new Boot3NotSupportedTypeReconciler(), - new NoAutowiredOnConstructorReconciler(quickfixRegistry), - new WebSecurityConfigurerAdapterReconciler(quickfixRegistry) - }; - } - - public void setSpelExpressionSyntaxValidationEnabled(boolean spelExpressionValidationEnabled) { - this.spelExpressionReconciler.setEnabled(spelExpressionValidationEnabled); + this.reconcilers = reconcilers; } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ModulithTypeReferenceViolationReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ModulithTypeReferenceViolationReconciler.java new file mode 100644 index 000000000..3a53d6693 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ModulithTypeReferenceViolationReconciler.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * Copyright (c) 2023 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.reconcilers; + +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.SimpleType; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.ide.vscode.boot.java.Boot3JavaProblemType; +import org.springframework.ide.vscode.boot.modulith.AppModules; +import org.springframework.ide.vscode.boot.modulith.ModulithService; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; +import org.springframework.ide.vscode.commons.java.IJavaProject; +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; + +public class ModulithTypeReferenceViolationReconciler implements JdtAstReconciler, ApplicationContextAware { + + private ApplicationContext appContext; + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) throws RequiredCompleteAstException { + Path sourceFile = Paths.get(docUri); + if (IClasspathUtil.getProjectJavaSourceFoldersWithoutTests(project.getClasspath()) + .anyMatch(f -> sourceFile.startsWith(f.toPath()))) { + ModulithService modulithService = appContext.getBean(ModulithService.class); + AppModules appModules = modulithService.getModulesData(project); + + if (appModules != null) { + final String packageName = cu.getPackage().getName().getFullyQualifiedName(); + cu.accept(new ASTVisitor() { + @Override + public boolean visit(ImportDeclaration node) { + if (!node.isOnDemand()) { + String typeFqName = node.getName().getFullyQualifiedName(); + appModules.getModuleNotExposingType(packageName, getBinaryName(typeFqName)).ifPresent(module -> { + problemCollector.accept(new ReconcileProblemImpl(getProblemType(), + "Cannot use type in this package. Type is not exposed in module '" + + module.name() + "'.", + node.getName().getStartPosition(), node.getName().getLength())); + }); + } + return false; + } + + @Override + public boolean visit(SimpleType node) { + if (node.getName().isQualifiedName()) { + appModules.getModuleNotExposingType(packageName, getBinaryName(node.getName().getFullyQualifiedName())).ifPresent(module -> { + problemCollector.accept(new ReconcileProblemImpl(getProblemType(), + "Cannot use type in this package. Type is not exposed in module '" + + module.name() + "'.", + node.getName().getStartPosition(), node.getName().getLength())); + }); + } else if (node.getName().isSimpleName()) { + String typeName = node.getName().getFullyQualifiedName(); + for (Object i : cu.imports()) { + ImportDeclaration importDecl = (ImportDeclaration) i; + if (importDecl.isOnDemand()) { + appModules.getModuleNotExposingType(packageName, getBinaryName(importDecl.getName().getFullyQualifiedName() + "." + typeName)).ifPresent(module -> { + problemCollector.accept(new ReconcileProblemImpl(getProblemType(), + "Cannot use type in this package. Type is not exposed in module '" + + module.name() + "'.", + node.getName().getStartPosition(), node.getName().getLength())); + }); + } + } + } + return super.visit(node); + } + }); + + } + } + } + + private static String getBinaryName(String fqName) { + String pkgName = ModulithService.getPackageNameFromTypeFQName(fqName); + if (pkgName.length() < fqName.length() - 1) { + String typeName = fqName.substring(pkgName.length() + 1); + StringBuilder sb = new StringBuilder(); + sb.append(pkgName); + sb.append('.'); + sb.append(typeName.replace('.', '$')); + return sb.toString(); + } + return fqName; + } + + @Override + public boolean isApplicable(IJavaProject project) { + return ModulithService.isModulithDependentProject(project); + } + + @Override + public ProblemType getProblemType() { + return Boot3JavaProblemType.MODULITH_TYPE_REF_VIOLATION; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.appContext = applicationContext; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoRepoAnnotationReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoRepoAnnotationReconciler.java new file mode 100644 index 000000000..fb8f398ca --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoRepoAnnotationReconciler.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * Copyright (c) 2023 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.reconcilers; + +import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual; + +import java.net.URI; +import java.util.List; + +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.MarkerAnnotation; +import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.openrewrite.java.spring.NoRepoAnnotationOnRepoInterface; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +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.IProblemCollector; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeCodeActionDescriptor; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; + +public class NoRepoAnnotationReconciler implements JdtAstReconciler { + + private static final String ID = NoRepoAnnotationOnRepoInterface.class.getName(); + private static final String LABEL = "Remove Unnecessary @Repository"; + private static final String INTERFACE_REPOSITORY = "org.springframework.data.repository.Repository"; + + private QuickfixRegistry registry; + + public NoRepoAnnotationReconciler(QuickfixRegistry registry) { + this.registry = registry; + } + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) throws RequiredCompleteAstException { + cu.accept(new ASTVisitor() { + + + @Override + public boolean visit(TypeDeclaration typeDecl) { + if (typeDecl.isInterface()) { + for (Object o : typeDecl.modifiers()) { + if (o instanceof Annotation) { + Annotation a = (Annotation) o; + if (isApplicableRepoAnnotation(a)) { + ITypeBinding type = typeDecl.resolveBinding(); + if (type != null && isRepo(type)) { + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL, a.getStartPosition(), a.getLength()); + String uri = docUri.toASCIIString(); + RewriteQuickFixUtils.setRewriteFixes(registry, problem, List.of( +// new FixDescriptor(ID, List.of(uri), LABEL) +// .withRangeScope(RewriteQuickFixUtils.createOpenRewriteRange(cu, typeDecl)) +// .withRecipeScope(RecipeScope.NODE), + new FixDescriptor(ID, List.of(uri), + RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.FILE)) + .withRecipeScope(RecipeScope.FILE), + new FixDescriptor(ID, List.of(uri), + RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.PROJECT)) + .withRecipeScope(RecipeScope.PROJECT) + )); + problemCollector.accept(problem); + } + } + } + } + } + return super.visit(typeDecl); + } + + private boolean isApplicableRepoAnnotation(Annotation a) { + if (a instanceof MarkerAnnotation || (a.isNormalAnnotation() && ((NormalAnnotation) a).properties().isEmpty())) { + String typeName = a.getTypeName().getFullyQualifiedName(); + if (Annotations.REPOSITORY.equals(typeName)) { + return true; + } else if (typeName.endsWith("Repository")) { + ITypeBinding type = a.resolveTypeBinding(); + if (type != null && Annotations.REPOSITORY.equals(type.getQualifiedName())) { + return true; + } + } + } + return false; + } + + private boolean isRepo(ITypeBinding t) { + if (INTERFACE_REPOSITORY.equals(t.getQualifiedName())) { + return true; + } else { + for (ITypeBinding st : t.getInterfaces()) { + if (isRepo(st)) { + return true; + } + } + } + return false; + } + + }); + } + + @Override + public boolean isApplicable(IJavaProject project) { + return springBootVersionGreaterOrEqual(2, 0, 0).test(project); + } + + @Override + public Boot2JavaProblemType getProblemType() { + return Boot2JavaProblemType.JAVA_REPOSITORY; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoRequestMappingAnnotationReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoRequestMappingAnnotationReconciler.java new file mode 100644 index 000000000..e2d2ba25c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoRequestMappingAnnotationReconciler.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2023 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.reconcilers; + +import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual; + +import java.net.URI; +import java.util.List; + +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.MarkerAnnotation; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.openrewrite.java.spring.NoRequestMappingAnnotation; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +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.IProblemCollector; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeCodeActionDescriptor; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; + +public class NoRequestMappingAnnotationReconciler implements JdtAstReconciler { + + private static final String LABEL = "Replace @RequestMapping with specific @GetMapping, @PostMapping etc."; + private static final String ID = NoRequestMappingAnnotation.class.getName(); + + private QuickfixRegistry registry; + + public NoRequestMappingAnnotationReconciler(QuickfixRegistry registry) { + this.registry = registry; + } + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) throws RequiredCompleteAstException { + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(MarkerAnnotation node) { + processAnnotation(node); + return false; + } + + @Override + public boolean visit(NormalAnnotation node) { + processAnnotation(node); + return false; + } + + @Override + public boolean visit(SingleMemberAnnotation node) { + processAnnotation(node); + return false; + } + + private void processAnnotation(Annotation a) { + if (a.getParent() instanceof MethodDeclaration && isRequestMappingAnnotation(cu, a)) { + String uri = docUri.toASCIIString(); + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL, a.getStartPosition(), a.getLength()); + RewriteQuickFixUtils.setRewriteFixes(registry, problem, List.of( +// new FixDescriptor(ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.NODE)) +// .withRangeScope(RewriteQuickFixUtils.createOpenRewriteRange(cu, a)) +// .withRecipeScope(RecipeScope.NODE), + new FixDescriptor(ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.FILE)) + .withRecipeScope(RecipeScope.FILE), + new FixDescriptor(ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.PROJECT)) + .withRecipeScope(RecipeScope.PROJECT) + )); + problemCollector.accept(problem); + } + } + + }); + } + + private static boolean isRequestMappingAnnotation(CompilationUnit cu, Annotation a) { + String typeName = a.getTypeName().getFullyQualifiedName(); + if (Annotations.SPRING_REQUEST_MAPPING.equals(typeName)) { + return true; + } else if (typeName.endsWith("RequestMapping")) { + ITypeBinding type = a.resolveTypeBinding(); + if (type != null && Annotations.SPRING_REQUEST_MAPPING.equals(type.getQualifiedName())) { + return true; + } + } + return false; + } + + + @Override + public boolean isApplicable(IJavaProject project) { + return springBootVersionGreaterOrEqual(2, 0, 0).test(project); + } + + @Override + public ProblemType getProblemType() { + return Boot2JavaProblemType.JAVA_PRECISE_REQUEST_MAPPING; + } + +} 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 new file mode 100644 index 000000000..d2c079b32 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/PreciseBeanTypeReconciler.java @@ -0,0 +1,146 @@ +/******************************************************************************* + * Copyright (c) 2023 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.reconcilers; + +import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.ReturnStatement; +import org.openrewrite.java.spring.boot3.PreciseBeanType; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; +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.IProblemCollector; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeCodeActionDescriptor; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; + +public class PreciseBeanTypeReconciler implements JdtAstReconciler { + + private static final String RECIPE_ID = PreciseBeanType.class.getName(); + + private static final String LABEL = "Ensure concrete bean type"; + + private QuickfixRegistry registry; + + public PreciseBeanTypeReconciler(QuickfixRegistry registry) { + this.registry = registry; + + } + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) throws RequiredCompleteAstException { + AtomicBoolean requiresCompleteAst = new AtomicBoolean(false); + cu.accept(new ASTVisitor() { + + private MethodDeclaration currentMethod; + + private List currentReturnTypes = new ArrayList<>(); + + @Override + 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()))); + if (isBeanMethod) { + if (isCompleteAst) { + if (currentMethod == null) {// Do not jump into anonymous class methods + currentMethod = method; + currentReturnTypes = new ArrayList<>(); + return true; + } + } else { + requiresCompleteAst.set(true); + } + } + } + return false; + } + + @Override + public void endVisit(MethodDeclaration method) { + if (currentMethod == method) { + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL, method.getReturnType2().getStartPosition(), method.getReturnType2().getLength()); + if (currentReturnTypes.size() > 1) { + problemCollector.accept(problem); + } else if (currentReturnTypes.size() == 1 && !method.resolveBinding().getReturnType().isAssignmentCompatible(currentReturnTypes.get(0))) { + String uri = docUri.toASCIIString(); + String replacementType = currentReturnTypes.get(0).getName(); + RewriteQuickFixUtils.setRewriteFixes(registry, problem, List.of( + new FixDescriptor(RECIPE_ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel("Replace return type with '" + replacementType + "'", RecipeScope.NODE)) + .withRecipeScope(RecipeScope.NODE) + .withRangeScope(RewriteQuickFixUtils.createOpenRewriteRange(cu, method)), + new FixDescriptor(RECIPE_ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.FILE)) + .withRecipeScope(RecipeScope.FILE), + new FixDescriptor(RECIPE_ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.PROJECT)) + .withRecipeScope(RecipeScope.PROJECT) + )); + problemCollector.accept(problem); + } + + currentMethod = null; + currentReturnTypes = new ArrayList<>(); + } + super.endVisit(method); + } + + @Override + public boolean visit(ReturnStatement node) { + ITypeBinding type = node.getExpression().resolveTypeBinding(); + if (currentReturnTypes.isEmpty()) { + currentReturnTypes.add(type); + } else { + for (ListIterator itr = currentReturnTypes.listIterator(); itr.hasNext();) { + ITypeBinding t = itr.next(); + if (t.isAssignmentCompatible(type)) { + itr.remove(); + } + } + currentReturnTypes.add(type); + } + return super.visit(node); + } + + }); + + if (requiresCompleteAst.get()) { + throw new RequiredCompleteAstException(); + } + } + + @Override + public boolean isApplicable(IJavaProject project) { + return springBootVersionGreaterOrEqual(3, 0, 0).test(project); + } + + @Override + public ProblemType getProblemType() { + return SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RewriteQuickFixUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RewriteQuickFixUtils.java index 9106ee579..e88d17de1 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RewriteQuickFixUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RewriteQuickFixUtils.java @@ -89,5 +89,22 @@ public class RewriteQuickFixUtils { for (; type != type.getErasure(); type = type.getErasure()) {} return type; } + + public static String getSimpleName(String fqName) { + int idx = fqName.lastIndexOf('.'); + if (idx >= 0 && idx < fqName.length() - 1) { + return fqName.substring(idx + 1); + } + return fqName; + } + + public static String getFirstTokenBeforeDot(String fqName) { + int idx = fqName.indexOf('.'); + if (idx > 0) { + return fqName.substring(0, idx); + } + return fqName; + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/UnnecessarySpringExtensionReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/UnnecessarySpringExtensionReconciler.java new file mode 100644 index 000000000..4a4c40d76 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/UnnecessarySpringExtensionReconciler.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (c) 2023 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.reconcilers; + +import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +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.IMemberValuePairBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.openrewrite.java.spring.boot2.UnnecessarySpringExtension; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +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.IProblemCollector; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; + +public class UnnecessarySpringExtensionReconciler implements JdtAstReconciler { + + private static final String LABEL = "Remove unnecessary @SpringExtension"; + private static final String ID = UnnecessarySpringExtension.class.getName(); + private static final List SPRING_BOOT_TEST_ANNOTATIONS = Arrays.asList( + "org.springframework.boot.test.context.SpringBootTest", + "org.springframework.boot.test.autoconfigure.jdbc.JdbcTest", + "org.springframework.boot.test.autoconfigure.web.client.RestClientTest", + "org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest", + "org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest", + "org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest", + "org.springframework.boot.test.autoconfigure.webservices.client.WebServiceClientTest", + "org.springframework.boot.test.autoconfigure.jooq.JooqTest", + "org.springframework.boot.test.autoconfigure.json.JsonTest", + "org.springframework.boot.test.autoconfigure.data.cassandra.DataCassandraTest", + "org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest", + "org.springframework.boot.test.autoconfigure.data.ldap.DataLdapTest", + "org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest", + "org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest", + "org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest", + "org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest" + ); + + private static final String FQN_EXTEND_WITH = "org.junit.jupiter.api.extension.ExtendWith"; + private static final String FQN_SPRING_EXT = "org.springframework.test.context.junit.jupiter.SpringExtension"; + + private QuickfixRegistry registry; + + public UnnecessarySpringExtensionReconciler(QuickfixRegistry registry) { + this.registry = registry; + + } + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) throws RequiredCompleteAstException { + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(TypeDeclaration typeDecl) { + Annotation testAnnotation = null; + Annotation extendWithAnnotation = null; + for (Object o : typeDecl.modifiers()) { + if (o instanceof Annotation) { + Annotation a = (Annotation) o; + if (testAnnotation == null && isApplicableTestAnnotation(a)) { + testAnnotation = a; + } + if (extendWithAnnotation == null && isApplicableExtendsWith(a)) { + extendWithAnnotation = a; + } + if (testAnnotation != null && extendWithAnnotation != null) { + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL, extendWithAnnotation.getStartPosition(), extendWithAnnotation.getLength()); + RewriteQuickFixUtils.setRewriteFixes(registry, problem, List.of( + new FixDescriptor(ID, List.of(docUri.toASCIIString()), RewriteQuickFixUtils.buildLabel(LABEL, RecipeScope.PROJECT)) + )); + problemCollector.accept(problem); + break; + } + } + } + return super.visit(typeDecl); + } + + private boolean isApplicableExtendsWith(Annotation a) { + if (FQN_EXTEND_WITH.endsWith(a.getTypeName().getFullyQualifiedName())) { + IAnnotationBinding annotationBinding = a.resolveAnnotationBinding(); + if (annotationBinding != null && FQN_EXTEND_WITH.equals(annotationBinding.getAnnotationType().getQualifiedName()) && annotationBinding.getDeclaredMemberValuePairs().length == 1) { + IMemberValuePairBinding pair = annotationBinding.getDeclaredMemberValuePairs()[0]; + if ("value".equals(pair.getName())) { + ITypeBinding typeBinding = null; + if (pair.getValue() instanceof ITypeBinding) { + typeBinding = (ITypeBinding) pair.getValue(); + } else if (pair.getValue() instanceof Object[]) { + Object[] arr = (Object[]) pair.getValue(); + if (arr.length > 0 && arr[0] instanceof ITypeBinding) { + typeBinding = (ITypeBinding) arr[0]; + } + } + return typeBinding != null && FQN_SPRING_EXT.equals(typeBinding.getQualifiedName()); + } + } + } + return false; + } + + private boolean isApplicableTestAnnotation(Annotation a) { + String annotationTypeFqn = a.getTypeName().getFullyQualifiedName(); + if (SPRING_BOOT_TEST_ANNOTATIONS.stream().anyMatch(fqn -> fqn.endsWith(annotationTypeFqn))) { + IAnnotationBinding annotationBinding = a.resolveAnnotationBinding(); + if (annotationBinding != null && SPRING_BOOT_TEST_ANNOTATIONS.contains(annotationBinding.getAnnotationType().getQualifiedName())) { + return true; + } + } + return false; + } + }); + } + + @Override + public boolean isApplicable(IJavaProject project) { + return springBootVersionGreaterOrEqual(2, 1, 0).test(project); + } + + @Override + public Boot2JavaProblemType getProblemType() { + return Boot2JavaProblemType.JAVA_TEST_SPRING_EXTENSION; + } + +} 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 92450aad8..d31644b77 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 @@ -643,10 +643,40 @@ public class SpringIndexerJava implements SpringIndexer { }); // reconciling + IProblemCollector problemCollector = new IProblemCollector() { + + List problems = new ArrayList<>(); + + @Override + public void endCollecting() { + for (ReconcileProblem p : problems) { + context.getProblemCollector().accept(p); + } + } + + @Override + public void beginCollecting() { + problems.clear(); + } + + @Override + public void accept(ReconcileProblem problem) { + problems.add(problem); + } + }; + try { - reconciler.reconcile(context.getProject(), URI.create(context.getDocURI()), context.getCu(), context.getProblemCollector(), context.getPass() == SCAN_PASS.TWO); + problemCollector.beginCollecting(); + reconciler.reconcile(context.getProject(), URI.create(context.getDocURI()), context.getCu(), problemCollector, context.getPass() == SCAN_PASS.TWO); + problemCollector.endCollecting(); } catch (RequiredCompleteAstException e) { - context.getNextPassFiles().add(context.getFile()); + if (context.getPass() == SCAN_PASS.TWO) { + problemCollector.endCollecting(); + log.error("Complete AST required but it is complete already. Parsing ", context.getDocURI()); + } else { + // Let problems be found in the next pass, don't add the problems to the aggregate problems collector to not duplicate them with the next pass + context.getNextPassFiles().add(context.getFile()); + } } dependencyTracker.update(context.getFile(), context.getDependencies());; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java index b64ff72d4..181a78548 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/modulith/ModulithService.java @@ -23,7 +23,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -40,8 +39,8 @@ import org.springframework.ide.vscode.boot.app.BootJavaConfig; import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.Boot3JavaProblemType; -import org.springframework.ide.vscode.boot.java.handlers.BootJavaProjectReconcilerScheduler; import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.SpringProjectUtil; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; @@ -66,10 +65,8 @@ public class ModulithService { private static final String CMD_LIST_MODULITH_PROJECTS = "sts/modulith/projects"; private SimpleLanguageServer server; - private Optional projectReconcileScheduler; private SpringSymbolIndex springIndex; private BootJavaReconcileEngine reconciler; - private JavaProjectFinder projectFinder; private BootJavaConfig config; private Map cache; @@ -81,15 +78,12 @@ public class ModulithService { ProjectObserver projectObserver, SpringSymbolIndex springIndex, BootJavaReconcileEngine reconciler, - Optional projectReconcileScheduler, BootJavaConfig config ) { - this.projectFinder = projectFinder; this.config = config; this.cache = new ConcurrentHashMap<>(); this.metadataRequested = new ConcurrentHashMap<>(); this.server = server; - this.projectReconcileScheduler = projectReconcileScheduler; this.springIndex = springIndex; this.reconciler = reconciler; @@ -209,12 +203,28 @@ public class ModulithService { private void validate(IJavaProject project) { if (server.getDiagnosticSeverityProvider().getDiagnosticSeverity(Boot3JavaProblemType.MODULITH_TYPE_REF_VIOLATION) != null && config.getProblemApplicability(Boot3JavaProblemType.MODULITH_TYPE_REF_VIOLATION) != Option.OFF) { - for (TextDocument doc : server.getTextDocumentService().getAll()) { - if (projectFinder.find(doc.getId()).orElse(null) == project) { + + List javaSources = IClasspathUtil.getProjectJavaSourceFoldersWithoutTests(project.getClasspath()) + .flatMap(sourceFolder -> { + try { + return Files.walk(sourceFolder.toPath()).filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".java")); + } catch (IOException e) { + log.error("", e); + return Stream.empty(); + } + }).collect(Collectors.toList()); + List fileUriToUpdate = new ArrayList<>(javaSources.size()); + for (Path javaSource : javaSources) { + String docUri = javaSource.toUri().toASCIIString(); + TextDocument doc = server.getTextDocumentService().getLatestSnapshot(docUri); + if (doc == null) { + fileUriToUpdate.add(docUri); + } else { server.validateWith(doc.getId(), reconciler); } } - projectReconcileScheduler.ifPresent(r -> r.scheduleValidation(project)); + String[] uris = fileUriToUpdate.toArray(new String[fileUriToUpdate.size()]); + springIndex.deleteDocuments(uris).thenAccept(v -> springIndex.updateDocuments(uris, "Modulith Metadata Changed")); } } @@ -304,7 +314,7 @@ public class ModulithService { }); } - static String getPackageNameFromTypeFQName(String fqn) { + public static String getPackageNameFromTypeFQName(String fqn) { int idx = 0; for (; idx < fqn.length() - 1; idx++) { char c = fqn.charAt(idx); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoRepoAnnotationReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoRepoAnnotationReconcilerTest.java new file mode 100644 index 000000000..a25f1a0f8 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoRepoAnnotationReconcilerTest.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2023 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.reconcilers.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +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.Boot2JavaProblemType; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.NoRepoAnnotationReconciler; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; + +public class NoRepoAnnotationReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "norepoannotation"; + } + + @Override + protected String getProjectName() { + return "test-spring-validations"; + } + + @Override + protected JdtAstReconciler getReconciler() { + return new NoRepoAnnotationReconciler(new QuickfixRegistry()); + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void sanityTest() throws Exception { + String source = """ + package example.demo; + + import org.springframework.data.repository.Repository; + + @org.springframework.stereotype.Repository + interface A extends Repository { + + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.JAVA_REPOSITORY, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("@org.springframework.stereotype.Repository", markedStr); + + assertEquals(2, problem.getQuickfixes().size()); + + } + + @Test + void inverseSanityTest() throws Exception { + String source = """ + package example.demo; + + import org.springframework.stereotype.Repository; + + @Repository + interface A extends org.springframework.data.repository.Repository { + + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.JAVA_REPOSITORY, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("@Repository", markedStr); + + assertEquals(2, problem.getQuickfixes().size()); + + } + + @Test + void emptyRepoAnnotation() throws Exception { + String source = """ + package example.demo; + + import org.springframework.data.repository.Repository; + + @org.springframework.stereotype.Repository() + interface A extends Repository { + + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.JAVA_REPOSITORY, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("@org.springframework.stereotype.Repository()", markedStr); + + assertEquals(2, problem.getQuickfixes().size()); + + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoRequestMappingAnnotationReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoRequestMappingAnnotationReconcilerTest.java new file mode 100644 index 000000000..0faa89db7 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoRequestMappingAnnotationReconcilerTest.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2023 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.reconcilers.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +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.Boot2JavaProblemType; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.NoRequestMappingAnnotationReconciler; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; + +public class NoRequestMappingAnnotationReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "norequestmapping"; + } + + @Override + protected String getProjectName() { + return "test-spring-validations"; + } + + @Override + protected JdtAstReconciler getReconciler() { + return new NoRequestMappingAnnotationReconciler(new QuickfixRegistry()); + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void sanityTest() throws Exception { + String source = """ + package example.demo; + + import org.springframework.web.bind.annotation.RequestMapping; + + + @RequestMapping("/hello") + class A { + + @RequestMapping("/1") + String hello1() { + return "1"; + }; + + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.JAVA_PRECISE_REQUEST_MAPPING, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("@RequestMapping(\"/1\")", markedStr); + + assertEquals(2, problem.getQuickfixes().size()); + + } + + @Test + void noProblems() throws Exception { + String source = """ + package example.demo; + + import org.springframework.web.bind.annotation.RequestMapping; + import org.springframework.context.annotation.Bean; + import org.springframework.web.bind.annotation.GetMapping; + + + @RequestMapping("/hello") + class A { + + @Bean + Integer someBean() { + return 0; + } + + @GetMapping("/1") + String hello1() { + return "1"; + }; + + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(0, problems.size()); + + } +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/PreciseBeanTypeReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/PreciseBeanTypeReconcilerTest.java new file mode 100644 index 000000000..418769454 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/PreciseBeanTypeReconcilerTest.java @@ -0,0 +1,347 @@ +/******************************************************************************* + * Copyright (c) 2023 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.reconcilers.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.List; + +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.SpringAotJavaProblemType; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.PreciseBeanTypeReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; + +public class PreciseBeanTypeReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "precisebeantype"; + } + + @Override + protected String getProjectName() { + return "test-spring-validations"; + } + + @Override + protected JdtAstReconciler getReconciler() { + return new PreciseBeanTypeReconciler(new QuickfixRegistry()); + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void requireFullAst() throws Exception { + String source = """ + package example.demo; + + import org.springframework.context.annotation.Bean; + + class A { + + @Bean + Number bean() { + return Integer.valueOf(5); + }; + + } + """; + try { + reconcile("A.java", source, false); + fail("Should require full AST with method bodies"); + } catch (RequiredCompleteAstException e) { + // pass + } + + } + + @Test + void singleReturnStatement() throws Exception { + String source = """ + package example.demo; + + import org.springframework.context.annotation.Bean; + + class A { + + @Bean + Number bean() { + return Integer.valueOf(5); + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("Number", markedStr); + + assertEquals(3, problem.getQuickfixes().size()); + + } + + @Test + void singleReturnStatementWithPrimitiveType() throws Exception { + String source = """ + package example.demo; + + import org.springframework.context.annotation.Bean; + + class A { + + @Bean + Number bean() { + return 5; + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("Number", markedStr); + + assertEquals(3, problem.getQuickfixes().size()); + + } + + @Test + void multipleReturnStatementSameType() throws Exception { + String source = """ + package example.demo; + + import org.springframework.context.annotation.Bean; + + class A { + + boolean b + + @Bean + Number bean() { + if (b) { + return 3; + } else { + return 5; + } + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("Number", markedStr); + + assertEquals(3, problem.getQuickfixes().size()); + + } + + @Test + void multipleReturnStatementDifferentType() throws Exception { + String source = """ + package example.demo; + + import org.springframework.context.annotation.Bean; + + class A { + + boolean b + + @Bean + Number bean() { + if (b) { + return 3.45; + } else { + return 5; + } + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("Number", markedStr); + + assertEquals(0, problem.getQuickfixes().size()); + + } + + @Test + void multipleReturnStatementDifferentType2() throws Exception { + String source = """ + package example.demo; + + import java.util.*; + import org.springframework.context.annotation.Bean; + + class A { + + boolean b + + @Bean + Collection bean() { + if (b) { + LinkedList l = new LinkedList<>(); + return l; + } else { + return List.of(5); + } + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("Collection", markedStr); + + assertEquals(3, problem.getQuickfixes().size()); + + FixDescriptor d = (FixDescriptor) problem.getQuickfixes().get(0).params; + + assertEquals("Replace return type with 'List'", d.getLabel()); + + } + + @Test + void noProblemSingleReturnStatement() throws Exception { + String source = """ + package example.demo; + + import org.springframework.context.annotation.Bean; + + class A { + + @Bean + Integer bean() { + return 5; + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(0, problems.size()); + + } + + @Test + void multipleReturnStatement() throws Exception { + String source = """ + package example.demo; + + import org.springframework.context.annotation.Bean; + + class A { + + boolean b + + @Bean + Double bean() { + if (b) { + return 3.45; + } else { + return 5; + } + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("Double", markedStr); + + assertEquals(0, problem.getQuickfixes().size()); + + } + + @Test + void noProblemMultipleReturnStatement() throws Exception { + String source = """ + package example.demo; + + import org.springframework.context.annotation.Bean; + + class A { + + boolean b + + @Bean + Double bean() { + if (b) { + return 3.45; + } else { + return 5.4; + } + }; + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(0, problems.size()); + + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/UnnecessarySpringExtensionReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/UnnecessarySpringExtensionReconcilerTest.java new file mode 100644 index 000000000..9e05952ec --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/UnnecessarySpringExtensionReconcilerTest.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright (c) 2023 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.reconcilers.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +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.Boot2JavaProblemType; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.UnnecessarySpringExtensionReconciler; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; + +public class UnnecessarySpringExtensionReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + // TODO Auto-generated method stub + return "unnecassaryextendwith"; + } + + @Override + protected String getProjectName() { + return "test-spring-validations"; + } + + @Override + protected JdtAstReconciler getReconciler() { + return new UnnecessarySpringExtensionReconciler(new QuickfixRegistry()); + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void sanity() throws Exception { + String source = """ + package example.demo; + + import org.springframework.boot.test.context.SpringBootTest; + import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.test.context.junit.jupiter.SpringExtension; + + @ExtendWith(SpringExtension.class) + @SpringBootTest + class A { + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.JAVA_TEST_SPRING_EXTENSION, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("@ExtendWith(SpringExtension.class)", markedStr); + + assertEquals(1, problem.getQuickfixes().size()); + + } + + @Test + void noTestAnnotation() throws Exception { + String source = """ + package example.demo; + + import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.test.context.junit.jupiter.SpringExtension; + + @ExtendWith(SpringExtension.class) + class A { + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(0, problems.size()); + + } + + @Test + void noExtendWithAnnotation() throws Exception { + String source = """ + package example.demo; + + import org.springframework.boot.test.context.SpringBootTest; + + @SpringBootTest + class A { + + } + """; + List problems = reconcile("A.java", source, true); + + assertEquals(0, problems.size()); + + } +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueSpelExpressionValidationTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueSpelExpressionValidationTest.java index b653b34c3..cf267c3a3 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueSpelExpressionValidationTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/value/test/ValueSpelExpressionValidationTest.java @@ -45,7 +45,9 @@ import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid; import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine; import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory; import org.springframework.ide.vscode.boot.java.links.SourceLinks; +import org.springframework.ide.vscode.boot.java.reconcilers.AnnotationNodeReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.JavaReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.metadata.ValueProviderRegistry; @@ -148,7 +150,9 @@ public class ValueSpelExpressionValidationTest { problemCollector = new TestProblemCollector(); reconcileEngine = new BootJavaReconcileEngine(projectFinder, new JavaReconciler[] { - new JdtReconciler(compilationUnitCache, null, config) + new JdtReconciler(compilationUnitCache, config, new JdtAstReconciler[] { + new AnnotationNodeReconciler(config) + }) }, server); } diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-validations/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-validations/pom.xml index 95f536460..1b4da9817 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-validations/pom.xml +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-validations/pom.xml @@ -31,6 +31,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-data-jpa + org.springframework.boot spring-boot-starter-actuator