Skip to content

Commit e6767e0

Browse files
SONARPY-2366 Propagate binary expressions of the same type
1 parent f251712 commit e6767e0

File tree

5 files changed

+177
-49
lines changed

5 files changed

+177
-49
lines changed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,18 @@
3535
import org.sonar.python.semantic.v2.UsageV2;
3636
import org.sonar.python.tree.NameImpl;
3737
import org.sonar.python.tree.TreeUtils;
38-
import org.sonar.python.types.HasTypeDependencies;
3938
import org.sonar.python.types.v2.PythonType;
4039
import org.sonar.python.types.v2.UnionType;
4140

4241
public class AstBasedTypeInference {
4342
private final Map<SymbolV2, Set<Propagation>> propagationsByLhs;
4443
private final Propagator propagator;
44+
private final TypeDependenciesCalculator typeDependenciesCalculator;
4545

4646
public AstBasedTypeInference(Map<SymbolV2, Set<Propagation>> propagationsByLhs, TypeTable typeTable) {
4747
this.propagationsByLhs = propagationsByLhs;
4848
this.propagator = new Propagator(typeTable);
49+
this.typeDependenciesCalculator = new TypeDependenciesCalculator();
4950
}
5051

5152
public Map<SymbolV2, Set<PythonType>> process(Set<SymbolV2> trackedVars) {
@@ -81,8 +82,8 @@ private void computeDependencies(Assignment assignment, Set<SymbolV2> trackedVar
8182
assignment.addVariableDependency(symbol);
8283
propagationsByLhs.get(symbol).forEach(propagation -> propagation.addDependent(assignment));
8384
}
84-
} else if (e instanceof HasTypeDependencies hasTypeDependencies) {
85-
workList.addAll(hasTypeDependencies.typeDependencies());
85+
} else if (typeDependenciesCalculator.hasTypeDependencies(e)) {
86+
workList.addAll(typeDependenciesCalculator.calculate(e));
8687
}
8788
}
8889
}

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,21 @@
1717
package org.sonar.python.semantic.v2.types;
1818

1919
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
20+
import org.sonar.plugins.python.api.tree.BinaryExpression;
21+
import org.sonar.plugins.python.api.tree.Tree;
2022
import org.sonar.plugins.python.api.tree.UnaryExpression;
2123
import org.sonar.plugins.python.api.types.BuiltinTypes;
2224
import org.sonar.python.semantic.v2.TypeTable;
25+
import org.sonar.python.tree.BinaryExpressionImpl;
2326
import org.sonar.python.tree.UnaryExpressionImpl;
27+
import org.sonar.python.types.v2.ClassType;
2428
import org.sonar.python.types.v2.ObjectType;
2529
import org.sonar.python.types.v2.PythonType;
2630
import org.sonar.python.types.v2.TriBool;
2731
import org.sonar.python.types.v2.TypeCheckBuilder;
32+
import org.sonar.python.types.v2.TypeSource;
2833
import org.sonar.python.types.v2.TypeUtils;
34+
import org.sonar.python.types.v2.UnionType;
2935

3036
public class TrivialTypePropagationVisitor extends BaseTreeVisitor {
3137
private final TypeCheckBuilder isBooleanTypeCheck;
@@ -57,6 +63,33 @@ public void visitUnaryExpression(UnaryExpression unaryExpr) {
5763
}
5864
}
5965

