Skip to content

Commit a828c2d

Browse files
SONARPY-1826 Enable flow sensitive type inference for function types
1 parent a41ac8c commit a828c2d

File tree

11 files changed

+521
-59
lines changed

11 files changed

+521
-59
lines changed

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,40 @@ def calling_global_func():
218218
def using_nonlocal_var():
219219
nonlocal some_nonlocal_var
220220
some_nonlocal_var() # Noncompliant
221+
222+
223+
def reassigned_function():
224+
if cond:
225+
def my_callable(): ...
226+
my_callable() # OK
227+
else:
228+
def my_callable(): ...
229+
my_callable = 42
230+
my_callable() # Noncompliant
231+
232+
233+
234+
def recursive_with_try_finally(x):
235+
if x is False:
236+
print("recursion!")
237+
return
238+
recursive_with_try_finally(False) # Noncompliant
239+
try:
240+
recursive_with_try_finally(False) # Noncompliant
241+
finally:
242+
recursive_with_try_finally = None
243+
recursive_with_try_finally(False) # Noncompliant
244+
recursive_with_try_finally(False) # Noncompliant
245+
246+
247+
248+
def nested_recursive_try_finally():
249+
def my_rec(x):
250+
if x is False:
251+
print("yeah")
252+
return
253+
my_rec(False)
254+
try:
255+
my_rec(True)
256+
finally:
257+
my_rec = None
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-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;
21+
22+
public class SymbolV2Utils {
23+
24+
private SymbolV2Utils() {}
25+
26+
public static boolean isFunctionOrClassDeclaration(UsageV2 usageV2) {
27+
return usageV2.kind() == UsageV2.Kind.FUNC_DECLARATION || usageV2.kind() == UsageV2.Kind.CLASS_DECLARATION;
28+
}
29+
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
import org.sonar.plugins.python.api.tree.Parameter;
3636
import org.sonar.plugins.python.api.tree.StatementList;
3737
import org.sonar.plugins.python.api.tree.Tree;
38-
import org.sonar.python.semantic.v2.types.Assignment;
3938
import org.sonar.python.semantic.v2.types.FlowSensitiveTypeInference;
39+
import org.sonar.python.semantic.v2.types.Propagation;
4040
import org.sonar.python.semantic.v2.types.PropagationVisitor;
4141
import org.sonar.python.semantic.v2.types.TrivialTypeInferenceVisitor;
4242
import org.sonar.python.semantic.v2.types.TryStatementVisitor;
@@ -111,9 +111,9 @@ private static void inferTypesAndMemberAccessSymbols(Tree scopeTree,
111111
) {
112112
PropagationVisitor propagationVisitor = new PropagationVisitor();
113113
scopeTree.accept(propagationVisitor);
114-
Set<Name> assignedNames = propagationVisitor.assignmentsByLhs().values().stream()
114+
Set<Name> assignedNames = propagationVisitor.propagationsByLhs().values().stream()
115115
.flatMap(Collection::stream)
116-
.map(Assignment::lhsName)
116+
.map(Propagation::lhsName)
117117
.collect(Collectors.toSet());
118118

119119
TryStatementVisitor tryStatementVisitor = new TryStatementVisitor();
@@ -135,6 +135,7 @@ private static void flowSensitiveTypeInference(ControlFlowGraph cfg, Set<SymbolV
135135
FlowSensitiveTypeInference flowSensitiveTypeInference = new FlowSensitiveTypeInference(
136136
trackedVars,
137137
propagationVisitor.assignmentsByAssignmentStatement(),
138+
propagationVisitor.definitionsByDefinitionStatement(),
138139
Map.of());
139140

140141
flowSensitiveTypeInference.compute(cfg);

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

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,23 @@
2121

2222
import java.util.ArrayDeque;
2323
import java.util.Deque;
24-
import java.util.HashSet;
2524
import java.util.Map;
2625
import java.util.Set;
2726
import org.sonar.plugins.python.api.tree.Expression;
2827
import org.sonar.plugins.python.api.tree.Name;
2928
import org.sonar.python.semantic.v2.SymbolV2;
30-
import org.sonar.python.semantic.v2.UsageV2;
31-
import org.sonar.python.tree.NameImpl;
3229
import org.sonar.python.types.HasTypeDependencies;
3330
import org.sonar.python.types.v2.PythonType;
34-
import org.sonar.python.types.v2.UnionType;
3531

36-
public class Assignment {
32+
public class Assignment extends Propagation {
3733

38-
final SymbolV2 lhsSymbol;
39-
Name lhsName;
4034
Expression rhs;
41-
Set<SymbolV2> variableDependencies = new HashSet<>();
42-
Set<Assignment> dependents = new HashSet<>();
43-
Map<SymbolV2, Set<Assignment>> assignmentsByLhs;
35+
Map<SymbolV2, Set<Propagation>> propagationsByLhs;
4436

45-
public Assignment(SymbolV2 lhsSymbol, Name lhsName, Expression rhs, Map<SymbolV2, Set<Assignment>> assignmentsByLhs) {
46-
this.lhsSymbol = lhsSymbol;
47-
this.lhsName = lhsName;
37+
public Assignment(SymbolV2 lhsSymbol, Name lhsName, Expression rhs, Map<SymbolV2, Set<Propagation>> propagationsByLhs) {
38+
super(lhsSymbol, lhsName);
4839
this.rhs = rhs;
49-
this.assignmentsByLhs = assignmentsByLhs;
40+
this.propagationsByLhs = propagationsByLhs;
5041
}
5142

5243
void computeDependencies(Expression expression, Set<SymbolV2> trackedVars) {
@@ -58,45 +49,28 @@ void computeDependencies(Expression expression, Set<SymbolV2> trackedVars) {
5849
SymbolV2 symbol = name.symbolV2();
5950
if (symbol != null && trackedVars.contains(symbol)) {
6051
variableDependencies.add(symbol);
61-
assignmentsByLhs.get(symbol).forEach(a -> a.dependents.add(this));
52+
propagationsByLhs.get(symbol).forEach(a -> a.dependents.add(this));
6253
}
6354
} else if (e instanceof HasTypeDependencies hasTypeDependencies) {
6455
workList.addAll(hasTypeDependencies.typeDependencies());
6556
}
6657
}
6758
}
6859

69-
boolean areDependenciesReady(Set<SymbolV2> initializedVars) {
70-
return initializedVars.containsAll(variableDependencies);
71-
}
72-
73-
/** @return true if the propagation effectively changed the inferred type of lhs */
74-
public boolean propagate(Set<SymbolV2> initializedVars) {
75-
PythonType rhsType = rhs.typeV2();
76-
if (initializedVars.add(lhsSymbol)) {
77-
lhsSymbol.usages().stream().map(UsageV2::tree).filter(NameImpl.class::isInstance).map(NameImpl.class::cast).forEach(n -> n.typeV2(rhsType));
78-
return true;
79-
} else {
80-
PythonType currentType = lhsName.typeV2();
81-
PythonType newType = UnionType.or(rhsType, currentType);
82-
lhsSymbol.usages().stream().map(UsageV2::tree).filter(NameImpl.class::isInstance).map(NameImpl.class::cast).forEach(n -> n.typeV2(newType));
83-
return !newType.equals(currentType);
84-
}
85-
}
86-
8760
public Name lhsName() {
8861
return lhsName;
8962
}
9063

64+
@Override
65+
public PythonType rhsType() {
66+
return rhs.typeV2();
67+
}
68+
9169
public SymbolV2 lhsSymbol() {
9270
return lhsSymbol;
9371
}
9472

9573
public Expression rhs() {
9674
return rhs;
9775
}
98-
99-
public Set<Assignment> dependents() {
100-
return dependents;
101-
}
10276
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 org.sonar.plugins.python.api.tree.Name;
23+
import org.sonar.python.semantic.v2.SymbolV2;
24+
import org.sonar.python.types.v2.PythonType;
25+
26+
/**
27+
* This represents a class or function definition
28+
* It can be modelled as an assignment of a class / function type to their symbol
29+
*/
30+
public class Definition extends Propagation {
31+
32+
public Definition(SymbolV2 symbol, Name name) {
33+
super(symbol, name);
34+
}
35+
36+
public SymbolV2 lhsSymbol() {
37+
return lhsSymbol;
38+
}
39+
40+
public Name lhsName() {
41+
return lhsName;
42+
}
43+
44+
@Override
45+
public PythonType rhsType() {
46+
return lhsName.typeV2();
47+
}
48+
}

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

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,18 @@
4242
public class FlowSensitiveTypeInference extends ForwardAnalysis {
4343
private final Set<SymbolV2> trackedVars;
4444
private final Map<Statement, Assignment> assignmentsByAssignmentStatement;
45+
private final Map<Statement, Definition> definitionsByDefinitionStatement;
4546
private final Map<String, PythonType> parameterTypesByName;
4647

47-
public FlowSensitiveTypeInference(Set<SymbolV2> trackedVars,
48-
Map<Statement, Assignment> assignmentsByAssignmentStatement, Map<String, PythonType> parameterTypesByName) {
48+
public FlowSensitiveTypeInference(
49+
Set<SymbolV2> trackedVars,
50+
Map<Statement, Assignment> assignmentsByAssignmentStatement,
51+
Map<Statement, Definition> definitionsByDefinitionStatement,
52+
Map<String, PythonType> parameterTypesByName
53+
) {
4954
this.trackedVars = trackedVars;
5055
this.assignmentsByAssignmentStatement = assignmentsByAssignmentStatement;
56+
this.definitionsByDefinitionStatement = definitionsByDefinitionStatement;
5157
this.parameterTypesByName = parameterTypesByName;
5258
}
5359

@@ -84,6 +90,8 @@ public void updateProgramState(Tree element, ProgramState programState) {
8490
// update lhs
8591
updateTree(assignment.variable(), state);
8692
}
93+
} else if (element instanceof FunctionDef functionDef) {
94+
handleDefinition(functionDef, state);
8795
} else {
8896
// TODO: isinstance visitor
8997
updateTree(element, state);
@@ -133,4 +141,14 @@ private void handleAssignment(Statement assignmentStatement, TypeInferenceProgra
133141
}
134142
});
135143
}
144+
145+
private void handleDefinition(Statement definitionStatement, TypeInferenceProgramState programState) {
146+
Optional.ofNullable(definitionsByDefinitionStatement.get(definitionStatement))
147+
.ifPresent(definition -> {
148+
SymbolV2 symbol = definition.lhsSymbol();
149+
if (trackedVars.contains(symbol)) {
150+
programState.setTypes(symbol, Set.of(definition.lhsName.typeV2()));
151+
}
152+
});
153+
}
136154
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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.HashSet;
23+
import java.util.Set;
24+
import java.util.stream.Stream;
25+
import org.sonar.plugins.python.api.tree.Expression;
26+
import org.sonar.plugins.python.api.tree.Name;
27+
import org.sonar.plugins.python.api.tree.Tree;
28+
import org.sonar.python.semantic.v2.SymbolV2;
29+
import org.sonar.python.semantic.v2.SymbolV2Utils;
30+
import org.sonar.python.semantic.v2.UsageV2;
31+
import org.sonar.python.tree.NameImpl;
32+
import org.sonar.python.types.v2.PythonType;
33+
import org.sonar.python.types.v2.UnionType;
34+
35+
public abstract class Propagation {
36+
37+
final Set<SymbolV2> variableDependencies;
38+
final Set<Propagation> dependents;
39+
40+
final SymbolV2 lhsSymbol;
41+
final Name lhsName;
42+
43+
protected Propagation(SymbolV2 lhsSymbol, Name lhsName) {
44+
this.lhsSymbol = lhsSymbol;
45+
this.lhsName = lhsName;
46+
this.variableDependencies = new HashSet<>();
47+
this.dependents = new HashSet<>();
48+
}
49+
50+
/**
51+
* This is used for AST-based type inference in try/catch statements
52+
* @return true if the propagation effectively changed the inferred type of assignment LHS
53+
*/
54+
boolean propagate(Set<SymbolV2> initializedVars) {
55+
PythonType rhsType = rhsType();
56+
if (initializedVars.add(lhsSymbol)) {
57+
getSymbolNonDeclarationUsageTrees(lhsSymbol)
58+
.filter(NameImpl.class::isInstance)
59+
.map(NameImpl.class::cast)
60+
.forEach(n -> n.typeV2(rhsType));
61+
return true;
62+
} else {
63+
PythonType currentType = currentType(lhsName);
64+
if (currentType == null) {
65+
return false;
66+
}
67+
PythonType newType = UnionType.or(rhsType, currentType);
68+
getSymbolNonDeclarationUsageTrees(lhsSymbol)
69+
.filter(NameImpl.class::isInstance)
70+
.map(NameImpl.class::cast)
71+
.forEach(n -> n.typeV2(newType));
72+
return !newType.equals(currentType);
73+
}
74+
}
75+
76+
public static Stream<Tree> getSymbolNonDeclarationUsageTrees(SymbolV2 symbol) {
77+
return symbol.usages()
78+
.stream()
79+
// Function and class definition names will always have FunctionType and ClassType respectively
80+
// so they are filtered out of type propagation
81+
.filter(u -> !SymbolV2Utils.isFunctionOrClassDeclaration(u))
82+
.map(UsageV2::tree);
83+
}
84+
85+
boolean areDependenciesReady(Set<SymbolV2> initializedVars) {
86+
return initializedVars.containsAll(variableDependencies);
87+
}
88+
89+
Set<Propagation> dependents() {
90+
return dependents;
91+
}
92+
93+
public abstract Name lhsName();
94+
95+
public abstract PythonType rhsType();
96+
97+
static PythonType currentType(Name lhsName) {
98+
return lhsName.symbolV2().usages()
99+
.stream()
100+
.filter(u -> !SymbolV2Utils.isFunctionOrClassDeclaration(u))
101+
.map(UsageV2::tree)
102+
.filter(Expression.class::isInstance)
103+
.map(Expression.class::cast)
104+
.findFirst()
105+
.map(Expression::typeV2)
106+
.orElse(null);
107+
}
108+
}

0 commit comments

Comments
 (0)