Skip to content

Commit 48b49f2

Browse files
Seppli11sonartech
authored andcommitted
SONARPY-2009 Fix FP on S5644 when a subscription expression refers to a class with a generic type set (#388)
GitOrigin-RevId: 9e74be1df2c70f591063da4978faa3d9ac3e9d98
1 parent a7dad3f commit 48b49f2

3 files changed

Lines changed: 74 additions & 17 deletions

File tree

python-checks/src/main/java/org/sonar/python/checks/ItemOperationsTypeCheck.java

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717
package org.sonar.python.checks;
1818

19-
import java.util.Collection;
19+
import java.util.List;
2020
import java.util.Map;
2121
import java.util.Optional;
2222
import javax.annotation.Nullable;
@@ -26,6 +26,7 @@
2626
import org.sonar.plugins.python.api.symbols.FunctionSymbol;
2727
import org.sonar.plugins.python.api.symbols.Symbol;
2828
import org.sonar.plugins.python.api.tree.ArgList;
29+
import org.sonar.plugins.python.api.tree.Argument;
2930
import org.sonar.plugins.python.api.tree.CallExpression;
3031
import org.sonar.plugins.python.api.tree.ClassDef;
3132
import org.sonar.plugins.python.api.tree.Expression;
@@ -77,7 +78,25 @@ private static boolean isValidSubscriptionSymbol(Symbol symbol, Expression subsc
7778
@Nullable String classRequiredMethod) {
7879
LocationInFile locationInFile = symbol.is(FUNCTION) ? ((FunctionSymbol) symbol).definitionLocation() : ((ClassSymbol) symbol).definitionLocation();
7980
secondaries.put(locationInFile, SECONDARY_MESSAGE.formatted(symbol.name()));
80-
return isSubscriptionInClassArg(subscriptionObject) || canHaveMethod(symbol, requiredMethod, classRequiredMethod);
81+
return isSubscriptionInClassArg(subscriptionObject)
82+
|| canHaveMethod(symbol, requiredMethod, classRequiredMethod)
83+
|| isValidGenericUsage(symbol, subscriptionObject, requiredMethod);
84+
}
85+
86+
private static boolean isValidGenericUsage(Symbol symbol, Expression subscriptionObject, String requiredMethod) {
87+
return "__getitem__".equals(requiredMethod) && symbol.is(CLASS) && !areSomeSubscriptsSuspicious(subscriptionObject);
88+
}
89+
90+
private static boolean areSomeSubscriptsSuspicious(Expression subscriptionObject) {
91+
var subscriptionExprTree = TreeUtils.firstAncestorOfKind(subscriptionObject, Tree.Kind.SUBSCRIPTION);
92+
return subscriptionExprTree instanceof SubscriptionExpression subscriptionExpr
93+
&& subscriptionExpr.subscripts().expressions().stream()
94+
.allMatch(ItemOperationsTypeCheck::isSubscriptSuspicious);
95+
}
96+
97+
private static boolean isSubscriptSuspicious(Expression expr) {
98+
// a subscript used as a generic should be a name of a class, alias, or a class as a string literal; Anything else is suspicious
99+
return !expr.is(Tree.Kind.NAME, Tree.Kind.STRING_LITERAL);
81100
}
82101

83102
private static boolean isInvalidSubscriptionCallExpr(Expression expression, Map<LocationInFile, String> secondaries) {
@@ -91,14 +110,23 @@ private static boolean isInvalidSubscriptionCallExpr(Expression expression, Map<
91110
}
92111

93112
private static boolean isSubscriptionInClassArg(Expression subscriptionObject) {
94-
return Optional.ofNullable(((ClassDef) TreeUtils.firstAncestorOfKind(subscriptionObject, Tree.Kind.CLASSDEF))).map(ClassDef::args).map(ArgList::arguments)
95-
.stream()
96-
.flatMap(Collection::stream)
113+
var classDefOptional = Optional.ofNullable(TreeUtils.firstAncestorOfKind(subscriptionObject, Tree.Kind.CLASSDEF))
114+
.map(ClassDef.class::cast);
115+
116+
List<Argument> classArguments = classDefOptional
117+
.map(ClassDef::args)
118+
.map(ArgList::arguments)
119+
.orElse(List.of());
120+
121+
var onlyRegularArgumentExpressions = classArguments.stream()
97122
.flatMap(TreeUtils.toStreamInstanceOfMapper(RegularArgument.class))
98-
.map(RegularArgument::expression)
123+
.map(RegularArgument::expression);
124+
125+
var subscriptionObjectStream = onlyRegularArgumentExpressions
99126
.flatMap(TreeUtils.toStreamInstanceOfMapper(SubscriptionExpression.class))
100-
.map(SubscriptionExpression::object)
101-
.anyMatch(subscriptionObject::equals);
127+
.map(SubscriptionExpression::object);
128+
129+
return subscriptionObjectStream.anyMatch(subscriptionObject::equals);
102130
}
103131

104132
@Override

python-checks/src/test/resources/checks/itemOperationsTypeCheck/itemOperations_getitem.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,16 @@ def __init__(self, values):
107107
self._values = values
108108

109109
a = A([0,1,2])
110-
111110
a[0] # Noncompliant
112111
# ^^^^
113112

114113
class B: ...
115-
116114
B[0] # Noncompliant
117115

116+
@some_decorator
117+
class ClassWithDecorator: ...
118+
ClassWithDecorator[0] # FN: decorator
119+
118120

119121
class C:
120122
def __init__(self, values):
@@ -134,7 +136,7 @@ def __class_getitem__(cls, key):
134136

135137

136138
def getitem(self, key):
137-
print(f"getting {key}")
139+
print(f"getting {key}")
138140

139141
def meta_classes():
140142
class MyMetaClassWithGet(type):
@@ -218,6 +220,37 @@ def custom_mock():
218220
a = ExtendedMock()[42]
219221

220222

221-
class MyGenericClass[T]: ...
223+
def generic_cases(unknown_type):
224+
class MyGenericClass[T]: ...
225+
226+
class MyGenericSubType(MyGenericClass[str]): ...
227+
228+
class SomeOtherClass: ...
229+
SomeOtherClassAlias = SomeOtherClass
230+
231+
T = TypeVar('T')
232+
GenericAlias = MyGenericClass[T] # OK
233+
IntAlias = MyGenericClass[int] # OK
234+
IntLiteralAlias = MyGenericClass[0] # Noncompliant
235+
StrAlias = MyGenericClass[str] # OK
236+
StrLiteralAlias = MyGenericClass["str"] # OK
237+
238+
a = MyGenericClass[int]()
239+
a = MyGenericClass[SomeOtherClass]()
240+
a = MyGenericClass[SomeOtherClassAlias]()
241+
b = MyGenericClass[unknown_type]()
242+
b = MyGenericClass[0]() # Noncompliant
243+
b = MyGenericClass["str"]() # OK
244+
245+
c = SomeOtherClass()
246+
247+
c[0] # Noncompliant
248+
c[int] # Noncompliant
249+
c["int"] # Noncompliant
222250

223-
class MyGenericSubType(MyGenericClass[str]): ...
251+
class Loader:
252+
def __getitem__(self, item):
253+
return self.loader[item] # Noncompliant
254+
255+
def loader(self):
256+
pass

python-checks/src/test/resources/checks/itemOperationsTypeCheck/itemOperations_setitem.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,6 @@ def __init__(self, values):
115115
a[0] = 42 # Noncompliant {{Fix this code; "a" does not have a "__setitem__" method.}}
116116
# ^^^^
117117

118-
class B: ...
119-
120-
B[0] # Noncompliant
121-
122118
class C:
123119
def __init__(self, values):
124120
self._values = values

0 commit comments

Comments
 (0)