Skip to content

Commit bbf6d05

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-2992 Create QuickFix for Rule S7500 (#312)
GitOrigin-RevId: 7f7032c229f2604e83f1a31781816482294b0f57
1 parent fcf9f0f commit bbf6d05

File tree

2 files changed

+93
-2
lines changed

2 files changed

+93
-2
lines changed

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,32 @@
1717
package org.sonar.python.checks;
1818

1919
import java.util.List;
20+
import java.util.Map;
2021
import java.util.Objects;
2122
import java.util.Optional;
2223
import org.sonar.check.Rule;
2324
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2425
import org.sonar.plugins.python.api.SubscriptionContext;
26+
import org.sonar.plugins.python.api.quickfix.PythonQuickFix;
2527
import org.sonar.plugins.python.api.tree.ComprehensionExpression;
2628
import org.sonar.plugins.python.api.tree.ComprehensionFor;
2729
import org.sonar.plugins.python.api.tree.DictCompExpression;
2830
import org.sonar.plugins.python.api.tree.Name;
2931
import org.sonar.plugins.python.api.tree.Tree;
3032
import org.sonar.plugins.python.api.tree.Tuple;
33+
import org.sonar.python.quickfix.TextEditUtils;
3134
import org.sonar.python.tree.TreeUtils;
3235

3336
@Rule(key = "S7500")
3437
public class UnnecessaryComprehensionCheck extends PythonSubscriptionCheck {
3538

39+
private static final Map<Tree.Kind, String> COMPREHENSION_TO_QUICK_FIX_FORMAT_MAPPING = Map.of(
40+
Tree.Kind.GENERATOR_EXPR, "%s",
41+
Tree.Kind.LIST_COMPREHENSION, "list(%s)",
42+
Tree.Kind.SET_COMPREHENSION, "set(%s)",
43+
Tree.Kind.DICT_COMPREHENSION, "dict(%s)"
44+
);
45+
3646
@Override
3747
public void initialize(Context context) {
3848
context.registerSyntaxNodeConsumer(Tree.Kind.GENERATOR_EXPR, UnnecessaryComprehensionCheck::checkComprehensionExpression);
@@ -54,7 +64,8 @@ private static void checkComprehensionExpression(SubscriptionContext ctx) {
5464
&& loopExpression instanceof Name loopValueName
5565
&& valueName.name().equals(loopValueName.name())
5666
) {
57-
ctx.addIssue(comprehension, "Replace this comprehension with passing the iterable to the collection constructor call");
67+
var issue = ctx.addIssue(comprehension, "Replace this comprehension with passing the iterable to the collection constructor call");
68+
createQuickFix(comprehension, comprehension.comprehensionFor()).ifPresent(issue::addQuickFix);
5869
}
5970
}
6071

@@ -78,7 +89,17 @@ private static void checkDictComprehensionExpression(SubscriptionContext ctx) {
7889
&& keyName.name().equals(loopKeyName.name())
7990
&& valueName.name().equals(loopValueName.name())
8091
) {
81-
ctx.addIssue(comprehension, "Replace this comprehension with passing the iterable to the dict constructor call");
92+
var issue = ctx.addIssue(comprehension, "Replace this comprehension with passing the iterable to the dict constructor call");
93+
createQuickFix(comprehension, comprehension.comprehensionFor()).ifPresent(issue::addQuickFix);
8294
}
8395
}
96+
97+
private static Optional<PythonQuickFix> createQuickFix(Tree comprehension, ComprehensionFor comprehensionFor) {
98+
return Optional.ofNullable(TreeUtils.treeToString(comprehensionFor.iterable(), false))
99+
.flatMap(iterableString -> Optional.ofNullable(COMPREHENSION_TO_QUICK_FIX_FORMAT_MAPPING.getOrDefault(comprehension.getKind(), null))
100+
.map(format -> format.formatted(iterableString))
101+
.map(replacementText -> TextEditUtils.replace(comprehension, replacementText))
102+
.map(textEdit -> PythonQuickFix.newQuickFix("Replace with collection constructor call").addTextEdit(textEdit))
103+
.map(PythonQuickFix.Builder::build));
104+
}
84105
}

python-checks/src/test/java/org/sonar/python/checks/UnnecessaryComprehensionCheckTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
*/
1717
package org.sonar.python.checks;
1818

19+
import java.util.stream.Stream;
1920
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.Arguments;
23+
import org.junit.jupiter.params.provider.MethodSource;
24+
import org.sonar.python.checks.quickfix.PythonQuickFixVerifier;
2025
import org.sonar.python.checks.utils.PythonCheckVerifier;
2126

2227
class UnnecessaryComprehensionCheckTest {
@@ -26,4 +31,69 @@ void test() {
2631
PythonCheckVerifier.verify("src/test/resources/checks/unnecessaryComprehension.py", new UnnecessaryComprehensionCheck());
2732
}
2833

34+
@ParameterizedTest
35+
@MethodSource("quickFixTestCases")
36+
void quickFixTest(String before, String after, String expectedMessage) {
37+
var check = new UnnecessaryComprehensionCheck();
38+
PythonQuickFixVerifier.verify(check, before, after);
39+
PythonQuickFixVerifier.verifyQuickFixMessages(check, before, expectedMessage);
40+
}
41+
42+
@ParameterizedTest
43+
@MethodSource("noQuickFixTestCases")
44+
void noQuickFixTest(String before) {
45+
PythonQuickFixVerifier.verifyNoQuickFixes(new UnnecessaryComprehensionCheck(), before);
46+
}
47+
48+
static Stream<Arguments> quickFixTestCases() {
49+
return Stream.of(
50+
Arguments.of(
51+
"""
52+
[x for x in some_iterable]
53+
""",
54+
"""
55+
list(some_iterable)
56+
""",
57+
"Replace with collection constructor call"),
58+
Arguments.of(
59+
"""
60+
{x for x in some_iterable}
61+
""",
62+
"""
63+
set(some_iterable)
64+
""",
65+
"Replace with collection constructor call"),
66+
Arguments.of(
67+
"""
68+
iterable_pairs = [('a', 1), ('b', 2)]
69+
{k: v for k, v in iterable_pairs}
70+
""",
71+
"""
72+
iterable_pairs = [('a', 1), ('b', 2)]
73+
dict(iterable_pairs)
74+
""",
75+
"Replace with collection constructor call"),
76+
Arguments.of(
77+
"""
78+
list(x for x in some_iterable)
79+
""",
80+
"""
81+
list(some_iterable)
82+
""",
83+
"Replace with collection constructor call")
84+
);
85+
}
86+
87+
static Stream<Arguments> noQuickFixTestCases() {
88+
return Stream.of(
89+
Arguments.of(
90+
"""
91+
iterable_pairs = [('a', 1), ('b', 2)]
92+
dict_comp = {k: v for k, v in [
93+
('a', 1),
94+
('b', 2)
95+
]}
96+
""")
97+
);
98+
}
2999
}

0 commit comments

Comments
 (0)