Skip to content

Commit 6be593a

Browse files
SONARPY-1836 Infer item type for basic loop over list iteration (#1805)
1 parent af4844d commit 6be593a

File tree

7 files changed

+438
-5
lines changed

7 files changed

+438
-5
lines changed

python-checks/src/test/resources/checks/nonCallableCalled.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,42 @@ def nested_function_in_try_catch():
310310
...
311311
def bar():
312312
foo()
313+
314+
315+
316+
def f1():
317+
...
318+
319+
def f2():
320+
...
321+
322+
def callable_from_loop_try_except():
323+
l = [f1, f2]
324+
try:
325+
for i in l:
326+
i()
327+
except:
328+
...
329+
330+
def non_callable_from_loop_try_except():
331+
l = ["f1", "f2"]
332+
try:
333+
for i in l:
334+
i() # Noncompliant
335+
except:
336+
...
337+
338+
339+
def callable_from_loop_append_noncallable():
340+
l = [f1, f2]
341+
l.append("1")
342+
for i in l:
343+
i() # FN
344+
345+
346+
def callable_from_loop_append_noncallable():
347+
l = ["1"]
348+
possible_modiffication(l)
349+
for i in l:
350+
# FP
351+
i() # Noncompliant

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.sonar.plugins.python.api.tree.AssignmentStatement;
2828
import org.sonar.plugins.python.api.tree.CompoundAssignmentStatement;
2929
import org.sonar.plugins.python.api.tree.Expression;
30+
import org.sonar.plugins.python.api.tree.ForStatement;
3031
import org.sonar.plugins.python.api.tree.FunctionDef;
3132
import org.sonar.plugins.python.api.tree.Name;
3233
import org.sonar.plugins.python.api.tree.Parameter;
@@ -88,6 +89,8 @@ public void updateProgramState(Tree element, ProgramState programState) {
8889
handleDefinition(functionDef, state);
8990
} else if (element instanceof Parameter parameter) {
9091
handleParameter(parameter, state);
92+
} else if (isForLoopAssignment(element)) {
93+
handleLoopAssignment(element, state);
9194
} else {
9295
// Here we should run "isinstance" visitor when we handle declared types, to avoid FPs when type guard checks are made
9396
updateTree(element, state);
@@ -110,6 +113,27 @@ private static void updateTree(Tree tree, TypeInferenceProgramState state) {
110113
tree.accept(new ProgramStateTypeInferenceVisitor(state));
111114
}
112115

116+
117+
private static boolean isForLoopAssignment(Tree tree) {
118+
return tree instanceof Name && tree.parent() instanceof ForStatement forStatement && forStatement.expressions().contains(tree);
119+
}
120+
121+
private void handleLoopAssignment(Tree element, TypeInferenceProgramState state) {
122+
Optional.of(element)
123+
.map(Tree::parent)
124+
.filter(ForStatement.class::isInstance)
125+
.map(ForStatement.class::cast)
126+
.ifPresent(forStatement -> {
127+
forStatement.testExpressions().forEach(t -> updateTree(t, state));
128+
Optional.ofNullable(assignmentsByAssignmentStatement.get(forStatement))
129+
.filter(assignment -> trackedVars.contains(assignment.lhsSymbol()))
130+
.ifPresent(assignment -> Optional.of(assignment)
131+
.map(Assignment::rhsType)
132+
.ifPresent(collectionItemType -> state.setTypes(assignment.lhsSymbol(), Set.of(collectionItemType)))
133+
);
134+
});
135+
}
136+
113137
private void handleAssignment(Statement assignmentStatement, TypeInferenceProgramState programState) {
114138
Optional.ofNullable(assignmentsByAssignmentStatement.get(assignmentStatement))
115139
.ifPresent(assignment -> {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2024 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 GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.semantic.v2.types;
21+
22+
import java.util.Collection;
23+
import java.util.Map;
24+
import java.util.Optional;
25+
import java.util.Set;
26+
import java.util.stream.Stream;
27+
import org.sonar.plugins.python.api.tree.Expression;
28+
import org.sonar.plugins.python.api.tree.Name;
29+
import org.sonar.python.semantic.v2.SymbolV2;
30+
import org.sonar.python.types.v2.ObjectType;
31+
import org.sonar.python.types.v2.PythonType;
32+
import org.sonar.python.types.v2.TriBool;
33+
34+
public class LoopAssignment extends Assignment {
35+
public LoopAssignment(SymbolV2 lhsSymbol, Name lhsName, Expression rhs, Map<SymbolV2, Set<Propagation>> propagationsByLhs) {
36+
super(lhsSymbol, lhsName, rhs, propagationsByLhs);
37+
}
38+
39+
@Override
40+
public PythonType rhsType() {
41+
return Optional.of(super.rhsType())
42+
.filter(ObjectType.class::isInstance)
43+
.map(ObjectType.class::cast)
44+
.filter(t -> t.hasMember("__iter__") == TriBool.TRUE)
45+
.map(ObjectType::attributes)
46+
.map(Collection::stream)
47+
.flatMap(Stream::findFirst)
48+
.orElse(PythonType.UNKNOWN);
49+
}
50+
}

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@
3131
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
3232
import org.sonar.plugins.python.api.tree.CompoundAssignmentStatement;
3333
import org.sonar.plugins.python.api.tree.Expression;
34+
import org.sonar.plugins.python.api.tree.ForStatement;
3435
import org.sonar.plugins.python.api.tree.FunctionDef;
3536
import org.sonar.plugins.python.api.tree.Name;
3637
import org.sonar.plugins.python.api.tree.Parameter;
3738
import org.sonar.plugins.python.api.tree.Statement;
3839
import org.sonar.python.semantic.v2.SymbolV2;
40+
import org.sonar.python.tree.NameImpl;
3941

4042
public class PropagationVisitor extends BaseTreeVisitor {
4143
private final Map<SymbolV2, Set<Propagation>> propagationsByLhs;
@@ -111,10 +113,35 @@ public void visitAnnotatedAssignment(AnnotatedAssignment annotatedAssignment){
111113
}
112114
}
113115

116+
@Override
117+
public void visitForStatement(ForStatement forStatement) {
118+
scan(forStatement.testExpressions());
119+
if (forStatement.testExpressions().size() == 1 && forStatement.expressions().size() == 1) {
120+
forStatement
121+
.testExpressions()
122+
.stream()
123+
.findFirst()
124+
.ifPresent(rhsExpression -> forStatement.expressions().stream()
125+
.findFirst()
126+
.filter(NameImpl.class::isInstance)
127+
.map(NameImpl.class::cast)
128+
.ifPresent(i -> {
129+
var symbol = i.symbolV2();
130+
var assignment = new LoopAssignment(symbol, i, rhsExpression, propagationsByLhs);
131+
assignmentsByAssignmentStatement.put(forStatement, assignment);
132+
propagationsByLhs.computeIfAbsent(symbol, s -> new HashSet<>()).add(assignment);
133+
})
134+
);
135+
136+
}
137+
scan(forStatement.body());
138+
scan(forStatement.elseClause());
139+
}
140+
114141
private void processAssignment(Statement assignmentStatement, Expression lhsExpression, Expression rhsExpression){
115142
if (lhsExpression instanceof Name lhs && lhs.symbolV2() != null) {
116143
var symbol = lhs.symbolV2();
117-
Assignment assignment = new Assignment(symbol, lhs, rhsExpression, propagationsByLhs);
144+
var assignment = new Assignment(symbol, lhs, rhsExpression, propagationsByLhs);
118145
assignmentsByAssignmentStatement.put(assignmentStatement, assignment);
119146
propagationsByLhs.computeIfAbsent(symbol, s -> new HashSet<>()).add(assignment);
120147
}

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
import org.sonar.python.types.v2.ModuleType;
7272
import org.sonar.python.types.v2.ObjectType;
7373
import org.sonar.python.types.v2.PythonType;
74+
import org.sonar.python.types.v2.UnionType;
7475

7576
import static org.sonar.python.semantic.SymbolUtils.pathOf;
7677
import static org.sonar.python.tree.TreeUtils.locationInFile;
@@ -153,11 +154,22 @@ public void visitNone(NoneExpression noneExpression) {
153154

154155
@Override
155156
public void visitListLiteral(ListLiteral listLiteral) {
156-
ModuleType builtins = this.projectLevelTypeTable.getModule();
157+
var builtins = this.projectLevelTypeTable.getModule();
157158
scan(listLiteral.elements());
158-
List<PythonType> pythonTypes = listLiteral.elements().expressions().stream().map(Expression::typeV2).distinct().toList();
159+
160+
var candidateTypes = listLiteral.elements()
161+
.expressions()
162+
.stream()
163+
.map(Expression::typeV2)
164+
.distinct()
165+
.toList();
166+
167+
var elementsType = UnionType.or(candidateTypes);
168+
169+
var attributes = new ArrayList<PythonType>();
170+
attributes.add(elementsType);
159171
PythonType listType = builtins.resolveMember("list").orElse(PythonType.UNKNOWN);
160-
((ListLiteralImpl) listLiteral).typeV2(new ObjectType(listType, pythonTypes, new ArrayList<>()));
172+
((ListLiteralImpl) listLiteral).typeV2(new ObjectType(listType, attributes, new ArrayList<>()));
161173
}
162174

163175
@Override

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.sonar.python.types.v2;
2121

2222
import java.util.ArrayList;
23+
import java.util.Collection;
2324
import java.util.HashSet;
2425
import java.util.List;
2526
import java.util.Optional;
@@ -60,6 +61,12 @@ public boolean isCompatibleWith(PythonType another) {
6061
.anyMatch(candidate -> candidate.isCompatibleWith(another));
6162
}
6263

64+
public static PythonType or(Collection<PythonType> candidates) {
65+
return candidates
66+
.stream()
67+
.reduce(new UnionType(new HashSet<>()), UnionType::or);
68+
}
69+
6370
public static PythonType or(PythonType type1, PythonType type2) {
6471
if (type1.equals(PythonType.UNKNOWN) || type2.equals(PythonType.UNKNOWN)) {
6572
return PythonType.UNKNOWN;
@@ -70,6 +77,9 @@ public static PythonType or(PythonType type1, PythonType type2) {
7077
Set<PythonType> types = new HashSet<>();
7178
addTypes(type1, types);
7279
addTypes(type2, types);
80+
if (types.size() == 1) {
81+
return types.iterator().next();
82+
}
7383
return new UnionType(types);
7484
}
7585

0 commit comments

Comments
 (0)