Skip to content

Commit 2528979

Browse files
GCI104 AI AvoidCreatingTensorUsingNumpyOrNativePython #Python #DLG #Build
Co-authored-by: DataLabGroupe-CreditAgricole <[email protected]>
1 parent c74f723 commit 2528979

File tree

6 files changed

+248
-1
lines changed

6 files changed

+248
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Add rule GCI 104 AvoidCreatingTensorUsingNumpyOrNativePython, a rule specific to AI/ML code
13+
1214
### Changed
1315

1416
- compatibility updates for SonarQube 25.5.0

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public class PythonRuleRepository implements RulesDefinition, PythonCustomRuleRe
4040
AvoidFullSQLRequest.class,
4141
AvoidListComprehensionInIterations.class,
4242
DetectUnoptimizedImageFormat.class,
43-
AvoidMultipleIfElseStatementCheck.class
43+
AvoidMultipleIfElseStatementCheck.class,
44+
AvoidCreatingTensorUsingNumpyOrNativePython.class
4445
);
4546

4647
public static final String LANGUAGE = "py";
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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.Priority;
21+
import org.sonar.check.Rule;
22+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
23+
import org.sonar.plugins.python.api.tree.Tree;
24+
import org.sonar.plugins.python.api.tree.CallExpression;
25+
import org.sonar.plugins.python.api.tree.RegularArgument;
26+
27+
import java.util.List;
28+
import java.util.Map;
29+
30+
import static java.util.Map.entry;
31+
import static org.sonar.plugins.python.api.tree.Tree.Kind.CALL_EXPR;
32+
33+
@Rule(key = "GCI104")
34+
public class AvoidCreatingTensorUsingNumpyOrNativePython extends PythonSubscriptionCheck {
35+
36+
public static final String RULE_KEY = "P5";
37+
private static final String dataArgumentName = "data";
38+
private static final int dataArgumentPosition = 0;
39+
private static final Map<String, String> torchOtherFunctionsMapping = Map.ofEntries(
40+
entry("numpy.random.rand", "torch.rand"),
41+
entry("numpy.random.randint", "torch.randint"),
42+
entry("numpy.random.randn", "torch.randn"),
43+
entry("numpy.zeros", "torch.zeros"),
44+
entry("numpy.zeros_like", "torch.zeros_like"),
45+
entry("numpy.ones", "torch.ones"),
46+
entry("numpy.ones_like", "torch.ones_like"),
47+
entry("numpy.full", "torch.full"),
48+
entry("numpy.full_like", "torch.full_like"),
49+
entry("numpy.eye", "torch.eye"),
50+
entry("numpy.arange", "torch.arange"),
51+
entry("numpy.linspace", "torch.linspace"),
52+
entry("numpy.logspace", "torch.logspace"),
53+
entry("numpy.identity", "torch.eye"),
54+
entry("numpy.tile", "torch.tile")
55+
);
56+
private static final List<String> torchTensorConstructors = List.of(
57+
"torch.tensor", "torch.FloatTensor",
58+
"torch.DoubleTensor", "torch.HalfTensor",
59+
"torch.BFloat16Tensor", "torch.ByteTensor",
60+
"torch.CharTensor", "torch.ShortTensor",
61+
"torch.IntTensor", "torch.LongTensor",
62+
"torch.BoolTensor", "torch.cuda.FloatTensor",
63+
"torch.cuda.DoubleTensor", "torch.cuda.HalfTensor",
64+
"torch.cuda.BFloat16Tensor", "torch.cuda.ByteTensor",
65+
"torch.cuda.CharTensor", "torch.cuda.ShortTensor",
66+
"torch.cuda.IntTensor", "torch.cuda.LongTensor",
67+
"torch.cuda.BoolTensor");
68+
protected static final String MESSAGE = "Directly create tensors as torch.Tensor. Use %s instead of %s.";
69+
70+
@Override
71+
public void initialize(Context context) {
72+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, ctx -> {
73+
CallExpression callExpression = (CallExpression) ctx.syntaxNode();
74+
if (torchTensorConstructors.contains(Utils.getQualifiedName(callExpression))) {
75+
RegularArgument tensorCreatorArgument = Utils.nthArgumentOrKeyword(dataArgumentPosition, dataArgumentName, callExpression.arguments());
76+
if (tensorCreatorArgument != null) {
77+
if (tensorCreatorArgument.expression().is(CALL_EXPR)) {
78+
String functionQualifiedName = Utils.getQualifiedName((CallExpression) tensorCreatorArgument.expression());
79+
if (torchOtherFunctionsMapping.containsKey(functionQualifiedName)) {
80+
ctx.addIssue(callExpression, String.format(MESSAGE, torchOtherFunctionsMapping.get(functionQualifiedName), functionQualifiedName));
81+
}
82+
}
83+
}
84+
}
85+
});
86+
}
87+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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.plugins.python.api.SubscriptionContext;
21+
import org.sonar.plugins.python.api.symbols.Symbol;
22+
import org.sonar.plugins.python.api.tree.Argument;
23+
import org.sonar.plugins.python.api.tree.AssignmentStatement;
24+
import org.sonar.plugins.python.api.tree.Tree;
25+
import org.sonar.plugins.python.api.tree.RegularArgument;
26+
import org.sonar.plugins.python.api.tree.Name;
27+
import org.sonar.plugins.python.api.tree.CallExpression;
28+
import org.sonar.plugins.python.api.tree.Expression;
29+
30+
import javax.annotation.CheckForNull;
31+
import java.util.List;
32+
import java.util.Objects;
33+
34+
public class Utils {
35+
36+
private static boolean hasKeyword(Argument argument, String keyword) {
37+
if (!argument.is(new Tree.Kind[] {Tree.Kind.REGULAR_ARGUMENT})) {
38+
return false;
39+
} else {
40+
Name keywordArgument = ((RegularArgument) argument).keywordArgument();
41+
return keywordArgument != null && keywordArgument.name().equals(keyword);
42+
}
43+
}
44+
45+
@CheckForNull
46+
public static RegularArgument nthArgumentOrKeyword(int argPosition, String keyword, List<Argument> arguments) {
47+
for (int i = 0; i < arguments.size(); ++i) {
48+
Argument argument = (Argument) arguments.get(i);
49+
if (hasKeyword(argument, keyword)) {
50+
return (RegularArgument) argument;
51+
}
52+
53+
if (argument.is(new Tree.Kind[] {Tree.Kind.REGULAR_ARGUMENT})) {
54+
RegularArgument regularArgument = (RegularArgument) argument;
55+
if (regularArgument.keywordArgument() == null && argPosition == i) {
56+
return regularArgument;
57+
}
58+
}
59+
}
60+
61+
return null;
62+
}
63+
64+
public static String getQualifiedName(CallExpression callExpression) {
65+
Symbol symbol = callExpression.calleeSymbol();
66+
67+
return symbol != null && symbol.fullyQualifiedName() != null ? symbol.fullyQualifiedName() : "";
68+
}
69+
70+
public static String getMethodName(CallExpression callExpression) {
71+
Symbol symbol = callExpression.calleeSymbol();
72+
return symbol != null && symbol.name() != null ? symbol.name() : "";
73+
}
74+
75+
public static List<Argument> getArgumentsFromCall(CallExpression callExpression) {
76+
try {
77+
return Objects.requireNonNull(callExpression.argumentList()).arguments();
78+
} catch (NullPointerException e) {
79+
return List.of();
80+
}
81+
}
82+
83+
public static String getVariableName(SubscriptionContext context) {
84+
Tree node = context.syntaxNode();
85+
Tree current = node;
86+
while (current != null && !current.is(Tree.Kind.ASSIGNMENT_STMT)) {
87+
current = current.parent();
88+
}
89+
if (current != null && current.is(Tree.Kind.ASSIGNMENT_STMT)) {
90+
AssignmentStatement assignment = (AssignmentStatement) current;
91+
if (!assignment.lhsExpressions().isEmpty() && !assignment.lhsExpressions().get(0).expressions().isEmpty()) {
92+
Expression leftExpr = assignment.lhsExpressions().get(0).expressions().get(0);
93+
if (leftExpr.is(Tree.Kind.NAME)) {
94+
Name variableName = (Name) leftExpr;
95+
return variableName.name();
96+
}
97+
}
98+
99+
}
100+
return null;
101+
}
102+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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.junit.Test;
21+
import org.sonar.python.checks.utils.PythonCheckVerifier;
22+
23+
24+
25+
public class AvoidCreatingTensorUsingNumpyOrNativePythonTest {
26+
@Test
27+
public void test() {
28+
PythonCheckVerifier.verify("src/test/resources/checks/avoidCreatingTensorUsingNumpyOrNativePython.py", new AvoidCreatingTensorUsingNumpyOrNativePython());
29+
}
30+
31+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import numpy as np
2+
import torch
3+
4+
def non_compliant_random_rand():
5+
tensor = torch.tensor(np.random.rand(1000, 1000)) # Noncompliant {{Directly create tensors as torch.Tensor. Use torch.rand instead of numpy.random.rand.}}
6+
7+
def compliant_random_rand():
8+
tensor = torch.rand([1000, 1000])
9+
10+
def compliant_zeros():
11+
tensor_ = torch.zeros(1, 2)
12+
print(tensor_)
13+
14+
def non_compliant_zeros():
15+
tensor_ = torch.IntTensor(np.zeros(1, 2)) # Noncompliant {{Directly create tensors as torch.Tensor. Use torch.zeros instead of numpy.zeros.}}
16+
print(tensor_)
17+
18+
def non_compliant_eye():
19+
tensor = torch.cuda.LongTensor(np.eye(5)) # Noncompliant {{Directly create tensors as torch.Tensor. Use torch.eye instead of numpy.eye.}}
20+
21+
def non_compliant_ones():
22+
import numpy
23+
from torch import FloatTensor
24+
tensor = FloatTensor(data=np.ones(shape=(1, 5))) # Noncompliant {{Directly create tensors as torch.Tensor. Use torch.ones instead of numpy.ones.}}

0 commit comments

Comments
 (0)