Skip to content

Commit 1e5afc0

Browse files
SONARPY-2023 Translate type source for binary expressions (#1896)
1 parent dcd4bec commit 1e5afc0

File tree

4 files changed

+160
-2
lines changed

4 files changed

+160
-2
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
import org.sonar.plugins.python.api.types.InferredType;
3535
import org.sonar.python.types.HasTypeDependencies;
3636
import org.sonar.python.types.InferredTypes;
37+
import org.sonar.python.types.v2.ClassType;
38+
import org.sonar.python.types.v2.ObjectType;
39+
import org.sonar.python.types.v2.PythonType;
40+
import org.sonar.python.types.v2.TypeSource;
41+
import org.sonar.python.types.v2.UnionType;
3742

3843
import static org.sonar.python.types.InferredTypes.DECL_INT;
3944
import static org.sonar.python.types.InferredTypes.DECL_STR;
@@ -137,6 +142,22 @@ public InferredType type() {
137142
return InferredTypes.anyType();
138143
}
139144

145+
@Override
146+
public PythonType typeV2() {
147+
if (is(Kind.AND, Kind.OR)) {
148+
return UnionType.or(leftOperand.typeV2(), rightOperand.typeV2());
149+
}
150+
if (is(Kind.PLUS)
151+
&& leftOperand.typeV2() instanceof ObjectType leftObjectType
152+
&& leftObjectType.unwrappedType() instanceof ClassType leftClassType
153+
&& rightOperand.typeV2() instanceof ObjectType rightObjectType
154+
&& rightObjectType.unwrappedType() instanceof ClassType rightClassType
155+
&& leftClassType == rightClassType) {
156+
return new ObjectType(leftClassType, TypeSource.min(leftObjectType.typeSource(), rightObjectType.typeSource()));
157+
}
158+
return PythonType.UNKNOWN;
159+
}
160+
140161
private static boolean shouldReturnDeclaredStr(InferredType leftType, InferredType rightType) {
141162
return (leftType.equals(DECL_STR) && rightType.equals(DECL_STR)) ||
142163
(leftType.equals(STR) && rightType.equals(DECL_STR)) ||

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

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

22+
import java.util.Comparator;
23+
import java.util.stream.Stream;
24+
2225
public enum TypeSource {
23-
EXACT,
24-
TYPE_HINT
26+
TYPE_HINT(0),
27+
EXACT(1);
28+
29+
private final int score;
30+
31+
TypeSource(int score) {
32+
this.score = score;
33+
}
34+
35+
public int score() {
36+
return score;
37+
}
38+
39+
public static TypeSource min(TypeSource... typeSources) {
40+
return Stream.of(typeSources)
41+
.min(Comparator.comparing(TypeSource::score))
42+
.orElse(EXACT);
43+
}
2544
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import java.util.ArrayList;
2323
import java.util.Collection;
24+
import java.util.Comparator;
2425
import java.util.HashSet;
2526
import java.util.List;
2627
import java.util.Optional;
@@ -64,6 +65,13 @@ public boolean isCompatibleWith(PythonType another) {
6465
.anyMatch(candidate -> candidate.isCompatibleWith(another));
6566
}
6667

68+
@Override
69+
public TypeSource typeSource() {
70+
return candidates.stream().map(PythonType::typeSource)
71+
.min(Comparator.comparing(TypeSource::score))
72+
.orElse(TypeSource.EXACT);
73+
}
74+
6775
@Beta
6876
public static PythonType or(Collection<PythonType> candidates) {
6977
if (candidates.isEmpty()) {

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

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2089,6 +2089,116 @@ def foo(x: list):
20892089
Assertions.assertThat(xType.unwrappedType()).isSameAs(LIST_TYPE);
20902090
}
20912091

2092+
@Test
2093+
void assignmentOrTest() {
2094+
var yType = (UnionType) lastExpression("""
2095+
def foo(x: int):
2096+
y = x or "str"
2097+
y
2098+
""").typeV2();
2099+
2100+
Assertions.assertThat(yType.candidates())
2101+
.allMatch(ObjectType.class::isInstance)
2102+
.extracting(PythonType::unwrappedType)
2103+
.containsOnly(INT_TYPE, STR_TYPE);
2104+
Assertions.assertThat(yType.typeSource()).isSameAs(TypeSource.TYPE_HINT);
2105+
}
2106+
2107+
@Test
2108+
void assignmentPlusTest() {
2109+
var fileInput = inferTypes("""
2110+
class A: ...
2111+
def foo(x: int, y: str):
2112+
a = x + 1
2113+
b = x + y
2114+
c = A + x
2115+
d = x + A
2116+
e = 2 + 3
2117+
a
2118+
b
2119+
c
2120+
d
2121+
e
2122+
""");
2123+
2124+
var statements = TreeUtils.firstChild(fileInput, FunctionDef.class::isInstance)
2125+
.map(FunctionDef.class::cast)
2126+
.map(FunctionDef::body)
2127+
.map(StatementList::statements)
2128+
.orElseGet(List::of);
2129+
2130+
var aType = ((ExpressionStatement) statements.get(statements.size() - 5)).expressions().get(0).typeV2();
2131+
var bType = ((ExpressionStatement) statements.get(statements.size() - 4)).expressions().get(0).typeV2();
2132+
var cType = ((ExpressionStatement) statements.get(statements.size() - 3)).expressions().get(0).typeV2();
2133+
var dType = ((ExpressionStatement) statements.get(statements.size() - 2)).expressions().get(0).typeV2();
2134+
var eType = ((ExpressionStatement) statements.get(statements.size() - 1)).expressions().get(0).typeV2();
2135+
2136+
Assertions.assertThat(aType.unwrappedType()).isSameAs(INT_TYPE);
2137+
Assertions.assertThat(aType.typeSource()).isSameAs(TypeSource.TYPE_HINT);
2138+
2139+
Assertions.assertThat(bType).isSameAs(PythonType.UNKNOWN);
2140+
Assertions.assertThat(cType).isSameAs(PythonType.UNKNOWN);
2141+
Assertions.assertThat(dType).isSameAs(PythonType.UNKNOWN);
2142+
2143+
Assertions.assertThat(eType.unwrappedType()).isSameAs(INT_TYPE);
2144+
Assertions.assertThat(eType.typeSource()).isSameAs(TypeSource.EXACT);
2145+
}
2146+
2147+
@Test
2148+
void assignmentPlusTest2() {
2149+
var fileInput = inferTypes("""
2150+
class A: ...
2151+
def foo(x):
2152+
if x:
2153+
t = int
2154+
else:
2155+
t = str
2156+
a = t()
2157+
b = 1 + a
2158+
c = a + 1
2159+
a
2160+
b
2161+
c
2162+
""");
2163+
2164+
var statements = TreeUtils.firstChild(fileInput, FunctionDef.class::isInstance)
2165+
.map(FunctionDef.class::cast)
2166+
.map(FunctionDef::body)
2167+
.map(StatementList::statements)
2168+
.orElseGet(List::of);
2169+
2170+
var aType = ((ExpressionStatement) statements.get(statements.size() - 3)).expressions().get(0).typeV2();
2171+
var bType = ((ExpressionStatement) statements.get(statements.size() - 2)).expressions().get(0).typeV2();
2172+
var cType = ((ExpressionStatement) statements.get(statements.size() - 1)).expressions().get(0).typeV2();
2173+
2174+
Assertions.assertThat(aType).isInstanceOf(ObjectType.class);
2175+
Assertions.assertThat(aType.unwrappedType()).isInstanceOf(UnionType.class);
2176+
var candidates = ((UnionType) aType.unwrappedType()).candidates();
2177+
Assertions.assertThat(candidates).containsOnly(INT_TYPE, STR_TYPE);
2178+
2179+
Assertions.assertThat(bType).isSameAs(PythonType.UNKNOWN);
2180+
Assertions.assertThat(cType).isSameAs(PythonType.UNKNOWN);
2181+
}
2182+
2183+
@Test
2184+
void assignmentMinusTest() {
2185+
var fileInput = inferTypes("""
2186+
def foo():
2187+
f = 2 - 3
2188+
f
2189+
""");
2190+
2191+
var statements = TreeUtils.firstChild(fileInput, FunctionDef.class::isInstance)
2192+
.map(FunctionDef.class::cast)
2193+
.map(FunctionDef::body)
2194+
.map(StatementList::statements)
2195+
.orElseGet(List::of);
2196+
2197+
var fType = ((ExpressionStatement) statements.get(statements.size() - 1)).expressions().get(0).typeV2();
2198+
Assertions.assertThat(fType).isSameAs(PythonType.UNKNOWN);
2199+
}
2200+
2201+
20922202
private static FileInput inferTypes(String lines) {
20932203
return inferTypes(lines, PROJECT_LEVEL_TYPE_TABLE);
20942204
}

0 commit comments

Comments
 (0)