Skip to content

Commit 35c8e76

Browse files
ghislainpiotthomas-serre-sonarsource
authored andcommitted
SONARPY-3124 S6709: Exclude SVC (#364)
Co-authored-by: Thomas Serre <thomas.serre@sonarsource.com> GitOrigin-RevId: e5684d58f5f1f2af154caeb7ae31db9786247dac
1 parent 9b91363 commit 35c8e76

5 files changed

Lines changed: 141 additions & 7 deletions

File tree

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,13 @@
3333
import org.sonar.plugins.python.api.tree.RegularArgument;
3434
import org.sonar.plugins.python.api.tree.Tree;
3535
import org.sonar.python.cfg.fixpoint.ReachingDefinitionsAnalysis;
36+
import org.sonar.python.checks.cdk.CdkPredicate;
3637
import org.sonar.python.semantic.ClassSymbolImpl;
3738
import org.sonar.python.semantic.SymbolUtils;
3839
import org.sonar.python.tree.TreeUtils;
40+
import org.sonar.python.types.v2.TypeCheckMap;
41+
42+
import static org.sonar.python.checks.hotspots.CommonValidationUtils.singleAssignedString;
3943

4044
@Rule(key = "S6709")
4145
public class RandomSeedCheck extends PythonSubscriptionCheck {
@@ -61,10 +65,44 @@ public class RandomSeedCheck extends PythonSubscriptionCheck {
6165

6266
private ReachingDefinitionsAnalysis reachingDefinitionsAnalysis;
6367

68+
private static Predicate<CallExpression> keywordAbsentOrNotIn(String keyword, String... restrictedValues) {
69+
Set<String> restrictedValueSet = Set.of(restrictedValues);
70+
return call -> {
71+
var arg = TreeUtils.argumentByKeyword(keyword, call.arguments());
72+
if (arg == null) {
73+
return true;
74+
}
75+
String expressionString = singleAssignedString(arg.expression());
76+
return restrictedValueSet.stream().noneMatch(expressionString::equals);
77+
};
78+
}
79+
private static Predicate<CallExpression> probabilityArgAbsent() {
80+
return call -> {
81+
var probabilityArg = TreeUtils.argumentByKeyword("probability", call.arguments());
82+
return probabilityArg == null || CdkPredicate.isFalse().test(probabilityArg.expression());
83+
};
84+
}
85+
86+
private static final Predicate<CallExpression> SOLVER_NOT_SAG_SAGA = keywordAbsentOrNotIn("solver", "sag", "saga");
87+
private static final Predicate<CallExpression> SELECTION_NOT_RANDOM = keywordAbsentOrNotIn("selection", "random");
88+
89+
private static final Map<String, Predicate<CallExpression>> SKLEARN_EXCEPTIONS = Map.ofEntries(
90+
Map.entry("sklearn.svm._classes.SVC", probabilityArgAbsent()),
91+
Map.entry("sklearn.linear_model._logistic.LogisticRegression", SOLVER_NOT_SAG_SAGA),
92+
Map.entry("sklearn.linear_model._ridge.Ridge", SOLVER_NOT_SAG_SAGA),
93+
Map.entry("sklearn.linear_model._coordinate_descent.Lasso", SELECTION_NOT_RANDOM),
94+
Map.entry("sklearn.linear_model._coordinate_descent.ElasticNet", SELECTION_NOT_RANDOM));
95+
96+
private TypeCheckMap<Predicate<CallExpression>> typeCheckMap;
97+
6498
@Override
6599
public void initialize(Context context) {
66100
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT,
67-
ctx -> this.reachingDefinitionsAnalysis = new ReachingDefinitionsAnalysis(ctx.pythonFile()));
101+
ctx -> {
102+
this.reachingDefinitionsAnalysis = new ReachingDefinitionsAnalysis(ctx.pythonFile());
103+
this.typeCheckMap = new TypeCheckMap<>();
104+
SKLEARN_EXCEPTIONS.forEach((fqn, predicate) -> this.typeCheckMap.put(ctx.typeChecker().typeCheckBuilder().isTypeWithFqn(fqn), predicate));
105+
});
68106
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::checkEmptySeedCall);
69107
}
70108