66+
@Override
67+
public void visitBinaryExpression(BinaryExpression binaryExpression) {
68+
super.visitBinaryExpression(binaryExpression);
69+
if (binaryExpression instanceof BinaryExpressionImpl binaryExpressionImpl) {
70+
var type = calculateBinaryExpressionType(binaryExpression);
71+
binaryExpressionImpl.typeV2(type);
72+
}
73+
}
74+
75+
private static PythonType calculateBinaryExpressionType(BinaryExpression binaryExpression) {
76+
var kind = binaryExpression.getKind();
77+
var leftOperand = binaryExpression.leftOperand();
78+
var rightOperand = binaryExpression.rightOperand();
79+
if (binaryExpression.is(Tree.Kind.AND, Tree.Kind.OR)) {
80+
return UnionType.or(leftOperand.typeV2(), rightOperand.typeV2());
81+
}
82+
if (TypeDependenciesCalculator.SAME_TYPE_PRODUCING_BINARY_EXPRESSION_KINDS.contains(kind)
83+
&& leftOperand.typeV2() instanceof ObjectType leftObjectType
84+
&& leftObjectType.unwrappedType() instanceof ClassType leftClassType
85+
&& rightOperand.typeV2() instanceof ObjectType rightObjectType
86+
&& rightObjectType.unwrappedType() instanceof ClassType rightClassType
87+
&& leftClassType == rightClassType) {
88+
return new ObjectType(leftClassType, TypeSource.min(leftObjectType.typeSource(), rightObjectType.typeSource()));
89+
}
90+
return PythonType.UNKNOWN;
91+
}
92+
6093
private PythonType calculateUnaryExprType(UnaryExpression unaryExpr) {
6194
String operator = unaryExpr.operator().value();
6295
return TypeUtils.map(unaryExpr.expression().typeV2(), type -> mapUnaryExprType(operator, type));
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.semantic.v2.types;
18+
19+
import java.util.EnumSet;
20+
import java.util.List;
21+
import java.util.Set;
22+
import org.sonar.plugins.python.api.tree.BinaryExpression;
23+
import org.sonar.plugins.python.api.tree.Expression;
24+
import org.sonar.plugins.python.api.tree.Tree;
25+
import org.sonar.python.types.HasTypeDependencies;
26+
27+
public class TypeDependenciesCalculator {
28+
static final Set<Tree.Kind> SAME_TYPE_PRODUCING_BINARY_EXPRESSION_KINDS = EnumSet.of(
29+
Tree.Kind.PLUS,
30+
Tree.Kind.MINUS,
31+
Tree.Kind.MULTIPLICATION,
32+
Tree.Kind.DIVISION,
33+
Tree.Kind.FLOOR_DIVISION,
34+
Tree.Kind.MODULO,
35+
Tree.Kind.POWER
36+
);
37+
38+
public boolean hasTypeDependencies(Expression expression) {
39+
return expression instanceof HasTypeDependencies;
40+
}
41+
42+
public List<Expression> calculate(Expression expression) {
43+
if (expression instanceof BinaryExpression binaryExpression) {
44+
return calculateBinaryExpressionTypeDependencies(binaryExpression);
45+
} else if (expression instanceof HasTypeDependencies hasTypeDependencies) {
46+
return hasTypeDependencies.typeDependencies();
47+
}
48+
return List.of();
49+
}
50+
51+
private static List<Expression> calculateBinaryExpressionTypeDependencies(BinaryExpression binaryExpression) {
52+
if (SAME_TYPE_PRODUCING_BINARY_EXPRESSION_KINDS.contains(binaryExpression.getKind())) {
53+
return List.of(binaryExpression.leftOperand(), binaryExpression.rightOperand());
54+
}
55+
return List.of();
56+
}
57+
58+
59+
}

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

Lines changed: 33 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.util.Arrays;
2020
import java.util.Collections;
21-
import java.util.HashMap;
2221
import java.util.List;
2322
import java.util.Map;
2423
import java.util.Objects;
@@ -31,11 +30,7 @@
3130
import org.sonar.plugins.python.api.types.InferredType;
3231
import org.sonar.python.types.HasTypeDependencies;
3332
import org.sonar.python.types.InferredTypes;
34-
import org.sonar.python.types.v2.ClassType;
35-
import org.sonar.python.types.v2.ObjectType;
3633
import org.sonar.python.types.v2.PythonType;
37-
import org.sonar.python.types.v2.TypeSource;
38-
import org.sonar.python.types.v2.UnionType;
3934

4035
import static org.sonar.python.types.InferredTypes.DECL_INT;
4136
import static org.sonar.python.types.InferredTypes.DECL_STR;
@@ -44,39 +39,38 @@
4439

4540
public class BinaryExpressionImpl extends PyTree implements BinaryExpression, HasTypeDependencies {
4641

47-
private static final Map<String, Kind> KINDS_BY_OPERATOR = kindsByOperator();
42+
private static final Map<String, Kind> KINDS_BY_OPERATOR = Map.ofEntries(
43+
Map.entry("+", Kind.PLUS),
44+
Map.entry("-", Kind.MINUS),
45+
Map.entry("*", Kind.MULTIPLICATION),
46+
Map.entry("/", Kind.DIVISION),
47+
Map.entry("//", Kind.FLOOR_DIVISION),
48+
Map.entry("%", Kind.MODULO),
49+
Map.entry("**", Kind.POWER),
50+
Map.entry("@", Kind.MATRIX_MULTIPLICATION),
51+
Map.entry(">>", Kind.SHIFT_EXPR),
52+
Map.entry("<<", Kind.SHIFT_EXPR),
53+
Map.entry("&", Kind.BITWISE_AND),
54+
Map.entry("|", Kind.BITWISE_OR),
55+
Map.entry("^", Kind.BITWISE_XOR),
56+
Map.entry("and", Kind.AND),
57+
Map.entry("or", Kind.OR),
58+
Map.entry("==", Kind.COMPARISON),
59+
Map.entry("<=", Kind.COMPARISON),
60+
Map.entry(">=", Kind.COMPARISON),
61+
Map.entry("<", Kind.COMPARISON),
62+
Map.entry(">", Kind.COMPARISON),
63+
Map.entry("!=", Kind.COMPARISON),
64+
Map.entry("<>", Kind.COMPARISON),
65+
Map.entry("in", Kind.IN),
66+
Map.entry("is", Kind.IS)
67+
);
4868

4969
private final Kind kind;
5070
private final Expression leftOperand;
5171
private final Token operator;
5272
private final Expression rightOperand;
53-
54-
private static Map<String, Kind> kindsByOperator() {
55-
Map<String, Kind> map = new HashMap<>();
56-
map.put("+", Kind.PLUS);
57-
map.put("-", Kind.MINUS);
58-
map.put("*", Kind.MULTIPLICATION);
59-
map.put("/", Kind.DIVISION);
60-
map.put("//", Kind.FLOOR_DIVISION);
61-
map.put("%", Kind.MODULO);
62-
map.put("**", Kind.POWER);
63-
map.put("@", Kind.MATRIX_MULTIPLICATION);
64-
map.put(">>", Kind.SHIFT_EXPR);
65-
map.put("<<", Kind.SHIFT_EXPR);
66-
map.put("&", Kind.BITWISE_AND);
67-
map.put("|", Kind.BITWISE_OR);
68-
map.put("^", Kind.BITWISE_XOR);
69-
map.put("and", Kind.AND);
70-
map.put("or", Kind.OR);
71-
map.put("==", Kind.COMPARISON);
72-
map.put("<=", Kind.COMPARISON);
73-
map.put(">=", Kind.COMPARISON);
74-
map.put("<", Kind.COMPARISON);
75-
map.put(">", Kind.COMPARISON);
76-
map.put("!=", Kind.COMPARISON);
77-
map.put("<>", Kind.COMPARISON);
78-
return map;
79-
}
73+
private PythonType type = PythonType.UNKNOWN;
8074

8175
public BinaryExpressionImpl(Expression leftOperand, Token operator, Expression rightOperand) {
8276
this.kind = KINDS_BY_OPERATOR.get(operator.value());
@@ -141,18 +135,12 @@ public InferredType type() {
141135

142136
@Override
143137
public PythonType typeV2() {
144-
if (is(Kind.AND, Kind.OR)) {
145-
return UnionType.or(leftOperand.typeV2(), rightOperand.typeV2());
146-
}
147-
if (is(Kind.PLUS)
148-
&& leftOperand.typeV2() instanceof ObjectType leftObjectType
149-
&& leftObjectType.unwrappedType() instanceof ClassType leftClassType
150-
&& rightOperand.typeV2() instanceof ObjectType rightObjectType
151-
&& rightObjectType.unwrappedType() instanceof ClassType rightClassType
152-
&& leftClassType == rightClassType) {
153-
return new ObjectType(leftClassType, TypeSource.min(leftObjectType.typeSource(), rightObjectType.typeSource()));
154-
}
155-
return PythonType.UNKNOWN;
138+
return type;
139+
}
140+
141+
public BinaryExpressionImpl typeV2(PythonType type) {
142+
this.type = type;
143+
return this;
156144
}
157145

158146
private static boolean shouldReturnDeclaredStr(InferredType leftType, InferredType rightType) {

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

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3069,6 +3069,53 @@ def foo(x: int):
30693069
assertThat(yType.typeSource()).isSameAs(TypeSource.TYPE_HINT);
30703070
}
30713071

3072+
public static Stream<Arguments> binaryExpressionsSource() {
3073+
return Stream.of(
3074+
Arguments.of("""
3075+
1 + 2
3076+
""", INT_TYPE),
3077+
Arguments.of("""
3078+
1 - 2
3079+
""", INT_TYPE),
3080+
Arguments.of("""
3081+
1 * 2
3082+
""", INT_TYPE),
3083+
Arguments.of("""
3084+
1 / 2
3085+
""", INT_TYPE),
3086+
Arguments.of("""
3087+
'1' + '2'
3088+
""", STR_TYPE),
3089+
Arguments.of("""
3090+
a = 1
3091+
b = 2
3092+
a + b
3093+
""", INT_TYPE),
3094+
Arguments.of("""
3095+
a = 1
3096+
b = 2
3097+
c = a - b
3098+
c
3099+
""", INT_TYPE),
3100+
Arguments.of("""
3101+
try:
3102+
...
3103+
except:
3104+
...
3105+
a = 1
3106+
b = 2
3107+
c = a - b
3108+
c
3109+
""", INT_TYPE)
3110+
);
3111+
}
3112+
3113+
@ParameterizedTest
3114+
@MethodSource("binaryExpressionsSource")
3115+
void binaryExpressionTest(String code, PythonType expectedType) {
3116+
assertThat(lastExpression(code).typeV2()).isInstanceOf(ObjectType.class).extracting(PythonType::unwrappedType).isEqualTo(expectedType);
3117+
}
3118+
30723119
@Test
30733120
void assignmentPlusTest() {
30743121
var fileInput = inferTypes("""
@@ -3159,7 +3206,7 @@ def foo():
31593206
.orElseGet(List::of);
31603207

31613208
var fType = ((ExpressionStatement) statements.get(statements.size() - 1)).expressions().get(0).typeV2();
3162-
assertThat(fType).isSameAs(PythonType.UNKNOWN);
3209+
assertThat(fType).isInstanceOf(ObjectType.class).extracting(PythonType::unwrappedType).isSameAs(INT_TYPE);
31633210
}
31643211

31653212
@Test

0 commit comments

Comments
 (0)