Skip to content

Commit 713ba46

Browse files
carlosu7nahsra
andauthored
Sonarcloud codemod - Harden String Parse to primitives (.valueOf cases) (#250)
Add new Sonarcloud codemod that enforces the appropriate parsing technique for converting Strings to primitive types in the codebase. This PR only handles the `.valueOf` cases (`MethodCallExpr` nodes), the constructor cases (`ObjectCreationExpr` nodes) will be handled in another PR . Sonar rule reference https://rules.sonarsource.com/java/RSPEC-2130/ sonar cloud issues related to string-to-primitive conversion: https://sonarcloud.io/project/issues?fileUuids=AYvtrjqILCzGLicz7AY_&resolved=false&id=nahsra_WebGoat_10_23 --------- Co-authored-by: Arshan Dabirsiaghi <[email protected]>
1 parent 62900b4 commit 713ba46

File tree

9 files changed

+506
-38
lines changed

9 files changed

+506
-38
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package io.codemodder.codemods;
2+
3+
import com.github.javaparser.ast.CompilationUnit;
4+
import com.github.javaparser.ast.Node;
5+
import com.github.javaparser.ast.NodeList;
6+
import com.github.javaparser.ast.expr.Expression;
7+
import com.github.javaparser.ast.expr.MethodCallExpr;
8+
import com.github.javaparser.ast.type.Type;
9+
import io.codemodder.*;
10+
import io.codemodder.providers.sonar.ProvidedSonarScan;
11+
import io.codemodder.providers.sonar.RuleIssues;
12+
import io.codemodder.providers.sonar.SonarPluginJavaParserChanger;
13+
import io.codemodder.providers.sonar.api.Issue;
14+
import java.util.Optional;
15+
import javax.inject.Inject;
16+
17+
/**
18+
* A codemod that enforces the appropriate parsing technique for converting Strings to primitive
19+
* types in the codebase.
20+
*/
21+
@Codemod(
22+
id = "sonar:java/harden-string-parse-to-primitives-s2130",
23+
reviewGuidance = ReviewGuidance.MERGE_WITHOUT_REVIEW,
24+
executionPriority = CodemodExecutionPriority.HIGH)
25+
public final class HardenStringParseToPrimitivesCodemod extends CompositeJavaParserChanger {
26+
27+
// TODO create another static sonar java parser changer to handle constructor cases
28+
// (ObjectCreationExpr)
29+
@Inject
30+
public HardenStringParseToPrimitivesCodemod(
31+
final HardenParseForValueOfChanger parseMethodCallExprChanger) {
32+
super(parseMethodCallExprChanger);
33+
}
34+
35+
private static final class HardenParseForValueOfChanger
36+
extends SonarPluginJavaParserChanger<MethodCallExpr> {
37+
38+
@Inject
39+
public HardenParseForValueOfChanger(
40+
@ProvidedSonarScan(ruleId = "java:S2130") final RuleIssues issues) {
41+
super(
42+
issues,
43+
MethodCallExpr.class,
44+
RegionNodeMatcher.MATCHES_START,
45+
CodemodReporterStrategy.empty());
46+
}
47+
48+
@Override
49+
public boolean onIssueFound(
50+
final CodemodInvocationContext context,
51+
final CompilationUnit cu,
52+
final MethodCallExpr methodCallExpr,
53+
final Issue issue) {
54+
55+
final String methodName = methodCallExpr.getNameAsString();
56+
57+
if ("valueOf".equals(methodName)) {
58+
final String targetType = retrieveTargetTypeFromMethodCallExpr(methodCallExpr);
59+
60+
final String replacementMethod = determineParsingMethodForType(targetType);
61+
62+
if (replacementMethod != null) {
63+
methodCallExpr.setName(replacementMethod);
64+
65+
return handleMethodCallChainsAfterValueOfIfNeeded(methodCallExpr);
66+
}
67+
}
68+
69+
return false;
70+
}
71+
72+
private String retrieveTargetTypeFromMethodCallExpr(final MethodCallExpr methodCallExpr) {
73+
final Optional<NodeList<Type>> optionalTypeArguments = methodCallExpr.getTypeArguments();
74+
return optionalTypeArguments.isEmpty()
75+
? methodCallExpr
76+
.getScope()
77+
.map(expr -> expr.calculateResolvedType().describe())
78+
.orElse("")
79+
: optionalTypeArguments.get().get(0).toString();
80+
}
81+
82+
private String determineParsingMethodForType(final String type) {
83+
return switch (type) {
84+
case "java.lang.Integer" -> "parseInt";
85+
case "java.lang.Float" -> "parseFloat";
86+
// Add more cases if needed
87+
default -> null;
88+
};
89+
}
90+
91+
private boolean handleMethodCallChainsAfterValueOfIfNeeded(
92+
final MethodCallExpr methodCallExpr) {
93+
94+
final Optional<Node> optionalParentNode = methodCallExpr.getParentNode();
95+
if (optionalParentNode.isPresent()
96+
&& optionalParentNode.get() instanceof MethodCallExpr parentMethodCall) {
97+
98+
final String parentMethodName = parentMethodCall.getNameAsString();
99+
100+
final Optional<Expression> optionalScope = parentMethodCall.getScope();
101+
if (optionalScope.isEmpty()) {
102+
return false;
103+
}
104+
105+
if ("intValue".equals(parentMethodName) || "floatValue".equals(parentMethodName)) {
106+
parentMethodCall.replace(optionalScope.get());
107+
}
108+
}
109+
110+
return true;
111+
}
112+
}
113+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
This change updates `String`-to-number conversions by leveraging the intended parse methods.
2+
3+
This change makes developer intent clearer, and sometimes with a more concise expression.
4+
5+
Our changes look like this:
6+
7+
```diff
8+
String number = "7.1";
9+
10+
- int integerNum = Integer.valueOf(number);
11+
+ int integerNum = Integer.parseInt(number);
12+
13+
- float floatNumVal = Float.valueOf(number).floatValue();
14+
+ float floatNumVal = Float.parseFloat(number);
15+
16+
- int integerNumber = new Integer(number);
17+
+ int integerNumber = Integer.parseInt(number);
18+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"summary" : "Implemented parsing usage when converting Strings to primitives (Sonar).",
3+
"change" : "Implemented parsing usage when converting Strings to primitives.",
4+
"references" : [
5+
"https://rules.sonarsource.com/java/RSPEC-2130/"
6+
]
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.codemodder.codemods;
2+
3+
import io.codemodder.testutils.CodemodTestMixin;
4+
import io.codemodder.testutils.Metadata;
5+
6+
@Metadata(
7+
codemodType = HardenStringParseToPrimitivesCodemod.class,
8+
testResourceDir = "harden-string-parse-to-primitives-s2130",
9+
renameTestFile = "src/main/java/org/owasp/webgoat/lessons/challenges/Flags.java",
10+
dependencies = {})
11+
final class HardenStringParseToPrimitivesCodemodTest implements CodemodTestMixin {}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.owasp.webgoat.lessons.challenges;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import java.util.UUID;
6+
import java.util.stream.IntStream;
7+
import org.owasp.webgoat.container.lessons.Lesson;
8+
import org.springframework.context.annotation.Configuration;
9+
10+
@Configuration
11+
public class Flags {
12+
private final Map<Integer, Flag> FLAGS = new HashMap<>();
13+
14+
public Flags() {
15+
IntStream.range(1, 10).forEach(i -> FLAGS.put(i, new Flag(i, UUID.randomUUID().toString())));
16+
}
17+
18+
public Flag getFlag(Lesson forLesson) {
19+
String myNum = "42.0";
20+
float myFloat = new Float(myNum);
21+
22+
String lessonName = forLesson.getName();
23+
int challengeNumber = Integer.parseInt(lessonName.substring(lessonName.length() - 1));
24+
float floatNumber = Float.parseFloat(myNum);
25+
float floatValue = Float.parseFloat(myNum);
26+
float intValue = Integer.parseInt(myNum);
27+
System.out.println(myFloat + floatValue + intValue + floatNumber);
28+
return FLAGS.get(challengeNumber);
29+
}
30+
31+
public Flag getFlag(int flagNumber) {
32+
return FLAGS.get(flagNumber);
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.owasp.webgoat.lessons.challenges;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import java.util.UUID;
6+
import java.util.stream.IntStream;
7+
import org.owasp.webgoat.container.lessons.Lesson;
8+
import org.springframework.context.annotation.Configuration;
9+
10+
@Configuration
11+
public class Flags {
12+
private final Map<Integer, Flag> FLAGS = new HashMap<>();
13+
14+
public Flags() {
15+
IntStream.range(1, 10).forEach(i -> FLAGS.put(i, new Flag(i, UUID.randomUUID().toString())));
16+
}
17+
18+
public Flag getFlag(Lesson forLesson) {
19+
String myNum = "42.0";
20+
float myFloat = new Float(myNum);
21+
22+
String lessonName = forLesson.getName();
23+
int challengeNumber = Integer.valueOf(lessonName.substring(lessonName.length() - 1));
24+
float floatNumber = Float.valueOf(myNum);
25+
float floatValue = Float.valueOf(myNum).floatValue();
26+
float intValue = Integer.valueOf(myNum).intValue();
27+
System.out.println(myFloat + floatValue + intValue + floatNumber);
28+
return FLAGS.get(challengeNumber);
29+
}
30+
31+
public Flag getFlag(int flagNumber) {
32+
return FLAGS.get(flagNumber);
33+
}
34+
}

0 commit comments

Comments
 (0)