Next batch of JDT reconcilers

This commit is contained in:
aboyko
2023-08-09 02:14:01 -04:00
parent 231ad105a4
commit 061f07ca93
18 changed files with 1537 additions and 39 deletions

View File

@@ -62,6 +62,7 @@ import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessConnec
import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessConnectorService;
import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveDataProvider;
import org.springframework.ide.vscode.boot.java.reconcilers.JavaReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRecipeRepository;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
@@ -241,8 +242,8 @@ public class BootLanguageServerBootApp {
return new CompilationUnitCache(params.projectFinder, server, params.projectObserver);
}
@Bean JdtReconciler jdtReconciler(CompilationUnitCache cuCache, BootJavaConfig config, SimpleLanguageServer server) {
return new JdtReconciler(cuCache, server.getQuickfixRegistry(), config);
@Bean JdtReconciler jdtReconciler(CompilationUnitCache cuCache, BootJavaConfig config, SimpleLanguageServer server, JdtAstReconciler[] reconcilers) {
return new JdtReconciler(cuCache, config, reconcilers);
}
@Bean SpringXMLCompletionEngine xmlCompletionEngine(SimpleLanguageServer server, JavaProjectFinder projectFinder, SpringSymbolIndex symbolIndex, BootJavaConfig config) {
@@ -378,9 +379,9 @@ public class BootLanguageServerBootApp {
@Bean
ModulithService modulithService(SimpleLanguageServer server, JavaProjectFinder projectFinder,
ProjectObserver projectObserver, SpringSymbolIndex springIndex,
Optional<BootJavaProjectReconcilerScheduler> projectReconcileScheduler, BootJavaReconcileEngine reconciler,
BootJavaReconcileEngine reconciler,
BootJavaConfig config) {
return new ModulithService(server, projectFinder, projectObserver, springIndex, reconciler, projectReconcileScheduler, config);
return new ModulithService(server, projectFinder, projectObserver, springIndex, reconciler, config);
}
}

View File

@@ -0,0 +1,80 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.app;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ide.vscode.boot.java.reconcilers.AddConfigurationIfBeansPresentReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.AnnotationNodeReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.AutowiredFieldIntoConstructorParameterReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.BeanMethodNotPublicReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.Boot3NotSupportedTypeReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.ModulithTypeReferenceViolationReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.NoAutowiredOnConstructorReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.NoRepoAnnotationReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.NoRequestMappingAnnotationReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.PreciseBeanTypeReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.UnnecessarySpringExtensionReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.WebSecurityConfigurerAdapterReconciler;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
@Configuration(proxyBeanMethods = false)
public class JdtConfig {
@Bean AnnotationNodeReconciler annotationNodeReconciler(BootJavaConfig config) {
return new AnnotationNodeReconciler(config);
}
@Bean BeanMethodNotPublicReconciler beanMethodNotPublicReconciler(SimpleLanguageServer server) {
return new BeanMethodNotPublicReconciler(server.getQuickfixRegistry());
}
@Bean AddConfigurationIfBeansPresentReconciler addConfigurationIfBeansPresentReconciler(SimpleLanguageServer server) {
return new AddConfigurationIfBeansPresentReconciler(server.getQuickfixRegistry());
}
@Bean AutowiredFieldIntoConstructorParameterReconciler autowiredFieldIntoConstructorParameterReconciler(SimpleLanguageServer server) {
return new AutowiredFieldIntoConstructorParameterReconciler(server.getQuickfixRegistry());
}
@Bean Boot3NotSupportedTypeReconciler boot3NotSupportedTypeReconciler() {
return new Boot3NotSupportedTypeReconciler();
}
@Bean NoAutowiredOnConstructorReconciler noAutowiredOnConstructorReconciler(SimpleLanguageServer server) {
return new NoAutowiredOnConstructorReconciler(server.getQuickfixRegistry());
}
@Bean WebSecurityConfigurerAdapterReconciler webSecurityConfigurerAdapterReconciler(SimpleLanguageServer server) {
return new WebSecurityConfigurerAdapterReconciler(server.getQuickfixRegistry());
}
@Bean PreciseBeanTypeReconciler preciseBeanTypeReconciler(SimpleLanguageServer server) {
return new PreciseBeanTypeReconciler(server.getQuickfixRegistry());
}
@Bean NoRequestMappingAnnotationReconciler noRequestMappingAnnotationReconciler(SimpleLanguageServer server) {
return new NoRequestMappingAnnotationReconciler(server.getQuickfixRegistry());
}
@Bean ModulithTypeReferenceViolationReconciler modulithTypeReferenceViolationReconciler() {
return new ModulithTypeReferenceViolationReconciler();
}
@Bean NoRepoAnnotationReconciler noRepoAnnotationReconciler(SimpleLanguageServer server) {
return new NoRepoAnnotationReconciler(server.getQuickfixRegistry());
}
@Bean UnnecessarySpringExtensionReconciler unnecessarySpringExtensionReconciler(SimpleLanguageServer server) {
return new UnnecessarySpringExtensionReconciler(server.getQuickfixRegistry());
}
}

View File

@@ -59,7 +59,6 @@ public class AddConfigurationIfBeansPresentReconciler implements JdtAstReconcile
@Override
public boolean visit(TypeDeclaration classDecl) {
if (isApplicableClass(cu, classDecl)) {
SimpleName nameAst = classDecl.getName();
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), PROBLEM_LABEL,
nameAst.getStartPosition(), nameAst.getLength());

View File

@@ -21,11 +21,9 @@ import org.eclipse.jdt.core.dom.CompilationUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.app.BootJavaConfig;
import org.springframework.ide.vscode.boot.java.handlers.SpelExpressionReconciler;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
import org.springframework.ide.vscode.commons.util.text.IDocument;
@@ -50,28 +48,12 @@ public class JdtReconciler implements JavaReconciler {
private final CompilationUnitCache compilationUnitCache;
private final JdtAstReconciler[] reconcilers;
private final SpelExpressionReconciler spelExpressionReconciler;
private BootJavaConfig config;
public JdtReconciler(CompilationUnitCache compilationUnitCache, QuickfixRegistry quickfixRegistry, BootJavaConfig config) {
public JdtReconciler(CompilationUnitCache compilationUnitCache, BootJavaConfig config, JdtAstReconciler[] reconcilers) {
this.compilationUnitCache = compilationUnitCache;
this.config = config;
config.addListener(evt -> setSpelExpressionSyntaxValidationEnabled(config.isSpelExpressionValidationEnabled()));
this.spelExpressionReconciler = new SpelExpressionReconciler();
this.reconcilers = new JdtAstReconciler[] {
new AnnotationNodeReconciler(config),
new BeanMethodNotPublicReconciler(quickfixRegistry),
new AddConfigurationIfBeansPresentReconciler(quickfixRegistry),
new AutowiredFieldIntoConstructorParameterReconciler(quickfixRegistry),
new Boot3NotSupportedTypeReconciler(),
new NoAutowiredOnConstructorReconciler(quickfixRegistry),
new WebSecurityConfigurerAdapterReconciler(quickfixRegistry)
};
}
public void setSpelExpressionSyntaxValidationEnabled(boolean spelExpressionValidationEnabled) {
this.spelExpressionReconciler.setEnabled(spelExpressionValidationEnabled);
this.reconcilers = reconcilers;
}
@Override

View File

@@ -0,0 +1,122 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.reconcilers;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.SimpleType;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.ide.vscode.boot.java.Boot3JavaProblemType;
import org.springframework.ide.vscode.boot.modulith.AppModules;
import org.springframework.ide.vscode.boot.modulith.ModulithService;
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
public class ModulithTypeReferenceViolationReconciler implements JdtAstReconciler, ApplicationContextAware {
private ApplicationContext appContext;
@Override
public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector,
boolean isCompleteAst) throws RequiredCompleteAstException {
Path sourceFile = Paths.get(docUri);
if (IClasspathUtil.getProjectJavaSourceFoldersWithoutTests(project.getClasspath())
.anyMatch(f -> sourceFile.startsWith(f.toPath()))) {
ModulithService modulithService = appContext.getBean(ModulithService.class);
AppModules appModules = modulithService.getModulesData(project);
if (appModules != null) {
final String packageName = cu.getPackage().getName().getFullyQualifiedName();
cu.accept(new ASTVisitor() {
@Override
public boolean visit(ImportDeclaration node) {
if (!node.isOnDemand()) {
String typeFqName = node.getName().getFullyQualifiedName();
appModules.getModuleNotExposingType(packageName, getBinaryName(typeFqName)).ifPresent(module -> {
problemCollector.accept(new ReconcileProblemImpl(getProblemType(),
"Cannot use type in this package. Type is not exposed in module '"
+ module.name() + "'.",
node.getName().getStartPosition(), node.getName().getLength()));
});
}
return false;
}
@Override
public boolean visit(SimpleType node) {
if (node.getName().isQualifiedName()) {
appModules.getModuleNotExposingType(packageName, getBinaryName(node.getName().getFullyQualifiedName())).ifPresent(module -> {
problemCollector.accept(new ReconcileProblemImpl(getProblemType(),
"Cannot use type in this package. Type is not exposed in module '"
+ module.name() + "'.",
node.getName().getStartPosition(), node.getName().getLength()));
});
} else if (node.getName().isSimpleName()) {
String typeName = node.getName().getFullyQualifiedName();
for (Object i : cu.imports()) {
ImportDeclaration importDecl = (ImportDeclaration) i;
if (importDecl.isOnDemand()) {
appModules.getModuleNotExposingType(packageName, getBinaryName(importDecl.getName().getFullyQualifiedName() + "." + typeName)).ifPresent(module -> {
problemCollector.accept(new ReconcileProblemImpl(getProblemType(),
"Cannot use type in this package. Type is not exposed in module '"
+ module.name() + "'.",
node.getName().getStartPosition(), node.getName().getLength()));
});
}
}
}
return super.visit(node);
}
});
}
}
}
private static String getBinaryName(String fqName) {
String pkgName = ModulithService.getPackageNameFromTypeFQName(fqName);
if (pkgName.length() < fqName.length() - 1) {
String typeName = fqName.substring(pkgName.length() + 1);
StringBuilder sb = new StringBuilder();
sb.append(pkgName);
sb.append('.');
sb.append(typeName.replace('.', '$'));
return sb.toString();
}
return fqName;
}
@Override
public boolean isApplicable(IJavaProject project) {
return ModulithService.isModulithDependentProject(project);
}
@Override
public ProblemType getProblemType() {
return Boot3JavaProblemType.MODULITH_TYPE_REF_VIOLATION;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.appContext = applicationContext;
}
}

View File

@@ -0,0 +1,126 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.reconcilers;
import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual;
import java.net.URI;
import java.util.List;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.openrewrite.java.spring.NoRepoAnnotationOnRepoInterface;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeCodeActionDescriptor;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
public class NoRepoAnnotationReconciler implements JdtAstReconciler {
private static final String ID = NoRepoAnnotationOnRepoInterface.class.getName();
private static final String LABEL = "Remove Unnecessary @Repository";
private static final String INTERFACE_REPOSITORY = "org.springframework.data.repository.Repository";
private QuickfixRegistry registry;
public NoRepoAnnotationReconciler(QuickfixRegistry registry) {
this.registry = registry;
}
@Override
public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector,
boolean isCompleteAst) throws RequiredCompleteAstException {
cu.accept(new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration typeDecl) {
if (typeDecl.isInterface()) {
for (Object o : typeDecl.modifiers()) {
if (o instanceof Annotation) {
Annotation a = (Annotation) o;
if (isApplicableRepoAnnotation(a)) {
ITypeBinding type = typeDecl.resolveBinding();
if (type != null && isRepo(type)) {
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL, a.getStartPosition(), a.getLength());
String uri = docUri.toASCIIString();
RewriteQuickFixUtils.setRewriteFixes(registry, problem, List.of(
// new FixDescriptor(ID, List.of(uri), LABEL)
// .withRangeScope(RewriteQuickFixUtils.createOpenRewriteRange(cu, typeDecl))
// .withRecipeScope(RecipeScope.NODE),
new FixDescriptor(ID, List.of(uri),
RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.FILE))
.withRecipeScope(RecipeScope.FILE),
new FixDescriptor(ID, List.of(uri),
RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.PROJECT))
.withRecipeScope(RecipeScope.PROJECT)
));
problemCollector.accept(problem);
}
}
}
}
}
return super.visit(typeDecl);
}
private boolean isApplicableRepoAnnotation(Annotation a) {
if (a instanceof MarkerAnnotation || (a.isNormalAnnotation() && ((NormalAnnotation) a).properties().isEmpty())) {
String typeName = a.getTypeName().getFullyQualifiedName();
if (Annotations.REPOSITORY.equals(typeName)) {
return true;
} else if (typeName.endsWith("Repository")) {
ITypeBinding type = a.resolveTypeBinding();
if (type != null && Annotations.REPOSITORY.equals(type.getQualifiedName())) {
return true;
}
}
}
return false;
}
private boolean isRepo(ITypeBinding t) {
if (INTERFACE_REPOSITORY.equals(t.getQualifiedName())) {
return true;
} else {
for (ITypeBinding st : t.getInterfaces()) {
if (isRepo(st)) {
return true;
}
}
}
return false;
}
});
}
@Override
public boolean isApplicable(IJavaProject project) {
return springBootVersionGreaterOrEqual(2, 0, 0).test(project);
}
@Override
public Boot2JavaProblemType getProblemType() {
return Boot2JavaProblemType.JAVA_REPOSITORY;
}
}

