Repository navigation
SONARJAVA-7043: Implemented rule S9412 "Collections.sort()" should not be used #6221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c04b1ac
SONARJAVA-7043: Implemented rule S9412 "Collections.sort()" should no…
romainbrenguier 7c3d57c
SONARJAVA-7043: Add Java 8 version gate, fix quickfix parenthesizatio…
romainbrenguier af789ec
Update ruling results
github-actions[bot] 4f54e6b
SONARJAVA-7043: Use EnumSet instead of Set.of() for Tree.Kind collection
romainbrenguier 9ae90fc
SONARJAVA-7043: Use a fixed quick fix title
romainbrenguier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
14 changes: 14 additions & 0 deletions
14
its/ruling/src/test/resources/expected/eclipse-jetty/java-S9412.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/ProxyCustomizerTest.java": [ | ||
| 97 | ||
| ], | ||
| "org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerTest.java": [ | ||
| 792 | ||
| ], | ||
| "org.eclipse.jetty:jetty-project:jetty-util/src/main/java/org/eclipse/jetty/util/PathWatcher.java": [ | ||
| 840 | ||
| ], | ||
| "org.eclipse.jetty:jetty-project:jetty-xml/src/test/java/org/eclipse/jetty/xml/XmlConfigurationTest.java": [ | ||
| 693 | ||
| ] | ||
| } |
14 changes: 14 additions & 0 deletions
14
its/ruling/src/test/resources/expected/guava/java-S9412.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "com.google.guava:guava:src/com/google/common/collect/ImmutableMultimap.java": [ | ||
| 280 | ||
| ], | ||
| "com.google.guava:guava:src/com/google/common/collect/Ordering.java": [ | ||
| 698 | ||
| ], | ||
| "com.google.guava:guava:src/com/google/common/collect/RegularImmutableTable.java": [ | ||
| 125 | ||
| ], | ||
| "com.google.guava:guava:src/com/google/common/util/concurrent/ServiceManager.java": [ | ||
| 597 | ||
| ] | ||
| } |
19 changes: 19 additions & 0 deletions
19
its/ruling/src/test/resources/expected/sonar-server/java-S9412.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/source/DuplicationLineReader.java": [ | ||
| 69 | ||
| ], | ||
| "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/source/SymbolsLineReader.java": [ | ||
| 58, | ||
| 83 | ||
| ], | ||
| "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/duplication/ws/DuplicationsParser.java": [ | ||
| 75, | ||
| 78 | ||
| ], | ||
| "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/IndexDefinitionHash.java": [ | ||
| 79 | ||
| ], | ||
| "org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/duplication/TextBlockTest.java": [ | ||
| 92 | ||
| ] | ||
| } |
75 changes: 75 additions & 0 deletions
75
java-checks-test-sources/default/src/main/java/checks/CollectionsSortCheckSample.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package checks; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.Comparator; | ||
| import java.util.LinkedList; | ||
| import java.util.List; | ||
|
|
||
| class CollectionsSortCheckSample { | ||
|
|
||
| void noncompliant(List<String> myList, Comparator<String> myComparator) { | ||
| Collections.sort(myList); // Noncompliant {{Replace this "Collections.sort()" with "List.sort()".}} [[quickfixes=qf1]] | ||
| // ^^^^ | ||
| // fix@qf1 {{Replace with "List.sort()"}} | ||
| // edit@qf1 [[sc=5;ec=29]] {{myList.sort(null)}} | ||
| Collections.sort(myList, Comparator.naturalOrder()); // Noncompliant {{Replace this "Collections.sort()" with "List.sort()".}} [[quickfixes=qf2]] | ||
| // ^^^^ | ||
| // fix@qf2 {{Replace with "List.sort()"}} | ||
| // edit@qf2 [[sc=5;ec=56]] {{myList.sort(Comparator.naturalOrder())}} | ||
| Collections.sort(myList, myComparator); // Noncompliant {{Replace this "Collections.sort()" with "List.sort()".}} [[quickfixes=qf3]] | ||
| // ^^^^ | ||
| // fix@qf3 {{Replace with "List.sort()"}} | ||
| // edit@qf3 [[sc=5;ec=43]] {{myList.sort(myComparator)}} | ||
| } | ||
|
|
||
| void noncompliantSubtypes() { | ||
| ArrayList<String> arrayList = new ArrayList<>(); | ||
| Collections.sort(arrayList); // Noncompliant | ||
| // ^^^^ | ||
|
|
||
| LinkedList<Integer> linkedList = new LinkedList<>(); | ||
| Collections.sort(linkedList); // Noncompliant | ||
| // ^^^^ | ||
| } | ||
|
|
||
| void noncompliantMethodReturn() { | ||
| Collections.sort(getList()); // Noncompliant | ||
| // ^^^^ | ||
| } | ||
|
|
||
| void noncompliantComplexExpression(boolean flag, List<String> a, List<String> b) { | ||
| Collections.sort(flag ? a : b); // Noncompliant [[quickfixes=qf4]] | ||
| // ^^^^ | ||
| // fix@qf4 {{Replace with "List.sort()"}} | ||
| // edit@qf4 [[sc=5;ec=35]] {{(flag ? a : b).sort(null)}} | ||
| } | ||
|
|
||
| void noncompliantFullyQualified(List<String> myList) { | ||
| java.util.Collections.sort(myList); // Noncompliant | ||
| } | ||
|
|
||
| void compliant(List<String> myList, Comparator<String> comparator) { | ||
| myList.sort(null); | ||
| myList.sort(comparator); | ||
| Collections.shuffle(myList); | ||
| Collections.reverse(myList); | ||
| Collections.unmodifiableList(myList); | ||
| int[] array = {3, 1, 2}; | ||
| Arrays.sort(array); | ||
| } | ||
|
|
||
| void compliantCustomClass(List<String> list) { | ||
| MyCollections.sort(list); | ||
| } | ||
|
|
||
| private List<String> getList() { | ||
| return new ArrayList<>(); | ||
| } | ||
|
|
||
| static class MyCollections { | ||
| static <T> void sort(List<T> list) { | ||
| } | ||
| } | ||
| } |
91 changes: 91 additions & 0 deletions
91
java-checks/src/main/java/org/sonar/java/checks/CollectionsSortCheck.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| /* | ||
| * 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.EnumSet; | ||
| import java.util.Set; | ||
| import org.sonar.check.Rule; | ||
| import org.sonar.java.checks.helpers.QuickFixHelper; | ||
| import org.sonar.java.checks.methods.AbstractMethodDetection; | ||
| import org.sonar.java.model.ExpressionUtils; | ||
| import org.sonar.java.reporting.JavaQuickFix; | ||
| import org.sonar.java.reporting.JavaTextEdit; | ||
| import org.sonar.plugins.java.api.JavaVersion; | ||
| import org.sonar.plugins.java.api.JavaVersionAwareVisitor; | ||
| import org.sonar.plugins.java.api.semantic.MethodMatchers; | ||
| import org.sonar.plugins.java.api.tree.ExpressionTree; | ||
| import org.sonar.plugins.java.api.tree.MethodInvocationTree; | ||
| import org.sonar.plugins.java.api.tree.Tree; | ||
|
|
||
| @Rule(key = "S9412") | ||
| public class CollectionsSortCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor { | ||
|
|
||
| private static final Set<Tree.Kind> SAFE_KINDS = EnumSet.of( | ||
| Tree.Kind.IDENTIFIER, | ||
| Tree.Kind.MEMBER_SELECT, | ||
| Tree.Kind.METHOD_INVOCATION, | ||
| Tree.Kind.ARRAY_ACCESS_EXPRESSION, | ||
| Tree.Kind.PARENTHESIZED_EXPRESSION, | ||
| Tree.Kind.NEW_CLASS); | ||
|
|
||
| @Override | ||
| public boolean isCompatibleWithJavaVersion(JavaVersion version) { | ||
| return version.isJava8Compatible(); | ||
| } | ||
|
|
||
| private static final String MESSAGE = "Replace this \"Collections.sort()\" with \"List.sort()\"."; | ||
|
|
||
| @Override | ||
| protected MethodMatchers getMethodInvocationMatchers() { | ||
| return MethodMatchers.create() | ||
| .ofTypes("java.util.Collections") | ||
| .names("sort") | ||
| .addParametersMatcher("java.util.List") | ||
| .addParametersMatcher("java.util.List", "java.util.Comparator") | ||
| .build(); | ||
| } | ||
|
|
||
| @Override | ||
| protected void onMethodInvocationFound(MethodInvocationTree mit) { | ||
| ExpressionTree listArgument = mit.arguments().get(0); | ||
| boolean hasTwoArgs = mit.arguments().size() == 2; | ||
|
|
||
| QuickFixHelper.newIssue(context) | ||
| .forRule(this) | ||
| .onTree(ExpressionUtils.methodName(mit)) | ||
| .withMessage(MESSAGE) | ||
| .withQuickFix(() -> buildQuickFix(mit, listArgument, hasTwoArgs)) | ||
| .report(); | ||
| } | ||
|
|
||
| private JavaQuickFix buildQuickFix(MethodInvocationTree mit, ExpressionTree listArgument, boolean hasTwoArgs) { | ||
| String listText = QuickFixHelper.contentForTree(listArgument, context); | ||
| if (!listArgument.is(SAFE_KINDS.toArray(new Tree.Kind[0]))) { | ||
| listText = "(" + listText + ")"; | ||
| } | ||
| String replacement; | ||
| if (hasTwoArgs) { | ||
| String comparatorText = QuickFixHelper.contentForTree(mit.arguments().get(1), context); | ||
| replacement = listText + ".sort(" + comparatorText + ")"; | ||
| } else { | ||
| replacement = listText + ".sort(null)"; | ||
| } | ||
| return JavaQuickFix.newQuickFix("Replace with \"List.sort()\"") | ||
| .addTextEdit(JavaTextEdit.replaceTree(mit, replacement)) | ||
| .build(); | ||
|
gitar-bot[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
55 changes: 55 additions & 0 deletions
55
java-checks/src/test/java/org/sonar/java/checks/CollectionsSortCheckTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| /* | ||
| * 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 CollectionsSortCheckTest { | ||
|
|
||
| @Test | ||
| void test() { | ||
| createCheckVerifier() | ||
| .withJavaVersion(8) | ||
| .verifyIssues(); | ||
| } | ||
|
|
||
| @Test | ||
| void test_no_issues_for_older_versions() { | ||
| createCheckVerifier() | ||
| .withJavaVersion(7) | ||
| .verifyNoIssues(); | ||
| } | ||
|
|
||
| @Test | ||
| void test_without_semantic() { | ||
| CheckVerifier.newVerifier() | ||
| .onFile(mainCodeSourcesPath("checks/CollectionsSortCheckSample.java")) | ||
| .withCheck(new CollectionsSortCheck()) | ||
| .withJavaVersion(8) | ||
| .withoutSemantic() | ||
| .verifyIssues(); | ||
| } | ||
|
|
||
| private static CheckVerifier createCheckVerifier() { | ||
| return CheckVerifier.newVerifier() | ||
| .onFile(mainCodeSourcesPath("checks/CollectionsSortCheckSample.java")) | ||
| .withCheck(new CollectionsSortCheck()); | ||
| } | ||
| } |
48 changes: 48 additions & 0 deletions
48
sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9412.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| <p>This rule raises an issue when a standalone utility function for sorting is called on a collection object in language versions where the collection | ||
| type provides its own instance method for sorting.</p> | ||
| <p>In Java, this specifically refers to calling <code>Collections.sort()</code> on a <code>List</code> object in Java 8 or later, where | ||
| <code>List.sort()</code> should be used instead.</p> | ||
| <h2>Why is this an issue?</h2> | ||
| <p>In earlier versions of the language, sorting a collection required using a static utility method from a helper class. This approach followed the | ||
| pattern of having utility functions in separate classes for operations on data structures.</p> | ||
| <p>In later versions, the collection interface itself gained its own sorting method. This change embraced a more object-oriented design where the | ||
| collection can be sorted directly on the instance.</p> | ||
| <p>Using the static utility sorting function in modern code is outdated for several reasons:</p> | ||
| <ul> | ||
| <li><strong>Readability</strong>: <code>myCollection.sort(comparator)</code> reads more naturally than <code>UtilityClass.sort(myCollection, | ||
| comparator)</code>. The object performing the action comes first, making the code flow clearer.</li> | ||
| <li><strong>API design</strong>: The newer approach aligns with modern design patterns that favor adding methods directly to interfaces, reducing | ||
| the need for separate utility classes.</li> | ||
| <li><strong>Consistency</strong>: Using the instance method makes the code consistent with other modern APIs in the language.</li> | ||
| </ul> | ||
| <p>While the static utility sorting function still works correctly and has the same behavior, using it signals that the code hasn’t been updated to | ||
| take advantage of newer language features.</p> | ||
| <p>In Java specifically, this refers to using <code>Collections.sort()</code> versus the <code>List.sort()</code> method that was introduced in Java | ||
| 8.</p> | ||
| <h3>What is the potential impact?</h3> | ||
| <p>Using a utility class method to sort a collection instead of the collection’s own sorting method has minimal practical impact on the application’s | ||
| behavior, as both methods produce the same result. However, it affects code maintainability:</p> | ||
| <ul> | ||
| <li>The code appears outdated, suggesting it may not be taking advantage of other modern language features.</li> | ||
| <li>New team members familiar with modern language practices may find the code less intuitive.</li> | ||
| <li>The codebase becomes inconsistent if some parts use the collection’s instance method while others use the utility class method.</li> | ||
| </ul> | ||
| <h2>How to fix it</h2> | ||
| <p>Replace <code>Collections.sort(myList)</code> with <code>myList.sort(null)</code>. When sorting with the natural order, pass <code>null</code> as | ||
| the comparator to the <code>sort()</code> method.</p> | ||
| <h3>Code examples</h3> | ||
| <h4>Noncompliant code example</h4> | ||
| <pre data-diff-id="1" data-diff-type="noncompliant"> | ||
| Collections.sort(myList); // Noncompliant | ||
| </pre> | ||
| <h4>Compliant solution</h4> | ||
| <pre data-diff-id="1" data-diff-type="compliant"> | ||
| myList.sort(null); | ||
| </pre> | ||
| <h2>Resources</h2> | ||
| <h3>Documentation</h3> | ||
| <ul> | ||
| <li>Java SE Documentation - <a href="https://docs.oracle.com/javase/8/docs/api/java/util/List.html#sort-java.util.Comparator-">List (Java Platform | ||
| SE 8)</a></li> | ||
| </ul> | ||
|
|
25 changes: 25 additions & 0 deletions
25
sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9412.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| { | ||
| "title": "\"Collections.sort()\" should not be used", | ||
| "type": "CODE_SMELL", | ||
| "status": "ready", | ||
| "remediation": { | ||
| "func": "Constant\/Issue", | ||
| "constantCost": "5min" | ||
| }, | ||
| "tags": [ | ||
| "java8", | ||
| "obsolete", | ||
| "convention" | ||
| ], | ||
| "defaultSeverity": "Minor", | ||
| "ruleSpecification": "RSPEC-9412", | ||
| "sqKey": "S9412", | ||
| "scope": "All", | ||
| "quickfix": "targeted", | ||
| "code": { | ||
| "impacts": { | ||
| "MAINTAINABILITY": "LOW" | ||
| }, | ||
| "attribute": "CONVENTIONAL" | ||
| } | ||
| } |
Empty file.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Collections.sort(List<T>)enforcesT extends Comparable<? super T>at compile time, whilelist.sort(null)does not: if the element type later stops beingComparable, a compile error becomes a runtimeClassCastException.list.sort(Comparator.naturalOrder())keeps the type check (at the cost of an import in the quick fix). This follows the RSPEC's "How to fix it" section, so it may be better settled on SonarSource/rspec#8233.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kept
list.sort(null)for now: it matches the RSPEC's "How to fix it" section and avoids adding an import in the quick fix. I agree the compile-time check is worth keeping, so I'll raise it on SonarSource/rspec#8233. If the RSPEC moves toComparator.naturalOrder(), we'll update the quick fix to match.