Skip to content

Commit 3d0deee

Browse files
SONARPY-1985 Infer a function return type based on its type hint for locally defined functions (#1900)
1 parent 8e09061 commit 3d0deee

File tree

9 files changed

+142
-6
lines changed

9 files changed

+142
-6
lines changed

python-checks/src/test/resources/checks/confusingTypeChecking/nonCallableCalled.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,9 @@ def foo():
4949
def bar():
5050
...
5151

52+
def local_function() -> int:
53+
...
54+
55+
def calling_local_function():
56+
x = local_function()
57+
x() # Noncompliant

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.sonar.python.types.v2.FunctionType;
3636
import org.sonar.python.types.v2.ParameterV2;
3737
import org.sonar.python.types.v2.PythonType;
38+
import org.sonar.python.types.v2.TypeOrigin;
3839

3940
import static org.sonar.python.tree.TreeUtils.locationInFile;
4041

@@ -49,6 +50,7 @@ public class FunctionTypeBuilder implements TypeBuilder<FunctionType> {
4950
private boolean isInstanceMethod;
5051
private PythonType owner;
5152
private PythonType returnType = PythonType.UNKNOWN;
53+
private TypeOrigin typeOrigin = TypeOrigin.STUB;
5254
private LocationInFile definitionLocation;
5355

5456
private static final String CLASS_METHOD_DECORATOR = "classmethod";
@@ -110,6 +112,11 @@ public FunctionTypeBuilder withReturnType(PythonType returnType) {
110112
return this;
111113
}
112114

115+
public FunctionTypeBuilder withTypeOrigin(TypeOrigin typeOrigin) {
116+
this.typeOrigin = typeOrigin;
117+
return this;
118+
}
119+
113120
@Override
114121
public FunctionTypeBuilder withDefinitionLocation(@Nullable LocationInFile definitionLocation) {
115122
this.definitionLocation = definitionLocation;
@@ -118,7 +125,7 @@ public FunctionTypeBuilder withDefinitionLocation(@Nullable LocationInFile defin
118125

119126
public FunctionType build() {
120127
return new FunctionType(
121-
name, attributes, parameters, returnType, isAsynchronous, hasDecorators, isInstanceMethod, hasVariadicParameter, owner, definitionLocation
128+
name, attributes, parameters, returnType, typeOrigin, isAsynchronous, hasDecorators, isInstanceMethod, hasVariadicParameter, owner, definitionLocation
122129
);
123130
}
124131

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.sonar.python.types.v2.ModuleType;
4848
import org.sonar.python.types.v2.ParameterV2;
4949
import org.sonar.python.types.v2.PythonType;
50+
import org.sonar.python.types.v2.TypeOrigin;
5051
import org.sonar.python.types.v2.UnionType;
5152

5253
public class SymbolsModuleTypeProvider {
@@ -122,12 +123,14 @@ private PythonType convertToFunctionType(FunctionSymbol symbol, Map<Symbol, Pyth
122123
InferredType inferredType = ((FunctionSymbolImpl) symbol).declaredReturnType();
123124
ClassSymbol classSymbol = inferredType.runtimeTypeSymbol();
124125
var returnType = resolveReturnType(createdTypesBySymbol, classSymbol);
126+
TypeOrigin typeOrigin = symbol.isStub() ? TypeOrigin.STUB : TypeOrigin.LOCAL;
125127

126128
FunctionTypeBuilder functionTypeBuilder =
127129
new FunctionTypeBuilder(symbol.name())
128130
.withAttributes(List.of())
129131
.withParameters(parameters)
130132
.withReturnType(returnType)
133+
.withTypeOrigin(typeOrigin)
131134
.withAsynchronous(symbol.isAsynchronous())
132135
.withHasDecorators(symbol.hasDecorators())
133136
.withInstanceMethod(symbol.isInstanceMethod())

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
import org.sonar.python.types.v2.ModuleType;
8080
import org.sonar.python.types.v2.ObjectType;
8181
import org.sonar.python.types.v2.PythonType;
82+
import org.sonar.python.types.v2.TypeOrigin;
8283
import org.sonar.python.types.v2.TypeSource;
8384
import org.sonar.python.types.v2.UnionType;
8485

@@ -257,13 +258,13 @@ public void visitFunctionDef(FunctionDef functionDef) {
257258
scan(functionDef.decorators());
258259
scan(functionDef.typeParams());
259260
scan(functionDef.parameters());
261+
scan(functionDef.returnTypeAnnotation());
260262
FunctionType functionType = buildFunctionType(functionDef);
261263
((NameImpl) functionDef.name()).typeV2(functionType);
262264
inTypeScope(functionType, () -> {
263265
// TODO: check scope accuracy
264266
scan(functionDef.typeParams());
265267
scan(functionDef.parameters());
266-
scan(functionDef.returnTypeAnnotation());
267268
scan(functionDef.body());
268269
});
269270
}
@@ -279,6 +280,12 @@ private FunctionType buildFunctionType(FunctionDef functionDef) {
279280
if (owner != null) {
280281
functionTypeBuilder.withOwner(owner);
281282
}
283+
TypeAnnotation typeAnnotation = functionDef.returnTypeAnnotation();
284+
if (typeAnnotation != null) {
285+
PythonType returnType = typeAnnotation.expression().typeV2();
286+
functionTypeBuilder.withReturnType(returnType);
287+
functionTypeBuilder.withTypeOrigin(TypeOrigin.LOCAL);
288+
}
282289
FunctionType functionType = functionTypeBuilder.build();
283290
if (owner != null) {
284291
if (functionDef.name().symbolV2().hasSingleBindingUsage()) {

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.sonar.python.types.v2.FunctionType;
5252
import org.sonar.python.types.v2.ObjectType;
5353
import org.sonar.python.types.v2.PythonType;
54+
import org.sonar.python.types.v2.TypeOrigin;
5455
import org.sonar.python.types.v2.TypeSource;
5556
import org.sonar.python.types.v2.UnionType;
5657

@@ -189,11 +190,29 @@ public List<Expression> typeDependencies() {
189190

190191
@Override
191192
public PythonType typeV2() {
192-
TypeSource typeSource = computeTypeSource();
193-
PythonType pythonType = returnTypeOfCall(callee().typeV2());
193+
PythonType calleeType = callee().typeV2();
194+
TypeSource typeSource = computeTypeSource(calleeType);
195+
PythonType pythonType = returnTypeOfCall(calleeType);
194196
return pythonType != PythonType.UNKNOWN ? new ObjectType(pythonType, typeSource) : PythonType.UNKNOWN;
195197
}
196198

199+
private TypeSource computeTypeSource(PythonType calleeType) {
200+
if (isCalleeLocallyDefinedFunction(calleeType)) {
201+
return TypeSource.TYPE_HINT;
202+
}
203+
return calleeTypeSource();
204+
}
205+
206+
boolean isCalleeLocallyDefinedFunction(PythonType pythonType) {
207+
if (pythonType instanceof FunctionType functionType) {
208+
return functionType.typeOrigin() == TypeOrigin.LOCAL;
209+
}
210+
if (pythonType instanceof UnionType unionType) {
211+
return unionType.candidates().stream().anyMatch(this::isCalleeLocallyDefinedFunction);
212+
}
213+
return false;
214+
}
215+
197216
static PythonType returnTypeOfCall(PythonType calleeType) {
198217
if (calleeType instanceof ClassType classType) {
199218
return classType;
@@ -219,7 +238,7 @@ static PythonType returnTypeOfCall(PythonType calleeType) {
219238
return PythonType.UNKNOWN;
220239
}
221240

222-
TypeSource computeTypeSource() {
241+
TypeSource calleeTypeSource() {
223242
if (callee() instanceof QualifiedExpression qualifiedExpression) {
224243
return qualifiedExpression.qualifier().typeV2().typeSource();
225244
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public final class FunctionType implements PythonType {
3535
private final List<PythonType> attributes;
3636
private final List<ParameterV2> parameters;
3737
private PythonType returnType;
38+
private final TypeOrigin typeOrigin;
3839
private final boolean isAsynchronous;
3940
private final boolean hasDecorators;
4041
private final boolean isInstanceMethod;
@@ -50,6 +51,7 @@ public FunctionType(
5051
List<PythonType> attributes,
5152
List<ParameterV2> parameters,
5253
PythonType returnType,
54+
TypeOrigin typeOrigin,
5355
boolean isAsynchronous,
5456
boolean hasDecorators,
5557
boolean isInstanceMethod,
@@ -61,6 +63,7 @@ public FunctionType(
6163
this.attributes = attributes;
6264
this.parameters = parameters;
6365
this.returnType = returnType;
66+
this.typeOrigin = typeOrigin;
6467
this.isAsynchronous = isAsynchronous;
6568
this.hasDecorators = hasDecorators;
6669
this.isInstanceMethod = isInstanceMethod;
@@ -128,4 +131,8 @@ public void resolveLazyReturnType(PythonType pythonType) {
128131
}
129132
this.returnType = pythonType;
130133
}
134+
135+
public TypeOrigin typeOrigin() {
136+
return typeOrigin;
137+
}
131138
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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.types.v2;
21+
22+
public enum TypeOrigin {
23+
LOCAL,
24+
STUB
25+
}

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

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,16 @@
6060
import org.sonar.python.types.v2.ObjectType;
6161
import org.sonar.python.types.v2.ParameterV2;
6262
import org.sonar.python.types.v2.PythonType;
63+
import org.sonar.python.types.v2.TypeOrigin;
6364
import org.sonar.python.types.v2.TypeSource;
6465
import org.sonar.python.types.v2.UnionType;
6566
import org.sonar.python.types.v2.UnknownType;
6667

6768
import static org.assertj.core.api.Assertions.assertThat;
6869
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
6970
import static org.sonar.python.PythonTestUtils.parse;
71+
import static org.sonar.python.PythonTestUtils.parseWithoutSymbols;
72+
import static org.sonar.python.PythonTestUtils.pythonFile;
7073
import static org.sonar.python.types.v2.TypesTestUtils.DICT_TYPE;
7174
import static org.sonar.python.types.v2.TypesTestUtils.FROZENSET_TYPE;
7275
import static org.sonar.python.types.v2.TypesTestUtils.INT_TYPE;
@@ -423,7 +426,7 @@ void typeSourceIsExactByDefault() {
423426

424427
CallExpression callExpressionSpy = Mockito.spy(callExpression);
425428
Expression calleeSpy = Mockito.spy(callExpression.callee());
426-
FunctionType functionType = new FunctionType("foo", List.of(), List.of(), INT_TYPE, false, false, false, false, null, null);
429+
FunctionType functionType = new FunctionType("foo", List.of(), List.of(), INT_TYPE, TypeOrigin.STUB, false, false, false, false, null, null);
427430
Mockito.when(calleeSpy.typeV2()).thenReturn(functionType);
428431
Mockito.when(callExpressionSpy.callee()).thenReturn(calleeSpy);
429432

@@ -1855,6 +1858,56 @@ void return_type_of_call_expression_2() {
18551858
).typeV2().unwrappedType()).isEqualTo(NONE_TYPE);
18561859
}
18571860

1861+
@Test
1862+
void return_type_of_call_of_locally_defined_function() {
1863+
var type = lastExpression("""
1864+
def foo() -> int: ...
1865+
x = foo()
1866+
x
1867+
""").typeV2();
1868+
assertThat(type.unwrappedType()).isEqualTo(INT_TYPE);
1869+
assertThat(type.typeSource()).isEqualTo(TypeSource.TYPE_HINT);
1870+
}
1871+
1872+
@Test
1873+
void type_origin_of_stub_function() {
1874+
FileInput fileInput = inferTypes("""
1875+
len
1876+
x = len([1,2])
1877+
x
1878+
""");
1879+
1880+
FunctionType lenType = (FunctionType) ((ExpressionStatement) fileInput.statements().statements().get(0)).expressions().get(0).typeV2();
1881+
assertThat(lenType.typeOrigin()).isEqualTo(TypeOrigin.STUB);
1882+
PythonType xType = ((ExpressionStatement) fileInput.statements().statements().get(2)).expressions().get(0).typeV2();
1883+
assertThat(xType.unwrappedType()).isEqualTo(INT_TYPE);
1884+
assertThat(xType.typeSource()).isEqualTo(TypeSource.EXACT);
1885+
1886+
}
1887+
1888+
@Test
1889+
void type_origin_of_project_function() {
1890+
FileInput tree = parseWithoutSymbols(
1891+
"def foo() -> int: ..."
1892+
);
1893+
ProjectLevelSymbolTable projectLevelSymbolTable = new ProjectLevelSymbolTable();
1894+
projectLevelSymbolTable.addModule(tree, "", pythonFile("mod.py"));
1895+
ProjectLevelTypeTable projectLevelTypeTable = new ProjectLevelTypeTable(projectLevelSymbolTable);
1896+
1897+
var lines = """
1898+
from mod import foo
1899+
foo
1900+
x = foo()
1901+
x
1902+
""";
1903+
FileInput fileInput = inferTypes(lines, projectLevelTypeTable);
1904+
FunctionType fooType = (FunctionType) ((ExpressionStatement) fileInput.statements().statements().get(1)).expressions().get(0).typeV2();
1905+
assertThat(fooType.typeOrigin()).isEqualTo(TypeOrigin.LOCAL);
1906+
PythonType xType = ((ExpressionStatement) fileInput.statements().statements().get(3)).expressions().get(0).typeV2();
1907+
// Declared return types of local functions are currently not stored in the project level symbol table
1908+
assertThat(xType).isEqualTo(PythonType.UNKNOWN);
1909+
}
1910+
18581911
@Test
18591912
void return_type_of_call_expression_union_type() {
18601913
FileInput fileInput = inferTypes(

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
import static org.assertj.core.api.Assertions.assertThat;
3535
import static org.sonar.python.PythonTestUtils.parseWithoutSymbols;
36+
import static org.sonar.python.types.v2.TypesTestUtils.INT_TYPE;
3637
import static org.sonar.python.types.v2.TypesTestUtils.PROJECT_LEVEL_TYPE_TABLE;
3738

3839
class FunctionTypeTest {
@@ -148,6 +149,14 @@ void declaredTypes() {
148149
assertThat(functionType.parameters()).extracting(ParameterV2::declaredType).containsExactly(PythonType.UNKNOWN);
149150
}
150151

152+
@Test
153+
void declared_return_type() {
154+
FunctionType functionType = functionType("def fn() -> int: ...");
155+
assertThat(functionType.returnType()).isEqualTo(INT_TYPE);
156+
functionType = functionType("def fn() -> unknown: ...");
157+
assertThat(functionType.returnType()).isEqualTo(PythonType.UNKNOWN);
158+
}
159+
151160
@Test
152161
void decorators() {
153162
// TODO: SONARPY-1772 Handle decorators

0 commit comments

Comments
 (0)