Skip to content

Commit 94f44c7

Browse files
authored
SONARJAVA-6273 S3706: "stream" should not be used for Collection "forEach" calls (#5581)
1 parent 8652ee2 commit 94f44c7

10 files changed

Lines changed: 193 additions & 1 deletion

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ruleKey": "S3706",
3+
"hasTruePositives": true,
4+
"falseNegatives": 0,
5+
"falsePositives": 0
6+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/permission/ws/template/SearchTemplatesDataLoader.java": [
3+
96
4+
],
5+
"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/ws/ShowAction.java": [
6+
140
7+
],
8+
"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/qualityprofile/RuleActivatorTest.java": [
9+
863,
10+
898
11+
]
12+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package checks;
2+
3+
import java.util.Collection;
4+
import java.util.List;
5+
import java.util.Set;
6+
import java.util.stream.Stream;
7+
8+
public class StreamForeachCheck {
9+
10+
void unnecessaryStreamForEach(Collection<Integer> collection) {
11+
collection.stream().forEach(System.out::println); // Noncompliant {{Simplify the code by replacing .stream().forEach() with .forEach().}}
12+
// ^^^^^^^^^^^^^^^^
13+
}
14+
15+
void compliantCollectionForEach(Collection<Integer> collection) {
16+
collection.forEach(System.out::println); // Compliant
17+
}
18+
19+
void necessaryFilterForEach(Collection<String> col) {
20+
col.stream().filter(s -> !s.isEmpty()).forEach(System.out::println);
21+
}
22+
23+
void unnecessaryForEachOnSet(Set<String> set) {
24+
set.stream().forEach(e -> System.out.println("Element: " + e)); // Noncompliant
25+
}
26+
27+
void unnecessaryForEachOnList(List<String> list) {
28+
list.stream().forEach(e -> System.out.println("Element: " + e)); // Noncompliant
29+
}
30+
31+
void necessaryForEachOnParallelStream(Set<String> set) {
32+
set.parallelStream().forEach(System.out::println); // Compliant
33+
}
34+
35+
void sequentialStreamForEach(Collection<String> col) {
36+
Stream<String> s = col.stream();
37+
s.forEach(System.out::println); // Compliant (the rule ignores this case)
38+
}
39+
40+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* You can redistribute and/or modify this program under the terms of
7+
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.checks;
18+
19+
import java.util.List;
20+
import org.sonar.check.Rule;
21+
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
22+
import org.sonar.plugins.java.api.semantic.MethodMatchers;
23+
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
24+
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
25+
import org.sonar.plugins.java.api.tree.Tree;
26+
27+
@Rule(key = "S3706")
28+
public class StreamForeachCheck extends IssuableSubscriptionVisitor {
29+
30+
private static final MethodMatchers STREAM_METHOD = MethodMatchers.create()
31+
.ofSubTypes("java.util.Collection")
32+
.names("stream")
33+
.addWithoutParametersMatcher()
34+
.build();
35+
36+
private static final MethodMatchers STREAM_FOREACH_METHOD = MethodMatchers.create()
37+
.ofSubTypes("java.util.stream.Stream")
38+
.names("forEach")
39+
.withAnyParameters()
40+
.build();
41+
42+
@Override
43+
public List<Tree.Kind> nodesToVisit() {
44+
return List.of(Tree.Kind.METHOD_INVOCATION);
45+
}
46+
47+
@Override
48+
public void visitNode(Tree tree) {
49+
if (tree instanceof MethodInvocationTree mit && STREAM_FOREACH_METHOD.matches(mit)) {
50+
checkUnnecessaryForEach(mit);
51+
}
52+
}
53+
54+
private void checkUnnecessaryForEach(MethodInvocationTree mitForEach) {
55+
if (mitForEach.methodSelect() instanceof MemberSelectExpressionTree msetForEach
56+
&& msetForEach.expression() instanceof MethodInvocationTree mitStream
57+
&& STREAM_METHOD.matches(mitStream)
58+
&& mitStream.methodSelect() instanceof MemberSelectExpressionTree msetStream) {
59+
reportIssue(msetStream.identifier(), msetForEach.identifier(), "Simplify the code by replacing .stream().forEach() with .forEach().");
60+
}
61+
}
62+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* You can redistribute and/or modify this program under the terms of
7+
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.java.checks.verifier.CheckVerifier;
21+
22+
import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;
23+
24+
class StreamForeachCheckTest {
25+
@Test
26+
void test() {
27+
StreamForeachCheck check = new StreamForeachCheck();
28+
CheckVerifier.newVerifier()
29+
.onFile(mainCodeSourcesPath("checks/StreamForeachCheck.java"))
30+
.withCheck(check)
31+
.verifyIssues();
32+
}
33+
34+
@Test
35+
void testWithoutSemantic() {
36+
StreamForeachCheck check = new StreamForeachCheck();
37+
CheckVerifier.newVerifier()
38+
.onFile(mainCodeSourcesPath("checks/StreamForeachCheck.java"))
39+
.withoutSemantic()
40+
.withCheck(check)
41+
.verifyIssues();
42+
}
43+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<h2>Why is this an issue?</h2>
2+
<p>There’s no need to invoke <code>stream()</code> on a <code>Collection</code> before a <code>forEach</code> call because each
3+
<code>Collection</code> has its own <code>forEach</code> method.</p>
4+
<h3>Noncompliant code example</h3>
5+
<pre>
6+
identifiers.stream().forEach(System.out::println); // Noncompliant
7+
</pre>
8+
<h3>Compliant solution</h3>
9+
<pre>
10+
identifiers.forEach(System.out::println); // Compliant
11+
</pre>
12+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"title": "\"stream\" should not be used for Collection \"forEach\" calls",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "2min"
8+
},
9+
"tags": [],
10+
"defaultSeverity": "Minor",
11+
"ruleSpecification": "RSPEC-3706",
12+
"sqKey": "S3706",
13+
"scope": "All",
14+
"quickfix": "unknown"
15+
}

sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@
259259
"S3599",
260260
"S3626",
261261
"S3631",
262+
"S3706",
262263
"S3740",
263264
"S3751",
264265
"S3752",

sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@
276276
"S3599",
277277
"S3626",
278278
"S3631",
279+
"S3706",
279280
"S3740",
280281
"S3751",
281282
"S3752",

sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ void profile_is_registered_as_expected() {
5353
BuiltInQualityProfilesDefinition.BuiltInQualityProfile actualProfile = profilesPerLanguages.get("java").get("Sonar agentic AI");
5454
assertThat(actualProfile.isDefault()).isFalse();
5555
assertThat(actualProfile.rules())
56-
.hasSize(467)
56+
.hasSize(468)
5757
.extracting(BuiltInQualityProfilesDefinition.BuiltInActiveRule::ruleKey)
5858
.doesNotContainAnyElementsOf(List.of(
5959
"S101",

0 commit comments

Comments
 (0)