Skip to content

Commit f738def

Browse files
Seppli11sonartech
authored andcommitted
SONARPY-2940 Rule S7498: Unnecessary empty <dict/list/tuple> call - rewrite as a literal (#261)
GitOrigin-RevId: e4ce22778fbcddd91e4d6cdd4133842cf6042532
1 parent a3db92d commit f738def

7 files changed

Lines changed: 241 additions & 0 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
import org.sonar.check.Rule;
23+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
24+
import org.sonar.plugins.python.api.tree.Argument;
25+
import org.sonar.plugins.python.api.tree.CallExpression;
26+
import org.sonar.plugins.python.api.tree.Expression;
27+
import org.sonar.plugins.python.api.tree.RegularArgument;
28+
import org.sonar.plugins.python.api.tree.Tree;
29+
import org.sonar.plugins.python.api.types.v2.TriBool;
30+
import org.sonar.python.types.v2.TypeCheckBuilder;
31+
32+
@Rule(key = "S7498")
33+
public class EmptyCollectionConstructorCheck extends PythonSubscriptionCheck {
34+
35+
private static final String MESSAGE = "Replace this constructor call with a literal.";
36+
private static final List<String> COLLECTION_CONSTRUCTORS = Arrays.asList("dict", "list", "tuple");
37+
38+
private List<TypeCheckBuilder> collectionConstructorTypeCheckers = null;
39+
private TypeCheckBuilder dictChecker = null;
40+
41+
@Override
42+
public void initialize(Context context) {
43+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, ctx -> {
44+
dictChecker = ctx.typeChecker().typeCheckBuilder().isTypeWithFqn("dict");
45+
46+
collectionConstructorTypeCheckers = new ArrayList<>();
47+
for (String constructor : COLLECTION_CONSTRUCTORS) {
48+
collectionConstructorTypeCheckers.add(ctx.typeChecker().typeCheckBuilder().isTypeWithFqn(constructor));
49+
}
50+
});
51+
52+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, ctx -> {
53+
CallExpression callExpression = (CallExpression) ctx.syntaxNode();
54+
55+
if (isUnnecessaryCollectionConstructor(callExpression)) {
56+
ctx.addIssue(callExpression.callee(), MESSAGE);
57+
}
58+
});
59+
}
60+
61+
private boolean isUnnecessaryCollectionConstructor(CallExpression callExpression) {
62+
return (isCollectionConstructor(callExpression.callee()) && isEmptyCall(callExpression))
63+
|| isDictConstructorWithOnlyMappings(callExpression);
64+
}
65+
66+
private boolean isCollectionConstructor(Expression calleeExpression) {
67+
var type = calleeExpression.typeV2();
68+
return collectionConstructorTypeCheckers.stream().map(checker -> checker.check(type)).anyMatch(TriBool.TRUE::equals);
69+
}
70+
71+
private static boolean isEmptyCall(CallExpression callExpression) {
72+
return callExpression.arguments().isEmpty();
73+
}
74+
75+
private boolean isDictConstructorWithOnlyMappings(CallExpression callExpression) {
76+
return isDictConstructor(callExpression) && hasOnlyKeywordArguments(callExpression);
77+
}
78+
79+
private boolean isDictConstructor(CallExpression callExpression) {
80+
return dictChecker.check(callExpression.callee().typeV2()) == TriBool.TRUE;
81+
}
82+
83+
private static boolean hasOnlyKeywordArguments(CallExpression callExpression) {
84+
return callExpression.arguments().stream().allMatch(EmptyCollectionConstructorCheck::isKeywordArg);
85+
}
86+
87+
private static boolean isKeywordArg(Argument arg) {
88+
return arg instanceof RegularArgument regularArg && regularArg.keywordArgument() != null;
89+
}
90+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ public Stream<Class<?>> getChecks() {
177177
ElseAfterLoopsWithoutBreakCheck.class,
178178
EmailSendingCheck.class,
179179
EmptyAlternativeCheck.class,
180+
EmptyCollectionConstructorCheck.class,
180181
EmptyGroupCheck.class,
181182
EmptyFunctionCheck.class,
182183
EmptyNestedBlockCheck.class,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<p>This rule raises an issue when <code>dict()</code>, <code>list()</code>, or <code>tuple()</code> are used to create empty collections or to
2+
initialize dictionaries with keyword arguments.</p>
3+
<h2>Why is this an issue?</h2>
4+
<p>Python provides concise literal syntax for creating empty dictionaries (<code>{}</code>), lists (<code>[]</code>), and tuples (<code>()</code>). It
5+
also offers a direct literal syntax for initializing dictionaries with key-value pairs (e.g., <code>{'a': 1, 'b': 2}</code>).</p>
6+
<p>Using the function calls <code>dict()</code>, <code>list()</code>, and <code>tuple()</code> for these purposes is generally less preferred for a
7+
few reasons:</p>
8+
<ul>
9+
<li> Readability and Directness: Literals are often considered more "Pythonic" and directly express the intent of creating an empty container or a
10+
dictionary with specific initial values. </li>
11+
<li> Consistency: Python encourages the use of literals where they are clear and effective. </li>
12+
<li> Performance: Calling a function involves overhead, including name lookup for <code>dict</code>, <code>list</code>, or <code>tuple</code> in the
13+
current scope. While often negligible for single calls, using literals is a direct instruction to the interpreter and can be marginally faster,
14+
especially in performance-sensitive code or tight loops. </li>
15+
</ul>
16+
<p>Specifically, the following patterns are discouraged:</p>
17+
<ul>
18+
<li> Using <code>dict()</code> to create an empty dictionary instead of <code>{}</code>. </li>
19+
<li> Using <code>list()</code> to create an empty list instead of <code>[]</code>. </li>
20+
<li> Using <code>tuple()</code> to create an empty tuple instead of <code>()</code>. </li>
21+
<li> Using <code>dict(key='value', …​)</code> to initialize a dictionary instead of <code>{'key': 'value', …​}</code> when keys are simple strings
22+
valid as identifiers. </li>
23+
</ul>
24+
<p>While the functional difference is minimal for creating empty collections or simple dictionaries, adopting literals promotes a more direct and
25+
idiomatic coding style.</p>
26+
<h2>How to fix it</h2>
27+
<p>To fix this replace the function calls <code>dict()</code>, <code>list()</code>, and <code>tuple()</code> with their equivalent literal syntax.</p>
28+
<ul>
29+
<li> To create an empty dictionary, replace <code>dict()</code> with <code>{}</code>. </li>
30+
<li> To create an empty list, replace <code>list()</code> with <code>[]</code>. </li>
31+
<li> To create an empty tuple, replace <code>tuple()</code> with <code>()</code>. </li>
32+
<li> To initialize a dictionary with keyword arguments (where keys are simple strings that are valid identifiers), replace <code>dict(key1=value1,
33+
key2=value2)</code> with <code>{'key1': value1, 'key2': value2}</code>. </li>
34+
</ul>
35+
<pre data-diff-id="1" data-diff-type="noncompliant">
36+
empty_d = dict() # Noncompliant: the dict constructor is used instead of the literal syntax
37+
</pre>
38+
<pre data-diff-id="1" data-diff-type="compliant">
39+
empty_d = {} # Compliant
40+
</pre>
41+
<h2>Resources</h2>
42+
<h3>Documentation</h3>
43+
<ul>
44+
<li> Python Documentation - <a href="https://docs.python.org/3/library/stdtypes.html#dict">dict()</a> </li>
45+
<li> Python Documentation - <a href="https://docs.python.org/3/library/stdtypes.html#list">list()</a> </li>
46+
<li> Python Documentation - <a href="https://docs.python.org/3/library/stdtypes.html#tuple">tuple()</a> </li>
47+
</ul>
48+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"title": "Literal syntax should be preferred when creating empty collections or dictionaries with keyword arguments",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [],
10+
"defaultSeverity": "Minor",
11+
"ruleSpecification": "RSPEC-7498",
12+
"sqKey": "S7498",
13+
"scope": "All",
14+
"quickfix": "targeted",
15+
"code": {
16+
"impacts": {
17+
"MAINTAINABILITY": "LOW"
18+
},
19+
"attribute": "CONVENTIONAL"
20+
}
21+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@
258258
"S7488",
259259
"S7491",
260260
"S7493",
261+
"S7498",
261262
"S7499",
262263
"S7501",
263264
"S7510"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.python.checks.utils.PythonCheckVerifier;
21+
22+
class EmptyCollectionConstructorCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/emptyCollectionConstructor.py", new EmptyCollectionConstructorCheck());
27+
}
28+
29+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
empty_dict1 = dict() # Noncompliant {{Replace this constructor call with a literal.}}
2+
# ^^^^
3+
empty_list = list() # Noncompliant
4+
empty_tuple = tuple() # Noncompliant
5+
empty_set = set()
6+
7+
user = dict(name="John", age=30) # Noncompliant
8+
9+
empty_dict2 = {}
10+
empty_list2 = []
11+
empty_tuple2 = ()
12+
13+
user2 = {"name": "John", "age": 30}
14+
15+
nums_list = list([1, 2, 3])
16+
nums_tuple = tuple([1, 2, 3])
17+
combined_dict = dict({"a": 1})
18+
19+
20+
dict_from_items = dict([('a', 1), ('b', 2)])
21+
dict_from_mapping = dict({'one': 1, 'two': 2})
22+
23+
def passing_variable(collection):
24+
list(collection)
25+
tuple(collection)
26+
set(collection)
27+
dict(collection)
28+
dict(collection, b=2)
29+
dict(*collection)
30+
dict(**collection)
31+
32+
dict_comp = dict((k, v) for k, v in [('a', 1), ('b', 2)])
33+
list_comp = list(x for x in range(3))
34+
tuple_comp = tuple(x for x in range(3))
35+
36+
dict({"a": 1}, b=2)
37+
38+
some_unrelated_method()
39+
a.some_unrelated_method()
40+
41+
def overwriting_collection_constructors():
42+
def list(): pass
43+
def tuple(): pass
44+
def set(): pass
45+
def dict(): pass
46+
47+
# compliant since the built-in functions are shadowed
48+
list()
49+
tuple()
50+
set()
51+
dict()

0 commit comments

Comments
 (0)