View File

@@ -0,0 +1,116 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.reconcilers;
import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual;
import java.net.URI;
import java.util.List;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.openrewrite.java.spring.NoRequestMappingAnnotation;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeCodeActionDescriptor;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
public class NoRequestMappingAnnotationReconciler implements JdtAstReconciler {
private static final String LABEL = "Replace @RequestMapping with specific @GetMapping, @PostMapping etc.";
private static final String ID = NoRequestMappingAnnotation.class.getName();
private QuickfixRegistry registry;
public NoRequestMappingAnnotationReconciler(QuickfixRegistry registry) {
this.registry = registry;
}
@Override
public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector,
boolean isCompleteAst) throws RequiredCompleteAstException {
cu.accept(new ASTVisitor() {
@Override
public boolean visit(MarkerAnnotation node) {
processAnnotation(node);
return false;
}
@Override
public boolean visit(NormalAnnotation node) {
processAnnotation(node);
return false;
}
@Override
public boolean visit(SingleMemberAnnotation node) {
processAnnotation(node);
return false;
}
private void processAnnotation(Annotation a) {
if (a.getParent() instanceof MethodDeclaration && isRequestMappingAnnotation(cu, a)) {
String uri = docUri.toASCIIString();
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL, a.getStartPosition(), a.getLength());
RewriteQuickFixUtils.setRewriteFixes(registry, problem, List.of(
// new FixDescriptor(ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.NODE))
// .withRangeScope(RewriteQuickFixUtils.createOpenRewriteRange(cu, a))
// .withRecipeScope(RecipeScope.NODE),
new FixDescriptor(ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.FILE))
.withRecipeScope(RecipeScope.FILE),
new FixDescriptor(ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.PROJECT))
.withRecipeScope(RecipeScope.PROJECT)
));
problemCollector.accept(problem);
}
}
});
}
private static boolean isRequestMappingAnnotation(CompilationUnit cu, Annotation a) {
String typeName = a.getTypeName().getFullyQualifiedName();
if (Annotations.SPRING_REQUEST_MAPPING.equals(typeName)) {
return true;
} else if (typeName.endsWith("RequestMapping")) {
ITypeBinding type = a.resolveTypeBinding();
if (type != null && Annotations.SPRING_REQUEST_MAPPING.equals(type.getQualifiedName())) {
return true;
}
}
return false;
}
@Override
public boolean isApplicable(IJavaProject project) {
return springBootVersionGreaterOrEqual(2, 0, 0).test(project);
}
@Override
public ProblemType getProblemType() {
return Boot2JavaProblemType.JAVA_PRECISE_REQUEST_MAPPING;
}
}

