diff --git a/CHANGELOG.md b/CHANGELOG.md index e3dfb7b1..50cc9101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [#146](https://github.com/green-code-initiative/creedengo-python/pull/146) Add rule GCI24: Returned SQL results should be limited (avoid SELECT...FROM without LIMIT) +- Add rule GCI116: Avoid range(len()) pattern, prefer direct iteration or enumerate() ### Changed diff --git a/src/it/java/org/greencodeinitiative/creedengo/python/integration/tests/GCIRulesIT.java b/src/it/java/org/greencodeinitiative/creedengo/python/integration/tests/GCIRulesIT.java index 38ab00ae..74340e75 100644 --- a/src/it/java/org/greencodeinitiative/creedengo/python/integration/tests/GCIRulesIT.java +++ b/src/it/java/org/greencodeinitiative/creedengo/python/integration/tests/GCIRulesIT.java @@ -548,6 +548,28 @@ void testGCI203_compliant() { checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_1H); } + @Test + void testGCI116_compliant() { + String filePath = "src/GCI116/preferEnumerateOverRangeLenCompliant.py"; + String ruleId = "creedengo-python:GCI116"; + String ruleMsg = "Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access"; + int[] startLines = new int[]{}; + int[] endLines = new int[]{}; + + checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_5MIN); + } + + @Test + void testGCI116_nonCompliant() { + String filePath = "src/GCI116/preferEnumerateOverRangeLenNonCompliant.py"; + String ruleId = "creedengo-python:GCI116"; + String ruleMsg = "Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access"; + int[] startLines = new int[]{3, 7, 12, 16, 20, 21, 24, 26, 28}; + int[] endLines = new int[]{3, 7, 12, 16, 20, 21, 24, 26, 28}; + + checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_5MIN); + } + @Test void testGCI404() { String filePath = "src/GCI404/avoidListComprehensionInIterations.py"; diff --git a/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI116/preferEnumerateOverRangeLen.py b/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI116/preferEnumerateOverRangeLen.py new file mode 100644 index 00000000..72d48be7 --- /dev/null +++ b/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI116/preferEnumerateOverRangeLen.py @@ -0,0 +1,58 @@ +my_list = [1, 2, 3, 4, 5] + +for i in range(len(my_list)): # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + print(i, my_list[i]) + +names = ["Alice", "Bob", "Charlie"] +for i in range(len(names)): # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + print(names[i]) + +data = {"a": 1, "b": 2} +keys = list(data.keys()) +for i in range(len(keys)): # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + print(keys[i]) + +def process(items): + for i in range(len(items)): # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + items[i] = items[i] * 2 + +matrix = [[1, 2], [3, 4]] +for i in range(len(matrix)): # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + for j in range(len(matrix[i])): # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + print(matrix[i][j]) + +for i, val in enumerate(my_list): + print(i, val) + +for i in range(10): + print(i) + +n = 10 +for i in range(n): + print(i) + +for i in range(0, len(my_list)): + print(i) + +for i in range(0, len(my_list), 2): + print(i) + +for item in my_list: + print(item) + +length = len(my_list) +for i in range(length): + print(i) + +result = [my_list[i] for i in range(len(my_list))] # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + +squared = {i: my_list[i]**2 for i in range(len(my_list))} # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + +gen = sum(my_list[i] for i in range(len(my_list))) # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + +compliant_comp = [val for val in my_list] + +compliant_enum = [val for i, val in enumerate(my_list)] + +compliant_range = [i for i in range(10)] + diff --git a/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI116/preferEnumerateOverRangeLenCompliant.py b/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI116/preferEnumerateOverRangeLenCompliant.py new file mode 100644 index 00000000..ec2f8501 --- /dev/null +++ b/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI116/preferEnumerateOverRangeLenCompliant.py @@ -0,0 +1,30 @@ +my_list = [1, 2, 3, 4, 5] + +for i, val in enumerate(my_list): + print(i, val) + +for i in range(10): + print(i) + +n = 10 +for i in range(n): + print(i) + +for i in range(0, len(my_list)): + print(i) + +for i in range(0, len(my_list), 2): + print(i) + +for item in my_list: + print(item) + +length = len(my_list) +for i in range(length): + print(i) + +compliant_comp = [val for val in my_list] + +compliant_enum = [val for i, val in enumerate(my_list)] + +compliant_range = [i for i in range(10)] diff --git a/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI116/preferEnumerateOverRangeLenNonCompliant.py b/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI116/preferEnumerateOverRangeLenNonCompliant.py new file mode 100644 index 00000000..cab24a06 --- /dev/null +++ b/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI116/preferEnumerateOverRangeLenNonCompliant.py @@ -0,0 +1,29 @@ +my_list = [1, 2, 3, 4, 5] + +for i in range(len(my_list)): # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + print(i, my_list[i]) + +names = ["Alice", "Bob", "Charlie"] +for i in range(len(names)): # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + print(names[i]) + +data = {"a": 1, "b": 2} +keys = list(data.keys()) +for i in range(len(keys)): # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + print(keys[i]) + +def process(items): + for i in range(len(items)): # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + items[i] = items[i] * 2 + +matrix = [[1, 2], [3, 4]] +for i in range(len(matrix)): # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + for j in range(len(matrix[i])): # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + print(matrix[i][j]) + +result = [my_list[i] for i in range(len(my_list))] # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + +squared = {i: my_list[i]**2 for i in range(len(my_list))} # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + +gen = sum(my_list[i] for i in range(len(my_list))) # Noncompliant {{Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access}} + diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java b/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java index a0bb6d86..beafc985 100644 --- a/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java +++ b/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java @@ -56,7 +56,8 @@ public record PythonRuleRepository(SonarRuntime sonarRuntime) implements RulesDe GCI104AvoidCreatingTensorUsingNumpyOrNativePython.class, GCI110AvoidWildcardImportsCheck.class, GCI109AvoidExceptionsForControlFlowCheck.class, - GCI112UsingSlotsOnDataClasses.class + GCI112UsingSlotsOnDataClasses.class, + GCI116PreferEnumerateOverRangeLen.class ); public static final String LANGUAGE = "py"; diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/GCI116PreferEnumerateOverRangeLen.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/GCI116PreferEnumerateOverRangeLen.java new file mode 100644 index 00000000..ed5aa0db --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/GCI116PreferEnumerateOverRangeLen.java @@ -0,0 +1,109 @@ +/* + * 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.ComprehensionFor; +import org.sonar.plugins.python.api.tree.Expression; +import org.sonar.plugins.python.api.tree.ForStatement; +import org.sonar.plugins.python.api.tree.Name; +import org.sonar.plugins.python.api.tree.RegularArgument; +import org.sonar.plugins.python.api.tree.Tree; + +import java.util.List; + +@Rule(key = "GCI116") +public class GCI116PreferEnumerateOverRangeLen extends PythonSubscriptionCheck { + + public static final String DESCRIPTION = "Avoid range(len()) pattern, prefer direct iteration or enumerate() which avoids costly index-based access"; + + @Override + public void initialize(Context context) { + context.registerSyntaxNodeConsumer(Tree.Kind.FOR_STMT, this::visitForStatement); + context.registerSyntaxNodeConsumer(Tree.Kind.COMP_FOR, this::visitComprehensionFor); + } + + private void visitForStatement(SubscriptionContext context) { + ForStatement forStatement = (ForStatement) context.syntaxNode(); + + List testExpressions = forStatement.testExpressions(); + if (testExpressions.size() != 1) { + return; + } + + Expression iterable = testExpressions.get(0); + if (isRangeLenCall(iterable)) { + context.addIssue(iterable, DESCRIPTION); + } + } + + private void visitComprehensionFor(SubscriptionContext context) { + ComprehensionFor compFor = (ComprehensionFor) context.syntaxNode(); + Expression iterable = compFor.iterable(); + if (isRangeLenCall(iterable)) { + context.addIssue(iterable, DESCRIPTION); + } + } + + private boolean isRangeLenCall(Expression expression) { + if (!expression.is(Tree.Kind.CALL_EXPR)) { + return false; + } + + CallExpression callExpression = (CallExpression) expression; + Expression callee = callExpression.callee(); + + if (!callee.is(Tree.Kind.NAME)) { + return false; + } + + if (!"range".equals(((Name) callee).name())) { + return false; + } + + List arguments = callExpression.arguments(); + if (arguments.size() != 1) { + return false; + } + + org.sonar.plugins.python.api.tree.Argument firstArg = arguments.get(0); + if (!(firstArg instanceof RegularArgument)) { + return false; + } + + return isLenCall(((RegularArgument) firstArg).expression()); + } + + private boolean isLenCall(Expression expression) { + if (!expression.is(Tree.Kind.CALL_EXPR)) { + return false; + } + + CallExpression callExpression = (CallExpression) expression; + Expression callee = callExpression.callee(); + + if (!callee.is(Tree.Kind.NAME)) { + return false; + } + + return "len".equals(((Name) callee).name()); + } +} diff --git a/src/main/resources/org/greencodeinitiative/creedengo/python/creedengo_way_profile.json b/src/main/resources/org/greencodeinitiative/creedengo/python/creedengo_way_profile.json index 6a107ce8..3c724659 100644 --- a/src/main/resources/org/greencodeinitiative/creedengo/python/creedengo_way_profile.json +++ b/src/main/resources/org/greencodeinitiative/creedengo/python/creedengo_way_profile.json @@ -27,6 +27,7 @@ "GCI110", "GCI111", "GCI112", + "GCI116", "GCI203", "GCI404" ] diff --git a/src/test/java/org/greencodeinitiative/creedengo/python/checks/GCI116PreferEnumerateOverRangeLenTest.java b/src/test/java/org/greencodeinitiative/creedengo/python/checks/GCI116PreferEnumerateOverRangeLenTest.java new file mode 100644 index 00000000..6da17f83 --- /dev/null +++ b/src/test/java/org/greencodeinitiative/creedengo/python/checks/GCI116PreferEnumerateOverRangeLenTest.java @@ -0,0 +1,40 @@ +/* + * 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.jupiter.api.Test; +import org.sonar.python.checks.utils.PythonCheckVerifier; + +class GCI116PreferEnumerateOverRangeLenTest { + + @Test + void testNonCompliant() { + PythonCheckVerifier.verify(System.getProperty("testfiles.path") + "/GCI116/preferEnumerateOverRangeLen.py", new GCI116PreferEnumerateOverRangeLen()); + } + + @Test + void testNonCompliantOnly() { + PythonCheckVerifier.verify(System.getProperty("testfiles.path") + "/GCI116/preferEnumerateOverRangeLenNonCompliant.py", new GCI116PreferEnumerateOverRangeLen()); + } + + @Test + void testCompliant() { + PythonCheckVerifier.verifyNoIssue(System.getProperty("testfiles.path") + "/GCI116/preferEnumerateOverRangeLenCompliant.py", new GCI116PreferEnumerateOverRangeLen()); + } +} +