Skip to content

Commit 4721b07

Browse files
SONARPY-2017 Avoid type-related FPs in case of isinstance checks (#1885)
1 parent ea52d33 commit 4721b07

File tree

4 files changed

+122
-4
lines changed

4 files changed

+122
-4
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ private void inferTypesAndMemberAccessSymbols(FunctionDef functionDef) {
102102
}
103103

104104

105-
private static void inferTypesAndMemberAccessSymbols(Tree scopeTree,
105+
private void inferTypesAndMemberAccessSymbols(Tree scopeTree,
106106
StatementList statements,
107107
Set<SymbolV2> declaredVariables,
108108
Set<Name> annotatedParameterNames,
@@ -130,7 +130,7 @@ private static void inferTypesAndMemberAccessSymbols(Tree scopeTree,
130130
}
131131
}
132132

133-
private static void flowSensitiveTypeInference(ControlFlowGraph cfg, Set<SymbolV2> trackedVars, PropagationVisitor propagationVisitor) {
133+
private void flowSensitiveTypeInference(ControlFlowGraph cfg, Set<SymbolV2> trackedVars, PropagationVisitor propagationVisitor) {
134134
// TODO: infer parameter type based on default value assignement
135135
var parameterTypes = trackedVars
136136
.stream()
@@ -140,6 +140,7 @@ private static void flowSensitiveTypeInference(ControlFlowGraph cfg, Set<SymbolV
140140
.collect(Collectors.toMap(SymbolV2::name, TypeInferenceV2::getParameterType));
141141

142142
FlowSensitiveTypeInference flowSensitiveTypeInference = new FlowSensitiveTypeInference(
143+
projectLevelTypeTable,
143144
trackedVars,
144145
propagationVisitor.assignmentsByAssignmentStatement(),
145146
propagationVisitor.definitionsByDefinitionStatement(),

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.sonar.plugins.python.api.tree.Tree;
3737
import org.sonar.python.cfg.fixpoint.ForwardAnalysis;
3838
import org.sonar.python.cfg.fixpoint.ProgramState;
39+
import org.sonar.python.semantic.v2.ProjectLevelTypeTable;
3940
import org.sonar.python.semantic.v2.SymbolV2;
4041
import org.sonar.python.types.v2.PythonType;
4142

@@ -44,9 +45,10 @@ public class FlowSensitiveTypeInference extends ForwardAnalysis {
4445
private final Map<Statement, Assignment> assignmentsByAssignmentStatement;
4546
private final Map<Statement, Set<Definition>> definitionsByDefinitionStatement;
4647
private final Map<String, PythonType> parameterTypesByName;
48+
private final IsInstanceVisitor isInstanceVisitor;
4749

4850
public FlowSensitiveTypeInference(
49-
Set<SymbolV2> trackedVars,
51+
ProjectLevelTypeTable projectLevelTypeTable, Set<SymbolV2> trackedVars,
5052
Map<Statement, Assignment> assignmentsByAssignmentStatement,
5153
Map<Statement, Set<Definition>> definitionsByDefinitionStatement,
5254
Map<String, PythonType> parameterTypesByName
@@ -55,6 +57,7 @@ public FlowSensitiveTypeInference(
5557
this.assignmentsByAssignmentStatement = assignmentsByAssignmentStatement;
5658
this.definitionsByDefinitionStatement = definitionsByDefinitionStatement;
5759
this.parameterTypesByName = parameterTypesByName;
60+
this.isInstanceVisitor = new IsInstanceVisitor(projectLevelTypeTable);
5861
}
5962

6063
@Override
@@ -95,7 +98,8 @@ public void updateProgramState(Tree element, ProgramState programState) {
9598
} else if (isForLoopAssignment(element)) {
9699
handleLoopAssignment(element, state);
97100
} else {
98-
// Here we should run "isinstance" visitor when we handle declared types, to avoid FPs when type guard checks are made
101+
isInstanceVisitor.setState(state);
102+
element.accept(isInstanceVisitor);
99103
updateTree(element, state);
100104
}
101105
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
23+
import java.util.Set;
24+
import javax.annotation.CheckForNull;
25+
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
26+
import org.sonar.plugins.python.api.tree.CallExpression;
27+
import org.sonar.plugins.python.api.tree.Name;
28+
import org.sonar.plugins.python.api.tree.RegularArgument;
29+
import org.sonar.python.semantic.v2.ProjectLevelTypeTable;
30+
import org.sonar.python.semantic.v2.SymbolV2;
31+
import org.sonar.python.types.v2.PythonType;
32+
import org.sonar.python.types.v2.TypeSource;
33+
34+
public class IsInstanceVisitor extends BaseTreeVisitor {
35+
private final PythonType isInstanceFunctionType;
36+
private TypeInferenceProgramState state;
37+
38+
public IsInstanceVisitor(ProjectLevelTypeTable projectLevelTypeTable) {
39+
isInstanceFunctionType = projectLevelTypeTable.getType("isinstance");
40+
}
41+
42+
public void setState(TypeInferenceProgramState state) {
43+
this.state = state;
44+
}
45+
46+
@Override
47+
public void visitCallExpression(CallExpression callExpression) {
48+
if (callExpression.callee().typeV2() == isInstanceFunctionType && callExpression.arguments().size() == 2) {
49+
var firstArgumentSymbol = getFirstArgumentSymbol(callExpression);
50+
if (firstArgumentSymbol != null) {
51+
state.setTypes(firstArgumentSymbol, Set.of(PythonType.UNKNOWN));
52+
}
53+
}
54+
super.visitCallExpression(callExpression);
55+
}
56+
57+
@CheckForNull
58+
private SymbolV2 getFirstArgumentSymbol(CallExpression callExpression) {
59+
var argument = callExpression.arguments().get(0);
60+
if (argument instanceof RegularArgument regularArgument
61+
&& regularArgument.expression() instanceof Name variableName
62+
&& state.getTypes(variableName.symbolV2()).stream()
63+
.anyMatch(type -> type != PythonType.UNKNOWN && type.typeSource() == TypeSource.TYPE_HINT)) {
64+
return variableName.symbolV2();
65+
}
66+
return null;
67+
}
68+
}

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,6 +1843,51 @@ void basic_imported_symbols() {
18431843
assertThat(((UnionType) mathModule.resolveMember("acos").get()).candidates()).allMatch(FunctionType.class::isInstance);
18441844
}
18451845

1846+
@Test
1847+
void isInstanceTests() {
1848+
var xType = lastExpression("""
1849+
def foo(x: int):
1850+
if isinstance(x, Foo):
1851+
...
1852+
x
1853+
""").typeV2();
1854+
Assertions.assertThat(xType).isSameAs(PythonType.UNKNOWN);
1855+
1856+
xType = lastExpression("""
1857+
def foo(x: int):
1858+
if isinstance(x):
1859+
...
1860+
x
1861+
""").typeV2();
1862+
Assertions.assertThat(xType.unwrappedType()).isSameAs(INT_TYPE);
1863+
1864+
xType = lastExpression("""
1865+
def foo(x: int):
1866+
if isinstance(x.b, Foo):
1867+
...
1868+
x
1869+
""").typeV2();
1870+
Assertions.assertThat(xType.unwrappedType()).isSameAs(INT_TYPE);
1871+
1872+
xType = lastExpression("""
1873+
def foo():
1874+
x = 10
1875+
if isinstance(x, Foo):
1876+
...
1877+
x
1878+
""").typeV2();
1879+
Assertions.assertThat(xType.unwrappedType()).isSameAs(INT_TYPE);
1880+
1881+
xType = lastExpression("""
1882+
def foo(x: list):
1883+
if isinstance(**x, Foo):
1884+
...
1885+
x
1886+
""").typeV2();
1887+
1888+
Assertions.assertThat(xType.unwrappedType()).isSameAs(LIST_TYPE);
1889+
}
1890+
18461891
private static FileInput inferTypes(String lines) {
18471892
return inferTypes(lines, PROJECT_LEVEL_TYPE_TABLE);
18481893
}

0 commit comments

Comments
 (0)