Skip to content

Commit 5990a05

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-2945 Rule S7505: Unnecessary map usage - rewrite using a generator expression/<list/set/dict> comprehension (#288)
GitOrigin-RevId: 8e2cd0a07312662226af9c8d7ed9923775be49c3
1 parent bc0f575 commit 5990a05

7 files changed

Lines changed: 205 additions & 0 deletions

File tree

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
@@ -399,6 +399,7 @@ public Stream<Class<?>> getChecks() {
399399
UnionTypeExpressionCheck.class,
400400
UnnecessaryListComprehensionArgumentCheck.class,
401401
UnnecessaryComprehensionCheck.class,
402+
UnnecessaryLambdaMapCallCheck.class,
402403
UnnecessaryReversedCallCheck.class,
403404
UnnecessarySubscriptReversalCheck.class,
404405
UnquantifiedNonCapturingGroupCheck.class,
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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.List;
20+
import java.util.Optional;
21+
import javax.annotation.Nullable;
22+
import org.sonar.check.Rule;
23+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
24+
import org.sonar.plugins.python.api.SubscriptionContext;
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.LambdaExpression;
28+
import org.sonar.plugins.python.api.tree.Name;
29+
import org.sonar.plugins.python.api.tree.RegularArgument;
30+
import org.sonar.plugins.python.api.tree.Tree;
31+
import org.sonar.plugins.python.api.types.v2.TriBool;
32+
import org.sonar.python.checks.utils.Expressions;
33+
import org.sonar.python.semantic.v2.SymbolV2;
34+
import org.sonar.python.tree.TreeUtils;
35+
import org.sonar.python.types.v2.TypeCheckBuilder;
36+
37+
@Rule(key = "S7505")
38+
public class UnnecessaryLambdaMapCallCheck extends PythonSubscriptionCheck {
39+
private TypeCheckBuilder isMapTypeCheck;
40+
41+
@Override
42+
public void initialize(Context context) {
43+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::initChecks);
44+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::check);
45+
}
46+
47+
private void initChecks(SubscriptionContext ctx) {
48+
isMapTypeCheck = ctx.typeChecker().typeCheckBuilder().isTypeWithName("map");
49+
}
50+
51+
private void check(SubscriptionContext ctx) {
52+
var callExpression = (CallExpression) ctx.syntaxNode();
53+
if (isMapCall(callExpression)) {
54+
TreeUtils.nthArgumentOrKeywordOptional(0, "", callExpression.arguments())
55+
.map(RegularArgument::expression)
56+
.ifPresent(argumentExpression -> {
57+
if (isLambda(argumentExpression) || isAssignedToLambda(argumentExpression)) {
58+
ctx.addIssue(callExpression, "Replace this map call with a comprehension.");
59+
}
60+
});
61+
}
62+
}
63+
64+
private boolean isMapCall(CallExpression callExpression) {
65+
return isMapTypeCheck.check(callExpression.callee().typeV2()) == TriBool.TRUE;
66+
}
67+
68+
private static boolean isLambda(@Nullable Expression argumentExpression) {
69+
return argumentExpression instanceof LambdaExpression;
70+
}
71+
72+
private static boolean isAssignedToLambda(Expression argumentExpression) {
73+
return argumentExpression instanceof Name name
74+
&& getUsageCount(name) == 2
75+
&& isLambda(Expressions.singleAssignedValue(name));
76+
}
77+
78+
private static int getUsageCount(Name name) {
79+
return Optional.ofNullable(name.symbolV2())
80+
.map(SymbolV2::usages)
81+
.map(List::size)
82+
.orElse(0);
83+
}
84+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<p>This rule raises an issue when a collection is created with the use of a <code>lambda</code> function inside of <code>map()</code>.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>The <code>map()</code> function applies a given function to each item of an iterable. When this function is a <code>lambda</code>, especially a
4+
simple one, the resulting code can sometimes be less readable than its comprehension or generator expression equivalent. For example: A comprehension
5+
like <code>[x * 2 for x in nums]</code> is more straightforward to read and understand at a glance than <code>list(map(lambda x: x * 2, nums))</code>.
6+
The logic is more self-contained and doesn’t require mentally parsing the <code>map</code> and <code>lambda</code> separately.</p>
7+
<p>Using <code>map()</code> could also have an impact on performance. While <code>map()</code> can be very efficient when used with built-in functions
8+
or pre-defined functions written in C, the use of a Python <code>lambda</code> introduces function call overhead for each element in the iterable.</p>
9+
<h2>How to fix it</h2>
10+
<p>To fix this issue replace the <code>map(lambda …​)</code> construct with an equivalent generator expression or a list, set, or dictionary
11+
comprehension.</p>
12+
<ul>
13+
<li> Replace <code>map(lambda var: expression, iterable)</code> with the generator expression <code>(expression for var in iterable)</code>. </li>
14+
<li> Replace <code>list(map(lambda var: expression, iterable))</code> with the list comprehension <code>[expression for var in iterable]</code>.
15+
</li>
16+
<li> Replace <code>set(map(lambda var: expression, iterable))</code> with the set comprehension <code>{expression for var in iterable}</code>. </li>
17+
<li> Replace <code>dict(map(lambda var: (key_expr, value_expr), iterable))</code> with the dictionary comprehension <code>{key_expr: value_expr for
18+
var in iterable}</code> . </li>
19+
</ul>
20+
<pre data-diff-id="1" data-diff-type="noncompliant">
21+
nums = [1, 2, 3, 4]
22+
23+
list_map = list(map(lambda num: num * 2, nums)) # Noncompliant: the map and lambda function can be replaced by a list-comprehension
24+
</pre>
25+
<pre data-diff-id="1" data-diff-type="compliant">
26+
nums = [1, 2, 3, 4]
27+
28+
list_comp = [num * 2 for num in nums] # Compliant
29+
</pre>
30+
<h2>Resources</h2>
31+
<h3>Documentation</h3>
32+
<ul>
33+
<li> Python Documentation - <a href="https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions">List Comprehensions</a> </li>
34+
<li> Python Documentation - <a href="https://docs.python.org/3/tutorial/classes.html#generator-expressions">Generator Expressions</a> </li>
35+
<li> Python Documentation - <a href="https://docs.python.org/3/library/functions.html#map">map()</a> </li>
36+
<li> Python Documentation - <a href="https://docs.python.org/3/reference/expressions.html#lambda">Lambdas</a> </li>
37+
</ul>
38+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"title": "Generators and comprehensions should be preferred over the usage of \"map\" and \"lambda\" when creating collection",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [],
10+
"defaultSeverity": "Major",
11+
"ruleSpecification": "RSPEC-7505",
12+
"sqKey": "S7505",
13+
"scope": "All",
14+
"quickfix": "infeasible",
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
@@ -270,6 +270,7 @@
270270
"S7501",
271271
"S7502",
272272
"S7503",
273+
"S7505",
273274
"S7506",
274275
"S7507",
275276
"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 UnnecessaryLambdaMapCallCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/unnecessaryLambdaMapCall.py", new UnnecessaryLambdaMapCallCheck());
27+
}
28+
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
def case1():
3+
nums = [1, 2, 3, 4]
4+
gen_map = map(lambda x: x + 1, nums) # Noncompliant
5+
l = lambda x: x + 1
6+
gen_map = map(l, nums) # Noncompliant
7+
8+
9+
def case2():
10+
nums = [1, 2, 3, 4]
11+
gen_map = not_a_map_call(lambda x: x + 1, nums)
12+
13+
def case3():
14+
nums = [1, 2, 3, 4]
15+
l = not_a_lambda
16+
gen_map = map(l, nums)
17+
18+
def case4():
19+
gen_map = map()
20+
21+
def case5():
22+
nums = [1, 2, 3, 4]
23+
l = lambda x: x + 1
24+
usage_of_lambda(l)
25+
gen_map = map(l, nums)
26+
27+
def case5():
28+
nums = [1, 2, 3, 4]
29+
l = "not a lambda with more usages"
30+
x = l
31+
gen_map = map(l, nums)

0 commit comments

Comments
 (0)