Skip to content

Commit e126b7f

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-3013 Create QuickFix for Rule S7517 (#314)
GitOrigin-RevId: 85e9c524235197fb536f7c1e27508f59881c0d50
1 parent 3cd35fd commit e126b7f

File tree

2 files changed

+80
-4
lines changed

2 files changed

+80
-4
lines changed

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,26 @@
1616
*/
1717
package org.sonar.python.checks;
1818

19+
import java.util.Optional;
1920
import org.sonar.check.Rule;
2021
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2122
import org.sonar.plugins.python.api.SubscriptionContext;
23+
import org.sonar.plugins.python.api.quickfix.PythonQuickFix;
2224
import org.sonar.plugins.python.api.tree.ComprehensionFor;
25+
import org.sonar.plugins.python.api.tree.Expression;
2326
import org.sonar.plugins.python.api.tree.ForStatement;
2427
import org.sonar.plugins.python.api.tree.Tree;
2528
import org.sonar.plugins.python.api.tree.Tuple;
2629
import org.sonar.plugins.python.api.types.v2.TriBool;
30+
import org.sonar.python.quickfix.TextEditUtils;
31+
import org.sonar.python.tree.TreeUtils;
2732
import org.sonar.python.types.v2.TypeCheckBuilder;
2833

2934
@Rule(key = "S7517")
3035
public class LoopOverDictKeyValuesCheck extends PythonSubscriptionCheck {
31-
public static final String DICT_FQN = "dict";
32-
public static final String MESSAGE = "Use items to iterate over key-value pairs";
36+
private static final String DICT_FQN = "dict";
37+
private static final String MESSAGE = "Use items to iterate over key-value pairs";
38+
private static final String QUICK_FIX_MESSAGE = "Replace with items method call";
3339
private TypeCheckBuilder dictTypeCheck;
3440

3541

@@ -51,7 +57,9 @@ private void checkForStatement(SubscriptionContext ctx) {
5157
if (expressions.size() == 2
5258
&& testExpressions.size() == 1
5359
&& dictTypeCheck.check(testExpressions.get(0).typeV2()) == TriBool.TRUE) {
54-
ctx.addIssue(testExpressions.get(0), MESSAGE);
60+
var dict = testExpressions.get(0);
61+
var issue = ctx.addIssue(dict, MESSAGE);
62+
createQuickFix(dict).ifPresent(issue::addQuickFix);
5563
}
5664
}
5765

@@ -60,7 +68,17 @@ private void checkComprehensionFor(SubscriptionContext ctx) {
6068
if (comprehensionFor.loopExpression() instanceof Tuple tuple
6169
&& tuple.elements().size() == 2
6270
&& dictTypeCheck.check(comprehensionFor.iterable().typeV2()) == TriBool.TRUE) {
63-
ctx.addIssue(comprehensionFor.iterable(), MESSAGE);
71+
var dict = comprehensionFor.iterable();
72+
var issue = ctx.addIssue(dict, MESSAGE);
73+
createQuickFix(dict).ifPresent(issue::addQuickFix);
6474
}
6575
}
76+
77+
private static Optional<PythonQuickFix> createQuickFix(Expression dict) {
78+
return Optional.ofNullable(TreeUtils.treeToString(dict, false))
79+
.map("%s.items()"::formatted)
80+
.map(replacementText -> TextEditUtils.replace(dict, replacementText))
81+
.map(textEdit -> PythonQuickFix.newQuickFix(QUICK_FIX_MESSAGE).addTextEdit(textEdit))
82+
.map(PythonQuickFix.Builder::build);
83+
}
6684
}

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

Lines changed: 58 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 LoopOverDictKeyValuesCheckTest {
@@ -26,4 +31,57 @@ void test() {
2631
PythonCheckVerifier.verify("src/test/resources/checks/loopOverDictKeyValues.py", new LoopOverDictKeyValuesCheck());
2732
}
2833

34+
@ParameterizedTest
35+
@MethodSource("quickFixTestCases")
36+
void quickFixTest(String before, String after, String expectedMessage) {
37+
var check = new LoopOverDictKeyValuesCheck();
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 LoopOverDictKeyValuesCheck(), before);
46+
}
47+
48+
static Stream<Arguments> quickFixTestCases() {
49+
return Stream.of(
50+
Arguments.of(
51+
"""
52+
some_dict = { "a": "b"}
53+
for k, v in some_dict:
54+
...
55+
""",
56+
"""
57+
some_dict = { "a": "b"}
58+
for k, v in some_dict.items():
59+
...
60+
""",
61+
"Replace with items method call"),
62+
Arguments.of(
63+
"""
64+
some_dict = { "hi": "hello"}
65+
{k: v for k, v in some_dict}
66+
""",
67+
"""
68+
some_dict = { "hi": "hello"}
69+
{k: v for k, v in some_dict.items()}
70+
""",
71+
"Replace with items method call")
72+
);
73+
}
74+
75+
static Stream<Arguments> noQuickFixTestCases() {
76+
return Stream.of(
77+
Arguments.of(
78+
"""
79+
some_dict = {"hi": "hello"}
80+
a = {k: v for k, v in {
81+
"hi": "hello"
82+
}}
83+
""")
84+
);
85+
}
86+
2987
}

0 commit comments

Comments
 (0)