diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a930d6..ed5adff 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 +- [#108](https://github.com/green-code-initiative/creedengo-python/pull/108) Add rule GCI109 Avoid using exceptions for control flow + ### Changed - compatibility updates for SonarQube 25.9.0 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 4fab77f..7195b1a 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 @@ -462,4 +462,19 @@ void testGCI108(){ checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_10MIN); } + @Test + void testGCI109(){ + String filePath = "src/avoidExceptionsForControlFlow.py"; + String ruleId = "creedengo-python:GCI109"; + String ruleMsg = "Avoid using exceptions for control flow"; + int[] startLines = new int[]{ + 4, 10, 16, 22, 29 + }; + int[] endLines = new int[]{ + 4, 10, 16, 22, 29 + }; + + checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY_MAJOR, TYPE, EFFORT_5MIN); + } + } diff --git a/src/it/test-projects/creedengo-python-plugin-test-project/src/avoidExceptionsForControlFlow.py b/src/it/test-projects/creedengo-python-plugin-test-project/src/avoidExceptionsForControlFlow.py new file mode 100644 index 0000000..ed483a0 --- /dev/null +++ b/src/it/test-projects/creedengo-python-plugin-test-project/src/avoidExceptionsForControlFlow.py @@ -0,0 +1,85 @@ +# using KeyError for control flow +try: + value = my_dict[key] +except KeyError: # Noncompliant {{Avoid using exceptions for control flow}} + value = default + +# using IndexError for control flow +try: + item = my_list[index] +except IndexError: # Noncompliant {{Avoid using exceptions for control flow}} + item = None + +# using AttributeError for control flow +try: + value = obj.attribute +except AttributeError: # Noncompliant {{Avoid using exceptions for control flow}} + value = None + +# using StopIteration for control flow +try: + value = next(iterator) +except StopIteration: # Noncompliant {{Avoid using exceptions for control flow}} + value = None + +# KeyError in loop +for key in keys: + try: + values.append(my_dict[key]) + except KeyError: # Noncompliant {{Avoid using exceptions for control flow}} + values.append(default) + +# multiple exceptions in tuple +try: + value = my_dict[key] +except (KeyError, ValueError): # Noncompliant {{Avoid using exceptions for control flow}} + value = None + +# multiple control flow exceptions in tuple +try: + item = my_list[index] +except (IndexError, KeyError): # Noncompliant {{Avoid using exceptions for control flow}} + item = None + +# AttributeError with other exceptions in tuple +try: + value = obj.attribute +except (AttributeError, TypeError): # Noncompliant {{Avoid using exceptions for control flow}} + value = None + + + +### Compliant cases ### +value = my_dict.get(key, default) + + + +if 0 <= index < len(my_list): + item = my_list[index] +else: + item = None + + + +value = getattr(obj, 'attribute', None) + + +value = next(iterator, None) + + +try: + result = risky_operation() +except ValueError: + result = None + + + +try: + process_file() +except (IOError, OSError): + handle_error() + + + +for key in keys: + values.append(my_dict.get(key, default)) diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java b/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java index ad4955e..d284739 100644 --- a/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java +++ b/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java @@ -50,7 +50,8 @@ public record PythonRuleRepository(SonarRuntime sonarRuntime) implements RulesDe DisableGradientForModelEval.class, StringConcatenation.class, PreferAppendLeft.class, - AvoidCreatingTensorUsingNumpyOrNativePython.class + AvoidCreatingTensorUsingNumpyOrNativePython.class, + AvoidExceptionsForControlFlowCheck.class ); public static final String LANGUAGE = "py"; diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheck.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheck.java new file mode 100644 index 0000000..b545004 --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheck.java @@ -0,0 +1,80 @@ +/* + * 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.ExceptClause; +import org.sonar.plugins.python.api.tree.Expression; +import org.sonar.plugins.python.api.tree.Tree; +import org.sonar.plugins.python.api.tree.TryStatement; +import org.sonar.plugins.python.api.tree.Tuple; + +import java.util.Arrays; +import java.util.List; + +@Rule(key = "GCI109") +public class AvoidExceptionsForControlFlowCheck extends PythonSubscriptionCheck { + + public static final String DESCRIPTION = "Avoid using exceptions for control flow"; + + private static final List CONTROL_FLOW_EXCEPTIONS = Arrays.asList( + "KeyError", + "IndexError", + "AttributeError", + "StopIteration" + ); + + @Override + public void initialize(Context context) { + context.registerSyntaxNodeConsumer(Tree.Kind.TRY_STMT, this::visitTryStatement); + } + + private void visitTryStatement(SubscriptionContext context) { + TryStatement tryStatement = (TryStatement) context.syntaxNode(); + + List exceptClauses = tryStatement.exceptClauses(); + if (exceptClauses.isEmpty()) { + return; + } + + for (ExceptClause exceptClause : exceptClauses) { + Expression exception = exceptClause.exception(); + if (exception != null && isControlFlowException(exception)) { + context.addIssue(exceptClause.exceptKeyword(), DESCRIPTION); + } + } + } + + private boolean isControlFlowException(Expression exception) { + if (exception.is(Tree.Kind.TUPLE)) { + Tuple tuple = (Tuple) exception; + for (Expression element : tuple.elements()) { + if (isControlFlowException(element)) { + return true; + } + } + return false; + } + + String exceptionName = exception.firstToken().value(); + return CONTROL_FLOW_EXCEPTIONS.contains(exceptionName); + } +} + 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 d33223c..86bda65 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 @@ -22,6 +22,7 @@ "GCI106", "GCI107", "GCI108", + "GCI109", "GCI203", "GCI404" ] diff --git a/src/test/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheckTest.java b/src/test/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheckTest.java new file mode 100644 index 0000000..9fdbcb8 --- /dev/null +++ b/src/test/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheckTest.java @@ -0,0 +1,30 @@ +/* + * 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; + +public class AvoidExceptionsForControlFlowCheckTest { + + @Test + public void test() { + PythonCheckVerifier.verify("src/test/resources/checks/avoidExceptionsForControlFlow.py", new AvoidExceptionsForControlFlowCheck()); + } +} + diff --git a/src/test/resources/checks/avoidExceptionsForControlFlow.py b/src/test/resources/checks/avoidExceptionsForControlFlow.py new file mode 100644 index 0000000..ed483a0 --- /dev/null +++ b/src/test/resources/checks/avoidExceptionsForControlFlow.py @@ -0,0 +1,85 @@ +# using KeyError for control flow +try: + value = my_dict[key] +except KeyError: # Noncompliant {{Avoid using exceptions for control flow}} + value = default + +# using IndexError for control flow +try: + item = my_list[index] +except IndexError: # Noncompliant {{Avoid using exceptions for control flow}} + item = None + +# using AttributeError for control flow +try: + value = obj.attribute +except AttributeError: # Noncompliant {{Avoid using exceptions for control flow}} + value = None + +# using StopIteration for control flow +try: + value = next(iterator) +except StopIteration: # Noncompliant {{Avoid using exceptions for control flow}} + value = None + +# KeyError in loop +for key in keys: + try: + values.append(my_dict[key]) + except KeyError: # Noncompliant {{Avoid using exceptions for control flow}} + values.append(default) + +# multiple exceptions in tuple +try: + value = my_dict[key] +except (KeyError, ValueError): # Noncompliant {{Avoid using exceptions for control flow}} + value = None + +# multiple control flow exceptions in tuple +try: + item = my_list[index] +except (IndexError, KeyError): # Noncompliant {{Avoid using exceptions for control flow}} + item = None + +# AttributeError with other exceptions in tuple +try: + value = obj.attribute +except (AttributeError, TypeError): # Noncompliant {{Avoid using exceptions for control flow}} + value = None + + + +### Compliant cases ### +value = my_dict.get(key, default) + + + +if 0 <= index < len(my_list): + item = my_list[index] +else: + item = None + + + +value = getattr(obj, 'attribute', None) + + +value = next(iterator, None) + + +try: + result = risky_operation() +except ValueError: + result = None + + + +try: + process_file() +except (IOError, OSError): + handle_error() + + + +for key in keys: + values.append(my_dict.get(key, default))