Skip to content

Commit c59f45f

Browse files
SONARPY-1764 Infer type of function declaration and function calls
1 parent cb812de commit c59f45f

File tree

9 files changed

+504
-51
lines changed

9 files changed

+504
-51
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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 java.util.ArrayList;
23+
import java.util.List;
24+
import java.util.Objects;
25+
import java.util.Optional;
26+
import javax.annotation.Nullable;
27+
import org.sonar.plugins.python.api.tree.AnyParameter;
28+
import org.sonar.plugins.python.api.tree.FunctionDef;
29+
import org.sonar.plugins.python.api.tree.Name;
30+
import org.sonar.plugins.python.api.tree.ParameterList;
31+
import org.sonar.plugins.python.api.tree.Token;
32+
import org.sonar.plugins.python.api.tree.Tree;
33+
import org.sonar.python.tree.TreeUtils;
34+
import org.sonar.python.types.v2.FunctionType;
35+
import org.sonar.python.types.v2.ParameterV2;
36+
import org.sonar.python.types.v2.PythonType;
37+
38+
import static org.sonar.python.tree.TreeUtils.locationInFile;
39+
40+
public class FunctionTypeBuilder {
41+
42+
private boolean hasVariadicParameter;
43+
private String name;
44+
private List<PythonType> attributes;
45+
private List<ParameterV2> parameters;
46+
private boolean isAsynchronous;
47+
private boolean hasDecorators;
48+
private boolean isInstanceMethod;
49+
private PythonType owner;
50+
private PythonType returnType = PythonType.UNKNOWN;
51+
52+
private static final String CLASS_METHOD_DECORATOR = "classmethod";
53+
private static final String STATIC_METHOD_DECORATOR = "staticmethod";
54+
55+
public FunctionTypeBuilder fromFunctionDef(FunctionDef functionDef) {
56+
this.name = functionDef.name().name();
57+
this.attributes = new ArrayList<>();
58+
this.parameters = new ArrayList<>();
59+
isAsynchronous = functionDef.asyncKeyword() != null;
60+
hasDecorators = !functionDef.decorators().isEmpty();
61+
isInstanceMethod = isInstanceMethod(functionDef);
62+
ParameterList parameterList = functionDef.parameters();
63+
if (parameterList != null) {
64+
createParameterNames(parameterList.all(), null);
65+
}
66+
return this;
67+
}
68+
69+
public FunctionType build() {
70+
return new FunctionType(name, attributes, parameters, returnType, isAsynchronous, hasDecorators, isInstanceMethod, hasVariadicParameter, owner);
71+
}
72+
73+
private static boolean isInstanceMethod(FunctionDef functionDef) {
74+
return !"__new__".equals(functionDef.name().name()) && functionDef.isMethodDefinition() && functionDef.decorators().stream()
75+
.map(decorator -> TreeUtils.decoratorNameFromExpression(decorator.expression()))
76+
.filter(Objects::nonNull)
77+
.noneMatch(decorator -> decorator.equals(STATIC_METHOD_DECORATOR) || decorator.equals(CLASS_METHOD_DECORATOR));
78+
}
79+
80+
public void setOwner(PythonType owner) {
81+
this.owner = owner;
82+
}
83+
84+
private void createParameterNames(List<AnyParameter> parameterTrees, @Nullable String fileId) {
85+
ParameterState parameterState = new ParameterState();
86+
parameterState.positionalOnly = parameterTrees.stream().anyMatch(param -> Optional.of(param)
87+
.filter(p -> p.is(Tree.Kind.PARAMETER))
88+
.map(p -> ((org.sonar.plugins.python.api.tree.Parameter) p).starToken())
89+
.map(Token::value)
90+
.filter("/"::equals)
91+
.isPresent()
92+
);
93+
for (AnyParameter anyParameter : parameterTrees) {
94+
if (anyParameter.is(Tree.Kind.PARAMETER)) {
95+
addParameter((org.sonar.plugins.python.api.tree.Parameter) anyParameter, fileId, parameterState);
96+
} else {
97+
parameters.add(new ParameterV2(null, PythonType.UNKNOWN, false,
98+
parameterState.keywordOnly, parameterState.positionalOnly, false, false, locationInFile(anyParameter, fileId)));
99+
}
100+
}
101+
}
102+
103+
private void addParameter(org.sonar.plugins.python.api.tree.Parameter parameter, @Nullable String fileId, ParameterState parameterState) {
104+
Name parameterName = parameter.name();
105+
Token starToken = parameter.starToken();
106+
if (parameterName != null) {
107+
ParameterType parameterType = getParameterType(parameter);
108+
this.parameters.add(new ParameterV2(parameterName.name(), parameterType.pythonType(), parameter.defaultValue() != null,
109+
parameterState.keywordOnly, parameterState.positionalOnly, parameterType.isKeywordVariadic(), parameterType.isPositionalVariadic(), locationInFile(parameter, fileId)));
110+
if (starToken != null) {
111+
hasVariadicParameter = true;
112+
parameterState.keywordOnly = true;
113+
parameterState.positionalOnly = false;
114+
}
115+
} else if (starToken != null) {
116+
if ("*".equals(starToken.value())) {
117+
parameterState.keywordOnly = true;
118+
parameterState.positionalOnly = false;
119+
}
120+
if ("/".equals(starToken.value())) {
121+
parameterState.positionalOnly = false;
122+
}
123+
}
124+
}
125+
126+
private ParameterType getParameterType(org.sonar.plugins.python.api.tree.Parameter parameter) {
127+
boolean isPositionalVariadic = false;
128+
boolean isKeywordVariadic = false;
129+
Token starToken = parameter.starToken();
130+
if (starToken != null) {
131+
// https://docs.python.org/3/reference/compound_stmts.html#function-definitions
132+
hasVariadicParameter = true;
133+
if ("*".equals(starToken.value())) {
134+
// if the form “*identifier” is present, it is initialized to a tuple receiving any excess positional parameters
135+
isPositionalVariadic = true;
136+
// Should set PythonType to TUPLE
137+
}
138+
if ("**".equals(starToken.value())) {
139+
// If the form “**identifier” is present, it is initialized to a new ordered mapping receiving any excess keyword arguments
140+
isKeywordVariadic = true;
141+
// Should set PythonType to DICT
142+
}
143+
}
144+
// TODO: SONARPY-1773 handle parameter declared types
145+
return new ParameterType(PythonType.UNKNOWN, isKeywordVariadic, isPositionalVariadic);
146+
}
147+
148+
public static class ParameterState {
149+
boolean keywordOnly = false;
150+
boolean positionalOnly = false;
151+
}
152+
153+
record ParameterType(PythonType pythonType, boolean isKeywordVariadic, boolean isPositionalVariadic) { }
154+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ private PythonType converToObjectType(Symbol symbol) {
8989
}
9090

9191
private PythonType convertToFunctionType(FunctionSymbol symbol) {
92-
return new FunctionType(symbol.name(), List.of(), List.of(), PythonType.UNKNOWN);
92+
return new FunctionType(symbol.name(), List.of(), List.of(), PythonType.UNKNOWN, false, false, false, false, null);
9393
}
9494

9595
private PythonType converToClassType(ClassSymbol symbol) {

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

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,13 @@
2020
package org.sonar.python.semantic.v2;
2121

2222
import java.util.ArrayDeque;
23-
import java.util.ArrayList;
2423
import java.util.Collection;
2524
import java.util.Deque;
2625
import java.util.HashMap;
2726
import java.util.List;
2827
import java.util.Optional;
2928
import org.sonar.plugins.python.api.tree.AssignmentStatement;
3029
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
31-
import org.sonar.plugins.python.api.tree.CallExpression;
3230
import org.sonar.plugins.python.api.tree.ClassDef;
3331
import org.sonar.plugins.python.api.tree.Expression;
3432
import org.sonar.plugins.python.api.tree.ExpressionList;
@@ -51,7 +49,6 @@
5149
import org.sonar.python.types.v2.Member;
5250
import org.sonar.python.types.v2.ModuleType;
5351
import org.sonar.python.types.v2.ObjectType;
54-
import org.sonar.python.types.v2.ObjectTypeBuilder;
5552
import org.sonar.python.types.v2.PythonType;
5653

5754
public class TypeInferenceV2 extends BaseTreeVisitor {
@@ -109,15 +106,9 @@ public void visitClassDef(ClassDef classDef) {
109106
@Override
110107
public void visitFunctionDef(FunctionDef functionDef) {
111108
scan(functionDef.decorators());
112-
FunctionType functionType = new FunctionType(functionDef.name().name(), new ArrayList<>(), new ArrayList<>(), PythonType.UNKNOWN);
113-
if (currentType() instanceof ClassType classType) {
114-
if (functionDef.name().symbolV2().hasSingleBindingUsage()) {
115-
classType.members().add(new Member(functionType.name(), functionType));
116-
} else {
117-
// TODO: properly infer type in case of multiple assignments
118-
classType.members().add(new Member(functionType.name(), PythonType.UNKNOWN));
119-
}
120-
}
109+
scan(functionDef.typeParams());
110+
scan(functionDef.parameters());
111+
FunctionType functionType = buildFunctionType(functionDef);
121112
((NameImpl) functionDef.name()).typeV2(functionType);
122113
inTypeScope(functionType, () -> {
123114
// TODO: check scope accuracy
@@ -128,6 +119,26 @@ public void visitFunctionDef(FunctionDef functionDef) {
128119
});
129120
}
130121

122+
private FunctionType buildFunctionType(FunctionDef functionDef) {
123+
FunctionTypeBuilder functionTypeBuilder = new FunctionTypeBuilder().fromFunctionDef(functionDef);
124+
ClassType owner = null;
125+
if (currentType() instanceof ClassType classType) {
126+
owner = classType;
127+
}
128+
if (owner != null) {
129+
functionTypeBuilder.setOwner(owner);
130+
}
131+
FunctionType functionType = functionTypeBuilder.build();
132+
if (owner != null) {
133+
if (functionDef.name().symbolV2().hasSingleBindingUsage()) {
134+
owner.members().add(new Member(functionType.name(), functionType));
135+
} else {
136+
owner.members().add(new Member(functionType.name(), PythonType.UNKNOWN));
137+
}
138+
}
139+
return functionType;
140+
}
141+
131142
@Override
132143
public void visitImportName(ImportName importName) {
133144
//createImportedNames(importName.modules(), null, Collections.emptyList());

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@
2020
package org.sonar.python.types.v2;
2121

2222
import java.util.List;
23+
import javax.annotation.Nullable;
2324

2425
/**
2526
* FunctionType
2627
*/
2728
public record FunctionType(
2829
String name,
2930
List<PythonType> attributes,
30-
List<Parameter> parameters,
31-
PythonType returnType) implements PythonType {
32-
// TODO: Decorators? Parameters should express keyword/positional-only information - Difference between attributes and typeVars?
33-
}
31+
List<ParameterV2> parameters,
32+
PythonType returnType,
33+
boolean isAsynchronous,
34+
boolean hasDecorators,
35+
boolean isInstanceMethod,
36+
boolean hasVariadicParameter,
37+
@Nullable PythonType owner
38+
) implements PythonType { }

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

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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+
import javax.annotation.CheckForNull;
23+
import javax.annotation.Nullable;
24+
import org.sonar.plugins.python.api.LocationInFile;
25+
26+
public class ParameterV2 {
27+
28+
private final String name;
29+
private PythonType declaredType;
30+
private final boolean hasDefaultValue;
31+
private final boolean isKeywordVariadic;
32+
private final boolean isPositionalVariadic;
33+
private final boolean isKeywordOnly;
34+
private final boolean isPositionalOnly;
35+
private final LocationInFile location;
36+
37+
public ParameterV2(@Nullable String name, PythonType declaredType, boolean hasDefaultValue,
38+
boolean isKeywordOnly, boolean isPositionalOnly, boolean isKeywordVariadic, boolean isPositionalVariadic,
39+
@Nullable LocationInFile location) {
40+
this.name = name;
41+
this.declaredType = declaredType;
42+
this.hasDefaultValue = hasDefaultValue;
43+
this.isKeywordVariadic = isKeywordVariadic;
44+
this.isPositionalVariadic = isPositionalVariadic;
45+
this.isKeywordOnly = isKeywordOnly;
46+
this.isPositionalOnly = isPositionalOnly;
47+
this.location = location;
48+
}
49+
50+
@CheckForNull
51+
public String name() {
52+
return name;
53+
}
54+
55+
public PythonType declaredType() {
56+
return declaredType;
57+
}
58+
59+
public boolean hasDefaultValue() {
60+
return hasDefaultValue;
61+
}
62+
63+
public boolean isVariadic() {
64+
return isKeywordVariadic || isPositionalVariadic;
65+
}
66+
67+
public boolean isKeywordOnly() {
68+
return isKeywordOnly;
69+
}
70+
71+
public boolean isPositionalOnly() {
72+
return isPositionalOnly;
73+
}
74+
75+
public boolean isKeywordVariadic() {
76+
return isKeywordVariadic;
77+
}
78+
79+
public boolean isPositionalVariadic() {
80+
return isPositionalVariadic;
81+
}
82+
83+
@CheckForNull
84+
public LocationInFile location() {
85+
return location;
86+
}
87+
}
88+

0 commit comments

Comments
 (0)