Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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)]

Original file line number Diff line number Diff line change
@@ -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)]
Original file line number Diff line number Diff line change
@@ -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}}

Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Expression> 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<org.sonar.plugins.python.api.tree.Argument> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"GCI110",
"GCI111",
"GCI112",
"GCI116",
"GCI203",
"GCI404"
]
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}