Skip to content

Commit 6b0e9be

Browse files
SONARPY-1984 Infer type of function parameters based on its type hints (#1854)
1 parent 16bdfcb commit 6b0e9be

File tree

8 files changed

+264
-10
lines changed

8 files changed

+264
-10
lines changed

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.sonar.plugins.python.api.PythonFile;
2929
import org.sonar.plugins.python.api.cfg.ControlFlowGraph;
3030
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
31+
import org.sonar.plugins.python.api.tree.Expression;
3132
import org.sonar.plugins.python.api.tree.FileInput;
3233
import org.sonar.plugins.python.api.tree.FunctionDef;
3334
import org.sonar.plugins.python.api.tree.Name;
@@ -130,11 +131,13 @@ private static void inferTypesAndMemberAccessSymbols(Tree scopeTree,
130131
}
131132

132133
private static void flowSensitiveTypeInference(ControlFlowGraph cfg, Set<SymbolV2> trackedVars, PropagationVisitor propagationVisitor) {
133-
// TODO: infer parameter type based on type hint or default value assignement
134+
// TODO: infer parameter type based on default value assignement
134135
var parameterTypes = trackedVars
135136
.stream()
136-
.filter(s -> s.usages().stream().map(UsageV2::kind).anyMatch(UsageV2.Kind.PARAMETER::equals))
137-
.collect(Collectors.toMap(SymbolV2::name, v -> PythonType.UNKNOWN));
137+
.filter(symbol -> symbol.usages()
138+
.stream()
139+
.anyMatch(usage -> usage.kind() == UsageV2.Kind.PARAMETER))
140+
.collect(Collectors.toMap(SymbolV2::name, TypeInferenceV2::getParameterType));
138141

139142
FlowSensitiveTypeInference flowSensitiveTypeInference = new FlowSensitiveTypeInference(
140143
trackedVars,
@@ -146,6 +149,18 @@ private static void flowSensitiveTypeInference(ControlFlowGraph cfg, Set<SymbolV
146149
flowSensitiveTypeInference.compute(cfg);
147150
}
148151

152+
private static PythonType getParameterType(SymbolV2 symbol) {
153+
return symbol.usages()
154+
.stream()
155+
.filter(usage -> usage.kind() == UsageV2.Kind.PARAMETER)
156+
.map(UsageV2::tree)
157+
.filter(Expression.class::isInstance)
158+
.map(Expression.class::cast)
159+
.map(Expression::typeV2)
160+
.findFirst()
161+
.orElse(PythonType.UNKNOWN);
162+
}
163+
149164
private static Set<SymbolV2> getTrackedVars(Set<SymbolV2> localVariables, Set<Name> assignedNames) {
150165
Set<SymbolV2> trackedVars = new HashSet<>();
151166
for (SymbolV2 variable : localVariables) {

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

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.sonar.plugins.python.api.tree.ArgList;
3232
import org.sonar.plugins.python.api.tree.AssignmentStatement;
3333
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
34+
import org.sonar.plugins.python.api.tree.BinaryExpression;
3435
import org.sonar.plugins.python.api.tree.ClassDef;
3536
import org.sonar.plugins.python.api.tree.DictionaryLiteral;
3637
import org.sonar.plugins.python.api.tree.DottedName;
@@ -44,12 +45,15 @@
4445
import org.sonar.plugins.python.api.tree.Name;
4546
import org.sonar.plugins.python.api.tree.NoneExpression;
4647
import org.sonar.plugins.python.api.tree.NumericLiteral;
48+
import org.sonar.plugins.python.api.tree.Parameter;
4749
import org.sonar.plugins.python.api.tree.QualifiedExpression;
4850
import org.sonar.plugins.python.api.tree.RegularArgument;
4951
import org.sonar.plugins.python.api.tree.SetLiteral;
5052
import org.sonar.plugins.python.api.tree.StringLiteral;
53+
import org.sonar.plugins.python.api.tree.SubscriptionExpression;
5154
import org.sonar.plugins.python.api.tree.Tree;
5255
import org.sonar.plugins.python.api.tree.Tuple;
56+
import org.sonar.plugins.python.api.tree.TypeAnnotation;
5357
import org.sonar.plugins.python.api.types.InferredType;
5458
import org.sonar.python.semantic.v2.ClassTypeBuilder;
5559
import org.sonar.python.semantic.v2.FunctionTypeBuilder;
@@ -71,6 +75,7 @@
7175
import org.sonar.python.types.v2.ModuleType;
7276
import org.sonar.python.types.v2.ObjectType;
7377
import org.sonar.python.types.v2.PythonType;
78+
import org.sonar.python.types.v2.TypeSource;
7479
import org.sonar.python.types.v2.UnionType;
7580

7681
import static org.sonar.python.semantic.SymbolUtils.pathOf;
@@ -114,23 +119,23 @@ public void visitTuple(Tuple tuple) {
114119
}
115120
ModuleType builtins = this.projectLevelTypeTable.getModule();
116121
PythonType tupleType = builtins.resolveMember("tuple").orElse(PythonType.UNKNOWN);
117-
((TupleImpl) tuple).typeV2(new ObjectType(tupleType, attributes, new ArrayList<>()));
122+
((TupleImpl) tuple).typeV2(new ObjectType(tupleType, attributes, new ArrayList<>()));
118123
}
119124

120125
@Override
121126
public void visitDictionaryLiteral(DictionaryLiteral dictionaryLiteral) {
122127
super.visitDictionaryLiteral(dictionaryLiteral);
123128
ModuleType builtins = this.projectLevelTypeTable.getModule();
124129
PythonType dictType = builtins.resolveMember("dict").orElse(PythonType.UNKNOWN);
125-
((DictionaryLiteralImpl) dictionaryLiteral).typeV2(new ObjectType(dictType, new ArrayList<>(), new ArrayList<>()));
130+
((DictionaryLiteralImpl) dictionaryLiteral).typeV2(new ObjectType(dictType, new ArrayList<>(), new ArrayList<>()));
126131
}
127132

128133
@Override
129134
public void visitSetLiteral(SetLiteral setLiteral) {
130135
super.visitSetLiteral(setLiteral);
131136
ModuleType builtins = this.projectLevelTypeTable.getModule();
132137
PythonType setType = builtins.resolveMember("set").orElse(PythonType.UNKNOWN);
133-
((SetLiteralImpl) setLiteral).typeV2(new ObjectType(setType, new ArrayList<>(), new ArrayList<>()));
138+
((SetLiteralImpl) setLiteral).typeV2(new ObjectType(setType, new ArrayList<>(), new ArrayList<>()));
134139
}
135140

136141
@Override
@@ -329,6 +334,43 @@ public void visitAssignmentStatement(AssignmentStatement assignmentStatement) {
329334
});
330335
}
331336

337+
@Override
338+
public void visitParameter(Parameter parameter) {
339+
scan(parameter.typeAnnotation());
340+
scan(parameter.defaultValue());
341+
Optional.ofNullable(parameter.typeAnnotation())
342+
.map(TypeAnnotation::expression)
343+
.map(TrivialTypeInferenceVisitor::resolveTypeAnnotationExpressionType)
344+
.ifPresent(type -> setTypeToName(parameter.name(), type));
345+
scan(parameter.name());
346+
}
347+
348+
private static PythonType resolveTypeAnnotationExpressionType(Expression expression) {
349+
if (expression instanceof Name name && name.typeV2() != PythonType.UNKNOWN) {
350+
return new ObjectType(name.typeV2(), TypeSource.TYPE_HINT);
351+
} else if (expression instanceof SubscriptionExpression subscriptionExpression && subscriptionExpression.object().typeV2() != PythonType.UNKNOWN) {
352+
var candidateTypes = subscriptionExpression.subscripts()
353+
.expressions()
354+
.stream()
355+
.map(Expression::typeV2)
356+
.distinct()
357+
.toList();
358+
359+
var elementsType = UnionType.or(candidateTypes);
360+
361+
var attributes = new ArrayList<PythonType>();
362+
attributes.add(new ObjectType(elementsType, TypeSource.TYPE_HINT));
363+
return new ObjectType(subscriptionExpression.object().typeV2(), attributes, new ArrayList<>(), TypeSource.TYPE_HINT);
364+
} else if (expression instanceof BinaryExpression binaryExpression) {
365+
var left = resolveTypeAnnotationExpressionType(binaryExpression.leftOperand());
366+
var right = resolveTypeAnnotationExpressionType(binaryExpression.rightOperand());
367+
// TODO: we need to make a decision on should here be a union type of object types or an object type of a union type.
368+
// ATM it is blocked by the generic types resolution redesign
369+
return UnionType.or(left, right);
370+
}
371+
return PythonType.UNKNOWN;
372+
}
373+
332374
@Override
333375
public void visitQualifiedExpression(QualifiedExpression qualifiedExpression) {
334376
scan(qualifiedExpression.qualifier());

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,18 @@
2727
import org.sonar.plugins.python.api.LocationInFile;
2828

2929
@Beta
30-
public record ObjectType(PythonType type, List<PythonType> attributes, List<Member> members) implements PythonType {
30+
public record ObjectType(PythonType type, List<PythonType> attributes, List<Member> members, TypeSource typeSource) implements PythonType {
3131

3232
public ObjectType(PythonType type) {
33-
this(type, new ArrayList<>(), new ArrayList<>());
33+
this(type, TypeSource.EXACT);
34+
}
35+
36+
public ObjectType(PythonType type, TypeSource typeSource) {
37+
this(type, new ArrayList<>(), new ArrayList<>(), typeSource);
38+
}
39+
40+
public ObjectType(PythonType type, List<PythonType> attributes, List<Member> members) {
41+
this(type, attributes, members, TypeSource.EXACT);
3442
}
3543

3644
@Override

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,9 @@ default Optional<LocationInFile> definitionLocation() {
7474
default PythonType unwrappedType() {
7575
return this;
7676
}
77+
78+
@Beta
79+
default TypeSource typeSource() {
80+
return TypeSource.EXACT;
81+
}
7782
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ public TypeCheckBuilder instancesHaveMember(String memberName) {
4242
return this;
4343
}
4444

45+
public TypeCheckBuilder isTypeHintTypeSource() {
46+
predicates.add(new TypeSourceMatcherTypePredicate(TypeSource.TYPE_HINT));
47+
return this;
48+
}
49+
4550
public TriBool check(PythonType pythonType) {
4651
TriBool result = TriBool.TRUE;
4752
for (TypePredicate predicate : predicates) {
@@ -84,4 +89,12 @@ public TriBool test(PythonType pythonType) {
8489
return TriBool.FALSE;
8590
}
8691
}
92+
93+
record TypeSourceMatcherTypePredicate(TypeSource typeSource) implements TypePredicate {
94+
95+
@Override
96+
public TriBool test(PythonType pythonType) {
97+
return pythonType.typeSource() == typeSource ? TriBool.TRUE : TriBool.FALSE;
98+
}
99+
}
87100
}
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 TypeSource {
23+
EXACT,
24+
TYPE_HINT
25+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.assertj.core.api.Assertions;
23+
import org.junit.jupiter.api.Test;
24+
import org.sonar.python.types.v2.ObjectType;
25+
import org.sonar.python.types.v2.PythonType;
26+
import org.sonar.python.types.v2.TriBool;
27+
import org.sonar.python.types.v2.TypeCheckBuilder;
28+
import org.sonar.python.types.v2.TypeSource;
29+
30+
class TypeCheckerBuilderTest {
31+
32+
@Test
33+
void typeSourceTest() {
34+
var builder = new TypeCheckBuilder(null).isTypeHintTypeSource();
35+
Assertions.assertThat(builder.check(new ObjectType(PythonType.UNKNOWN, TypeSource.TYPE_HINT)))
36+
.isEqualTo(TriBool.TRUE);
37+
Assertions.assertThat(builder.check(new ObjectType(PythonType.UNKNOWN, TypeSource.EXACT)))
38+
.isEqualTo(TriBool.FALSE);
39+
}
40+
41+
}

0 commit comments

Comments
 (0)