Skip to content

Commit b2cba26

Browse files
SONARPY-764 Rule S5914: Assertions should not fail or succeed unconditionally (#1179)
1 parent 9bf9a6d commit b2cba26

File tree

10 files changed

+526
-45
lines changed

10 files changed

+526
-45
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
'project:django-cms-3.7.1/cms/tests/test_apphooks.py':[
3+
621,
4+
629,
5+
],
6+
'project:django-cms-3.7.1/cms/tests/test_plugins.py':[
7+
1113,
8+
],
9+
'project:tensorflow/python/client/events_writer_test.py':[
10+
51,
11+
],
12+
'project:tensorflow/python/data/experimental/kernel_tests/group_by_window_test.py':[
13+
222,
14+
],
15+
'project:tensorflow/python/framework/function_test.py':[
16+
704,
17+
715,
18+
],
19+
'project:tensorflow/python/kernel_tests/fifo_queue_test.py':[
20+
717,
21+
718,
22+
721,
23+
722,
24+
],
25+
'project:tensorflow/python/kernel_tests/lookup_ops_test.py':[
26+
852,
27+
871,
28+
889,
29+
],
30+
'project:tensorflow/python/training/monitored_session_test.py':[
31+
1677,
32+
1695,
33+
1715,
34+
2266,
35+
],
36+
'project:tensorflow/python/training/supervisor_test.py':[
37+
94,
38+
],
39+
'project:tensorflow/tools/compatibility/tf_upgrade_v2_test.py':[
40+
190,
41+
219,
42+
],
43+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
import org.sonar.python.checks.tests.ImplicitlySkippedTestCheck;
8181
import org.sonar.python.checks.tests.NotDiscoverableTestMethodCheck;
8282
import org.sonar.python.checks.tests.SkippedTestNoReasonCheck;
83+
import org.sonar.python.checks.tests.UnconditionalAssertionCheck;
8384

8485
public final class CheckList {
8586

@@ -274,6 +275,7 @@ public static Iterable<Class> getChecks() {
274275
ReferencedBeforeAssignmentCheck.class,
275276
RegexComplexityCheck.class,
276277
RegexLookaheadCheck.class,
278+
UnconditionalAssertionCheck.class,
277279
UndefinedNameAllPropertyCheck.class,
278280
UnquantifiedNonCapturingGroupCheck.class,
279281
UnreachableExceptCheck.class,

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,23 @@
2626
import org.sonar.plugins.python.api.tree.Argument;
2727
import org.sonar.plugins.python.api.tree.CallExpression;
2828
import org.sonar.plugins.python.api.tree.ClassDef;
29+
import org.sonar.plugins.python.api.tree.DictionaryLiteral;
2930
import org.sonar.plugins.python.api.tree.Expression;
31+
import org.sonar.plugins.python.api.tree.ListLiteral;
3032
import org.sonar.plugins.python.api.tree.Name;
33+
import org.sonar.plugins.python.api.tree.SetLiteral;
3134
import org.sonar.plugins.python.api.tree.Tree;
35+
import org.sonar.plugins.python.api.tree.Tuple;
3236
import org.sonar.python.api.PythonTokenType;
3337
import org.sonar.python.tree.TreeUtils;
3438

39+
import static org.sonar.plugins.python.api.tree.Tree.Kind.GENERATOR_EXPR;
40+
import static org.sonar.plugins.python.api.tree.Tree.Kind.LAMBDA;
41+
import static org.sonar.plugins.python.api.tree.Tree.Kind.NONE;
42+
import static org.sonar.plugins.python.api.tree.Tree.Kind.NUMERIC_LITERAL;
43+
import static org.sonar.plugins.python.api.tree.Tree.Kind.STRING_LITERAL;
44+
import static org.sonar.plugins.python.api.tree.Tree.Kind.UNPACKING_EXPR;
45+
3546
public class CheckUtils {
3647

3748
private CheckUtils() {
@@ -104,4 +115,35 @@ private static boolean calleeHasNameLocals(CallExpression callExpression) {
104115
Expression callee = callExpression.callee();
105116
return callee.is(Tree.Kind.NAME) && "locals".equals(((Name) callee).name());
106117
}
118+
119+
public static boolean isConstant(Expression condition) {
120+
return isImmutableConstant(condition) || isConstantCollectionLiteral(condition);
121+
}
122+
123+
public static boolean isImmutableConstant(Expression condition) {
124+
return TreeUtils.isBooleanLiteral(condition) ||
125+
condition.is(NUMERIC_LITERAL, STRING_LITERAL, NONE, LAMBDA, GENERATOR_EXPR);
126+
}
127+
128+
public static boolean isConstantCollectionLiteral(Expression condition) {
129+
switch (condition.getKind()) {
130+
case LIST_LITERAL:
131+
return doesNotContainUnpackingExpression(((ListLiteral) condition).elements().expressions());
132+
case DICTIONARY_LITERAL:
133+
return doesNotContainUnpackingExpression(((DictionaryLiteral) condition).elements());
134+
case SET_LITERAL:
135+
return doesNotContainUnpackingExpression(((SetLiteral) condition).elements());
136+
case TUPLE:
137+
return doesNotContainUnpackingExpression(((Tuple) condition).elements());
138+
default:
139+
return false;
140+
}
141+
}
142+
143+
private static boolean doesNotContainUnpackingExpression(List<? extends Tree> elements) {
144+
if (elements.isEmpty()) {
145+
return true;
146+
}
147+
return elements.stream().anyMatch(element -> !element.is(UNPACKING_EXPR));
148+
}
107149
}

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

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

22-
import java.util.Arrays;
2322
import java.util.List;
2423
import java.util.Set;
2524
import org.sonar.check.Rule;
@@ -31,37 +30,27 @@
3130
import org.sonar.plugins.python.api.tree.BinaryExpression;
3231
import org.sonar.plugins.python.api.tree.ComprehensionIf;
3332
import org.sonar.plugins.python.api.tree.ConditionalExpression;
34-
import org.sonar.plugins.python.api.tree.DictionaryLiteral;
3533
import org.sonar.plugins.python.api.tree.Expression;
3634
import org.sonar.plugins.python.api.tree.FileInput;
3735
import org.sonar.plugins.python.api.tree.HasSymbol;
3836
import org.sonar.plugins.python.api.tree.IfStatement;
39-
import org.sonar.plugins.python.api.tree.ListLiteral;
4037
import org.sonar.plugins.python.api.tree.Name;
41-
import org.sonar.plugins.python.api.tree.SetLiteral;
42-
import org.sonar.plugins.python.api.tree.Tree;
43-
import org.sonar.plugins.python.api.tree.Tuple;
4438
import org.sonar.plugins.python.api.tree.UnaryExpression;
4539
import org.sonar.python.cfg.fixpoint.ReachingDefinitionsAnalysis;
46-
import org.sonar.python.tree.TreeUtils;
4740

4841
import static org.sonar.plugins.python.api.tree.Tree.Kind.AND;
49-
import static org.sonar.plugins.python.api.tree.Tree.Kind.GENERATOR_EXPR;
50-
import static org.sonar.plugins.python.api.tree.Tree.Kind.LAMBDA;
5142
import static org.sonar.plugins.python.api.tree.Tree.Kind.NAME;
52-
import static org.sonar.plugins.python.api.tree.Tree.Kind.NONE;
5343
import static org.sonar.plugins.python.api.tree.Tree.Kind.NOT;
54-
import static org.sonar.plugins.python.api.tree.Tree.Kind.NUMERIC_LITERAL;
5544
import static org.sonar.plugins.python.api.tree.Tree.Kind.OR;
5645
import static org.sonar.plugins.python.api.tree.Tree.Kind.QUALIFIED_EXPR;
57-
import static org.sonar.plugins.python.api.tree.Tree.Kind.STRING_LITERAL;
58-
import static org.sonar.plugins.python.api.tree.Tree.Kind.UNPACKING_EXPR;
46+
import static org.sonar.python.checks.CheckUtils.isConstant;
47+
import static org.sonar.python.checks.CheckUtils.isImmutableConstant;
5948

6049
@Rule(key = "S5797")
6150
public class ConstantConditionCheck extends PythonVisitorCheck {
6251

6352
private static final String MESSAGE = "Replace this expression; used as a condition it will always be constant.";
64-
private static final List<String> ACCEPTED_DECORATORS = Arrays.asList("overload", "staticmethod", "classmethod");
53+
private static final List<String> ACCEPTED_DECORATORS = List.of("overload", "staticmethod", "classmethod");
6554
private ReachingDefinitionsAnalysis reachingDefinitionsAnalysis;
6655

6756
@Override
@@ -98,15 +87,6 @@ private void checkConstantCondition(Expression condition) {
9887
checkExpression(condition);
9988
}
10089

101-
private static boolean isConstant(Expression condition) {
102-
return isImmutableConstant(condition) || isConstantCollectionLiteral(condition);
103-
}
104-
105-
private static boolean isImmutableConstant(Expression condition) {
106-
return TreeUtils.isBooleanLiteral(condition) ||
107-
condition.is(NUMERIC_LITERAL, STRING_LITERAL, NONE, LAMBDA, GENERATOR_EXPR);
108-
}
109-
11090
private static Expression getConstantBooleanExpression(Expression condition) {
11191
if (condition.is(AND, OR)) {
11292
BinaryExpression binaryExpression = (BinaryExpression) condition;
@@ -123,28 +103,6 @@ private static Expression getConstantBooleanExpression(Expression condition) {
123103
return null;
124104
}
125105

126-
private static boolean isConstantCollectionLiteral(Expression condition) {
127-
switch (condition.getKind()) {
128-
case LIST_LITERAL:
129-
return isAlwaysEmptyOrNonEmptyCollection(((ListLiteral) condition).elements().expressions());
130-
case DICTIONARY_LITERAL:
131-
return isAlwaysEmptyOrNonEmptyCollection(((DictionaryLiteral) condition).elements());
132-
case SET_LITERAL:
133-
return isAlwaysEmptyOrNonEmptyCollection(((SetLiteral) condition).elements());
134-
case TUPLE:
135-
return isAlwaysEmptyOrNonEmptyCollection(((Tuple) condition).elements());
136-
default:
137-
return false;
138-
}
139-
}
140-
141-
private static boolean isAlwaysEmptyOrNonEmptyCollection(List<? extends Tree> elements) {
142-
if (elements.isEmpty()) {
143-
return true;
144-
}
145-
return elements.stream().anyMatch(element -> !element.is(UNPACKING_EXPR));
146-
}
147-
148106
/**
149107
* Checks if boolean expressions are used as an alternative to 'Conditional Expression'.
150108
* e.g. 'x = f() or 3 or g()'
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2022 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.checks.tests;
21+
22+
import java.util.List;
23+
import java.util.Set;
24+
import org.sonar.check.Rule;
25+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
26+
import org.sonar.plugins.python.api.SubscriptionContext;
27+
import org.sonar.plugins.python.api.symbols.FunctionSymbol;
28+
import org.sonar.plugins.python.api.symbols.Symbol;
29+
import org.sonar.plugins.python.api.tree.Argument;
30+
import org.sonar.plugins.python.api.tree.AssertStatement;
31+
import org.sonar.plugins.python.api.tree.CallExpression;
32+
import org.sonar.plugins.python.api.tree.Expression;
33+
import org.sonar.plugins.python.api.tree.HasSymbol;
34+
import org.sonar.plugins.python.api.tree.Name;
35+
import org.sonar.plugins.python.api.tree.NumericLiteral;
36+
import org.sonar.plugins.python.api.tree.RegularArgument;
37+
import org.sonar.plugins.python.api.tree.Tree;
38+
import org.sonar.python.cfg.fixpoint.ReachingDefinitionsAnalysis;
39+
import org.sonar.python.checks.CheckUtils;
40+
import org.sonar.python.tree.TreeUtils;
41+
42+
import static org.sonar.plugins.python.api.tree.Tree.Kind.NAME;
43+
import static org.sonar.plugins.python.api.tree.Tree.Kind.NUMERIC_LITERAL;
44+
import static org.sonar.plugins.python.api.tree.Tree.Kind.QUALIFIED_EXPR;
45+
import static org.sonar.python.checks.CheckUtils.isConstant;
46+
47+
@Rule(key = "S5914")
48+
public class UnconditionalAssertionCheck extends PythonSubscriptionCheck {
49+
50+
private static final List<String> BOOLEAN_ASSERTIONS = List.of("assertTrue", "assertFalse");
51+
private static final List<String> NONE_ASSERTIONS = List.of("assertIsNone", "assertIsNotNone");
52+
private static final List<String> IS_ASSERTIONS = List.of("assertIs", "assertIsNot");
53+
54+
private static final String BOOLEAN_MESSAGE = "Replace this expression; its boolean value is constant.";
55+
private static final String NONE_MESSAGE = "Remove this identity assertion; its value is constant.";
56+
private static final String IS_MESSAGE = "Replace this \"assertIs\" call with an \"assertEqual\" call.";
57+
private static final String IS_NOT_MESSAGE = "Replace this \"assertIsNot\" call with an \"assertNotEqual\" call.";
58+
private static final String IS_SECONDARY_MESSAGE = "This expression creates a new object every time.";
59+
60+
private static final List<String> ACCEPTED_DECORATORS = List.of("overload", "staticmethod", "classmethod");
61+
62+
private ReachingDefinitionsAnalysis reachingDefinitionsAnalysis;
63+
64+
@Override
65+
public void initialize(Context context) {
66+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, ctx ->
67+
reachingDefinitionsAnalysis = new ReachingDefinitionsAnalysis(ctx.pythonFile()));
68+
69+
context.registerSyntaxNodeConsumer(Tree.Kind.ASSERT_STMT, ctx -> {
70+
AssertStatement assertStatement = (AssertStatement) ctx.syntaxNode();
71+
Expression condition = assertStatement.condition();
72+
if (!condition.is(Tree.Kind.TUPLE) && !isFalseOrZeroLiteral(condition) && CheckUtils.isConstant(condition)) {
73+
ctx.addIssue(condition, BOOLEAN_MESSAGE);
74+
}
75+
});
76+
77+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, ctx -> {
78+
CallExpression call = (CallExpression) ctx.syntaxNode();
79+
Symbol symbol = call.calleeSymbol();
80+
if (symbol == null) {
81+
return;
82+
}
83+
84+
String name = symbol.name();
85+
List<Argument> arguments = call.arguments();
86+
87+
if (BOOLEAN_ASSERTIONS.contains(name)) {
88+
checkBooleanAssertion(ctx, TreeUtils.nthArgumentOrKeyword(0, "expr", arguments));
89+
} else if (NONE_ASSERTIONS.contains(name)) {
90+
checkNoneAssertion(ctx, call, TreeUtils.nthArgumentOrKeyword(0, "expr", arguments));
91+
} else if (IS_ASSERTIONS.contains(name)) {
92+
String message = "assertIs".equals(name) ? IS_MESSAGE : IS_NOT_MESSAGE;
93+
checkIsAssertion(ctx, call, TreeUtils.nthArgumentOrKeyword(0, "first", arguments), message);
94+
checkIsAssertion(ctx, call, TreeUtils.nthArgumentOrKeyword(1, "second", arguments), message);
95+
}
96+
});
97+
}
98+
99+
/**
100+
* `assert False` or `assert 0` is often used to make a test fail.
101+
* Usually it is better to use another assertion or throw an AssertionException.
102+
* However, this rule is not intended to check this best practice.
103+
*/
104+
private static boolean isFalseOrZeroLiteral(Expression expression) {
105+
if (expression.is(NAME)) {
106+
return "False".equals(((Name) expression).name());
107+
}
108+
if (expression.is(NUMERIC_LITERAL)) {
109+
return ((NumericLiteral) expression).valueAsLong() == 0;
110+
}
111+
return false;
112+
}
113+
114+
private void checkNoneAssertion(SubscriptionContext ctx, CallExpression call, RegularArgument arg) {
115+
if (isUnconditional(arg)) {
116+
ctx.addIssue(call, NONE_MESSAGE);
117+
}
118+
}
119+
120+
private void checkBooleanAssertion(SubscriptionContext ctx, RegularArgument arg) {
121+
if (isUnconditional(arg)) {
122+
ctx.addIssue(arg, BOOLEAN_MESSAGE);
123+
}
124+
}
125+
126+
private static void checkIsAssertion(SubscriptionContext ctx, CallExpression call, RegularArgument arg, String message) {
127+
if (CheckUtils.isConstantCollectionLiteral(arg.expression())) {
128+
ctx.addIssue(call.callee(), message).secondary(arg, IS_SECONDARY_MESSAGE);
129+
}
130+
}
131+
132+
private boolean isUnconditional(RegularArgument argument) {
133+
Expression expression = argument.expression();
134+
if (isConstant(expression)) {
135+
return true;
136+
}
137+
138+
if (expression.is(NAME) || expression.is(QUALIFIED_EXPR)) {
139+
Symbol symbol = ((HasSymbol) expression).symbol();
140+
if (symbol != null && isClassOrFunction(symbol)) {
141+
return true;
142+
}
143+
}
144+
145+
if (expression.is(NAME)) {
146+
Set<Expression> valuesAtLocation = reachingDefinitionsAnalysis.valuesAtLocation(((Name) expression));
147+
if (valuesAtLocation.size() == 1) {
148+
return CheckUtils.isImmutableConstant(valuesAtLocation.iterator().next());
149+
}
150+
}
151+
152+
return false;
153+
}
154+
155+
private static boolean isClassOrFunction(Symbol symbol) {
156+
if (symbol.is(Symbol.Kind.CLASS)) {
157+
return true;
158+
}
159+
if (symbol.is(Symbol.Kind.FUNCTION)) {
160+
// Avoid potential FPs with properties: only report on limited selection of "safe" decorators
161+
return ACCEPTED_DECORATORS.containsAll(((FunctionSymbol) symbol).decorators());
162+
}
163+
return false;
164+
}
165+
166+
@Override
167+
public CheckScope scope() {
168+
return CheckScope.ALL;
169+
}
170+
}

0 commit comments

Comments
 (0)