Skip to content

Commit ea4a0e0

Browse files
SONARPY-2128 Implement FunctionDescriptorToPythonTypeConverter (#1970)
1 parent 7fc2ede commit ea4a0e0

File tree

9 files changed

+237
-22
lines changed

9 files changed

+237
-22
lines changed

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.sonar.python.types.v2.PythonType;
4040
import org.sonar.python.types.v2.SimpleTypeWrapper;
4141
import org.sonar.python.types.v2.TypeOrigin;
42+
import org.sonar.python.types.v2.TypeWrapper;
4243

4344
import static org.sonar.python.tree.TreeUtils.locationInFile;
4445

@@ -52,7 +53,7 @@ public class FunctionTypeBuilder implements TypeBuilder<FunctionType> {
5253
private boolean hasDecorators;
5354
private boolean isInstanceMethod;
5455
private PythonType owner;
55-
private PythonType returnType = PythonType.UNKNOWN;
56+
private TypeWrapper returnType = TypeWrapper.UNKNOWN_TYPE_WRAPPER;
5657
private TypeOrigin typeOrigin = TypeOrigin.STUB;
5758
private LocationInFile definitionLocation;
5859

@@ -80,6 +81,11 @@ public FunctionTypeBuilder(String name) {
8081
public FunctionTypeBuilder() {
8182
}
8283

84+
public FunctionTypeBuilder withName(String name) {
85+
this.name = name;
86+
return this;
87+
}
88+
8389
public FunctionTypeBuilder withHasVariadicParameter(boolean hasVariadicParameter) {
8490
this.hasVariadicParameter = hasVariadicParameter;
8591
return this;
@@ -111,6 +117,11 @@ public FunctionTypeBuilder withInstanceMethod(boolean instanceMethod) {
111117
}
112118

113119
public FunctionTypeBuilder withReturnType(PythonType returnType) {
120+
withReturnType(new LazyTypeWrapper(returnType));
121+
return this;
122+
}
123+
124+
public FunctionTypeBuilder withReturnType(TypeWrapper returnType) {
114125
this.returnType = returnType;
115126
return this;
116127
}

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@
4242
import org.sonar.python.types.protobuf.SymbolsProtos;
4343
import org.sonar.python.types.v2.ClassType;
4444
import org.sonar.python.types.v2.FunctionType;
45-
import org.sonar.python.types.v2.LazyType;
4645
import org.sonar.python.types.v2.LazyTypeWrapper;
4746
import org.sonar.python.types.v2.Member;
4847
import org.sonar.python.types.v2.ModuleType;
4948
import org.sonar.python.types.v2.ObjectType;
5049
import org.sonar.python.types.v2.ParameterV2;
5150
import org.sonar.python.types.v2.PythonType;
51+
import org.sonar.python.types.v2.SimpleTypeWrapper;
5252
import org.sonar.python.types.v2.TypeOrigin;
5353
import org.sonar.python.types.v2.TypeWrapper;
5454
import org.sonar.python.types.v2.UnionType;
@@ -140,18 +140,16 @@ private PythonType convertToFunctionType(FunctionSymbol symbol, Map<Symbol, Pyth
140140
.withHasVariadicParameter(symbol.hasVariadicParameter())
141141
.withDefinitionLocation(symbol.definitionLocation());
142142
FunctionType functionType = functionTypeBuilder.build();
143-
if (returnType instanceof LazyType lazyType) {
144-
lazyType.addConsumer(functionType::resolveLazyReturnType);
145-
}
146143
createdTypesBySymbol.put(symbol, functionType);
147144
return functionType;
148145
}
149146

150-
private PythonType getReturnTypeFromSymbol(FunctionSymbol symbol) {
147+
private TypeWrapper getReturnTypeFromSymbol(FunctionSymbol symbol) {
151148
var returnTypeFqns = getReturnTypeFqn(symbol);
152149
var returnTypeList = returnTypeFqns.stream().map(lazyTypesContext::getOrCreateLazyTypeWrapper).map(ObjectType::new).toList();
153150
//TODO Support type unions (SONARPY-2132)
154-
return returnTypeList.size() == 1 ? returnTypeList.get(0) : PythonType.UNKNOWN;
151+
var type = returnTypeList.size() == 1 ? returnTypeList.get(0) : PythonType.UNKNOWN;
152+
return new SimpleTypeWrapper(type);
155153
}
156154

157155
PythonType resolvePossibleLazyType(String fullyQualifiedName) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public class AnyDescriptorToPythonTypeConverter {
3434
public AnyDescriptorToPythonTypeConverter(LazyTypesContext lazyTypesContext) {
3535
this.lazyTypesContext = lazyTypesContext;
3636
converters = new EnumMap<>(Map.of(
37-
Descriptor.Kind.CLASS, new ClassDescriptorToPythonTypeConverter()
37+
Descriptor.Kind.CLASS, new ClassDescriptorToPythonTypeConverter(),
38+
Descriptor.Kind.FUNCTION, new FunctionDescriptorToPythonTypeConverter()
3839
));
3940
}
4041

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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.converter;
21+
22+
import java.util.List;
23+
import java.util.Optional;
24+
import org.sonar.python.index.Descriptor;
25+
import org.sonar.python.index.FunctionDescriptor;
26+
import org.sonar.python.semantic.v2.FunctionTypeBuilder;
27+
import org.sonar.python.types.v2.ParameterV2;
28+
import org.sonar.python.types.v2.PythonType;
29+
import org.sonar.python.types.v2.TypeOrigin;
30+
31+
public class FunctionDescriptorToPythonTypeConverter implements DescriptorToPythonTypeConverter {
32+
33+
private final ParameterConverter parameterConverter;
34+
35+
public FunctionDescriptorToPythonTypeConverter() {
36+
parameterConverter = new ParameterConverter();
37+
}
38+
39+
public PythonType convert(ConversionContext ctx, FunctionDescriptor from) {
40+
var parameters = from.parameters()
41+
.stream()
42+
.map(parameter -> parameterConverter.convert(ctx, parameter))
43+
.toList();
44+
45+
var returnType = Optional.ofNullable(from.annotatedReturnTypeName())
46+
.map(fqn -> (PythonType) ctx.lazyTypesContext().getOrCreateLazyType(fqn))
47+
.orElse(PythonType.UNKNOWN);
48+
49+
var typeOrigin = TypeOrigin.LOCAL;
50+
51+
var hasVariadicParameter = hasVariadicParameter(parameters);
52+
53+
var toBuilder = new FunctionTypeBuilder()
54+
.withOwner(ctx.currentParent())
55+
.withName(from.name())
56+
.withParameters(parameters)
57+
.withReturnType(returnType)
58+
.withTypeOrigin(typeOrigin)
59+
.withAsynchronous(from.isAsynchronous())
60+
.withHasDecorators(from.hasDecorators())
61+
.withInstanceMethod(from.isInstanceMethod())
62+
.withHasVariadicParameter(hasVariadicParameter)
63+
.withDefinitionLocation(from.definitionLocation());
64+
65+
return toBuilder.build();
66+
}
67+
68+
private static boolean hasVariadicParameter(List<ParameterV2> parameters) {
69+
return parameters.stream()
70+
.anyMatch(p -> p.isKeywordVariadic() || p.isPositionalVariadic());
71+
}
72+
73+
@Override
74+
public PythonType convert(ConversionContext ctx, Descriptor from) {
75+
if (from instanceof FunctionDescriptor functionDescriptor) {
76+
return convert(ctx, functionDescriptor);
77+
}
78+
throw new IllegalArgumentException("Unsupported Descriptor");
79+
}
80+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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.converter;
21+
22+
import java.util.Optional;
23+
import org.sonar.python.index.FunctionDescriptor;
24+
import org.sonar.python.types.v2.LazyTypeWrapper;
25+
import org.sonar.python.types.v2.ObjectType;
26+
import org.sonar.python.types.v2.ParameterV2;
27+
import org.sonar.python.types.v2.PythonType;
28+
import org.sonar.python.types.v2.SimpleTypeWrapper;
29+
import org.sonar.python.types.v2.TypeWrapper;
30+
31+
public class ParameterConverter {
32+
33+
public ParameterV2 convert(ConversionContext ctx, FunctionDescriptor.Parameter parameter) {
34+
var typeWrapper = Optional.ofNullable(parameter.annotatedType())
35+
.map(fqn -> (PythonType) ctx.lazyTypesContext().getOrCreateLazyType(fqn))
36+
.map(lt -> (TypeWrapper) new LazyTypeWrapper(lt))
37+
.orElseGet(() -> new SimpleTypeWrapper(PythonType.UNKNOWN));
38+
39+
var type = new ObjectType(typeWrapper);
40+
41+
return new ParameterV2(parameter.name(),
42+
new SimpleTypeWrapper(type),
43+
parameter.hasDefaultValue(),
44+
parameter.isKeywordOnly(),
45+
parameter.isPositionalOnly(),
46+
parameter.isKeywordVariadic(),
47+
parameter.isPositionalVariadic(),
48+
parameter.location());
49+
}
50+
51+
}

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public final class FunctionType implements PythonType {
3434
private final String name;
3535
private final List<PythonType> attributes;
3636
private final List<ParameterV2> parameters;
37-
private PythonType returnType;
37+
private TypeWrapper returnType;
3838
private final TypeOrigin typeOrigin;
3939
private final boolean isAsynchronous;
4040
private final boolean hasDecorators;
@@ -50,7 +50,7 @@ public FunctionType(
5050
String name,
5151
List<PythonType> attributes,
5252
List<ParameterV2> parameters,
53-
PythonType returnType,
53+
TypeWrapper returnType,
5454
TypeOrigin typeOrigin,
5555
boolean isAsynchronous,
5656
boolean hasDecorators,
@@ -101,7 +101,7 @@ public List<ParameterV2> parameters() {
101101
}
102102

103103
public PythonType returnType() {
104-
return TypeUtils.resolved(returnType);
104+
return returnType.type();
105105
}
106106

107107
public boolean isAsynchronous() {
@@ -125,13 +125,6 @@ public PythonType owner() {
125125
return owner;
126126
}
127127

128-
public void resolveLazyReturnType(PythonType pythonType) {
129-
if (!(returnType instanceof LazyType)) {
130-
throw new IllegalStateException("Trying to resolve an already resolved lazy type.");
131-
}
132-
this.returnType = pythonType;
133-
}
134-
135128
public TypeOrigin typeOrigin() {
136129
return typeOrigin;
137130
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import org.sonar.python.types.v2.ClassType;
6262
import org.sonar.python.types.v2.FunctionType;
6363
import org.sonar.python.types.v2.LazyType;
64+
import org.sonar.python.types.v2.SimpleTypeWrapper;
6465
import org.sonar.python.types.v2.TypeWrapper;
6566
import org.sonar.python.types.v2.ModuleType;
6667
import org.sonar.python.types.v2.ObjectType;
@@ -436,7 +437,7 @@ void typeSourceIsExactByDefault() {
436437

437438
CallExpression callExpressionSpy = Mockito.spy(callExpression);
438439
Expression calleeSpy = Mockito.spy(callExpression.callee());
439-
FunctionType functionType = new FunctionType("foo", List.of(), List.of(), new ObjectType(INT_TYPE), TypeOrigin.STUB, false, false, false, false, null, null);
440+
FunctionType functionType = new FunctionType("foo", List.of(), List.of(), new SimpleTypeWrapper(new ObjectType(INT_TYPE)), TypeOrigin.STUB, false, false, false, false, null, null);
440441
Mockito.when(calleeSpy.typeV2()).thenReturn(functionType);
441442
Mockito.when(callExpressionSpy.callee()).thenReturn(calleeSpy);
442443

@@ -2273,9 +2274,6 @@ void resolvedTypingLazyType() {
22732274
FunctionType functionType = ((FunctionType) ((ExpressionStatement) fileInput.statements().statements().get(1)).expressions().get(0).typeV2());
22742275
PythonType returnType = functionType.returnType();
22752276
assertThat(returnType.unwrappedType()).isInstanceOf(ClassType.class);
2276-
assertThatThrownBy(() -> functionType.resolveLazyReturnType(PythonType.UNKNOWN))
2277-
.isInstanceOf(IllegalStateException.class)
2278-
.hasMessage("Trying to resolve an already resolved lazy type.");
22792277
}
22802278

22812279
@Test

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@
2323
import org.assertj.core.api.Assertions;
2424
import org.junit.jupiter.api.Test;
2525
import org.mockito.Mockito;
26+
import org.sonar.plugins.python.api.LocationInFile;
2627
import org.sonar.python.index.AmbiguousDescriptor;
2728
import org.sonar.python.index.ClassDescriptor;
2829
import org.sonar.python.index.Descriptor;
30+
import org.sonar.python.index.FunctionDescriptor;
2931
import org.sonar.python.semantic.v2.ClassTypeBuilder;
3032
import org.sonar.python.semantic.v2.LazyTypesContext;
3133
import org.sonar.python.types.v2.ClassType;
34+
import org.sonar.python.types.v2.FunctionType;
3235
import org.sonar.python.types.v2.LazyType;
3336
import org.sonar.python.types.v2.Member;
3437
import org.sonar.python.types.v2.PythonType;
@@ -95,6 +98,49 @@ void classDescriptorConversionTest() {
9598
.containsOnly("member");
9699
}
97100

101+
@Test
102+
void functionDescriptorConversionTest() {
103+
var lazyTypesContext = Mockito.mock(LazyTypesContext.class);
104+
var converter = new AnyDescriptorToPythonTypeConverter(lazyTypesContext);
105+
var descriptor = Mockito.mock(FunctionDescriptor.class);
106+
107+
var returnTypeName = "Returned";
108+
var resolvedReturnType = new ClassTypeBuilder().withName(returnTypeName).build();
109+
110+
Mockito.when(descriptor.kind()).thenReturn(Descriptor.Kind.FUNCTION);
111+
Mockito.when(descriptor.name()).thenReturn("Sample");
112+
Mockito.when(descriptor.annotatedReturnTypeName()).thenReturn(returnTypeName);
113+
Mockito.when(descriptor.parameters()).thenReturn(List.of(
114+
new FunctionDescriptor.Parameter(
115+
"p1",
116+
"Returned",
117+
false,
118+
false,
119+
true,
120+
false,
121+
false,
122+
new LocationInFile("m1", 1, 10, 1, 15))
123+
));
124+
125+
Mockito.when(lazyTypesContext.getOrCreateLazyType(returnTypeName))
126+
.thenReturn(new LazyType(returnTypeName, lazyTypesContext));
127+
128+
Mockito.when(lazyTypesContext.resolveLazyType(Mockito.argThat(lt -> returnTypeName.equals(lt.fullyQualifiedName()))))
129+
.thenReturn(resolvedReturnType);
130+
131+
var type = (FunctionType) converter.convert(descriptor);
132+
Assertions.assertThat(type.name()).isEqualTo("Sample");
133+
134+
Assertions.assertThat(type.parameters()).hasSize(1);
135+
var parameter = type.parameters().get(0);
136+
Assertions.assertThat(parameter.declaredType())
137+
.extracting(TypeWrapper::type)
138+
.extracting(PythonType::unwrappedType)
139+
.isSameAs(resolvedReturnType);
140+
Assertions.assertThat(type.returnType())
141+
.extracting(PythonType::unwrappedType)
142+
.isSameAs(resolvedReturnType);
143+
}
98144

99145

100146
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.converter;
21+
22+
import org.assertj.core.api.Assertions;
23+
import org.junit.jupiter.api.Test;
24+
import org.mockito.Mockito;
25+
import org.sonar.python.index.ClassDescriptor;
26+
27+
class FunctionDescriptorToPythonTypeConverterTest {
28+
@Test
29+
void unsupportedClassTest() {
30+
var ctx = Mockito.mock(ConversionContext.class);
31+
var descriptor = Mockito.mock(ClassDescriptor.class);
32+
var converter = new FunctionDescriptorToPythonTypeConverter();
33+
Assertions.assertThatThrownBy(() -> converter.convert(ctx, descriptor))
34+
.isInstanceOf(IllegalArgumentException.class)
35+
.hasMessage("Unsupported Descriptor");
36+
}
37+
}

0 commit comments

Comments
 (0)