Skip to content

Commit 4fbd638

Browse files
SONARPY-1191 The quick fix of S1854 (DeadStoreCheck) should remove indent before removed line (#1279)
1 parent a98dda0 commit 4fbd638

File tree

6 files changed

+243
-121
lines changed

6 files changed

+243
-121
lines changed

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

Lines changed: 27 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
*/
2020
package org.sonar.python.checks;
2121

22-
import java.util.List;
23-
import java.util.Optional;
2422
import java.util.Set;
2523
import javax.annotation.Nullable;
2624
import org.sonar.check.Rule;
@@ -83,15 +81,25 @@ private static void verifyBlock(SubscriptionContext ctx, CfgBlock block, LiveVar
8381
// symbols should have at least one read usage (otherwise will be reported by S1481)
8482
.filter(unnecessaryAssignment -> readSymbols.contains(unnecessaryAssignment.symbol))
8583
.filter((unnecessaryAssignment -> !isException(unnecessaryAssignment.symbol, unnecessaryAssignment.element, functionDef)))
86-
.forEach(unnecessaryAssignment -> {
87-
Tree element = unnecessaryAssignment.element;
88-
String message = String.format(MESSAGE_TEMPLATE, unnecessaryAssignment.symbol.name());
89-
IssueWithQuickFix issue = (IssueWithQuickFix) separatorTokenForIssue(element)
90-
.map(separator -> ctx.addIssue(element.firstToken(), separator, message))
91-
.orElseGet(() -> ctx.addIssue(element, message));
84+
.forEach(unnecessaryAssignment -> raiseIssue(ctx, unnecessaryAssignment));
85+
}
86+
87+
private static void raiseIssue(SubscriptionContext ctx, DeadStoreUtils.UnnecessaryAssignment unnecessaryAssignment) {
88+
Tree element = unnecessaryAssignment.element;
89+
String message = String.format(MESSAGE_TEMPLATE, unnecessaryAssignment.symbol.name());
90+
Token lastRelevantToken = TreeUtils.getTreeSeparatorOrLastToken(element);
91+
PreciseIssue issue;
92+
if ("\n".equals(lastRelevantToken.value())) {
93+
issue = ctx.addIssue(element, message);
94+
} else {
95+
issue = ctx.addIssue(element.firstToken(), lastRelevantToken, message);
96+
}
97+
98+
if (element instanceof Statement && !isExceptionForQuickFix((Statement) element)) {
99+
((IssueWithQuickFix) issue).addQuickFix(PythonQuickFix.newQuickFix("Remove the unused statement",
100+
PythonTextEdit.removeStatement((Statement) element)));
101+
}
92102

93-
createQuickFix(issue, element);
94-
});
95103
}
96104

97105
private static boolean isMultipleAssignement(Tree element) {
@@ -157,60 +165,21 @@ private static boolean isFunctionDeclarationSymbol(Symbol symbol) {
157165
return symbol.usages().stream().anyMatch(u -> u.kind() == Usage.Kind.FUNC_DECLARATION);
158166
}
159167

160-
private static Optional<Token> separatorTokenForIssue(Tree element) {
161-
return separatorTokenOfElement(element)
162-
.filter(sep -> !"\n".equals(sep.value()));
163-
}
164-
165-
private static Optional<Token> separatorTokenOfElement(Tree element) {
166-
if (element.is(Tree.Kind.ASSIGNMENT_STMT, Tree.Kind.EXPRESSION_STMT)) {
167-
Token separator = ((Statement) element).separator();
168-
return Optional.ofNullable(separator);
169-
}
170-
return Optional.empty();
171-
}
172-
173-
private static PythonTextEdit removeDeadStore(Tree currentTree) {
174-
Optional<Token> tokenSeparator = separatorTokenOfElement(currentTree);
175-
List<Tree> childrenOfParent = currentTree.parent().children();
176-
177-
if (childrenOfParent.size() == 1) {
178-
return PythonTextEdit.replace(currentTree, "pass");
179-
}
180168

181-
Token currentFirstToken = currentTree.firstToken();
182-
int indexOfCurrentTree = childrenOfParent.indexOf(currentTree);
183-
184-
// If the tree to remove is the last one, we remove from the end of the previous tree until the end of the current one
185-
if (indexOfCurrentTree == childrenOfParent.size() - 1) {
186-
Tree previousTree = childrenOfParent.get(indexOfCurrentTree - 1);
187-
Optional<Token> previousSep = separatorTokenOfElement(previousTree);
188-
Token previous = previousSep.orElse(previousTree.lastToken());
189-
currentFirstToken = tokenSeparator.orElse(currentTree.lastToken());
190-
return removeFromEndOfTillEndOf(previous, currentFirstToken);
191-
}
192-
193-
// If the next tree is more than 1 line after the currentFirstToken tree, we only remove the separator
194-
Token next;
195-
int currentLine = currentTree.lastToken().line();
196-
int nextLine = childrenOfParent.get(indexOfCurrentTree + 1).firstToken().line();
197-
if (nextLine > currentLine + 1) {
198-
next = tokenSeparator.orElse(currentTree.lastToken());
199-
// Remove from the start of the currentFirstToken token until after the separator
200-
return new PythonTextEdit("", currentFirstToken.line(), currentFirstToken.column(), next.line(), next.column() + next.value().length());
201-
} else {
202-
// Remove from the start of the currentFirstToken token until the next token
203-
next = childrenOfParent.get(indexOfCurrentTree + 1).firstToken();
204-
return new PythonTextEdit("", currentFirstToken.line(), currentFirstToken.column(), next.line(), next.column());
205-
}
206-
}
207-
208-
private static boolean hasPotentialSideEffect(Tree tree) {
169+
private static boolean isExceptionForQuickFix(Statement tree) {
209170
switch (tree.getKind()) {
171+
// foo:str = bar
210172
case ANNOTATED_ASSIGNMENT:
211173
return SideEffectDetector.hasSideEffect(((AnnotatedAssignment) tree).assignedValue());
174+
// foo = bar or foo = bar = 1
212175
case ASSIGNMENT_STMT:
176+
// TODO: SONARPY-1192 Provide quick fix for chained assignment
177+
AssignmentStatement assignmentStatement = (AssignmentStatement) tree;
178+
if(assignmentStatement.lhsExpressions().size() > 1) {
179+
return true;
180+
}
213181
return SideEffectDetector.hasSideEffect(((AssignmentStatement) tree).assignedValue());
182+
// foo(bar:=3)
214183
case EXPRESSION_STMT:
215184
ExpressionStatement expressionStatement = (ExpressionStatement) tree;
216185
return expressionStatement.expressions().stream().anyMatch(SideEffectDetector::hasSideEffect);
@@ -219,22 +188,6 @@ private static boolean hasPotentialSideEffect(Tree tree) {
219188
}
220189
}
221190

222-
private static void createQuickFix(IssueWithQuickFix issue, Tree unnecessaryAssignment) {
223-
if (hasPotentialSideEffect(unnecessaryAssignment)) {
224-
return;
225-
}
226-
PythonTextEdit edit = removeDeadStore(unnecessaryAssignment);
227-
PythonQuickFix quickFix = PythonQuickFix.newQuickFix("Remove the unused statement")
228-
.addTextEdit(edit)
229-
.build();
230-
issue.addQuickFix(quickFix);
231-
}
232-
233-
private static PythonTextEdit removeFromEndOfTillEndOf(Token first, Token last) {
234-
return new PythonTextEdit("", first.line(), first.column() + first.value().length(),
235-
last.line(), last.column() + last.value().length());
236-
}
237-
238191
private static class SideEffectDetector extends BaseTreeVisitor {
239192

240193
private boolean sideEffect = false;

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

Lines changed: 86 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public void test() {
3636
}
3737

3838
@Test
39-
public void quickfix() {
39+
public void assignment_on_single_line() {
4040
String codeWithIssue = code(
4141
"def foo():",
4242
" x = 42",
@@ -50,7 +50,7 @@ public void quickfix() {
5050
}
5151

5252
@Test
53-
public void semicolon() {
53+
public void assignment_with_semicolon_on_single_line() {
5454
String codeWithIssue = code(
5555
"def foo():",
5656
" x = 42 ;",
@@ -64,7 +64,35 @@ public void semicolon() {
6464
}
6565

6666
@Test
67-
public void space() {
67+
public void assignment_between_two_siblings() {
68+
String codeWithIssue = code(
69+
"def foo():",
70+
" y = 0; x = 42; x = 0",
71+
" print(x)");
72+
String codeFixed = code(
73+
"def foo():",
74+
" y = 0; x = 0",
75+
" print(x)");
76+
PythonQuickFixVerifier.verify(check, codeWithIssue, codeFixed);
77+
}
78+
79+
@Test
80+
public void assignment_with_previous_element_on_line() {
81+
String codeWithIssue = code(
82+
"def foo():",
83+
" y = 0; x = 42",
84+
" x = 0",
85+
" print(x)");
86+
String codeFixed = code(
87+
"def foo():",
88+
" y = 0;",
89+
" x = 0",
90+
" print(x)");
91+
PythonQuickFixVerifier.verify(check, codeWithIssue, codeFixed);
92+
}
93+
94+
@Test
95+
public void assignment_with_trailing_whitespace() {
6896
String codeWithIssue = code(
6997
"def foo():",
7098
" x = 42 ",
@@ -78,11 +106,11 @@ public void space() {
78106
}
79107

80108
@Test
81-
public void quickfix_after_non_issue() {
109+
public void assignment_wrapped_by_elements_on_different_lines() {
82110
String codeWithIssue = code(
83111
"def foo():",
84112
" a = 1",
85-
" x = 10 ;",
113+
" x = 10",
86114
" x = 0",
87115
" print(x)");
88116
String codeFixed = code(
@@ -94,14 +122,14 @@ public void quickfix_after_non_issue() {
94122
}
95123

96124
@Test
97-
public void quickfix_oneline() {
125+
public void all_statements_on_single_line() {
98126
String codeWithIssue = "def dead_store(): unused = 24; unused = 42; print(unused)";
99127
String codeFixed = "def dead_store(): unused = 42; print(unused)";
100128
PythonQuickFixVerifier.verify(check, codeWithIssue, codeFixed);
101129
}
102130

103131
@Test
104-
public void quickfix_in_condition() {
132+
public void assignment_before_condition() {
105133
String codeWithIssue = code(
106134
"def simple_conditional():",
107135
" x = 10",
@@ -138,39 +166,7 @@ public void assignment_expression_in_function_call() {
138166
PythonQuickFixVerifier.verifyNoQuickFixes(check, codeWithIssue);
139167
}
140168

141-
@Test
142-
public void no_separator_found(){
143-
String codeWithIssue = code(
144-
"def ab():",
145-
" a = foo()",
146-
" print(a)",
147-
" a = foo()",
148-
"");
149-
PythonQuickFixVerifier.verifyNoQuickFixes(check, codeWithIssue);
150-
}
151169

152-
@Test
153-
public void one_separator_found(){
154-
String codeWithIssue = code(
155-
"def ab():",
156-
" a = foo()",
157-
" print(a)",
158-
" a = foo();");
159-
PythonQuickFixVerifier.verifyNoQuickFixes(check, codeWithIssue);
160-
}
161-
162-
@Test
163-
public void two_separators_found(){
164-
String codeWithIssue = code(
165-
"def ab():",
166-
" a = foo()",
167-
" print(a)",
168-
" a = foo();",
169-
"");
170-
PythonQuickFixVerifier.verifyNoQuickFixes(check, codeWithIssue);
171-
}
172-
173-
// TODO: The quick fix should remove indent before removed line (SONARPY-1191)
174170
@Test
175171
public void comment_after_should_not_be_removed(){
176172
String codeWithIssue = code(
@@ -181,35 +177,34 @@ public void comment_after_should_not_be_removed(){
181177
" print(a)");
182178
String codeFixed = code(
183179
"def ab():",
184-
" # This is an important comment",
180+
" # This is an important comment",
185181
" a = 43",
186182
" print(a)");
187183
PythonQuickFixVerifier.verify(check, codeWithIssue, codeFixed);
188184
}
189185

190-
// TODO: The quick fix should remove indent before removed line (SONARPY-1191)
191186
@Test
192-
public void space_comment_after_should_not_be_removed(){
187+
public void new_line_after_assignment_should_not_be_removed(){
193188
String codeWithIssue = code(
194189
"def ab():",
195190
" b = 1",
196191
" a = 42",
197-
"\n"+
192+
"",
198193
" # This is an important comment",
199194
" a = 43",
200195
" print(a)");
201196
String codeFixed = code(
202197
"def ab():",
203198
" b = 1",
204-
" \n"+
199+
"",
205200
" # This is an important comment",
206201
" a = 43",
207202
" print(a)");
208203
PythonQuickFixVerifier.verify(check, codeWithIssue, codeFixed);
209204
}
210205

211206
@Test
212-
public void deadstore_one_branch(){
207+
public void assignment_on_branch(){
213208
String codeWithIssue = code(
214209
"def a():",
215210
" x = 42",
@@ -225,6 +220,41 @@ public void deadstore_one_branch(){
225220
PythonQuickFixVerifier.verify(check, codeWithIssue, codeFixed);
226221
}
227222

223+
@Test
224+
public void assignment_on_branch_with_multiple_elements(){
225+
String codeWithIssue = code(
226+
"def a():",
227+
" x = 42",
228+
" if x:",
229+
" x = 43",
230+
" a = 4",
231+
" print(a)");
232+
String codeFixed = code(
233+
"def a():",
234+
" x = 42",
235+
" if x:",
236+
" a = 4",
237+
" print(a)");
238+
PythonQuickFixVerifier.verify(check, codeWithIssue, codeFixed);
239+
}
240+
241+
@Test
242+
public void assignment_on_branch_with_multiple_elements_on_same_line(){
243+
String codeWithIssue = code(
244+
"def a():",
245+
" x = 42",
246+
" if x:",
247+
" a = 4; x = 43",
248+
" print(a)");
249+
String codeFixed = code(
250+
"def a():",
251+
" x = 42",
252+
" if x:",
253+
" a = 4;",
254+
" print(a)");
255+
PythonQuickFixVerifier.verify(check, codeWithIssue, codeFixed);
256+
}
257+
228258
@Test
229259
public void side_effect_in_binary_op(){
230260
String codeWithIssue = code(
@@ -234,4 +264,15 @@ public void side_effect_in_binary_op(){
234264
" print(a)");
235265
PythonQuickFixVerifier.verifyNoQuickFixes(check, codeWithIssue);
236266
}
267+
268+
@Test
269+
public void chain_assignment() {
270+
String codeWithIssue = code(
271+
"def chain_assign():",
272+
" a = b = 42",
273+
" a = foo()",
274+
" print(a, b)"
275+
);
276+
PythonQuickFixVerifier.verifyNoQuickFixes(check, codeWithIssue);
277+
}
237278
}

0 commit comments

Comments
 (0)