diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S3706.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S3706.json new file mode 100644 index 00000000000..b7e24fd8fac --- /dev/null +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S3706.json @@ -0,0 +1,6 @@ +{ + "ruleKey": "S3706", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 +} diff --git a/its/ruling/src/test/resources/sonar-server/java-S3706.json b/its/ruling/src/test/resources/sonar-server/java-S3706.json new file mode 100644 index 00000000000..6bbb082d221 --- /dev/null +++ b/its/ruling/src/test/resources/sonar-server/java-S3706.json @@ -0,0 +1,12 @@ +{ + "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/permission/ws/template/SearchTemplatesDataLoader.java": [ + 96 + ], + "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/ws/ShowAction.java": [ + 140 + ], + "org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/qualityprofile/RuleActivatorTest.java": [ + 863, + 898 + ] +} diff --git a/java-checks-test-sources/default/src/main/java/checks/StreamForeachCheck.java b/java-checks-test-sources/default/src/main/java/checks/StreamForeachCheck.java new file mode 100644 index 00000000000..c2694a1d575 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/StreamForeachCheck.java @@ -0,0 +1,40 @@ +package checks; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +public class StreamForeachCheck { + + void unnecessaryStreamForEach(Collection collection) { + collection.stream().forEach(System.out::println); // Noncompliant {{Simplify the code by replacing .stream().forEach() with .forEach().}} + // ^^^^^^^^^^^^^^^^ + } + + void compliantCollectionForEach(Collection collection) { + collection.forEach(System.out::println); // Compliant + } + + void necessaryFilterForEach(Collection col) { + col.stream().filter(s -> !s.isEmpty()).forEach(System.out::println); + } + + void unnecessaryForEachOnSet(Set set) { + set.stream().forEach(e -> System.out.println("Element: " + e)); // Noncompliant + } + + void unnecessaryForEachOnList(List list) { + list.stream().forEach(e -> System.out.println("Element: " + e)); // Noncompliant + } + + void necessaryForEachOnParallelStream(Set set) { + set.parallelStream().forEach(System.out::println); // Compliant + } + + void sequentialStreamForEach(Collection col) { + Stream s = col.stream(); + s.forEach(System.out::println); // Compliant (the rule ignores this case) + } + +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/StreamForeachCheck.java b/java-checks/src/main/java/org/sonar/java/checks/StreamForeachCheck.java new file mode 100644 index 00000000000..bc3c64b45c8 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/StreamForeachCheck.java @@ -0,0 +1,62 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import java.util.List; +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S3706") +public class StreamForeachCheck extends IssuableSubscriptionVisitor { + + private static final MethodMatchers STREAM_METHOD = MethodMatchers.create() + .ofSubTypes("java.util.Collection") + .names("stream") + .addWithoutParametersMatcher() + .build(); + + private static final MethodMatchers STREAM_FOREACH_METHOD = MethodMatchers.create() + .ofSubTypes("java.util.stream.Stream") + .names("forEach") + .withAnyParameters() + .build(); + + @Override + public List nodesToVisit() { + return List.of(Tree.Kind.METHOD_INVOCATION); + } + + @Override + public void visitNode(Tree tree) { + if (tree instanceof MethodInvocationTree mit && STREAM_FOREACH_METHOD.matches(mit)) { + checkUnnecessaryForEach(mit); + } + } + + private void checkUnnecessaryForEach(MethodInvocationTree mitForEach) { + if (mitForEach.methodSelect() instanceof MemberSelectExpressionTree msetForEach + && msetForEach.expression() instanceof MethodInvocationTree mitStream + && STREAM_METHOD.matches(mitStream) + && mitStream.methodSelect() instanceof MemberSelectExpressionTree msetStream) { + reportIssue(msetStream.identifier(), msetForEach.identifier(), "Simplify the code by replacing .stream().forEach() with .forEach()."); + } + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/StreamForeachCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/StreamForeachCheckTest.java new file mode 100644 index 00000000000..1f4a3838661 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/StreamForeachCheckTest.java @@ -0,0 +1,43 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; + +class StreamForeachCheckTest { + @Test + void test() { + StreamForeachCheck check = new StreamForeachCheck(); + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/StreamForeachCheck.java")) + .withCheck(check) + .verifyIssues(); + } + + @Test + void testWithoutSemantic() { + StreamForeachCheck check = new StreamForeachCheck(); + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/StreamForeachCheck.java")) + .withoutSemantic() + .withCheck(check) + .verifyIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3706.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3706.html new file mode 100644 index 00000000000..c17b880b047 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3706.html @@ -0,0 +1,12 @@ +

Why is this an issue?

+

There’s no need to invoke stream() on a Collection before a forEach call because each +Collection has its own forEach method.

+

Noncompliant code example

+
+identifiers.stream().forEach(System.out::println);  // Noncompliant
+
+

Compliant solution

+
+identifiers.forEach(System.out::println);  // Compliant
+
+ diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3706.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3706.json new file mode 100644 index 00000000000..3ec15855155 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3706.json @@ -0,0 +1,15 @@ +{ + "title": "\"stream\" should not be used for Collection \"forEach\" calls", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "2min" + }, + "tags": [], + "defaultSeverity": "Minor", + "ruleSpecification": "RSPEC-3706", + "sqKey": "S3706", + "scope": "All", + "quickfix": "unknown" +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json index c60eaa9f57b..50a1ca0a6bd 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_agentic_AI_profile.json @@ -259,6 +259,7 @@ "S3599", "S3626", "S3631", + "S3706", "S3740", "S3751", "S3752", diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json index 85a39068bed..584a1c6a008 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json @@ -276,6 +276,7 @@ "S3599", "S3626", "S3631", + "S3706", "S3740", "S3751", "S3752", diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java index 8f2933b4a37..07b8e02b4e2 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/JavaAgenticWayProfileTest.java @@ -53,7 +53,7 @@ void profile_is_registered_as_expected() { BuiltInQualityProfilesDefinition.BuiltInQualityProfile actualProfile = profilesPerLanguages.get("java").get("Sonar agentic AI"); assertThat(actualProfile.isDefault()).isFalse(); assertThat(actualProfile.rules()) - .hasSize(467) + .hasSize(468) .extracting(BuiltInQualityProfilesDefinition.BuiltInActiveRule::ruleKey) .doesNotContainAnyElementsOf(List.of( "S101",