Skip to content

Commit 0112353

Browse files
GCI103 DictionaryItemsUnused update: add integrations/real tests
Co-authored-by: DataLabGroupe-CreditAgricole <[email protected]>
2 parents 7cccac6 + d032dbe commit 0112353

File tree

15 files changed

+411
-4
lines changed

15 files changed

+411
-4
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- Add rule GCI 103 Dictionary Items Unused. A rule specifying that dictionary iteration should consider the pertinence of the element used.
12+
- [#76](https://github.com/green-code-initiative/creedengo-python/pull/76) Add rule GCI 103 Dictionary Items Unused. A rule specifying that dictionary iteration should consider the pertinence of the element used.
13+
- [#71](https://github.com/green-code-initiative/creedengo-python/pull/71) Add rule GCI96 Require Usecols Argument in Pandas Read Functions
14+
- [#72](https://github.com/green-code-initiative/creedengo-python/pull/72) Add rule GCI97 Optimize square computation (scalar vs vectorized method)
1315

1416
### Changed
1517

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
<lombok.version>1.18.38</lombok.version>
7171

7272
<!-- temporary version waiting for a real automatic release in creedengo repository -->
73-
<creedengo-rules-specifications.version>2.2.2</creedengo-rules-specifications.version>
73+
<creedengo-rules-specifications.version>2.4.0</creedengo-rules-specifications.version>
7474

7575
<!-- URL of the Maven repository where sonarqube will be downloaded -->
7676
<test-it.orchestrator.artifactory.url>https://repo1.maven.org/maven2</test-it.orchestrator.artifactory.url>

src/it/java/org/greencodeinitiative/creedengo/python/integration/tests/GCIRulesIT.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,4 +273,48 @@ void testGCI203_compliant() {
273273

274274
}
275275

276+
@Test
277+
void testGCI96() {
278+
String filePath = "src/pandasRequireUsecols.py";
279+
String ruleId = "creedengo-python:GCI96";
280+
String ruleMsg = "Specify 'usecols' or 'columns' when reading a DataFrame using Pandas to load only necessary columns";
281+
int[] startLines = new int[]{
282+
3, 4, 5, 6, 7, 16, 19
283+
};
284+
int[] endLines = new int[]{
285+
3, 4, 5, 6, 7, 16, 19
286+
};
287+
288+
checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_10MIN);
289+
}
290+
void testGCI97(){
291+
String filePath = "src/optimizeSquareComputation.py";
292+
String ruleId = "creedengo-python:GCI97";
293+
String ruleMsg = "Use x*x instead of x**2 or math.pow(x,2) to calculate the square of a value";
294+
int[] startLines = new int[]{
295+
4, 7, 19, 20, 25, 26, 31, 38
296+
};
297+
int[] endLines = new int[]{
298+
4, 7, 19, 20, 25, 26, 31, 38
299+
};
300+
301+
checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_1MIN);
302+
}
303+
304+
void testGCI103(){
305+
306+
String filePath = "src/dictionaryItemsUnused.py";
307+
String ruleId = "creedengo-python:GCI103";
308+
String ruleMsg = "Use dict.keys() or dict.values() instead of dict.items() when only one part of the key-value pair is used";
309+
int[] startLines = new int[]{
310+
5, 8, 12, 32, 35, 44
311+
};
312+
int[] endLines = new int[]{
313+
5, 8, 12, 32, 35, 44
314+
};
315+
316+
checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_1MIN);
317+
318+
}
319+
276320
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import math
2+
3+
x = 5
4+
result1 = x**2 # Noncompliant {{Use x*x instead of x**2 or math.pow(x,2) to calculate the square of a value}}
5+
6+
z = 7
7+
result4 = math.pow(z, 2) # Noncompliant {{Use x*x instead of x**2 or math.pow(x,2) to calculate the square of a value}}
8+
9+
a = 3
10+
result5 = a*a
11+
12+
b = 4
13+
result6 = b*3
14+
result7 = 5*b
15+
result8 = math.pow(b, 3)
16+
result9 = b**3
17+
18+
c = 2.5
19+
result10 = c**2 # Noncompliant {{Use x*x instead of x**2 or math.pow(x,2) to calculate the square of a value}}
20+
result11 = math.pow(c, 2) # Noncompliant {{Use x*x instead of x**2 or math.pow(x,2) to calculate the square of a value}}
21+
22+
23+
d = 8
24+
e = 9
25+
result12 = math.pow(d+e, 2) # Noncompliant {{Use x*x instead of x**2 or math.pow(x,2) to calculate the square of a value}}
26+
result13 = (d+e)**2 # Noncompliant {{Use x*x instead of x**2 or math.pow(x,2) to calculate the square of a value}}
27+
result14 = (d+e)*(d+e)
28+
29+
30+
def square(x):
31+
return x**2 # Noncompliant {{Use x*x instead of x**2 or math.pow(x,2) to calculate the square of a value}}
32+
33+
def better_square(x):
34+
return x*x
35+
36+
37+
import math as m
38+
result15 = m.pow(d, 2) # Noncompliant {{Use x*x instead of x**2 or math.pow(x,2) to calculate the square of a value}}
39+
40+
result16 = math.sqrt(d)
41+
result17 = math.sin(d)
42+
result18 = math.pow(d, 1.5)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import pandas as pd
2+
3+
df1 = pd.read_csv('data.csv') # Noncompliant {{Specify 'usecols' or 'columns' when reading a DataFrame using Pandas to load only necessary columns}}
4+
df2 = pd.read_parquet('data.parquet') # Noncompliant {{Specify 'usecols' or 'columns' when reading a DataFrame using Pandas to load only necessary columns}}
5+
df3 = pd.read_excel('data.xlsx') # Noncompliant {{Specify 'usecols' or 'columns' when reading a DataFrame using Pandas to load only necessary columns}}
6+
df4 = pd.read_json('data.json') # Noncompliant {{Specify 'usecols' or 'columns' when reading a DataFrame using Pandas to load only necessary columns}}
7+
df5 = pd.read_feather('data.feather') # Noncompliant {{Specify 'usecols' or 'columns' when reading a DataFrame using Pandas to load only necessary columns}}
8+
9+
df7 = pd.read_csv('data.csv', usecols=['col1', 'col2'])
10+
df8 = pd.read_parquet('data.parquet', columns=['col1', 'col2'])
11+
df9 = pd.read_excel('data.xlsx', usecols=[0, 1, 2])
12+
df10 = pd.read_json('data.json', columns=['col1', 'col2'])
13+
df11 = pd.read_feather('data.feather', columns=['col1', 'col2'])
14+
15+
import pandas as pandas_alias
16+
df14 = pandas_alias.read_csv('data.csv') # Noncompliant {{Specify 'usecols' or 'columns' when reading a DataFrame using Pandas to load only necessary columns}}
17+
df15 = pandas_alias.read_csv('data.csv', usecols=['col1'])
18+
19+
df16 = pd.read_csv('data.csv', sep=',', header=0) # Noncompliant {{Specify 'usecols' or 'columns' when reading a DataFrame using Pandas to load only necessary columns}}
20+
df17 = pd.read_csv('data.csv', sep=',', header=0, usecols=['col1', 'col2'])
21+
22+
cols_to_use = ['col1', 'col2', 'col3']
23+
df18 = pd.read_parquet('data.parquet', columns=cols_to_use)

