Skip to content

Commit 9a53632

Browse files
SONARPY-1024: Add quick fixes for S5799 (ImplicitStringConcatenationCheck) (#1140)
1 parent 9ef6b4e commit 9a53632

File tree

8 files changed

+117
-22
lines changed

8 files changed

+117
-22
lines changed

python-checks/pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@
5353
<scope>test</scope>
5454
</dependency>
5555
<dependency>
56-
<groupId>org.sonarsource.analyzer-commons</groupId>
57-
<artifactId>sonar-analyzer-commons</artifactId>
58-
<scope>test</scope>
56+
<groupId>org.sonarsource.analyzer-commons</groupId>
57+
<artifactId>sonar-analyzer-commons</artifactId>
58+
<scope>test</scope>
5959
</dependency>
6060
<dependency>
6161
<groupId>org.mockito</groupId>

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
import org.sonar.plugins.python.api.tree.StringElement;
2828
import org.sonar.plugins.python.api.tree.StringLiteral;
2929
import org.sonar.plugins.python.api.tree.Tree;
30+
import org.sonar.python.quickfix.IssueWithQuickFix;
31+
import org.sonar.python.quickfix.PythonQuickFix;
32+
33+
import static org.sonar.python.quickfix.PythonTextEdit.insertAfter;
3034

3135
@Rule(key = "S5799")
3236
public class ImplicitStringConcatenationCheck extends PythonSubscriptionCheck {
@@ -58,17 +62,17 @@ private static void checkStringLiteral(StringLiteral stringLiteral, Subscription
5862
List<StringElement> stringElements = stringLiteral.stringElements();
5963
for (int i = 1; i < stringElements.size(); i++) {
6064
StringElement current = stringElements.get(i);
61-
StringElement previous = stringElements.get(i-1);
65+
StringElement previous = stringElements.get(i - 1);
6266
if (!current.prefix().equalsIgnoreCase(previous.prefix()) || !haveSameQuotes(current, previous)) {
6367
continue;
6468
}
6569
if (current.firstToken().line() == previous.firstToken().line()) {
66-
ctx.addIssue(previous.firstToken(), MESSAGE_SINGLE_LINE).secondary(current.firstToken(), null);
70+
createQuickFix(ctx.addIssue(previous.firstToken(), MESSAGE_SINGLE_LINE).secondary(current.firstToken(), null), previous);
6771
// Only raise 1 issue per string literal
6872
return;
6973
}
7074
if ((isWithinCollection(stringLiteral) && !isException(previous, current))) {
71-
ctx.addIssue(previous.firstToken(), MESSAGE_MULTIPLE_LINES).secondary(current.firstToken(), null);
75+
createQuickFix(ctx.addIssue(previous.firstToken(), MESSAGE_MULTIPLE_LINES).secondary(current.firstToken(), null), previous);
7276
return;
7377
}
7478
}
@@ -89,5 +93,28 @@ private static boolean haveSameQuotes(StringElement first, StringElement second)
8993
return first.isTripleQuoted() == second.isTripleQuoted() &&
9094
first.value().charAt(first.value().length() - 1) == second.value().charAt(second.value().length() - 1);
9195
}
92-
}
9396

97+
private static boolean isInFunctionOrArrayOrTupleOrExpression(StringElement token) {
98+
Tree t = token;
99+
while (t.parent().is(Tree.Kind.STRING_LITERAL)) {
100+
t = t.parent();
101+
}
102+
return t.parent().is(Tree.Kind.ARG_LIST, Tree.Kind.EXPRESSION_LIST, Tree.Kind.PARAMETER_LIST, Tree.Kind.TUPLE);
103+
}
104+
105+
private static void createQuickFix(PreciseIssue issueRaised, StringElement stringElement) {
106+
IssueWithQuickFix issue = (IssueWithQuickFix) issueRaised;
107+
108+
if (isInFunctionOrArrayOrTupleOrExpression(stringElement)) {
109+
PythonQuickFix quickFix = PythonQuickFix.newQuickFix("Add the comma between string or byte tokens.")
110+
.addTextEdit(insertAfter(stringElement, ","))
111+
.build();
112+
issue.addQuickFix(quickFix);
113+
}
114+
115+
PythonQuickFix quickFix = PythonQuickFix.newQuickFix("Make the addition sign between string or byte tokens explicit.")
116+
.addTextEdit(insertAfter(stringElement, "+"))
117+
.build();
118+
issue.addQuickFix(quickFix);
119+
}
120+
}

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,40 @@
2020
package org.sonar.python.checks;
2121

