Skip to content

Commit b31a132

Browse files
joke1196sonartech
authored andcommitted
SONARPY-3932 Migrated 5 rules to typeV2 (#973)
GitOrigin-RevId: 3cd5167203c1642d632a0b70f78114750623c6e0
1 parent 03d16ed commit b31a132

File tree

9 files changed

+110
-73
lines changed

9 files changed

+110
-73
lines changed

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,23 @@
2020
import java.util.EnumSet;
2121
import org.sonar.check.Rule;
2222
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
23+
import org.sonar.plugins.python.api.SubscriptionContext;
2324
import org.sonar.plugins.python.api.tree.BinaryExpression;
2425
import org.sonar.plugins.python.api.tree.CallExpression;
2526
import org.sonar.plugins.python.api.tree.Expression;
2627
import org.sonar.plugins.python.api.tree.NumericLiteral;
2728
import org.sonar.plugins.python.api.tree.Tree.Kind;
29+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher;
30+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers;
2831
import org.sonar.python.api.PythonPunctuator;
29-
import org.sonar.plugins.python.api.symbols.Symbol;
3032

3133
import static org.sonar.python.checks.utils.Expressions.removeParentheses;
3234

3335
@Rule(key = "S3981")
3436
public class CollectionLengthComparisonCheck extends PythonSubscriptionCheck {
3537

38+
private static final TypeMatcher LEN_MATCHER = TypeMatchers.isType("len");
39+
3640
private static final EnumSet<PythonPunctuator> INVALID_OPERATORS =
3741
EnumSet.of(PythonPunctuator.LT, PythonPunctuator.GT_EQU);
3842

@@ -46,8 +50,8 @@ public void initialize(Context context) {
4650
Expression left = removeParentheses(comparison.leftOperand());
4751
Expression right = removeParentheses(comparison.rightOperand());
4852
TokenType operator = comparison.operator().type();
49-
if ((isCallToLen(left) && isZero(right) && INVALID_OPERATORS.contains(operator))
50-
|| (isCallToLen(right) && isZero(left) && INVALID_REVERSE_OPERATORS.contains(operator))) {
53+
if ((isCallToLen(left, ctx) && isZero(right) && INVALID_OPERATORS.contains(operator))
54+
|| (isCallToLen(right, ctx) && isZero(left) && INVALID_REVERSE_OPERATORS.contains(operator))) {
5155
ctx.addIssue(comparison, "The length of a collection is always \">=0\", so update this test to either \"==0\" or \">0\".");
5256
}
5357
});
@@ -57,12 +61,8 @@ private static boolean isZero(Expression expression) {
5761
return expression.is(Kind.NUMERIC_LITERAL) && "0".equals(((NumericLiteral) expression).valueAsString());
5862
}
5963

60-
private static boolean isCallToLen(Expression expression) {
61-
if (expression.is(Kind.CALL_EXPR)) {
62-
Symbol calleeSymbol = ((CallExpression) expression).calleeSymbol();
63-
return calleeSymbol != null && "len".equals(calleeSymbol.fullyQualifiedName());
64-
}
65-
return false;
64+
private static boolean isCallToLen(Expression expression, SubscriptionContext ctx) {
65+
return expression.is(Kind.CALL_EXPR) && LEN_MATCHER.isTrueFor(((CallExpression) expression).callee(), ctx);
6666
}
6767

6868

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,22 @@
2222
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2323
import org.sonar.plugins.python.api.SubscriptionContext;
2424
import org.sonar.plugins.python.api.quickfix.PythonQuickFix;
25-
import org.sonar.plugins.python.api.symbols.Symbol;
2625
import org.sonar.plugins.python.api.tree.Argument;
2726
import org.sonar.plugins.python.api.tree.CallExpression;
2827
import org.sonar.plugins.python.api.tree.Name;
2928
import org.sonar.plugins.python.api.tree.QualifiedExpression;
3029
import org.sonar.plugins.python.api.tree.RegularArgument;
3130
import org.sonar.plugins.python.api.tree.Tree;
31+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher;
32+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers;
3233
import org.sonar.python.quickfix.TextEditUtils;
3334

3435
@Rule(key = "S6729")
3536
public class NumpyWhereOneConditionCheck extends PythonSubscriptionCheck {
3637

3738
private static final String MESSAGE = "Use \"np.nonzero\" when only the condition parameter is provided to \"np.where\".";
39+
// numpy.where has no stubs, so the type resolves as UnresolvedImportType. Use withFQN to match on the FQN directly.
40+
private static final TypeMatcher NUMPY_WHERE = TypeMatchers.withFQN("numpy.where");
3841

3942
@Override
4043
public void initialize(Context context) {
@@ -43,15 +46,11 @@ public void initialize(Context context) {
4346

4447
private static void checkNumpyWhereCall(SubscriptionContext ctx) {
4548
CallExpression ce = (CallExpression) ctx.syntaxNode();
46-
Symbol symbol = ce.calleeSymbol();
47-
Optional.ofNullable(symbol)
48-
.map(Symbol::fullyQualifiedName)
49-
.filter("numpy.where"::equals)
50-
.filter(fqn -> hasOneParameter(ce))
51-
.ifPresent(fqn -> {
52-
PreciseIssue issue = ctx.addIssue(ce, MESSAGE);
53-
addQuickFix(ce, issue);
54-
});
49+
if (!NUMPY_WHERE.isTrueFor(ce.callee(), ctx) || !hasOneParameter(ce)) {
50+
return;
51+
}
52+
PreciseIssue issue = ctx.addIssue(ce, MESSAGE);
53+
addQuickFix(ce, issue);
5554
}
5655

5756
private static void addQuickFix(CallExpression ce, PreciseIssue issue) {

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@
1919
import org.sonar.check.Rule;
2020
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2121
import org.sonar.plugins.python.api.SubscriptionContext;
22-
import org.sonar.plugins.python.api.symbols.Symbol;
2322
import org.sonar.plugins.python.api.tree.CallExpression;
2423
import org.sonar.plugins.python.api.tree.Expression;
2524
import org.sonar.plugins.python.api.tree.RegularArgument;
2625
import org.sonar.plugins.python.api.tree.StringLiteral;
2726
import org.sonar.plugins.python.api.tree.Tree;
27+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher;
28+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers;
2829
import org.sonar.python.checks.utils.Expressions;
2930
import org.sonar.python.tree.TreeUtils;
3031

@@ -34,6 +35,7 @@ public class StrftimeConfusingHourSystemCheck extends PythonSubscriptionCheck {
3435
public static final String MESSAGE = "Use %I (12-hour clock) or %H (24-hour clock) without %p (AM/PM).";
3536
public static final String MESSAGE_12_HOURS = "Use %I (12-hour clock) with %p (AM/PM).";
3637
private static final String MESSAGE_SECONDARY_LOCATION = "Wrong format created here.";
38+
private static final TypeMatcher STRFTIME_MATCHER = TypeMatchers.isType("datetime.time.strftime");
3739

3840
@Override
3941
public void initialize(Context context) {
@@ -70,12 +72,7 @@ private static void checkDateFormatStringLiteral(SubscriptionContext context, Tr
7072

7173
private static void checkCallExpr(SubscriptionContext context) {
7274
CallExpression callExpression = (CallExpression) context.syntaxNode();
73-
Symbol calleeSymbol = callExpression.calleeSymbol();
74-
if (calleeSymbol == null) {
75-
return;
76-
}
77-
String fullyQualifiedName = calleeSymbol.fullyQualifiedName();
78-
if (!"datetime.time.strftime".equals(fullyQualifiedName)) {
75+
if (!STRFTIME_MATCHER.isTrueFor(callExpression.callee(), context)) {
7976
return;
8077
}
8178

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

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

19-
import java.util.Set;
19+
import java.util.Optional;
2020
import org.sonar.check.Rule;
2121
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2222
import org.sonar.plugins.python.api.SubscriptionContext;
23-
import org.sonar.plugins.python.api.symbols.Symbol;
2423
import org.sonar.plugins.python.api.tree.CallExpression;
2524
import org.sonar.plugins.python.api.tree.Tree;
25+
import org.sonar.plugins.python.api.types.v2.FullyQualifiedNameHelper;
26+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher;
27+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers;
2628

2729
@Rule(key = "S6903")
2830
public class TimezoneNaiveDatetimeConstructorsCheck extends PythonSubscriptionCheck {
2931
private static final String MESSAGE = "Don't use `%s` to create this datetime object.";
30-
private static final Set<String> NON_COMPLIANT_FQNS = Set.of("datetime.datetime.utcnow", "datetime.datetime.utcfromtimestamp");
32+
private static final TypeMatcher NON_COMPLIANT_MATCHER = TypeMatchers.any(
33+
TypeMatchers.isType("datetime.datetime.utcnow"),
34+
TypeMatchers.isType("datetime.datetime.utcfromtimestamp"));
3135

3236
@Override
3337
public void initialize(Context context) {
@@ -36,15 +40,9 @@ public void initialize(Context context) {
3640

3741
private static void checkCallExpr(SubscriptionContext context) {
3842
CallExpression callExpression = (CallExpression) context.syntaxNode();
39-
Symbol calleeSymbol = callExpression.calleeSymbol();
40-
41-
if (calleeSymbol == null) {
42-
return;
43-
}
44-
String fullyQualifiedName = calleeSymbol.fullyQualifiedName();
45-
if (fullyQualifiedName == null || !NON_COMPLIANT_FQNS.contains(fullyQualifiedName)) {
46-
return;
47-
}
48-
context.addIssue(callExpression, String.format(MESSAGE, fullyQualifiedName));
43+
Optional.of(callExpression.callee())
44+
.filter(callee -> NON_COMPLIANT_MATCHER.isTrueFor(callee, context))
45+
.flatMap(callee -> FullyQualifiedNameHelper.getFullyQualifiedName(callee.typeV2()))
46+
.ifPresent(fqn -> context.addIssue(callExpression.callee(), String.format(MESSAGE, fqn)));
4947
}
5048
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,22 @@
1818

1919
import org.sonar.check.Rule;
2020
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
21-
import org.sonar.plugins.python.api.symbols.Symbol;
2221
import org.sonar.plugins.python.api.tree.CallExpression;
2322
import org.sonar.plugins.python.api.tree.Tree;
23+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher;
24+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers;
2425

2526
@Rule(key = "S6979")
2627
public class TorchAutogradVariableShouldNotBeUsedCheck extends PythonSubscriptionCheck {
2728
private static final String MESSAGE = "Replace this call with a call to \"torch.tensor\".";
28-
private static final String TORCH_AUTOGRAD_VARIABLE = "torch.autograd.Variable";
29+
// torch.autograd.Variable has no stubs, so the type resolves as UnresolvedImportType. Use withFQN to match on the FQN directly.
30+
private static final TypeMatcher TORCH_AUTOGRAD_VARIABLE = TypeMatchers.withFQN("torch.autograd.Variable");
2931

3032
@Override
3133
public void initialize(Context context) {
3234
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, ctx -> {
3335
CallExpression callExpression = (CallExpression) ctx.syntaxNode();
34-
Symbol calleeSymbol = callExpression.calleeSymbol();
35-
if (calleeSymbol != null && TORCH_AUTOGRAD_VARIABLE.equals(calleeSymbol.fullyQualifiedName())) {
36+
if (TORCH_AUTOGRAD_VARIABLE.isTrueFor(callExpression.callee(), ctx)) {
3637
ctx.addIssue(callExpression.callee(), MESSAGE);
3738
}
3839
});
Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,39 @@
11
def some_function():
22
from datetime import datetime
3-
datetime.utcnow() # Noncompliant {{Don't use `datetime.datetime.utcnow` to create this datetime object.}}
4-
#^^^^^^^^^^^^^^^^^
3+
4+
datetime.utcnow() # Noncompliant {{Don't use `datetime.datetime.utcnow` to create this datetime object.}}
5+
# ^^^^^^^^^^^^^^^
56
timestamp = 1571595618.0
6-
datetime.utcfromtimestamp(timestamp) # Noncompliant {{Don't use `datetime.datetime.utcfromtimestamp` to create this datetime object.}}
7-
#^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7+
datetime.utcfromtimestamp( # Noncompliant {{Don't use `datetime.datetime.utcfromtimestamp` to create this datetime object.}}
8+
# ^^^^^^^^^^^^^^^^^^^^^^^^^
9+
timestamp
10+
)
11+
812

913
def other_function():
1014
import datetime
15+
1116
timestamp = 1571595618.0
12-
datetime.datetime.utcnow() # Noncompliant
13-
datetime.datetime.utcfromtimestamp(timestamp) # Noncompliant
17+
datetime.datetime.utcnow() # Noncompliant
18+
datetime.datetime.utcfromtimestamp(timestamp) # Noncompliant
19+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
20+
21+
test = "test"
22+
a = f"test/{datetime.datetime.utcnow().strftime('%Y-%m-%d')}/{str(test)}" # Noncompliant
23+
# ^^^^^^^^^^^^^^^^^^^^^^^^
1424

1525
from datetime import datetime as dt
16-
dt.utcnow() # Noncompliant
17-
dt.utcfromtimestamp(timestamp) # Noncompliant
1826

19-
def unknown_symbols():
27+
dt.utcnow() # Noncompliant
28+
dt.utcfromtimestamp(timestamp) # Noncompliant
29+
2030

31+
def unknown_symbols():
2132
unrelated_call()
2233

23-
datetime.datetime.utcnow()
24-
datetime.datetime.utcfromtimestamp()
34+
datetime.datetime.utcnow() # Noncompliant
35+
datetime.datetime.utcfromtimestamp() # Noncompliant
36+
2537

2638
def compliant_examples():
2739
from datetime import datetime, timezone
@@ -30,11 +42,13 @@ def compliant_examples():
3042
timestamp = 1571595618.0
3143
datetime.fromtimestamp(timestamp, timezone.utc)
3244

45+
3346
def aliased_utcnow():
3447
from datetime import datetime
48+
3549
reassigned = datetime.utcnow
36-
# FN because we lose the FQN in the assignment
37-
reassigned()
50+
reassigned() # Noncompliant
3851

3952
from datetime import datetime as aliased
40-
aliased.utcnow() # Noncompliant
53+
54+
aliased.utcnow() # Noncompliant
Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,51 @@
11
def torch_import():
22
import torch
33

4-
x6 = Variable(torch.tensor([15]))
4+
x6 = Variable(torch.tensor([15]))
55

6-
x = torch.autograd.Variable(torch.tensor([1.0]), requires_grad=True) # Noncompliant {{Replace this call with a call to "torch.tensor".}}
7-
#^^^^^^^^^^^^^^^^^^^^^^^
8-
x2 = torch.autograd.Variable(torch.tensor([1.0])) # Noncompliant
9-
#^^^^^^^^^^^^^^^^^^^^^^^
10-
11-
x4 = torch.autograd.Variable() # Noncompliant
6+
x = torch.autograd.Variable( # Noncompliant {{Replace this call with a call to "torch.tensor".}}
7+
# ^^^^^^^^^^^^^^^^^^^^^^^
8+
torch.tensor([1.0]),
9+
requires_grad=True,
10+
)
11+
x2 = torch.autograd.Variable(torch.tensor([1.0])) # Noncompliant
12+
13+
x4 = torch.autograd.Variable() # Noncompliant
1214

1315
# Compliant solution
1416
c_x3 = torch.tensor([1.0])
1517

18+
1619
def torch_autograd_import_as():
1720
from torch.autograd import Variable as V
18-
x3 = V(torch.tensor([15])) # Noncompliant
19-
#^
20-
21+
22+
x3 = V(torch.tensor([15])) # Noncompliant
23+
# ^
24+
25+
2126
def torch_alias_import():
2227
import torch as t
23-
x5 = t.autograd.Variable(torch.tensor([15])) # Noncompliant
28+
29+
x5 = t.autograd.Variable(torch.tensor([15])) # Noncompliant
2430

2531

2632
def unrelated_import():
2733
from something.autograd import Variable
28-
x6 = Variable(torch.tensor([15]))
34+
35+
x6 = Variable(torch.tensor([15]))
36+
2937

3038
def multiple_imports():
31-
# Resulting symbol will be ambiguous, therefore no issue is raised
3239
from something.autograd import Variable
33-
x6 = Variable(torch.tensor([15]))
40+
41+
x6 = Variable(torch.tensor([15]))
3442
from torch.autograd import Variable
35-
x6 = Variable(torch.tensor([15]))
3643

37-
def use_before_assignment():
3844
x6 = Variable(torch.tensor([15])) # Noncompliant
45+
46+
47+
def use_before_assignment():
48+
x6 = Variable(torch.tensor([15]))
3949
from torch.autograd import Variable
50+
4051
x6 = Variable(torch.tensor([15])) # Noncompliant

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ public void visitFileInput(FileInput fileInput) {
192192

193193
@Override
194194
public void visitStringLiteral(StringLiteral stringLiteral) {
195+
super.visitStringLiteral(stringLiteral);
195196
var builtins = this.projectLevelTypeTable.getBuiltinsModule();
196197
// TODO: SONARPY-1867 multiple object types to represent str instance?
197198
if (((StringLiteralImpl) stringLiteral).isTemplate()) {

python-frontend/src/test/java/org/sonar/python/semantic/v2/types/typecalculator/QualifiedExpressionCalculatorTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,22 @@ class A:
115115
assertThat(result).isEqualTo(PythonType.UNKNOWN);
116116
}
117117

118+
@Test
119+
void calculate_nonExistentMemberOnClass_returnsUnresolvedImportType() {
120+
FileInput fileInput = parseAndInferTypes("""
121+
class A:
122+
pass
123+
A.non_existent
124+
""");
125+
126+
var qualifiedExpression = getQualifiedExpressionFromStatement(fileInput);
127+
PythonType result = calculator.calculate(qualifiedExpression);
128+
129+
assertThat(result).isInstanceOf(UnknownType.UnresolvedImportType.class);
130+
var unresolvedType = (UnknownType.UnresolvedImportType) result;
131+
assertThat(unresolvedType.importPath()).isEqualTo("my_package.mod.A.non_existent");
132+
}
133+
118134
@Test
119135
void calculate_unknownMemberOnKnownModule_returnsUnresolvedImportType() {
120136
FileInput fileInput = parseAndInferTypes("""

0 commit comments

Comments
 (0)