Skip to content

Commit

Permalink
RequestMapping codemod (#142)
Browse files Browse the repository at this point in the history
Codemod created for migrating RequestMapping to shortcut annotations on verbose
cases.

---------

Co-authored-by: Arshan Dabirsiaghi <[email protected]>
  • Loading branch information
danielostrow and nahsra committed Aug 25, 2023
1 parent 4eee3b6 commit f4db05e
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.codemodder.codemods;

import static io.codemodder.ast.ASTTransforms.addImportIfMissing;

import com.contrastsecurity.sarif.Result;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.expr.MemberValuePair;
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
import io.codemodder.*;
import io.codemodder.providers.sarif.semgrep.SemgrepScan;
import java.util.Optional;
import javax.inject.Inject;

@Codemod(
id = "pixee:java/verbose-request-mapping",
reviewGuidance = ReviewGuidance.MERGE_WITHOUT_REVIEW)
public final class VerboseRequestMappingCodemod
extends SarifPluginJavaParserChanger<NormalAnnotationExpr> {

@Inject
public VerboseRequestMappingCodemod(
@SemgrepScan(ruleId = "verbose-request-mapping") final RuleSarif sarif) {
super(sarif, NormalAnnotationExpr.class);
}

@Override
public boolean onResultFound(
final CodemodInvocationContext context,
final CompilationUnit cu,
final NormalAnnotationExpr annotationExpr,
final Result result) {

// Find the http method if it exists
Optional<MemberValuePair> method =
annotationExpr.getPairs().stream()
.filter(pair -> pair.getNameAsString().equals("method"))
.findFirst();
if (method.isPresent()) {
MemberValuePair methodAttribute = method.get();
// Store method and remove the "method" attribute
String httpMethod = methodAttribute.getValue().toString();
Optional<String> newType = getType(httpMethod);
if (newType.isPresent()) {
annotationExpr.getPairs().remove(methodAttribute);
annotationExpr.setName(newType.get());
addImportIfMissing(cu, "org.springframework.web.bind.annotation." + newType.get());
return true;
}
}
return false;
}

public static Optional<String> getType(final String httpMethod) {
String newType =
switch (httpMethod) {
case "RequestMethod.GET", "GET" -> "GetMapping";
case "RequestMethod.PUT", "PUT" -> "PutMapping";
case "RequestMethod.DELETE", "DELETE" -> "DeleteMapping";
case "RequestMethod.POST", "POST" -> "PostMapping";
case "RequestMethod.PATCH", "PATCH" -> "PatchMapping";
default -> null;
};
return Optional.ofNullable(newType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
This change simplifies Spring Framework annotations by making use of shortened annotations when applicable.
Code that is easy to read is easy to review, reason about, and detect bugs in.

Making use of shortcut annotations accomplishes this by removing *wordy for no reason* elements.


Version 4.3 of Spring Framework introduced method-level variants for `@RequestMapping`.
- `@GetMapping`
- `@PutMapping`
- `@PostMapping`
- `@DeleteMapping`
- `@PatchMapping`

```diff
- @RequestMapping(value = "/example", method = RequestMethod.GET)
...
+ @GetMapping(value = "/example")
```

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"summary": "Replaced @RequestMapping annotation with shortcut annotation for requested HTTP Method",
"change": "Replaced @RequestMapping annotation with shortcut annotation for requested HTTP Method",
"references": [
"https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-requestmapping.html",
"https://dzone.com/articles/using-the-spring-requestmapping-annotation"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
rules:
- id: verbose-request-mapping
patterns:
- pattern-either:
- pattern: |
@RequestMapping(method = $REQUEST_METHOD, ...)
- pattern: |
@RequestMapping(method = $VALUE, ...)
message: Semgrep found a match.
languages: [java]
severity: WARNING
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.codemodder.codemods;

import io.codemodder.testutils.CodemodTestMixin;
import io.codemodder.testutils.Metadata;

@Metadata(
codemodType = VerboseRequestMappingCodemod.class,
testResourceDir = "verbose-request-mapping",
dependencies = {})
final class VerboseRequestMappingCodemodTest implements CodemodTestMixin {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

public class Test {

@RequestMapping("/example1")
public String case1() {
return "Case 1";
}

@GetMapping(value = "/example2")
public String case2() {
return "Case 2";
}

@GetMapping(value = "/example3")
public String case3() {
return "Case 3";
}

@RequestMapping(value = "/example4")
public String case4() {
return "Case 4";
}

@NonsenseMapping(value = "/example5")
public String case5() { return "Case 5"; }

@NonsenseMapping(value = "/example6", method = RequestMethod.NONSENSE)
public String case6() { return "Case 6"; }

}

@Controller
@RequestMapping("/boys")
public class DemoController {

@ResponseBody
@RequestMapping("/hello")
public String helloWorld() {
return "Hello World!";
}

@ResponseBody
@RequestMapping("/example")
public String welcomeGfgMessage() {
return "Welcome";
}

@ResponseBody
@GetMapping(value = "/greet")
public String greet() {
return "Greetings!";
}

@ResponseBody
@PutMapping(value = "/update")
public String updateData() {
return "Data Updated!";
}

@ResponseBody
@DeleteMapping(value = "/delete")
public String deleteData() {
return "Data Deleted!";
}

@ResponseBody
@PostMapping(value = "/create")
public String createData() {
return "Data Created!";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

public class Test {

@RequestMapping("/example1")
public String case1() {
return "Case 1";
}

@RequestMapping(method = RequestMethod.GET, value = "/example2")
public String case2() {
return "Case 2";
}

@RequestMapping(value = "/example3", method = GET)
public String case3() {
return "Case 3";
}

@RequestMapping(value = "/example4")
public String case4() {
return "Case 4";
}

@NonsenseMapping(value = "/example5")
public String case5() { return "Case 5"; }

@NonsenseMapping(value = "/example6", method = RequestMethod.NONSENSE)
public String case6() { return "Case 6"; }

}

@Controller
@RequestMapping("/boys")
public class DemoController {

@ResponseBody
@RequestMapping("/hello")
public String helloWorld() {
return "Hello World!";
}

@ResponseBody
@RequestMapping("/example")
public String welcomeGfgMessage() {
return "Welcome";
}

@ResponseBody
@RequestMapping(value = "/greet", method = RequestMethod.GET)
public String greet() {
return "Greetings!";
}

@ResponseBody
@RequestMapping(value = "/update", method = RequestMethod.PUT)
public String updateData() {
return "Data Updated!";
}

@ResponseBody
@RequestMapping(value = "/delete", method = RequestMethod.DELETE)
public String deleteData() {
return "Data Deleted!";
}

@ResponseBody
@RequestMapping(value = "/create", method = RequestMethod.POST)
public String createData() {
return "Data Created!";
}
}

0 comments on commit f4db05e

Please sign in to comment.