src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public class PythonRuleRepository implements RulesDefinition, PythonCustomRuleRe
4141
AvoidListComprehensionInIterations.class,
4242
DetectUnoptimizedImageFormat.class,
4343
AvoidMultipleIfElseStatementCheck.class,
44+
PandasRequireUsecolsArgument.class,
45+
OptimizeSquareComputation.class,
4446
DictionaryItemsUnused.class
4547
);
4648

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* creedengo - Python language - Provides rules to reduce the environmental footprint of your Python programs
3+
* Copyright © 2024 Green Code Initiative (https://green-code-initiative.org)
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
package org.greencodeinitiative.creedengo.python.checks;
19+
20+
import org.sonar.check.Rule;
21+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
22+
import org.sonar.plugins.python.api.SubscriptionContext;
23+
import org.sonar.plugins.python.api.tree.BinaryExpression;
24+
import org.sonar.plugins.python.api.tree.CallExpression;
25+
import org.sonar.plugins.python.api.tree.Expression;
26+
import org.sonar.plugins.python.api.tree.NumericLiteral;
27+
import org.sonar.plugins.python.api.tree.QualifiedExpression;
28+
import org.sonar.plugins.python.api.tree.RegularArgument;
29+
30+
import static org.sonar.plugins.python.api.tree.Tree.Kind.*;
31+
32+
@Rule(key = "GCI97")
33+
public class OptimizeSquareComputation extends PythonSubscriptionCheck {
34+
35+
public static final String DESCRIPTION = "Use x*x instead of x**2 or math.pow(x,2) to calculate the square of a value";
36+
37+
@Override
38+
public void initialize(Context context) {
39+
context.registerSyntaxNodeConsumer(CALL_EXPR, this::checkMathPowCall);
40+
context.registerSyntaxNodeConsumer(POWER, this::checkPowerOf2);
41+
}
42+
43+
private boolean isNumericLiteralWithValue(Expression expr, String value) {
44+
if (expr.is(NUMERIC_LITERAL)) {
45+
NumericLiteral numericLiteral = (NumericLiteral) expr;
46+
return value.equals(numericLiteral.valueAsString());
47+
}
48+
return false;
49+
}
50+
51+
private void checkMathPowCall(SubscriptionContext context) {
52+
CallExpression callExpression = (CallExpression) context.syntaxNode();
53+
54+
if (isMathPowCall(callExpression)) {
55+
context.addIssue(callExpression, DESCRIPTION);
56+
}
57+
}
58+
59+
private boolean isMathPowCall(CallExpression callExpression) {
60+
Expression callee = callExpression.callee();
61+
62+
if (callee.is(QUALIFIED_EXPR)) {
63+
QualifiedExpression qualifiedExpr = (QualifiedExpression) callee;
64+
String name = qualifiedExpr.name().name();
65+
66+
if ("pow".equals(name)) {
67+
68+
if (callExpression.arguments().size() >= 2) {
69+
Expression secondArg = ((RegularArgument)callExpression.arguments().get(1)).expression();
70+
return isNumericLiteralWithValue(secondArg, "2");
71+
}
72+
}
73+
}
74+
75+
return false;
76+
}
77+
78+
private void checkPowerOf2(SubscriptionContext context) {
79+
BinaryExpression power = (BinaryExpression) context.syntaxNode();
80+
81+
if (isNumericLiteralWithValue(power.rightOperand(), "2")) {
82+
context.addIssue(power, DESCRIPTION);
83+
}
84+
}
85+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* creedengo - Python language - Provides rules to reduce the environmental footprint of your Python programs
3+
* Copyright © 2024 Green Code Initiative (https://green-code-initiative.org)
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
package org.greencodeinitiative.creedengo.python.checks;
19+
20+
import java.util.Arrays;
21+
import java.util.List;
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.tree.Argument;
26+
import org.sonar.plugins.python.api.tree.CallExpression;
27+
import org.sonar.plugins.python.api.tree.Tree;
28+
import org.sonar.plugins.python.api.tree.Expression;
29+
import org.sonar.plugins.python.api.tree.QualifiedExpression;
30+
import org.sonar.plugins.python.api.tree.RegularArgument;
31+
import static org.sonar.plugins.python.api.tree.Tree.Kind.*;
32+
33+
@Rule(key = "GCI96")
34+
public class PandasRequireUsecolsArgument extends PythonSubscriptionCheck {
35+
36+
public static final String DESCRIPTION = "Specify 'usecols' or 'columns' when reading a DataFrame using Pandas to load only necessary columns";
37+
private static final List<String> READ_METHODS = Arrays.asList(
38+
"read_csv", "read_parquet", "read_excel", "read_feather", "read_json"
39+
);
40+
41+
@Override
42+
public void initialize(Context context) {
43+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::visitCallExpression);
44+
}
45+
46+
public void visitCallExpression(SubscriptionContext ctx) {
47+
CallExpression callExpression = (CallExpression) ctx.syntaxNode();
48+
Expression callee = callExpression.callee();
49+
50+
if (callee.is(Tree.Kind.QUALIFIED_EXPR)) {
51+
QualifiedExpression qualifiedExpression = (QualifiedExpression) callee;
52+
String methodName = qualifiedExpression.name().name();
53+
54+
if (READ_METHODS.contains(methodName)) {
55+
56+
if (!hasColumnsSpecified(callExpression)) {
57+
ctx.addIssue(callExpression.firstToken(), DESCRIPTION);
58+
}
59+
}
60+
}
61+
}
62+
63+
private boolean hasColumnsSpecified(CallExpression callExpression) {
64+
List<Argument> arguments = callExpression.arguments();
65+
66+
for (Argument arg : arguments) {
67+
if (arg.is(REGULAR_ARGUMENT)) {
68+
RegularArgument regularArg = (RegularArgument) arg;
69+
String paramName = regularArg.keywordArgument() != null ? regularArg.keywordArgument().name() : null;
70+
if (paramName != null && (paramName.equals("usecols") || paramName.equals("columns"))) {
71+
return true;
72+
}
73+
}
74+
}
75+
return false;
76+
}
77+
}

src/main/resources/org/greencodeinitiative/creedengo/python/creedengo_way_profile.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
"GCI72",
1111
"GCI74",
1212
"GCI89",
13+
"GCI96",
14+
"GCI97",
15+
"GCI103",
1316
"GCI203",
1417
"GCI404"
15-
]
18+
]
1619
}

src/test/java/org/greencodeinitiative/creedengo/python/checks/DictionaryItemsUnusedTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ public class DictionaryItemsUnusedTest {
2424

2525
@Test
2626
public void test() {
27-
PythonCheckVerifier.verify("src/test/resources/checks/DictionaryItemsUnused.py", new DictionaryItemsUnused());
27+
PythonCheckVerifier.verify("src/test/resources/checks/dictionaryItemsUnused.py", new DictionaryItemsUnused());
2828
}
2929
}

0 commit comments

Comments
 (0)