Skip to content

Commit 1f1af46

Browse files
SONARPY-1818 Enable flow-sensitive type inference within functions (#1791)
1 parent c09c9b7 commit 1f1af46

File tree

9 files changed

+246
-26
lines changed

9 files changed

+246
-26
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def call_noncallable(p):
4040
x = 42
4141
else:
4242
x = 'str'
43-
x() # FN: multiple assignment not handled
43+
x() # Noncompliant
4444

4545

4646
def call_no_name():
@@ -49,12 +49,12 @@ def call_no_name():
4949
def flow_sensitivity():
5050
my_var = "hello"
5151
my_var = 42
52-
my_var() # FN: multiple assignment not handled
52+
my_var() # Noncompliant
5353

5454
my_other_var = func
5555
my_other_var() # OK
5656
my_other_var = 42
57-
my_other_var() # FN: multiple assignment not handled
57+
my_other_var() # Noncompliant
5858

5959
def flow_sensitivity_nested_try_except():
6060
def func_with_try_except():
@@ -66,7 +66,7 @@ def func_with_try_except():
6666
def other_func():
6767
my_var = "hello"
6868
my_var = 42
69-
my_var() # FN: multiple assignments
69+
my_var() # Noncompliant
7070

7171
def member_access():
7272
my_callable = MyCallable()

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,19 @@
2828
import java.util.stream.Collectors;
2929
import org.sonar.plugins.python.api.PythonFile;
3030
import org.sonar.plugins.python.api.cfg.ControlFlowGraph;
31+
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
3132
import org.sonar.plugins.python.api.tree.FileInput;
33+
import org.sonar.plugins.python.api.tree.FunctionDef;
3234
import org.sonar.plugins.python.api.tree.Name;
35+
import org.sonar.plugins.python.api.tree.Parameter;
3336
import org.sonar.plugins.python.api.tree.StatementList;
3437
import org.sonar.plugins.python.api.tree.Tree;
3538
import org.sonar.python.semantic.v2.types.Assignment;
3639
import org.sonar.python.semantic.v2.types.FlowSensitiveTypeInference;
3740
import org.sonar.python.semantic.v2.types.PropagationVisitor;
3841
import org.sonar.python.semantic.v2.types.TrivialTypeInferenceVisitor;
3942
import org.sonar.python.semantic.v2.types.TryStatementVisitor;
43+
import org.sonar.python.tree.TreeUtils;
4044

4145
public class TypeInferenceV2 {
4246

@@ -55,6 +59,14 @@ public void inferTypes(FileInput fileInput) {
5559
fileInput.accept(trivialTypeInferenceVisitor);
5660

5761
inferTypesAndMemberAccessSymbols(fileInput);
62+
63+
fileInput.accept(new BaseTreeVisitor() {
64+
@Override
65+
public void visitFunctionDef(FunctionDef funcDef) {
66+
super.visitFunctionDef(funcDef);
67+
inferTypesAndMemberAccessSymbols(funcDef);
68+
}
69+
});
5870
}
5971

6072

@@ -74,6 +86,22 @@ private void inferTypesAndMemberAccessSymbols(FileInput fileInput) {
7486
);
7587
}
7688

89+
private void inferTypesAndMemberAccessSymbols(FunctionDef functionDef) {
90+
Set<Name> parameterNames = TreeUtils.nonTupleParameters(functionDef).stream()
91+
// TODO: it probably doesn't make sense to restrict to annotated parameters here
92+
.filter(parameter -> parameter.typeAnnotation() != null)
93+
.map(Parameter::name)
94+
.collect(Collectors.toSet());
95+
Set<SymbolV2> localVariables = symbolTable.getSymbolsByRootTree(functionDef);
96+
inferTypesAndMemberAccessSymbols(
97+
functionDef,
98+
functionDef.body(),
99+
localVariables,
100+
parameterNames,
101+
() -> ControlFlowGraph.build(functionDef, pythonFile)
102+
);
103+
}
104+
77105

78106
private static void inferTypesAndMemberAccessSymbols(Tree scopeTree,
79107
StatementList statements,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,9 @@ public boolean equals(Object o) {
184184
public int hashCode() {
185185
return Objects.hash(name, members, attributes, superClasses);
186186
}
187+
188+
@Override
189+
public String toString() {
190+
return "ClassType[%s]".formatted(name);
191+
}
187192
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,10 @@ public Optional<LocationInFile> definitionLocation() {
7070
public int hashCode() {
7171
return Objects.hash(name, attributes, parameters, returnType, isAsynchronous, hasDecorators, isInstanceMethod, hasVariadicParameter);
7272
}
73+
74+
75+
@Override
76+
public String toString() {
77+
return "FunctionType[%s]".formatted(name);
78+
}
7379
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ public Optional<String> displayName() {
4242
return Optional.of(name);
4343
}
4444

45+
46+
/**
47+
* For UnionType, hasMember will return true if all alternatives have the member
48+
* It will return false if all alternatives DON'T have the member
49+
* It will return unknown in all other cases
50+
*/
51+
@Override
52+
public TriBool hasMember(String memberName) {
53+
Set<TriBool> uniqueResult = candidates.stream().map(c -> c.hasMember(memberName)).collect(Collectors.toSet());
54+
return uniqueResult.size() == 1 ? uniqueResult.iterator().next() : TriBool.UNKNOWN;
55+
}
56+
4557
@Override
4658
public boolean isCompatibleWith(PythonType another) {
4759
return candidates.isEmpty() || candidates.stream()

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

Lines changed: 165 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,13 @@
1919
*/
2020
package org.sonar.python.semantic.v2;
2121

22-
import java.io.File;
2322
import java.util.Comparator;
2423
import java.util.HashMap;
2524
import java.util.HashSet;
2625
import java.util.List;
2726
import java.util.Map;
2827
import java.util.Set;
2928
import org.assertj.core.api.Assertions;
30-
import org.junit.jupiter.api.BeforeAll;
3129
import org.junit.jupiter.api.Disabled;
3230
import org.junit.jupiter.api.Test;
3331
import org.sonar.plugins.python.api.PythonFile;
@@ -45,7 +43,6 @@
4543
import org.sonar.plugins.python.api.tree.StatementList;
4644
import org.sonar.plugins.python.api.tree.Tree;
4745
import org.sonar.python.PythonTestUtils;
48-
import org.sonar.python.TestPythonVisitorRunner;
4946
import org.sonar.python.semantic.ClassSymbolImpl;
5047
import org.sonar.python.semantic.ProjectLevelSymbolTable;
5148
import org.sonar.python.tree.TreeUtils;
@@ -63,26 +60,9 @@
6360
import static org.sonar.python.types.v2.TypesTestUtils.STR_TYPE;
6461

6562
class TypeInferenceV2Test {
66-
private static FileInput fileInput;
6763

6864
static PythonFile pythonFile = PythonTestUtils.pythonFile("");
6965

70-
@BeforeAll
71-
static void init() {
72-
var context = TestPythonVisitorRunner.createContext(new File("src/test/resources/semantic/v2/script.py"));
73-
fileInput = context.rootTree();
74-
}
75-
76-
@Test
77-
void test() {
78-
var pythonFile = PythonTestUtils.pythonFile("script.py");
79-
var symbolTable = new SymbolTableBuilderV2(fileInput)
80-
.build();
81-
new TypeInferenceV2(new ProjectLevelTypeTable(ProjectLevelSymbolTable.empty()), pythonFile, symbolTable).inferTypes(fileInput);
82-
83-
System.out.println("hello");
84-
}
85-
8666
@Test
8767
void testTypeshedImports() {
8868
FileInput root = inferTypes("""
@@ -263,7 +243,114 @@ void inferTypeForBuiltins() {
263243
}
264244

265245
@Test
266-
void inferTypeForReassignedBuiltins() {
246+
@Disabled("Single assigned is approximated for now")
247+
void inferTypesInsideFunction1() {
248+
FileInput root = inferTypes("""
249+
x = 42
250+
def foo():
251+
x
252+
""");
253+
254+
var functionDef = (FunctionDef) root.statements().statements().get(1);
255+
var lastExpressionStatement = (ExpressionStatement) functionDef.body().statements().get(functionDef.body().statements().size() -1);
256+
Assertions.assertThat(lastExpressionStatement.expressions().get(0).typeV2().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
257+
}
258+
259+
@Test
260+
void inferTypesInsideFunction2() {
261+
FileInput root = inferTypes("""
262+
x = 42
263+
def foo():
264+
x
265+
x = "hello"
266+
""");
267+
268+
var functionDef = (FunctionDef) root.statements().statements().get(1);
269+
var lastExpressionStatement = (ExpressionStatement) functionDef.body().statements().get(functionDef.body().statements().size() -1);
270+
Assertions.assertThat(lastExpressionStatement.expressions().get(0).typeV2().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
271+
}
272+
273+
@Test
274+
void inferTypesInsideFunction3() {
275+
FileInput root = inferTypes("""
276+
x = "hello"
277+
def foo():
278+
x = 42
279+
x
280+
x = "world"
281+
""");
282+
283+
var functionDef = (FunctionDef) root.statements().statements().get(1);
284+
var lastExpressionStatement = (ExpressionStatement) functionDef.body().statements().get(functionDef.body().statements().size() -1);
285+
Assertions.assertThat(lastExpressionStatement.expressions().get(0).typeV2().unwrappedType()).isEqualTo(INT_TYPE);
286+
}
287+
288+
@Test
289+
void inferTypesInsideFunction4() {
290+
FileInput root = inferTypes("""
291+
def foo():
292+
x = 42
293+
x
294+
""");
295+
296+
var lastExpressionStatement = (ExpressionStatement) root.statements().statements().get(root.statements().statements().size() -1);
297+
Assertions.assertThat(lastExpressionStatement.expressions().get(0).typeV2().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
298+
}
299+
300+
@Test
301+
void inferTypesInsideFunction5() {
302+
FileInput root = inferTypes("""
303+
def foo(param: int):
304+
param
305+
""");
306+
307+
var functionDef = (FunctionDef) root.statements().statements().get(0);
308+
var lastExpressionStatement = (ExpressionStatement) functionDef.body().statements().get(functionDef.body().statements().size() -1);
309+
// TODO: should be declared int
310+
Assertions.assertThat(lastExpressionStatement.expressions().get(0).typeV2().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
311+
}
312+
313+
@Test
314+
void inferTypesInsideFunction6() {
315+
FileInput root = inferTypes("""
316+
def foo(param: int):
317+
param = "hello"
318+
param
319+
""");
320+
321+
var functionDef = (FunctionDef) root.statements().statements().get(0);
322+
var lastExpressionStatement = (ExpressionStatement) functionDef.body().statements().get(functionDef.body().statements().size() -1);
323+
Assertions.assertThat(lastExpressionStatement.expressions().get(0).typeV2().unwrappedType()).isEqualTo(STR_TYPE);
324+
}
325+
326+
@Test
327+
void inferTypesInsideFunction7() {
328+
FileInput root = inferTypes("""
329+
def foo(param):
330+
param = "hello"
331+
param
332+
""");
333+
334+
var functionDef = (FunctionDef) root.statements().statements().get(0);
335+
var lastExpressionStatement = (ExpressionStatement) functionDef.body().statements().get(functionDef.body().statements().size() -1);
336+
Assertions.assertThat(lastExpressionStatement.expressions().get(0).typeV2().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
337+
}
338+
339+
@Test
340+
void inferTypesInsideFunction8() {
341+
FileInput root = inferTypes("""
342+
def foo(param: int):
343+
x = param
344+
x
345+
""");
346+
347+
var functionDef = (FunctionDef) root.statements().statements().get(0);
348+
var lastExpressionStatement = (ExpressionStatement) functionDef.body().statements().get(functionDef.body().statements().size() -1);
349+
Assertions.assertThat(lastExpressionStatement.expressions().get(0).typeV2().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
350+
}
351+
352+
@Test
353+
void inferTypeForReassignedBuiltinsInsideFunction() {
267354
FileInput root = inferTypes("""
268355
def foo():
269356
global list
@@ -274,7 +361,55 @@ def foo():
274361

275362
var functionDef = (FunctionDef) root.statements().statements().get(0);
276363
var expressionStatement = (ExpressionStatement) functionDef.body().statements().get(3);
277-
Assertions.assertThat(expressionStatement.expressions().get(0).typeV2()).isEqualTo(PythonType.UNKNOWN);
364+
// TODO: Shouldn't this be UNKNOWN due to glboal?
365+
assertThat(expressionStatement.expressions().get(0).typeV2().unwrappedType()).isEqualTo(STR_TYPE);
366+
}
367+
368+
@Test
369+
void global_variable() {
370+
assertThat(lastExpression("""
371+
global a
372+
a = 42
373+
a
374+
""").typeV2().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
375+
}
376+
377+
@Test
378+
void global_variable_builtin() {
379+
assertThat(lastExpression("""
380+
global list
381+
list = 42
382+
list
383+
""").typeV2().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
384+
}
385+
386+
@Test
387+
void conditional_assignment() {
388+
PythonType type = lastExpression("""
389+
if p:
390+
x = 42
391+
else:
392+
x = 'str'
393+
x
394+
""").typeV2().unwrappedType();
395+
assertThat(type).isInstanceOf(UnionType.class);
396+
assertThat(((UnionType) type).candidates()).extracting(PythonType::unwrappedType).containsExactlyInAnyOrder(INT_TYPE, STR_TYPE);
397+
}
398+
399+
@Test
400+
void conditional_assignment_in_function() {
401+
FileInput fileInput = inferTypes("""
402+
def foo():
403+
if p:
404+
x = 42
405+
else:
406+
x = 'str'
407+
x
408+
""");
409+
var functionDef = (FunctionDef) fileInput.statements().statements().get(0);
410+
var lastExpressionStatement = (ExpressionStatement) functionDef.body().statements().get(functionDef.body().statements().size() -1);
411+
assertThat(lastExpressionStatement.expressions().get(0).typeV2()).isInstanceOf(UnionType.class);
412+
assertThat(((UnionType) lastExpressionStatement.expressions().get(0).typeV2()).candidates()).extracting(PythonType::unwrappedType).containsExactlyInAnyOrder(INT_TYPE, STR_TYPE);
278413
}
279414

280415
@Test
@@ -428,6 +563,14 @@ void annotation_with_reassignment() {
428563
""").typeV2().unwrappedType()).isEqualTo(STR_TYPE);
429564
}
430565

566+
@Test
567+
void annotation_without_reassignment() {
568+
assertThat(lastExpression("""
569+
a: int
570+
a
571+
""").typeV2().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
572+
}
573+
431574
@Test
432575
@Disabled("ObjectType[PythonType.UNKNOWN] should just be PythonType.UNKNOWN")
433576
void call_expression() {

python-frontend/src/test/java/org/sonar/python/types/v2/ClassTypeTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ void no_parents() {
5757
assertThat(classType.hasUnresolvedHierarchy()).isFalse();
5858
// TODO: not correct
5959
assertThat(classType.key()).isEqualTo("C[]");
60+
assertThat(classType).hasToString("ClassType[C]");
6061

6162
assertThat(classType.hasMember("__call__")).isEqualTo(TriBool.TRUE);
6263
assertThat(classType.hasMember("unknown")).isEqualTo(TriBool.UNKNOWN);

python-frontend/src/test/java/org/sonar/python/types/v2/FunctionTypeTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ void arity() {
4646
assertThat(functionType.isAsynchronous()).isFalse();
4747
assertThat(functionType.parameters()).isEmpty();
4848
assertThat(functionType.displayName()).contains("Callable");
49+
assertThat(functionType).hasToString("FunctionType[fn]");
4950
assertThat(functionType.unwrappedType()).isEqualTo(functionType);
5051
assertThat(functionType.instanceDisplayName()).isEmpty();
5152
String fileId = SymbolUtils.pathOf(pythonFile).toString();

0 commit comments

Comments
 (0)