View File

@@ -0,0 +1,146 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.reconcilers;
import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.openrewrite.java.spring.boot3.PreciseBeanType;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeCodeActionDescriptor;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
public class PreciseBeanTypeReconciler implements JdtAstReconciler {
private static final String RECIPE_ID = PreciseBeanType.class.getName();
private static final String LABEL = "Ensure concrete bean type";
private QuickfixRegistry registry;
public PreciseBeanTypeReconciler(QuickfixRegistry registry) {
this.registry = registry;
}
@Override
public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector,
boolean isCompleteAst) throws RequiredCompleteAstException {
AtomicBoolean requiresCompleteAst = new AtomicBoolean(false);
cu.accept(new ASTVisitor() {
private MethodDeclaration currentMethod;
private List<ITypeBinding> currentReturnTypes = new ArrayList<>();
@Override
public boolean visit(MethodDeclaration method) {
IMethodBinding methodBinding = method.resolveBinding();
if (methodBinding != null) {
boolean isBeanMethod = Arrays.stream(methodBinding.getAnnotations())
.anyMatch(a -> AnnotationHierarchies.findTransitiveSuperAnnotationBindings(a).anyMatch(an -> Annotations.BEAN.equals(an.getAnnotationType().getQualifiedName())));
if (isBeanMethod) {
if (isCompleteAst) {
if (currentMethod == null) {// Do not jump into anonymous class methods
currentMethod = method;
currentReturnTypes = new ArrayList<>();
return true;
}
} else {
requiresCompleteAst.set(true);
}
}
}
return false;
}
@Override
public void endVisit(MethodDeclaration method) {
if (currentMethod == method) {
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL, method.getReturnType2().getStartPosition(), method.getReturnType2().getLength());
if (currentReturnTypes.size() > 1) {
problemCollector.accept(problem);
} else if (currentReturnTypes.size() == 1 && !method.resolveBinding().getReturnType().isAssignmentCompatible(currentReturnTypes.get(0))) {
String uri = docUri.toASCIIString();
String replacementType = currentReturnTypes.get(0).getName();
RewriteQuickFixUtils.setRewriteFixes(registry, problem, List.of(
new FixDescriptor(RECIPE_ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel("Replace return type with '" + replacementType + "'", RecipeScope.NODE))
.withRecipeScope(RecipeScope.NODE)
.withRangeScope(RewriteQuickFixUtils.createOpenRewriteRange(cu, method)),
new FixDescriptor(RECIPE_ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.FILE))
.withRecipeScope(RecipeScope.FILE),
new FixDescriptor(RECIPE_ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.PROJECT))
.withRecipeScope(RecipeScope.PROJECT)
));
problemCollector.accept(problem);
}
currentMethod = null;
currentReturnTypes = new ArrayList<>();
}
super.endVisit(method);
}
@Override
public boolean visit(ReturnStatement node) {
ITypeBinding type = node.getExpression().resolveTypeBinding();
if (currentReturnTypes.isEmpty()) {
currentReturnTypes.add(type);
} else {
for (ListIterator<ITypeBinding> itr = currentReturnTypes.listIterator(); itr.hasNext();) {
ITypeBinding t = itr.next();
if (t.isAssignmentCompatible(type)) {
itr.remove();
}
}
currentReturnTypes.add(type);
}
return super.visit(node);
}
});
if (requiresCompleteAst.get()) {
throw new RequiredCompleteAstException();
}
}
@Override
public boolean isApplicable(IJavaProject project) {
return springBootVersionGreaterOrEqual(3, 0, 0).test(project);
}
@Override
public ProblemType getProblemType() {
return SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE;
}
}