2222
import org.junit.Test;
23+
import org.sonar.plugins.python.api.PythonCheck;
24+
import org.sonar.python.checks.quickfix.PythonQuickFixVerifier;
2325
import org.sonar.python.checks.utils.PythonCheckVerifier;
2426

2527
public class ImplicitStringConcatenationCheckTest {
2628

29+
private static final PythonCheck check = new ImplicitStringConcatenationCheck();
30+
2731
@Test
2832
public void test() {
29-
PythonCheckVerifier.verify("src/test/resources/checks/implicitStringConcatenation.py", new ImplicitStringConcatenationCheck());
33+
PythonCheckVerifier.verify("src/test/resources/checks/implicitStringConcatenation.py", check);
34+
}
35+
36+
@Test
37+
public void simple_expression_quickfix() {
38+
String codeWithIssue = "a = '1' '2'";
39+
String codeFixed1 = "a = '1'+ '2'";
40+
PythonQuickFixVerifier.verify(check, codeWithIssue, codeFixed1);
41+
42+
codeWithIssue = "a = ['1' '2']";
43+
codeFixed1 = "a = ['1', '2']";
44+
String codeFixed2 = "a = ['1'+ '2']";
45+
PythonQuickFixVerifier.verify(check, codeWithIssue, codeFixed1, codeFixed2);
3046
}
47+
48+
@Test
49+
public void complex_expression_quickfix() {
50+
String codeWithIssue = "def a():\n" +
51+
" b = ['1' '2']\n";
52+
String codeFixed1 = "def a():\n" +
53+
" b = ['1', '2']\n";
54+
String codeFixed2 = "def a():\n" +
55+
" b = ['1'+ '2']\n";
56+
PythonQuickFixVerifier.verify(check, codeWithIssue, codeFixed1, codeFixed2);
57+
}
58+
3159
}

python-checks/src/test/java/org/sonar/python/checks/quickfix/PythonQuickFixVerifier.java

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121

