Skip to content

Commit fadff2f

Browse files
ghislainpiotguillaume-dequenne
authored andcommitted
SONARPY-2135 Convert function parameter return type to use TypeWrapper
1 parent 264374f commit fadff2f

File tree

9 files changed

+150
-38
lines changed

9 files changed

+150
-38
lines changed

its/ruling/src/test/resources/expected/python-S3699.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,9 @@
1515
],
1616
"project:tensorflow/tools/pip_package/setup.py": [
1717
141
18+
],
19+
"project:twisted-12.1.0/twisted/trial/test/test_script.py": [
20+
407,
21+
417
1822
]
1923
}
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
{
2-
'salt:salt/cloud/clouds/linode.py':[
3-
1814,
2+
"salt:salt/cloud/clouds/linode.py": [
3+
1814
44
],
5-
'salt:salt/utils/http.py':[
6-
453,
5+
"salt:salt/ext/tornado/test/util.py": [
6+
113
77
],
8+
"salt:salt/utils/http.py": [
9+
453
10+
]
811
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import java.util.HashMap;
2323
import java.util.Map;
2424
import org.sonar.python.types.v2.LazyType;
25+
import org.sonar.python.types.v2.LazyTypeWrapper;
2526
import org.sonar.python.types.v2.PythonType;
27+
import org.sonar.python.types.v2.TypeWrapper;
2628

2729
public class LazyTypesContext {
2830
private final Map<String, LazyType> lazyTypes;
@@ -33,6 +35,10 @@ public LazyTypesContext(ProjectLevelTypeTable projectLevelTypeTable) {
3335
this.projectLevelTypeTable = projectLevelTypeTable;
3436
}
3537

38+
public TypeWrapper getOrCreateLazyTypeWrapper(String fullyQualifiedName) {
39+
return new LazyTypeWrapper(getOrCreateLazyType(fullyQualifiedName));
40+
}
41+
3642
public LazyType getOrCreateLazyType(String fullyQualifiedName) {
3743
if (lazyTypes.containsKey(fullyQualifiedName)) {
3844
return lazyTypes.get(fullyQualifiedName);

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

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,15 @@
4646
import org.sonar.python.types.v2.LazyTypeWrapper;
4747
import org.sonar.python.types.v2.Member;
4848
import org.sonar.python.types.v2.ModuleType;
49+
import org.sonar.python.types.v2.ObjectType;
4950
import org.sonar.python.types.v2.ParameterV2;
5051
import org.sonar.python.types.v2.PythonType;
5152
import org.sonar.python.types.v2.SimpleTypeWrapper;
5253
import org.sonar.python.types.v2.TypeOrigin;
5354
import org.sonar.python.types.v2.UnionType;
5455

5556
public class SymbolsModuleTypeProvider {
57+
public static final String OBJECT_TYPE_FQN = "object";
5658
private final ProjectLevelSymbolTable projectLevelSymbolTable;
5759
private final TypeShed typeShed;
5860
private ModuleType rootModule;
@@ -122,11 +124,8 @@ private PythonType convertToFunctionType(FunctionSymbol symbol, Map<Symbol, Pyth
122124
.map(SymbolsModuleTypeProvider::convertParameter)
123125
.toList();
124126

125-
var returnType = PythonType.UNKNOWN;
126-
var protoReturnType = ((FunctionSymbolImpl) symbol).protobufReturnType();
127-
if (protoReturnType != null) {
128-
returnType = convertProtobufType(protoReturnType);
129-
}
127+
var returnType = getReturnTypeFromSymbol(symbol);
128+
130129
TypeOrigin typeOrigin = symbol.isStub() ? TypeOrigin.STUB : TypeOrigin.LOCAL;
131130

132131
FunctionTypeBuilder functionTypeBuilder =
@@ -148,6 +147,13 @@ private PythonType convertToFunctionType(FunctionSymbol symbol, Map<Symbol, Pyth
148147
return functionType;
149148
}
150149

150+
private PythonType getReturnTypeFromSymbol(FunctionSymbol symbol) {
151+
var returnTypeFqns = getReturnTypeFqn(symbol);
152+
var returnTypeList = returnTypeFqns.stream().map(lazyTypesContext::getOrCreateLazyTypeWrapper).map(ObjectType::new).toList();
153+
//TODO Support type unions (SONARPY-2132)
154+
return returnTypeList.size() == 1 ? returnTypeList.get(0) : PythonType.UNKNOWN;
155+
}
156+
151157
PythonType resolvePossibleLazyType(String fullyQualifiedName) {
152158
if (rootModule == null) {
153159
// If root module has not been created yet, return lazy type
@@ -229,7 +235,21 @@ private PythonType convertToType(Symbol symbol, Map<Symbol, PythonType> createdT
229235
};
230236
}
231237

232-
public PythonType convertProtobufType(SymbolsProtos.Type type) {
238+
private List<String> getReturnTypeFqn(FunctionSymbol symbol) {
239+
List<String> fqnList = List.of();
240+
if (symbol.annotatedReturnTypeName() != null && !OBJECT_TYPE_FQN.equals(symbol.annotatedReturnTypeName())) {
241+
fqnList = List.of(symbol.annotatedReturnTypeName());
242+
} else if (symbol instanceof FunctionSymbolImpl functionSymbol && functionSymbol.protobufReturnType() != null) {
243+
var protoReturnType = functionSymbol.protobufReturnType();
244+
fqnList = getSymbolTypeFqn(protoReturnType);
245+
}
246+
return fqnList;
247+
}
248+
249+
List<String> getSymbolTypeFqn(SymbolsProtos.Type type) {
250+
if (OBJECT_TYPE_FQN.equals(type.getFullyQualifiedName())) {
251+
return List.of();
252+
}
233253
switch (type.getKind()) {
234254
case INSTANCE:
235255
String typeName = type.getFullyQualifiedName();
@@ -238,33 +258,35 @@ public PythonType convertProtobufType(SymbolsProtos.Type type) {
238258
// This doesn't seem to be very precisely specified in typeshed, because it has special semantic.
239259
// To avoid FPs, we treat it as ANY
240260
if ("typing._SpecialForm".equals(typeName)) {
241-
return PythonType.UNKNOWN;
261+
return List.of();
242262
}
243263
typeName = typeName.replaceFirst("^builtins\\.", "");
244-
return typeName.isEmpty() ? PythonType.UNKNOWN : resolvePossibleLazyType(typeName);
264+
return typeName.isEmpty() ? List.of() : List.of(typeName);
245265
case TYPE:
246-
return resolvePossibleLazyType("type");
266+
return List.of("type");
247267
case TYPE_ALIAS:
248-
return convertProtobufType(type.getArgs(0));
268+
return getSymbolTypeFqn(type.getArgs(0));
249269
case CALLABLE:
250270
// this should be handled as a function type - see SONARPY-953
251-
return PythonType.UNKNOWN;
271+
// Creates FPs with `sys.gettrace`
272+
return List.of();
252273
case UNION:
253-
return UnionType.or(type.getArgsList().stream().map(this::convertProtobufType).collect(Collectors.toSet()));
274+
return type.getArgsList().stream().map(this::getSymbolTypeFqn).flatMap(Collection::stream).toList();
254275
case TUPLE:
255-
return resolvePossibleLazyType("tuple");
276+
return List.of("tuple");
256277
case NONE:
257-
return resolvePossibleLazyType("NoneType");
278+
return List.of("NoneType");
258279
case TYPED_DICT:
259-
return resolvePossibleLazyType("dict");
280+
return List.of("dict");
260281
case TYPE_VAR:
261282
return Optional.of(type)
262283
.filter(InferredTypes::filterTypeVar)
263284
.map(SymbolsProtos.Type::getFullyQualifiedName)
264-
.map(this::resolvePossibleLazyType)
265-
.orElse(PythonType.UNKNOWN);
285+
.map(List::of)
286+
.orElseGet(List::of);
266287
default:
267-
return PythonType.UNKNOWN;
288+
return List.of();
268289
}
269290
}
291+
270292
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ private FunctionType buildFunctionType(FunctionDef functionDef) {
283283
TypeAnnotation typeAnnotation = functionDef.returnTypeAnnotation();
284284
if (typeAnnotation != null) {
285285
PythonType returnType = typeAnnotation.expression().typeV2();
286-
functionTypeBuilder.withReturnType(returnType);
286+
functionTypeBuilder.withReturnType(returnType == PythonType.UNKNOWN ? returnType : new ObjectType(returnType, TypeSource.TYPE_HINT));
287287
functionTypeBuilder.withTypeOrigin(TypeOrigin.LOCAL);
288288
}
289289
FunctionType functionType = functionTypeBuilder.build();

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.sonar.plugins.python.api.types.InferredType;
4545
import org.sonar.python.semantic.ClassSymbolImpl;
4646
import org.sonar.python.semantic.FunctionSymbolImpl;
47+
import org.sonar.python.semantic.v2.ObjectTypeBuilder;
4748
import org.sonar.python.types.DeclaredType;
4849
import org.sonar.python.types.HasTypeDependencies;
4950
import org.sonar.python.types.InferredTypes;
@@ -193,7 +194,12 @@ public PythonType typeV2() {
193194
PythonType calleeType = callee().typeV2();
194195
TypeSource typeSource = computeTypeSource(calleeType);
195196
PythonType pythonType = returnTypeOfCall(calleeType);
196-
return pythonType != PythonType.UNKNOWN ? new ObjectType(pythonType, typeSource) : PythonType.UNKNOWN;
197+
if (pythonType instanceof ObjectType objectType) {
198+
return ObjectTypeBuilder.fromObjectType(objectType)
199+
.withTypeSource(typeSource)
200+
.build();
201+
}
202+
return pythonType;
197203
}
198204

199205
private TypeSource computeTypeSource(PythonType calleeType) {
@@ -215,7 +221,7 @@ boolean isCalleeLocallyDefinedFunction(PythonType pythonType) {
215221

216222
static PythonType returnTypeOfCall(PythonType calleeType) {
217223
if (calleeType instanceof ClassType classType) {
218-
return classType;
224+
return new ObjectType(classType);
219225
}
220226
if (calleeType instanceof FunctionType functionType) {
221227
return functionType.returnType();
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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;
21+
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
import org.sonar.python.semantic.ProjectLevelSymbolTable;
25+
import org.sonar.python.types.protobuf.SymbolsProtos;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
class SymbolsModuleTypeProviderTest {
30+
31+
SymbolsModuleTypeProvider symbolsModuleTypeProvider;
32+
33+
@BeforeEach
34+
void setUp() {
35+
ProjectLevelSymbolTable empty = ProjectLevelSymbolTable.empty();
36+
TypeShed typeShed = new TypeShed(empty);
37+
ProjectLevelTypeTable projectLevelTypeTable = new ProjectLevelTypeTable(empty, typeShed);
38+
LazyTypesContext lazyTypesContext = projectLevelTypeTable.lazyTypesContext();
39+
symbolsModuleTypeProvider = new SymbolsModuleTypeProvider(empty, typeShed, lazyTypesContext);
40+
}
41+
42+
@Test
43+
void getSymbolTypeFqnSpecialForm() {
44+
var type = SymbolsProtos.Type.newBuilder().setKind(SymbolsProtos.TypeKind.INSTANCE).setFullyQualifiedName("typing._SpecialForm").build();
45+
assertThat(symbolsModuleTypeProvider.getSymbolTypeFqn(type)).isEmpty();
46+
}
47+
48+
@Test
49+
void getSymbolTypeFqnTypedDict() {
50+
var type = SymbolsProtos.Type.newBuilder().setKind(SymbolsProtos.TypeKind.TYPED_DICT).build();
51+
assertThat(symbolsModuleTypeProvider.getSymbolTypeFqn(type)).containsExactly("dict");
52+
}
53+
54+
@Test
55+
void getSymbolTypeFqnType() {
56+
var type = SymbolsProtos.Type.newBuilder().setKind(SymbolsProtos.TypeKind.TYPE).build();
57+
assertThat(symbolsModuleTypeProvider.getSymbolTypeFqn(type)).containsExactly("type");
58+
}
59+
60+
@Test
61+
void getSymbolTypeFqnTypeAlias() {
62+
var type = SymbolsProtos.Type.newBuilder().setKind(SymbolsProtos.TypeKind.INSTANCE).setFullyQualifiedName("builtins.float").build();
63+
var typeAlias = SymbolsProtos.Type.newBuilder().setKind(SymbolsProtos.TypeKind.TYPE_ALIAS).addArgs(type).build();
64+
assertThat(symbolsModuleTypeProvider.getSymbolTypeFqn(typeAlias)).containsExactly("float");
65+
}
66+
67+
@Test
68+
void getSymbolTypeObject() {
69+
var type = SymbolsProtos.Type.newBuilder().setKind(SymbolsProtos.TypeKind.INSTANCE).setFullyQualifiedName("object").build();
70+
assertThat(symbolsModuleTypeProvider.getSymbolTypeFqn(type)).isEmpty();
71+
}
72+
}

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

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ void typeSourceIsExactByDefault() {
431431

432432
CallExpression callExpressionSpy = Mockito.spy(callExpression);
433433
Expression calleeSpy = Mockito.spy(callExpression.callee());
434-
FunctionType functionType = new FunctionType("foo", List.of(), List.of(), INT_TYPE, TypeOrigin.STUB, false, false, false, false, null, null);
434+
FunctionType functionType = new FunctionType("foo", List.of(), List.of(), new ObjectType(INT_TYPE), TypeOrigin.STUB, false, false, false, false, null, null);
435435
Mockito.when(calleeSpy.typeV2()).thenReturn(functionType);
436436
Mockito.when(callExpressionSpy.callee()).thenReturn(calleeSpy);
437437

@@ -1873,12 +1873,12 @@ void imported_ambiguous_symbol() {
18731873
UnionType acosType = (UnionType) ((ExpressionStatement) fileInput.statements().statements().get(1)).expressions().get(0).typeV2();
18741874
assertThat(acosType.candidates()).allMatch(p -> p instanceof FunctionType);
18751875
assertThat(acosType.candidates()).extracting(PythonType::name).containsExactly("acos", "acos");
1876-
assertThat(acosType.candidates()).map(FunctionType.class::cast).extracting(FunctionType::returnType).containsExactly(FLOAT_TYPE, FLOAT_TYPE);
1876+
assertThat(acosType.candidates()).map(FunctionType.class::cast).extracting(FunctionType::returnType).extracting(PythonType::unwrappedType).containsExactly(FLOAT_TYPE, FLOAT_TYPE);
18771877

18781878
UnionType atanType = (UnionType) ((ExpressionStatement) fileInput.statements().statements().get(2)).expressions().get(0).typeV2();
18791879
assertThat(atanType.candidates()).allMatch(p -> p instanceof FunctionType);
18801880
assertThat(atanType.candidates()).extracting(PythonType::name).containsExactly("atan", "atan");
1881-
assertThat(atanType.candidates()).map(FunctionType.class::cast).extracting(FunctionType::returnType).containsExactly(FLOAT_TYPE, FLOAT_TYPE);
1881+
assertThat(atanType.candidates()).map(FunctionType.class::cast).extracting(FunctionType::returnType).extracting(PythonType::unwrappedType).containsExactly(FLOAT_TYPE, FLOAT_TYPE);
18821882
}
18831883

18841884
@Test
@@ -1899,11 +1899,11 @@ void imported_ambiguous_symbol_try_except() {
18991899
.get();
19001900
UnionType acosType1 = (UnionType) acosExpr1.typeV2();
19011901
assertThat(acosType1.candidates()).allMatch(p -> p instanceof FunctionType);
1902-
assertThat(acosType1.candidates()).map(FunctionType.class::cast).extracting(FunctionType::returnType).containsExactly(FLOAT_TYPE, FLOAT_TYPE);
1902+
assertThat(acosType1.candidates()).map(FunctionType.class::cast).extracting(FunctionType::returnType).extracting(PythonType::unwrappedType).containsExactly(FLOAT_TYPE, FLOAT_TYPE);
19031903

19041904
UnionType acosType2 = (UnionType) ((ExpressionStatement) fileInput.statements().statements().get(1)).expressions().get(0).typeV2();
19051905
assertThat(acosType2.candidates()).allMatch(p -> p instanceof FunctionType);
1906-
assertThat(acosType2.candidates()).map(FunctionType.class::cast).extracting(FunctionType::returnType).containsExactly(FLOAT_TYPE, FLOAT_TYPE);
1906+
assertThat(acosType2.candidates()).map(FunctionType.class::cast).extracting(FunctionType::returnType).extracting(PythonType::unwrappedType).containsExactly(FLOAT_TYPE, FLOAT_TYPE);
19071907
}
19081908

19091909
@Test
@@ -1988,9 +1988,9 @@ def bar(self): ...
19881988
.map(ClassType.class::cast)
19891989
.get();
19901990

1991-
assertThat(((ExpressionStatement) fileInput.statements().statements().get(6)).expressions().get(0).typeV2()).isInstanceOf(ObjectType.class);
1991+
assertThat(((ExpressionStatement) fileInput.statements().statements().get(6)).expressions().get(0).typeV2()).isInstanceOf(UnionType.class);
19921992
UnionType unionType = (UnionType) ((ExpressionStatement) fileInput.statements().statements().get(6)).expressions().get(0).typeV2().unwrappedType();
1993-
assertThat(unionType.candidates()).containsExactlyInAnyOrder(classA, classB);
1993+
assertThat(unionType.candidates()).extracting(PythonType::unwrappedType).containsExactlyInAnyOrder(classA, classB);
19941994
}
19951995

19961996
@Test
@@ -2117,7 +2117,7 @@ void resolvedTypingLazyType() {
21172117
""");
21182118
FunctionType functionType = ((FunctionType) ((ExpressionStatement) fileInput.statements().statements().get(1)).expressions().get(0).typeV2());
21192119
PythonType returnType = functionType.returnType();
2120-
assertThat(returnType).isInstanceOf(ClassType.class);
2120+
assertThat(returnType.unwrappedType()).isInstanceOf(ClassType.class);
21212121
assertThatThrownBy(() -> functionType.resolveLazyReturnType(PythonType.UNKNOWN))
21222122
.isInstanceOf(IllegalStateException.class)
21232123
.hasMessage("Trying to resolve an already resolved lazy type.");
@@ -2333,10 +2333,9 @@ def foo(x):
23332333
var bType = ((ExpressionStatement) statements.get(statements.size() - 2)).expressions().get(0).typeV2();
23342334
var cType = ((ExpressionStatement) statements.get(statements.size() - 1)).expressions().get(0).typeV2();
23352335

2336-
Assertions.assertThat(aType).isInstanceOf(ObjectType.class);
2337-
Assertions.assertThat(aType.unwrappedType()).isInstanceOf(UnionType.class);
2338-
var candidates = ((UnionType) aType.unwrappedType()).candidates();
2339-
Assertions.assertThat(candidates).containsOnly(INT_TYPE, STR_TYPE);
2336+
Assertions.assertThat(aType).isInstanceOf(UnionType.class);
2337+
Assertions.assertThat(((UnionType) aType).candidates()).extracting(PythonType::unwrappedType).containsOnly(INT_TYPE, STR_TYPE);
2338+
23402339

23412340
Assertions.assertThat(bType).isSameAs(PythonType.UNKNOWN);
23422341
Assertions.assertThat(cType).isSameAs(PythonType.UNKNOWN);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ void declaredTypes() {
152152
@Test
153153
void declared_return_type() {
154154
FunctionType functionType = functionType("def fn() -> int: ...");
155-
assertThat(functionType.returnType()).isEqualTo(INT_TYPE);
155+
assertThat(functionType.returnType().unwrappedType()).isEqualTo(INT_TYPE);
156156
functionType = functionType("def fn() -> unknown: ...");
157157
assertThat(functionType.returnType()).isEqualTo(PythonType.UNKNOWN);
158158
}

0 commit comments

Comments
 (0)