View File

@@ -89,5 +89,22 @@ public class RewriteQuickFixUtils {
for (; type != type.getErasure(); type = type.getErasure()) {}
return type;
}
public static String getSimpleName(String fqName) {
int idx = fqName.lastIndexOf('.');
if (idx >= 0 && idx < fqName.length() - 1) {
return fqName.substring(idx + 1);
}
return fqName;
}
public static String getFirstTokenBeforeDot(String fqName) {
int idx = fqName.indexOf('.');
if (idx > 0) {
return fqName.substring(0, idx);
}
return fqName;
}
}

View File

@@ -0,0 +1,144 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.reconcilers;
import static org.springframework.ide.vscode.commons.java.SpringProjectUtil.springBootVersionGreaterOrEqual;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.openrewrite.java.spring.boot2.UnnecessarySpringExtension;
import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
public class UnnecessarySpringExtensionReconciler implements JdtAstReconciler {
private static final String LABEL = "Remove unnecessary @SpringExtension";
private static final String ID = UnnecessarySpringExtension.class.getName();
private static final List<String> SPRING_BOOT_TEST_ANNOTATIONS = Arrays.asList(
"org.springframework.boot.test.context.SpringBootTest",
"org.springframework.boot.test.autoconfigure.jdbc.JdbcTest",
"org.springframework.boot.test.autoconfigure.web.client.RestClientTest",
"org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest",
"org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest",
"org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest",
"org.springframework.boot.test.autoconfigure.webservices.client.WebServiceClientTest",
"org.springframework.boot.test.autoconfigure.jooq.JooqTest",
"org.springframework.boot.test.autoconfigure.json.JsonTest",
"org.springframework.boot.test.autoconfigure.data.cassandra.DataCassandraTest",
"org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest",
"org.springframework.boot.test.autoconfigure.data.ldap.DataLdapTest",
"org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest",
"org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest",
"org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest",
"org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest"
);
private static final String FQN_EXTEND_WITH = "org.junit.jupiter.api.extension.ExtendWith";
private static final String FQN_SPRING_EXT = "org.springframework.test.context.junit.jupiter.SpringExtension";
private QuickfixRegistry registry;
public UnnecessarySpringExtensionReconciler(QuickfixRegistry registry) {
this.registry = registry;
}
@Override
public void reconcile(IJavaProject project, URI docUri, CompilationUnit cu, IProblemCollector problemCollector,
boolean isCompleteAst) throws RequiredCompleteAstException {
cu.accept(new ASTVisitor() {
@Override
public boolean visit(TypeDeclaration typeDecl) {
Annotation testAnnotation = null;
Annotation extendWithAnnotation = null;
for (Object o : typeDecl.modifiers()) {
if (o instanceof Annotation) {
Annotation a = (Annotation) o;
if (testAnnotation == null && isApplicableTestAnnotation(a)) {
testAnnotation = a;
}
if (extendWithAnnotation == null && isApplicableExtendsWith(a)) {
extendWithAnnotation = a;
}
if (testAnnotation != null && extendWithAnnotation != null) {
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL, extendWithAnnotation.getStartPosition(), extendWithAnnotation.getLength());
RewriteQuickFixUtils.setRewriteFixes(registry, problem, List.of(
new FixDescriptor(ID, List.of(docUri.toASCIIString()), RewriteQuickFixUtils.buildLabel(LABEL, RecipeScope.PROJECT))
));
problemCollector.accept(problem);
break;
}
}
}
return super.visit(typeDecl);
}
private boolean isApplicableExtendsWith(Annotation a) {
if (FQN_EXTEND_WITH.endsWith(a.getTypeName().getFullyQualifiedName())) {
IAnnotationBinding annotationBinding = a.resolveAnnotationBinding();
if (annotationBinding != null && FQN_EXTEND_WITH.equals(annotationBinding.getAnnotationType().getQualifiedName()) && annotationBinding.getDeclaredMemberValuePairs().length == 1) {
IMemberValuePairBinding pair = annotationBinding.getDeclaredMemberValuePairs()[0];
if ("value".equals(pair.getName())) {
ITypeBinding typeBinding = null;
if (pair.getValue() instanceof ITypeBinding) {
typeBinding = (ITypeBinding) pair.getValue();
} else if (pair.getValue() instanceof Object[]) {
Object[] arr = (Object[]) pair.getValue();
if (arr.length > 0 && arr[0] instanceof ITypeBinding) {
typeBinding = (ITypeBinding) arr[0];
}
}
return typeBinding != null && FQN_SPRING_EXT.equals(typeBinding.getQualifiedName());
}
}
}
return false;
}
private boolean isApplicableTestAnnotation(Annotation a) {
String annotationTypeFqn = a.getTypeName().getFullyQualifiedName();
if (SPRING_BOOT_TEST_ANNOTATIONS.stream().anyMatch(fqn -> fqn.endsWith(annotationTypeFqn))) {
IAnnotationBinding annotationBinding = a.resolveAnnotationBinding();
if (annotationBinding != null && SPRING_BOOT_TEST_ANNOTATIONS.contains(annotationBinding.getAnnotationType().getQualifiedName())) {
return true;
}
}
return false;
}
});
}
@Override
public boolean isApplicable(IJavaProject project) {
return springBootVersionGreaterOrEqual(2, 1, 0).test(project);
}
@Override
public Boot2JavaProblemType getProblemType() {
return Boot2JavaProblemType.JAVA_TEST_SPRING_EXTENSION;
}
}

