Skip to content

Commit 85a4e1e

Browse files
SONARPY-1881 S5756: NonCallableCalledCheck fix FP when a function parameter has reassignment to non callable type (#1812)
1 parent 775b43c commit 85a4e1e

File tree

6 files changed

+155
-6
lines changed

6 files changed

+155
-6
lines changed

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,31 @@ def using_isinstance_with_runtime_type():
272272
my_non_callable() # Noncompliant
273273
...
274274
my_non_callable() # Noncompliant
275+
276+
def reassigned_param(a, param):
277+
param = 1
278+
if a:
279+
param = [1,2,3]
280+
param() # Noncompliant
281+
282+
def conditionaly_reassigned_param(a, param):
283+
if a:
284+
param = [1,2,3]
285+
param()
286+
287+
def reassigned_param_try_except(a, param):
288+
try:
289+
param = 1
290+
if a:
291+
param = [1,2,3]
292+
param() # FN
293+
except:
294+
...
295+
296+
def conditionally_reassigned_param_try_except(a, param):
297+
try:
298+
if a:
299+
param = [1,2,3]
300+
param()
301+
except:
302+
...

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.sonar.python.semantic.v2.types.TrivialTypeInferenceVisitor;
4242
import org.sonar.python.semantic.v2.types.TryStatementVisitor;
4343
import org.sonar.python.tree.TreeUtils;
44+
import org.sonar.python.types.v2.PythonType;
4445

4546
public class TypeInferenceV2 {
4647

@@ -130,11 +131,17 @@ private static void inferTypesAndMemberAccessSymbols(Tree scopeTree,
130131
}
131132

132133
private static void flowSensitiveTypeInference(ControlFlowGraph cfg, Set<SymbolV2> trackedVars, PropagationVisitor propagationVisitor) {
134+
// TODO: infer parameter type based on type hint or default value assignement
135+
var parameterTypes = trackedVars
136+
.stream()
137+
.filter(s -> s.usages().stream().map(UsageV2::kind).anyMatch(UsageV2.Kind.PARAMETER::equals))
138+
.collect(Collectors.toMap(SymbolV2::name, v -> PythonType.UNKNOWN));
139+
133140
FlowSensitiveTypeInference flowSensitiveTypeInference = new FlowSensitiveTypeInference(
134141
trackedVars,
135142
propagationVisitor.assignmentsByAssignmentStatement(),
136143
propagationVisitor.definitionsByDefinitionStatement(),
137-
Map.of());
144+
parameterTypes);
138145

139146
flowSensitiveTypeInference.compute(cfg);
140147
flowSensitiveTypeInference.compute(cfg);

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*/
2020
package org.sonar.python.semantic.v2.types;
2121

22+
import java.util.HashSet;
2223
import java.util.Map;
2324
import java.util.Optional;
2425
import java.util.Set;
@@ -28,6 +29,7 @@
2829
import org.sonar.plugins.python.api.tree.Expression;
2930
import org.sonar.plugins.python.api.tree.FunctionDef;
3031
import org.sonar.plugins.python.api.tree.Name;
32+
import org.sonar.plugins.python.api.tree.Parameter;
3133
import org.sonar.plugins.python.api.tree.Statement;
3234
import org.sonar.plugins.python.api.tree.Tree;
3335
import org.sonar.python.cfg.fixpoint.ForwardAnalysis;
@@ -57,11 +59,7 @@ public FlowSensitiveTypeInference(
5759
public ProgramState initialState() {
5860
TypeInferenceProgramState initialState = new TypeInferenceProgramState();
5961
for (SymbolV2 variable : trackedVars) {
60-
var pythonTypeSet = Optional.of(variable.name())
61-
.map(parameterTypesByName::get)
62-
.map(Set::of)
63-
.orElseGet(Set::of);
64-
initialState.setTypes(variable, pythonTypeSet);
62+
initialState.setTypes(variable, Set.of());
6563
}
6664
return initialState;
6765
}
@@ -88,12 +86,26 @@ public void updateProgramState(Tree element, ProgramState programState) {
8886
}
8987
} else if (element instanceof FunctionDef functionDef) {
9088
handleDefinition(functionDef, state);
89+
} else if (element instanceof Parameter parameter) {
90+
handleParameter(parameter, state);
9191
} else {
9292
// Here we should run "isinstance" visitor when we handle declared types, to avoid FPs when type guard checks are made
9393
updateTree(element, state);
9494
}
9595
}
9696

97+
private void handleParameter(Parameter parameter, TypeInferenceProgramState state) {
98+
var name = parameter.name();
99+
100+
if (name == null || !trackedVars.contains(name.symbolV2())) {
101+
return;
102+
}
103+
104+
var type = parameterTypesByName.getOrDefault(name.name(), PythonType.UNKNOWN);
105+
state.setTypes(name.symbolV2(), new HashSet<>(Set.of(type)));
106+
updateTree(name, state);
107+
}
108+
97109
private static void updateTree(Tree tree, TypeInferenceProgramState state) {
98110
tree.accept(new ProgramStateTypeInferenceVisitor(state));
99111
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
public class ParameterDefinition extends Propagation {
27+
28+
protected ParameterDefinition(SymbolV2 lhsSymbol, Name lhsName) {
29+
super(lhsSymbol, lhsName);
30+
}
31+
32+
@Override
33+
public Name lhsName() {
34+
return lhsName;
35+
}
36+
37+
@Override
38+
public PythonType rhsType() {
39+
// TODO: process type calculation based on default value assignment or type hint
40+
return PythonType.UNKNOWN;
41+
}
42+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Iterator;
2525
import java.util.List;
2626
import java.util.Map;
27+
import java.util.Optional;
2728
import java.util.Set;
2829
import org.sonar.plugins.python.api.tree.AnnotatedAssignment;
2930
import org.sonar.plugins.python.api.tree.AssignmentStatement;
@@ -32,6 +33,7 @@
3233
import org.sonar.plugins.python.api.tree.Expression;
3334
import org.sonar.plugins.python.api.tree.FunctionDef;
3435
import org.sonar.plugins.python.api.tree.Name;
36+
import org.sonar.plugins.python.api.tree.Parameter;
3537
import org.sonar.plugins.python.api.tree.Statement;
3638
import org.sonar.python.semantic.v2.SymbolV2;
3739

@@ -69,6 +71,16 @@ public void visitFunctionDef(FunctionDef functionDef) {
6971
propagationsByLhs.computeIfAbsent(symbol, s -> new HashSet<>()).add(definition);
7072
}
7173

74+
@Override
75+
public void visitParameter(Parameter parameter) {
76+
Optional.ofNullable(parameter.name())
77+
.ifPresent(name -> {
78+
var symbol = name.symbolV2();
79+
var parametedDefinition = new ParameterDefinition(symbol, name);
80+
propagationsByLhs.computeIfAbsent(symbol, s -> new HashSet<>()).add(parametedDefinition);
81+
});
82+
}
83+
7284
@Override
7385
public void visitAssignmentStatement(AssignmentStatement assignmentStatement) {
7486
super.visitAssignmentStatement(assignmentStatement);

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,54 @@ def f():
12261226
.isSameAs(childClassType);
12271227
}
12281228

1229+
@Test
1230+
void inferReassignedParameterType() {
1231+
var root = inferTypes("""
1232+
def reassigned_param(a, param):
1233+
param = 1
1234+
if a:
1235+
param = "1"
1236+
param()
1237+
""");
1238+
1239+
var paramType = TreeUtils.firstChild(root, CallExpression.class::isInstance)
1240+
.map(CallExpression.class::cast)
1241+
.map(CallExpression::callee)
1242+
.map(Expression::typeV2)
1243+
.map(UnionType.class::cast)
1244+
.get();
1245+
1246+
1247+
Assertions.assertThat(paramType).isInstanceOf(UnionType.class);
1248+
Assertions.assertThat(paramType.candidates())
1249+
.hasSize(2);
1250+
1251+
var candidatesUnwrappedType = paramType.candidates().stream()
1252+
.map(PythonType::unwrappedType)
1253+
.toList();
1254+
1255+
Assertions.assertThat(candidatesUnwrappedType)
1256+
.contains(INT_TYPE, STR_TYPE);
1257+
}
1258+
1259+
@Test
1260+
void inferConditionallyReassignedParameterType() {
1261+
var root = inferTypes("""
1262+
def reassigned_param(a, param):
1263+
if a:
1264+
param = "1"
1265+
param()
1266+
""");
1267+
1268+
var paramType = TreeUtils.firstChild(root, CallExpression.class::isInstance)
1269+
.map(CallExpression.class::cast)
1270+
.map(CallExpression::callee)
1271+
.map(Expression::typeV2)
1272+
.get();
1273+
1274+
Assertions.assertThat(paramType).isSameAs(PythonType.UNKNOWN);
1275+
}
1276+
12291277
private static FileInput inferTypes(String lines) {
12301278
return inferTypes(lines, new HashMap<>());
12311279
}

0 commit comments

Comments
 (0)