Skip to content

Commit 037a304

Browse files
committed
SONARPY-2257 propagate unary expressions with out try-except blocks
remove unary expr propagation from TrivialTypeInferenceVisistor Move propagation to TrivialTypePropagation
1 parent 20d037d commit 037a304

File tree

9 files changed

+239
-42
lines changed

9 files changed

+239
-42
lines changed

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,13 @@ public class FlowSensitiveTypeInference extends ForwardAnalysis {
4545
private final Map<Statement, Assignment> assignmentsByAssignmentStatement;
4646
private final Map<Statement, Set<Definition>> definitionsByDefinitionStatement;
4747
private final Map<String, PythonType> parameterTypesByName;
48+
49+
private final TypeTable typeTable;
4850
private final IsInstanceVisitor isInstanceVisitor;
4951

52+
5053
public FlowSensitiveTypeInference(
51-
TypeTable projectLevelTypeTable, Set<SymbolV2> trackedVars,
54+
TypeTable typeTable, Set<SymbolV2> trackedVars,
5255
Map<Statement, Assignment> assignmentsByAssignmentStatement,
5356
Map<Statement, Set<Definition>> definitionsByDefinitionStatement,
5457
Map<String, PythonType> parameterTypesByName
@@ -57,7 +60,9 @@ public FlowSensitiveTypeInference(
5760
this.assignmentsByAssignmentStatement = assignmentsByAssignmentStatement;
5861
this.definitionsByDefinitionStatement = definitionsByDefinitionStatement;
5962
this.parameterTypesByName = parameterTypesByName;
60-
this.isInstanceVisitor = new IsInstanceVisitor(projectLevelTypeTable);
63+
64+
this.typeTable = typeTable;
65+
this.isInstanceVisitor = new IsInstanceVisitor(typeTable);
6166
}
6267

6368
@Override
@@ -129,8 +134,8 @@ private void handleParameter(Parameter parameter, TypeInferenceProgramState stat
129134
updateTree(name, state);
130135
}
131136

132-
private static void updateTree(Tree tree, TypeInferenceProgramState state) {
133-
tree.accept(new ProgramStateTypeInferenceVisitor(state));
137+
private void updateTree(Tree tree, TypeInferenceProgramState state) {
138+
tree.accept(new ProgramStateTypeInferenceVisitor(state, typeTable));
134139
}
135140

136141

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,23 @@
1919
import java.util.Optional;
2020
import java.util.Set;
2121
import java.util.stream.Stream;
22-
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
2322
import org.sonar.plugins.python.api.tree.Expression;
2423
import org.sonar.plugins.python.api.tree.FunctionDef;
2524
import org.sonar.plugins.python.api.tree.Name;
2625
import org.sonar.plugins.python.api.tree.QualifiedExpression;
26+
import org.sonar.python.semantic.v2.TypeTable;
2727
import org.sonar.python.tree.NameImpl;
2828
import org.sonar.python.types.v2.PythonType;
2929
import org.sonar.python.types.v2.UnionType;
3030

3131
/**
3232
* Used in FlowSensitiveTypeInference to update name types based on program state
3333
*/
34-
public class ProgramStateTypeInferenceVisitor extends BaseTreeVisitor {
34+
public class ProgramStateTypeInferenceVisitor extends TrivialTypePropagationVisitor {
3535
private final TypeInferenceProgramState state;
3636

37-
public ProgramStateTypeInferenceVisitor(TypeInferenceProgramState state) {
37+
public ProgramStateTypeInferenceVisitor(TypeInferenceProgramState state, TypeTable typeTable) {
38+
super(typeTable);
3839
this.state = state;
3940
}
4041

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

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@
6464
import org.sonar.plugins.python.api.tree.Tree;
6565
import org.sonar.plugins.python.api.tree.Tuple;
6666
import org.sonar.plugins.python.api.tree.TypeAnnotation;
67-
import org.sonar.plugins.python.api.tree.UnaryExpression;
68-
import org.sonar.plugins.python.api.types.BuiltinTypes;
6967
import org.sonar.python.semantic.v2.ClassTypeBuilder;
7068
import org.sonar.python.semantic.v2.FunctionTypeBuilder;
7169
import org.sonar.python.semantic.v2.SymbolV2;
@@ -83,7 +81,6 @@
8381
import org.sonar.python.tree.SubscriptionExpressionImpl;
8482
import org.sonar.python.tree.TreeUtils;
8583
import org.sonar.python.tree.TupleImpl;
86-
import org.sonar.python.tree.UnaryExpressionImpl;
8784
import org.sonar.python.types.v2.ClassType;
8885
import org.sonar.python.types.v2.FunctionType;
8986
import org.sonar.python.types.v2.Member;
@@ -92,7 +89,6 @@
9289
import org.sonar.python.types.v2.PythonType;
9390
import org.sonar.python.types.v2.SpecialFormType;
9491
import org.sonar.python.types.v2.TriBool;
95-
import org.sonar.python.types.v2.TypeCheckBuilder;
9692
import org.sonar.python.types.v2.TypeChecker;
9793
import org.sonar.python.types.v2.TypeOrigin;
9894
import org.sonar.python.types.v2.TypeSource;
@@ -184,36 +180,6 @@ public void visitNumericLiteral(NumericLiteral numericLiteral) {
184180
numericLiteralImpl.typeV2(new ObjectType(pythonType, new ArrayList<>(), new ArrayList<>()));
185181
}
186182

187-
@Override
188-
public void visitUnaryExpression(UnaryExpression unaryExpr) {
189-
super.visitUnaryExpression(unaryExpr);
190-
191-
var builtins = projectLevelTypeTable.getBuiltinsModule();
192-
Token operator = unaryExpr.operator();
193-
PythonType exprType = switch (operator.value()) {
194-
case "~" -> builtins.resolveMember(BuiltinTypes.INT).orElse(PythonType.UNKNOWN);
195-
case "not" -> builtins.resolveMember(BuiltinTypes.BOOL).orElse(PythonType.UNKNOWN);
196-
case "+", "-" -> getTypeWhenUnaryPlusMinus(unaryExpr);
197-
default -> unaryExpr.expression().typeV2();
198-
};
199-
200-
if (unaryExpr instanceof UnaryExpressionImpl unaryExprImpl) {
201-
unaryExprImpl.typeV2(exprType);
202-
}
203-
}
204-
205-
private PythonType getTypeWhenUnaryPlusMinus(UnaryExpression unaryExpr) {
206-
var builtins = projectLevelTypeTable.getBuiltinsModule();
207-
var isBooleanTypeCheck = new TypeCheckBuilder(projectLevelTypeTable).isBuiltinWithName(BuiltinTypes.BOOL);
208-
var innerExprType = unaryExpr.expression().typeV2();
209-
210-
if (isBooleanTypeCheck.check(innerExprType) == TriBool.TRUE) {
211-
return builtins.resolveMember(BuiltinTypes.INT).orElse(PythonType.UNKNOWN);
212-
} else {
213-
return innerExprType;
214-
}
215-
}
216-
217183
@Override
218184
public void visitNone(NoneExpression noneExpression) {
219185
var builtins = this.projectLevelTypeTable.getBuiltinsModule();
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.BaseTreeVisitor;
23+
import org.sonar.plugins.python.api.tree.Token;
24+
import org.sonar.plugins.python.api.tree.UnaryExpression;
25+
import org.sonar.plugins.python.api.types.BuiltinTypes;
26+
import org.sonar.python.semantic.v2.TypeTable;
27+
import org.sonar.python.tree.UnaryExpressionImpl;
28+
import org.sonar.python.types.v2.ObjectType;
29+
import org.sonar.python.types.v2.PythonType;
30+
import org.sonar.python.types.v2.TriBool;
31+
import org.sonar.python.types.v2.TypeCheckBuilder;
32+
import org.sonar.python.types.v2.TypeUtils;
33+
34+
public class TrivialTypePropagationVisitor extends BaseTreeVisitor {
35+
private TypeCheckBuilder isBooleanTypeCheck;
36+
private TypeCheckBuilder isIntTypeCheck;
37+
private TypeCheckBuilder isFloatTypeCheck;
38+
private TypeCheckBuilder isComplexTypeCheck;
39+
40+
private PythonType intType;
41+
private PythonType boolType;
42+
43+
public TrivialTypePropagationVisitor(TypeTable typeTable) {
44+
this.isBooleanTypeCheck = new TypeCheckBuilder(typeTable).isBuiltinWithName(BuiltinTypes.BOOL);
45+
this.isIntTypeCheck = new TypeCheckBuilder(typeTable).isBuiltinWithName(BuiltinTypes.INT);
46+
this.isFloatTypeCheck = new TypeCheckBuilder(typeTable).isBuiltinWithName(BuiltinTypes.FLOAT);
47+
this.isComplexTypeCheck = new TypeCheckBuilder(typeTable).isBuiltinWithName(BuiltinTypes.COMPLEX);
48+
49+
var builtins = typeTable.getBuiltinsModule();
50+
this.intType = builtins.resolveMember(BuiltinTypes.INT).orElse(PythonType.UNKNOWN);
51+
this.boolType = builtins.resolveMember(BuiltinTypes.BOOL).orElse(PythonType.UNKNOWN);
52+
}
53+
54+
@Override
55+
public void visitUnaryExpression(UnaryExpression unaryExpr) {
56+
super.visitUnaryExpression(unaryExpr);
57+
58+
Token operator = unaryExpr.operator();
59+
PythonType exprType = switch (operator.value()) {
60+
case "~" -> intType;
61+
case "not" -> boolType;
62+
case "+", "-" -> getTypeWhenUnaryPlusMinus(unaryExpr);
63+
default -> PythonType.UNKNOWN;
64+
};
65+
66+
if (unaryExpr instanceof UnaryExpressionImpl unaryExprImpl) {
67+
unaryExprImpl.typeV2(toObjectType(exprType));
68+
}
69+
}
70+
71+
private PythonType getTypeWhenUnaryPlusMinus(UnaryExpression unaryExpr) {
72+
var innerExprType = unaryExpr.expression().typeV2();
73+
return TypeUtils.map(innerExprType, this::mapUnaryPlusMinusType);
74+
}
75+
76+
private PythonType mapUnaryPlusMinusType(PythonType type) {
77+
if (isNumber(type)) {
78+
return type;
79+
} else if(isBooleanTypeCheck.check(type) == TriBool.TRUE) {
80+
return toObjectType(intType);
81+
}
82+
return PythonType.UNKNOWN;
83+
}
84+
85+
private boolean isNumber(PythonType type) {
86+
return isIntTypeCheck.check(type) == TriBool.TRUE
87+
|| isFloatTypeCheck.check(type) == TriBool.TRUE
88+
|| isComplexTypeCheck.check(type) == TriBool.TRUE;
89+
}
90+
91+
private static PythonType toObjectType(PythonType type) {
92+
if(type instanceof ObjectType || type == PythonType.UNKNOWN) {
93+
return type;
94+
}
95+
return new ObjectType(type);
96+
}
97+
}

python-frontend/src/main/java/org/sonar/python/tree/ParenthesizedExpressionImpl.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.sonar.plugins.python.api.tree.TreeVisitor;
2727
import org.sonar.plugins.python.api.types.InferredType;
2828
import org.sonar.python.types.HasTypeDependencies;
29+
import org.sonar.python.types.v2.PythonType;
2930

3031
public class ParenthesizedExpressionImpl extends PyTree implements ParenthesizedExpression, HasTypeDependencies {
3132

@@ -74,6 +75,11 @@ public InferredType type() {
7475
return expression.type();
7576
}
7677

78+
@Override
79+
public PythonType typeV2() {
80+
return expression.typeV2();
81+
}
82+
7783
@Override
7884
public List<Expression> typeDependencies() {
7985
return Collections.singletonList(expression);

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717
package org.sonar.python.types.v2;
1818

19+
import java.util.function.UnaryOperator;
20+
1921
public class TypeUtils {
2022

2123
private TypeUtils() {
@@ -35,4 +37,12 @@ public static PythonType ensureWrappedObjectType(PythonType pythonType) {
3537
}
3638
return pythonType;
3739
}
40+
41+
public static PythonType map(PythonType type, UnaryOperator<PythonType> mapper) {
42+
if(type instanceof UnionType unionType) {
43+
return unionType.candidates().stream().map(mapper).reduce(UnionType::or).orElse(PythonType.UNKNOWN);
44+
} else {
45+
return mapper.apply(type);
46+
}
47+
}
3848
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2603,6 +2603,7 @@ void unary_expression() {
26032603
static Stream<Arguments> unary_expression_of_variables() {
26042604
return Stream.of(
26052605
Arguments.of("x = 1; -x", INT_TYPE),
2606+
Arguments.of("x = 1; -(-x)", INT_TYPE),
26062607
Arguments.of("x = 1; +x", INT_TYPE),
26072608
Arguments.of("x = True; -x", INT_TYPE),
26082609
Arguments.of("x = True; +x", INT_TYPE),
@@ -2614,7 +2615,6 @@ static Stream<Arguments> unary_expression_of_variables() {
26142615
);
26152616
}
26162617

2617-
@Disabled("SONARPY-2257 unary expressions of non-literals do not propagate their type")
26182618
@ParameterizedTest
26192619
@MethodSource("unary_expression_of_variables")
26202620
void unary_expression_of_variables(String code, PythonType expectedType) {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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.stream.Stream;
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.Arguments;
27+
import org.junit.jupiter.params.provider.MethodSource;
28+
import org.sonar.python.types.v2.ObjectType;
29+
import org.sonar.python.types.v2.PythonType;
30+
import org.sonar.python.types.v2.TypesTestUtils;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.sonar.python.PythonTestUtils.lastExpression;
34+
import static org.sonar.python.PythonTestUtils.pythonFile;
35+
import static org.sonar.python.types.v2.TypesTestUtils.PROJECT_LEVEL_TYPE_TABLE;
36+
37+
class TrivialTypePropagationVisitorTest {
38+
private TrivialTypeInferenceVisitor trivialTypeInferenceVisitor;
39+
private TrivialTypePropagationVisitor trivialTypePropagationVisitor;
40+
41+
@BeforeEach
42+
void setup() {
43+
trivialTypeInferenceVisitor = new TrivialTypeInferenceVisitor(PROJECT_LEVEL_TYPE_TABLE, pythonFile("mod"), "mod");
44+
trivialTypePropagationVisitor = new TrivialTypePropagationVisitor(PROJECT_LEVEL_TYPE_TABLE);
45+
}
46+
47+
static Stream<Arguments> testSources() {
48+
return Stream.of(
49+
Arguments.of("-1", TypesTestUtils.INT_TYPE),
50+
Arguments.of("-1.0", TypesTestUtils.FLOAT_TYPE),
51+
Arguments.of("-(True)", TypesTestUtils.INT_TYPE),
52+
Arguments.of("-(1j)", TypesTestUtils.COMPLEX_TYPE),
53+
54+
Arguments.of("+1", TypesTestUtils.INT_TYPE),
55+
Arguments.of("+1.0", TypesTestUtils.FLOAT_TYPE),
56+
Arguments.of("+(1j)", TypesTestUtils.COMPLEX_TYPE),
57+
Arguments.of("+(True)", TypesTestUtils.INT_TYPE),
58+
59+
Arguments.of("~1", TypesTestUtils.INT_TYPE),
60+
Arguments.of("~1.0", TypesTestUtils.INT_TYPE),
61+
Arguments.of("~(3+2j)", TypesTestUtils.INT_TYPE),
62+
Arguments.of("~(1j)", TypesTestUtils.INT_TYPE),
63+
Arguments.of("~(True)", TypesTestUtils.INT_TYPE),
64+
65+
Arguments.of("not 1", TypesTestUtils.BOOL_TYPE),
66+
Arguments.of("not 1.0", TypesTestUtils.BOOL_TYPE),
67+
Arguments.of("not (2j)", TypesTestUtils.BOOL_TYPE),
68+
Arguments.of("not (True)", TypesTestUtils.BOOL_TYPE)
69+
);
70+
}
71+
72+
@MethodSource("testSources")
73+
@ParameterizedTest
74+
void test(String code, PythonType expectedType) {
75+
var expr = lastExpression(code);
76+
expr.accept(trivialTypeInferenceVisitor);
77+
expr.accept(trivialTypePropagationVisitor);
78+
assertThat(expr.typeV2())
79+
.isInstanceOfSatisfying(ObjectType.class, objectType ->
80+
assertThat(objectType.type()).isEqualTo(expectedType));
81+
}
82+
83+
static Stream<Arguments> customNumberClassTestSource() {
84+
return Stream.of(
85+
Arguments.of("+(MyNum())", PythonType.UNKNOWN),
86+
Arguments.of("-(MyNum())", PythonType.UNKNOWN),
87+
Arguments.of("not (MyNum())", new ObjectType(TypesTestUtils.BOOL_TYPE)),
88+
Arguments.of("~(MyNum())", new ObjectType(TypesTestUtils.INT_TYPE))
89+
);
90+
}
91+
92+
@MethodSource("customNumberClassTestSource")
93+
@ParameterizedTest
94+
void testCustomNumberClass(String code, PythonType expectedType) {
95+
var expr = lastExpression("class MyNum: pass", code);
96+
expr.accept(trivialTypeInferenceVisitor);
97+
expr.accept(trivialTypePropagationVisitor);
98+
99+
assertThat(expr.typeV2()).isEqualTo(expectedType);
100+
}
101+
102+
@Test
103+
void testNotOfCustomClass() {
104+
var expr = lastExpression("class MyNum: pass", "not MyNum()");
105+
expr.accept(trivialTypeInferenceVisitor);
106+
expr.accept(trivialTypePropagationVisitor);
107+
108+
assertThat(expr.typeV2()).isInstanceOfSatisfying(ObjectType.class, objectType ->
109+
assertThat(objectType.type()).isEqualTo(TypesTestUtils.BOOL_TYPE));
110+
}
111+
}

0 commit comments

Comments
 (0)