Skip to content

Commit ae84cf2

Browse files
rombirlisonartech
authored andcommitted
SONARPY-5012 S9409: Only raise on for-loops and name the iterated collection
GitOrigin-RevId: d727bfea6e2b112da6b08f9e6cdcb9a62f9882a0
1 parent f03ae0d commit ae84cf2

6 files changed

Lines changed: 64 additions & 163 deletions

File tree

‎python-checks/src/main/java/org/sonar/python/checks/ListExtendOverIndividualAppendsCheck.java‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,5 @@ public class ListExtendOverIndividualAppendsCheck extends PythonSubscriptionChec
3636
@Override
3737
public void initialize(Context context) {
3838
context.registerSyntaxNodeConsumer(Tree.Kind.FOR_STMT, ctx -> CollectionBulkAdditionUtils.checkForStatement(ctx, LIST_EXTEND));
39-
context.registerSyntaxNodeConsumer(Tree.Kind.STATEMENT_LIST, ctx -> CollectionBulkAdditionUtils.checkStatementList(ctx, LIST_EXTEND));
4039
}
4140
}

‎python-checks/src/main/java/org/sonar/python/checks/utils/CollectionBulkAdditionUtils.java‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ public final class CollectionBulkAdditionUtils {
4646

4747
private static final int MIN_CONSECUTIVE_ADDITIONS = 3;
4848

49-
private static final String FOR_LOOP_MESSAGE = "Use \"%s.%s()\" instead of a for-loop with \"%s()\".";
49+
private static final String FOR_LOOP_NAMED_MESSAGE = "Use \"%s.%s()\" to add all elements of \"%s\" instead of a for-loop with \"%s()\".";
50+
private static final String FOR_LOOP_MESSAGE = "Use \"%s.%s()\" to add all elements of the iterated collection instead of a for-loop with \"%s()\".";
5051
private static final String CONSECUTIVE_MESSAGE = "Use \"%s.%s()\" instead of consecutive \"%s()\" calls.";
5152
private static final String SECONDARY_MESSAGE = "This call is part of the same sequence.";
5253

@@ -77,6 +78,7 @@ public static void checkForStatement(SubscriptionContext ctx, BulkAddition bulkA
7778
return;
7879
}
7980
Expression iterable = forStatement.testExpressions().get(0);
81+
Name iterableName = Expressions.removeParentheses(iterable) instanceof Name name ? name : null;
8082

8183
CallExpression call = singleBodyCall(forStatement);
8284
if (call == null || !(call.callee() instanceof QualifiedExpression callee)) {
@@ -99,7 +101,7 @@ public static void checkForStatement(SubscriptionContext ctx, BulkAddition bulkA
99101
return;
100102
}
101103

102-
ctx.addIssue(callee, message(FOR_LOOP_MESSAGE, bulkAddition));
104+
ctx.addIssue(callee, forLoopMessage(bulkAddition, iterableName));
103105
}
104106

105107
public static void checkStatementList(SubscriptionContext ctx, BulkAddition bulkAddition) {
@@ -153,6 +155,13 @@ private static void reportRun(SubscriptionContext ctx, BulkAddition bulkAddition
153155
run.stream().skip(1).forEach(addition -> issue.secondary(addition.call(), SECONDARY_MESSAGE));
154156
}
155157

158+
private static String forLoopMessage(BulkAddition bulkAddition, @Nullable Name collection) {
159+
if (collection != null) {
160+
return String.format(FOR_LOOP_NAMED_MESSAGE, bulkAddition.typeName(), bulkAddition.bulkMethod(), collection.name(), bulkAddition.elementMethod());
161+
}
162+
return message(FOR_LOOP_MESSAGE, bulkAddition);
163+
}
164+
156165
private static String message(String template, BulkAddition bulkAddition) {
157166
return String.format(template, bulkAddition.typeName(), bulkAddition.bulkMethod(), bulkAddition.elementMethod());
158167
}
Lines changed: 17 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,53 @@
1-
<p>This rule raises an issue when several elements are added to the same list through consecutive <code>append()</code> calls, either as a sequence of
2-
statements or through a for-loop.</p>
1+
<p>This rule raises an issue when a for-loop adds every element of a collection to a list through <code>append()</code>.</p>
32
<h2>Why is this an issue?</h2>
4-
<p>Adding elements to a list one at a time is unnecessary when the <code>extend()</code> method exists. This happens with a sequence of
5-
<code>append()</code> statements:</p>
6-
<pre>
7-
my_list.append(first)
8-
my_list.append(second)
9-
my_list.append(third)
10-
</pre>
11-
<p>It also happens when a for-loop appends every element of another collection, which is the same sequence of calls spread over an iteration:</p>
3+
<p>Adding elements to a list one at a time is unnecessary when the <code>extend()</code> method exists. This happens when a for-loop appends every
4+
element of another collection:</p>
125
<pre>
136
for element in other_collection:
147
my_list.append(element)
158
</pre>
16-
<p>The <code>extend()</code> method combines all additions into a single call and avoids the overhead of repeated <code>append()</code>
17-
invocations:</p>
9+
<p>The <code>extend()</code> method adds all elements in a single call and avoids the overhead of repeated <code>append()</code> invocations:</p>
1810
<pre>
19-
my_list.extend([first, second, third])
2011
my_list.extend(other_collection)
2112
</pre>
2213
<h3>What is the potential impact?</h3>
23-
<p>Grouping the additions makes the intent explicit: the reader sees one operation that grows the list instead of a series of independent statements.
14+
<p>Replacing the loop makes the intent explicit: the reader sees one operation that grows the list instead of an iteration whose body has to be read.
2415
It also removes redundant call overhead, since the attribute lookup and the method call are resolved once instead of once per element. In hot code or
2516
when the source collection is large, the difference becomes measurable.</p>
2617
<h3>Exceptions</h3>
27-
<p>No issue is raised when other statements are interleaved between the <code>append()</code> calls, or when the body of the loop does more than
28-
append the loop variable, for example when it filters or transforms the elements:</p>
18+
<p>No issue is raised when the body of the loop does more than append the loop variable, for example when it filters or transforms the elements:</p>
2919
<pre>
3020
for element in other_collection:
3121
if element.is_valid():
3222
my_list.append(element)
3323
</pre>
3424
<h2>How to fix it</h2>
35-
<p>Group the appended elements into a single <code>extend()</code> call. For consecutive statements, pass the elements as a list literal. For a loop,
36-
pass the iterated collection directly, as <code>extend()</code> accepts any iterable.</p>
25+
<p>Replace the loop with a single <code>extend()</code> call and pass the iterated collection directly, as <code>extend()</code> accepts any
26+
iterable.</p>
3727
<h3>Code examples</h3>
3828
<h4>Noncompliant code example</h4>
3929
<pre data-diff-id="1" data-diff-type="noncompliant">
40-
def build_command(base_command, path):
41-
command = list(base_command)
42-
command.append("--archive") # Noncompliant
43-
command.append("--verbose")
44-
command.append(path)
45-
return command
46-
</pre>
47-
<h4>Compliant solution</h4>
48-
<pre data-diff-id="1" data-diff-type="compliant">
49-
def build_command(base_command, path):
50-
command = list(base_command)
51-
command.extend(["--archive", "--verbose", path])
52-
return command
53-
</pre>
54-
<h4>Noncompliant code example</h4>
55-
<pre data-diff-id="2" data-diff-type="noncompliant">
5630
def collect_results(results, new_results):
5731
for result in new_results:
5832
results.append(result) # Noncompliant
5933
return results
6034
</pre>
6135
<h4>Compliant solution</h4>
62-
<pre data-diff-id="2" data-diff-type="compliant">
36+
<pre data-diff-id="1" data-diff-type="compliant">
6337
def collect_results(results, new_results):
6438
results.extend(new_results)
6539
return results
6640
</pre>
6741
<h3>How does this work?</h3>
68-
<p><code>extend()</code> iterates over its argument and appends every element to the list, so the result is exactly the same as the individual calls.
69-
The augmented assignment <code>my_list += [first, second, third]</code> is equivalent to <code>extend()</code> and can be used instead when it reads
70-
better.</p>
42+
<p><code>extend()</code> iterates over its argument and appends every element to the list, so the result is exactly the same as the loop. The
43+
augmented assignment <code>my_list += other_collection</code> is equivalent to <code>extend()</code> and can be used instead when it reads better.</p>
7144
<h3>Pitfalls</h3>
7245
<p>Do not confuse <code>extend()</code> with <code>append()</code>: <code>my_list.append(other_collection)</code> adds the collection itself as a
7346
single element, while <code>extend()</code> adds its elements.</p>
74-
<p>A loop that appends to the list it iterates over never terminates, while <code>my_list.extend(my_list)</code> simply doubles the list. The fix is
75-
behavior-preserving in every other case, so make sure the loop you replace iterates over a different collection.</p>
47+
<p>A loop that appends to the list it iterates over never terminates, while <code>my_list.extend(my_list)</code> simply doubles the list. Make sure
48+
the loop you replace iterates over a different collection.</p>
49+
<p>After a for-loop, the loop variable stays bound to the last element, while <code>extend()</code> defines no variable. If code after the loop reads
50+
that variable, keep it defined before replacing the loop.</p>
7651
<h2>Resources</h2>
7752
<h3>Documentation</h3>
7853
<ul>
@@ -82,6 +57,7 @@ <h3>Documentation</h3>
8257
</ul>
8358
<h3>Related rules</h3>
8459
<ul>
85-
<li>{rule:python:S8502} - "set.update()" should be used instead of consecutive "add()" calls</li>
60+
<li>{rule:python:S8502} - "set.update()" should be used instead of a for-loop with "add()"</li>
61+
<li>{rule:python:S9454} - "list.extend()" should be used instead of consecutive "append()" calls</li>
8662
</ul>
8763

‎python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S9409.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"title": "\"list.extend()\" should be used instead of consecutive \"append()\" calls",
2+
"title": "\"list.extend()\" should be used instead of a for-loop with \"append()\"",
33
"type": "CODE_SMELL",
44
"status": "ready",
55
"remediation": {

‎python-checks/src/test/resources/checks/listExtendOverIndividualAppends.py‎

Lines changed: 34 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ def noncompliant_for_loop():
33
my_list = [1, 2, 3]
44
other_collection = [4, 5, 6]
55
for element in other_collection:
6-
my_list.append(element) # Noncompliant {{Use "list.extend()" instead of a for-loop with "append()".}}
6+
my_list.append(element) # Noncompliant {{Use "list.extend()" to add all elements of "other_collection" instead of a for-loop with "append()".}}
77
# ^^^^^^^^^^^^^^
88

99

@@ -14,7 +14,7 @@ def noncompliant_for_loop_multiline_iterable():
1414
2,
1515
3,
1616
]:
17-
my_list.append(element) # Noncompliant
17+
my_list.append(element) # Noncompliant {{Use "list.extend()" to add all elements of the iterated collection instead of a for-loop with "append()".}}
1818
# ^^^^^^^^^^^^^^
1919

2020

@@ -26,57 +26,46 @@ def noncompliant_for_loop_multiline_receiver():
2626
).append(item)
2727

2828

29-
def noncompliant_consecutive_appends(base_command, path):
30-
command = list(base_command)
31-
command.append("--archive") # Noncompliant {{Use "list.extend()" instead of consecutive "append()" calls.}}
32-
# ^^^^^^^^^^^^^^
33-
command.append("--verbose")
34-
# ^^^^^^^^^^^^^^< {{This call is part of the same sequence.}}
35-
command.append(path)
36-
# ^^^^^^^^^^^^^^< {{This call is part of the same sequence.}}
37-
return command
29+
def noncompliant_for_loop_named_iterable_message(new_results):
30+
results = []
31+
for result in new_results:
32+
results.append(result) # Noncompliant {{Use "list.extend()" to add all elements of "new_results" instead of a for-loop with "append()".}}
33+
# ^^^^^^^^^^^^^^
3834

3935

40-
def noncompliant_consecutive_appends_four_calls():
41-
my_list = []
42-
my_list.append(1) # Noncompliant
43-
my_list.append(2)
44-
my_list.append(3)
45-
my_list.append(4)
36+
def noncompliant_for_loop_parenthesized_iterable_message(new_results):
37+
results = []
38+
for result in (new_results):
39+
results.append(result) # Noncompliant {{Use "list.extend()" to add all elements of "new_results" instead of a for-loop with "append()".}}
40+
# ^^^^^^^^^^^^^^
4641

4742

48-
def noncompliant_consecutive_appends_complex_arguments(items, flag):
43+
def noncompliant_for_loop_call_iterable_message():
4944
my_list = []
50-
my_list.append(items[0]) # Noncompliant
51-
my_list.append(items[1] if flag else None)
52-
my_list.append(len(items))
45+
for i in range(10):
46+
my_list.append(i) # Noncompliant {{Use "list.extend()" to add all elements of the iterated collection instead of a for-loop with "append()".}}
47+
# ^^^^^^^^^^^^^^
5348

5449

55-
def noncompliant_consecutive_appends_after_other_statements():
50+
def noncompliant_for_loop_attribute_iterable_message(config):
5651
my_list = []
57-
print("start")
58-
my_list.append(1) # Noncompliant
59-
my_list.append(2)
60-
my_list.append(3)
52+
for path in config.paths:
53+
my_list.append(path) # Noncompliant {{Use "list.extend()" to add all elements of the iterated collection instead of a for-loop with "append()".}}
54+
# ^^^^^^^^^^^^^^
6155

6256

63-
def noncompliant_consecutive_appends_two_runs(other_list):
57+
def noncompliant_for_loop_subscription_iterable_message(groups):
6458
my_list = []
65-
my_list.append(1) # Noncompliant
66-
my_list.append(2)
67-
my_list.append(3)
68-
print("separator")
69-
my_list.append(4) # Noncompliant
70-
my_list.append(5)
71-
my_list.append(6)
59+
for item in groups[0]:
60+
my_list.append(item) # Noncompliant {{Use "list.extend()" to add all elements of the iterated collection instead of a for-loop with "append()".}}
61+
# ^^^^^^^^^^^^^^
7262

7363

7464
class MyClass:
75-
def build(self, path):
65+
def build(self, items):
7666
my_list = []
77-
my_list.append("first") # Noncompliant
78-
my_list.append("second")
79-
my_list.append(path)
67+
for item in items:
68+
my_list.append(item) # Noncompliant
8069
return my_list
8170

8271

@@ -87,91 +76,19 @@ def compliant_extend_call():
8776
my_list.extend(["a", "b", "c"])
8877

8978

90-
def compliant_two_consecutive_appends():
91-
my_list = []
92-
my_list.append(1)
93-
my_list.append(2)
94-
95-
96-
def compliant_appends_interleaved_with_other_statements():
97-
my_list = []
98-
my_list.append(1)
99-
print("in between")
100-
my_list.append(2)
101-
print("in between")
102-
my_list.append(3)
103-
104-
105-
def compliant_appends_on_different_lists():
106-
first = []
107-
second = []
108-
third = []
109-
first.append(1)
110-
second.append(2)
111-
third.append(3)
112-
113-
114-
def compliant_appends_referencing_the_list(items):
115-
# Grouping these calls would change the result: each argument observes the previous append
116-
my_list = []
117-
my_list.append(len(my_list))
118-
my_list.append(len(my_list))
119-
my_list.append(len(my_list))
120-
121-
122-
def compliant_appends_in_different_blocks(flag):
123-
my_list = []
124-
if flag:
125-
my_list.append(1)
126-
else:
127-
my_list.append(2)
128-
my_list.append(3)
129-
130-
131-
def compliant_appends_on_unknown_receiver(unknown_receiver):
132-
unknown_receiver.append(1)
133-
unknown_receiver.append(2)
134-
unknown_receiver.append(3)
79+
def compliant_consecutive_appends(base_command, path):
80+
# Consecutive calls are covered by S9454
81+
command = list(base_command)
82+
command.append("--archive")
83+
command.append("--verbose")
84+
command.append(path)
85+
return command
13586

13687

13788
def get_list():
13889
return []
13990

14091

141-
def compliant_appends_on_transient_receiver():
142-
get_list().append(1)
143-
get_list().append(2)
144-
get_list().append(3)
145-
146-
147-
def compliant_appends_on_a_set():
148-
my_set = {1}
149-
my_set.add(1)
150-
my_set.add(2)
151-
my_set.add(3)
152-
153-
154-
def compliant_appends_with_wrong_arity():
155-
my_list = []
156-
my_list.append()
157-
my_list.append(1, 2)
158-
my_list.append(3)
159-
160-
161-
def compliant_appends_with_keyword_argument():
162-
my_list = []
163-
my_list.append(item=1)
164-
my_list.append(item=2)
165-
my_list.append(item=3)
166-
167-
168-
def compliant_appends_with_starred_argument(args):
169-
my_list = []
170-
my_list.append(*args)
171-
my_list.append(*args)
172-
my_list.append(*args)
173-
174-
17592
def compliant_for_loop_tuple_unpacking_loop_var():
17693
my_list = []
17794
pairs = [(1, "a"), (2, "b")]

‎python-checks/src/test/resources/checks/setUpdateOverIndividualAdds.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ def noncompliant_basic():
33
my_set = {1, 2, 3}
44
other_collection = [4, 5, 6]
55
for element in other_collection:
6-
my_set.add(element) # Noncompliant {{Use "set.update()" instead of a for-loop with "add()".}}
6+
my_set.add(element) # Noncompliant {{Use "set.update()" to add all elements of "other_collection" instead of a for-loop with "add()".}}
77
# ^^^^^^^^^^
88

99

0 commit comments

Comments
 (0)