From f66c9f7a6ce2d3d00b8ce2b309eb2e08ef308222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ophas=20Fournier?= Date: Tue, 20 May 2025 15:42:48 +0200 Subject: [PATCH 1/6] GCI107 UseTorchFromNumpy #Python #DLG #Build Co-authored-by: DataLabGroupe-CreditAgricole --- CHANGELOG.md | 2 + .../python/PythonRuleRepository.java | 3 +- .../python/checks/UseTorchFromNumpy.java | 96 +++++++++++++++++ .../creedengo/python/checks/Utils.java | 102 ++++++++++++++++++ .../python/checks/UseTorchFromNumpyTest.java | 29 +++++ .../resources/checks/useTorchFromNumpy.py | 9 ++ 6 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java create mode 100644 src/main/java/org/greencodeinitiative/creedengo/python/checks/Utils.java create mode 100644 src/test/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpyTest.java create mode 100644 src/test/resources/checks/useTorchFromNumpy.py diff --git a/CHANGELOG.md b/CHANGELOG.md index bea9b72..aad1048 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..5007cd9 --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java @@ -0,0 +1,96 @@ +/* + * 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.CallExpression; +import org.sonar.plugins.python.api.tree.Expression; +import org.sonar.plugins.python.api.tree.StringLiteral; +import org.sonar.plugins.python.api.tree.Tree; +import org.sonar.plugins.python.api.tree.QualifiedExpression; +import org.sonar.plugins.python.api.tree.Argument; +import org.sonar.plugins.python.api.tree.AssignmentStatement; +import org.sonar.plugins.python.api.tree.RegularArgument; + +import java.util.ArrayList; +import java.util.List; + +import static org.sonar.plugins.python.api.tree.Tree.Kind.*; +@Rule(key = "GCI314") +public class UseTorchFromNumpy extends PythonSubscriptionCheck { + + private final List numpyArrayList = new ArrayList<>(); + + public static final String DESCRIPTION = "Use torch.from_numpy() instead of torch.tensor() to create tensors from numpy arrays"; + + @Override + public void initialize(Context context) { + context.registerSyntaxNodeConsumer(ASSIGNMENT_STMT, this::visitAssiStmt); + context.registerSyntaxNodeConsumer(CALL_EXPR, this::visitCallExpression); + } + + private void visitAssiStmt(SubscriptionContext ctx) { + AssignmentStatement assignmentStmt = (AssignmentStatement) ctx.syntaxNode(); + Expression value = assignmentStmt.assignedValue(); + + if (value.is(CALL_EXPR)) { + if (checkNumpyCallExpression((CallExpression) value)) { + String variableName = Utils.getVariableName(ctx); + System.out.println(("added one variable: " + variableName)); + if (variableName != null) { + numpyArrayList.add(variableName); + } + } + } + } + + private boolean checkNumpyCallExpression(CallExpression callExpression) { + Expression callee = callExpression.callee(); + + if (callee.is(QUALIFIED_EXPR)) { + return Utils.getQualifiedName(callExpression).equals("numpy.array"); + } + return false; + } + + private void visitCallExpression(SubscriptionContext ctx) { + CallExpression callExpression = (CallExpression) ctx.syntaxNode(); + // System.out.println("visiting call expression at line"+ callExpression.firstToken().line()); + // System.out.println("Utils"+ Utils.getQualifiedName(callExpression)); + // System.out.println("callExpression"+ callExpression.); + // System.out.println((callExpression)); + + if (Utils.getQualifiedName(callExpression).equals("torch.tensor")) { + System.out.println(("detected torch.tensor() call at line: " + callExpression.firstToken().line())); + for (Argument arg : callExpression.arguments()) { + if (arg.is(REGULAR_ARGUMENT)) { + RegularArgument regArg = (RegularArgument) arg; + String varName = regArg.expression().toString(); + + if (numpyArrayList.contains(varName)) { + ctx.addIssue(callExpression, 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..4b898a4 --- /dev/null +++ b/src/test/resources/checks/useTorchFromNumpy.py @@ -0,0 +1,9 @@ +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}} \ No newline at end of file From 7268950e303d96d09d9fb07cf32f06e2404f48f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ophas=20Fournier?= Date: Tue, 20 May 2025 15:42:48 +0200 Subject: [PATCH 2/6] GCI107 UseTorchFromNumpy #Python #DLG #Build Co-authored-by: DataLabGroupe-CreditAgricole --- CHANGELOG.md | 2 + .../python/PythonRuleRepository.java | 3 +- .../python/checks/UseTorchFromNumpy.java | 96 +++++++++++++++++ .../creedengo/python/checks/Utils.java | 102 ++++++++++++++++++ .../python/checks/UseTorchFromNumpyTest.java | 29 +++++ .../resources/checks/useTorchFromNumpy.py | 9 ++ 6 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java create mode 100644 src/main/java/org/greencodeinitiative/creedengo/python/checks/Utils.java create mode 100644 src/test/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpyTest.java create mode 100644 src/test/resources/checks/useTorchFromNumpy.py diff --git a/CHANGELOG.md b/CHANGELOG.md index acb0422..041eab3 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..5007cd9 --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java @@ -0,0 +1,96 @@ +/* + * 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.CallExpression; +import org.sonar.plugins.python.api.tree.Expression; +import org.sonar.plugins.python.api.tree.StringLiteral; +import org.sonar.plugins.python.api.tree.Tree; +import org.sonar.plugins.python.api.tree.QualifiedExpression; +import org.sonar.plugins.python.api.tree.Argument; +import org.sonar.plugins.python.api.tree.AssignmentStatement; +import org.sonar.plugins.python.api.tree.RegularArgument; + +import java.util.ArrayList; +import java.util.List; + +import static org.sonar.plugins.python.api.tree.Tree.Kind.*; +@Rule(key = "GCI314") +public class UseTorchFromNumpy extends PythonSubscriptionCheck { + + private final List numpyArrayList = new ArrayList<>(); + + public static final String DESCRIPTION = "Use torch.from_numpy() instead of torch.tensor() to create tensors from numpy arrays"; + + @Override + public void initialize(Context context) { + context.registerSyntaxNodeConsumer(ASSIGNMENT_STMT, this::visitAssiStmt); + context.registerSyntaxNodeConsumer(CALL_EXPR, this::visitCallExpression); + } + + private void visitAssiStmt(SubscriptionContext ctx) { + AssignmentStatement assignmentStmt = (AssignmentStatement) ctx.syntaxNode(); + Expression value = assignmentStmt.assignedValue(); + + if (value.is(CALL_EXPR)) { + if (checkNumpyCallExpression((CallExpression) value)) { + String variableName = Utils.getVariableName(ctx); + System.out.println(("added one variable: " + variableName)); + if (variableName != null) { + numpyArrayList.add(variableName); + } + } + } + } + + private boolean checkNumpyCallExpression(CallExpression callExpression) { + Expression callee = callExpression.callee(); + + if (callee.is(QUALIFIED_EXPR)) { + return Utils.getQualifiedName(callExpression).equals("numpy.array"); + } + return false; + } + + private void visitCallExpression(SubscriptionContext ctx) { + CallExpression callExpression = (CallExpression) ctx.syntaxNode(); + // System.out.println("visiting call expression at line"+ callExpression.firstToken().line()); + // System.out.println("Utils"+ Utils.getQualifiedName(callExpression)); + // System.out.println("callExpression"+ callExpression.); + // System.out.println((callExpression)); + + if (Utils.getQualifiedName(callExpression).equals("torch.tensor")) { + System.out.println(("detected torch.tensor() call at line: " + callExpression.firstToken().line())); + for (Argument arg : callExpression.arguments()) { + if (arg.is(REGULAR_ARGUMENT)) { + RegularArgument regArg = (RegularArgument) arg; + String varName = regArg.expression().toString(); + + if (numpyArrayList.contains(varName)) { + ctx.addIssue(callExpression, 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..4b898a4 --- /dev/null +++ b/src/test/resources/checks/useTorchFromNumpy.py @@ -0,0 +1,9 @@ +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}} \ No newline at end of file From 44c50da9d56d57446138e1443a18365fe7f782c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ophas=20Fournier?= Date: Tue, 20 May 2025 15:42:48 +0200 Subject: [PATCH 3/6] GCI107 UseTorchFromNumpy #Python #DLG #Build Co-authored-by: DataLabGroupe-CreditAgricole --- CHANGELOG.md | 2 + .../python/PythonRuleRepository.java | 3 +- .../python/checks/UseTorchFromNumpy.java | 96 +++++++++++++++++ .../creedengo/python/checks/Utils.java | 102 ++++++++++++++++++ .../python/checks/UseTorchFromNumpyTest.java | 29 +++++ .../resources/checks/useTorchFromNumpy.py | 9 ++ 6 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java create mode 100644 src/main/java/org/greencodeinitiative/creedengo/python/checks/Utils.java create mode 100644 src/test/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpyTest.java create mode 100644 src/test/resources/checks/useTorchFromNumpy.py 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..5007cd9 --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java @@ -0,0 +1,96 @@ +/* + * 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.CallExpression; +import org.sonar.plugins.python.api.tree.Expression; +import org.sonar.plugins.python.api.tree.StringLiteral; +import org.sonar.plugins.python.api.tree.Tree; +import org.sonar.plugins.python.api.tree.QualifiedExpression; +import org.sonar.plugins.python.api.tree.Argument; +import org.sonar.plugins.python.api.tree.AssignmentStatement; +import org.sonar.plugins.python.api.tree.RegularArgument; + +import java.util.ArrayList; +import java.util.List; + +import static org.sonar.plugins.python.api.tree.Tree.Kind.*; +@Rule(key = "GCI314") +public class UseTorchFromNumpy extends PythonSubscriptionCheck { + + private final List numpyArrayList = new ArrayList<>(); + + public static final String DESCRIPTION = "Use torch.from_numpy() instead of torch.tensor() to create tensors from numpy arrays"; + + @Override + public void initialize(Context context) { + context.registerSyntaxNodeConsumer(ASSIGNMENT_STMT, this::visitAssiStmt); + context.registerSyntaxNodeConsumer(CALL_EXPR, this::visitCallExpression); + } + + private void visitAssiStmt(SubscriptionContext ctx) { + AssignmentStatement assignmentStmt = (AssignmentStatement) ctx.syntaxNode(); + Expression value = assignmentStmt.assignedValue(); + + if (value.is(CALL_EXPR)) { + if (checkNumpyCallExpression((CallExpression) value)) { + String variableName = Utils.getVariableName(ctx); + System.out.println(("added one variable: " + variableName)); + if (variableName != null) { + numpyArrayList.add(variableName); + } + } + } + } + + private boolean checkNumpyCallExpression(CallExpression callExpression) { + Expression callee = callExpression.callee(); + + if (callee.is(QUALIFIED_EXPR)) { + return Utils.getQualifiedName(callExpression).equals("numpy.array"); + } + return false; + } + + private void visitCallExpression(SubscriptionContext ctx) { + CallExpression callExpression = (CallExpression) ctx.syntaxNode(); + // System.out.println("visiting call expression at line"+ callExpression.firstToken().line()); + // System.out.println("Utils"+ Utils.getQualifiedName(callExpression)); + // System.out.println("callExpression"+ callExpression.); + // System.out.println((callExpression)); + + if (Utils.getQualifiedName(callExpression).equals("torch.tensor")) { + System.out.println(("detected torch.tensor() call at line: " + callExpression.firstToken().line())); + for (Argument arg : callExpression.arguments()) { + if (arg.is(REGULAR_ARGUMENT)) { + RegularArgument regArg = (RegularArgument) arg; + String varName = regArg.expression().toString(); + + if (numpyArrayList.contains(varName)) { + ctx.addIssue(callExpression, 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..4b898a4 --- /dev/null +++ b/src/test/resources/checks/useTorchFromNumpy.py @@ -0,0 +1,9 @@ +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}} \ No newline at end of file From 0f6502de7c88b975af5acd255d129c35a123a47d Mon Sep 17 00:00:00 2001 From: dirdr Date: Wed, 21 May 2025 12:21:14 +0200 Subject: [PATCH 4/6] feat/GCI107-python Add test cases, rule ok --- .../python/checks/UseTorchFromNumpy.java | 105 +++++++++--------- .../resources/checks/useTorchFromNumpy.py | 42 ++++++- 2 files changed, 94 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java index 5007cd9..6399740 100644 --- a/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java @@ -15,82 +15,87 @@ * 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.CallExpression; -import org.sonar.plugins.python.api.tree.Expression; -import org.sonar.plugins.python.api.tree.StringLiteral; -import org.sonar.plugins.python.api.tree.Tree; -import org.sonar.plugins.python.api.tree.QualifiedExpression; -import org.sonar.plugins.python.api.tree.Argument; -import org.sonar.plugins.python.api.tree.AssignmentStatement; -import org.sonar.plugins.python.api.tree.RegularArgument; - -import java.util.ArrayList; -import java.util.List; +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 = "GCI314") public class UseTorchFromNumpy extends PythonSubscriptionCheck { - private final List numpyArrayList = new ArrayList<>(); - 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::visitAssiStmt); + context.registerSyntaxNodeConsumer(ASSIGNMENT_STMT, this::visitAssignmentStatement); context.registerSyntaxNodeConsumer(CALL_EXPR, this::visitCallExpression); } - private void visitAssiStmt(SubscriptionContext ctx) { - AssignmentStatement assignmentStmt = (AssignmentStatement) ctx.syntaxNode(); - Expression value = assignmentStmt.assignedValue(); + private void visitAssignmentStatement(SubscriptionContext ctx) { + var assignmentStmt = (AssignmentStatement) ctx.syntaxNode(); + var value = assignmentStmt.assignedValue(); - if (value.is(CALL_EXPR)) { - if (checkNumpyCallExpression((CallExpression) value)) { - String variableName = Utils.getVariableName(ctx); - System.out.println(("added one variable: " + variableName)); - if (variableName != null) { - numpyArrayList.add(variableName); - } + if (value.is(CALL_EXPR) && isNumpyArrayCreation((CallExpression) value)) { + String variableName = Utils.getVariableName(ctx); + if (variableName != null) { + numpyArrayVariables.add(variableName); } } } - private boolean checkNumpyCallExpression(CallExpression callExpression) { - Expression callee = callExpression.callee(); - - if (callee.is(QUALIFIED_EXPR)) { - return Utils.getQualifiedName(callExpression).equals("numpy.array"); - } - return false; + private boolean isNumpyArrayCreation(CallExpression callExpression) { + return NUMPY_ARRAY_FUNCTION.equals(Utils.getQualifiedName(callExpression)); } private void visitCallExpression(SubscriptionContext ctx) { - CallExpression callExpression = (CallExpression) ctx.syntaxNode(); - // System.out.println("visiting call expression at line"+ callExpression.firstToken().line()); - // System.out.println("Utils"+ Utils.getQualifiedName(callExpression)); - // System.out.println("callExpression"+ callExpression.); - // System.out.println((callExpression)); - - if (Utils.getQualifiedName(callExpression).equals("torch.tensor")) { - System.out.println(("detected torch.tensor() call at line: " + callExpression.firstToken().line())); - for (Argument arg : callExpression.arguments()) { - if (arg.is(REGULAR_ARGUMENT)) { - RegularArgument regArg = (RegularArgument) arg; - String varName = regArg.expression().toString(); - - if (numpyArrayList.contains(varName)) { - ctx.addIssue(callExpression, DESCRIPTION); - - } + var callExpression = (CallExpression) ctx.syntaxNode(); + + if (!TORCH_TENSOR_FUNCTION.equals(Utils.getQualifiedName(callExpression))) { + 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); } } } } - -} +} \ No newline at end of file diff --git a/src/test/resources/checks/useTorchFromNumpy.py b/src/test/resources/checks/useTorchFromNumpy.py index 4b898a4..ce3bbff 100644 --- a/src/test/resources/checks/useTorchFromNumpy.py +++ b/src/test/resources/checks/useTorchFromNumpy.py @@ -1,9 +1,45 @@ +# Case 1: Standard imports +import numpy +import torch + +numpy_array1 = numpy.array([1, 2, 3, 4]) + +compliant1 = torch.from_numpy(numpy_array1) + +non_compliant1 = torch.tensor(numpy_array1) # 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 -np_array = np.array([1, 2, 3]) +numpy_array2 = np.array([5, 6, 7, 8]) + +compliant2 = tt.from_numpy(numpy_array2) + +non_compliant2 = tt.tensor(numpy_array2) # 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 + +numpy_array3 = array([9, 10, 11, 12]) + +compliant3 = from_numpy(numpy_array3) + +non_compliant3 = tensor(numpy_array3) # 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 + +numpy_array4 = np_arr([13, 14, 15, 16]) + +compliant4 = t_from_numpy(numpy_array4) + +# Case 5: Direct np call as function argument +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 +non_compliant6 = t_tensor(np.array([1, 2, 3])) # Noncompliant {{Use torch.from_numpy() instead of torch.tensor() to create tensors from numpy arrays}} -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}} \ No newline at end of file From 333e84f9040f8e664c9b3695af03542dead52e6b Mon Sep 17 00:00:00 2001 From: dirdr Date: Wed, 21 May 2025 14:15:47 +0200 Subject: [PATCH 5/6] fix noncompliant test case --- src/test/resources/checks/useTorchFromNumpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/checks/useTorchFromNumpy.py b/src/test/resources/checks/useTorchFromNumpy.py index 3f7aa0a..57ed464 100644 --- a/src/test/resources/checks/useTorchFromNumpy.py +++ b/src/test/resources/checks/useTorchFromNumpy.py @@ -31,7 +31,7 @@ compliant4 = t_from_numpy(numpy_array) # Compliant -non_compliant4 = t_tensor(numpy_array) +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])) From 3783ff3d94be68016be910f355923629e61666dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ophas=20Fournier?= Date: Tue, 3 Jun 2025 10:20:26 +0200 Subject: [PATCH 6/6] GCI107 UseTorchFromNumpy Improve Build Co-authored-by: DataLabGroupe-CreditAgricole --- .../creedengo/python/checks/UseTorchFromNumpy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java index 8c65494..2b4cc9f 100644 --- a/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UseTorchFromNumpy.java @@ -31,7 +31,7 @@ * 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 = "GCI314") +@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";