diff --git a/CHANGELOG.md b/CHANGELOG.md index f97f08d..944e9cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add rule GCI107 Torch from numpy, the rule isn't finished yet + ### Changed - compatibility updates for SonarQube 25.5.0 diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java b/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java index c385979..070fae2 100644 --- a/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java +++ b/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java @@ -40,7 +40,8 @@ public class PythonRuleRepository implements RulesDefinition, PythonCustomRuleRe AvoidFullSQLRequest.class, AvoidListComprehensionInIterations.class, DetectUnoptimizedImageFormat.class, - AvoidMultipleIfElseStatementCheck.class + AvoidMultipleIfElseStatementCheck.class, + UseTorchFromNumpy.class ); public static final String LANGUAGE = "py"; diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java new file mode 100644 index 0000000..2b4cc9f --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java @@ -0,0 +1,100 @@ +/* + * creedengo - Python language - Provides rules to reduce the environmental footprint of your Python programs + * Copyright © 2024 Green Code Initiative (https://green-code-initiative.org) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.greencodeinitiative.creedengo.python.checks; + +import org.sonar.check.Rule; +import org.sonar.plugins.python.api.PythonSubscriptionCheck; +import org.sonar.plugins.python.api.SubscriptionContext; +import org.sonar.plugins.python.api.tree.*; + +import java.util.HashSet; +import java.util.Set; + +import static org.sonar.plugins.python.api.tree.Tree.Kind.*; + +/** + * Rule to enforce the use of torch.from_numpy() instead of torch.tensor() when working with NumPy arrays. + * This optimization reduces memory usage and computational overhead by avoiding unnecessary data copying. + */ +@Rule(key = "GCI107") +public class UseTorchFromNumpy extends PythonSubscriptionCheck { + + public static final String DESCRIPTION = "Use torch.from_numpy() instead of torch.tensor() to create tensors from numpy arrays"; + private static final String NUMPY_ARRAY_FUNCTION = "numpy.array"; + private static final String TORCH_TENSOR_FUNCTION = "torch.tensor"; + + private final Set numpyArrayVariables = new HashSet<>(); + + @Override + public void initialize(Context context) { + context.registerSyntaxNodeConsumer(ASSIGNMENT_STMT, this::visitAssignmentStatement); + context.registerSyntaxNodeConsumer(CALL_EXPR, this::visitCallExpression); + } + + private void visitAssignmentStatement(SubscriptionContext ctx) { + var assignmentStmt = (AssignmentStatement) ctx.syntaxNode(); + var value = assignmentStmt.assignedValue(); + + if (value.is(CALL_EXPR) && isNumpyArrayCreation((CallExpression) value)) { + String variableName = Utils.getVariableName(ctx); + if (variableName != null) { + numpyArrayVariables.add(variableName); + } + } + } + + private boolean isNumpyArrayCreation(CallExpression callExpression) { + return NUMPY_ARRAY_FUNCTION.equals(Utils.getQualifiedName(callExpression)); + } + + private void visitCallExpression(SubscriptionContext ctx) { + var callExpression = (CallExpression) ctx.syntaxNode(); + + if (!TORCH_TENSOR_FUNCTION.equals(Utils.getQualifiedName(callExpression)) && !TORCH_TENSOR_FUNCTION.equals(callExpression.callee().firstToken().value()+"."+callExpression.calleeSymbol().name())) { + return; + } + + for (Argument arg : callExpression.arguments()) { + if (!arg.is(REGULAR_ARGUMENT)) { + continue; + } + + var regArg = (RegularArgument) arg; + var argumentExpression = regArg.expression(); + + // Case 1: Direct np.array call in the argument + if (argumentExpression.is(CALL_EXPR)) { + var argCallExpression = (CallExpression) argumentExpression; + if (isNumpyArrayCreation(argCallExpression)) { + ctx.addIssue(argumentExpression, DESCRIPTION); + continue; + } + } + + // Case 2: Variable reference to a previously defined numpy array + if (argumentExpression.is(NAME)) { + var name = (Name) argumentExpression; + var variableName = name.name(); + + if (numpyArrayVariables.contains(variableName)) { + ctx.addIssue(argumentExpression, DESCRIPTION); + } + } + } + } +} diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/Utils.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/Utils.java new file mode 100644 index 0000000..44f2183 --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/Utils.java @@ -0,0 +1,102 @@ +/* + * creedengo - Python language - Provides rules to reduce the environmental footprint of your Python programs + * Copyright © 2024 Green Code Initiative (https://green-code-initiative.org) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.greencodeinitiative.creedengo.python.checks; + +import org.sonar.plugins.python.api.SubscriptionContext; +import org.sonar.plugins.python.api.symbols.Symbol; +import org.sonar.plugins.python.api.tree.Argument; +import org.sonar.plugins.python.api.tree.AssignmentStatement; +import org.sonar.plugins.python.api.tree.Tree; +import org.sonar.plugins.python.api.tree.RegularArgument; +import org.sonar.plugins.python.api.tree.Name; +import org.sonar.plugins.python.api.tree.CallExpression; +import org.sonar.plugins.python.api.tree.Expression; + +import javax.annotation.CheckForNull; +import java.util.List; +import java.util.Objects; + +public class Utils { + + private static boolean hasKeyword(Argument argument, String keyword) { + if (!argument.is(new Tree.Kind[] {Tree.Kind.REGULAR_ARGUMENT})) { + return false; + } else { + Name keywordArgument = ((RegularArgument) argument).keywordArgument(); + return keywordArgument != null && keywordArgument.name().equals(keyword); + } + } + + @CheckForNull + public static RegularArgument nthArgumentOrKeyword(int argPosition, String keyword, List arguments) { + for (int i = 0; i < arguments.size(); ++i) { + Argument argument = (Argument) arguments.get(i); + if (hasKeyword(argument, keyword)) { + return (RegularArgument) argument; + } + + if (argument.is(new Tree.Kind[] {Tree.Kind.REGULAR_ARGUMENT})) { + RegularArgument regularArgument = (RegularArgument) argument; + if (regularArgument.keywordArgument() == null && argPosition == i) { + return regularArgument; + } + } + } + + return null; + } + + public static String getQualifiedName(CallExpression callExpression) { + Symbol symbol = callExpression.calleeSymbol(); + + return symbol != null && symbol.fullyQualifiedName() != null ? symbol.fullyQualifiedName() : ""; + } + + public static String getMethodName(CallExpression callExpression) { + Symbol symbol = callExpression.calleeSymbol(); + return symbol != null && symbol.name() != null ? symbol.name() : ""; + } + + public static List getArgumentsFromCall(CallExpression callExpression) { + try { + return Objects.requireNonNull(callExpression.argumentList()).arguments(); + } catch (NullPointerException e) { + return List.of(); + } + } + + public static String getVariableName(SubscriptionContext context) { + Tree node = context.syntaxNode(); + Tree current = node; + while (current != null && !current.is(Tree.Kind.ASSIGNMENT_STMT)) { + current = current.parent(); + } + if (current != null && current.is(Tree.Kind.ASSIGNMENT_STMT)) { + AssignmentStatement assignment = (AssignmentStatement) current; + if (!assignment.lhsExpressions().isEmpty() && !assignment.lhsExpressions().get(0).expressions().isEmpty()) { + Expression leftExpr = assignment.lhsExpressions().get(0).expressions().get(0); + if (leftExpr.is(Tree.Kind.NAME)) { + Name variableName = (Name) leftExpr; + return variableName.name(); + } + } + + } + return null; + } +} diff --git a/src/test/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpyTest.java b/src/test/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpyTest.java new file mode 100644 index 0000000..9cfc58b --- /dev/null +++ b/src/test/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpyTest.java @@ -0,0 +1,29 @@ +/* + * creedengo - Python language - Provides rules to reduce the environmental footprint of your Python programs + * Copyright © 2024 Green Code Initiative (https://green-code-initiative.org) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.greencodeinitiative.creedengo.python.checks; + +import org.junit.Test; +import org.sonar.python.checks.utils.PythonCheckVerifier; + +public class UseTorchFromNumpyTest { + + @Test + public void test() { + PythonCheckVerifier.verify("src/test/resources/checks/useTorchFromNumpy.py", new UseTorchFromNumpy()); + } +} diff --git a/src/test/resources/checks/useTorchFromNumpy.py b/src/test/resources/checks/useTorchFromNumpy.py new file mode 100644 index 0000000..e8dcee8 --- /dev/null +++ b/src/test/resources/checks/useTorchFromNumpy.py @@ -0,0 +1,53 @@ +import numpy as np +import torch as tt + +np_array = np.array([1, 2, 3]) + + +torch_tensor = tt.from_numpy(np_array) # Compliant + +torch = tt.tensor(np_array) # Noncompliant {{Use torch.from_numpy() instead of torch.tensor() to create tensors from numpy arrays}} +# Case 1: Standard imports +import numpy +import torch + +numpy_array = numpy.array([1, 2, 3, 4]) + +compliant1 = torch.from_numpy(numpy_array) # Compliant + +non_compliant1 = torch.tensor(numpy_array) # Noncompliant {{Use torch.from_numpy() instead of torch.tensor() to create tensors from numpy arrays}} + +# Case 2: Aliased imports +import numpy as np +import torch as tt + +compliant2 = tt.from_numpy(numpy_array) # Compliant + +non_compliant2 = tt.tensor(numpy_array) # Noncompliant {{Use torch.from_numpy() instead of torch.tensor() to create tensors from numpy arrays}} + +# Case 3: From imports +from numpy import array +from torch import tensor, from_numpy + + +compliant3 = from_numpy(numpy_array) # Compliant + +non_compliant3 = tensor(numpy_array) # Noncompliant {{Use torch.from_numpy() instead of torch.tensor() to create tensors from numpy arrays}} + +# Case 4: From imports with aliases +from numpy import array as np_arr +from torch import tensor as t_tensor, from_numpy as t_from_numpy + + +compliant4 = t_from_numpy(numpy_array) # Compliant +non_compliant4 = t_tensor(numpy_array) # Noncompliant {{Use torch.from_numpy() instead of torch.tensor() to create tensors from numpy arrays}} + +# Case 5: Direct np call as function argument +compliant5 = tt.from_numpy(np.array([1, 2, 3])) +non_compliant5 = tt.tensor(np.array([1, 2, 3])) # Noncompliant {{Use torch.from_numpy() instead of torch.tensor() to create tensors from numpy arrays}} + +# Case 6: Alias direct np call as function argument +compliant5 = t_from_numpy(np.array([1, 2, 3])) +non_compliant6 = t_tensor(np.array([1, 2, 3])) # Noncompliant {{Use torch.from_numpy() instead of torch.tensor() to create tensors from numpy arrays}} + +