Skip to content

Commit 6c38d33

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-2964 Rule S7511: Unnecessary subscript reversal of iterable within <reversed/set/sorted>() (#269)
GitOrigin-RevId: baf7e5b39a48e9f59c1bb8eaa0ac7864879b2f46
1 parent 06dd76d commit 6c38d33

7 files changed

Lines changed: 243 additions & 2 deletions

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
@@ -391,6 +391,7 @@ public Stream<Class<?>> getChecks() {
391391
UnencryptedEbsVolumeCheck.class,
392392
UnionTypeExpressionCheck.class,
393393
UnnecessaryReversedCallCheck.class,
394+
UnnecessarySubscriptReversalCheck.class,
394395
UnquantifiedNonCapturingGroupCheck.class,
395396
UnreachableExceptCheck.class,
396397
UnreadPrivateAttributesCheck.class,
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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 java.util.Optional;
20+
import java.util.stream.Stream;
21+
import javax.annotation.Nullable;
22+
import org.sonar.check.Rule;
23+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
24+
import org.sonar.plugins.python.api.SubscriptionContext;
25+
import org.sonar.plugins.python.api.tree.CallExpression;
26+
import org.sonar.plugins.python.api.tree.Expression;
27+
import org.sonar.plugins.python.api.tree.Name;
28+
import org.sonar.plugins.python.api.tree.NumericLiteral;
29+
import org.sonar.plugins.python.api.tree.RegularArgument;
30+
import org.sonar.plugins.python.api.tree.SliceExpression;
31+
import org.sonar.plugins.python.api.tree.SliceItem;
32+
import org.sonar.plugins.python.api.tree.Tree;
33+
import org.sonar.plugins.python.api.tree.UnaryExpression;
34+
import org.sonar.plugins.python.api.types.v2.TriBool;
35+
import org.sonar.python.checks.utils.Expressions;
36+
import org.sonar.python.tree.TreeUtils;
37+
import org.sonar.python.types.v2.TypeCheckBuilder;
38+
39+
@Rule(key = "S7511")
40+
public class UnnecessarySubscriptReversalCheck extends PythonSubscriptionCheck {
41+
private TypeCheckBuilder isReversedTypeCheck;
42+
private TypeCheckBuilder isSortedTypeCheck;
43+
private TypeCheckBuilder isSetTypeCheck;
44+
45+
@Override
46+
public void initialize(Context context) {
47+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::initChecks);
48+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::check);
49+
}
50+
51+
private void initChecks(SubscriptionContext ctx) {
52+
isReversedTypeCheck = ctx.typeChecker().typeCheckBuilder().isTypeWithName("reversed");
53+
isSortedTypeCheck = ctx.typeChecker().typeCheckBuilder().isTypeWithName("sorted");
54+
isSetTypeCheck = ctx.typeChecker().typeCheckBuilder().isTypeWithName("set");
55+
}
56+
57+
private void check(SubscriptionContext ctx) {
58+
TreeUtils.toOptionalInstanceOf(CallExpression.class, ctx.syntaxNode())
59+
.filter(this::isSensitiveCall)
60+
.flatMap(callExpression -> getArgumentSubscriptReversal(callExpression)
61+
.or(() -> getOuterSubscriptReversal(callExpression)))
62+
.ifPresent(redundantSubscriptReversal -> ctx.addIssue(redundantSubscriptReversal, "Remove this redundant subscript reversal"));
63+
}
64+
65+
private static Optional<Tree> getOuterSubscriptReversal(CallExpression callExpression) {
66+
return Optional.of(callExpression)
67+
.map(Tree::parent)
68+
.filter(UnnecessarySubscriptReversalCheck::isSubscriptReversal);
69+
}
70+
71+
private static Optional<Tree> getArgumentSubscriptReversal(CallExpression callExpression) {
72+
return TreeUtils.nthArgumentOrKeywordOptional(0, "", callExpression.arguments())
73+
.map(RegularArgument::expression)
74+
.filter(arg -> isSubscriptReversal(arg) || isAssignedToSubscriptReversal(arg))
75+
.map(Tree.class::cast);
76+
}
77+
78+
private static boolean isAssignedToSubscriptReversal(Expression argumentExpression) {
79+
return argumentExpression instanceof Name name
80+
&& getUsageCount(name) == 2
81+
&& isSubscriptReversal(Expressions.singleAssignedValue(name));
82+
}
83+
84+
private boolean isSensitiveCall(CallExpression callExpression) {
85+
return Stream.of(isReversedTypeCheck, isSortedTypeCheck, isSetTypeCheck)
86+
.map(check -> check.check(callExpression.callee().typeV2()))
87+
.anyMatch(TriBool.TRUE::equals);
88+
}
89+
90+
private static boolean isSubscriptReversal(@Nullable Tree expression) {
91+
return expression instanceof SliceExpression sliceExpression
92+
&& sliceExpression.sliceList().slices().size() == 1
93+
&& sliceExpression.sliceList().slices().get(0) instanceof SliceItem sliceItem
94+
&& sliceItem.lowerBound() == null
95+
&& sliceItem.upperBound() == null
96+
&& sliceItem.stride() instanceof UnaryExpression unaryExpression
97+
&& unaryExpression.is(Tree.Kind.UNARY_MINUS)
98+
&& unaryExpression.expression() instanceof NumericLiteral numericLiteral
99+
&& numericLiteral.valueAsLong() == 1;
100+
}
101+
102+
private static int getUsageCount(Name name) {
103+
var symbol = name.symbolV2();
104+
if (symbol == null) return 0;
105+
return symbol.usages().size();
106+
}
107+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<p>This rule raises an issue when an iterable is reversed using slicing, like <code>[::-1]</code>, before being passed to <code>set()</code>,
2+
<code>sorted()</code>, or <code>reversed()</code>.</p>
3+
<h2>Why is this an issue?</h2>
4+
<p>Reversing an iterable using slicing, likle <code>[::-1]</code>, before passing it to <code>set()</code>, <code>sorted()</code>, or
5+
<code>reversed()</code> is unnecessary and inefficient. The slicing operation creates a new copy of the iterable in reverse order, which is not needed
6+
for the following reasons:</p>
7+
<ul>
8+
<li> <code>set()</code>: The order of elements in a set is inherently undefined, so reversing the iterable before creating the set has no effect on
9+
the final set, and it introduces unnecessary computation. </li>
10+
<li> <code>sorted()</code>: The <code>sorted()</code> function has a <code>reverse</code> parameter that provides a more efficient way to sort in
11+
descending order. Using slicing to reverse the result of <code>sorted()</code> is less efficient and less readable. </li>
12+
<li> <code>reversed()</code>: Applying <code>reversed()</code> twice on the same iterable effectively returns the original iterable, if it supports
13+
direct iteration. If the iterable is a one-time iterator, then you will need to create an iterator from the original iterable using
14+
<code>iter(iterable)</code>. Using slicing adds unnecessary overhead. </li>
15+
</ul>
16+
<h2>How to fix it</h2>
17+
<p>To fix these issues remove the redundant slicing operation: * <code>set(iterable[::-1])</code>: Replace with <code>set(iterable)</code>. *
18+
<code>sorted(iterable)[::-1]</code>: Replace with <code>sorted(iterable, reverse=True)</code>. * <code>reversed(iterable[::-1])</code>: Replace with
19+
<code>iterable</code> if it supports direct iteration, or <code>iter(iterable)</code> if it is a one-time iterator.</p>
20+
<h3>Code examples</h3>
21+
<h4>Noncompliant code example</h4>
22+
<pre data-diff-id="1" data-diff-type="noncompliant">
23+
iterable = [1, 3, 2]
24+
result = set(iterable[::-1]) # Noncompliant
25+
</pre>
26+
<h4>Compliant solution</h4>
27+
<pre data-diff-id="1" data-diff-type="compliant">
28+
iterable = [1, 3, 2]
29+
result = set(iterable)
30+
</pre>
31+
<h2>Resources</h2>
32+
<h3>Documentation</h3>
33+
<ul>
34+
<li> Python Documentation - <a href="https://docs.python.org/3/library/functions.html#sorted">sorted</a> </li>
35+
<li> Python Documentation - <a href="https://docs.python.org/3/library/functions.html#reversed">reversed</a> </li>
36+
<li> Python Documentation - <a href="https://docs.python.org/3/library/functions.html#func-set">set</a> </li>
37+
</ul>
38+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"title": "Passing a reversed iterable to \"set()\", \"sorted()\", or \"reversed()\" should be avoided",
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-7511",
12+
"sqKey": "S7511",
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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@
255255
"S6984",
256256
"S6985",
257257
"S7483",
258+
"S7486",
258259
"S7488",
259260
"S7491",
260261
"S7493",
@@ -263,7 +264,6 @@
263264
"S7499",
264265
"S7501",
265266
"S7510",
266-
"S7486",
267-
"S7501"
267+
"S7511"
268268
]
269269
}
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 UnnecessarySubscriptReversalCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/unnecessarySubscriptReversal.py", new UnnecessarySubscriptReversalCheck());
27+
}
28+
29+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
2+
def case1():
3+
data = [3, 1, 4, 1, 5, 9]
4+
reversed(data[::-1]) # Noncompliant
5+
sorted(data[::-1]) # Noncompliant
6+
set(data[::-1]) # Noncompliant
7+
reversed(data[1::-1])
8+
sorted(data[:1:-1])
9+
set(data[1:1:-1])
10+
set(data[::])
11+
reversed(data)
12+
sorted(data)
13+
set(data)
14+
15+
def case2():
16+
data = [3, 1, 4, 1, 5, 9]
17+
reversed_data = data[::-1]
18+
sorted(reversed_data) # Noncompliant
19+
sorted(data[:3,3:])
20+
reversed_modified_data = data[::-1]
21+
reversed_modified_data.append(10)
22+
sorted(reversed_modified_data)
23+
some_data = data
24+
sorted(some_data)
25+
26+
def case3():
27+
data = [3, 1, 4, 1, 5, 9]
28+
reversed(data)[::-1] # Noncompliant
29+
sorted(data)[::-1] # Noncompliant
30+
set(data)[::-1] # Noncompliant
31+
32+
def case4():
33+
data = [3, 1, 4, 1, 5, 9]
34+
set(data[::+1])
35+
set(data[::-2])
36+
set(data[::1])
37+
set(data[::-b])
38+
list(data[::-1])
39+
40+
def case5():
41+
data = [3, 1, 4, 1, 5, 9]
42+
reversed()
43+
sorted(data)[1]
44+
sorted(reverse=True)
45+
set()

0 commit comments

Comments
 (0)