Skip to content

Commit 79f3f29

Browse files
SONARPY-1869 Provide basic support for generics in type inference V2
1 parent 20697b0 commit 79f3f29

19 files changed

+281
-37
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ public class ClassTypeBuilder implements TypeBuilder<ClassType> {
4040
List<TypeWrapper> superClasses = new ArrayList<>();
4141
List<PythonType> metaClasses = new ArrayList<>();
4242
boolean hasDecorators = false;
43+
boolean isGeneric = false;
4344
LocationInFile definitionLocation;
4445

4546
@Override
4647
public ClassType build() {
47-
return new ClassType(name, fullyQualifiedName, members, attributes, superClasses, metaClasses, hasDecorators, definitionLocation);
48+
return new ClassType(name, fullyQualifiedName, members, attributes, superClasses, metaClasses, hasDecorators, isGeneric, definitionLocation);
4849
}
4950

5051
public ClassTypeBuilder(String name, String fullyQualifiedName) {
@@ -57,6 +58,11 @@ public ClassTypeBuilder withHasDecorators(boolean hasDecorators) {
5758
return this;
5859
}
5960

61+
public ClassTypeBuilder withIsGeneric(boolean isGeneric) {
62+
this.isGeneric = isGeneric;
63+
return this;
64+
}
65+
6066
@Override
6167
public ClassTypeBuilder withDefinitionLocation(@Nullable LocationInFile definitionLocation) {
6268
this.definitionLocation = definitionLocation;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class ClassDescriptorToPythonTypeConverter implements DescriptorToPythonT
3030

3131
private static PythonType convert(ConversionContext ctx, ClassDescriptor from) {
3232
var typeBuilder = new ClassTypeBuilder(from.name(), from.fullyQualifiedName())
33+
.withIsGeneric(from.supportsGenerics())
3334
.withDefinitionLocation(from.definitionLocation());
3435

3536
from.superClasses().stream()

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ private static Descriptor convert(String moduleFqn, String parentFqn, String sym
152152
hasSuperClassWithoutDescriptor,
153153
type.hasMetaClass(),
154154
metaclassFQN,
155-
false
155+
type.isGeneric()
156156
);
157157
}
158158

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,30 @@
1919
*/
2020
package org.sonar.python.semantic.v2.converter;
2121

22-
import java.util.Optional;
2322
import org.sonar.python.index.Descriptor;
2423
import org.sonar.python.index.VariableDescriptor;
2524
import org.sonar.python.types.v2.ObjectType;
2625
import org.sonar.python.types.v2.PythonType;
26+
import org.sonar.python.types.v2.SpecialFormType;
27+
import org.sonar.python.types.v2.TypeWrapper;
2728

2829
public class VariableDescriptorToPythonTypeConverter implements DescriptorToPythonTypeConverter {
2930

3031
public PythonType convert(ConversionContext ctx, VariableDescriptor from) {
31-
if (from.isImportedModule()) {
32-
var fqn = from.fullyQualifiedName();
33-
if (fqn != null) {
34-
return ctx.lazyTypesContext().getOrCreateLazyType(fqn);
32+
String fullyQualifiedName = from.fullyQualifiedName();
33+
if (from.isImportedModule() && fullyQualifiedName != null) {
34+
return ctx.lazyTypesContext().getOrCreateLazyType(fullyQualifiedName);
35+
}
36+
String annotatedType = from.annotatedType();
37+
if (annotatedType != null) {
38+
if ("typing._SpecialForm".equals(annotatedType) && fullyQualifiedName != null) {
39+
// Defensive null check on fullyQualifiedName: it should never be null for SpecialForm
40+
return new SpecialFormType(fullyQualifiedName);
3541
}
42+
TypeWrapper typeWrapper = ctx.lazyTypesContext().getOrCreateLazyTypeWrapper(annotatedType);
43+
return new ObjectType(typeWrapper);
3644
}
37-
return Optional.ofNullable(from.annotatedType())
38-
.map(fqn -> ctx.lazyTypesContext().getOrCreateLazyTypeWrapper(fqn))
39-
.map(t -> (PythonType) new ObjectType(t))
40-
.orElse(PythonType.UNKNOWN);
45+
return PythonType.UNKNOWN;
4146
}
4247

4348
@Override

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
import org.sonar.python.tree.NumericLiteralImpl;
8282
import org.sonar.python.tree.SetLiteralImpl;
8383
import org.sonar.python.tree.StringLiteralImpl;
84+
import org.sonar.python.tree.SubscriptionExpressionImpl;
8485
import org.sonar.python.tree.TupleImpl;
8586
import org.sonar.python.tree.UnaryExpressionImpl;
8687
import org.sonar.python.types.v2.ClassType;
@@ -89,8 +90,10 @@
8990
import org.sonar.python.types.v2.ModuleType;
9091
import org.sonar.python.types.v2.ObjectType;
9192
import org.sonar.python.types.v2.PythonType;
93+
import org.sonar.python.types.v2.SpecialFormType;
9294
import org.sonar.python.types.v2.TriBool;
9395
import org.sonar.python.types.v2.TypeCheckBuilder;
96+
import org.sonar.python.types.v2.TypeChecker;
9497
import org.sonar.python.types.v2.TypeOrigin;
9598
import org.sonar.python.types.v2.TypeSource;
9699
import org.sonar.python.types.v2.TypeWrapper;
@@ -109,6 +112,7 @@ public class TrivialTypeInferenceVisitor extends BaseTreeVisitor {
109112

110113
private final Deque<Scope> typeStack = new ArrayDeque<>();
111114
private final Set<String> importedModulesFQN = new HashSet<>();
115+
private final TypeChecker typeChecker;
112116

113117
private final Map<String, TypeWrapper> wildcardImportedTypes = new HashMap<>();
114118

@@ -120,6 +124,7 @@ public TrivialTypeInferenceVisitor(TypeTable projectLevelTypeTable, PythonFile p
120124
this.moduleName = pythonFile.fileName();
121125
this.fileId = path != null ? path.toString() : pythonFile.toString();
122126
this.fullyQualifiedModuleName = fullyQualifiedModuleName;
127+
this.typeChecker = new TypeChecker(projectLevelTypeTable);
123128
}
124129

125130
public Set<String> importedModulesFQN() {
@@ -273,6 +278,9 @@ private ClassType buildClassType(ClassDef classDef) {
273278
.withHasDecorators(!classDef.decorators().isEmpty())
274279
.withDefinitionLocation(locationInFile(className, fileId));
275280
resolveTypeHierarchy(classDef, classTypeBuilder);
281+
if (classDef.typeParams() != null) {
282+
classTypeBuilder.withIsGeneric(true);
283+
}
276284
ClassType classType = classTypeBuilder.build();
277285

278286
if (currentType() instanceof ClassType ownerClass) {
@@ -285,22 +293,22 @@ private ClassType buildClassType(ClassDef classDef) {
285293
return classType;
286294
}
287295

288-
static void resolveTypeHierarchy(ClassDef classDef, ClassTypeBuilder classTypeBuilder) {
296+
void resolveTypeHierarchy(ClassDef classDef, ClassTypeBuilder classTypeBuilder) {
289297
Optional.of(classDef)
290298
.map(ClassDef::args)
291299
.map(ArgList::arguments)
292300
.stream()
293301
.flatMap(Collection::stream)
294302
.forEach(argument -> {
295303
if (argument instanceof RegularArgument regularArgument) {
296-
addParentClass(classTypeBuilder, regularArgument);
304+
this.addParentClass(classTypeBuilder, regularArgument);
297305
} else {
298306
classTypeBuilder.addSuperClass(PythonType.UNKNOWN);
299307
}
300308
});
301309
}
302310

303-
private static void addParentClass(ClassTypeBuilder classTypeBuilder, RegularArgument regularArgument) {
311+
private void addParentClass(ClassTypeBuilder classTypeBuilder, RegularArgument regularArgument) {
304312
Name keyword = regularArgument.keywordArgument();
305313
// TODO: SONARPY-1871 store names if not resolved properly
306314
if (keyword != null) {
@@ -312,7 +320,10 @@ private static void addParentClass(ClassTypeBuilder classTypeBuilder, RegularArg
312320
}
313321
PythonType argumentType = getTypeV2FromArgument(regularArgument);
314322
classTypeBuilder.addSuperClass(argumentType);
315-
// TODO: SONARPY-1869 handle generics
323+
if (typeChecker.typeCheckBuilder().isGeneric().check(argumentType) == TriBool.TRUE && regularArgument.expression().is(Tree.Kind.SUBSCRIPTION)) {
324+
// SONARPY-2356: checking that we have a subscription only is too naive (e.g. specialized classes)
325+
classTypeBuilder.withIsGeneric(true);
326+
}
316327
}
317328

318329
private static PythonType getTypeV2FromArgument(RegularArgument regularArgument) {
@@ -561,6 +572,14 @@ public void visitQualifiedExpression(QualifiedExpression qualifiedExpression) {
561572
}
562573
}
563574

575+
@Override
576+
public void visitSubscriptionExpression(SubscriptionExpression subscriptionExpression) {
577+
super.visitSubscriptionExpression(subscriptionExpression);
578+
PythonType pythonType = subscriptionExpression.object().typeV2();
579+
if (typeChecker.typeCheckBuilder().isGeneric().check(pythonType) == TriBool.TRUE) {
580+
((SubscriptionExpressionImpl) subscriptionExpression).typeV2(pythonType);
581+
}
582+
}
564583

565584
@Override
566585
public void visitName(Name name) {
@@ -611,7 +630,8 @@ private static boolean shouldTypeBeEagerlyPropagated(PythonType t) {
611630
return (t instanceof ClassType)
612631
|| (t instanceof FunctionType)
613632
|| (t instanceof ModuleType)
614-
|| (t instanceof UnknownType.UnresolvedImportType);
633+
|| (t instanceof UnknownType.UnresolvedImportType)
634+
|| (t instanceof SpecialFormType);
615635
}
616636

617637
private PythonType currentType() {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public ClassDescriptor convert(SymbolsProtos.ClassSymbol classSymbol) {
7272
.withMetaclassFQN(metaclassName)
7373
.withHasMetaClass(classSymbol.getHasMetaclass())
7474
.withHasDecorators(classSymbol.getHasDecorators())
75+
.withSupportsGenerics(classSymbol.getIsGeneric())
7576
.withMembers(members)
7677
.build();
7778
}

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,6 @@ private static String getTypesFqn(SymbolsProtos.Type type) {
5555
switch (type.getKind()) {
5656
case INSTANCE:
5757
String typeName = type.getFullyQualifiedName();
58-
// _SpecialForm is the type used for some special types, like Callable, Union, TypeVar, ...
59-
// It comes from CPython impl: https://github.com/python/cpython/blob/e39ae6bef2c357a88e232dcab2e4b4c0f367544b/Lib/typing.py#L439
60-
// This doesn't seem to be very precisely specified in typeshed, because it has special semantic.
61-
// To avoid FPs, we treat it as ANY
62-
if ("typing._SpecialForm".equals(typeName)) {
63-
return null;
64-
}
6558
return typeName.isEmpty() ? null : typeName;
6659
case TYPE_ALIAS:
6760
return getTypesFqn(type.getArgs(0));

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*/
2020
package org.sonar.python.semantic.v2.typeshed;
2121

22+
import javax.annotation.Nullable;
2223
import org.sonar.python.index.Descriptor;
2324
import org.sonar.python.index.VariableDescriptor;
2425
import org.sonar.python.types.protobuf.SymbolsProtos;
@@ -27,15 +28,16 @@ public class VarSymbolToDescriptorConverter {
2728

2829
public Descriptor convert(SymbolsProtos.VarSymbol varSymbol) {
2930
var fullyQualifiedName = TypeShedUtils.normalizedFqn(varSymbol.getFullyQualifiedName());
30-
var typeAnnotation = TypeShedUtils.getTypesNormalizedFqn(varSymbol.getTypeAnnotation());
31+
SymbolsProtos.Type protoTypeAnnotation = varSymbol.getTypeAnnotation();
3132
var isImportedModule = varSymbol.getIsImportedModule();
33+
var typeAnnotation = TypeShedUtils.getTypesNormalizedFqn(protoTypeAnnotation);
3234
if (isTypeAnnotationKnownToBeIncorrect(fullyQualifiedName)) {
3335
return new VariableDescriptor(varSymbol.getName(), fullyQualifiedName, null, isImportedModule);
3436
}
3537
return new VariableDescriptor(varSymbol.getName(), fullyQualifiedName, typeAnnotation, isImportedModule);
3638
}
3739

38-
private static boolean isTypeAnnotationKnownToBeIncorrect(String fullyQualifiedName) {
40+
private static boolean isTypeAnnotationKnownToBeIncorrect(@Nullable String fullyQualifiedName) {
3941
// TypedDict is defined to have type "object" in Typeshed, which is incorrect and leads to FPs
4042
return "typing.TypedDict".equals(fullyQualifiedName) || "typing_extensions.TypedDict".equals(fullyQualifiedName);
4143
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@
2828
import org.sonar.plugins.python.api.tree.Token;
2929
import org.sonar.plugins.python.api.tree.Tree;
3030
import org.sonar.plugins.python.api.tree.TreeVisitor;
31+
import org.sonar.python.types.v2.PythonType;
3132

3233
public class SubscriptionExpressionImpl extends PyTree implements SubscriptionExpression {
3334

3435
private final Expression object;
3536
private final Token lBracket;
3637
private final ExpressionList subscripts;
3738
private final Token rBracket;
39+
private PythonType pythonType = PythonType.UNKNOWN;
3840

3941
public SubscriptionExpressionImpl(Expression object, Token lBracket, ExpressionList subscripts, Token rBracket) {
4042
this.object = object;
@@ -63,6 +65,16 @@ public Token rightBracket() {
6365
return rBracket;
6466
}
6567

68+
@Override
69+
public PythonType typeV2() {
70+
return pythonType;
71+
}
72+
73+
public SubscriptionExpression typeV2(PythonType pythonType) {
74+
this.pythonType = pythonType;
75+
return this;
76+
}
77+
6678
@Override
6779
public void accept(TreeVisitor visitor) {
6880
visitor.visitSubscriptionExpression(this);

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public final class ClassType implements PythonType {
4444
private final List<TypeWrapper> superClasses;
4545
private final List<PythonType> metaClasses;
4646
private final boolean hasDecorators;
47+
private final boolean isGeneric;
4748
private final LocationInFile locationInFile;
4849

4950
public ClassType(
@@ -54,6 +55,7 @@ public ClassType(
5455
List<TypeWrapper> superClasses,
5556
List<PythonType> metaClasses,
5657
boolean hasDecorators,
58+
boolean isGeneric,
5759
@Nullable LocationInFile locationInFile) {
5860
this.name = name;
5961
this.fullyQualifiedName = fullyQualifiedName;
@@ -62,11 +64,12 @@ public ClassType(
6264
this.superClasses = superClasses;
6365
this.metaClasses = metaClasses;
6466
this.hasDecorators = hasDecorators;
67+
this.isGeneric = isGeneric;
6568
this.locationInFile = locationInFile;
6669
}
6770

6871
public ClassType(String name, String fullyQualifiedName) {
69-
this(name, fullyQualifiedName, new HashSet<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), false, null);
72+
this(name, fullyQualifiedName, new HashSet<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), false, false, null);
7073
}
7174

7275
@Override
@@ -232,4 +235,8 @@ public List<PythonType> metaClasses() {
232235
public boolean hasDecorators() {
233236
return hasDecorators;
234237
}
238+
239+
public boolean isGeneric() {
240+
return isGeneric;
241+
}
235242
}

0 commit comments

Comments
 (0)