Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

- compatibility updates for SonarQube 25.5.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,4 +548,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[]{25};
int[] endLines = new int[]{25};

checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_5MIN);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I execute the integration test, I have the following error and it's relevant for me. You should modify your startLines, endLines and severity value.

[ERROR] Failures:
[ERROR] GCIRulesIT.testGCI91:559->GCIRulesBase.checkIssuesForFile:84 [Extracted: rule, message, textRange.startLine, textRange.endLine, severity, type, effort]
Expecting ArrayList:
[("creedengo-java:GCI91", "Use 'filter' before 'sorted' for better efficiency.", 11, 14, MAJOR, CODE_SMELL, "5min"),
("creedengo-java:GCI91", "Use 'filter' before 'sorted' for better efficiency.", 16, 20, MAJOR, CODE_SMELL, "5min")]
to contain:
[("creedengo-java:GCI91", "Use 'filter' before 'sorted' for better efficiency.", 25, 25, MINOR, CODE_SMELL, "5min")]
but could not find the following element(s):
[("creedengo-java:GCI91", "Use 'filter' before 'sorted' for better efficiency.", 25, 25, MINOR, CODE_SMELL, "5min")]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the line 25 is the good one and not the bad one.

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.greencodeinitiative.creedengo.java.checks;

import java.util.List;
import java.util.stream.Collectors;

class UseFilterBeforeSort {
UseFilterBeforeSort() {
}

public void manipulateStream(final List<String> list) {
list.stream() // Noncompliant {{Use 'filter' before 'sorted' for better efficiency.}}
.sorted()
.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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ public class JavaCheckRegistrar implements CheckRegistrar {
FreeResourcesOfAutoCloseableInterface.class,
AvoidMultipleIfElseStatement.class,
UseOptionalOrElseGetVsOrElse.class,
MakeNonReassignedVariablesConstants.class
MakeNonReassignedVariablesConstants.class,
UseFilterBeforeSort.class
);

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Tree.Kind> 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; // On ignore les appels intermédiaires (filter, sorted, etc.)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please put here comments in english ...

}

List<String> methodChain = extractChainedMethodNames(mit);

int sortedIndex = methodChain.indexOf("sorted");
int filterIndex = methodChain.indexOf("filter");

if (sortedIndex != -1 && filterIndex != -1 && sortedIndex < filterIndex) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure about your algorithms ... I think in streams we can have several select method ... thus you can control only the first one ? or more complex, control all the stream and we want only control th second part ... for example :
stream
. filtrer()
.sorted()
. findAny() /// first part
. sort()
. filter()
. collect() // second part

what is the behaviour for examples with the same complexity (several parts) ?
can you add some such cases in your test resources files to control the behaviour ?

reportIssue(tree, "Use 'filter' before 'sorted' for better efficiency.");
}
}
}

private List<String> extractChainedMethodNames(MethodInvocationTree terminalInvocation) {
List<String> 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(); // remonter à l'appel précédent
} else {
break;
}
}

Collections.reverse(methodNames); // pour avoir l’ordre 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
Copy link

Copilot AI Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is in French. For consistency with the codebase, it should be in English: '// Add all known terminal operations here'

Suggested change
// Ajouter ici tous les terminaux connus
// Add all known terminal operations here

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please correct this feedback

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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"GCI78",
"GCI79",
"GCI82",
"GCI91",
"GCI94"
]
}
41 changes: 41 additions & 0 deletions src/test/files/UseFilterBeforeSort.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.greencodeinitiative.creedengo.java.checks;

class UseFilterBeforeSort {
UseFilterBeforeSort() {
}

public void manipulateStream(List<String> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading