Skip to content

Commit a2cd7b8

Browse files
authored
SONARPY-1643 S6900: Numpy weekmask should have a valid value (#1737)
1 parent 0b4f278 commit a2cd7b8

File tree

7 files changed

+322
-0
lines changed

7 files changed

+322
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ public static Iterable<Class> getChecks() {
278278
NoSonarCommentCheck.class,
279279
NotDiscoverableTestMethodCheck.class,
280280
NotImplementedErrorInOperatorMethodsCheck.class,
281+
NumpyWeekMaskValidationCheck.class,
281282
NumpyIsNanCheck.class,
282283
NumpyListOverGeneratorCheck.class,
283284
NumpyRandomSeedCheck.class,
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2024 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;
21+
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Optional;
25+
import java.util.Set;
26+
import java.util.regex.Pattern;
27+
import javax.annotation.Nullable;
28+
import org.sonar.check.Rule;
29+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
30+
import org.sonar.plugins.python.api.SubscriptionContext;
31+
import org.sonar.plugins.python.api.symbols.Symbol;
32+
import org.sonar.plugins.python.api.tree.CallExpression;
33+
import org.sonar.plugins.python.api.tree.Expression;
34+
import org.sonar.plugins.python.api.tree.ExpressionList;
35+
import org.sonar.plugins.python.api.tree.ListLiteral;
36+
import org.sonar.plugins.python.api.tree.Name;
37+
import org.sonar.plugins.python.api.tree.NumericLiteral;
38+
import org.sonar.plugins.python.api.tree.RegularArgument;
39+
import org.sonar.plugins.python.api.tree.StringLiteral;
40+
import org.sonar.plugins.python.api.tree.Tree;
41+
import org.sonar.python.checks.utils.Expressions;
42+
import org.sonar.python.tree.TreeUtils;
43+
44+
@Rule(key = "S6900")
45+
public class NumpyWeekMaskValidationCheck extends PythonSubscriptionCheck {
46+
private static final Pattern PATTERN_STRING1 = Pattern.compile("^[01]{7}$");
47+
private static final Pattern PATTERN_STRING2 = Pattern
48+
.compile("^(Mon|Tue|Wed|Thu|Fri|Sat|Sun|\\s|\\\\t|\\\\n|\\\\x0b|\\\\x0c|\\\\r)*+$");
49+
private static final Set<String> VALID_WEEKMASK_ARRAY_VALUES = Set.of("0", "1");
50+
private static final String MESSAGE_ARRAY = "Array must have 7 elements, all of which are 0 or 1.";
51+
private static final String MESSAGE_STRING = "String must be either 7 characters long and contain only 0 and 1, or contain abbreviated weekdays.";
52+
private static final String MESSAGE_SECONDARY_LOCATION = "Invalid mask is created here.";
53+
private static final Map<String, Integer> FUNCTIONS_PARAMETER_POSITION = Map.of(
54+
"numpy.busday_offset", 3,
55+
"numpy.busday_count", 2,
56+
"numpy.is_busday", 1,
57+
"numpy.busdaycalendar", 0);
58+
59+
@Override
60+
public void initialize(Context context) {
61+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, NumpyWeekMaskValidationCheck::checkCallExpr);
62+
}
63+
64+
private static void checkCallExpr(SubscriptionContext context) {
65+
CallExpression callExpression = (CallExpression) context.syntaxNode();
66+
var weekmaskArgumentOptional = Optional.ofNullable(callExpression.calleeSymbol())
67+
.map(Symbol::fullyQualifiedName)
68+
.filter(FUNCTIONS_PARAMETER_POSITION::containsKey)
69+
.map(FUNCTIONS_PARAMETER_POSITION::get)
70+
.map(position -> TreeUtils.nthArgumentOrKeyword(position, "weekmask", callExpression.arguments()));
71+
if (weekmaskArgumentOptional.isEmpty()) {
72+
return;
73+
}
74+
75+
RegularArgument weekmaskArgument = weekmaskArgumentOptional.get();
76+
checkExpression(context, weekmaskArgument.expression(), weekmaskArgument.expression(), null);
77+
}
78+
79+
private static void checkExpression(SubscriptionContext context, Expression expression, Tree primaryLocation, @Nullable Tree secondaryLocation) {
80+
if (expression.is(Tree.Kind.STRING_LITERAL)) {
81+
checkString(context, ((StringLiteral) expression).trimmedQuotesValue(), primaryLocation, secondaryLocation);
82+
} else if (expression.is(Tree.Kind.LIST_LITERAL)) {
83+
checkList(context, ((ListLiteral) expression), primaryLocation, secondaryLocation);
84+
} else if (expression.is(Tree.Kind.NAME)) {
85+
Expressions.singleAssignedNonNameValue((Name) expression).ifPresent(assignedExpression -> checkExpression(context, assignedExpression, primaryLocation, assignedExpression));
86+
}
87+
}
88+
89+
private static void checkString(SubscriptionContext context, String string, Tree primaryLocation, @Nullable Tree secondaryLocation) {
90+
if (PATTERN_STRING1.matcher(string).matches() || PATTERN_STRING2.matcher(string).matches()) {
91+
return;
92+
}
93+
createIssue(context, MESSAGE_STRING, primaryLocation, secondaryLocation);
94+
}
95+
96+
private static void checkList(SubscriptionContext context, ListLiteral listLiteral, Tree primaryLocation, @Nullable Tree secondaryLocation) {
97+
ExpressionList listElements = listLiteral.elements();
98+
List<Expression> expressionList = listElements.expressions();
99+
if (expressionList.size() == 7 && expressionList.stream()
100+
.allMatch(e -> TreeUtils.toOptionalInstanceOf(NumericLiteral.class, e)
101+
.map(NumericLiteral::valueAsString)
102+
.filter(VALID_WEEKMASK_ARRAY_VALUES::contains)
103+
.isPresent())) {
104+
return;
105+
}
106+
createIssue(context, MESSAGE_ARRAY, primaryLocation, secondaryLocation);
107+
}
108+
109+
private static void createIssue(SubscriptionContext context, String message, Tree primaryLocation, @Nullable Tree secondaryLocation) {
110+
PreciseIssue issue = context.addIssue(primaryLocation, message);
111+
if (secondaryLocation != null) {
112+
issue.secondary(secondaryLocation, MESSAGE_SECONDARY_LOCATION);
113+
}
114+
}
115+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<p>This rule raises an issue when a <code>numpy.busday_offset</code> object is created with an incorrect weekmask.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>To allow a datetime to be used in contexts where only certain days of the week are valid, NumPy includes a set of business day functions.
4+
<code>Weekmask</code> is used to customize valid business days.</p>
5+
<p><code>Weekmask</code> can be specified in several formats:</p>
6+
<ol>
7+
<li> As an array of 7 <code>1</code> or <code>0</code> values, e.g. <code>[1, 1, 1, 1, 1, 0, 0]</code> </li>
8+
<li> As a string of 7 <code>1</code> or <code>0</code> characters, e.g. <code>"1111100"</code> </li>
9+
<li> As a string with abbreviations of valid days from this list: <code>Mon Tue Wed Thu Fri Sat Sun</code>, e.g. <code>"Mon Tue Wed Thu Fri"</code>
10+
</li>
11+
</ol>
12+
<p>Setting an incorrect <code>weekmask</code> leads to <code>ValueError</code>.</p>
13+
<h2>How to fix it</h2>
14+
<p>Provide a <code>weekmask</code> with correct values.</p>
15+
<h3>Code examples</h3>
16+
<h4>Noncompliant code example</h4>
17+
<pre data-diff-id="1" data-diff-type="noncompliant">
18+
import numpy as np
19+
20+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask='01') # Noncompliant: ValueError
21+
</pre>
22+
<h4>Compliant solution</h4>
23+
<pre data-diff-id="1" data-diff-type="compliant">
24+
import numpy as np
25+
26+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask='0111100') # OK
27+
</pre>
28+
<h2>Resources</h2>
29+
<h3>Documentation</h3>
30+
<ul>
31+
<li> Numpy documentation - <a href="https://numpy.org/doc/stable/reference/arrays.datetime.html#business-day-functionality">Business Day
32+
Functionality</a> </li>
33+
<li> Numpy documentation - <a href="https://numpy.org/doc/stable/reference/arrays.datetime.html#custom-weekmasks">Custom Weekmasks</a> </li>
34+
</ul>
35+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "Numpy weekmask should have a valid value",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [],
10+
"defaultSeverity": "Major",
11+
"ruleSpecification": "RSPEC-6900",
12+
"sqKey": "S6900",
13+
"scope": "All",
14+
"quickfix": "unknown",
15+
"code": {
16+
"impacts": {
17+
"MAINTAINABILITY": "HIGH",
18+
"RELIABILITY": "MEDIUM",
19+
"SECURITY": "LOW"
20+
},
21+
"attribute": "CONVENTIONAL"
22+
}
23+
}