@@ -82,6 +120,7 @@ private void checkEmptySeedCall(SubscriptionContext ctx) {
82120
.filter(symbol -> symbol.fullyQualifiedName() != null && symbol.fullyQualifiedName().startsWith(SKLEARN_FQN))
83121
.filter(RandomSeedCheck::hasRandomStateParameter)
84122
.filter(symbol -> isArgumentAbsentOrNone(TreeUtils.argumentByKeyword(SKLEARN_ARG_NAME, call.arguments())))
123+
.filter(symbol -> !isSKLearnException(call))
85124
.map(symbol -> SKLEARN_MESSAGE))
86125
.ifPresent(message -> ctx.addIssue(call.callee(), message));
87126
}
@@ -128,4 +167,11 @@ private boolean isAssignedNone(Expression exp) {
128167
.filter(Predicate.not(Set::isEmpty))
129168
.filter(values -> values.stream().allMatch(value -> value.is(Tree.Kind.NONE))).isPresent();
130169
}
170+
171+
private boolean isSKLearnException(CallExpression call) {
172+
var calleeType = call.callee().typeV2();
173+
return typeCheckMap.getOptionalForType(calleeType)
174+
.map(predicate -> predicate.test(call))
175+
.orElse(false);
176+
}
131177
}

python-checks/src/main/java/org/sonar/python/checks/hotspots/CommonValidationUtils.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.sonar.python.checks.hotspots;
1818

1919
import java.util.function.BiConsumer;
20+
2021
import org.sonar.plugins.python.api.SubscriptionContext;
2122
import org.sonar.plugins.python.api.tree.BinaryExpression;
2223
import org.sonar.plugins.python.api.tree.CallExpression;
@@ -99,7 +100,7 @@ private static boolean isNumericLiteralEqualToDouble(NumericLiteral numericLiter
99100
}
100101
}
101102

