Skip to content

Commit 79e4649

Browse files
authored
SONARPY-1639 S6882: Constructor attributes of date and time objects should be in the range of possible values (#1734)
1 parent da210e2 commit 79e4649

File tree

7 files changed

+328
-0
lines changed

7 files changed

+328
-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
@@ -301,6 +301,7 @@ public static Iterable<Class> getChecks() {
301301
PrivilegePolicyCheck.class,
302302
ProcessSignallingCheck.class,
303303
PropertyAccessorParameterCountCheck.class,
304+
IncorrectParameterDatetimeConstructorsCheck.class,
304305
PseudoRandomCheck.class,
305306
PublicApiIsSecuritySensitiveCheck.class,
306307
PubliclyWritableDirectoriesCheck.class,
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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.sonar.check.Rule;
23+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
24+
import org.sonar.plugins.python.api.SubscriptionContext;
25+
import org.sonar.plugins.python.api.symbols.Symbol;
26+
import org.sonar.plugins.python.api.tree.CallExpression;
27+
import org.sonar.plugins.python.api.tree.Expression;
28+
import org.sonar.plugins.python.api.tree.Name;
29+
import org.sonar.plugins.python.api.tree.NumericLiteral;
30+
import org.sonar.plugins.python.api.tree.RegularArgument;
31+
import org.sonar.plugins.python.api.tree.Tree;
32+
import org.sonar.plugins.python.api.tree.UnaryExpression;
33+
import org.sonar.python.checks.utils.Expressions;
34+
import org.sonar.python.tree.TreeUtils;
35+
36+
@Rule(key = "S6882")
37+
public class IncorrectParameterDatetimeConstructorsCheck extends PythonSubscriptionCheck {
38+
private static final int MIN_YEAR = 1;
39+
private static final int MAX_YEAR = 9999;
40+
private static final String MESSAGE = "Provide a correct value for the `%s` parameter.";
41+
private static final String MESSAGE_SECONDARY_LOCATION = "An invalid value is assigned here.";
42+
43+
@Override
44+
public void initialize(Context context) {
45+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, IncorrectParameterDatetimeConstructorsCheck::checkCallExpr);
46+
}
47+
48+
private static void checkCallExpr(SubscriptionContext context) {
49+
CallExpression callExpression = (CallExpression) context.syntaxNode();
50+
Symbol calleeSymbol = callExpression.calleeSymbol();
51+
if (calleeSymbol == null) {
52+
return;
53+
}
54+
if ("datetime.date".equals(calleeSymbol.fullyQualifiedName())) {
55+
checkDate(context, callExpression);
56+
} else if ("datetime.time".equals(calleeSymbol.fullyQualifiedName())) {
57+
checkTime(context, callExpression);
58+
} else if ("datetime.datetime".equals(calleeSymbol.fullyQualifiedName())) {
59+
checkDate(context, callExpression);
60+
checkTime(context, callExpression, 3);
61+
}
62+
}
63+
64+
private static void checkTime(SubscriptionContext context, CallExpression callExpression) {
65+
checkTime(context, callExpression, 0);
66+
}
67+
68+
private static void checkTime(SubscriptionContext context, CallExpression callExpression, int parameterOffset) {
69+
RegularArgument hourArgument = TreeUtils.nthArgumentOrKeyword(parameterOffset, "hour", callExpression.arguments());
70+
RegularArgument minuteArgument = TreeUtils.nthArgumentOrKeyword(parameterOffset + 1, "minute", callExpression.arguments());
71+
RegularArgument secondArgument = TreeUtils.nthArgumentOrKeyword(parameterOffset + 2, "second", callExpression.arguments());
72+
RegularArgument microsecondArgument = TreeUtils.nthArgumentOrKeyword(parameterOffset + 3, "microsecond", callExpression.arguments());
73+
74+
if (hourArgument != null) {
75+
checkArgument(context, hourArgument, 0, 23, "hour");
76+
}
77+
if (minuteArgument != null) {
78+
checkArgument(context, minuteArgument, 0, 59, "minute");
79+
}
80+
if (secondArgument != null) {
81+
checkArgument(context, secondArgument, 0, 59, "second");
82+
}
83+
if (microsecondArgument != null) {
84+
checkArgument(context, microsecondArgument, 0, 999_999, "microsecond");
85+
}
86+
}
87+
88+
private static class ValueWithExpression {
89+
private final long value;
90+
private final Tree expression;
91+
92+
public ValueWithExpression(long value, Tree expression) {
93+
this.value = value;
94+
this.expression = expression;
95+
}
96+
97+
public long value() {
98+
return value;
99+
}
100+
101+
public Tree expression() {
102+
return expression;
103+
}
104+
}
105+
106+
private static ValueWithExpression getValue(Expression expression) {
107+
if (expression.is(Tree.Kind.NUMERIC_LITERAL)) {
108+
return new ValueWithExpression(((NumericLiteral) expression).valueAsLong(), expression);
109+
} else if (expression.is(Tree.Kind.UNARY_MINUS)) {
110+
UnaryExpression unaryExpression = (UnaryExpression) expression;
111+
if (!unaryExpression.expression().is(Tree.Kind.NUMERIC_LITERAL)) {
112+
return null;
113+
}
114+
return new ValueWithExpression(-((NumericLiteral) unaryExpression.expression()).valueAsLong(), unaryExpression);
115+
} else if (expression.is(Tree.Kind.NAME)) {
116+
return Expressions.singleAssignedNonNameValue((Name) expression).map(IncorrectParameterDatetimeConstructorsCheck::getValue).orElse(null);
117+
}
118+
return null;
119+
}
120+
121+
private static void checkArgument(SubscriptionContext context, RegularArgument argument, long min, long max, String name) {
122+
ValueWithExpression valueWithExpression = getValue(argument.expression());
123+
if (valueWithExpression == null) {
124+
return;
125+
}
126+
long value = valueWithExpression.value();
127+
Tree secondaryLocation = argument.expression() == valueWithExpression.expression() ? null : valueWithExpression.expression();
128+
if (value < min || value > max) {
129+
PreciseIssue issue = context.addIssue(argument, String.format(MESSAGE, name));
130+
if (secondaryLocation != null) {
131+
issue.secondary(secondaryLocation, MESSAGE_SECONDARY_LOCATION);
132+
}
133+
}
134+
}
135+
136+
private static void checkDate(SubscriptionContext context, CallExpression callExpression) {
137+
RegularArgument yearArgument = TreeUtils.nthArgumentOrKeyword(0, "year", callExpression.arguments());
138+
RegularArgument monthArgument = TreeUtils.nthArgumentOrKeyword(1, "month", callExpression.arguments());
139+
RegularArgument dayArgument = TreeUtils.nthArgumentOrKeyword(2, "day", callExpression.arguments());
140+
141+
if (yearArgument == null || monthArgument == null || dayArgument == null) {
142+
return;
143+
}
144+
145+
checkArgument(context, yearArgument, MIN_YEAR, MAX_YEAR, "year");
146+
checkArgument(context, monthArgument, 1, 12, "month");
147+
checkArgument(context, dayArgument, 1, 31, "day");
148+
}
149+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<p>This rule raises an issue when an incorrect value is set as an attribute of <code>datetime.date</code>, <code>datetime.time</code>, or
2+
<code>datetime.datetime</code></p>
3+
<h2>Why is this an issue?</h2>
4+
<p>Setting a date attribute value with a value which is out of the range of possible values will lead to a <code>ValueError</code>.</p>
5+
<h2>How to fix it</h2>
6+
<p>Set attribute values with values that are within the range of possible values.</p>
7+
<h3>Code examples</h3>
8+
<h4>Noncompliant code example</h4>
9+
<pre data-diff-id="1" data-diff-type="noncompliant">
10+
def foo():
11+
dt = datetime(year=2024, day=66, month=1, hour=16, minute=1) # ValueError: day is out of range for month
12+
</pre>
13+
<h4>Compliant solution</h4>
14+
<pre data-diff-id="1" data-diff-type="compliant">
15+
def foo():
16+
dt = datetime(year=2024, day=1, month=1, hour=16, minute=1)
17+
</pre>
18+
<h2>Resources</h2>
19+
<h3>Documentation</h3>
20+
<ul>
21+
<li> Python documentation - <a href="https://docs.python.org/3/library/datetime.html#">datetime</a> </li>
22+
</ul>
23+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "Constructor attributes of date and time objects should be in the range of possible values",
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-6882",
12+
"sqKey": "S6882",
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
@@ -225,6 +225,7 @@
225225
"S6794",
226226
"S6796",
227227
"S6799",
228+
"S6882",
228229
"S6887",
229230
"S6903",
230231
"S6894"
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 IncorrectParameterDatetimeConstructorsCheckTest {
26+
@Test
27+
void test() {
28+
PythonCheckVerifier.verify("src/test/resources/checks/incorrectParameterDatetimeConstructorsCheck.py", new IncorrectParameterDatetimeConstructorsCheck());
29+
}
30+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import datetime
2+
3+
def compliant_examples():
4+
datetime.time(12, 30, 0, 0)
5+
datetime.date(2024, 3, 1)
6+
datetime.datetime(2024, 3, 1, 12, 30, 0, 0)
7+
8+
def non_compliant_examples():
9+
datetime.date(2024, 3, 32) # Noncompliant {{Provide a correct value for the `day` parameter.}}
10+
#^^
11+
datetime.date(2024, 13, 1) # Noncompliant {{Provide a correct value for the `month` parameter.}}
12+
#^^
13+
datetime.date(2024, 0, 1) # Noncompliant
14+
datetime.date(2024, 3, 0) # Noncompliant
15+
datetime.date(-5, 3, 1) # Noncompliant {{Provide a correct value for the `year` parameter.}}
16+
#^^
17+
datetime.date(100000, 5, 3) # Noncompliant
18+
19+
datetime.time(-5, 30, 0, 0) # Noncompliant {{Provide a correct value for the `hour` parameter.}}
20+
#^^
21+
datetime.time(12, 60, 0, 0) # Noncompliant {{Provide a correct value for the `minute` parameter.}}
22+
#^^
23+
datetime.time(12, 30, 60, 0) # Noncompliant {{Provide a correct value for the `second` parameter.}}
24+
#^^
25+
datetime.time(12, 30, 0, -1) # Noncompliant {{Provide a correct value for the `microsecond` parameter.}}
26+
#^^
27+
datetime.time(12, 30, 0, 1000000) # Noncompliant
28+
datetime.time(28) # Noncompliant
29+
datetime.time(5, 61) # Noncompliant
30+
datetime.time(5, 5, 61) # Noncompliant
31+
32+
datetime.datetime(2024, 3, 1, 12, 30, -2, 1) # Noncompliant {{Provide a correct value for the `second` parameter.}}
33+
#^^
34+
datetime.datetime(2024, 3, 1, 12, 30, 0, -1) # Noncompliant {{Provide a correct value for the `microsecond` parameter.}}
35+
#^^
36+
datetime.datetime(2024, 3, 1, 12, 30, 0, 1000000) # Noncompliant
37+
datetime.datetime(2024, 3, 32, 12, 30, 0, 0) # Noncompliant {{Provide a correct value for the `day` parameter.}}
38+
#^^
39+
datetime.datetime(2024, 13, 1, 12, 30, 0, 0) # Noncompliant {{Provide a correct value for the `month` parameter.}}
40+
#^^
41+
datetime.datetime(2024, 0, 1, 12, 30, 0, 0) # Noncompliant
42+
datetime.datetime(-5, 3, 1, 12, 30, 0, 0) # Noncompliant {{Provide a correct value for the `year` parameter.}}
43+
#^^
44+
datetime.datetime(2024, 3, 1, 26, 30, 0, 4) # Noncompliant {{Provide a correct value for the `hour` parameter.}}
45+
#^^
46+
datetime.datetime(100000, 5, 3, 12, 30, 0, 0) # Noncompliant
47+
datetime.datetime(2024, 3, 1, 12, 60, 0, 0) # Noncompliant
48+
datetime.datetime(2024, 3, 1, 12, 30, 60, 0) # Noncompliant
49+
datetime.datetime(2024, 3, 1, 12, 30, 0, -1) # Noncompliant
50+
51+
def assigned_value():
52+
y1 = 2024
53+
m1 = 3
54+
d1 = -2
55+
#^^> 1 {{An invalid value is assigned here.}}
56+
datetime.date(y1, m1, d1) # Noncompliant {{Provide a correct value for the `day` parameter.}}
57+
#^^
58+
if cond():
59+
y2 = -1
60+
else:
61+
y2 = 2024
62+
datetime.date(y2, 3, 1) # FN limitation of singleAssignedValue
63+
def false_negatives_tuple_unpacking():
64+
datetime.date(*(2024, -1, -1))
65+
66+
def different_imported_name():
67+
import datetime as dt
68+
dt.date(2024, 3, 32) # Noncompliant
69+
dt.time(12, 60, 0, 0) # Noncompliant
70+
dt.datetime(2024, 3, 32, 12, 30, 0, 0) # Noncompliant
71+
72+
from datetime import date as d
73+
from datetime import time as t
74+
from datetime import datetime as dtt
75+
d(2024, 3, 32) # Noncompliant
76+
t(12, 60, 0, 0) # Noncompliant
77+
dtt(2024, 3, 32, 12, 30, 0, 0) # Noncompliant
78+
79+
def no_issue_on_syntax_errors():
80+
datetime.time(12, 5, True)
81+
datetime.date(2024, True, 5)
82+
datetime.date()
83+
datetime.time()
84+
datetime.datetime()
85+
datetime.date(19)
86+
datetime.date(19, 5)
87+
datetime.time()
88+
89+
datetime.datetime(2024, 3, 1, 12, 30, 0, True)
90+
datetime.datetime(2024, 3, 1, 12, 30, True, 0)
91+
datetime.datetime(2024, 3, 1, 12, True, 0, 0)
92+
datetime.datetime(2024, 3, 1, True, 30, 0, 0)
93+
datetime.datetime(2024, 3, True, 12, 30, 0, 0)
94+
datetime.datetime(2024, True, 1, 12, 30, 0, 0)
95+
datetime.datetime(True, 3, 1, 12, 30, 0, 0)
96+
97+
datetime.time(-random(), 12, 5, 0)
98+
datetime.time(12 + 65, 12, 5, 0)
99+
100+
def some_function(): ...
101+
some_function()

0 commit comments

Comments
 (0)