python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@
230230
"S6887",
231231
"S6890",
232232
"S6894",
233+
"S6900",
233234
"S6903"
234235
]
235236
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2024 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;
21+
22+
import org.junit.jupiter.api.Test;
23+
import org.sonar.python.checks.utils.PythonCheckVerifier;
24+
25+
class NumpyWeekMaskValidationCheckTest {
26+
@Test
27+
void test() {
28+
PythonCheckVerifier.verify("src/test/resources/checks/numpyWeekMaskValidation.py", new NumpyWeekMaskValidationCheck());
29+
}
30+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import numpy as np
2+
from something import some_function as my_function
3+
def unrelated():
4+
unkwown_function()
5+
np.busday_offset('2012-05' , 1, roll='forward')
6+
def other_function(): ...
7+
other_function()
8+
np.busday_offset('2012-05' , 1, roll='forward', weekmask=other_function())
9+
my_function()
10+
11+
def some_cases():
12+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask='0111100')
13+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask=[0, 1, 1, 1, 1, 0, 0])
14+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask="Tue Wed Thu Fri")
15+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask="Tue Wed Thu Fri Fri")
16+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask="TueWedThuFriFri")
17+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask="TueWedThuFri Fri")
18+
offset = np.busday_offset('2012-05', 1, weekmask="TueWed ThuFri Fri")
19+
offset = np.busday_offset('2012-05', 1, 'forward', "TueWed ThuFri Fri")
20+
21+
offset = np.busday_offset('2012-05', 1, 'forward', "TueWed ThuFri igpifdjpigdg") # Noncompliant {{String must be either 7 characters long and contain only 0 and 1, or contain abbreviated weekdays.}}
22+
#^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask='01') # Noncompliant {{String must be either 7 characters long and contain only 0 and 1, or contain abbreviated weekdays.}}
24+
#^^^^
25+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask="fsfdiopj") # Noncompliant {{String must be either 7 characters long and contain only 0 and 1, or contain abbreviated weekdays.}}
26+
#^^^^^^^^^^
27+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask=[1,1,1,1,1,1,1,1]) # Noncompliant {{Array must have 7 elements, all of which are 0 or 1.}}
28+
#^^^^^^^^^^^^^^^^^
29+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask=[1,1]) # Noncompliant
30+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask=["a", "b", "c"]) # Noncompliant {{Array must have 7 elements, all of which are 0 or 1.}}
31+
#^^^^^^^^^^^^^^^
32+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask=[1,1,1,"1",1,1,1]) # Noncompliant {{Array must have 7 elements, all of which are 0 or 1.}}
33+
#^^^^^^^^^^^^^^^^^
34+
offset = np.busday_offset('2012-05', 1, roll='forward', weekmask=[1,1,0,"0",1,1,1]) # Noncompliant {{Array must have 7 elements, all of which are 0 or 1.}}
35+
#^^^^^^^^^^^^^^^^^
36+
def with_assigned_values():
37+
weekmask1 = "Tue Wed Thu Fri"
38+
offset1 = np.busday_offset('2012-05', 1, roll='forward', weekmask=weekmask1)
39+
40+
weekmask2 = "0111100"
41+
offset2 = np.busday_offset('2012-05', 1, roll='forward', weekmask=weekmask2)
42+
43+
weekmask3 = [0, 1, 1, 1, 1, 0, 0]
44+
offset3 = np.busday_offset('2012-05', 1, roll='forward', weekmask=weekmask3)
45+
46+
weekmask4 = "sdfgsdfgsdg"
47+
offset4 = np.busday_offset('2012-05', 1, roll='forward', weekmask=weekmask4) # Noncompliant
48+
49+
weekmask5 = [1,1,1,1,1,1,1,1]
50+
#^^^^^^^^^^^^^^^^^> 1 {{Invalid mask is created here.}}
51+
offset5 = np.busday_offset('2012-05', 1, roll='forward', weekmask=weekmask5) # Noncompliant {{Array must have 7 elements, all of which are 0 or 1.}}
52+
#^^^^^^^^^
53+
weekmask6 = [1,1]
54+
offset6 = np.busday_offset('2012-05', 1, roll='forward', weekmask=weekmask6) # Noncompliant
55+
56+
weekmask7 = ["a", "b", "c"]
57+
offset7 = np.busday_offset('2012-05', 1, roll='forward', weekmask=weekmask7) # Noncompliant
58+
59+
weekmask8 = "01"
60+
#^^^^> 1 {{Invalid mask is created here.}}
61+
offset8 = np.busday_offset('2012-05', 1, roll='forward', weekmask=weekmask8) # Noncompliant {{String must be either 7 characters long and contain only 0 and 1, or contain abbreviated weekdays.}}
62+
#^^^^^^^^^
63+
weekmask9 = ("TueWed")
64+
offset9 = np.busday_offset('2012-05', 1, roll='forward', weekmask=weekmask9)
65+
66+
weekmask10 = [1,1,1,1,1,1,2]
67+
offset10 = np.busday_offset('2012-05', 1, roll='forward', weekmask=weekmask10) # Noncompliant {{Array must have 7 elements, all of which are 0 or 1.}}
68+
69+
def busday_count():
70+
offset = np.busday_count('2012-05', '2012-06', weekmask="TueWed ThuFri Fri")
71+
offset = np.busday_count('2012-05', '2012-06', weekmask="TueWed ThuFri igpifdjpigdg") # Noncompliant
72+
offset = np.busday_count('2012-05', '2012-06', weekmask='01') # Noncompliant
73+
offset = np.busday_count('2012-05', '2012-06', weekmask="fsfdiopj") # Noncompliant
74+
offset = np.busday_count('2012-05', '2012-06', weekmask=[1,1,1,1,1,1,1,1]) # Noncompliant
75+
offset = np.busday_count('2012-05', '2012-06', weekmask=[1,1,1,1,1,1,1])
76+
offset = np.busday_count('2012-05', '2012-06', weekmask=[1,1]) # Noncompliant
77+
offset = np.busday_count('2012-05', '2012-06', weekmask=["a", "b", "c"]) # Noncompliant
78+
offset = np.busday_count('2012-05', '2012-06', weekmask=[1,1,1,1,1,1,2]) # Noncompliant
79+
offset = np.busday_count('2012-05', '2012-06', weekmask=[1,1,1,1,0,1,1,1,1]) # Noncompliant
80+
offset = np.busday_count('2012-05', '2012-06', weekmask="Tue Wed Thu Fri")
81+
offset = np.busday_count('2012-05', '2012-06', weekmask="Tue Wed Thu Fri Fri")
82+
offset = np.busday_count('2012-05', '2012-06', weekmask="TueWedThuFriFri", holidays=["2012-05-01", "2012-05-02"])
83+
84+
def is_busday():
85+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask="TueWed ThuFri Fri")
86+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask="TueWed ThuFri igpifdjpigdg") # Noncompliant
87+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask='01') # Noncompliant
88+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask="fsfdiopj") # Noncompliant
89+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask=[1,1,1,1,1,1,1,1]) # Noncompliant
90+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask=[1,1,1,0,1,1,1])
91+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask=[1,1]) # Noncompliant
92+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask=["a", "b", "c"]) # Noncompliant
93+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask=[1,1,1,1,1,1,2]) # Noncompliant
94+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask=[1,1,1,1,0,1,1,1,1]) # Noncompliant
95+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask="Tue Wed Thu Fri")
96+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask="Tue Wed Thu Fri Fri")
97+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], weekmask="TueWedThuFriFri")
98+
bools = np.is_busday(['2012-05', '2012-06', '2012-07'], holidays=["2012-05-01", "2012-05-02"], weekmask="TueWedThuFriFri")
99+
100+
def busdaycalendar():
101+
np.busdaycalendar(weekmask="TueWed ThuFri Fri")
102+
np.busdaycalendar(weekmask="TueWed ThuFri igpifdjpigdg") # Noncompliant
103+
np.busdaycalendar(weekmask='01') # Noncompliant
104+
np.busdaycalendar(weekmask="fsfdiopj") # Noncompliant
105+
np.busdaycalendar(weekmask=[1,1,1,1,1,1,1,1]) # Noncompliant
106+
np.busdaycalendar(weekmask=[1,1,1,0,1,1,1])
107+
np.busdaycalendar(weekmask=[1,1]) # Noncompliant
108+
np.busdaycalendar(weekmask=["a", "b", "c"]) # Noncompliant
109+
np.busdaycalendar(weekmask=[1,1,1,1,1,1,2]) # Noncompliant
110+
np.busdaycalendar(weekmask=[1,1,1,1,0,1,1,1,1]) # Noncompliant
111+
np.busdaycalendar(weekmask="Tue Wed Thu Fri")
112+
np.busdaycalendar(weekmask="Tue Wed Thu Fri Fri", holidays=["2012-05-01", "2012-05-02"])
113+
np.busdaycalendar(holidays=["2012-05-01", "2012-05-02"], weekmask="Tue Wed Thu Fri Fri")
114+
115+
np.busdaycalendar(weekmask="Sun TueWed \x0b")
116+
np.busdaycalendar(weekmask="Sun TueWed \x0b", holidays=["2012-05-01", "2012-05-02"])
117+
np.busdaycalendar(weekmask="Sun TueWed \x0b \t \t \n\r \x0c Fri")

0 commit comments

Comments
 (0)