diff --git a/CHANGELOG.md b/CHANGELOG.md index e657561c..cc47da74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- [GCI91](https://github.com/green-code-initiative/creedengo-java/pull/111) New rule to detect if a sort method is called before a filter in stream + ### Changed - [#103](https://github.com/green-code-initiative/creedengo-java/pull/103) GCI69 Java : calls to hasMoreElements() and nextElement() methods from java.util.Enumeration interface aren't flagged anymore when called in a for loop diff --git a/pom.xml b/pom.xml index ae43a190..014b03b9 100644 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,7 @@ 1.8 - 2.2.2 + 2.2.3 https://repo1.maven.org/maven2 @@ -231,6 +231,10 @@ org.apache.maven.plugins maven-compiler-plugin 3.13.0 + + 16 + 16 + org.apache.maven.plugins diff --git a/src/it/java/org/greencodeinitiative/creedengo/java/integration/tests/GCIRulesIT.java b/src/it/java/org/greencodeinitiative/creedengo/java/integration/tests/GCIRulesIT.java index eff99826..064272ab 100644 --- a/src/it/java/org/greencodeinitiative/creedengo/java/integration/tests/GCIRulesIT.java +++ b/src/it/java/org/greencodeinitiative/creedengo/java/integration/tests/GCIRulesIT.java @@ -1,6 +1,7 @@ package org.greencodeinitiative.creedengo.java.integration.tests; import org.junit.jupiter.api.Test; +import org.sonarqube.ws.Common; import org.sonarqube.ws.Issues; import org.sonarqube.ws.Measures; @@ -548,4 +549,15 @@ void testGCI94() { checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_1MIN); } + @Test + void testGCI91() { + String filePath = "src/main/java/org/greencodeinitiative/creedengo/java/checks/UseFilterBeforeSort.java"; + String ruleId = "creedengo-java:GCI91"; + String ruleMsg = "Use 'filter' before 'sorted' for better efficiency."; + int[] startLines = new int[]{11,16}; + int[] endLines = new int[]{14, 20}; + + checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, Common.Severity.MAJOR, TYPE, EFFORT_5MIN); + } + } diff --git a/src/it/test-projects/creedengo-java-plugin-test-project/src/main/java/org/greencodeinitiative/creedengo/java/checks/UseFilterBeforeSort.java b/src/it/test-projects/creedengo-java-plugin-test-project/src/main/java/org/greencodeinitiative/creedengo/java/checks/UseFilterBeforeSort.java new file mode 100644 index 00000000..18b9c321 --- /dev/null +++ b/src/it/test-projects/creedengo-java-plugin-test-project/src/main/java/org/greencodeinitiative/creedengo/java/checks/UseFilterBeforeSort.java @@ -0,0 +1,27 @@ +package org.greencodeinitiative.creedengo.java.checks; + +import java.util.List; +import java.util.stream.Collectors; + +class UseFilterBeforeSort { + UseFilterBeforeSort() { + } + + public void manipulateStream(final List list) { + list.stream() // Noncompliant {{Use 'filter' before 'sorted' for better efficiency.}} + .sorted() + .filter(s -> s.startsWith("A")) + .collect(Collectors.toList()); + + list.stream() // Noncompliant {{Use 'filter' before 'sorted' for better efficiency.}} + .sorted() + .map(element -> element.toString()) + .filter(s -> s.startsWith("A")) + .collect(Collectors.toList()); + + list.stream() // Compliant {{Use 'filter' before 'sorted' for better efficiency.}} + .filter(s -> s.startsWith("A")) + .sorted() + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/org/greencodeinitiative/creedengo/java/JavaCheckRegistrar.java b/src/main/java/org/greencodeinitiative/creedengo/java/JavaCheckRegistrar.java index 791f0cef..1f084e39 100644 --- a/src/main/java/org/greencodeinitiative/creedengo/java/JavaCheckRegistrar.java +++ b/src/main/java/org/greencodeinitiative/creedengo/java/JavaCheckRegistrar.java @@ -50,7 +50,8 @@ public class JavaCheckRegistrar implements CheckRegistrar { FreeResourcesOfAutoCloseableInterface.class, AvoidMultipleIfElseStatement.class, UseOptionalOrElseGetVsOrElse.class, - MakeNonReassignedVariablesConstants.class + MakeNonReassignedVariablesConstants.class, + UseFilterBeforeSort.class ); /** diff --git a/src/main/java/org/greencodeinitiative/creedengo/java/checks/UseFilterBeforeSort.java b/src/main/java/org/greencodeinitiative/creedengo/java/checks/UseFilterBeforeSort.java new file mode 100644 index 00000000..8b6b04fe --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/java/checks/UseFilterBeforeSort.java @@ -0,0 +1,77 @@ +package org.greencodeinitiative.creedengo.java.checks; + +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.tree.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Rule(key = "GCI91") +public class UseFilterBeforeSort extends IssuableSubscriptionVisitor { + @Override + public List nodesToVisit() { + return Collections.singletonList(Tree.Kind.METHOD_INVOCATION); + } + + @Override + public void visitNode(Tree tree) { + if (tree instanceof MethodInvocationTree) { + MethodInvocationTree mit = (MethodInvocationTree) tree; + + if (!isTerminalOperation(mit)) { + return; // Ignoring intermediate steps (filter, sorted, etc.) + } + + List methodChain = extractChainedMethodNames(mit); + + int sortedIndex = methodChain.indexOf("sorted"); + int filterIndex = methodChain.indexOf("filter"); + + if (sortedIndex != -1 && filterIndex != -1 && sortedIndex < filterIndex) { + reportIssue(tree, "Use 'filter' before 'sorted' for better efficiency."); + } + } + } + + private List extractChainedMethodNames(MethodInvocationTree terminalInvocation) { + List methodNames = new ArrayList<>(); + ExpressionTree current = terminalInvocation; + + while (current instanceof MethodInvocationTree) { + MethodInvocationTree methodCall = (MethodInvocationTree) current; + ExpressionTree methodSelect = methodCall.methodSelect(); + + if (methodSelect instanceof MemberSelectExpressionTree) { + MemberSelectExpressionTree memberSelect = (MemberSelectExpressionTree) methodSelect; + methodNames.add(memberSelect.identifier().name()); + current = memberSelect.expression(); // Go to the previous method + } else { + break; + } + } + + Collections.reverse(methodNames); // Reverse to get right order in collection like stream → ... → collect + return methodNames; + } + + private boolean isTerminalOperation(MethodInvocationTree tree) { + if (!(tree.methodSelect() instanceof MemberSelectExpressionTree)) { + return false; + } + String methodName = ((MemberSelectExpressionTree) tree.methodSelect()).identifier().name(); + // Ajouter ici tous les terminaux connus + return "collect".equals(methodName) + || "forEach".equals(methodName) + || "reduce".equals(methodName) + || "count".equals(methodName) + || "toArray".equals(methodName) + || "anyMatch".equals(methodName) + || "allMatch".equals(methodName) + || "noneMatch".equals(methodName) + || "findFirst".equals(methodName) + || "findAny".equals(methodName); + } + +} diff --git a/src/main/resources/org/greencodeinitiative/creedengo/java/creedengo_way_profile.json b/src/main/resources/org/greencodeinitiative/creedengo/java/creedengo_way_profile.json index 059bf0f5..33fb1a7e 100644 --- a/src/main/resources/org/greencodeinitiative/creedengo/java/creedengo_way_profile.json +++ b/src/main/resources/org/greencodeinitiative/creedengo/java/creedengo_way_profile.json @@ -18,6 +18,7 @@ "GCI78", "GCI79", "GCI82", + "GCI91", "GCI94" ] } diff --git a/src/test/files/UseFilterBeforeSort.java b/src/test/files/UseFilterBeforeSort.java new file mode 100644 index 00000000..1b5712ce --- /dev/null +++ b/src/test/files/UseFilterBeforeSort.java @@ -0,0 +1,44 @@ +/* + * creedengo - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2024 Green Code Initiative (https://green-code-initiative.org/) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.greencodeinitiative.creedengo.java.checks; + +import java.util.List; +import java.util.stream.Collectors; + +class UseFilterBeforeSort { + UseFilterBeforeSort() { + } + + public void manipulateStream(List list) { + list.stream() // Noncompliant {{Use 'filter' before 'sorted' for better efficiency.}} + .sorted() + .filter(s -> s.startsWith("A")) + .collect(Collectors.toList()); + + list.stream() // Noncompliant {{Use 'filter' before 'sorted' for better efficiency.}} + .sorted() + .otherMethod() + .filter(s -> s.startsWith("A")) + .collect(Collectors.toList()); + + list.stream() // Compliant {{Use 'filter' before 'sorted' for better efficiency.}} + .filter(s -> s.startsWith("A")) + .sorted() + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/test/java/org/greencodeinitiative/creedengo/java/checks/UseFilterBeforeSortTest.java b/src/test/java/org/greencodeinitiative/creedengo/java/checks/UseFilterBeforeSortTest.java new file mode 100644 index 00000000..4d9a53d5 --- /dev/null +++ b/src/test/java/org/greencodeinitiative/creedengo/java/checks/UseFilterBeforeSortTest.java @@ -0,0 +1,14 @@ +package org.greencodeinitiative.creedengo.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +public class UseFilterBeforeSortTest { + @Test + void testHasIssues() { + CheckVerifier.newVerifier() + .onFile("src/test/files/UseFilterBeforeSort.java") + .withCheck(new UseFilterBeforeSort()) + .verifyIssues(); + } +}