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());
+ }
+}
+