Skip to content

Commit 2145da2

Browse files
authored
Sonar codemod to replace Stream.collect(Collectors.toList()) with Java 16 + Stream.toList() (#264)
Since Java 16 there is now a better variant to produce an unmodifiable list directly from a stream: Stream.toList(). Reference https://rules.sonarsource.com/java/RSPEC-6204/ Example: https://sonarcloud.io/project/issues?open=AYu2g5Hn97lOFaqEjR0v&id=pixee_codemodder-java
1 parent 9362e5f commit 2145da2

File tree

7 files changed

+364
-0
lines changed

7 files changed

+364
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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.MethodCallExpr;
7+
import io.codemodder.Codemod;
8+
import io.codemodder.CodemodExecutionPriority;
9+
import io.codemodder.CodemodInvocationContext;
10+
import io.codemodder.ReviewGuidance;
11+
import io.codemodder.providers.sonar.ProvidedSonarScan;
12+
import io.codemodder.providers.sonar.RuleIssues;
13+
import io.codemodder.providers.sonar.SonarPluginJavaParserChanger;
14+
import io.codemodder.providers.sonar.api.Issue;
15+
import java.util.Optional;
16+
import javax.inject.Inject;
17+
18+
/** A codemod for replacing 'Stream.collect(Collectors.toList())' with 'Stream.toList()' */
19+
@Codemod(
20+
id = "sonar:java/replace-stream-collectors-to-list-s6204",
21+
reviewGuidance = ReviewGuidance.MERGE_WITHOUT_REVIEW,
22+
executionPriority = CodemodExecutionPriority.HIGH)
23+
public final class ReplaceStreamCollectorsToListCodemod
24+
extends SonarPluginJavaParserChanger<MethodCallExpr> {
25+
26+
@Inject
27+
public ReplaceStreamCollectorsToListCodemod(
28+
@ProvidedSonarScan(ruleId = "java:S6204") final RuleIssues issues) {
29+
super(issues, MethodCallExpr.class);
30+
}
31+
32+
@Override
33+
public boolean onIssueFound(
34+
final CodemodInvocationContext context,
35+
final CompilationUnit cu,
36+
final MethodCallExpr methodCallExpr,
37+
final Issue issue) {
38+
39+
final Optional<Node> collectMethodExprOptional = methodCallExpr.getParentNode();
40+
41+
if (collectMethodExprOptional.isEmpty()) {
42+
return false;
43+
}
44+
45+
final MethodCallExpr collectMethodExpr = (MethodCallExpr) collectMethodExprOptional.get();
46+
collectMethodExpr.setName("toList");
47+
48+
collectMethodExpr.setArguments(new NodeList<>());
49+
50+
return true;
51+
}
52+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
This change modernizes the a stream's `List` creation to be driven from the simple, and more readable [`Stream#toList()`](https://docs.oracle.com/javase/16/docs/api/java.base/java/util/stream/Collectors.html#toList()) method.
2+
3+
Our changes look something like this:
4+
5+
```diff
6+
- List<Integer> numbers = someStream.collect(Collectors.toList());
7+
+ List<Integer> numbers = someStream.toList();
8+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"summary" : "Replaced `Stream.collect(Collectors.toList())` with `Stream.toList()` (Sonar)",
3+
"change" : "Replaced `Stream.collect(Collectors.toList())` with `Stream.toList()`.",
4+
"references" : [
5+
"https://rules.sonarsource.com/java/RSPEC-6204/"
6+
]
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.codemodder.codemods;
2+
3+
import io.codemodder.testutils.CodemodTestMixin;
4+
import io.codemodder.testutils.Metadata;
5+
6+
@Metadata(
7+
codemodType = ReplaceStreamCollectorsToListCodemod.class,
8+
testResourceDir = "replace-collectors-toList-s6204",
9+
renameTestFile =
10+
"core-codemods/src/main/java/io/codemodder/codemods/MavenSecureURLCodemod.java",
11+
dependencies = {})
12+
final class ReplaceStreamCollectorsToListCodemodTest implements CodemodTestMixin {}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package io.codemodder.codemods;
2+
3+
import com.contrastsecurity.sarif.Result;
4+
import io.codemodder.*;
5+
import io.codemodder.providers.sarif.codeql.ProvidedCodeQLScan;
6+
import java.io.IOException;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.nio.file.StandardCopyOption;
10+
import java.util.List;
11+
import java.util.Objects;
12+
import java.util.Optional;
13+
import java.util.Set;
14+
import java.util.stream.Collectors;
15+
import javax.inject.Inject;
16+
import javax.xml.stream.XMLEventFactory;
17+
import javax.xml.stream.XMLEventReader;
18+
import javax.xml.stream.XMLEventWriter;
19+
import javax.xml.stream.XMLStreamException;
20+
import javax.xml.stream.events.XMLEvent;
21+
import org.dom4j.DocumentException;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
import org.xml.sax.SAXException;
25+
26+
/** Fixes issues reported under the id "java/maven/non-https-url". */
27+
@Codemod(
28+
id = "codeql:java/maven/non-https-url",
29+
reviewGuidance = ReviewGuidance.MERGE_WITHOUT_REVIEW,
30+
executionPriority = CodemodExecutionPriority.HIGH)
31+
public final class MavenSecureURLCodemod extends SarifPluginRawFileChanger {
32+
33+
private final XPathStreamProcessor processor;
34+
35+
@Inject
36+
MavenSecureURLCodemod(
37+
@ProvidedCodeQLScan(ruleId = "java/maven/non-https-url") final RuleSarif sarif,
38+
final XPathStreamProcessor processor) {
39+
super(sarif);
40+
this.processor = Objects.requireNonNull(processor);
41+
}
42+
43+
@Override
44+
public List<CodemodChange> onFileFound(
45+
final CodemodInvocationContext context, final List<Result> results) {
46+
try {
47+
return processXml(context.path());
48+
} catch (SAXException | DocumentException | IOException | XMLStreamException e) {
49+
LOG.error("Problem transforming xml file: {}", context.path());
50+
return List.of();
51+
}
52+
}
53+
54+
private List<CodemodChange> processXml(final Path file)
55+
throws SAXException, IOException, DocumentException, XMLStreamException {
56+
Optional<XPathStreamProcessChange> change =
57+
processor.process(
58+
file,
59+
"//*[local-name()='repository']/*[local-name()='url'] |"
60+
+ " //*[local-name()='pluginRepository']/*[local-name()='url'] |"
61+
+ " //*[local-name()='snapshotRepository']/*[local-name()='url']",
62+
MavenSecureURLCodemod::handle);
63+
64+
if (change.isEmpty()) {
65+
return List.of();
66+
}
67+
68+
XPathStreamProcessChange xmlChange = change.get();
69+
Set<Integer> linesAffected = xmlChange.linesAffected();
70+
71+
List<CodemodChange> allWeaves =
72+
linesAffected.stream().map(CodemodChange::from).toList();
73+
74+
// overwrite the previous web.xml with the new one
75+
Files.copy(xmlChange.transformedXml(), file, StandardCopyOption.REPLACE_EXISTING);
76+
return allWeaves;
77+
}
78+
79+
/*
80+
* Change contents of the {@code url} tag if it it uses an insecure protocol.
81+
*/
82+
private static void handle(
83+
final XMLEventReader xmlEventReader,
84+
final XMLEventWriter xmlEventWriter,
85+
final XMLEvent currentEvent)
86+
throws XMLStreamException {
87+
final var xmlEventFactory = XMLEventFactory.newInstance();
88+
xmlEventWriter.add(currentEvent);
89+
final var nextEvent = xmlEventReader.nextEvent();
90+
final var url = nextEvent.asCharacters().getData();
91+
if (url.startsWith("http:")) {
92+
final var fixed = "https:" + url.substring(5);
93+
xmlEventWriter.add(xmlEventFactory.createCharacters(fixed));
94+
} else if (url.startsWith("ftp:")) {
95+
final var fixed = "ftps:" + url.substring(4);
96+
xmlEventWriter.add(xmlEventFactory.createCharacters(fixed));
97+
} else {
98+
xmlEventWriter.add(nextEvent);
99+
}
100+
}
101+
102+
private static final Logger LOG = LoggerFactory.getLogger(MavenSecureURLCodemod.class);
103+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package io.codemodder.codemods;
2+
3+
import com.contrastsecurity.sarif.Result;
4+
import io.codemodder.*;
5+
import io.codemodder.providers.sarif.codeql.ProvidedCodeQLScan;
6+
import java.io.IOException;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.nio.file.StandardCopyOption;
10+
import java.util.List;
11+
import java.util.Objects;
12+
import java.util.Optional;
13+
import java.util.Set;
14+
import java.util.stream.Collectors;
15+
import javax.inject.Inject;
16+
import javax.xml.stream.XMLEventFactory;
17+
import javax.xml.stream.XMLEventReader;
18+
import javax.xml.stream.XMLEventWriter;
19+
import javax.xml.stream.XMLStreamException;
20+
import javax.xml.stream.events.XMLEvent;
21+
import org.dom4j.DocumentException;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
import org.xml.sax.SAXException;
25+
26+
/** Fixes issues reported under the id "java/maven/non-https-url". */
27+
@Codemod(
28+
id = "codeql:java/maven/non-https-url",
29+
reviewGuidance = ReviewGuidance.MERGE_WITHOUT_REVIEW,
30+
executionPriority = CodemodExecutionPriority.HIGH)
31+
public final class MavenSecureURLCodemod extends SarifPluginRawFileChanger {
32+
33+
private final XPathStreamProcessor processor;
34+
35+
@Inject
36+
MavenSecureURLCodemod(
37+
@ProvidedCodeQLScan(ruleId = "java/maven/non-https-url") final RuleSarif sarif,
38+
final XPathStreamProcessor processor) {
39+
super(sarif);
40+
this.processor = Objects.requireNonNull(processor);
41+
}
42+
43+
@Override
44+
public List<CodemodChange> onFileFound(
45+
final CodemodInvocationContext context, final List<Result> results) {
46+
try {
47+
return processXml(context.path());
48+
} catch (SAXException | DocumentException | IOException | XMLStreamException e) {
49+
LOG.error("Problem transforming xml file: {}", context.path());
50+
return List.of();
51+
}
52+
}
53+
54+
private List<CodemodChange> processXml(final Path file)
55+
throws SAXException, IOException, DocumentException, XMLStreamException {
56+
Optional<XPathStreamProcessChange> change =
57+
processor.process(
58+
file,
59+
"//*[local-name()='repository']/*[local-name()='url'] |"
60+
+ " //*[local-name()='pluginRepository']/*[local-name()='url'] |"
61+
+ " //*[local-name()='snapshotRepository']/*[local-name()='url']",
62+
MavenSecureURLCodemod::handle);
63+
64+
if (change.isEmpty()) {
65+
return List.of();
66+
}
67+
68+
XPathStreamProcessChange xmlChange = change.get();
69+
Set<Integer> linesAffected = xmlChange.linesAffected();
70+
71+
List<CodemodChange> allWeaves =
72+
linesAffected.stream().map(CodemodChange::from).collect(Collectors.toList());
73+
74+
// overwrite the previous web.xml with the new one
75+
Files.copy(xmlChange.transformedXml(), file, StandardCopyOption.REPLACE_EXISTING);
76+
return allWeaves;
77+
}
78+
79+
/*
80+
* Change contents of the {@code url} tag if it it uses an insecure protocol.
81+
*/
82+
private static void handle(
83+
final XMLEventReader xmlEventReader,
84+
final XMLEventWriter xmlEventWriter,
85+
final XMLEvent currentEvent)
86+
throws XMLStreamException {
87+
final var xmlEventFactory = XMLEventFactory.newInstance();
88+
xmlEventWriter.add(currentEvent);
89+
final var nextEvent = xmlEventReader.nextEvent();
90+
final var url = nextEvent.asCharacters().getData();
91+
if (url.startsWith("http:")) {
92+
final var fixed = "https:" + url.substring(5);
93+
xmlEventWriter.add(xmlEventFactory.createCharacters(fixed));
94+
} else if (url.startsWith("ftp:")) {
95+
final var fixed = "ftps:" + url.substring(4);
96+
xmlEventWriter.add(xmlEventFactory.createCharacters(fixed));
97+
} else {
98+
xmlEventWriter.add(nextEvent);
99+
}
100+
}
101+
102+
private static final Logger LOG = LoggerFactory.getLogger(MavenSecureURLCodemod.class);
103+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"total": 1,
3+
"p": 1,
4+
"ps": 500,
5+
"paging": {
6+
"pageIndex": 1,
7+
"pageSize": 500,
8+
"total": 1
9+
},
10+
"effortTotal": 5,
11+
"debtTotal": 5,
12+
"issues": [
13+
{
14+
"key": "AYu2g5Hn97lOFaqEjR0v",
15+
"rule": "java:S6204",
16+
"severity": "MAJOR",
17+
"component": "pixee_codemodder-java:core-codemods/src/main/java/io/codemodder/codemods/MavenSecureURLCodemod.java",
18+
"project": "pixee_codemodder-java",
19+
"line": 72,
20+
"hash": "dde477e51d696ed510a0ade800320f23",
21+
"textRange": {
22+
"startLine": 72,
23+
"endLine": 72,
24+
"startOffset": 64,
25+
"endOffset": 83
26+
},
27+
"flows": [],
28+
"status": "OPEN",
29+
"message": "Replace this usage of 'Stream.collect(Collectors.toList())' with 'Stream.toList()'",
30+
"effort": "5min",
31+
"debt": "5min",
32+
"assignee": "nahsra@github",
33+
"author": "[email protected]",
34+
"tags": [
35+
"java16"
36+
],
37+
"creationDate": "2023-07-28T17:02:08+0200",
38+
"updateDate": "2023-11-10T00:55:25+0100",
39+
"type": "CODE_SMELL",
40+
"organization": "pixee",
41+
"cleanCodeAttribute": "CLEAR",
42+
"cleanCodeAttributeCategory": "INTENTIONAL",
43+
"impacts": [
44+
{
45+
"softwareQuality": "MAINTAINABILITY",
46+
"severity": "MEDIUM"
47+
}
48+
]
49+
}
50+
],
51+
"components": [
52+
{
53+
"organization": "pixee",
54+
"key": "pixee_codemodder-java",
55+
"uuid": "AYu2gu7gBsP_nwHMid1L",
56+
"enabled": true,
57+
"qualifier": "TRK",
58+
"name": "codemodder-java",
59+
"longName": "codemodder-java"
60+
},
61+
{
62+
"organization": "pixee",
63+
"key": "pixee_codemodder-java:core-codemods/src/main/java/io/codemodder/codemods/MavenSecureURLCodemod.java",
64+
"uuid": "AYu2g4-e97lOFaqEjRrD",
65+
"enabled": true,
66+
"qualifier": "FIL",
67+
"name": "MavenSecureURLCodemod.java",
68+
"longName": "core-codemods/src/main/java/io/codemodder/codemods/MavenSecureURLCodemod.java",
69+
"path": "core-codemods/src/main/java/io/codemodder/codemods/MavenSecureURLCodemod.java"
70+
}
71+
],
72+
"organizations": [
73+
{
74+
"key": "pixee",
75+
"name": "Pixee"
76+
}
77+
],
78+
"facets": []
79+
}

0 commit comments

Comments
 (0)