Skip to content

Commit 0a4a5b8

Browse files
SONARPY-1869 Ensure conservative heuristics for generics inheritance
1 parent e4d132b commit 0a4a5b8

File tree

5 files changed

+151
-15
lines changed

5 files changed

+151
-15
lines changed
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
from typing import Generic
1+
from typing import Generic, TypeVar
2+
3+
T = TypeVar('T')
4+
25
class SomeGeneric(Generic[T]):
36
...
47

@@ -7,3 +10,7 @@ class SomeGenericWithTypeParam[T](): ...
710
class MyImportedGenericTypeVarChild(SomeGeneric[T]): ...
811
class MyImportedNonGenericChild(SomeGeneric): ...
912
class MyImportedConcreteChild(SomeGeneric[str]): ...
13+
14+
# U has not been defined
15+
class SomeGenericIncorrectlyDefined(Generic[U]):
16+
...

python-checks/src/test/resources/checks/genericTypeWithoutArgumentImporting.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
# mod.py
2-
from genericTypeWithoutArgumentImported import SomeGeneric, SomeGenericWithTypeParam, MyImportedGenericTypeVarChild, MyImportedNonGenericChild, MyImportedConcreteChild
1+
from genericTypeWithoutArgumentImported import (
2+
SomeGeneric,
3+
SomeGenericWithTypeParam,
4+
MyImportedGenericTypeVarChild,
5+
MyImportedNonGenericChild,
6+
MyImportedConcreteChild,
7+
SomeGenericIncorrectlyDefined
8+
)
39

410
def local_generic():
511
from typing import Generic
@@ -16,8 +22,7 @@ def bar() -> SomeGenericWithTypeParam: # Noncompliant
1622

1723
def returning_imported_child() -> MyImportedGenericTypeVarChild: ... # Noncompliant
1824
def returning_imported_non_generic_child() -> MyImportedNonGenericChild: ... # OK
19-
# FP SONARPY-2356: MyImportedConcreteChild is not actually generic (specialized class)
20-
def returning_imported_concrete_child() -> MyImportedConcreteChild: ... # Noncompliant
25+
def returning_imported_concrete_child() -> MyImportedConcreteChild: ... # OK
2126

2227
class MyChild(SomeGeneric[T]): ...
2328
def returning_my_child() -> MyChild: # FN
@@ -30,3 +35,6 @@ def returning_my_non_generic_child() -> MyNonGenericChild: # OK
3035
class MyConcreteChild(SomeGeneric[str]): ...
3136
def returning_my_concrete_chil3() -> MyConcreteChild: # OK
3237
...
38+
39+
def returning_incorrect_generic() -> SomeGenericIncorrectlyDefined:
40+
...

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

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@
3737
import org.sonar.plugins.python.api.tree.AliasedName;
3838
import org.sonar.plugins.python.api.tree.AnnotatedAssignment;
3939
import org.sonar.plugins.python.api.tree.ArgList;
40+
import org.sonar.plugins.python.api.tree.AssignmentExpression;
4041
import org.sonar.plugins.python.api.tree.AssignmentStatement;
4142
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
4243
import org.sonar.plugins.python.api.tree.BinaryExpression;
44+
import org.sonar.plugins.python.api.tree.CallExpression;
4345
import org.sonar.plugins.python.api.tree.ClassDef;
4446
import org.sonar.plugins.python.api.tree.ComprehensionExpression;
4547
import org.sonar.plugins.python.api.tree.DictCompExpression;
@@ -82,6 +84,7 @@
8284
import org.sonar.python.tree.SetLiteralImpl;
8385
import org.sonar.python.tree.StringLiteralImpl;
8486
import org.sonar.python.tree.SubscriptionExpressionImpl;
87+
import org.sonar.python.tree.TreeUtils;
8588
import org.sonar.python.tree.TupleImpl;
8689
import org.sonar.python.tree.UnaryExpressionImpl;
8790
import org.sonar.python.types.v2.ClassType;
@@ -113,6 +116,7 @@ public class TrivialTypeInferenceVisitor extends BaseTreeVisitor {
113116
private final Deque<Scope> typeStack = new ArrayDeque<>();
114117
private final Set<String> importedModulesFQN = new HashSet<>();
115118
private final TypeChecker typeChecker;
119+
private final List<String> typeVarNames = new ArrayList<>();
116120

117121
private final Map<String, TypeWrapper> wildcardImportedTypes = new HashMap<>();
118122

@@ -320,12 +324,17 @@ private void addParentClass(ClassTypeBuilder classTypeBuilder, RegularArgument r
320324
}
321325
PythonType argumentType = getTypeV2FromArgument(regularArgument);
322326
classTypeBuilder.addSuperClass(argumentType);
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)
327+
if (isParentAGenericClass(regularArgument, argumentType)) {
325328
classTypeBuilder.withIsGeneric(true);
326329
}
327330
}
328331