View File

@@ -643,10 +643,40 @@ public class SpringIndexerJava implements SpringIndexer {
});
// reconciling
IProblemCollector problemCollector = new IProblemCollector() {
List<ReconcileProblem> problems = new ArrayList<>();
@Override
public void endCollecting() {
for (ReconcileProblem p : problems) {
context.getProblemCollector().accept(p);
}
}
@Override
public void beginCollecting() {
problems.clear();
}
@Override
public void accept(ReconcileProblem problem) {
problems.add(problem);
}
};
try {
reconciler.reconcile(context.getProject(), URI.create(context.getDocURI()), context.getCu(), context.getProblemCollector(), context.getPass() == SCAN_PASS.TWO);
problemCollector.beginCollecting();
reconciler.reconcile(context.getProject(), URI.create(context.getDocURI()), context.getCu(), problemCollector, context.getPass() == SCAN_PASS.TWO);
problemCollector.endCollecting();
} catch (RequiredCompleteAstException e) {
context.getNextPassFiles().add(context.getFile());
if (context.getPass() == SCAN_PASS.TWO) {
problemCollector.endCollecting();
log.error("Complete AST required but it is complete already. Parsing ", context.getDocURI());
} else {
// Let problems be found in the next pass, don't add the problems to the aggregate problems collector to not duplicate them with the next pass
context.getNextPassFiles().add(context.getFile());
}
}
dependencyTracker.update(context.getFile(), context.getDependencies());;

