Skip to content

Commit c282d36

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-2998 Rule S7519: Unnecessary dict comprehension - rewrite using dict.fromkeys() (#308)
GitOrigin-RevId: 88768da1ca860eb29e8fd52f5199fbd6303cfc4b
1 parent e7a50ee commit c282d36

File tree

7 files changed

+166
-1
lines changed

7 files changed

+166
-1
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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.sonar.check.Rule;
20+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
21+
import org.sonar.plugins.python.api.SubscriptionContext;
22+
import org.sonar.plugins.python.api.tree.DictCompExpression;
23+
import org.sonar.plugins.python.api.tree.Name;
24+
import org.sonar.plugins.python.api.tree.Tree;
25+
import org.sonar.python.semantic.v2.UsageV2;
26+
import org.sonar.python.tree.TreeUtils;
27+
28+
@Rule(key = "S7519")
29+
public class ConstantValueDictComprehensionCheck extends PythonSubscriptionCheck {
30+
public static final String MESSAGE = "Replace with dict fromkeys method call";
31+
32+
33+
@Override
34+
public void initialize(Context context) {
35+
context.registerSyntaxNodeConsumer(Tree.Kind.DICT_COMPREHENSION, ConstantValueDictComprehensionCheck::checkDictComprehension);
36+
}
37+
38+
private static void checkDictComprehension(SubscriptionContext ctx) {
39+
var dictComprehension = (DictCompExpression) ctx.syntaxNode();
40+
if (isConstantValueDictComprehension(dictComprehension)) {
41+
ctx.addIssue(dictComprehension, MESSAGE);
42+
}
43+
}
44+
45+
private static boolean isConstantValueDictComprehension(DictCompExpression dictComprehension) {
46+
if (!(dictComprehension.keyExpression() instanceof Name)
47+
|| dictComprehension.comprehensionFor().nestedClause() != null) {
48+
return false;
49+
}
50+
51+
if (dictComprehension.valueExpression() instanceof Name valueName) {
52+
var valueSymbol = valueName.symbolV2();
53+
return valueSymbol == null || !valueSymbol
54+
.usages()
55+
.stream()
56+
.map(UsageV2::tree)
57+
.allMatch(ut -> TreeUtils.firstAncestor(ut, dictComprehension::equals) != null);
58+
} else {
59+
return dictComprehension.valueExpression().is(Tree.Kind.NONE, Tree.Kind.STRING_LITERAL, Tree.Kind.NUMERIC_LITERAL, Tree.Kind.BOOLEAN_LITERAL_PATTERN);
60+
}
61+
}
62+
63+
64+
}

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
@@ -156,6 +156,7 @@ public Stream<Class<?>> getChecks() {
156156
ConfusingWalrusCheck.class,
157157
ConsistentReturnCheck.class,
158158
ConstantConditionCheck.class,
159+
ConstantValueDictComprehensionCheck.class,
159160
CorsCheck.class,
160161
CsrfDisabledCheck.class,
161162
DataEncryptionCheck.class,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<p>This rule raises an issue when a dictionary comprehension is used to create a dictionary where all values are the same constant.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>Using a dictionary comprehension to build a dictionary where every key maps to the exact same constant value e.g., <code>{k: 1 for k in
4+
keys}</code> is less efficient and less idiomatic than using the <code>dict.fromkeys()</code> class method. <code>dict.fromkeys()</code> is
5+
specifically designed for this use case and offers better performance, especially for large iterables, as it avoids the overhead of creating and
6+
processing individual key-value pairs in a comprehension.</p>
7+
<h2>How to fix it</h2>
8+
<p>Rewrite the dictionary comprehension <code>{x: constant for x in iterable}</code> as <code>dict.fromkeys(iterable, constant)</code>. If the
9+
constant value is <code>None</code>, you can omit the value argument in <code>dict.fromkeys()</code>, as it defaults to <code>None</code>.</p>
10+
<h3>Code examples</h3>
11+
<h4>Noncompliant code example</h4>
12+
<pre data-diff-id="1" data-diff-type="noncompliant">
13+
keys = ['a', 'b', 'c']
14+
15+
dict_comp_one = {k: 1 for k in keys} # Noncompliant
16+
</pre>
17+
<h4>Compliant solution</h4>
18+
<pre data-diff-id="1" data-diff-type="compliant">
19+
keys = ['a', 'b', 'c']
20+
21+
dict_fromkeys_one = dict.fromkeys(keys, 1)
22+
</pre>
23+
<h2>Resources</h2>
24+
<h3>Documentation</h3>
25+
<ul>
26+
<li> Python Documentation - <a href="https://docs.python.org/3/library/stdtypes.html#dict.fromkeys">dict.fromkeys</a> </li>
27+
</ul>
28+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"title": "Populating a dictionary with a constant value should be done with dict.fromkeys() method call",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "1min"
8+
},
9+
"tags": [],
10+
"defaultSeverity": "Minor",
11+
"ruleSpecification": "RSPEC-7519",
12+
"sqKey": "S7519",
13+
"scope": "All",
14+
"quickfix": "unknown",
15+
"code": {
16+
"impacts": {
17+
"RELIABILITY": "LOW"
18+
},
19+
"attribute": "CONVENTIONAL"
20+
}
21+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@
282282
"S7511",
283283
"S7512",
284284
"S7516",
285-
"S7517"
285+
"S7517",
286+
"S7519"
286287
]
287288
}
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 ConstantValueDictComprehensionCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/constantValueDictComprehension.py", new ConstantValueDictComprehensionCheck());
27+
}
28+
29+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
def case1():
2+
keys = ['a', 'b', 'c']
3+
some_v = "v"
4+
# Non-compliant
5+
dict_comp_one = {k: 1 for k in keys} # Noncompliant
6+
dict_comp_one = {k: "1" for k in keys} # Noncompliant
7+
dict_comp_one = {k: True for k in keys} # Noncompliant
8+
dict_comp_none = {k: None for k in keys} # Noncompliant
9+
dict_comp_v = {k: some_v for k in keys} # Noncompliant
10+
11+
some_dict = {"a": "1", "b": "2"}
12+
one_more = {k: some_v for k, v in some_dict} # Noncompliant
13+
one_more = {k: v for k, v in some_dict}
14+
15+
def case2():
16+
dict_of_dicts = [{"a": 1, "b": 2}, {"c": 3, "d": 4}]
17+
result = {k: v for result in dict_of_dicts for k, v in result.items()}
18+
19+
def case3():
20+
some_list = ["a/b", "c/d"]
21+
result = {q.split("/")[-1]: q for q in some_list}

0 commit comments

Comments
 (0)