diff --git a/CHANGELOG.md b/CHANGELOG.md index f97f08d..f3c85c3 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 GCI96 avoidIterativeMatrixOperations + ### 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..f83a8ff 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, + AvoidIterativeMatrixOperations.class ); public static final String LANGUAGE = "py"; diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/AvoidIterativeMatrixOperations.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/AvoidIterativeMatrixOperations.java new file mode 100644 index 0000000..073bc5d --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/AvoidIterativeMatrixOperations.java @@ -0,0 +1,167 @@ +/* + * 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.Expression; +import org.sonar.plugins.python.api.tree.BinaryExpression; +import org.sonar.plugins.python.api.tree.CompoundAssignmentStatement; +import org.sonar.plugins.python.api.tree.ForStatement; +import org.sonar.plugins.python.api.tree.Tree; +import org.sonar.plugins.python.api.tree.Statement; +import org.sonar.plugins.python.api.tree.SubscriptionExpression; +import org.sonar.plugins.python.api.tree.AssignmentStatement; + +import java.util.List; + +@Rule(key = "GCI96") + +public class AvoidIterativeMatrixOperations extends PythonSubscriptionCheck { + + private static final String DESCRIPTION = "Avoid iterative matrix operations, use numpy dot or outer function instead"; + + @Override + public void initialize(Context context) { + context.registerSyntaxNodeConsumer(Tree.Kind.FOR_STMT, this::visitForStatement); + } + + private void visitForStatement(SubscriptionContext context) { + ForStatement forStatement = (ForStatement) context.syntaxNode(); + if (isDotProduct(forStatement) || isOuterProduct(forStatement) || isMatrixDotProduct(forStatement)) { + context.addIssue(forStatement.firstToken(), DESCRIPTION); + } + } + + + private boolean isDotProduct(ForStatement forStatement) { + List statements = forStatement.body().statements(); + for (Statement stmt : statements) { + if (stmt.is(Tree.Kind.COMPOUND_ASSIGNMENT)) { + CompoundAssignmentStatement assign = (CompoundAssignmentStatement) stmt; + Expression lhsExpression = assign.lhsExpression(); + if (assign.compoundAssignmentToken().value().equals("+=") + && isMultiplicationOfIndexedElements(assign.rhsExpression(),false) + && !isDoubleSubscription(lhsExpression)) { + System.out.println("Dot product found"); + return true; + } + } + } + return false; + } + + private boolean isOuterProduct(ForStatement outerForStatement) { + List outerStatements = outerForStatement.body().statements(); + for (Statement outerStatement : outerStatements) { + if (outerStatement.is(Tree.Kind.FOR_STMT)) { + ForStatement innerForStatement = (ForStatement) outerStatement; + List innerStatements = innerForStatement.body().statements(); + for (Statement innermostStatement : innerStatements) { + if (isOuterProductOperation(innermostStatement)) { + System.out.println("Outer product found"); + return true; + } + } + } + } + return false; + + } + private boolean isOuterProductOperation(Statement statement) { + if (statement.is(Tree.Kind.ASSIGNMENT_STMT)) { + AssignmentStatement assignmentStmt = (AssignmentStatement) statement; + Expression lhsExpression = assignmentStmt.lhsExpressions().get(0).expressions().get(0); + + if (isDoubleSubscription(lhsExpression)) { + Expression rhsExpression = assignmentStmt.assignedValue(); + return containsMultiplicationOfIndexedElements(rhsExpression, false); + } + } + return false; +} + +private boolean containsMultiplicationOfIndexedElements(Expression expr, boolean matrixOps) { + if (isMultiplicationOfIndexedElements(expr, matrixOps)) { + return true; + } + + if (expr instanceof BinaryExpression) { + BinaryExpression bin = (BinaryExpression) expr; + return containsMultiplicationOfIndexedElements(bin.leftOperand(), matrixOps) + || containsMultiplicationOfIndexedElements(bin.rightOperand(), matrixOps); + } + + return false; +} + + private boolean isMatrixDotProduct(ForStatement outerForStatement) { + List outerStatements = outerForStatement.body().statements(); + for (Statement outerStatement : outerStatements) { + if (outerStatement.is(Tree.Kind.FOR_STMT)) { + ForStatement middleForStatement = (ForStatement) outerStatement; + List middleStatements = middleForStatement.body().statements(); + for (Statement middleStatement : middleStatements) { + if (middleStatement.is(Tree.Kind.FOR_STMT)) { + ForStatement innerForStatement = (ForStatement) middleStatement; + List innerStatements = innerForStatement.body().statements(); + for (Statement innermostStatement : innerStatements) { + if (isMatrixDotProductOperation(innermostStatement)) { + System.out.println("Matrix dot product found"); + return true; + } + } + } + } + } + } + return false; + } + + private boolean isMatrixDotProductOperation(Statement statement) { + if (statement.is(Tree.Kind.COMPOUND_ASSIGNMENT)) { + CompoundAssignmentStatement compoundStatement = (CompoundAssignmentStatement) statement; + String operator = compoundStatement.compoundAssignmentToken().value(); + if (operator.equals("+=")) { + if (isDoubleSubscription(compoundStatement.lhsExpression())) { + Expression rhsExpression = compoundStatement.rhsExpression(); + return isMultiplicationOfIndexedElements(rhsExpression, true); + } + } + } + return false; + } + + private boolean isMultiplicationOfIndexedElements(Expression expr, boolean matrixOps) { + if (expr.is(Tree.Kind.MULTIPLICATION)) { + BinaryExpression bin = (BinaryExpression) expr; + if (matrixOps) { + return isDoubleSubscription(bin.leftOperand()) && isDoubleSubscription(bin.rightOperand()); + } else { + return bin.leftOperand().is(Tree.Kind.SUBSCRIPTION) && bin.rightOperand().is(Tree.Kind.SUBSCRIPTION); + } + } + return false; + } + + private boolean isDoubleSubscription(Expression expr) { + return expr.is(Tree.Kind.SUBSCRIPTION) && ((SubscriptionExpression) expr).object().is(Tree.Kind.SUBSCRIPTION); + } +} \ No newline at end of file diff --git a/src/test/java/org/greencodeinitiative/creedengo/python/checks/AvoidIterativeMatrixOperationsTest.java b/src/test/java/org/greencodeinitiative/creedengo/python/checks/AvoidIterativeMatrixOperationsTest.java new file mode 100644 index 0000000..ecf2ba5 --- /dev/null +++ b/src/test/java/org/greencodeinitiative/creedengo/python/checks/AvoidIterativeMatrixOperationsTest.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 AvoidIterativeMatrixOperationsTest { + + @Test + public void test() { + PythonCheckVerifier.verify("src/test/resources/checks/avoidIterativeMatrixOperations.py", new AvoidIterativeMatrixOperations()); + } +} diff --git a/src/test/resources/checks/avoidIterativeMatrixOperations.py b/src/test/resources/checks/avoidIterativeMatrixOperations.py new file mode 100644 index 0000000..b52709e --- /dev/null +++ b/src/test/resources/checks/avoidIterativeMatrixOperations.py @@ -0,0 +1,121 @@ +import numpy as np + +# Test 1: Simple dot product +a = [1, 2, 3, 4] +b = [2, 3, 4, 5] + +dot = 0 +for i in range(len(a)): # Noncompliant {{Avoid iterative matrix operations, use numpy dot or outer function instead}} + dot += a[i] * b[i] + +dot_numpy = np.dot(a, b) # Compliant + +# Test 2: Matrix dot product +A = [[1, 2], [3, 4]] +B = [[5, 6], [7, 8]] + +def iterative_matrix_product(A, B): + results = [[0 for _ in range(len(B[0]))] for _ in range(len(A))] + + for i in range(len(A)): # Noncompliant {{Avoid iterative matrix operations, use numpy dot or outer function instead}} + for j in range(len(B[0])): + for k in range(len(B)): + results[i][j] += A[i][k] * B[k][j] + + return results + +results = iterative_matrix_product(A, B) + +results_numpy = np.dot(A, B) # Compliant + +# Test 3: Outer product +x = np.random.rand(100) +y = np.random.rand(100) + +o = np.zeros((len(x), len(y))) +for i in range(len(x)): # Noncompliant {{Avoid iterative matrix operations, use numpy dot or outer function instead}} + for j in range(len(y)): + o[i][j] = x[i] * y[j] + +outer_numpy = np.outer(x, y) # Compliant + +# Test 4: Dot product with different variable names +vec1 = [1, 2, 3] +vec2 = [4, 5, 6] +res = 0 +for idx in range(3): # Noncompliant {{Avoid iterative matrix operations, use numpy dot or outer function instead}} + res += vec1[idx] * vec2[idx] + +# Test 5: False positive - scalar addition in loop +total = 0 +for i in range(10): + total += i # Compliant + +# Test 6: False positive - unrelated list indexing +c = [10, 20, 30] +d = [5, 6, 7] +e = [] +for i in range(len(c)): + e.append(c[i] + d[i]) # Compliant + +# Test 7: Dot product in list comprehension (should be compliant) +dp = sum([a[i] * b[i] for i in range(len(a))]) # Compliant + +# Test 8: Double subscription but not matrix op +m = [[1, 2], [3, 4]] +n = [[5, 6], [7, 8]] +for i in range(len(m)): + for j in range(len(n)): + print(m[i][j] + n[i][j]) # Compliant + +# Test 9: Outer product with extra operation +x = [1, 2] +y = [3, 4] +result = [[0]*len(y) for _ in range(len(x))] +for i in range(len(x)): # Noncompliant {{Avoid iterative matrix operations, use numpy dot or outer function instead}} + for j in range(len(y)): + result[i][j] = x[i] * y[j] + 1 + +# Test 9: Outer product with extra operation +x = [1, 2] +y = [3, 4] +result = [[0]*len(y) for _ in range(len(x))] +for i in range(len(x)): # Noncompliant {{Avoid iterative matrix operations, use numpy dot or outer function instead}} + for j in range(len(y)): + result[i][j] = x[i] * y[j] -10 + +# Test 10: 3-level nested matrix product with aliases +X = [[1, 2], [3, 4]] +Y = [[5, 6], [7, 8]] +Z = [[0, 0], [0, 0]] +for r in range(2): # Noncompliant {{Avoid iterative matrix operations, use numpy dot or outer function instead}} + for c in range(2): + for t in range(2): + Z[r][c] += X[r][t] * Y[t][c] + +# Test 11: False positive - nested loops without multiplication +total = 0 +for i in range(10): + for j in range(5): + total += i + j # Compliant + +# Test 12: Matrix dot with transpose +M1 = [[1, 2], [3, 4]] +M2 = [[5, 7], [6, 8]] +out = [[0 for _ in range(len(M2))] for _ in range(len(M1))] +for i in range(len(M1)): # Noncompliant {{Avoid iterative matrix operations, use numpy dot or outer function instead}} + for j in range(len(M2)): + for k in range(len(M1[0])): + out[i][j] += M1[i][k] * M2[j][k] # Transposed multiplication + +# Test 13: Outer product with offset indexing (still counts) +x = [1, 2] +y = [3, 4] +res = [[0, 0], [0, 0]] +for i in range(2): # Noncompliant {{Avoid iterative matrix operations, use numpy dot or outer function instead}} + for j in range(2): + res[i][j] = x[i] * y[j] + + +# Test 15: Matrix dot using zip (compliant) +res = sum(i * j for i, j in zip(a, b)) # Compliant