Skip to content

Commit 02bd1da

Browse files
SONARPY-2008 Infer types for comprehensions (#1866)
1 parent 6b0e9be commit 02bd1da

File tree

12 files changed

+113
-8
lines changed

12 files changed

+113
-8
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.sonar.plugins.python.api.tree.ComprehensionExpression;
4444
import org.sonar.plugins.python.api.tree.ComprehensionIf;
4545
import org.sonar.plugins.python.api.tree.ConditionalExpression;
46+
import org.sonar.plugins.python.api.tree.DictCompExpression;
4647
import org.sonar.plugins.python.api.tree.Expression;
4748
import org.sonar.plugins.python.api.tree.ExpressionList;
4849
import org.sonar.plugins.python.api.tree.FunctionDef;
@@ -194,7 +195,7 @@ public void visitComprehensionIf(ComprehensionIf tree) {
194195
}
195196

196197
@Override
197-
public void visitDictCompExpression(DictCompExpressionImpl tree) {
198+
public void visitDictCompExpression(DictCompExpression tree) {
198199
// ignore, not broken down in the cfg
199200
}
200201

python-frontend/src/main/java/org/sonar/plugins/python/api/tree/BaseTreeVisitor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ public void visitKeyValuePattern(KeyValuePattern keyValuePattern) {
431431
}
432432

433433
@Override
434-
public void visitDictCompExpression(DictCompExpressionImpl tree) {
434+
public void visitDictCompExpression(DictCompExpression tree) {
435435
scan(tree.keyExpression());
436436
scan(tree.valueExpression());
437437
scan(tree.comprehensionFor());

python-frontend/src/main/java/org/sonar/plugins/python/api/tree/TreeVisitor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public interface TreeVisitor {
157157

158158
void visitKeyValuePattern(KeyValuePattern keyValuePattern);
159159

160-
void visitDictCompExpression(DictCompExpressionImpl dictCompExpression);
160+
void visitDictCompExpression(DictCompExpression dictCompExpression);
161161

162162
void visitCompoundAssignment(CompoundAssignmentStatement compoundAssignmentStatement);
163163

python-frontend/src/main/java/org/sonar/python/semantic/SymbolTableBuilder.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.sonar.plugins.python.api.tree.ComprehensionExpression;
5454
import org.sonar.plugins.python.api.tree.ComprehensionFor;
5555
import org.sonar.plugins.python.api.tree.Decorator;
56+
import org.sonar.plugins.python.api.tree.DictCompExpression;
5657
import org.sonar.plugins.python.api.tree.DottedName;
5758
import org.sonar.plugins.python.api.tree.ExceptClause;
5859
import org.sonar.plugins.python.api.tree.Expression;
@@ -294,7 +295,7 @@ public void visitLambda(LambdaExpression pyLambdaExpressionTree) {
294295
}
295296

296297
@Override
297-
public void visitDictCompExpression(DictCompExpressionImpl tree) {
298+
public void visitDictCompExpression(DictCompExpression tree) {
298299
createScope(tree, currentScope());
299300
enterScope(tree);
300301
super.visitDictCompExpression(tree);
@@ -662,7 +663,7 @@ public void visitPyListOrSetCompExpression(ComprehensionExpression tree) {
662663
}
663664

664665
@Override
665-
public void visitDictCompExpression(DictCompExpressionImpl tree) {
666+
public void visitDictCompExpression(DictCompExpression tree) {
666667
enterScope(tree);
667668
scan(tree.keyExpression());
668669
scan(tree.valueExpression());

python-frontend/src/main/java/org/sonar/python/semantic/v2/ReadUsagesVisitor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.sonar.plugins.python.api.tree.ClassDef;
2424
import org.sonar.plugins.python.api.tree.ComprehensionExpression;
2525
import org.sonar.plugins.python.api.tree.ComprehensionFor;
26+
import org.sonar.plugins.python.api.tree.DictCompExpression;
2627
import org.sonar.plugins.python.api.tree.FileInput;
2728
import org.sonar.plugins.python.api.tree.FunctionDef;
2829
import org.sonar.plugins.python.api.tree.LambdaExpression;
@@ -91,7 +92,7 @@ public void visitPyListOrSetCompExpression(ComprehensionExpression tree) {
9192
}
9293

9394
@Override
94-
public void visitDictCompExpression(DictCompExpressionImpl tree) {
95+
public void visitDictCompExpression(DictCompExpression tree) {
9596
enterScope(tree);
9697
scan(tree.keyExpression());
9798
scan(tree.valueExpression());

python-frontend/src/main/java/org/sonar/python/semantic/v2/WriteUsagesVisitor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.sonar.plugins.python.api.tree.ComprehensionExpression;
3939
import org.sonar.plugins.python.api.tree.ComprehensionFor;
4040
import org.sonar.plugins.python.api.tree.Decorator;
41+
import org.sonar.plugins.python.api.tree.DictCompExpression;
4142
import org.sonar.plugins.python.api.tree.DottedName;
4243
import org.sonar.plugins.python.api.tree.ExceptClause;
4344
import org.sonar.plugins.python.api.tree.FileInput;
@@ -87,7 +88,7 @@ public void visitLambda(LambdaExpression lambdaExpression) {
8788
}
8889

8990
@Override
90-
public void visitDictCompExpression(DictCompExpressionImpl tree) {
91+
public void visitDictCompExpression(DictCompExpression tree) {
9192
createAndEnterScope(tree, currentScope());
9293
super.visitDictCompExpression(tree);
9394
leaveScope();

python-frontend/src/main/java/org/sonar/python/semantic/v2/types/TrivialTypeInferenceVisitor.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
3434
import org.sonar.plugins.python.api.tree.BinaryExpression;
3535
import org.sonar.plugins.python.api.tree.ClassDef;
36+
import org.sonar.plugins.python.api.tree.ComprehensionExpression;
37+
import org.sonar.plugins.python.api.tree.DictCompExpression;
3638
import org.sonar.plugins.python.api.tree.DictionaryLiteral;
3739
import org.sonar.plugins.python.api.tree.DottedName;
3840
import org.sonar.plugins.python.api.tree.Expression;
@@ -60,6 +62,8 @@
6062
import org.sonar.python.semantic.v2.ProjectLevelTypeTable;
6163
import org.sonar.python.semantic.v2.SymbolV2;
6264
import org.sonar.python.semantic.v2.UsageV2;
65+
import org.sonar.python.tree.ComprehensionExpressionImpl;
66+
import org.sonar.python.tree.DictCompExpressionImpl;
6367
import org.sonar.python.tree.DictionaryLiteralImpl;
6468
import org.sonar.python.tree.ListLiteralImpl;
6569
import org.sonar.python.tree.NameImpl;
@@ -177,6 +181,26 @@ public void visitListLiteral(ListLiteral listLiteral) {
177181
((ListLiteralImpl) listLiteral).typeV2(new ObjectType(listType, attributes, new ArrayList<>()));
178182
}
179183

184+
@Override
185+
public void visitPyListOrSetCompExpression(ComprehensionExpression comprehensionExpression) {
186+
super.visitPyListOrSetCompExpression(comprehensionExpression);
187+
var builtins = this.projectLevelTypeTable.getModule();
188+
var pythonType = switch (comprehensionExpression.getKind()) {
189+
case LIST_COMPREHENSION -> builtins.resolveMember("list").orElse(PythonType.UNKNOWN);
190+
case SET_COMPREHENSION -> builtins.resolveMember("set").orElse(PythonType.UNKNOWN);
191+
default -> PythonType.UNKNOWN;
192+
};
193+
((ComprehensionExpressionImpl) comprehensionExpression).typeV2(new ObjectType(pythonType, new ArrayList<>(), new ArrayList<>()));
194+
}
195+
196+
@Override
197+
public void visitDictCompExpression(DictCompExpression dictCompExpression) {
198+
super.visitDictCompExpression(dictCompExpression);
199+
var builtins = this.projectLevelTypeTable.getModule();
200+
var dictType = builtins.resolveMember("dict").orElse(PythonType.UNKNOWN);
201+
((DictCompExpressionImpl) dictCompExpression).typeV2(new ObjectType(dictType, new ArrayList<>(), new ArrayList<>()));
202+
}
203+
180204
@Override
181205
public void visitClassDef(ClassDef classDef) {
182206
scan(classDef.args());

python-frontend/src/main/java/org/sonar/python/tree/ComprehensionExpressionImpl.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.sonar.plugins.python.api.tree.TreeVisitor;
3535
import org.sonar.plugins.python.api.types.InferredType;
3636
import org.sonar.python.types.InferredTypes;
37+
import org.sonar.python.types.v2.PythonType;
3738

3839
public class ComprehensionExpressionImpl extends PyTree implements ComprehensionExpression {
3940

@@ -43,6 +44,7 @@ public class ComprehensionExpressionImpl extends PyTree implements Comprehension
4344
private final ComprehensionFor comprehensionFor;
4445
private final Token closingToken;
4546
private Set<Symbol> symbols = new HashSet<>();
47+
private PythonType pythonType;
4648

4749
public ComprehensionExpressionImpl(Kind kind, @Nullable Token openingToken, Expression resultExpression,
4850
ComprehensionFor compFor, @Nullable Token closingToken) {
@@ -99,4 +101,13 @@ public InferredType type() {
99101
return InferredTypes.anyType();
100102
}
101103
}
104+
105+
@Override
106+
public PythonType typeV2() {
107+
return this.pythonType;
108+
}
109+
110+
public void typeV2(PythonType type) {
111+
this.pythonType = type;
112+
}
102113
}

python-frontend/src/main/java/org/sonar/python/tree/DictCompExpressionImpl.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.sonar.plugins.python.api.tree.TreeVisitor;
3434
import org.sonar.plugins.python.api.types.InferredType;
3535
import org.sonar.python.types.InferredTypes;
36+
import org.sonar.python.types.v2.PythonType;
3637

3738
public class DictCompExpressionImpl extends PyTree implements DictCompExpression {
3839

@@ -43,6 +44,7 @@ public class DictCompExpressionImpl extends PyTree implements DictCompExpression
4344
private final ComprehensionFor comprehensionFor;
4445
private final Token closingBrace;
4546
private Set<Symbol> symbols = new HashSet<>();
47+
private PythonType pythonType;
4648

4749
public DictCompExpressionImpl(Token openingBrace, Expression keyExpression, Token colon, Expression valueExpression,
4850
ComprehensionFor compFor, Token closingBrace) {
@@ -102,4 +104,13 @@ public void addLocalVariableSymbol(Symbol symbol) {
102104
public InferredType type() {
103105
return InferredTypes.DICT;
104106
}
107+
108+
@Override
109+
public PythonType typeV2() {
110+
return this.pythonType;
111+
}
112+
113+
public void typeV2(PythonType type) {
114+
this.pythonType = type;
115+
}
105116
}

python-frontend/src/test/java/org/sonar/python/semantic/v2/TypeInferenceV2Test.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,12 @@
6262

6363
import static org.assertj.core.api.Assertions.assertThat;
6464
import static org.sonar.python.PythonTestUtils.parse;
65+
import static org.sonar.python.types.v2.TypesTestUtils.DICT_TYPE;
6566
import static org.sonar.python.types.v2.TypesTestUtils.INT_TYPE;
6667
import static org.sonar.python.types.v2.TypesTestUtils.LIST_TYPE;
6768
import static org.sonar.python.types.v2.TypesTestUtils.NONE_TYPE;
6869
import static org.sonar.python.types.v2.TypesTestUtils.PROJECT_LEVEL_TYPE_TABLE;
70+
import static org.sonar.python.types.v2.TypesTestUtils.SET_TYPE;
6971
import static org.sonar.python.types.v2.TypesTestUtils.STR_TYPE;
7072

7173
class TypeInferenceV2Test {
@@ -1679,6 +1681,56 @@ def reassigned_param(a, param):
16791681
Assertions.assertThat(paramType).isSameAs(PythonType.UNKNOWN);
16801682
}
16811683

1684+
@Test
1685+
void list_comprehension() {
1686+
assertThat(lastExpression(
1687+
"""
1688+
x = [a for a in foo()]
1689+
x
1690+
"""
1691+
).typeV2().unwrappedType()).isEqualTo(LIST_TYPE);
1692+
}
1693+
1694+
@Test
1695+
void set_comprehension() {
1696+
assertThat(lastExpression(
1697+
"""
1698+
x = {a for a in foo()}
1699+
x
1700+
"""
1701+
).typeV2().unwrappedType()).isEqualTo(SET_TYPE);
1702+
}
1703+
1704+
@Test
1705+
void dict_comprehension() {
1706+
assertThat(lastExpression(
1707+
"""
1708+
x = {num: num**2 for num in numbers()}
1709+
x
1710+
"""
1711+
).typeV2().unwrappedType()).isEqualTo(DICT_TYPE);
1712+
}
1713+
1714+
@Test
1715+
void comprehension_if() {
1716+
assertThat(lastExpression(
1717+
"""
1718+
x = [num for num in numbers if num % 2 == 0]
1719+
x
1720+
"""
1721+
).typeV2().unwrappedType()).isEqualTo(LIST_TYPE);
1722+
}
1723+
1724+
@Test
1725+
void generator_expression() {
1726+
assertThat(lastExpression(
1727+
"""
1728+
x = (num**2 for num in numbers())
1729+
x
1730+
"""
1731+
).typeV2().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
1732+
}
1733+
16821734
@Test
16831735
void return_type_of_call_expression_1() {
16841736
assertThat(lastExpression(

0 commit comments

Comments
 (0)