View File

@@ -23,7 +23,6 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
@@ -40,8 +39,8 @@ import org.springframework.ide.vscode.boot.app.BootJavaConfig;
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.Boot3JavaProblemType;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaProjectReconcilerScheduler;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine;
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
@@ -66,10 +65,8 @@ public class ModulithService {
private static final String CMD_LIST_MODULITH_PROJECTS = "sts/modulith/projects";
private SimpleLanguageServer server;
private Optional<BootJavaProjectReconcilerScheduler> projectReconcileScheduler;
private SpringSymbolIndex springIndex;
private BootJavaReconcileEngine reconciler;
private JavaProjectFinder projectFinder;
private BootJavaConfig config;
private Map<URI, AppModules> cache;
@@ -81,15 +78,12 @@ public class ModulithService {
ProjectObserver projectObserver,
SpringSymbolIndex springIndex,
BootJavaReconcileEngine reconciler,
Optional<BootJavaProjectReconcilerScheduler> projectReconcileScheduler,
BootJavaConfig config
) {
this.projectFinder = projectFinder;
this.config = config;
this.cache = new ConcurrentHashMap<>();
this.metadataRequested = new ConcurrentHashMap<>();
this.server = server;
this.projectReconcileScheduler = projectReconcileScheduler;
this.springIndex = springIndex;
this.reconciler = reconciler;
@@ -209,12 +203,28 @@ public class ModulithService {
private void validate(IJavaProject project) {
if (server.getDiagnosticSeverityProvider().getDiagnosticSeverity(Boot3JavaProblemType.MODULITH_TYPE_REF_VIOLATION) != null
&& config.getProblemApplicability(Boot3JavaProblemType.MODULITH_TYPE_REF_VIOLATION) != Option.OFF) {
for (TextDocument doc : server.getTextDocumentService().getAll()) {
if (projectFinder.find(doc.getId()).orElse(null) == project) {
List<Path> javaSources = IClasspathUtil.getProjectJavaSourceFoldersWithoutTests(project.getClasspath())
.flatMap(sourceFolder -> {
try {
return Files.walk(sourceFolder.toPath()).filter(p -> Files.isRegularFile(p) && p.toString().endsWith(".java"));
} catch (IOException e) {
log.error("", e);
return Stream.empty();
}
}).collect(Collectors.toList());
List<String> fileUriToUpdate = new ArrayList<>(javaSources.size());
for (Path javaSource : javaSources) {
String docUri = javaSource.toUri().toASCIIString();
TextDocument doc = server.getTextDocumentService().getLatestSnapshot(docUri);
if (doc == null) {
fileUriToUpdate.add(docUri);
} else {
server.validateWith(doc.getId(), reconciler);
}
}
projectReconcileScheduler.ifPresent(r -> r.scheduleValidation(project));
String[] uris = fileUriToUpdate.toArray(new String[fileUriToUpdate.size()]);
springIndex.deleteDocuments(uris).thenAccept(v -> springIndex.updateDocuments(uris, "Modulith Metadata Changed"));
}
}
@@ -304,7 +314,7 @@ public class ModulithService {
});
}
static String getPackageNameFromTypeFQName(String fqn) {
public static String getPackageNameFromTypeFQName(String fqn) {
int idx = 0;
for (; idx < fqn.length() - 1; idx++) {
char c = fqn.charAt(idx);

View File

@@ -0,0 +1,134 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.reconcilers.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.NoRepoAnnotationReconciler;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
public class NoRepoAnnotationReconcilerTest extends BaseReconcilerTest {
@Override
protected String getFolder() {
return "norepoannotation";
}
@Override
protected String getProjectName() {
return "test-spring-validations";
}
@Override
protected JdtAstReconciler getReconciler() {
return new NoRepoAnnotationReconciler(new QuickfixRegistry());
}
@BeforeEach
void setup() throws Exception {
super.setup();
}
@AfterEach
void tearDown() throws Exception {
super.tearDown();
}
@Test
void sanityTest() throws Exception {
String source = """
package example.demo;
import org.springframework.data.repository.Repository;
@org.springframework.stereotype.Repository
interface A extends Repository {
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, false);
assertEquals(1, problems.size());
ReconcileProblem problem = problems.get(0);
assertEquals(Boot2JavaProblemType.JAVA_REPOSITORY, problem.getType());
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("@org.springframework.stereotype.Repository", markedStr);
assertEquals(2, problem.getQuickfixes().size());
}
@Test
void inverseSanityTest() throws Exception {
String source = """
package example.demo;
import org.springframework.stereotype.Repository;
@Repository
interface A extends org.springframework.data.repository.Repository {
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, false);
assertEquals(1, problems.size());
ReconcileProblem problem = problems.get(0);
assertEquals(Boot2JavaProblemType.JAVA_REPOSITORY, problem.getType());
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("@Repository", markedStr);
assertEquals(2, problem.getQuickfixes().size());
}
@Test
void emptyRepoAnnotation() throws Exception {
String source = """
package example.demo;
import org.springframework.data.repository.Repository;
@org.springframework.stereotype.Repository()
interface A extends Repository {
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, false);
assertEquals(1, problems.size());
ReconcileProblem problem = problems.get(0);
assertEquals(Boot2JavaProblemType.JAVA_REPOSITORY, problem.getType());
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("@org.springframework.stereotype.Repository()", markedStr);
assertEquals(2, problem.getQuickfixes().size());
}
}

View File

@@ -0,0 +1,116 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.reconcilers.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.NoRequestMappingAnnotationReconciler;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
public class NoRequestMappingAnnotationReconcilerTest extends BaseReconcilerTest {
@Override
protected String getFolder() {
return "norequestmapping";
}
@Override
protected String getProjectName() {
return "test-spring-validations";
}
@Override
protected JdtAstReconciler getReconciler() {
return new NoRequestMappingAnnotationReconciler(new QuickfixRegistry());
}
@BeforeEach
void setup() throws Exception {
super.setup();
}
@AfterEach
void tearDown() throws Exception {
super.tearDown();
}
@Test
void sanityTest() throws Exception {
String source = """
package example.demo;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/hello")
class A {
@RequestMapping("/1")
String hello1() {
return "1";
};
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, false);
assertEquals(1, problems.size());
ReconcileProblem problem = problems.get(0);
assertEquals(Boot2JavaProblemType.JAVA_PRECISE_REQUEST_MAPPING, problem.getType());
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("@RequestMapping(\"/1\")", markedStr);
assertEquals(2, problem.getQuickfixes().size());
}
@Test
void noProblems() throws Exception {
String source = """
package example.demo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
@RequestMapping("/hello")
class A {
@Bean
Integer someBean() {
return 0;
}
@GetMapping("/1")
String hello1() {
return "1";
};
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, false);
assertEquals(0, problems.size());
}
}

View File

@@ -0,0 +1,347 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.reconcilers.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.ide.vscode.boot.java.SpringAotJavaProblemType;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.PreciseBeanTypeReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
public class PreciseBeanTypeReconcilerTest extends BaseReconcilerTest {
@Override
protected String getFolder() {
return "precisebeantype";
}
@Override
protected String getProjectName() {
return "test-spring-validations";
}
@Override
protected JdtAstReconciler getReconciler() {
return new PreciseBeanTypeReconciler(new QuickfixRegistry());
}
@BeforeEach
void setup() throws Exception {
super.setup();
}
@AfterEach
void tearDown() throws Exception {
super.tearDown();
}
@Test
void requireFullAst() throws Exception {
String source = """
package example.demo;
import org.springframework.context.annotation.Bean;
class A {
@Bean
Number bean() {
return Integer.valueOf(5);
};
}
""";
try {
reconcile("A.java", source, false);
fail("Should require full AST with method bodies");
} catch (RequiredCompleteAstException e) {
// pass
}
}
@Test
void singleReturnStatement() throws Exception {
String source = """
package example.demo;
import org.springframework.context.annotation.Bean;
class A {
@Bean
Number bean() {
return Integer.valueOf(5);
};
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, true);
assertEquals(1, problems.size());
ReconcileProblem problem = problems.get(0);
assertEquals(SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE, problem.getType());
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("Number", markedStr);
assertEquals(3, problem.getQuickfixes().size());
}
@Test
void singleReturnStatementWithPrimitiveType() throws Exception {
String source = """
package example.demo;
import org.springframework.context.annotation.Bean;
class A {
@Bean
Number bean() {
return 5;
};
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, true);
assertEquals(1, problems.size());
ReconcileProblem problem = problems.get(0);
assertEquals(SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE, problem.getType());
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("Number", markedStr);
assertEquals(3, problem.getQuickfixes().size());
}
@Test
void multipleReturnStatementSameType() throws Exception {
String source = """
package example.demo;
import org.springframework.context.annotation.Bean;
class A {
boolean b
@Bean
Number bean() {
if (b) {
return 3;
} else {
return 5;
}
};
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, true);
assertEquals(1, problems.size());
ReconcileProblem problem = problems.get(0);
assertEquals(SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE, problem.getType());
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("Number", markedStr);
assertEquals(3, problem.getQuickfixes().size());
}
@Test
void multipleReturnStatementDifferentType() throws Exception {
String source = """
package example.demo;
import org.springframework.context.annotation.Bean;
class A {
boolean b
@Bean
Number bean() {
if (b) {
return 3.45;
} else {
return 5;
}
};
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, true);
assertEquals(1, problems.size());
ReconcileProblem problem = problems.get(0);
assertEquals(SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE, problem.getType());
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("Number", markedStr);
assertEquals(0, problem.getQuickfixes().size());
}
@Test
void multipleReturnStatementDifferentType2() throws Exception {
String source = """
package example.demo;
import java.util.*;
import org.springframework.context.annotation.Bean;
class A {
boolean b
@Bean
Collection<Integer> bean() {
if (b) {
LinkedList<Integer> l = new LinkedList<>();
return l;
} else {
return List.of(5);
}
};
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, true);
assertEquals(1, problems.size());
ReconcileProblem problem = problems.get(0);
assertEquals(SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE, problem.getType());
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("Collection<Integer>", markedStr);
assertEquals(3, problem.getQuickfixes().size());
FixDescriptor d = (FixDescriptor) problem.getQuickfixes().get(0).params;
assertEquals("Replace return type with 'List<Integer>'", d.getLabel());
}
@Test
void noProblemSingleReturnStatement() throws Exception {
String source = """
package example.demo;
import org.springframework.context.annotation.Bean;
class A {
@Bean
Integer bean() {
return 5;
};
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, true);
assertEquals(0, problems.size());
}
@Test
void multipleReturnStatement() throws Exception {
String source = """
package example.demo;
import org.springframework.context.annotation.Bean;
class A {
boolean b
@Bean
Double bean() {
if (b) {
return 3.45;
} else {
return 5;
}
};
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, true);
assertEquals(1, problems.size());
ReconcileProblem problem = problems.get(0);
assertEquals(SpringAotJavaProblemType.JAVA_CONCRETE_BEAN_TYPE, problem.getType());
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("Double", markedStr);
assertEquals(0, problem.getQuickfixes().size());
}
@Test
void noProblemMultipleReturnStatement() throws Exception {
String source = """
package example.demo;
import org.springframework.context.annotation.Bean;
class A {
boolean b
@Bean
Double bean() {
if (b) {
return 3.45;
} else {
return 5.4;
}
};
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, true);
assertEquals(0, problems.size());
}
}

View File

@@ -0,0 +1,120 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.reconcilers.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.UnnecessarySpringExtensionReconciler;
import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry;
import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem;
public class UnnecessarySpringExtensionReconcilerTest extends BaseReconcilerTest {
@Override
protected String getFolder() {
// TODO Auto-generated method stub
return "unnecassaryextendwith";
}
@Override
protected String getProjectName() {
return "test-spring-validations";
}
@Override
protected JdtAstReconciler getReconciler() {
return new UnnecessarySpringExtensionReconciler(new QuickfixRegistry());
}
@BeforeEach
void setup() throws Exception {
super.setup();
}
@AfterEach
void tearDown() throws Exception {
super.tearDown();
}
@Test
void sanity() throws Exception {
String source = """
package example.demo;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest
class A {
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, true);
assertEquals(1, problems.size());
ReconcileProblem problem = problems.get(0);
assertEquals(Boot2JavaProblemType.JAVA_TEST_SPRING_EXTENSION, problem.getType());
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("@ExtendWith(SpringExtension.class)", markedStr);
assertEquals(1, problem.getQuickfixes().size());
}
@Test
void noTestAnnotation() throws Exception {
String source = """
package example.demo;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
class A {
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, true);
assertEquals(0, problems.size());
}
@Test
void noExtendWithAnnotation() throws Exception {
String source = """
package example.demo;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class A {
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, true);
assertEquals(0, problems.size());
}
}

View File

@@ -45,7 +45,9 @@ import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine;
import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory;
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
import org.springframework.ide.vscode.boot.java.reconcilers.AnnotationNodeReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.JavaReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.boot.metadata.ValueProviderRegistry;
@@ -148,7 +150,9 @@ public class ValueSpelExpressionValidationTest {
problemCollector = new TestProblemCollector();
reconcileEngine = new BootJavaReconcileEngine(projectFinder, new JavaReconciler[] {
new JdtReconciler(compilationUnitCache, null, config)
new JdtReconciler(compilationUnitCache, config, new JdtAstReconciler[] {
new AnnotationNodeReconciler(config)
})
}, server);
}

View File

@@ -31,6 +31,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>