From 231ad105a4637f4ff8f1dbb66b8bff708c6fdcca Mon Sep 17 00:00:00 2001 From: aboyko Date: Mon, 7 Aug 2023 23:31:30 -0400 Subject: [PATCH] First batch of reconcilers migrated to JDT --- .../commons/rewrite/java/ORAstUtils.java | 38 +-- .../ide/vscode/boot/app/BootJavaConfig.java | 12 +- .../ide/vscode/boot/app/RewriteConfig.java | 18 +- .../vscode/boot/app/SpringSymbolIndex.java | 8 +- .../ide/vscode/boot/java/Annotations.java | 1 + ...ConfigurationIfBeansPresentReconciler.java | 153 ++++++++++++ .../reconcilers/AnnotationNodeReconciler.java | 138 +++++++++++ .../AnnotationParamReconciler.java | 4 +- .../reconcilers/AnnotationReconciler.java | 5 +- ...eldIntoConstructorParameterReconciler.java | 171 +++++++++++++ .../BeanMethodNotPublicReconciler.java | 136 ++++++----- .../Boot3NotSupportedTypeReconciler.java | 109 +++++++++ .../java/reconcilers/JdtAstReconciler.java | 28 +++ .../boot/java/reconcilers/JdtReconciler.java | 132 ++++------- .../NoAutowiredOnConstructorReconciler.java | 103 ++++++++ .../RequiredCompleteAstException.java | 17 ++ .../reconcilers/RewriteQuickFixUtils.java | 93 ++++++++ ...ebSecurityConfigurerAdapterReconciler.java | 149 ++++++++++++ .../java/rewrite/RewriteRefactorings.java | 3 +- .../boot/java/utils/SpringIndexerJava.java | 27 ++- ...ntoConstructorParameterReconcilerTest.java | 224 ++++++++++++++++++ .../reconcilers/test/BaseReconcilerTest.java | 101 ++++++++ .../Boot3NotSupportedTypeReconcilerTest.java | 152 ++++++++++++ ...oAutowiredOnConstructorReconcilerTest.java | 103 ++++++++ ...curityConfigurerAdapterReconcilerTest.java | 155 ++++++++++++ .../ValueSpelExpressionValidationTest.java | 2 +- .../test-spring-indexing/pom.xml | 4 + 27 files changed, 1881 insertions(+), 205 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AddConfigurationIfBeansPresentReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationNodeReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AutowiredFieldIntoConstructorParameterReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/Boot3NotSupportedTypeReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtAstReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoAutowiredOnConstructorReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RequiredCompleteAstException.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RewriteQuickFixUtils.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/WebSecurityConfigurerAdapterReconciler.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/AutowiredFieldIntoConstructorParameterReconcilerTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BaseReconcilerTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/Boot3NotSupportedTypeReconcilerTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoAutowiredOnConstructorReconcilerTest.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/WebSecurityConfigurerAdapterReconcilerTest.java diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ORAstUtils.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ORAstUtils.java index 06f432592..82aa89673 100644 --- a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ORAstUtils.java +++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/ORAstUtils.java @@ -12,8 +12,6 @@ package org.springframework.ide.vscode.commons.rewrite.java; import java.io.ByteArrayInputStream; import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.nio.channels.ClosedByInterruptException; import java.nio.file.Files; import java.nio.file.Path; @@ -39,6 +37,7 @@ import org.openrewrite.java.JavaParser; import org.openrewrite.java.JavaParser.Builder; import org.openrewrite.java.JavaParsingException; import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.RemoveUnusedImports; import org.openrewrite.java.UpdateSourcePositions; import org.openrewrite.java.marker.JavaSourceSet; import org.openrewrite.java.tree.J; @@ -377,27 +376,6 @@ public class ORAstUtils { return fqName; } - @SuppressWarnings("unchecked") - private static List> getAfterVisitors(TreeVisitor visitor) { - try { - Method m = TreeVisitor.class.getDeclaredMethod("getAfterVisit"); - m.setAccessible(true); - return (List>) m.invoke(visitor); - } catch (Exception e) { - return Collections.emptyList(); - } - } - - private static void makeVisitorNonTopLevel(TreeVisitor visitor) { - try { - Field f = TreeVisitor.class.getDeclaredField("afterVisit"); - f.setAccessible(true); - f.set(visitor, new ArrayList<>()); - } catch (Exception e) { - // ignore - } - } - public static Recipe nodeRecipe(JavaVisitor v, Predicate condition) { return new NodeRecipe((JavaVisitor) v, condition); } @@ -415,8 +393,13 @@ public class ORAstUtils { this.visitor = treeVisitor; this.condition = condition; } - + @Override + public List getRecipeList() { + return List.of(new RemoveUnusedImports()); + } + + @Override public String getDisplayName() { return ""; } @@ -430,12 +413,7 @@ public class ORAstUtils { if (tree instanceof J) { J t = (J) tree; if (condition.test(t)) { - makeVisitorNonTopLevel(visitor); - t = (J) visitor.visit(t, ctx, getCursor()); - for (TreeVisitor v : getAfterVisitors(visitor)) { - doAfterVisit(v); - } - return t; + return (J) visitor.visit(t, ctx, getCursor()); } } return super.visit(tree, ctx); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java index 3d0297435..f39c528ae 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaConfig.java @@ -210,7 +210,17 @@ public class BootJavaConfig implements InitializingBean { if (problem != null && problem.getCategory() != null && problem.getCategory().getToggle() != null) { Toggle toggle = problem.getCategory().getToggle(); String s = settings.getString((toggle.getPreferenceKey()).split("\\.")); - return s == null || s.isEmpty() ? toggle.getDefaultValue() : Toggle.Option.valueOf(s); + try { + return s == null || s.isEmpty() ? toggle.getDefaultValue() : Toggle.Option.valueOf(s); + } catch (IllegalArgumentException e) { + // handle backward compatibility case of 'true'/'false' + Boolean b = Boolean.valueOf(s); + if (b == null) { + throw e; + } else { + return b.booleanValue() ? Toggle.Option.ON : Toggle.Option.OFF; + } + } } } catch (Exception e) { log.error("", e); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/RewriteConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/RewriteConfig.java index 74201122a..0934b1966 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/RewriteConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/RewriteConfig.java @@ -51,14 +51,14 @@ public class RewriteConfig { return new SpringBootUpgrade(server, recipeRepo, projectFinder); } - @ConditionalOnBean(RewriteRecipeRepository.class) - @Bean RewriteReconciler rewriteJavaReconciler(RewriteRecipeRepository recipeRepo, RewriteCompilationUnitCache cuCache, SimpleLanguageServer server, BootJavaConfig config) { - return new RewriteReconciler( - recipeRepo, - cuCache, - server.getQuickfixRegistry(), - config - ); - } +// @ConditionalOnBean(RewriteRecipeRepository.class) +// @Bean RewriteReconciler rewriteJavaReconciler(RewriteRecipeRepository recipeRepo, RewriteCompilationUnitCache cuCache, SimpleLanguageServer server, BootJavaConfig config) { +// return new RewriteReconciler( +// recipeRepo, +// cuCache, +// server.getQuickfixRegistry(), +// config +// ); +// } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndex.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndex.java index 9aad0dd65..1fad39d26 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndex.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndex.java @@ -55,8 +55,7 @@ import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyA import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation; import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; -import org.springframework.ide.vscode.boot.java.reconcilers.AnnotationReconciler; -import org.springframework.ide.vscode.boot.java.reconcilers.BeanMethodNotPublicReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler; import org.springframework.ide.vscode.boot.java.utils.DocumentDescriptor; import org.springframework.ide.vscode.boot.java.utils.SpringFactoriesIndexer; import org.springframework.ide.vscode.boot.java.utils.SpringIndexer; @@ -102,6 +101,7 @@ public class SpringSymbolIndex implements InitializingBean, SpringIndex { @Autowired IndexCache cache; @Autowired FutureProjectFinder futureProjectFinder; @Autowired SpringMetamodelIndex springIndex; + @Autowired JdtReconciler jdtReconciler; private static final String QUERY_PARAM_LOCATION_PREFIX = "locationPrefix:"; @@ -246,11 +246,9 @@ public class SpringSymbolIndex implements InitializingBean, SpringIndex { namespaceHandler.put("http://www.springframework.org/schema/beans", new SpringIndexerXMLNamespaceHandlerBeans()); springIndexerXML = new SpringIndexerXML(handler, namespaceHandler, this.cache, projectFinder()); - List reconcilers = new ArrayList<>(); - reconcilers.add(new BeanMethodNotPublicReconciler(server.getQuickfixRegistry())); BiFunction, BiConsumer, IProblemCollector> problemCollectorFactory = (docRef, aggregator) -> server.createProblemCollector(docRef, aggregator); - springIndexerJava = new SpringIndexerJava(handler, specificProviders, this.cache, projectFinder(), server.getProgressService(), reconcilers, problemCollectorFactory, config.getJavaValidationSettingsJson()); + springIndexerJava = new SpringIndexerJava(handler, specificProviders, this.cache, projectFinder(), server.getProgressService(), jdtReconciler, problemCollectorFactory, config.getJavaValidationSettingsJson()); factoriesIndexer = new SpringFactoriesIndexer(handler, cache); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java index 0e04ef7c3..17db9102c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/Annotations.java @@ -36,6 +36,7 @@ public class Annotations { public static final String SPRING_PUT_MAPPING = "org.springframework.web.bind.annotation.PutMapping"; public static final String SPRING_DELETE_MAPPING = "org.springframework.web.bind.annotation.DeleteMapping"; public static final String SPRING_PATCH_MAPPING = "org.springframework.web.bind.annotation.PatchMapping"; + public static final String SPRING_BOOT_TEST = "org.springframework.boot.test.context.SpringBootTest"; public static final String CONDITIONAL_ON_BEAN = "org.springframework.boot.autoconfigure.condition.ConditionalOnBean"; public static final String CONDITIONAL_ON_MISSING_BEAN = "org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean"; 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 new file mode 100644 index 000000000..9f0a03094 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AddConfigurationIfBeansPresentReconciler.java @@ -0,0 +1,153 @@ +/******************************************************************************* + * 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.lang.reflect.Modifier; +import java.net.URI; +import java.util.Iterator; +import java.util.List; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.openrewrite.java.spring.boot2.AddConfigurationAnnotationIfBeansPresent; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.SpringProjectUtil; +import org.springframework.ide.vscode.commons.java.Version; +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.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; + +public class AddConfigurationIfBeansPresentReconciler implements JdtAstReconciler { + + private static final String ID = AddConfigurationAnnotationIfBeansPresent.class.getName(); + + private static final String PROBLEM_LABEL = "'@Configuration' is missing on a class defining Spring Beans"; + + private static final String FIX_LABEL = "Add missing '@Configuration' annotations over classes"; + + private QuickfixRegistry quickfixRegistry; + + public AddConfigurationIfBeansPresentReconciler(QuickfixRegistry quickfixRegistry) { + this.quickfixRegistry = quickfixRegistry; + } + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) { + cu.accept(new ASTVisitor() { + + @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()); + + RewriteQuickFixUtils.setRewriteFixes(quickfixRegistry, problem, + List.of(new FixDescriptor(ID, List.of(docUri.toASCIIString()), + RewriteQuickFixUtils.buildLabel(FIX_LABEL, RecipeScope.FILE)) + .withRecipeScope(RecipeScope.FILE), + new FixDescriptor(ID, List.of(docUri.toASCIIString()), + RewriteQuickFixUtils.buildLabel(FIX_LABEL, RecipeScope.PROJECT)) + .withRecipeScope(RecipeScope.PROJECT))); + + problemCollector.accept(problem); + } + return true; + } + + }); + } + + private static boolean isApplicableClass(CompilationUnit cu, TypeDeclaration classDecl) { + if (classDecl.isInterface()) { + return false; + } + + if (Modifier.isAbstract(classDecl.getModifiers())) { + return false; + } + + boolean isStatic = Modifier.isStatic(classDecl.getModifiers()); + + if (!isStatic) { + // no static keyword? check if it is top level class in the CU + + for (ASTNode p = classDecl.getParent(); p != cu && p != null; p = p.getParent()) { + if (p instanceof TypeDeclaration) { + return false; + } + } + } + + // check if '@Configuration' is already over the class + for (Iterator itr = classDecl.modifiers().iterator(); itr.hasNext();) { + Object mod = itr.next(); + if (mod instanceof Annotation) { + Annotation a = (Annotation) mod; + ITypeBinding aType = a.resolveTypeBinding(); + if (aType != null && AnnotationHierarchies.isSubtypeOf(a, Annotations.CONFIGURATION)) { + // Found '@Configuration' annotation + return false; + } + } + } + + // No '@Configuration' present. Check if any methods have '@Bean' annotation + for (MethodDeclaration m : classDecl.getMethods()) { + if (isBeanMethod(m)) { + return true; + } + } + + return false; + } + + private static boolean isBeanMethod(MethodDeclaration m) { + for (Iterator itr = m.modifiers().iterator(); itr.hasNext();) { + Object mod = itr.next(); + if (mod instanceof Annotation) { + Annotation a = (Annotation) mod; + ITypeBinding aType = a.resolveTypeBinding(); + if (aType != null && AnnotationHierarchies.isSubtypeOf(a, Annotations.BEAN)) { + // Found '@Bean' annotation + return true; + } + } + } + return false; + } + + @Override + public boolean isApplicable(IJavaProject project) { + Version version = SpringProjectUtil.getDependencyVersion(project, "spring-context"); + return version != null && version.compareTo(new Version(3, 0, 0, null)) >= 0; + } + + @Override + public ProblemType getProblemType() { + return Boot2JavaProblemType.MISSING_CONFIGURATION_ANNOTATION; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationNodeReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationNodeReconciler.java new file mode 100644 index 000000000..7ece477f2 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationNodeReconciler.java @@ -0,0 +1,138 @@ +/******************************************************************************* + * 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 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.SingleMemberAnnotation; +import org.springframework.ide.vscode.boot.app.BootJavaConfig; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.SpelProblemType; +import org.springframework.ide.vscode.boot.java.handlers.SpelExpressionReconciler; +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; + +public class AnnotationNodeReconciler implements JdtAstReconciler { + + public static final String SPRING_CACHEABLE = "org.springframework.cache.annotation.Cacheable"; + public static final String SPRING_CACHE_EVICT = "org.springframework.cache.annotation.CacheEvict"; + + public static final String SPRING_EVENT_LISTENER = "org.springframework.context.event.EventListener"; + + public static final String SPRING_PRE_AUTHORIZE = "org.springframework.security.access.prepost.PreAuthorize"; + public static final String SPRING_PRE_FILTER = "org.springframework.security.access.prepost.PreFilter"; + public static final String SPRING_POST_AUTHORIZE = "org.springframework.security.access.prepost.PostAuthorize"; + public static final String SPRING_POST_FILTER= "org.springframework.security.access.prepost.PostFilter"; + + public static final String SPRING_CONDITIONAL_ON_EXPRESSION = "org.springframework.boot.autoconfigure.condition.ConditionalOnExpression"; + + private final SpelExpressionReconciler spelExpressionReconciler; + private final AnnotationReconciler[] reconcilers; + + public AnnotationNodeReconciler(BootJavaConfig config) { + this.spelExpressionReconciler = new SpelExpressionReconciler(); + this.reconcilers = new AnnotationReconciler[] { + + new AnnotationParamReconciler(Annotations.VALUE, null, "#{", "}", spelExpressionReconciler), + new AnnotationParamReconciler(Annotations.VALUE, "value", "#{", "}", spelExpressionReconciler), + + new AnnotationParamReconciler(SPRING_CACHEABLE, "key", "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_CACHEABLE, "condition", "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_CACHEABLE, "unless", "", "", spelExpressionReconciler), + + new AnnotationParamReconciler(SPRING_CACHE_EVICT, "key", "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_CACHE_EVICT, "condition", "", "", spelExpressionReconciler), + + new AnnotationParamReconciler(SPRING_EVENT_LISTENER, "condition", "", "", spelExpressionReconciler), + + new AnnotationParamReconciler(SPRING_PRE_AUTHORIZE, null, "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_PRE_AUTHORIZE, "value", "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_PRE_FILTER, null, "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_PRE_FILTER, "value", "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_POST_AUTHORIZE, null, "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_POST_AUTHORIZE, "value", "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_POST_FILTER, null, "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_POST_FILTER, "value", "", "", spelExpressionReconciler), + + new AnnotationParamReconciler(SPRING_CONDITIONAL_ON_EXPRESSION, null, "", "", spelExpressionReconciler), + new AnnotationParamReconciler(SPRING_CONDITIONAL_ON_EXPRESSION, "value", "", "", spelExpressionReconciler), + + }; + config.addListener(evt -> this.spelExpressionReconciler.setEnabled(config.isSpelExpressionValidationEnabled())); + } + + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, boolean isCompleteAst) { + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(SingleMemberAnnotation node) { + try { + visitAnnotation(project, docUri, node, problemCollector); + } + catch (Exception e) { + } + return super.visit(node); + } + + @Override + public boolean visit(NormalAnnotation node) { + try { + visitAnnotation(project, docUri, node, problemCollector); + } + catch (Exception e) { + } + return super.visit(node); + } + + @Override + public boolean visit(MarkerAnnotation node) { + try { + visitAnnotation(project, docUri, node, problemCollector); + } + catch (Exception e) { + } + return super.visit(node); + } + + }); + } + + private void visitAnnotation(IJavaProject project, URI docUri, Annotation node, IProblemCollector problemCollector) { + ITypeBinding typeBinding = node.resolveTypeBinding(); + if (typeBinding != null) { + for (int i = 0; i < reconcilers.length; i++) { + reconcilers[i].visit(project, docUri, node, typeBinding, problemCollector); + } + } + } + + + @Override + public boolean isApplicable(IJavaProject project) { + return true; + } + + + @Override + public ProblemType getProblemType() { + return SpelProblemType.JAVA_SPEL_EXPRESSION_SYNTAX; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationParamReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationParamReconciler.java index 693291cdd..1a4bff9db 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationParamReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationParamReconciler.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.reconcilers; +import java.net.URI; import java.util.List; import java.util.Set; @@ -24,7 +25,6 @@ import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchie import org.springframework.ide.vscode.boot.java.handlers.Reconciler; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; -import org.springframework.ide.vscode.commons.util.text.IDocument; /** * @author Martin Lippert @@ -46,7 +46,7 @@ public class AnnotationParamReconciler implements AnnotationReconciler { this.reconciler = reconciler; } - public void visit(IJavaProject project, IDocument doc, Annotation node, ITypeBinding typeBinding, IProblemCollector problemCollector) { + public void visit(IJavaProject project, URI docUri, Annotation node, ITypeBinding typeBinding, IProblemCollector problemCollector) { if (node instanceof SingleMemberAnnotation) { visitSingleMemberAnnotation((SingleMemberAnnotation) node, typeBinding, problemCollector); } else if (node instanceof NormalAnnotation) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationReconciler.java index 007db670d..57454895d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AnnotationReconciler.java @@ -10,14 +10,15 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.reconcilers; +import java.net.URI; + import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.ITypeBinding; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; -import org.springframework.ide.vscode.commons.util.text.IDocument; public interface AnnotationReconciler { - void visit(IJavaProject project, IDocument doc, Annotation node, ITypeBinding typeBinding, IProblemCollector problemCollector); + void visit(IJavaProject project, URI docUri, Annotation node, ITypeBinding typeBinding, IProblemCollector problemCollector); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AutowiredFieldIntoConstructorParameterReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AutowiredFieldIntoConstructorParameterReconciler.java new file mode 100644 index 000000000..09ff21cd0 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/AutowiredFieldIntoConstructorParameterReconciler.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * 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.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.Assignment; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ExpressionStatement; +import org.eclipse.jdt.core.dom.FieldAccess; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.IVariableBinding; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.ThisExpression; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; +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.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.ConvertAutowiredFieldIntoConstructorParameter; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; + +public class AutowiredFieldIntoConstructorParameterReconciler implements JdtAstReconciler { + + private static final String LABEL = "Convert @Autowired field into Constructor Parameter"; + private static final String ID = ConvertAutowiredFieldIntoConstructorParameter.class.getName(); + + private QuickfixRegistry registry; + + public AutowiredFieldIntoConstructorParameterReconciler(QuickfixRegistry registry) { + this.registry = registry; + } + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) throws RequiredCompleteAstException { + Path sourceFile = Paths.get(docUri); + // Check if source file belongs to non-test java sources folder + if (IClasspathUtil.getProjectJavaSourceFoldersWithoutTests(project.getClasspath()) + .anyMatch(f -> sourceFile.startsWith(f.toPath()))) { + AtomicBoolean completeAstRequired = new AtomicBoolean(false); + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(FieldDeclaration field) { + if (field.fragments().size() == 1) { + Annotation annotation = RewriteQuickFixUtils.findAnnotation(field, Annotations.AUTOWIRED, + false); + if (annotation != null && field.getParent() instanceof TypeDeclaration) { + TypeDeclaration typeDecl = (TypeDeclaration) field.getParent(); + List constructors = Arrays.stream(typeDecl.getMethods()) + .filter(c -> c.isConstructor()).collect(Collectors.toList()); + + VariableDeclarationFragment variableDeclarationFragment = (VariableDeclarationFragment) field + .fragments().get(0); + String fieldName = variableDeclarationFragment.getName().getIdentifier(); + + if (constructors.isEmpty()) { + problemCollector.accept(createProblem(cu, field, fieldName, docUri)); + } else if (constructors.size() == 1) { + if (!isCompleteAst) { + completeAstRequired.set(true); + return false; + } + if (!isAssigningField(constructors.get(0), variableDeclarationFragment.resolveBinding(), + fieldName)) { + problemCollector.accept(createProblem(cu, field, fieldName, docUri)); + } + } else { + List autowiredConstructors = constructors.stream() + .filter(constr -> RewriteQuickFixUtils.findAnnotation(constr, + Annotations.AUTOWIRED, true) != null) + .limit(2).collect(Collectors.toList()); + if (autowiredConstructors.size() == 1) { + if (!isCompleteAst) { + completeAstRequired.set(true); + return false; + } else if (!isAssigningField(autowiredConstructors.get(0), + variableDeclarationFragment.resolveBinding(), fieldName)) { + problemCollector.accept(createProblem(cu, field, fieldName, docUri)); + } + } + } + + } + } + return true; + } + + }); + if (completeAstRequired.get()) { + throw new RequiredCompleteAstException(); + } + } + + } + + private ReconcileProblemImpl createProblem(CompilationUnit cu, FieldDeclaration field, String fieldName, + URI docUri) { + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL, field.getStartPosition(), + field.getLength()); + TypeDeclaration typeDecl = (TypeDeclaration) field.getParent(); + String typeFqName = (cu.getPackage() != null && cu.getPackage().getName() != null + ? cu.getPackage().getName().getFullyQualifiedName() + "." + : "") + typeDecl.getName().getFullyQualifiedName(); + RewriteQuickFixUtils.setRewriteFixes(registry, problem, + List.of(new FixDescriptor(ID, List.of(docUri.toASCIIString()), LABEL) + .withRangeScope(RewriteQuickFixUtils.createOpenRewriteRange(cu, typeDecl)) + .withParameters(Map.of("classFqName", typeFqName, "fieldName", fieldName)) + .withRecipeScope(RecipeScope.NODE))); + return problem; + } + + private static boolean isAssigningField(MethodDeclaration c, IVariableBinding binding, String fieldName) { + for (Object n : c.getBody().statements()) { + if (n instanceof ExpressionStatement && ((ExpressionStatement) n).getExpression() instanceof Assignment) { + Assignment assignment = (Assignment) ((ExpressionStatement) n).getExpression(); + if (assignment.getLeftHandSide() instanceof FieldAccess) { + FieldAccess fa = (FieldAccess) assignment.getLeftHandSide(); + if (fieldName.equals(fa.getName().getIdentifier()) && fa.getExpression() instanceof ThisExpression) { + return true; + } + } + if (assignment.getLeftHandSide() instanceof SimpleName) { + SimpleName simpleName = (SimpleName) assignment.getLeftHandSide(); + if (fieldName.equals(simpleName.getIdentifier()) && binding.isEqualTo(simpleName.resolveBinding())) { + return true; + } + } + } + } + return false; + } + + @Override + public boolean isApplicable(IJavaProject project) { + return springBootVersionGreaterOrEqual(2, 0, 0).test(project); + } + + @Override + public ProblemType getProblemType() { + return Boot2JavaProblemType.JAVA_CONSTRUCTOR_PARAMETER_INJECTION; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanMethodNotPublicReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanMethodNotPublicReconciler.java index 9d8366abf..946db123f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanMethodNotPublicReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/BeanMethodNotPublicReconciler.java @@ -11,14 +11,19 @@ package org.springframework.ide.vscode.boot.java.reconcilers; import java.lang.reflect.Field; +import java.net.URI; import java.util.List; -import java.util.UUID; +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.IMethodBinding; 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.Modifier; +import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.SingleMemberAnnotation; import org.openrewrite.java.spring.BeanMethodsNotPublic; import org.openrewrite.marker.Range; import org.slf4j.Logger; @@ -33,14 +38,13 @@ import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix.Q import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixType; 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; -import org.springframework.ide.vscode.commons.util.BadLocationException; -import org.springframework.ide.vscode.commons.util.text.IDocument; -public class BeanMethodNotPublicReconciler implements AnnotationReconciler { +public class BeanMethodNotPublicReconciler implements JdtAstReconciler { private static final Logger log = LoggerFactory.getLogger(BeanMethodNotPublicReconciler.class); @@ -53,34 +57,28 @@ public class BeanMethodNotPublicReconciler implements AnnotationReconciler { this.quickfixRegistry = quickfixRegistry; } - @Override - public void visit(IJavaProject project, IDocument doc, Annotation node, ITypeBinding typeBinding, - IProblemCollector problemCollector) { - - if (Annotations.BEAN.equals(typeBinding.getQualifiedName()) && node.getParent() instanceof MethodDeclaration) { + private void visitAnnotation(IJavaProject project, CompilationUnit cu, URI docUri, Annotation node, IProblemCollector problemCollector) { + ITypeBinding typeBinding = node.resolveTypeBinding(); + if (typeBinding != null && Annotations.BEAN.equals(typeBinding.getQualifiedName()) && node.getParent() instanceof MethodDeclaration) { MethodDeclaration method = (MethodDeclaration) node.getParent(); - Version version = SpringProjectUtil.getDependencyVersion(project, SpringProjectUtil.SPRING_BOOT); - - if (version.getMajor() >= 2) { - IMethodBinding methodBinding = method.resolveBinding(); - if (isNotOverridingPublicMethod(methodBinding)) { - - ReconcileProblemImpl problem = ((List)method.modifiers()).stream() - .filter(Modifier.class::isInstance) - .map(Modifier.class::cast) - .filter(modifier -> modifier.isPublic()) - .findFirst() - .map(modifier -> new ReconcileProblemImpl( - Boot2JavaProblemType.JAVA_PUBLIC_BEAN_METHOD, Boot2JavaProblemType.JAVA_PUBLIC_BEAN_METHOD.getLabel(), - modifier.getStartPosition(), modifier.getLength())) - .orElse(new ReconcileProblemImpl( - Boot2JavaProblemType.JAVA_PUBLIC_BEAN_METHOD, Boot2JavaProblemType.JAVA_PUBLIC_BEAN_METHOD.getLabel(), - method.getName().getStartPosition(), method.getName().getLength())); + IMethodBinding methodBinding = method.resolveBinding(); + if (isNotOverridingPublicMethod(methodBinding)) { + + ReconcileProblemImpl problem = ((List)method.modifiers()).stream() + .filter(Modifier.class::isInstance) + .map(Modifier.class::cast) + .filter(modifier -> modifier.isPublic()) + .findFirst() + .map(modifier -> new ReconcileProblemImpl( + getProblemType(), Boot2JavaProblemType.JAVA_PUBLIC_BEAN_METHOD.getLabel(), + modifier.getStartPosition(), modifier.getLength())) + .orElse(new ReconcileProblemImpl( + getProblemType(), Boot2JavaProblemType.JAVA_PUBLIC_BEAN_METHOD.getLabel(), + method.getName().getStartPosition(), method.getName().getLength())); - addQuickFixes(doc, problem, method); - - problemCollector.accept(problem); - } + addQuickFixes(cu, docUri, problem, method); + + problemCollector.accept(problem); } } } @@ -101,26 +99,20 @@ public class BeanMethodNotPublicReconciler implements AnnotationReconciler { return !isOverriding(methodBinding) && (methodBinding.getModifiers() & Modifier.PUBLIC) != 0; } - private void addQuickFixes(IDocument doc, ReconcileProblemImpl problem, MethodDeclaration method) { - + private void addQuickFixes(CompilationUnit cu, URI docUri, ReconcileProblemImpl problem, MethodDeclaration method) { if (quickfixRegistry != null) { - FixDescriptor fix1 = new FixDescriptor(ID, List.of(doc.getUri()), LABEL) - .withRecipeScope(RecipeScope.NODE); + FixDescriptor fix1 = new FixDescriptor(ID, List.of(docUri.toASCIIString()), LABEL) + .withRecipeScope(RecipeScope.NODE) + .withRangeScope(RewriteQuickFixUtils.createOpenRewriteRange(cu, method)); - try { - Range methodRange = createOpenRewriteRange(doc, method); - fix1 = fix1.withRangeScope(methodRange); - } - catch (BadLocationException e) { - log.warn("bad location happened while calculating method range for " + method.toString(), e); - } - + Range methodRange = RewriteQuickFixUtils.createOpenRewriteRange(cu, method); + fix1 = fix1.withRangeScope(methodRange); - FixDescriptor fix2 = new FixDescriptor(ID, List.of(doc.getUri()), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.FILE)) + FixDescriptor fix2 = new FixDescriptor(ID, List.of(docUri.toASCIIString()), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.FILE)) .withRecipeScope(RecipeScope.FILE); - FixDescriptor fix3 = new FixDescriptor(ID, List.of(doc.getUri()), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.PROJECT)) + FixDescriptor fix3 = new FixDescriptor(ID, List.of(docUri.toASCIIString()), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.PROJECT)) .withRecipeScope(RecipeScope.PROJECT); @@ -136,21 +128,49 @@ public class BeanMethodNotPublicReconciler implements AnnotationReconciler { } } - private Range createOpenRewriteRange(IDocument doc, MethodDeclaration method) throws BadLocationException { - - int startOffset = method.getStartPosition(); - int endOffset = method.getStartPosition() + method.getLength(); + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, boolean isCompleteAst) { + cu.accept(new ASTVisitor() { - int startLine = doc.getLineOfOffset(startOffset); - int endLine = doc.getLineOfOffset(endOffset); - - int startColumn = startOffset - doc.getLineOffset(startLine); - int endColumn = endOffset - doc.getLineOffset(endLine); - - Range.Position startPosition = new Range.Position(startOffset, startLine, startColumn); - Range.Position endPosition = new Range.Position(endOffset, endLine, endColumn); - - return new Range(UUID.randomUUID(), startPosition, endPosition); + @Override + public boolean visit(SingleMemberAnnotation node) { + try { + visitAnnotation(project, cu, docUri, node, problemCollector); + } catch (Exception e) { + } + return super.visit(node); + } + + @Override + public boolean visit(NormalAnnotation node) { + try { + visitAnnotation(project, cu, docUri, node, problemCollector); + } catch (Exception e) { + } + return super.visit(node); + } + + @Override + public boolean visit(MarkerAnnotation node) { + try { + visitAnnotation(project, cu, docUri, node, problemCollector); + } catch (Exception e) { + } + return super.visit(node); + } + + }); + } + + @Override + public boolean isApplicable(IJavaProject project) { + Version version = SpringProjectUtil.getDependencyVersion(project, SpringProjectUtil.SPRING_BOOT); + return version != null && version.getMajor() >= 2; + } + + @Override + public ProblemType getProblemType() { + return Boot2JavaProblemType.JAVA_PUBLIC_BEAN_METHOD; } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/Boot3NotSupportedTypeReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/Boot3NotSupportedTypeReconciler.java new file mode 100644 index 000000000..5b2c62d2d --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/Boot3NotSupportedTypeReconciler.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * 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.List; + +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.ide.vscode.boot.java.Boot3JavaProblemType; +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 Boot3NotSupportedTypeReconciler implements JdtAstReconciler { + + private static final List TYPE_FQNAMES = List.of( + "org.springframework.web.multipart.commons.CommonsMultipartResolver", + "java.lang.SecurityManager", + "java.security.AccessControlException" + ); + + @Override + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, + boolean isCompleteAst) throws RequiredCompleteAstException { + cu.accept(new ASTVisitor() { + + @Override + public boolean visit(ImportDeclaration node) { + String fqName = node.getName().getFullyQualifiedName(); + if (TYPE_FQNAMES.contains(fqName)) { + problemCollector.accept(createProblem(fqName, node.getName().getStartPosition(), node.getName().getLength())); + } + return super.visit(node); + } + + @Override + public boolean visit(SimpleType node) { + String fqName = processType(cu, node.getName().getFullyQualifiedName()); + if (fqName != null) { + problemCollector.accept(createProblem(fqName, node.getStartPosition(), node.getLength())); + } + return super.visit(node); + } + + }); + } + + private String processType(CompilationUnit cu, String name) { + if (TYPE_FQNAMES.contains(name)) { + return name; + } else { + for (String fqName : createFqNamesFromWildcardImports(cu, name)) { + if (TYPE_FQNAMES.contains(fqName)) { + return fqName; + } + } + } + return null; + } + + private List createFqNamesFromWildcardImports(CompilationUnit cu, String name) { + List fqNames = new ArrayList<>(); + for (Object im : cu.imports()) { + ImportDeclaration importDecl = (ImportDeclaration) im; + if (importDecl.isOnDemand()) { + fqNames.add(importDecl.getName().getFullyQualifiedName() + "." + name); + } + } + return fqNames; + } + + @Override + public boolean isApplicable(IJavaProject project) { + return springBootVersionGreaterOrEqual(3, 0, 0).test(project); + } + + @Override + public ProblemType getProblemType() { + return Boot3JavaProblemType.JAVA_TYPE_NOT_SUPPORTED; + } + + private static String createLabel(String type) { + StringBuilder sb = new StringBuilder(); + sb.append("'"); + sb.append(type); + sb.append("' not supported as of Spring Boot 3"); + return sb.toString(); + } + + private ReconcileProblemImpl createProblem(String type, int offset, int length) { + return new ReconcileProblemImpl(getProblemType(), createLabel(type), offset, length); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtAstReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtAstReconciler.java new file mode 100644 index 000000000..6f6177f0d --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtAstReconciler.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * 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 org.eclipse.jdt.core.dom.CompilationUnit; +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; + +public interface JdtAstReconciler { + + void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, boolean isCompleteAst) throws RequiredCompleteAstException; + + boolean isApplicable(IJavaProject project); + + ProblemType getProblemType(); + +} 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 92e95ec90..586f04a53 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 @@ -11,25 +11,20 @@ package org.springframework.ide.vscode.boot.java.reconcilers; import java.net.URI; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; -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.SingleMemberAnnotation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.app.BootJavaConfig; -import org.springframework.ide.vscode.boot.java.Annotations; 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; @@ -54,44 +49,24 @@ public class JdtReconciler implements JavaReconciler { public static final String SPRING_CONDITIONAL_ON_EXPRESSION = "org.springframework.boot.autoconfigure.condition.ConditionalOnExpression"; private final CompilationUnitCache compilationUnitCache; - private final AnnotationReconciler[] reconcilers; + private final JdtAstReconciler[] reconcilers; private final SpelExpressionReconciler spelExpressionReconciler; - private BootJavaConfig config; - + public JdtReconciler(CompilationUnitCache compilationUnitCache, QuickfixRegistry quickfixRegistry, BootJavaConfig config) { this.compilationUnitCache = compilationUnitCache; this.config = config; config.addListener(evt -> setSpelExpressionSyntaxValidationEnabled(config.isSpelExpressionValidationEnabled())); this.spelExpressionReconciler = new SpelExpressionReconciler(); - this.reconcilers = new AnnotationReconciler[] { - - new AnnotationParamReconciler(Annotations.VALUE, null, "#{", "}", spelExpressionReconciler), - new AnnotationParamReconciler(Annotations.VALUE, "value", "#{", "}", spelExpressionReconciler), - - new AnnotationParamReconciler(SPRING_CACHEABLE, "key", "", "", spelExpressionReconciler), - new AnnotationParamReconciler(SPRING_CACHEABLE, "condition", "", "", spelExpressionReconciler), - new AnnotationParamReconciler(SPRING_CACHEABLE, "unless", "", "", spelExpressionReconciler), - - new AnnotationParamReconciler(SPRING_CACHE_EVICT, "key", "", "", spelExpressionReconciler), - new AnnotationParamReconciler(SPRING_CACHE_EVICT, "condition", "", "", spelExpressionReconciler), - - new AnnotationParamReconciler(SPRING_EVENT_LISTENER, "condition", "", "", spelExpressionReconciler), - - new AnnotationParamReconciler(SPRING_PRE_AUTHORIZE, null, "", "", spelExpressionReconciler), - new AnnotationParamReconciler(SPRING_PRE_AUTHORIZE, "value", "", "", spelExpressionReconciler), - new AnnotationParamReconciler(SPRING_PRE_FILTER, null, "", "", spelExpressionReconciler), - new AnnotationParamReconciler(SPRING_PRE_FILTER, "value", "", "", spelExpressionReconciler), - new AnnotationParamReconciler(SPRING_POST_AUTHORIZE, null, "", "", spelExpressionReconciler), - new AnnotationParamReconciler(SPRING_POST_AUTHORIZE, "value", "", "", spelExpressionReconciler), - new AnnotationParamReconciler(SPRING_POST_FILTER, null, "", "", spelExpressionReconciler), - new AnnotationParamReconciler(SPRING_POST_FILTER, "value", "", "", spelExpressionReconciler), - - new AnnotationParamReconciler(SPRING_CONDITIONAL_ON_EXPRESSION, null, "", "", spelExpressionReconciler), - new AnnotationParamReconciler(SPRING_CONDITIONAL_ON_EXPRESSION, "value", "", "", spelExpressionReconciler), - - new BeanMethodNotPublicReconciler(quickfixRegistry) + this.reconcilers = new JdtAstReconciler[] { + new AnnotationNodeReconciler(config), + new BeanMethodNotPublicReconciler(quickfixRegistry), + new AddConfigurationIfBeansPresentReconciler(quickfixRegistry), + new AutowiredFieldIntoConstructorParameterReconciler(quickfixRegistry), + new Boot3NotSupportedTypeReconciler(), + new NoAutowiredOnConstructorReconciler(quickfixRegistry), + new WebSecurityConfigurerAdapterReconciler(quickfixRegistry) }; } @@ -105,65 +80,54 @@ public class JdtReconciler implements JavaReconciler { URI uri = URI.create(doc.getUri()); compilationUnitCache.withCompilationUnit(project, uri, cu -> { if (cu != null) { - reconcileAST(project, doc, cu, problemCollector); + try { + reconcile(project, URI.create(doc.getUri()), cu, problemCollector, true); + } catch (RequiredCompleteAstException e) { + log.error("Unexpected incomplete AST", e); + } } log.info("reconciling (JDT): " + doc.getUri() + " done in " + (System.currentTimeMillis() - s) + "ms"); return null; }); } - private void reconcileAST(IJavaProject project, IDocument doc, CompilationUnit cu, IProblemCollector problemCollector) { - cu.accept(new ASTVisitor() { - - @Override - public boolean visit(SingleMemberAnnotation node) { - try { - visitAnnotation(project, doc, node, problemCollector); - } - catch (Exception e) { - } - return super.visit(node); - } - - @Override - public boolean visit(NormalAnnotation node) { - try { - visitAnnotation(project, doc, node, problemCollector); - } - catch (Exception e) { - } - return super.visit(node); - } - @Override - public boolean visit(MarkerAnnotation node) { - try { - visitAnnotation(project, doc, node, problemCollector); - } - catch (Exception e) { - } - return super.visit(node); - } - - }); - } - - protected void visitAnnotation(IJavaProject project, IDocument doc, Annotation node, IProblemCollector problemCollector) { - ITypeBinding typeBinding = node.resolveTypeBinding(); - - if (typeBinding != null) { - for (int i = 0; i < reconcilers.length; i++) { - reconcilers[i].visit(project, doc, node, typeBinding, problemCollector); + public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector, boolean isCompleteAst) throws RequiredCompleteAstException { + for (JdtAstReconciler reconciler : getApplicableReconcilers(project)) { + try { + reconciler.reconcile(project, docUri, cu, problemCollector, isCompleteAst); + } catch (RequiredCompleteAstException e) { + throw e; + } catch (Exception e) { + log.error("", e); } } } + + private List getApplicableReconcilers(IJavaProject project) { + List applicableReconcilers = new ArrayList<>(reconcilers.length); + for (JdtAstReconciler r : reconcilers) { + switch (config.getProblemApplicability(r.getProblemType())) { + case ON: + if (SpringProjectUtil.isBootProject(project)) { + applicableReconcilers.add(r); + } + break; + case OFF: + break; + default: // AUTO + if (r.isApplicable(project)) { + applicableReconcilers.add(r); + } + } + } + return applicableReconcilers; + } + @Override - public Map> reconcile(IJavaProject project, List docs, Runnable incrementProgress) { - - if (config.isRewriteReconcileEnabled()) { - } - + public Map> reconcile(IJavaProject project, List docs, + Runnable incrementProgress) { return Collections.emptyMap(); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoAutowiredOnConstructorReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoAutowiredOnConstructorReconciler.java new file mode 100644 index 000000000..0ea66d69d --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/NoAutowiredOnConstructorReconciler.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * 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.nio.file.Path; +import java.nio.file.Paths; +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.MethodDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.openrewrite.java.spring.NoAutowiredOnConstructor; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; +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.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; + +public class NoAutowiredOnConstructorReconciler implements JdtAstReconciler { + + private static final String ID = NoAutowiredOnConstructor.class.getName(); + private static final String LABEL = "Remove Unnecessary @Autowired"; + + private QuickfixRegistry registry; + + public NoAutowiredOnConstructorReconciler(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) { + Path sourceFile = Paths.get(docUri); + if (IClasspathUtil.getProjectJavaSourceFoldersWithoutTests(project.getClasspath()) + .anyMatch(f -> sourceFile.startsWith(f.toPath()))) { + + int constructorCount = 0; + MethodDeclaration constructor = null; + for (MethodDeclaration method : typeDecl.getMethods()) { + if (method.isConstructor()) { + constructorCount++; + if (constructorCount > 1) { + return super.visit(typeDecl); + } else { + constructor = method; + } + } + } + + if (constructor != null) { + Annotation autowiredAnnotation = RewriteQuickFixUtils.findAnnotation(constructor, + Annotations.AUTOWIRED, false); + if (autowiredAnnotation != null) { + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL, + autowiredAnnotation.getStartPosition(), autowiredAnnotation.getLength()); + RewriteQuickFixUtils.setRewriteFixes(registry, problem, + List.of(new FixDescriptor(ID, List.of(docUri.toASCIIString()), LABEL) + .withRecipeScope(RecipeScope.NODE) + .withRangeScope(RewriteQuickFixUtils.createOpenRewriteRange(cu, typeDecl)))); + problemCollector.accept(problem); + } + } + + } + return super.visit(typeDecl); + } + + }); + } + + @Override + public boolean isApplicable(IJavaProject project) { + return springBootVersionGreaterOrEqual(2, 0, 0).test(project); + } + + @Override + public ProblemType getProblemType() { + return Boot2JavaProblemType.JAVA_AUTOWIRED_CONSTRUCTOR; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RequiredCompleteAstException.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RequiredCompleteAstException.java new file mode 100644 index 000000000..8b61a497e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RequiredCompleteAstException.java @@ -0,0 +1,17 @@ +/******************************************************************************* + * 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; + +public class RequiredCompleteAstException extends Exception { + + private static final long serialVersionUID = 1L; + +} 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 new file mode 100644 index 000000000..9106ee579 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/RewriteQuickFixUtils.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * 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.util.Collection; +import java.util.Iterator; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.BodyDeclaration; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.openrewrite.Tree; +import org.openrewrite.marker.Range; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; +import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings; +import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix.QuickfixData; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixType; +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 RewriteQuickFixUtils { + + public static Range createOpenRewriteRange(CompilationUnit cu, ASTNode node) { + + int startOffset = node.getStartPosition(); + int startLine = cu.getLineNumber(startOffset); + int startColumn = cu.getColumnNumber(startOffset); + + int endOffset = startOffset + node.getLength() - 1; + int endLine = cu.getLineNumber(endOffset); + int endColumn = cu.getColumnNumber(endOffset); + + Range.Position startPosition = new Range.Position(startOffset, startLine, startColumn); + Range.Position endPosition = new Range.Position(endOffset, endLine, endColumn); + + return new Range(Tree.randomId(), startPosition, endPosition); + } + + public static QuickfixType getRewriteQuickFixType(QuickfixRegistry registry) { + return registry.getQuickfixType(RewriteRefactorings.REWRITE_RECIPE_QUICKFIX); + } + + public static void setRewriteFixes(QuickfixRegistry registry, ReconcileProblemImpl problem, Collection fixDescritptors) { + QuickfixType quickFixType = getRewriteQuickFixType(registry); + for (FixDescriptor f : fixDescritptors) { + problem.addQuickfix(new QuickfixData<>(quickFixType, f, f.getLabel())); + } + } + + public static String buildLabel(String label, RecipeScope s) { + switch (s) { + case FILE: + return label + " in file"; + case PROJECT: + return label + " in project"; + default: + return label; + } + } + + public static Annotation findAnnotation(BodyDeclaration decl, String annotationFqType, boolean includeMetaHierarchy) { + for (Iterator itr = decl.modifiers().iterator(); itr.hasNext();) { + Object mod = itr.next(); + if (mod instanceof Annotation) { + Annotation a = (Annotation) mod; + ITypeBinding aType = a.resolveTypeBinding(); + if (aType != null && ( + (includeMetaHierarchy && AnnotationHierarchies.isSubtypeOf(a, annotationFqType)) || (!includeMetaHierarchy && annotationFqType.equals(aType.getQualifiedName())) + )) { + return (Annotation) mod; + } + } + } + return null; + } + + public static ITypeBinding getDeepErasureType(ITypeBinding type) { + for (; type != type.getErasure(); type = type.getErasure()) {} + return type; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/WebSecurityConfigurerAdapterReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/WebSecurityConfigurerAdapterReconciler.java new file mode 100644 index 000000000..0d36435dd --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/WebSecurityConfigurerAdapterReconciler.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * 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.util.List; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.SimpleType; +import org.eclipse.jdt.core.dom.Type; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.openrewrite.java.spring.security5.WebSecurityConfigurerAdapter; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.SpringProjectUtil; +import org.springframework.ide.vscode.commons.java.Version; +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 WebSecurityConfigurerAdapterReconciler implements JdtAstReconciler { + + private static final String ID = WebSecurityConfigurerAdapter.class.getName(); + + private static final String WEB_SECURITY_CONFIGURER_ADAPTER = "WebSecurityConfigurerAdapter"; + + private static final String FQN_WEB_SECURITY_CONFIGURER_ADAPTER = "org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter"; + + private static final String PROBLEM_LABEL = "Class extends 'WebSecurityConfigurerAdapter' which is removed in Spring-Security 6.x"; + + private static final String FIX_LABEL = "Refactor class into a Configuration bean not extending 'WebSecurityConfigurerAdapter'"; + + private static final String STUB_WEB_SECURITY_CONFIG_ADAPTER = """ + package org.springframework.security.config.annotation.web.configuration; + + import org.springframework.security.authentication.AuthenticationManager; + import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; + import org.springframework.security.config.annotation.web.WebSecurityConfigurer; + import org.springframework.security.config.annotation.web.builders.HttpSecurity; + import org.springframework.security.config.annotation.web.builders.WebSecurity; + import org.springframework.security.core.userdetails.UserDetailsService; + + public abstract class WebSecurityConfigurerAdapter { + + public void init(WebSecurity web) throws Exception {} + + public AuthenticationManager authenticationManagerBean() throws Exception { return null; } + + public UserDetailsService userDetailsServiceBean() throws Exception { return null; } + + protected void configure(HttpSecurity http) throws Exception {} + + public void configure(WebSecurity web) throws Exception {} + + protected void configure(AuthenticationManagerBuilder auth) throws Exception {} + } + """; + + private QuickfixRegistry registry; + + public WebSecurityConfigurerAdapterReconciler(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) { + Type type = typeDecl.getSuperclassType(); + if (isWebSecurityConfigurerAdapter(cu, type)) { + ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), PROBLEM_LABEL, type.getStartPosition(), type.getLength()); + if (RewriteQuickFixUtils.findAnnotation(typeDecl, Annotations.CONFIGURATION, true) != null) { + ITypeBinding resolveBinding = type.resolveBinding(); + String[] typeStubs = resolveBinding == null || resolveBinding.isRecovered() ? new String[] { STUB_WEB_SECURITY_CONFIG_ADAPTER } : new String[0]; + String uri = docUri.toASCIIString(); + RewriteQuickFixUtils.setRewriteFixes(registry, problem, List.of( + new FixDescriptor(ID, List.of(uri), + RecipeCodeActionDescriptor.buildLabel(FIX_LABEL, RecipeScope.FILE)) + .withRecipeScope(RecipeScope.FILE) + .withTypeStubs(typeStubs), + new FixDescriptor(ID, List.of(uri), + RecipeCodeActionDescriptor.buildLabel(FIX_LABEL, RecipeScope.PROJECT)) + .withRecipeScope(RecipeScope.PROJECT) + .withTypeStubs(typeStubs)) + + ); + } + problemCollector.accept(problem); + } + return super.visit(typeDecl); + } + + }); + } + + private static boolean isWebSecurityConfigurerAdapter(CompilationUnit cu, Type type) { + if (type.isSimpleType()) { + String simpleName = ((SimpleType) type).getName().getFullyQualifiedName(); + if (FQN_WEB_SECURITY_CONFIGURER_ADAPTER.equals(simpleName)) { + return true; + } else if (WEB_SECURITY_CONFIGURER_ADAPTER.equals(simpleName)) { + // check imports + for (Object im : cu.imports()) { + ImportDeclaration importDecl = (ImportDeclaration) im; + if (importDecl.isOnDemand()) { + if (FQN_WEB_SECURITY_CONFIGURER_ADAPTER.equals(importDecl.getName().getFullyQualifiedName() + "." + WEB_SECURITY_CONFIGURER_ADAPTER)) { + return true; + } + } else if (FQN_WEB_SECURITY_CONFIGURER_ADAPTER.equals(importDecl.getName().getFullyQualifiedName())) { + return true; + } + } + } + } + return false; + } + + @Override + public boolean isApplicable(IJavaProject project) { + Version version = SpringProjectUtil.getDependencyVersion(project, "spring-security-config"); + return version != null && version.compareTo(new Version(5, 7, 0, null)) >= 0 && version.compareTo(new Version(6, 1, 0, null)) < 0; + } + + @Override + public ProblemType getProblemType() { + return Boot2JavaProblemType.WEB_SECURITY_CONFIGURER_ADAPTER; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java index d8656015d..879f8f3c1 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java @@ -202,7 +202,8 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler if (j != null) { Range range = j.getMarkers().findFirst(Range.class).orElse(null); if (range != null) { - return d.getRangeScope().getStart().getOffset() <= range.getStart().getOffset() && range.getEnd().getOffset() <= d.getRangeScope().getEnd().getOffset(); + // Rewrite range end offset is up to not including hence -1 + return d.getRangeScope().getStart().getOffset() <= range.getStart().getOffset() && range.getEnd().getOffset() - 1 <= d.getRangeScope().getEnd().getOffset(); } } return false; 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 56ce41d3b..92450aad8 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 @@ -58,8 +58,9 @@ import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyA import org.springframework.ide.vscode.boot.java.beans.CachedBean; import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; -import org.springframework.ide.vscode.boot.java.reconcilers.AnnotationReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.CachedDiagnostics; +import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler; import org.springframework.ide.vscode.commons.java.IClasspath; import org.springframework.ide.vscode.commons.java.IClasspathUtil; import org.springframework.ide.vscode.commons.java.IJavaProject; @@ -99,7 +100,7 @@ public class SpringIndexerJava implements SpringIndexer { private final SymbolHandler symbolHandler; private final AnnotationHierarchyAwareLookup symbolProviders; - private final List reconcilers; + private final JdtReconciler reconciler; private final IndexCache cache; private final JavaProjectFinder projectFinder; private final ProgressService progressService; @@ -114,12 +115,12 @@ public class SpringIndexerJava implements SpringIndexer { public SpringIndexerJava(SymbolHandler symbolHandler, AnnotationHierarchyAwareLookup symbolProviders, IndexCache cache, - JavaProjectFinder projectFimder, ProgressService progressService, List reconcilers, + JavaProjectFinder projectFimder, ProgressService progressService, JdtReconciler jdtReconciler, BiFunction, BiConsumer, IProblemCollector> problemCollectorCreator, JsonObject validationSeveritySettings) { this.symbolHandler = symbolHandler; this.symbolProviders = symbolProviders; - this.reconcilers = reconcilers; + this.reconciler = jdtReconciler; this.cache = cache; this.projectFinder = projectFimder; this.progressService = progressService; @@ -641,6 +642,13 @@ public class SpringIndexerJava implements SpringIndexer { } }); + // reconciling + try { + reconciler.reconcile(context.getProject(), URI.create(context.getDocURI()), context.getCu(), context.getProblemCollector(), context.getPass() == SCAN_PASS.TWO); + } catch (RequiredCompleteAstException e) { + context.getNextPassFiles().add(context.getFile()); + } + dependencyTracker.update(context.getFile(), context.getDependencies());; } @@ -686,11 +694,6 @@ public class SpringIndexerJava implements SpringIndexer { } } - // reconciling - for (AnnotationReconciler reconciler : this.reconcilers) { - reconciler.visit(context.getProject(), context.getDocRef().get(), node, typeBinding, context.getProblemCollector()); - } - } else { log.debug("type binding not around: " + context.getDocURI() + " - " + node.toString()); @@ -715,7 +718,7 @@ public class SpringIndexerJava implements SpringIndexer { return null; } - private ASTParser createParser(IJavaProject project, boolean ignoreMethodBodies) throws Exception { + public static ASTParser createParser(IJavaProject project, boolean ignoreMethodBodies) throws Exception { String[] classpathEntries = getClasspathEntries(project); String[] sourceEntries = getSourceEntries(project); @@ -733,7 +736,7 @@ public class SpringIndexerJava implements SpringIndexer { return parser; } - private String[] getClasspathEntries(IJavaProject project) throws Exception { + private static String[] getClasspathEntries(IJavaProject project) throws Exception { IClasspath classpath = project.getClasspath(); Stream classpathEntries = IClasspathUtil.getAllBinaryRoots(classpath).stream(); return classpathEntries @@ -742,7 +745,7 @@ public class SpringIndexerJava implements SpringIndexer { .toArray(String[]::new); } - private String[] getSourceEntries(IJavaProject project) throws Exception { + private static String[] getSourceEntries(IJavaProject project) throws Exception { IClasspath classpath = project.getClasspath(); Stream sourceEntries = IClasspathUtil.getSourceFolders(classpath); return sourceEntries diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/AutowiredFieldIntoConstructorParameterReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/AutowiredFieldIntoConstructorParameterReconcilerTest.java new file mode 100644 index 000000000..1e5cd68e9 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/AutowiredFieldIntoConstructorParameterReconcilerTest.java @@ -0,0 +1,224 @@ +/******************************************************************************* + * 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.Boot2JavaProblemType; +import org.springframework.ide.vscode.boot.java.reconcilers.AutowiredFieldIntoConstructorParameterReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +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; + +public class AutowiredFieldIntoConstructorParameterReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "autowiredfieldtest"; + } + + @Override + protected String getProjectName() { + return "test-spring-indexing"; + } + + protected JdtAstReconciler getReconciler() { + return new AutowiredFieldIntoConstructorParameterReconciler(new QuickfixRegistry()); + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void noConstructors() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.annotation.Autowired; + + class A { + + @Autowired + String a; + + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.JAVA_CONSTRUCTOR_PARAMETER_INJECTION, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("@Autowired\n\tString a;", markedStr); + + assertEquals(1, problem.getQuickfixes().size()); + + } + + @Test + void singleSimpleConstructorsNoMethodBodies() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.annotation.Autowired; + + class A { + + @Autowired + String a; + + A() { + } + + } + """; + try { + reconcile("A.java", source, false); + fail("Should require complete AST. Exception must be thrown."); + } catch (RequiredCompleteAstException e) { + } + + } + + @Test + void singleSimpleConstructorsWithMethodBodies() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.annotation.Autowired; + + class A { + + @Autowired + String a; + + A() { + } + + } + """; + List problems = reconcile("A.java", source, true); + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.JAVA_CONSTRUCTOR_PARAMETER_INJECTION, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("@Autowired\n\tString a;", markedStr); + + assertEquals(1, problem.getQuickfixes().size()); + + } + + @Test + void constrctorAssignsField() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.annotation.Autowired; + + class A { + + @Autowired + String a; + + A() { + this.a = "qq" + } + + } + """; + List problems = reconcile("A.java", source, true); + assertEquals(0, problems.size()); + + } + + @Test + void autowiredConstructor() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.annotation.Autowired; + + class A { + + @Autowired + String a; + + A(int y) { + } + + @Autowired + A() { + } + + } + """; + List problems = reconcile("A.java", source, true); + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.JAVA_CONSTRUCTOR_PARAMETER_INJECTION, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("@Autowired\n\tString a;", markedStr); + + assertEquals(1, problem.getQuickfixes().size()); + + } + + @Test + void autowiredConstructorAssigningField() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.annotation.Autowired; + + class A { + + @Autowired + String a; + + A(int y) { + } + + @Autowired + A() { + this.a = "qq" + } + + } + """; + 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/BaseReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BaseReconcilerTest.java new file mode 100644 index 000000000..b7d7222e7 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/BaseReconcilerTest.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * 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.assertFalse; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.FileASTRequestor; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException; +import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJava; +import org.springframework.ide.vscode.boot.java.value.test.ValueSpelExpressionValidationTest.TestProblemCollector; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; + +public abstract class BaseReconcilerTest { + + protected IJavaProject project; + + abstract protected String getFolder(); + + abstract protected String getProjectName(); + + abstract protected JdtAstReconciler getReconciler(); + + private Path createFile(String name, String content) throws IOException { + Path filePath = Paths.get(project.getLocationUri()).resolve("src/main/java").resolve(getFolder()).resolve(name); + Files.createDirectories(filePath.getParent()); + Files.createFile(filePath); + Files.write(filePath, content.getBytes(StandardCharsets.UTF_8)); + return filePath; + } + + private void clearTestFiles() throws IOException, URISyntaxException { + Path projectPath = Paths.get(getClass().getResource("/test-projects/" + getProjectName()).toURI()); + Path toRemove = projectPath.resolve("src/main/java").resolve(getFolder()); + if (Files.exists(toRemove)) { + Files.walk(toRemove).sorted(Comparator.reverseOrder()).forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + assertFalse(Files.exists(toRemove)); + } + + void setup() throws Exception { + clearTestFiles(); + project = ProjectsHarness.INSTANCE.mavenProject(getProjectName()); + } + + void tearDown() throws Exception { + clearTestFiles(); + } + + List reconcile(String fileName, String source, boolean isCompleteAst) throws Exception { + Path path = createFile(fileName, source); + TestProblemCollector problemCollector = new TestProblemCollector(); + AtomicBoolean requiredCompleteAst = new AtomicBoolean(false); + SpringIndexerJava.createParser(project, !isCompleteAst).createASTs(new String[] { path.toFile().toString() }, null, new String[0], new FileASTRequestor() { + + @Override + public void acceptAST(String sourceFilePath, CompilationUnit cu) { + try { + getReconciler().reconcile(project, path.toUri(), cu, problemCollector, isCompleteAst); + } catch (RequiredCompleteAstException e) { + requiredCompleteAst.set(true); + } + } + }, null); + + if (requiredCompleteAst.get()) { + throw new RequiredCompleteAstException(); + } + + return problemCollector.getCollectedProblems(); + } + + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/Boot3NotSupportedTypeReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/Boot3NotSupportedTypeReconcilerTest.java new file mode 100644 index 000000000..f79856133 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/Boot3NotSupportedTypeReconcilerTest.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * 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.Boot3JavaProblemType; +import org.springframework.ide.vscode.boot.java.reconcilers.Boot3NotSupportedTypeReconciler; +import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; + +public class Boot3NotSupportedTypeReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "boot3notsupportedtypes"; + } + + @Override + protected String getProjectName() { + return "test-spring-indexing"; + } + + @Override + protected JdtAstReconciler getReconciler() { + return new Boot3NotSupportedTypeReconciler(); + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void imports() throws Exception { + String source = """ + package example.demo; + + import org.springframework.web.multipart.commons.CommonsMultipartResolver; + + class A { + + CommonsMultipartResolver a; + + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot3JavaProblemType.JAVA_TYPE_NOT_SUPPORTED, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("org.springframework.web.multipart.commons.CommonsMultipartResolver", markedStr); + + assertEquals(0, problem.getQuickfixes().size()); + + } + + @Test + void fqType() throws Exception { + String source = """ + package example.demo; + + class A { + + org.springframework.web.multipart.commons.CommonsMultipartResolver a; + + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot3JavaProblemType.JAVA_TYPE_NOT_SUPPORTED, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("org.springframework.web.multipart.commons.CommonsMultipartResolver", markedStr); + + assertEquals(0, problem.getQuickfixes().size()); + + } + + @Test + void importWithWildcard() throws Exception { + String source = """ + package example.demo; + + import org.springframework.web.multipart.commons.*; + + class A { + + CommonsMultipartResolver a; + + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot3JavaProblemType.JAVA_TYPE_NOT_SUPPORTED, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("CommonsMultipartResolver", markedStr); + + assertEquals(0, problem.getQuickfixes().size()); + + } + + @Test + void importWithWildcardNoProblem() throws Exception { + String source = """ + package example.demo; + + import org.springframework.web.multipart.*; + + class A { + + CommonsMultipartResolver a; + + } + """; + 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/NoAutowiredOnConstructorReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoAutowiredOnConstructorReconcilerTest.java new file mode 100644 index 000000000..7add77d7b --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/NoAutowiredOnConstructorReconcilerTest.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * 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.NoAutowiredOnConstructorReconciler; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; + +public class NoAutowiredOnConstructorReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "noautowiredonconstructor"; + } + + @Override + protected String getProjectName() { + return "test-spring-validations"; + } + + @Override + protected JdtAstReconciler getReconciler() { + return new NoAutowiredOnConstructorReconciler(new QuickfixRegistry()); + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void singleConstructors() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.annotation.Autowired; + + class A { + + @Autowired + A() {}; + + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.JAVA_AUTOWIRED_CONSTRUCTOR, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("@Autowired", markedStr); + + assertEquals(1, problem.getQuickfixes().size()); + + } + + @Test + void multipleConstructors() throws Exception { + String source = """ + package example.demo; + + import org.springframework.beans.factory.annotation.Autowired; + + class A { + + @Autowired + A() {}; + + A(int k) {} + + } + """; + 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/WebSecurityConfigurerAdapterReconcilerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/WebSecurityConfigurerAdapterReconcilerTest.java new file mode 100644 index 000000000..df63eeb88 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/reconcilers/test/WebSecurityConfigurerAdapterReconcilerTest.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * 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.WebSecurityConfigurerAdapterReconciler; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; + +public class WebSecurityConfigurerAdapterReconcilerTest extends BaseReconcilerTest { + + @Override + protected String getFolder() { + return "websecurityconfigurer"; + } + + @Override + protected String getProjectName() { + return "test-spring-indexing"; + } + + protected JdtAstReconciler getReconciler() { + return new WebSecurityConfigurerAdapterReconciler(new QuickfixRegistry()); + } + + @BeforeEach + void setup() throws Exception { + super.setup(); + } + + @AfterEach + void tearDown() throws Exception { + super.tearDown(); + } + + @Test + void typeImport() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + import org.springframework.context.annotation.Configuration; + + @Configuration + class A extends WebSecurityConfigurerAdapter { + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.WEB_SECURITY_CONFIGURER_ADAPTER, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("WebSecurityConfigurerAdapter", markedStr); + + assertEquals(2, problem.getQuickfixes().size()); + + } + + @Test + void typeImportWithWildCard() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.annotation.web.configuration.*; + import org.springframework.context.annotation.Configuration; + + @Configuration + class A extends WebSecurityConfigurerAdapter { + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.WEB_SECURITY_CONFIGURER_ADAPTER, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("WebSecurityConfigurerAdapter", markedStr); + + assertEquals(2, problem.getQuickfixes().size()); + } + + @Test + void fqType() throws Exception { + String source = """ + package example.demo; + + import org.springframework.context.annotation.Configuration; + + @Configuration + class A extends org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter { + } + """; + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.WEB_SECURITY_CONFIGURER_ADAPTER, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter", markedStr); + + assertEquals(2, problem.getQuickfixes().size()); + } + + @Test + void notConfigBean() throws Exception { + String source = """ + package example.demo; + + import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + + class A extends WebSecurityConfigurerAdapter { + } + """; + + List problems = reconcile("A.java", source, false); + + assertEquals(1, problems.size()); + + ReconcileProblem problem = problems.get(0); + + assertEquals(Boot2JavaProblemType.WEB_SECURITY_CONFIGURER_ADAPTER, problem.getType()); + + String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength()); + assertEquals("WebSecurityConfigurerAdapter", markedStr); + + assertEquals(0, problem.getQuickfixes().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 925d57852..b653b34c3 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 @@ -347,7 +347,7 @@ public class ValueSpelExpressionValidationTest { problems.add(problem); } - protected List getCollectedProblems() { + public List getCollectedProblems() { return problems; } diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/pom.xml b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/pom.xml index adc7e8d19..8e5b72589 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/pom.xml +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-indexing/pom.xml @@ -39,6 +39,10 @@ org.springframework.boot spring-boot-starter-actuator + + org.springframework.boot + spring-boot-starter-security + org.springframework.boot spring-boot-starter-test