Skip to content

Commit 12241e6

Browse files
andreaguarinoguillaume-dequenne
authored andcommitted
SONARPY-741 RSPEC-5864 Identity check between incompatible types
1 parent c7fc2e3 commit 12241e6

3 files changed

Lines changed: 65 additions & 6 deletions

File tree

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

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@
1919
*/
2020
package org.sonar.python.checks;
2121

22+
import java.util.Collection;
2223
import java.util.List;
2324
import javax.annotation.CheckForNull;
2425
import javax.annotation.Nullable;
2526
import org.sonar.check.Rule;
2627
import org.sonar.plugins.python.api.LocationInFile;
2728
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2829
import org.sonar.plugins.python.api.SubscriptionContext;
30+
import org.sonar.plugins.python.api.symbols.ClassSymbol;
2931
import org.sonar.plugins.python.api.symbols.Symbol;
3032
import org.sonar.plugins.python.api.tree.Expression;
33+
import org.sonar.plugins.python.api.tree.IsExpression;
3134
import org.sonar.plugins.python.api.tree.Name;
3235
import org.sonar.plugins.python.api.tree.RaiseStatement;
3336
import org.sonar.plugins.python.api.tree.Token;
@@ -51,6 +54,7 @@ public void initialize(Context context) {
5154
new IterationOnNonIterableCheck().initialize(context);
5255
new SillyEqualityCheck().initialize(context);
5356
context.registerSyntaxNodeConsumer(Tree.Kind.RAISE_STMT, ConfusingTypeCheckingCheck::checkIncorrectExceptionType);
57+
context.registerSyntaxNodeConsumer(Tree.Kind.IS, ConfusingTypeCheckingCheck::checkSillyIdentity);
5458
}
5559

5660
private static class NonCallableCalledCheck extends NonCallableCalled {
@@ -175,12 +179,7 @@ private static class SillyEqualityCheck extends SillyEquality {
175179

176180
@Override
177181
boolean areIdentityComparableOrNone(InferredType leftType, InferredType rightType) {
178-
if (!containsDeclaredType(leftType) && !containsDeclaredType(rightType)) {
179-
return true;
180-
}
181-
return leftType.equals(rightType)
182-
|| (typeSymbols(leftType).stream().map(Symbol::fullyQualifiedName).allMatch("NoneType"::equals))
183-
|| (typeSymbols(rightType).stream().map(Symbol::fullyQualifiedName).allMatch("NoneType"::equals));
182+
return ConfusingTypeCheckingCheck.areIdentityComparableOrNone(leftType, rightType);
184183
}
185184

186185
@Override
@@ -204,4 +203,27 @@ String message(String result) {
204203
return "Fix this equality check; Previous type checks suggest that operands have incompatible types.";
205204
}
206205
}
206+
207+
private static boolean areIdentityComparableOrNone(InferredType leftType, InferredType rightType) {
208+
if (!containsDeclaredType(leftType) && !containsDeclaredType(rightType)) {
209+
return true;
210+
}
211+
Collection<ClassSymbol> leftSymbols = typeSymbols(leftType);
212+
Collection<ClassSymbol> rightSymbols = typeSymbols(rightType);
213+
return leftSymbols.stream().map(Symbol::fullyQualifiedName).anyMatch(fqn -> fqn == null || rightSymbols.stream().anyMatch(rs -> rs.canBeOrExtend(fqn)))
214+
|| rightSymbols.stream().map(Symbol::fullyQualifiedName).anyMatch(fqn -> fqn == null || leftSymbols.stream().anyMatch(ls -> ls.canBeOrExtend(fqn)))
215+
|| (typeSymbols(leftType).stream().map(Symbol::fullyQualifiedName).allMatch("NoneType"::equals))
216+
|| (typeSymbols(rightType).stream().map(Symbol::fullyQualifiedName).allMatch("NoneType"::equals));
217+
}
218+
219+
private static void checkSillyIdentity(SubscriptionContext ctx) {
220+
IsExpression isExpression = (IsExpression) ctx.syntaxNode();
221+
InferredType left = isExpression.leftOperand().type();
222+
InferredType right = isExpression.rightOperand().type();
223+
if (!areIdentityComparableOrNone(left, right)) {
224+
Token notToken = isExpression.notToken();
225+
Token lastToken = notToken == null ? isExpression.operator() : notToken;
226+
ctx.addIssue(isExpression.operator(), lastToken, "Fix this identity check; Previous type checks suggest that operands have incompatible types.");
227+
}
228+
}
207229
}

python-checks/src/test/java/org/sonar/python/checks/ConfusingTypeCheckingCheckTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ public void silly_equality() {
7272
assertNoIssuesInCorrespondingBugRule("src/test/resources/checks/sillyEquality.py");
7373
}
7474

75+
@Test
76+
public void silly_identity() {
77+
PythonCheckVerifier.verify("src/test/resources/checks/confusingTypeChecking/sillyIdentity.py", check);
78+
assertNoIssuesInCorrespondingBugRule("src/test/resources/checks/sillyIdentity.py");
79+
}
80+
7581
private void assertNoIssuesInCorrespondingBugRule(String path) {
7682
PythonVisitorContext context = TestPythonVisitorRunner.createContext(new File(path));
7783
SubscriptionVisitor.analyze(Collections.singletonList(check), context);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from typing import Union
2+
class A:
3+
def foo(self): ...
4+
class B: ...
5+
def custom(x: A, y: B, z: Union['foo', 'bar']):
6+
if x is y: ... # Noncompliant {{Fix this identity check; Previous type checks suggest that operands have incompatible types.}}
7+
# ^^
8+
if x is not y: ... # Noncompliant
9+
# ^^^^^^
10+
if x is x: ...
11+
xx = x
12+
if x:
13+
yy = xx
14+
else:
15+
yy = y
16+
if yy is not xx: ...
17+
if z is x: ...
18+
if x is z: ...
19+
20+
def builtins(x: int, y: str):
21+
if x is y: ... # Noncompliant
22+
if x is 'foo': ... # Noncompliant
23+
if 42 is y: ... # Noncompliant
24+
if x is None: ... # OK
25+
if None is y: ... # OK
26+
27+
class Base: ...
28+
class C(Base): ...
29+
def with_superclass(x: Base, y: C):
30+
if x is y: ... # OK
31+
if y is x: ... # OK

0 commit comments

Comments
 (0)