2222
import com.sonar.sslr.api.AstNode;
2323
import java.net.URI;
24+
import java.util.Arrays;
2425
import java.util.Collections;
2526
import java.util.List;
27+
import java.util.stream.Collectors;
2628
import org.sonar.plugins.python.api.PythonCheck;
2729
import org.sonar.plugins.python.api.PythonCheck.PreciseIssue;
2830
import org.sonar.plugins.python.api.PythonFile;
@@ -32,6 +34,7 @@
3234
import org.sonar.python.SubscriptionVisitor;
3335
import org.sonar.python.parser.PythonParser;
3436
import org.sonar.python.quickfix.IssueWithQuickFix;
37+
import org.sonar.python.quickfix.PythonQuickFix;
3538
import org.sonar.python.quickfix.PythonTextEdit;
3639
import org.sonar.python.semantic.ProjectLevelSymbolTable;
3740
import org.sonar.python.tree.PythonTreeMaker;
@@ -42,7 +45,7 @@ public class PythonQuickFixVerifier {
4245
private PythonQuickFixVerifier() {
4346
}
4447

45-
public static void verify(PythonCheck check, String codeWithIssue, String codeFixed) {
48+
public static void verify(PythonCheck check, String codeWithIssue, String... codesFixed) {
4649
List<PythonCheck.PreciseIssue> issues = PythonQuickFixVerifier
4750
.getIssuesWithQuickFix(check, codeWithIssue);
4851

@@ -54,14 +57,19 @@ public static void verify(PythonCheck check, String codeWithIssue, String codeFi
5457

5558
assertThat(issue.getQuickFixes())
5659
.as("Number of quickfixes")
57-
.overridingErrorMessage("Expected 1 quickfix but found %d", issue.getQuickFixes().size())
58-
.hasSize(1);
60+
.overridingErrorMessage("Expected %d quickfix but found %d", codesFixed.length, issue.getQuickFixes().size())
61+
.hasSize(codesFixed.length);
62+
63+
List<String> appliedQuickFix = issue.getQuickFixes().stream()
64+
.map(quickFix -> applyQuickFix(codeWithIssue, quickFix))
65+
.collect(Collectors.toList());
5966

60-
String codeQFApplied = PythonQuickFixVerifier.applyQuickFix(codeWithIssue, issue);
61-
assertThat(codeQFApplied)
67+
assertThat(appliedQuickFix)
6268
.as("Application of the quickfix")
63-
.overridingErrorMessage("The code with the quickfix applied is not the expected result : %s instead of %s", codeQFApplied, codeFixed)
64-
.isEqualTo(codeFixed);
69+
.overridingErrorMessage("The code with the quickfix applied is not the expected result.\n" +
70+
"Applied QuickFixes are:\n%s\nExpected result:\n%s", Arrays.asList(codesFixed), appliedQuickFix)
71+
.isEqualTo(Arrays.asList(codesFixed));
72+
6573
}
6674

6775
private static List<PreciseIssue> scanFileForIssues(PythonCheck check, PythonVisitorContext context) {
@@ -85,12 +93,11 @@ private static List<PreciseIssue> getIssuesWithQuickFix(PythonCheck check, Strin
8593
return scanFileForIssues(check, visitorContext);
8694
}
8795

88-
private static String applyQuickFix(String codeWithIssue, IssueWithQuickFix issueWithQuickFix) {
89-
assertThat(issueWithQuickFix.getQuickFixes()).hasSize(1);
90-
PythonTextEdit loc = issueWithQuickFix.getQuickFixes().get(0).getTextEdits().get(0);
91-
String replacement = loc.replacementText();
92-
int start = convertPositionToIndex(codeWithIssue, loc.startLine(), loc.startLineOffset());
93-
int end = convertPositionToIndex(codeWithIssue, loc.endLine(), loc.endLineOffset());
96+
private static String applyQuickFix(String codeWithIssue, PythonQuickFix quickFix) {
97+
PythonTextEdit textEdit = quickFix.getTextEdits().get(0);
98+
String replacement = textEdit.replacementText();
99+
int start = convertPositionToIndex(codeWithIssue, textEdit.startLine(), textEdit.startLineOffset());
100+
int end = convertPositionToIndex(codeWithIssue, textEdit.endLine(), textEdit.endLineOffset());
94101
return codeWithIssue.substring(0, start) + replacement + codeWithIssue.substring(end);
95102
}
96103

python-checks/src/test/java/org/sonar/python/checks/quickfix/PythonQuickFixVerifierTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ public void one_issue_one_qf_wrong_fix() {
6262
SimpleCheck simpleCheck = new SimpleCheck();
6363
assertThatThrownBy(() -> PythonQuickFixVerifier.verify(simpleCheck, "a=10", "a==10"))
6464
.isInstanceOf(AssertionError.class)
65-
.hasMessageContaining("[Application of the quickfix] The code with the quickfix applied is not the expected result : a!=10 instead of a==10");
65+
.hasMessageContaining("[Application of the quickfix] The code with the quickfix applied is not the expected result.\n" +
66+
"Applied QuickFixes are:\n" +
67+
"[a==10]\n" +
68+
"Expected result:\n" +
69+
"[a!=10]");
6670
}
6771

6872
@Test

python-checks/src/test/resources/checks/implicitStringConcatenation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def prefixes():
6161

6262
def different_quotes():
6363
["""1""" """2"""] # Noncompliant
64+
# ^^^^^^^ ^^^^^^^<
6465
"1" '2' # Ok
6566
"1" """
6667
2""" # Ok. Even if strange

python-frontend/src/main/java/org/sonar/python/quickfix/PythonTextEdit.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ public static PythonTextEdit insertBefore(Tree tree, String textToInsert) {
4343
return new PythonTextEdit(textToInsert, token.line(), token.column(), token.line(), token.column());
4444
}
4545

46+
public static PythonTextEdit insertAfter(Tree tree, String textToInsert) {
47+
Token token = tree.firstToken();
48+
int lengthToken = token.value().length();
49+
return new PythonTextEdit(textToInsert, token.line(), token.column() + lengthToken, token.line(), token.column() + lengthToken);
50+
}
51+
4652
public String replacementText() {
4753
return message;
4854
}

python-frontend/src/test/java/org/sonar/python/quickfix/PythonTextEditTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,26 @@ public void test() {
4646
assertThat(textEdit.endLine()).isEqualTo(1);
4747
assertThat(textEdit.endLineOffset()).isEqualTo(7);
4848
}
49+
50+
@Test
51+
public void insert_after() {
52+
String tokenValue = "token";
53+
String replacementText = "This is a replacement text";
54+
55+
Token token = Mockito.mock(Token.class);
56+
when(token.line()).thenReturn(1);
57+
when(token.column()).thenReturn(7);
58+
when(token.firstToken()).thenReturn(token);
59+
when(token.lastToken()).thenReturn(token);
60+
61+
when(token.value()).thenReturn(tokenValue);
62+
63+
PythonTextEdit textEdit = PythonTextEdit.insertAfter(token, replacementText);
64+
65+
assertThat(textEdit.replacementText()).isEqualTo(replacementText);
66+
assertThat(textEdit.startLine()).isEqualTo(1);
67+
assertThat(textEdit.startLineOffset()).isEqualTo(12);
68+
assertThat(textEdit.endLine()).isEqualTo(1);
69+
assertThat(textEdit.endLineOffset()).isEqualTo(12);
70+
}
4971
}

0 commit comments

Comments
 (0)