Multiple quick fixes for @RequestMapping

This commit is contained in:
aboyko
2023-09-15 15:02:58 -04:00
parent 6de7307c84
commit 78cd6ace6d
6 changed files with 370 additions and 48 deletions

View File

@@ -13,19 +13,25 @@ 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.Collections;
import java.util.List;
import java.util.Map;
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.ArrayInitializer;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.openrewrite.java.spring.NoRequestMappingAnnotation;
import org.openrewrite.marker.Range;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.Boot2JavaProblemType;
import org.springframework.ide.vscode.commons.java.IJavaProject;
@@ -35,11 +41,15 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemTy
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;
import org.springframework.ide.vscode.commons.rewrite.java.NoRequestMappingAnnotation;
public class NoRequestMappingAnnotationReconciler implements JdtAstReconciler {
private static final String UNSUPPORTED_REQUEST_METHOD = "UNSUPPORTED";
private static final List<String> SUPPORTED_REQUEST_METHODS = List.of("GET", "POST", "PUT", "DELETE", "PATCH");
private static final String LABEL = "Replace @RequestMapping with specific @GetMapping, @PostMapping etc.";
private static final String ID = NoRequestMappingAnnotation.class.getName();
private QuickfixRegistry registry;
@@ -71,58 +81,83 @@ public class NoRequestMappingAnnotationReconciler implements JdtAstReconciler {
}
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());
ReconcileUtils.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), ReconcileUtils.buildLabel(LABEL, RecipeScope.FILE))
.withRecipeScope(RecipeScope.FILE),
new FixDescriptor(ID, List.of(uri), ReconcileUtils.buildLabel(LABEL, RecipeScope.PROJECT))
.withRecipeScope(RecipeScope.PROJECT)
));
problemCollector.accept(problem);
if (a.getParent() instanceof MethodDeclaration) {
List<String> requestMethods = getRequestMethods(cu, a);
if (!requestMethods.isEmpty() && !requestMethods.contains(UNSUPPORTED_REQUEST_METHOD)) {
String uri = docUri.toASCIIString();
Range range = ReconcileUtils.createOpenRewriteRange(cu, a);
if (requestMethods.size() == 1 && SUPPORTED_REQUEST_METHODS.contains(requestMethods.get(0))) {
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), LABEL, a.getStartPosition(), a.getLength());
ReconcileUtils.setRewriteFixes(registry, problem, List.of(
createFixDescriptor(uri, range, requestMethods.get(0)),
new FixDescriptor(org.openrewrite.java.spring.NoRequestMappingAnnotation.class.getName(), List.of(uri), ReconcileUtils.buildLabel(LABEL, RecipeScope.FILE))
.withRecipeScope(RecipeScope.FILE),
new FixDescriptor(org.openrewrite.java.spring.NoRequestMappingAnnotation.class.getName(), List.of(uri), ReconcileUtils.buildLabel(LABEL, RecipeScope.PROJECT))
.withRecipeScope(RecipeScope.PROJECT)
));
problemCollector.accept(problem);
} else if (SUPPORTED_REQUEST_METHODS == requestMethods) { // the case of no request methods specified
ReconcileProblemImpl problem = new ReconcileProblemImpl(getProblemType(), "Consider replacing with precise mapping annotation", a.getStartPosition(), a.getLength());
ReconcileUtils.setRewriteFixes(registry, problem,
requestMethods.stream().map(m -> createFixDescriptor(uri, range, m)).collect(Collectors.toList()));
problemCollector.accept(problem);
}
}
}
}
});
}
private static boolean isRequestMappingAnnotation(CompilationUnit cu, Annotation a) {
// Consider only NormalAnnotation as we need to flag @RequestMapping with single method parameter value
if (a.isNormalAnnotation()) {
String typeName = a.getTypeName().getFullyQualifiedName();
if (Annotations.SPRING_REQUEST_MAPPING.equals(typeName)) {
return hasApplicableMethodParameter(a);
} else if (typeName.endsWith("RequestMapping")) {
ITypeBinding type = a.resolveTypeBinding();
if (type != null && Annotations.SPRING_REQUEST_MAPPING.equals(type.getQualifiedName())) {
return hasApplicableMethodParameter(a);
}
}
}
return false;
private static FixDescriptor createFixDescriptor(String uri, Range range, String requestMethod) {
return new FixDescriptor(NoRequestMappingAnnotation.class.getName(), List.of(uri), "Replace with '@%s'".formatted(NoRequestMappingAnnotation.associatedRequestMapping(requestMethod)))
.withRangeScope(range)
.withParameters(Map.of("preferredMapping", requestMethod))
.withRecipeScope(RecipeScope.NODE);
}
private static boolean hasApplicableMethodParameter(Annotation a) {
private static List<String> getRequestMethods(CompilationUnit cu, Annotation a) {
String typeName = a.getTypeName().getFullyQualifiedName();
if (Annotations.SPRING_REQUEST_MAPPING.equals(typeName)) {
return getRequestMethods(a);
} else if (typeName.endsWith("RequestMapping")) {
ITypeBinding type = a.resolveTypeBinding();
if (type != null && Annotations.SPRING_REQUEST_MAPPING.equals(type.getQualifiedName())) {
return getRequestMethods(a);
}
}
return Collections.emptyList();
}
@SuppressWarnings("unchecked")
private static List<String> getRequestMethods(Annotation a) {
if (a.isNormalAnnotation()) {
for (Object o : ((NormalAnnotation) a).values()) {
if (o instanceof MemberValuePair) {
MemberValuePair pair = (MemberValuePair) o;
if ("method".equals(pair.getName().getIdentifier())) {
if (pair.getValue() instanceof ArrayInitializer) {
return ((ArrayInitializer) pair.getValue()).expressions().size() == 1;
List<Expression> expressions = ((ArrayInitializer) pair.getValue()).expressions();
return expressions.stream().map(NoRequestMappingAnnotationReconciler::convertRequestMethodToString).collect(Collectors.toList());
} else {
return List.of(convertRequestMethodToString(pair.getValue()));
}
return true;
}
}
}
}
return false;
return SUPPORTED_REQUEST_METHODS;
}
private static String convertRequestMethodToString(Expression e) {
String requestMethod = UNSUPPORTED_REQUEST_METHOD;
if (e instanceof QualifiedName) {
requestMethod = ((QualifiedName) e).getName().getIdentifier();
} else if (e instanceof SimpleName) {
requestMethod = ((SimpleName) e).getIdentifier();
}
return SUPPORTED_REQUEST_METHODS.contains(requestMethod) ? requestMethod : UNSUPPORTED_REQUEST_METHOD;
}
@Override
public boolean isApplicable(IJavaProject project) {

View File

@@ -51,6 +51,7 @@ import org.springframework.ide.vscode.commons.rewrite.ORDocUtils;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
import org.springframework.ide.vscode.commons.rewrite.java.ORAstUtils;
import org.springframework.ide.vscode.commons.rewrite.java.RangeScopedRecipe;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
@@ -204,16 +205,20 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler
if (d.getRangeScope() == null) {
throw new IllegalArgumentException("Missing scope AST node!");
} else {
r = ORAstUtils.nodeRecipe(r, j -> {
if (j != null) {
Range range = j.getMarkers().findFirst(Range.class).orElse(null);
if (range != null) {
// 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;
});
if (r instanceof RangeScopedRecipe) {
((RangeScopedRecipe) r).setRange(d.getRangeScope());
} else {
r = ORAstUtils.nodeRecipe(r, j -> {
if (j != null) {
Range range = j.getMarkers().findFirst(Range.class).orElse(null);
if (range != null) {
// 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;
});
}
}
}
return r;

View File

@@ -80,10 +80,46 @@ public class NoRequestMappingAnnotationReconcilerTest extends BaseReconcilerTest
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("@RequestMapping(value = \"/1\", method = RequestMethod.GET)", markedStr);
assertEquals(2, problem.getQuickfixes().size());
assertEquals(3, problem.getQuickfixes().size());
assertEquals("Replace with '@GetMapping'", problems.get(0).getQuickfixes().get(0).title);
}
@Test
void staticImport() throws Exception {
String source = """
package example.demo;
import org.springframework.web.bind.annotation.RequestMapping;
import static org.springframework.web.bind.annotation.RequestMethod.*;
@RequestMapping("/hello")
class A {
@RequestMapping(value = "/1", method = GET)
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(value = \"/1\", method = GET)", markedStr);
assertEquals(3, problem.getQuickfixes().size());
assertEquals("Replace with '@GetMapping'", problems.get(0).getQuickfixes().get(0).title);
}
@Test
void arrayMethodTest() throws Exception {
String source = """
@@ -114,7 +150,8 @@ public class NoRequestMappingAnnotationReconcilerTest extends BaseReconcilerTest
String markedStr = source.substring(problem.getOffset(), problem.getOffset() + problem.getLength());
assertEquals("@RequestMapping(value = \"/1\", method = { RequestMethod.GET })", markedStr);
assertEquals(2, problem.getQuickfixes().size());
assertEquals(3, problem.getQuickfixes().size());
assertEquals("Replace with '@GetMapping'", problems.get(0).getQuickfixes().get(0).title);
}
@@ -143,7 +180,36 @@ public class NoRequestMappingAnnotationReconcilerTest extends BaseReconcilerTest
}
@Test
void noMethodsNoProblems() throws Exception {
void multiMethodTest_2() throws Exception {
String source = """
package example.demo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@RequestMapping("/hello")
class A {
@RequestMapping(value = "/1", method = { RequestMethod.GET, RequestMethod.PUT })
String hello1() {
return "1";
};
}
""";
List<ReconcileProblem> problems = reconcile("A.java", source, false);
assertEquals(0, problems.size());
// assertEquals(1, problems.size());
//
// assertEquals(2, problems.get(0).getQuickfixes().size());
// assertEquals("Replace with '@GetMapping'", problems.get(0).getQuickfixes().get(0).title);
// assertEquals("Replace with '@PutMapping'", problems.get(0).getQuickfixes().get(1).title);
}
@Test
void noMethods() throws Exception {
String source = """
package example.demo;
@@ -162,7 +228,14 @@ public class NoRequestMappingAnnotationReconcilerTest extends BaseReconcilerTest
""";
List<ReconcileProblem> problems = reconcile("A.java", source, false);
assertEquals(0, problems.size());
assertEquals(1, problems.size());
assertEquals(5, problems.get(0).getQuickfixes().size());
assertEquals("Replace with '@GetMapping'", problems.get(0).getQuickfixes().get(0).title);
assertEquals("Replace with '@PostMapping'", problems.get(0).getQuickfixes().get(1).title);
assertEquals("Replace with '@PutMapping'", problems.get(0).getQuickfixes().get(2).title);
assertEquals("Replace with '@DeleteMapping'", problems.get(0).getQuickfixes().get(3).title);
assertEquals("Replace with '@PatchMapping'", problems.get(0).getQuickfixes().get(4).title);
}
@Test