102-
static String singleAssignedString(Expression expression) {
103+
public static String singleAssignedString(Expression expression) {
103104
if (expression instanceof Name name) {
104105
return Expressions.singleAssignedNonNameValue(name)
105106
.map(CommonValidationUtils::singleAssignedString)
@@ -108,6 +109,18 @@ static String singleAssignedString(Expression expression) {
108109
return expression.is(Tree.Kind.STRING_LITERAL) ? ((StringLiteral) expression).trimmedQuotesValue() : "";
109110
}
110111

112+
public static boolean isStringEqualTo(Expression expression, String expected) {
113+
if (expression instanceof Name name) {
114+
return Expressions.singleAssignedNonNameValue(name)
115+
.map(value -> isStringEqualTo(value, expected))
116+
.orElse(false);
117+
}
118+
if (expression instanceof StringLiteral stringLiteral) {
119+
return expected.equals(stringLiteral.trimmedQuotesValue());
120+
}
121+
return false;
122+
}
123+
111124
interface CallValidator {
112125
void validate(SubscriptionContext ctx, CallExpression callExpression);
113126
}

python-checks/src/test/java/org/sonar/python/checks/hotspots/CommonValidationUtilsTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,30 @@ private static void checkCallExpr(SubscriptionContext ctx) {
5353
}
5454
}
5555

56+
static class isStringEqualToTestCheck extends PythonSubscriptionCheck {
57+
@Override
58+
public void initialize(Context context) {
59+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, isStringEqualToTestCheck::checkCallExpr);
60+
}
61+
62+
private static void checkCallExpr(SubscriptionContext ctx) {
63+
CallExpression callExpression = (CallExpression) ctx.syntaxNode();
64+
TreeUtils.nthArgumentOrKeywordOptional(0, "arg", callExpression.arguments())
65+
.ifPresent(argument -> {
66+
if (CommonValidationUtils.isStringEqualTo(argument.expression(), "abc")) {
67+
ctx.addIssue(argument, "Argument is abc");
68+
}
69+
});
70+
}
71+
}
72+
5673
@Test
5774
void isLessThan() {
5875
PythonCheckVerifier.verify("src/test/resources/checks/commonValidationUtils.py", new isLessThanMoreThanTestCheck());
5976
}
77+
78+
@Test
79+
void isStringEqualTo() {
80+
PythonCheckVerifier.verify("src/test/resources/checks/commonValidationUtilsString.py", new isStringEqualToTestCheck());
81+
}
6082
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
callExpr("abc") # Noncompliant {{Argument is abc}}
2+
callExpr("def")
3+
4+
abc_var = "abc"
5+
callExpr(abc_var) # Noncompliant {{Argument is abc}}
6+
7+
def_var = "def"
8+
callExpr(def_var)
9+
10+
callExpr(arg="abc") # Noncompliant {{Argument is abc}}
11+
callExpr(arg="def")
12+
13+
callExpr(arg=f"{abc_var}") # FN f-string are not supported
14+
callExpr(arg="def")
15+
16+
callExpr(arg=3)
17+
not_a_string = 3
18+
callExpr(arg=not_a_string)
Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,58 @@
11
from sklearn.model_selection import train_test_split
22
from sklearn.svm import SVC
3+
from sklearn.neural_network import MLPClassifier
34
from sklearn.datasets import load_iris, make_blobs
5+
from sklearn.linear_model import LogisticRegression
6+
from sklearn.linear_model import Ridge, Lasso, ElasticNet
47

58
def failure():
69
X, y = load_iris(return_X_y=True)
710
X_train, X_test, y_train, y_test = train_test_split(X, y) # Noncompliant {{Provide a seed for the random_state parameter.}}
811
# ^^^^^^^^^^^^^^^^
9-
svc = SVC() # Noncompliant
10-
# ^^^
12+
svc = SVC() # Compliant
13+
14+
svc2 = SVC(probability=True) # Noncompliant
15+
# ^^^
16+
17+
mlp = MLPClassifier() # Noncompliant
1118

1219
X, y = make_blobs(n_samples=1300, random_state=None) # Noncompliant
1320
# ^^^^^^^^^^
1421

22+
lr_default = LogisticRegression() # Compliant
23+
24+
lr_sag = LogisticRegression(solver="sag") # Noncompliant
25+
# ^^^^^^^^^^^^^^^^^^
26+
27+
lr_saga = LogisticRegression(solver='saga') # Noncompliant
28+
# ^^^^^^^^^^^^^^^^^^
29+
30+
lr_unknown_string = LogisticRegression(solver=some_unknown_string) # Compliant
31+
32+
ridge_default = Ridge() # Compliant
33+
34+
ridge_sag = Ridge(solver="sag") # Noncompliant
35+
# ^^^^^
36+
37+
ridge_saga = Ridge(solver='saga') # Noncompliant
38+
# ^^^^^
39+
40+
lasso_default = Lasso() # Compliant
41+
42+
lasso_random = Lasso(selection="random") # Noncompliant
43+
# ^^^^^
44+
45+
elastic_default = ElasticNet() # Compliant
46+
47+
elastic_random = ElasticNet(selection='random') # Noncompliant
48+
# ^^^^^^^^^^
49+
1550
def success():
1651
from sklearn.ensemble import RandomForestClassifier
1752
rfc = RandomForestClassifier(random_state=0) # Compliant
18-
from sklearn.linear_model import SGDClassifier
53+
from sklearn.linear_model import SGDClassifier
1954
sgd = SGDClassifier(random_state=foo()) # Compliant
20-
55+
2156
def sklearn_seed(rng):
2257
svc = SVC(random_state=rng) # Compliant
2358

@@ -30,5 +65,5 @@ def ambiguous():
3065
from sklearn.svm import SVC as something
3166
from sklearn.datasets import make_blobs as something
3267

33-
something = something() # FN
68+
something = something() # FN
3469

0 commit comments

Comments
 (0)