332+
private boolean isParentAGenericClass(RegularArgument regularArgument, PythonType argumentType) {
333+
return typeChecker.typeCheckBuilder().isGeneric().check(argumentType) == TriBool.TRUE
334+
&& regularArgument.expression() instanceof SubscriptionExpression subscriptionExpression
335+
&& subscriptionExpression.subscripts().expressions().stream().anyMatch(expression -> expression instanceof Name name && typeVarNames.contains(name.name()));
336+
}
337+
329338
private static PythonType getTypeV2FromArgument(RegularArgument regularArgument) {
330339
Expression expression = regularArgument.expression();
331340
// Ensure we support correctly typing symbols like "List[str] / list[str]"
@@ -503,6 +512,44 @@ public void visitAnnotatedAssignment(AnnotatedAssignment assignmentStatement) {
503512
});
504513
}
505514

515+
@Override
516+
public void visitCallExpression(CallExpression callExpression) {
517+
super.visitCallExpression(callExpression);
518+
assignPossibleTypeVar(callExpression);
519+
}
520+
521+
private void assignPossibleTypeVar(CallExpression callExpression) {
522+
PythonType pythonType = callExpression.callee().typeV2();
523+
TriBool check = typeChecker.typeCheckBuilder().isTypeWithName("typing.TypeVar").check(pythonType);
524+
if (check == TriBool.TRUE) {
525+
Tree parent = TreeUtils.firstAncestor(callExpression, t -> t.is(Tree.Kind.ASSIGNMENT_STMT, Tree.Kind.ANNOTATED_ASSIGNMENT, Tree.Kind.ASSIGNMENT_EXPRESSION));
526+
Optional<Name> assignedName = Optional.empty();
527+
if (parent instanceof AssignmentStatement assignmentStatement) {
528+
assignedName = extractAssignedName(assignmentStatement);
529+
}
530+
if (parent instanceof AnnotatedAssignment annotatedAssignment) {
531+
assignedName = extractAssignedName(annotatedAssignment);
532+
}
533+
if (parent instanceof AssignmentExpression assignmentExpression) {
534+
assignedName = extractAssignedName(assignmentExpression);
535+
}
536+
assignedName.ifPresent(name -> typeVarNames.add(name.name()));
537+
}
538+
}
539+
540+
private static Optional<Name> extractAssignedName(AssignmentExpression assignmentExpression) {
541+
return Optional.of(assignmentExpression.lhsName()).map(Name.class::cast);
542+
}
543+
544+
private static Optional<Name> extractAssignedName(AnnotatedAssignment annotatedAssignment) {
545+
return Optional.of(annotatedAssignment.variable()).filter(v -> v.is(Tree.Kind.NAME)).map(Name.class::cast);
546+
}
547+
548+
private static Optional<Name> extractAssignedName(AssignmentStatement assignmentStatement) {
549+
return assignmentStatement.lhsExpressions().stream().findFirst()
550+
.map(ExpressionList::expressions).stream().flatMap(List::stream).findFirst().filter(v -> v.is(Tree.Kind.NAME)).map(Name.class::cast);
551+
}
552+
506553
private static Optional<NameImpl> getFirstAssignmentName(AssignmentStatement assignmentStatement) {
507554
return Optional.of(assignmentStatement)
508555
.map(AssignmentStatement::lhsExpressions)

python-frontend/src/test/java/org/sonar/python/index/ClassDescriptorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ void classDescriptorGenerics() {
109109
ClassDescriptor classDescriptor = lastClassDescriptor(
110110
"from typing import Generic",
111111
"class A(Generic[str]): ...");
112-
assertThat(classDescriptor.supportsGenerics()).isTrue();
112+
assertThat(classDescriptor.supportsGenerics()).isFalse();
113113
assertDescriptorToProtobuf(classDescriptor);
114114
}
115115

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

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,21 +156,94 @@ void builtinGenericType() {
156156
void userDefinedGenericType() {
157157
FileInput fileInput = inferTypes(
158158
"""
159-
from typing import Generic
159+
from typing import Generic, TypeVar
160+
T = TypeVar('T')
160161
class MyClass(Generic[T]): ...
161162
x = MyClass[str]()
162163
x
163164
"""
164165
);
165-
PythonType classType = ((ClassDef) fileInput.statements().statements().get(1)).name().typeV2();
166-
ObjectType xType = (ObjectType) ((ExpressionStatement) fileInput.statements().statements().get(3)).expressions().get(0).typeV2();
166+
PythonType classType = ((ClassDef) fileInput.statements().statements().get(2)).name().typeV2();
167+
ObjectType xType = (ObjectType) ((ExpressionStatement) fileInput.statements().statements().get(4)).expressions().get(0).typeV2();
167168
assertThat(xType.unwrappedType()).isEqualTo(classType);
168169
// SONARPY-2356: Instantiation of specialized classes
169170
assertThat(xType.attributes()).isEmpty();
170171
}
171172

172173
@Test
173174
void inheritedGenericType() {
175+
FileInput fileInput = inferTypes(
176+
"""
177+
from typing import Generic, TypeVar
178+
T = TypeVar('T')
179+
class MyClass(Generic[T]): ...
180+
class MyOtherClass(MyClass[T]): ...
181+
x = MyOtherClass[str]()
182+
x
183+
"""
184+
);
185+
ClassType myOtherClassType = (ClassType) ((ClassDef) fileInput.statements().statements().get(3)).name().typeV2();
186+
assertThat(myOtherClassType.isGeneric()).isTrue();
187+
PythonType xType = ((ExpressionStatement) fileInput.statements().statements().get(5)).expressions().get(0).typeV2();
188+
assertThat(xType.unwrappedType()).isEqualTo(myOtherClassType);
189+
}
190+
191+
@Test
192+
void inheritedGenericTypeUnsupportedExpression() {
193+
FileInput fileInput = inferTypes(
194+
"""
195+
from typing import Generic, TypeVar
196+
T = TypeVar('T')
197+
class MyClass(Generic[T()]): ...
198+
class MyOtherClass(MyClass[T]): ...
199+
x = MyOtherClass[str]()
200+
x
201+
"""
202+
);
203+
ClassType myOtherClassType = (ClassType) ((ClassDef) fileInput.statements().statements().get(3)).name().typeV2();
204+
assertThat(myOtherClassType.isGeneric()).isFalse();
205+
PythonType xType = ((ExpressionStatement) fileInput.statements().statements().get(5)).expressions().get(0).typeV2();
206+
assertThat(xType.unwrappedType()).isInstanceOf(UnknownType.class);
207+
}
208+
209+
@Test
210+
void inheritedGenericTypeVarAnnotatedAssignment() {
211+
FileInput fileInput = inferTypes(
212+
"""
213+
from typing import Generic, TypeVar
214+
T: TypeVar = TypeVar('T')
215+
class MyClass(Generic[T]): ...
216+
class MyOtherClass(MyClass[T]): ...
217+
x = MyOtherClass[str]()
218+
x
219+
"""
220+
);
221+
ClassType myOtherClassType = (ClassType) ((ClassDef) fileInput.statements().statements().get(3)).name().typeV2();
222+
assertThat(myOtherClassType.isGeneric()).isTrue();
223+
PythonType xType = ((ExpressionStatement) fileInput.statements().statements().get(5)).expressions().get(0).typeV2();
224+
assertThat(xType.unwrappedType()).isEqualTo(myOtherClassType);
225+
}
226+
227+
@Test
228+
void inheritedGenericTypeVarAssignmentExpression() {
229+
FileInput fileInput = inferTypes(
230+
"""
231+
from typing import Generic, TypeVar
232+
foo(T:=TypeVar('T'))
233+
class MyClass(Generic[T]): ...
234+
class MyOtherClass(MyClass[T]): ...
235+
x = MyOtherClass[str]()
236+
x
237+
"""
238+
);
239+
ClassType myOtherClassType = (ClassType) ((ClassDef) fileInput.statements().statements().get(3)).name().typeV2();
240+
assertThat(myOtherClassType.isGeneric()).isTrue();
241+
PythonType xType = ((ExpressionStatement) fileInput.statements().statements().get(5)).expressions().get(0).typeV2();
242+
assertThat(xType.unwrappedType()).isEqualTo(myOtherClassType);
243+
}
244+
245+
@Test
246+
void inheritedGenericTypeUndefinedTypeVar() {
174247
FileInput fileInput = inferTypes(
175248
"""
176249
from typing import Generic
@@ -181,9 +254,10 @@ class MyOtherClass(MyClass[T]): ...
181254
"""
182255
);
183256
ClassType myOtherClassType = (ClassType) ((ClassDef) fileInput.statements().statements().get(2)).name().typeV2();
184-
assertThat(myOtherClassType.isGeneric()).isTrue();
257+
// TypeVar is undefined: not a proper generic
258+
assertThat(myOtherClassType.isGeneric()).isFalse();
185259
PythonType xType = ((ExpressionStatement) fileInput.statements().statements().get(4)).expressions().get(0).typeV2();
186-
assertThat(xType.unwrappedType()).isEqualTo(myOtherClassType);
260+
assertThat(xType.unwrappedType()).isInstanceOf(UnknownType.class);
187261
}
188262

189263
@Test
@@ -219,9 +293,9 @@ class MyOtherClass(MyClass[str]): ...
219293
);
220294
ClassType myOtherClassType = (ClassType) ((ClassDef) fileInput.statements().statements().get(2)).name().typeV2();
221295
// SONARPY-2356: MyOtherClass can no longer be considered generic (specialized version of MyClass)
222-
assertThat(myOtherClassType.isGeneric()).isTrue();
296+
assertThat(myOtherClassType.isGeneric()).isFalse();
223297
PythonType xType = ((ExpressionStatement) fileInput.statements().statements().get(4)).expressions().get(0).typeV2();
224-
assertThat(xType.unwrappedType()).isEqualTo(myOtherClassType);
298+
assertThat(xType.unwrappedType()).isInstanceOf(UnknownType.class);
225299
}
226300

227301
@Test

0 commit comments

Comments
 (0)