Skip to content

Commit a8f3343

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-2963 Rule S7508: Unnecessary <list/reversed/set/sorted/tuple> call within <list/set/sorted/tuple>() (#289)
GitOrigin-RevId: 7a1efd44260b165eef9807e14ac23b4fc83210ea
1 parent be557bd commit a8f3343

File tree

7 files changed

+242
-0
lines changed

7 files changed

+242
-0
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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.List;
20+
import java.util.Map;
21+
import java.util.Optional;
22+
import java.util.Set;
23+
import java.util.stream.Collectors;
24+
import javax.annotation.Nullable;
25+
import org.sonar.check.Rule;
26+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
27+
import org.sonar.plugins.python.api.SubscriptionContext;
28+
import org.sonar.plugins.python.api.tree.CallExpression;
29+
import org.sonar.plugins.python.api.tree.Expression;
30+
import org.sonar.plugins.python.api.tree.Name;
31+
import org.sonar.plugins.python.api.tree.RegularArgument;
32+
import org.sonar.plugins.python.api.tree.Tree;
33+
import org.sonar.plugins.python.api.types.v2.TriBool;
34+
import org.sonar.python.checks.utils.Expressions;
35+
import org.sonar.python.semantic.v2.SymbolV2;
36+
import org.sonar.python.tree.TreeUtils;
37+
import org.sonar.python.types.v2.TypeCheckBuilder;
38+
import org.sonar.python.types.v2.TypeCheckMap;
39+
40+
@Rule(key = "S7508")
41+
public class NestedCollectionsCreationCheck extends PythonSubscriptionCheck {
42+
public static final String LIST_FQN = "list";
43+
public static final String SORTED_FQN = "sorted";
44+
public static final String SET_FQN = "set";
45+
public static final String TUPLE_FQN = "tuple";
46+
public static final String REVERSED_FQN = "reversed";
47+
48+
private static final Map<String, Set<String>> SENSITIVE_NESTED_CALL_COMBINATIONS = Map.ofEntries(
49+
Map.entry(LIST_FQN, Set.of(LIST_FQN, TUPLE_FQN, SORTED_FQN)),
50+
Map.entry(SET_FQN, Set.of(LIST_FQN, SET_FQN, TUPLE_FQN, REVERSED_FQN, SORTED_FQN)),
51+
Map.entry(SORTED_FQN, Set.of(LIST_FQN, TUPLE_FQN, SORTED_FQN)),
52+
Map.entry(TUPLE_FQN, Set.of(LIST_FQN, TUPLE_FQN))
53+
);
54+
private TypeCheckMap<Set<TypeCheckBuilder>> sensitiveCallCombinationChecks;
55+
56+
57+
@Override
58+
public void initialize(Context context) {
59+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::initChecks);
60+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::check);
61+
}
62+
63+
private void initChecks(SubscriptionContext ctx) {
64+
sensitiveCallCombinationChecks = new TypeCheckMap<>();
65+
SENSITIVE_NESTED_CALL_COMBINATIONS.forEach((outerCallFqn, innerCallFqns) -> {
66+
var outerCallTypeCheck = ctx.typeChecker().typeCheckBuilder().isTypeWithName(outerCallFqn);
67+
var innerCallTypeChecks = innerCallFqns.stream()
68+
.map(sensitiveMethodFqn -> ctx.typeChecker().typeCheckBuilder().isTypeWithName(sensitiveMethodFqn))
69+
.collect(Collectors.toSet());
70+
sensitiveCallCombinationChecks.put(outerCallTypeCheck, innerCallTypeChecks);
71+
});
72+
}
73+
74+
private void check(SubscriptionContext ctx) {
75+
var callExpression = (CallExpression) ctx.syntaxNode();
76+
sensitiveCallCombinationChecks.getOptionalForType(callExpression.callee().typeV2())
77+
.ifPresent(nestedCallTypeChecks -> TreeUtils.nthArgumentOrKeywordOptional(0, "", callExpression.arguments())
78+
.map(RegularArgument::expression)
79+
.ifPresent(argumentExpression -> {
80+
if (isSensitiveMethodCall(argumentExpression, nestedCallTypeChecks) || isAssignedToSensitiveMethodCall(argumentExpression, nestedCallTypeChecks)) {
81+
ctx.addIssue(callExpression, "Remove this redundant call.");
82+
}
83+
}));
84+
}
85+
86+
private static boolean isSensitiveMethodCall(@Nullable Expression expression, Set<TypeCheckBuilder> sensitiveMethodsTypeChecks) {
87+
return expression instanceof CallExpression callExpression && sensitiveMethodsTypeChecks.stream()
88+
.map(check -> check.check(callExpression.callee().typeV2()))
89+
.anyMatch(TriBool.TRUE::equals);
90+
}
91+
92+
private static boolean isAssignedToSensitiveMethodCall(Expression argumentExpression, Set<TypeCheckBuilder> sensitiveMethodsTypeChecks) {
93+
return argumentExpression instanceof Name name
94+
&& getUsageCount(name) == 2
95+
&& isSensitiveMethodCall(Expressions.singleAssignedValue(name), sensitiveMethodsTypeChecks);
96+
}
97+
98+
private static int getUsageCount(Name name) {
99+
return Optional.ofNullable(name.symbolV2())
100+
.map(SymbolV2::usages)
101+
.map(List::size)
102+
.orElse(0);
103+
}
104+
}

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
@@ -279,6 +279,7 @@ public Stream<Class<?>> getChecks() {
279279
ModuleNameCheck.class,
280280
MultipleWhitespaceCheck.class,
281281
NeedlessPassCheck.class,
282+
NestedCollectionsCreationCheck.class,
282283
NestedConditionalExpressionCheck.class,
283284
NestedControlFlowDepthCheck.class,
284285
NewStyleClassCheck.class,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<p>This rule raises an issue when the functions <code>list()</code>, <code>tuple()</code>, <code>set()</code>, <code>sorted()</code>, or
2+
<code>reversed()</code> are unnecessarily wrapped around each other’s return values or used to convert values that don’t require conversion.</p>
3+
<h2>Why is this an issue?</h2>
4+
<p>Python’s built-in functions for processing iterables such as <code>list()</code>, <code>tuple()</code>, <code>set()</code>, <code>sorted()</code>,
5+
and <code>reversed()</code> are designed to accept any iterable as input. When these functions are unnecessarily nested within each other, it creates
6+
redundant operations that add unnecessary computational overhead by creating intermediate data structures, decrease code readability and make the
7+
intention less clear, and waste memory by duplicating data structures temporarily.</p>
8+
<h2>How to fix it</h2>
9+
<p>When the outer function is given a collection but could have been given an iterable, the unnecessary conversion should be removed. For example, in
10+
<code>sorted(list(iterable))</code>, the outer <code>sorted()</code> function can accept an iterable directly, so the inner <code>list()</code> call
11+
is redundant and should be removed.</p>
12+
<p>When the function <code>sorted()</code> is wrapped with <code>list()</code>, remove this conversion operation, since <code>sorted()</code> already
13+
returns a list.</p>
14+
<h3>Code examples</h3>
15+
<h4>Noncompliant code example</h4>
16+
<pre data-diff-id="1" data-diff-type="noncompliant">
17+
iterable = (3, 1, 4, 1)
18+
19+
sorted_of_list = list(sorted(iterable)) # Noncompliant
20+
</pre>
21+
<h4>Compliant solution</h4>
22+
<pre data-diff-id="1" data-diff-type="compliant">
23+
iterable = (3, 1, 4, 1)
24+
25+
sorted_of_list = sorted(iterable)
26+
</pre>
27+
<h2>Resources</h2>
28+
<h3>Documentation</h3>
29+
<ul>
30+
<li> Python Documentation - <a href="https://docs.python.org/3/library/stdtypes.html#list">list</a> </li>
31+
<li> Python Documentation - <a href="https://docs.python.org/3/library/stdtypes.html#tuple">tuple</a> </li>
32+
<li> Python Documentation - <a href="https://docs.python.org/3/library/stdtypes.html#set">set</a> </li>
33+
<li> Python Documentation - <a href="https://docs.python.org/3/library/functions.html#sorted">sorted</a> </li>
34+
<li> Python Documentation - <a href="https://docs.python.org/3/library/functions.html#reversed">reversed</a> </li>
35+
</ul>
36+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"title": "Redundant collection functions should be avoided",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [],
10+
"defaultSeverity": "Minor",
11+
"ruleSpecification": "RSPEC-7508",
12+
"sqKey": "S7508",
13+
"scope": "All",
14+
"quickfix": "targeted",
15+
"code": {
16+
"impacts": {
17+
"MAINTAINABILITY": "LOW"
18+
},
19+
"attribute": "CONVENTIONAL"
20+
}
21+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@
277277
"S7505",
278278
"S7506",
279279
"S7507",
280+
"S7508",
280281
"S7510",
281282
"S7511",
282283
"S7512"
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 NestedCollectionsCreationCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/nestedCollectionsCreation.py", new NestedCollectionsCreationCheck());
27+
}
28+
29+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
def case1():
2+
iterable = (3, 1, 4, 1)
3+
list(list(iterable)) # Noncompliant
4+
list(tuple(iterable)) # Noncompliant
5+
list(sorted(iterable)) # Noncompliant
6+
7+
def case2():
8+
iterable = (3, 1, 4, 1)
9+
set(list(iterable)) # Noncompliant
10+
set(set(iterable)) # Noncompliant
11+
set(tuple(iterable)) # Noncompliant
12+
set(reversed(iterable)) # Noncompliant
13+
set(sorted(iterable)) # Noncompliant
14+
15+
def case3():
16+
iterable = (3, 1, 4, 1)
17+
sorted(list(iterable)) # Noncompliant
18+
sorted(tuple(iterable)) # Noncompliant
19+
sorted(sorted(iterable)) # Noncompliant
20+
21+
def case4():
22+
iterable = (3, 1, 4, 1)
23+
tuple(list(iterable)) # Noncompliant
24+
tuple(tuple(iterable)) # Noncompliant
25+
26+
def case5():
27+
iterable = (3, 1, 4, 1)
28+
list_of_list = list(iterable)
29+
tuple_of_list = tuple(iterable)
30+
set_of_list = set(iterable)
31+
sorted_of_list = sorted(iterable)
32+
set_of_set = set(iterable)
33+
sorted_of_sorted = sorted(iterable)
34+
set_of_sorted = set(iterable)
35+
36+
def case6():
37+
iterable = (3, 1, 4, 1)
38+
single_usage_assigned_list = list(iterable)
39+
list_of_list = list(single_usage_assigned_list) # Noncompliant
40+
single_usage_assigned_not_sensitive = something_else(iterable)
41+
list_of_list = list(single_usage_assigned_not_sensitive)
42+
multiple_usage_assigned_list = list(iterable)
43+
multiple_usage_assigned_list.append(5)
44+
list_of_list = list(multiple_usage_assigned_list)
45+
46+
def case7():
47+
iterable = (3, 1, 4, 1)
48+
sorted(reversed(iterable)) # OK, it should be raided by S7510
49+
reversed(sorted(iterable)) # OK, it should be raided by S7510
50+
list(set(iterable))

0 commit comments

Comments
 (0)