Skip to content

Commit a3db92d

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-2956 Rule S7510: Unnecessary <reversed> call around sorted() (#259)
GitOrigin-RevId: 745efcfcfbe6cc9c83275302d7a97b1633b81293
1 parent a5c8cbd commit a3db92d

7 files changed

Lines changed: 186 additions & 1 deletion

File tree

python-checks/src/main/java/org/sonar/python/checks/OpenSourceCheckList.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ public Stream<Class<?>> getChecks() {
387387
UnencryptedSqsQueueCheck.class,
388388
UnencryptedEbsVolumeCheck.class,
389389
UnionTypeExpressionCheck.class,
390+
UnnecessaryReversedCallCheck.class,
390391
UnquantifiedNonCapturingGroupCheck.class,
391392
UnreachableExceptCheck.class,
392393
UnreadPrivateAttributesCheck.class,
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
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.python.checks;
18+
19+
import javax.annotation.Nullable;
20+
import org.sonar.check.Rule;
21+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
22+
import org.sonar.plugins.python.api.SubscriptionContext;
23+
import org.sonar.plugins.python.api.tree.CallExpression;
24+
import org.sonar.plugins.python.api.tree.Expression;
25+
import org.sonar.plugins.python.api.tree.Name;
26+
import org.sonar.plugins.python.api.tree.RegularArgument;
27+
import org.sonar.plugins.python.api.tree.Tree;
28+
import org.sonar.plugins.python.api.types.v2.TriBool;
29+
import org.sonar.python.checks.utils.Expressions;
30+
import org.sonar.python.tree.TreeUtils;
31+
import org.sonar.python.types.v2.TypeCheckBuilder;
32+
33+
@Rule(key = "S7510")
34+
public class UnnecessaryReversedCallCheck extends PythonSubscriptionCheck {
35+
private TypeCheckBuilder isReversedTypeCheck;
36+
private TypeCheckBuilder isSortedTypeCheck;
37+
38+
@Override
39+
public void initialize(Context context) {
40+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::initChecks);
41+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::check);
42+
}
43+
44+
private void initChecks(SubscriptionContext ctx) {
45+
isReversedTypeCheck = ctx.typeChecker().typeCheckBuilder().isTypeWithName("reversed");
46+
isSortedTypeCheck = ctx.typeChecker().typeCheckBuilder().isTypeWithName("sorted");
47+
}
48+
49+
private void check(SubscriptionContext ctx) {
50+
if (ctx.syntaxNode() instanceof CallExpression callExpression && isReversedCall(callExpression)) {
51+
TreeUtils.nthArgumentOrKeywordOptional(0, "", callExpression.arguments())
52+
.map(RegularArgument::expression)
53+
.ifPresent(argumentExpression -> {
54+
if (isSortedCall(argumentExpression) || isAssignedToSortedCall(argumentExpression)) {
55+
ctx.addIssue(callExpression, "Remove this redundant reversed call, use reverse argument of the sorted function call instead");
56+
}
57+
});
58+
}
59+
}
60+
61+
private boolean isAssignedToSortedCall(Expression argumentExpression) {
62+
return argumentExpression instanceof Name name
63+
&& getUsageCount(name) == 2
64+
&& isSortedCall(Expressions.singleAssignedValue(name));
65+
}
66+
67+
private boolean isReversedCall(CallExpression callExpression) {
68+
return isReversedTypeCheck.check(callExpression.callee().typeV2()) == TriBool.TRUE;
69+
}
70+
71+
private boolean isSortedCall(@Nullable Expression expression) {
72+
return expression instanceof CallExpression callExpression && isSortedTypeCheck.check(callExpression.callee().typeV2()) == TriBool.TRUE;
73+
}
74+
75+
private static int getUsageCount(Name name) {
76+
var symbol = name.symbolV2();
77+
if (symbol == null) return 0;
78+
return symbol.usages().size();
79+
}
80+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<p>This rule raises an issue when the <code>reversed()</code> function is called with a <code>sorted()</code> as an argument.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>The <code>sorted()</code> function has a <code>reverse</code> parameter that provides the same functionality as the <code>reversed()</code>
4+
function.</p>
5+
<h2>How to fix it</h2>
6+
<p>Use the <code>reverse</code> parameter of the <code>sorted()</code> function to sort in descending order instead of using
7+
<code>reversed()</code>.</p>
8+
<h3>Code examples</h3>
9+
<h4>Noncompliant code example</h4>
10+
<pre data-diff-id="1" data-diff-type="noncompliant">
11+
data = [3, 1, 4, 1, 5, 9]
12+
result = reversed(sorted(data)) # Noncompliant
13+
</pre>
14+
<h4>Compliant solution</h4>
15+
<pre data-diff-id="1" data-diff-type="compliant">
16+
data = [3, 1, 4, 1, 5, 9]
17+
result = sorted(data, reverse=True)
18+
</pre>
19+
<h2>Resources</h2>
20+
<h3>Documentation</h3>
21+
<ul>
22+
<li> Python reference documentation - <a href="https://docs.python.org/3/library/functions.html#sorted">sorted</a> </li>
23+
<li> Python reference documentation - <a href="https://docs.python.org/3/library/functions.html#reversed">reversed</a> </li>
24+
</ul>
25+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"title": "The \"sorted\" function call should not be passed to the \"reversed\" function as an argument",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "1min"
8+
},
9+
"tags": [],
10+
"defaultSeverity": "Major",
11+
"ruleSpecification": "RSPEC-7510",
12+
"sqKey": "S7510",
13+
"scope": "All",
14+
"quickfix": "unknown",
15+
"code": {
16+
"impacts": {
17+
"MAINTAINABILITY": "MEDIUM"
18+
},
19+
"attribute": "CONVENTIONAL"
20+
}
21+
}

python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@
259259
"S7491",
260260
"S7493",
261261
"S7499",
262-
"S7501"
262+
"S7501",
263+
"S7510"
263264
]
264265
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
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.python.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.python.checks.utils.PythonCheckVerifier;
21+
22+
class UnnecessaryReversedCallCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/unnecessaryReversedCall.py", new UnnecessaryReversedCallCheck());
27+
}
28+
29+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
def case1():
3+
data = [3, 1, 4, 1, 5, 9]
4+
result = reversed(data)
5+
6+
def case2():
7+
data = [3, 1, 4, 1, 5, 9]
8+
result = reversed(sorted(data)) # Noncompliant
9+
10+
def case3():
11+
data = [3, 1, 4, 1, 5, 9]
12+
sorted_data = sorted(data)
13+
result = reversed(sorted_data) # Noncompliant
14+
15+
def case4():
16+
data = [3, 1, 4, 1, 5, 9]
17+
modified = sorted(data)
18+
modified.append(3)
19+
result = reversed(modified)
20+
21+
def case5():
22+
data = [3, 1, 4, 1, 5, 9]
23+
result = reversed(list(data))
24+
25+
def case6():
26+
data = [3, 1, 4, 1, 5, 9]
27+
list_data = list(data)
28+
result = reversed(list_data)

0 commit comments

Comments
 (0)