Skip to content

Commit 7871dde

Browse files
SONARPY-1997 Propagate function return types to the result of call expressions (#1857)
1 parent 559f802 commit 7871dde

File tree

6 files changed

+106
-6
lines changed

6 files changed

+106
-6
lines changed

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@
3333
import org.sonar.plugins.python.api.symbols.ClassSymbol;
3434
import org.sonar.plugins.python.api.symbols.FunctionSymbol;
3535
import org.sonar.plugins.python.api.symbols.Symbol;
36+
import org.sonar.plugins.python.api.types.InferredType;
3637
import org.sonar.python.semantic.ClassSymbolImpl;
38+
import org.sonar.python.semantic.FunctionSymbolImpl;
3739
import org.sonar.python.semantic.ProjectLevelSymbolTable;
3840
import org.sonar.python.semantic.SymbolImpl;
3941
import org.sonar.python.types.v2.ClassType;
@@ -88,8 +90,9 @@ private static ModuleType createEmptyModule(String moduleName, ModuleType parent
8890

8991
private ModuleType createModuleFromSymbols(@Nullable String name, @Nullable ModuleType parent, Collection<Symbol> symbols) {
9092
var members = new HashMap<String, PythonType>();
93+
Map<Symbol, PythonType> createdTypesBySymbol = new HashMap<>();
9194
symbols.forEach(symbol -> {
92-
var type = convertToType(symbol, new HashMap<>());
95+
var type = convertToType(symbol, createdTypesBySymbol);
9396
members.put(symbol.name(), type);
9497
});
9598
var module = new ModuleType(name, parent);
@@ -102,7 +105,7 @@ private ModuleType createModuleFromSymbols(@Nullable String name, @Nullable Modu
102105
return module;
103106
}
104107

105-
private static PythonType convertToFunctionType(FunctionSymbol symbol, Map<Symbol, PythonType> createdTypesBySymbol) {
108+
private PythonType convertToFunctionType(FunctionSymbol symbol, Map<Symbol, PythonType> createdTypesBySymbol) {
106109
if (createdTypesBySymbol.containsKey(symbol)) {
107110
return createdTypesBySymbol.get(symbol);
108111
}
@@ -112,11 +115,18 @@ private static PythonType convertToFunctionType(FunctionSymbol symbol, Map<Symbo
112115
.map(SymbolsModuleTypeProvider::convertParameter)
113116
.toList();
114117

118+
InferredType inferredType = ((FunctionSymbolImpl) symbol).declaredReturnType();
119+
ClassSymbol classSymbol = inferredType.runtimeTypeSymbol();
120+
var returnType = PythonType.UNKNOWN;
121+
if (classSymbol != null) {
122+
returnType = convertToType(classSymbol, createdTypesBySymbol);
123+
}
124+
115125
FunctionTypeBuilder functionTypeBuilder =
116126
new FunctionTypeBuilder(symbol.name())
117127
.withAttributes(List.of())
118128
.withParameters(parameters)
119-
.withReturnType(PythonType.UNKNOWN)
129+
.withReturnType(returnType)
120130
.withAsynchronous(symbol.isAsynchronous())
121131
.withHasDecorators(symbol.hasDecorators())
122132
.withInstanceMethod(symbol.isInstanceMethod())
@@ -187,5 +197,4 @@ private PythonType convertToType(Symbol symbol, Map<Symbol, PythonType> createdT
187197
case OTHER -> PythonType.UNKNOWN;
188198
};
189199
}
190-
191200
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public class TypeShed {
9191
public TypeShed(ProjectLevelSymbolTable projectLevelSymbolTable) {
9292
// workaround to initialize supported python versions used in ClassSymbolImpl
9393
// TODO: remove once v2 types model will be populated from TypeShed bypassing conversion to symbols
94-
org.sonar.python.types.TypeShed.builtinSymbols();
94+
builtins = org.sonar.python.types.TypeShed.builtinSymbols();
9595
typeShedSymbols = new HashMap<>();
9696
modulesInProgress = new HashSet<>();
9797
this.projectLevelSymbolTable = projectLevelSymbolTable;

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@
4747
import org.sonar.python.types.HasTypeDependencies;
4848
import org.sonar.python.types.InferredTypes;
4949
import org.sonar.python.types.v2.ClassType;
50+
import org.sonar.python.types.v2.FunctionType;
5051
import org.sonar.python.types.v2.ObjectType;
5152
import org.sonar.python.types.v2.PythonType;
53+
import org.sonar.python.types.v2.UnionType;
5254

5355
import static org.sonar.plugins.python.api.symbols.Symbol.Kind.CLASS;
5456
import static org.sonar.plugins.python.api.tree.Tree.Kind.SUBSCRIPTION;
@@ -188,6 +190,21 @@ public PythonType typeV2() {
188190
if (callee().typeV2() instanceof ClassType classType) {
189191
return new ObjectType(classType);
190192
}
193+
if (callee().typeV2() instanceof FunctionType functionType) {
194+
return functionType.returnType();
195+
}
196+
if (callee().typeV2() instanceof UnionType unionType) {
197+
PythonType result = null;
198+
for (PythonType candidate : unionType.candidates()) {
199+
if (candidate instanceof ClassType classType) {
200+
result = UnionType.or(result, classType);
201+
}
202+
if (candidate instanceof FunctionType functionType) {
203+
result = UnionType.or(result, functionType.returnType());
204+
}
205+
}
206+
return result == null ? PythonType.UNKNOWN : new ObjectType(result);
207+
}
191208
return PythonType.UNKNOWN;
192209
}
193210
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Optional;
2727
import java.util.Set;
2828
import java.util.stream.Collectors;
29+
import javax.annotation.Nullable;
2930
import org.sonar.api.Beta;
3031

3132
@Beta
@@ -71,7 +72,13 @@ public static PythonType or(Collection<PythonType> candidates) {
7172
}
7273

7374
@Beta
74-
public static PythonType or(PythonType type1, PythonType type2) {
75+
public static PythonType or(@Nullable PythonType type1, @Nullable PythonType type2) {
76+
if (type1 == null) {
77+
return type2;
78+
}
79+
if (type2 == null) {
80+
return type1;
81+
}
7582
if (type1.equals(PythonType.UNKNOWN) || type2.equals(PythonType.UNKNOWN)) {
7683
return PythonType.UNKNOWN;
7784
}

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,6 +1574,65 @@ def reassigned_param(a, param):
15741574
Assertions.assertThat(paramType).isSameAs(PythonType.UNKNOWN);
15751575
}
15761576

1577+
@Test
1578+
void return_type_of_call_expression_1() {
1579+
assertThat(lastExpression(
1580+
"""
1581+
x = [1,2,3]
1582+
a = x.append(42)
1583+
a
1584+
"""
1585+
).typeV2().unwrappedType()).isEqualTo(NONE_TYPE);
1586+
}
1587+
1588+
@Test
1589+
void return_type_of_call_expression_2() {
1590+
assertThat(lastExpression(
1591+
"""
1592+
x = [1,2,3]
1593+
a = x.sort()
1594+
a
1595+
"""
1596+
).typeV2().unwrappedType()).isEqualTo(NONE_TYPE);
1597+
}
1598+
1599+
@Test
1600+
void return_type_of_call_expression_union_type() {
1601+
FileInput fileInput = inferTypes(
1602+
"""
1603+
class A:
1604+
def foo(self): ...
1605+
class B:
1606+
def bar(self): ...
1607+
a = A
1608+
b = B
1609+
if cond:
1610+
x = a
1611+
else:
1612+
x = b
1613+
y = x()
1614+
y
1615+
"""
1616+
);
1617+
var classA = TreeUtils.firstChild(fileInput.statements().statements().get(0), ClassDef.class::isInstance)
1618+
.map(ClassDef.class::cast)
1619+
.map(ClassDef::name)
1620+
.map(Expression::typeV2)
1621+
.map(ClassType.class::cast)
1622+
.get();
1623+
1624+
var classB = TreeUtils.firstChild(fileInput.statements().statements().get(1), ClassDef.class::isInstance)
1625+
.map(ClassDef.class::cast)
1626+
.map(ClassDef::name)
1627+
.map(Expression::typeV2)
1628+
.map(ClassType.class::cast)
1629+
.get();
1630+
1631+
assertThat(((ExpressionStatement) fileInput.statements().statements().get(6)).expressions().get(0).typeV2()).isInstanceOf(ObjectType.class);
1632+
UnionType unionType = (UnionType) ((ExpressionStatement) fileInput.statements().statements().get(6)).expressions().get(0).typeV2().unwrappedType();
1633+
assertThat(unionType.candidates()).containsExactlyInAnyOrder(classA, classB);
1634+
}
1635+
15771636
private static FileInput inferTypes(String lines) {
15781637
return inferTypes(lines, PROJECT_LEVEL_TYPE_TABLE);
15791638
}

python-frontend/src/test/java/org/sonar/python/types/v2/UnionTypeTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ void or_with_union_type() {
7777
assertThat(((UnionType) result).candidates()).containsExactlyInAnyOrder(INT_TYPE, STR_TYPE, BOOL_TYPE);
7878
}
7979

80+
@Test
81+
void or_with_null() {
82+
PythonType type = UnionType.or(INT_TYPE, null);
83+
assertThat(type).isEqualTo(INT_TYPE);
84+
type = UnionType.or(null, INT_TYPE);
85+
assertThat(type).isEqualTo(INT_TYPE);
86+
}
87+
8088
@Test
8189
void or_unionType() {
8290
FileInput fileInput = parseAndInferTypes("42;\"hello\"");

0 commit comments

Comments
 (0)