Skip to content

Commit c484eb2

Browse files
ghislainpiotguillaume-dequenne
authored andcommitted
SONARPY-2118 Ensure function parameter types are correctly mapped in V2 type model
1 parent fadff2f commit c484eb2

File tree

4 files changed

+139
-15
lines changed

4 files changed

+139
-15
lines changed

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.sonar.plugins.python.api.tree.AnyParameter;
2929
import org.sonar.plugins.python.api.tree.FunctionDef;
3030
import org.sonar.plugins.python.api.tree.Name;
31+
import org.sonar.plugins.python.api.tree.Parameter;
3132
import org.sonar.plugins.python.api.tree.ParameterList;
3233
import org.sonar.plugins.python.api.tree.Token;
3334
import org.sonar.plugins.python.api.tree.Tree;
@@ -58,7 +59,7 @@ public class FunctionTypeBuilder implements TypeBuilder<FunctionType> {
5859
private static final String CLASS_METHOD_DECORATOR = "classmethod";
5960
private static final String STATIC_METHOD_DECORATOR = "staticmethod";
6061

61-
public FunctionTypeBuilder fromFunctionDef(FunctionDef functionDef, @Nullable String fileId) {
62+
public FunctionTypeBuilder fromFunctionDef(FunctionDef functionDef, @Nullable String fileId, ProjectLevelTypeTable projectLevelTypeTable) {
6263
this.name = functionDef.name().name();
6364
this.attributes = new ArrayList<>();
6465
this.parameters = new ArrayList<>();
@@ -67,7 +68,7 @@ public FunctionTypeBuilder fromFunctionDef(FunctionDef functionDef, @Nullable St
6768
isInstanceMethod = isInstanceMethod(functionDef);
6869
ParameterList parameterList = functionDef.parameters();
6970
if (parameterList != null) {
70-
createParameterNames(parameterList.all(), fileId);
71+
createParameterNames(parameterList.all(), fileId, projectLevelTypeTable);
7172
}
7273
return this;
7374
}
@@ -143,30 +144,30 @@ public FunctionTypeBuilder withOwner(PythonType owner) {
143144
return this;
144145
}
145146

146-
private void createParameterNames(List<AnyParameter> parameterTrees, @Nullable String fileId) {
147+
private void createParameterNames(List<AnyParameter> parameterTrees, @Nullable String fileId, ProjectLevelTypeTable projectLevelTypeTable) {
147148
ParameterState parameterState = new ParameterState();
148149
parameterState.positionalOnly = parameterTrees.stream().anyMatch(param -> Optional.of(param)
149150
.filter(p -> p.is(Tree.Kind.PARAMETER))
150-
.map(p -> ((org.sonar.plugins.python.api.tree.Parameter) p).starToken())
151+
.map(p -> ((Parameter) p).starToken())
151152
.map(Token::value)
152153
.filter("/"::equals)
153154
.isPresent()
154155
);
155156
for (AnyParameter anyParameter : parameterTrees) {
156157
if (anyParameter.is(Tree.Kind.PARAMETER)) {
157-
addParameter((org.sonar.plugins.python.api.tree.Parameter) anyParameter, fileId, parameterState);
158+
addParameter((Parameter) anyParameter, fileId, parameterState, projectLevelTypeTable);
158159
} else {
159160
parameters.add(new ParameterV2(null, new SimpleTypeWrapper(PythonType.UNKNOWN), false,
160161
parameterState.keywordOnly, parameterState.positionalOnly, false, false, locationInFile(anyParameter, fileId)));
161162
}
162163
}
163164
}
164165

165-
private void addParameter(org.sonar.plugins.python.api.tree.Parameter parameter, @Nullable String fileId, ParameterState parameterState) {
166+
private void addParameter(Parameter parameter, @Nullable String fileId, ParameterState parameterState, ProjectLevelTypeTable projectLevelTypeTable) {
166167
Name parameterName = parameter.name();
167168
Token starToken = parameter.starToken();
168169
if (parameterName != null) {
169-
ParameterType parameterType = getParameterType(parameter);
170+
ParameterType parameterType = getParameterType(parameter, projectLevelTypeTable);
170171
this.parameters.add(new ParameterV2(parameterName.name(), new LazyTypeWrapper(parameterType.pythonType()), parameter.defaultValue() != null,
171172
parameterState.keywordOnly, parameterState.positionalOnly, parameterType.isKeywordVariadic(), parameterType.isPositionalVariadic(), locationInFile(parameter, fileId)));
172173
if (starToken != null) {
@@ -185,26 +186,28 @@ private void addParameter(org.sonar.plugins.python.api.tree.Parameter parameter,
185186
}
186187
}
187188

188-
private ParameterType getParameterType(org.sonar.plugins.python.api.tree.Parameter parameter) {
189+
private ParameterType getParameterType(Parameter parameter, ProjectLevelTypeTable projectLevelTypeTable) {
189190
boolean isPositionalVariadic = false;
190191
boolean isKeywordVariadic = false;
191192
Token starToken = parameter.starToken();
193+
var parameterType = Optional.ofNullable(parameter.name()).map(Name::typeV2).orElse(PythonType.UNKNOWN);
192194
if (starToken != null) {
193195
// https://docs.python.org/3/reference/compound_stmts.html#function-definitions
194196
hasVariadicParameter = true;
195197
if ("*".equals(starToken.value())) {
196198
// if the form “*identifier” is present, it is initialized to a tuple receiving any excess positional parameters
197199
isPositionalVariadic = true;
198200
// Should set PythonType to TUPLE
201+
parameterType = projectLevelTypeTable.getBuiltinsModule().resolveMember("tuple").orElse(PythonType.UNKNOWN);
199202
}
200203
if ("**".equals(starToken.value())) {
201204
// If the form “**identifier” is present, it is initialized to a new ordered mapping receiving any excess keyword arguments
202205
isKeywordVariadic = true;
203206
// Should set PythonType to DICT
207+
parameterType = projectLevelTypeTable.getBuiltinsModule().resolveMember("dict").orElse(PythonType.UNKNOWN);
204208
}
205209
}
206-
// TODO: SONARPY-1773 handle parameter declared types
207-
return new ParameterType(PythonType.UNKNOWN, isKeywordVariadic, isPositionalVariadic);
210+
return new ParameterType(parameterType, isKeywordVariadic, isPositionalVariadic);
208211
}
209212

210213
public static class ParameterState {

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
@@ -271,7 +271,7 @@ public void visitFunctionDef(FunctionDef functionDef) {
271271

272272
private FunctionType buildFunctionType(FunctionDef functionDef) {
273273
FunctionTypeBuilder functionTypeBuilder = new FunctionTypeBuilder()
274-
.fromFunctionDef(functionDef, fileId)
274+
.fromFunctionDef(functionDef, fileId, projectLevelTypeTable)
275275
.withDefinitionLocation(locationInFile(functionDef.name(), fileId));
276276
ClassType owner = null;
277277
if (currentType() instanceof ClassType classType) {

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

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import org.sonar.python.PythonTestUtils;
5555
import org.sonar.python.semantic.ClassSymbolImpl;
5656
import org.sonar.python.semantic.ProjectLevelSymbolTable;
57+
import org.sonar.python.tree.ExpressionStatementImpl;
5758
import org.sonar.python.tree.TreeUtils;
5859
import org.sonar.python.types.v2.ClassType;
5960
import org.sonar.python.types.v2.FunctionType;
@@ -369,9 +370,11 @@ def foo(param: int):
369370
""");
370371

371372
var functionDef = (FunctionDef) root.statements().statements().get(0);
372-
var lastExpressionStatement = (ExpressionStatement) functionDef.body().statements().get(functionDef.body().statements().size() -1);
373-
Assertions.assertThat(lastExpressionStatement.expressions().get(0).typeV2().unwrappedType()).isEqualTo(INT_TYPE);
374-
Assertions.assertThat(lastExpressionStatement.expressions().get(0).typeV2().typeSource()).isEqualTo(TypeSource.TYPE_HINT);
373+
var lastExpressionStatement = (ExpressionStatement) functionDef.body().statements().get(functionDef.body().statements().size() - 1);
374+
assertThat(lastExpressionStatement.expressions().get(0).typeV2().unwrappedType()).isEqualTo(INT_TYPE);
375+
assertThat(lastExpressionStatement.expressions().get(0).typeV2().typeSource()).isEqualTo(TypeSource.TYPE_HINT);
376+
377+
assertThat(((FunctionType) functionDef.name().typeV2()).parameters().get(0).declaredType().type().unwrappedType()).isEqualTo(INT_TYPE);
375378
}
376379

377380
@Test
@@ -584,6 +587,124 @@ def foo3(a: int = "123"):
584587
Assertions.assertThat(lastType.unwrappedType()).isEqualTo(INT_TYPE);
585588
}
586589

590+
@Test
591+
void inferFunctionParameterTypes() {
592+
FileInput root = inferTypes("""
593+
def foo(param: int, *args, **kwargs):
594+
...
595+
""");
596+
597+
var functionDef = (FunctionDef) root.statements().statements().get(0);
598+
assertThat(((FunctionType) functionDef.name().typeV2()).parameters().get(0).declaredType().type().unwrappedType()).isEqualTo(INT_TYPE);
599+
assertThat(((FunctionType) functionDef.name().typeV2()).parameters().get(1).declaredType().type().unwrappedType()).isEqualTo(TUPLE_TYPE);
600+
assertThat(((FunctionType) functionDef.name().typeV2()).parameters().get(2).declaredType().type().unwrappedType()).isEqualTo(DICT_TYPE);
601+
}
602+
603+
@Test
604+
void inferFunctionParameterTypes2() {
605+
FileInput root = inferTypes("""
606+
class A: ...
607+
class A: ...
608+
def foo(param: A) -> A:
609+
...
610+
""");
611+
612+
var functionDef = (FunctionDef) root.statements().statements().get(2);
613+
assertThat(((FunctionType) functionDef.name().typeV2()).parameters().get(0).declaredType().type().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
614+
assertThat(((FunctionType) functionDef.name().typeV2()).returnType().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
615+
}
616+
617+
@Test
618+
void inferFunctionParameterTypes3() {
619+
FileInput root = inferTypes("""
620+
class A:
621+
def foo():
622+
...
623+
def foo(param: A) -> A:
624+
...
625+
""");
626+
627+
var functionDef = (FunctionDef) root.statements().statements().get(1);
628+
var classType = ((ClassDef) root.statements().statements().get(0)).name().typeV2();
629+
630+
assertThat(((FunctionType) functionDef.name().typeV2()).parameters().get(0).declaredType().type().unwrappedType()).isEqualTo(classType);
631+
assertThat(((FunctionType) functionDef.name().typeV2()).returnType().unwrappedType()).isEqualTo(classType);
632+
}
633+
634+
@Test
635+
void inferFunctionParameterTypes4() {
636+
FileInput root = inferTypes("""
637+
from re import Pattern
638+
Pattern
639+
def foo(param: Pattern) -> Pattern:
640+
...
641+
""");
642+
643+
var functionDef = (FunctionDef) root.statements().statements().get(2);
644+
var patternType = ((Name) ((ExpressionStatementImpl) root.statements().statements().get(1)).expressions().get(0)).typeV2();
645+
646+
assertThat(((FunctionType) functionDef.name().typeV2()).parameters().get(0).declaredType().type().unwrappedType()).isEqualTo(patternType);
647+
assertThat(((FunctionType) functionDef.name().typeV2()).returnType().unwrappedType()).isEqualTo(patternType);
648+
}
649+
650+
@Test
651+
void inferFunctionParameterTypes5() {
652+
FileInput root = inferTypes("""
653+
my_alias = int
654+
def foo(param: my_alias): ...
655+
""");
656+
657+
var functionDef = (FunctionDef) root.statements().statements().get(1);
658+
assertThat(((FunctionType) functionDef.name().typeV2()).parameters().get(0).declaredType().type().unwrappedType()).isEqualTo(INT_TYPE);
659+
}
660+
661+
@Test
662+
void inferFunctionParameterTypes6() {
663+
FileInput root = inferTypes("""
664+
my_alias = int
665+
my_alias = str
666+
def foo(param: my_alias): ...
667+
""");
668+
669+
var functionDef = (FunctionDef) root.statements().statements().get(2);
670+
assertThat(((FunctionType) functionDef.name().typeV2()).parameters().get(0).declaredType().type().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
671+
}
672+
673+
@Test
674+
void inferFunctionParameterTypes7() {
675+
FileInput root = inferTypes("""
676+
a = int
677+
def foo(param: a): ...
678+
""");
679+
var functionDef = (FunctionDef) root.statements().statements().get(1);
680+
assertThat(((FunctionType) functionDef.name().typeV2()).parameters().get(0).declaredType().type().unwrappedType()).isEqualTo(INT_TYPE);
681+
}
682+
683+
@Test
684+
void inferFunctionParameterTypes8() {
685+
FileInput root = inferTypes("""
686+
if cond:
687+
my_alias = int
688+
else:
689+
my_alias = str
690+
def foo(param: my_alias): ...
691+
""");
692+
693+
var functionDef = (FunctionDef) root.statements().statements().get(root.statements().statements().size() - 1);
694+
assertThat(((FunctionType) functionDef.name().typeV2()).parameters().get(0).declaredType().type().unwrappedType()).isEqualTo(PythonType.UNKNOWN);
695+
}
696+
697+
@Test
698+
void inferFunctionReturnTypeType() {
699+
FileInput root = inferTypes("""
700+
from collections import namedtuple
701+
namedtuple
702+
""");
703+
704+
var expr = (ExpressionStatement) root.statements().statements().get(root.statements().statements().size() - 1);
705+
assertThat(expr.expressions().get(0).typeV2()).isInstanceOf(FunctionType.class);
706+
}
707+
587708
@Test
588709
void inferTypeForReassignedBuiltinsInsideFunction() {
589710
FileInput root = inferTypes("""

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
@@ -146,7 +146,7 @@ void declaredTypes() {
146146
// TODO: SONARPY-1776 handle declared return type
147147
FunctionType functionType = functionType("def fn(p1: int): pass");
148148
assertThat(functionType.returnType()).isEqualTo(PythonType.UNKNOWN);
149-
assertThat(functionType.parameters()).extracting(ParameterV2::declaredType).extracting(TypeWrapper::type).containsExactly(PythonType.UNKNOWN);
149+
assertThat(functionType.parameters()).extracting(ParameterV2::declaredType).extracting(TypeWrapper::type).extracting(PythonType::unwrappedType).containsExactly(INT_TYPE);
150150
}
151151

152152
@Test

0 commit comments

Comments
 (0)