Skip to content

Commit bc0f575

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-2937 Rule S7500: Unnecessary <dict/list/set> comprehension - rewrite using <dict/list/set>() (#284)
GitOrigin-RevId: 768bf94a77a026b1ab76c1b9abe5b5d7ddb58165
1 parent 87207ff commit bc0f575

7 files changed

Lines changed: 195 additions & 0 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
@@ -398,6 +398,7 @@ public Stream<Class<?>> getChecks() {
398398
UnencryptedEbsVolumeCheck.class,
399399
UnionTypeExpressionCheck.class,
400400
UnnecessaryListComprehensionArgumentCheck.class,
401+
UnnecessaryComprehensionCheck.class,
401402
UnnecessaryReversedCallCheck.class,
402403
UnnecessarySubscriptReversalCheck.class,
403404
UnquantifiedNonCapturingGroupCheck.class,
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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.Objects;
21+
import java.util.Optional;
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.ComprehensionExpression;
26+
import org.sonar.plugins.python.api.tree.ComprehensionFor;
27+
import org.sonar.plugins.python.api.tree.DictCompExpression;
28+
import org.sonar.plugins.python.api.tree.Name;
29+
import org.sonar.plugins.python.api.tree.Tree;
30+
import org.sonar.plugins.python.api.tree.Tuple;
31+
import org.sonar.python.tree.TreeUtils;
32+
33+
@Rule(key = "S7500")
34+
public class UnnecessaryComprehensionCheck extends PythonSubscriptionCheck {
35+
36+
@Override
37+
public void initialize(Context context) {
38+
context.registerSyntaxNodeConsumer(Tree.Kind.GENERATOR_EXPR, UnnecessaryComprehensionCheck::checkComprehensionExpression);
39+
context.registerSyntaxNodeConsumer(Tree.Kind.LIST_COMPREHENSION, UnnecessaryComprehensionCheck::checkComprehensionExpression);
40+
context.registerSyntaxNodeConsumer(Tree.Kind.SET_COMPREHENSION, UnnecessaryComprehensionCheck::checkComprehensionExpression);
41+
context.registerSyntaxNodeConsumer(Tree.Kind.DICT_COMPREHENSION, UnnecessaryComprehensionCheck::checkDictComprehensionExpression);
42+
}
43+
44+
private static void checkComprehensionExpression(SubscriptionContext ctx) {
45+
var comprehension = (ComprehensionExpression) ctx.syntaxNode();
46+
var valueExpression = comprehension.resultExpression();
47+
var loopExpression = Optional.of(comprehension)
48+
.map(ComprehensionExpression::comprehensionFor)
49+
.filter(comprehensionFor -> Objects.isNull(comprehensionFor.nestedClause()))
50+
.map(ComprehensionFor::loopExpression)
51+
.orElse(null);
52+
53+
if (valueExpression instanceof Name valueName
54+
&& loopExpression instanceof Name loopValueName
55+
&& valueName.name().equals(loopValueName.name())
56+
) {
57+
ctx.addIssue(comprehension, "Replace this comprehension with passing the iterable to the collection constructor call");
58+
}
59+
}
60+
61+
private static void checkDictComprehensionExpression(SubscriptionContext ctx) {
62+
var comprehension = (DictCompExpression) ctx.syntaxNode();
63+
var keyExpression = comprehension.keyExpression();
64+
var valueExpression = comprehension.valueExpression();
65+
var loopExpressions = Optional.of(comprehension)
66+
.map(DictCompExpression::comprehensionFor)
67+
.filter(comprehensionFor -> Objects.isNull(comprehensionFor.nestedClause()))
68+
.map(ComprehensionFor::loopExpression)
69+
.map(TreeUtils.toInstanceOfMapper(Tuple.class))
70+
.map(Tuple::elements)
71+
.orElseGet(List::of);
72+
73+
if (keyExpression instanceof Name keyName
74+
&& valueExpression instanceof Name valueName
75+
&& loopExpressions.size() == 2
76+
&& loopExpressions.get(0) instanceof Name loopKeyName
77+
&& loopExpressions.get(1) instanceof Name loopValueName
78+
&& keyName.name().equals(loopKeyName.name())
79+
&& valueName.name().equals(loopValueName.name())
80+
) {
81+
ctx.addIssue(comprehension, "Replace this comprehension with passing the iterable to the dict constructor call");
82+
}
83+
}
84+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<p>This rule raises an issue when a comprehension is used to only copy a collection. Instead, the respective constructor should be used.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>Python comprehensions are a concise way to create new collections while transforming or filtering elements. However, using a comprehension that
4+
copies elements from one collection to another without any transformation is less readable than using the respective constructor directly.</p>
5+
<p>Therefore, when a comprehension is only copying elements, use the appropriate constructor instead:</p>
6+
<ul>
7+
<li> Replace <code>[x for x in iterable]</code> with <code>list(iterable)</code> </li>
8+
<li> Replace <code>{x for x in iterable}</code> with <code>set(iterable)</code> </li>
9+
<li> Replace <code>{k: v for k, v in iterable.items()}</code> with <code>dict(iterable)</code> </li>
10+
</ul>
11+
<h2>How to fix it</h2>
12+
<p>Replace comprehensions that copy elements from one collection to another with the respective constructor.</p>
13+
<h3>Code examples</h3>
14+
<h4>Noncompliant code example</h4>
15+
<pre data-diff-id="1" data-diff-type="noncompliant">
16+
some_list = [1, 2, 3, 2]
17+
[x for x in some_list] # Noncompliant
18+
</pre>
19+
<h4>Compliant solution</h4>
20+
<pre data-diff-id="1" data-diff-type="compliant">
21+
some_list = [1, 2, 3, 2]
22+
list(some_list) # Compliant
23+
</pre>
24+
<h2>Resources</h2>
25+
<h3>Documentation</h3>
26+
<ul>
27+
<li> Python Documentation - <a href="https://docs.python.org/3/library/stdtypes.html#dict">dict()</a> </li>
28+
<li> Python Documentation - <a href="https://docs.python.org/3/library/stdtypes.html#list">list()</a> </li>
29+
<li> Python Documentation - <a href="https://docs.python.org/3/library/stdtypes.html#set">set()</a> </li>
30+
</ul>
31+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"title": "Comprehensions only used to copy should be replaced with the respective constructor calls",
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-7500",
12+
"sqKey": "S7500",
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
@@ -266,6 +266,7 @@
266266
"S7494",
267267
"S7498",
268268
"S7499",
269+
"S7500",
269270
"S7501",
270271
"S7502",
271272
"S7503",
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 UnnecessaryComprehensionCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/unnecessaryComprehension.py", new UnnecessaryComprehensionCheck());
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+
iterable_single = [1, 2, 3, 2]
4+
list_comp = [x for x in iterable_single] # Noncompliant
5+
set_comp = {x for x in iterable_single} # Noncompliant
6+
list_comp = list(x for x in iterable_single) # Noncompliant
7+
set_comp = set(x for x in iterable_single) # Noncompliant
8+
9+
iterable_pairs = [('a', 1), ('b', 2)]
10+
dict_comp = {k: v for k, v in iterable_pairs} # Noncompliant
11+
12+
def case2():
13+
iterable_single = [1, 2, 3, 2]
14+
list_comp = [y for x in iterable_single]
15+
tuple = tuple(x for x in iterable_single if not something(x))
16+
list_comp = [x + 1 for x in iterable_single]
17+
set_comp = {x + 1 for x in iterable_single}
18+
list_comp = list(x + 1 for x in iterable_single)
19+
set_comp = set(x + 1 for x in iterable_single)
20+
21+
iterable_pairs = [('a', 1), ('b', 2)]
22+
dict_comp = {k: v + 1 for k, v in iterable_pairs}
23+
dict_comp = {k: v for k, v.x in iterable_pairs}
24+
dict_comp = {k: v for k.x, v in iterable_pairs}
25+
dict_comp = {something(k): v for k, v in iterable_pairs}
26+
dict_comp = {k: v for k, v in iterable_pairs if something(k)}
27+
dict_comp = {k: v1 for k, v2 in iterable_pairs}
28+
dict_comp = {k1: v for k2, v in iterable_pairs}

0 commit comments

Comments
 (0)