From c172e2265568d7339f859858ae453a9dc612d667 Mon Sep 17 00:00:00 2001 From: Abdelkader HASSINE Date: Thu, 9 Oct 2025 23:45:42 +0200 Subject: [PATCH 1/4] added new rule GCI109 - avoid exception for control flow --- .../python/integration/tests/GCIRulesIT.java | 15 ++++ .../python/PythonRuleRepository.java | 3 +- .../AvoidExceptionsForControlFlowCheck.java | 69 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheck.java 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/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..34ce310 --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheck.java @@ -0,0 +1,69 @@ +/* + * 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 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) { + String exceptionName = exception.firstToken().value(); + return CONTROL_FLOW_EXCEPTIONS.contains(exceptionName); + } +} + From bec52c15d3ec63b83624a8c44bea44709b7eeeae Mon Sep 17 00:00:00 2001 From: Abdelkader HASSINE Date: Thu, 9 Oct 2025 23:46:14 +0200 Subject: [PATCH 2/4] add rule number to list --- .../creedengo/python/creedengo_way_profile.json | 1 + 1 file changed, 1 insertion(+) 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" ] From 71ea5eb4fcaf79cdb18d4261748d924e375f4360 Mon Sep 17 00:00:00 2001 From: Abdelkader HASSINE Date: Thu, 9 Oct 2025 23:46:39 +0200 Subject: [PATCH 3/4] tests - avoid exception for control flow --- .../src/avoidExceptionsForControlFlow.py | 67 +++++++++++++++++++ ...voidExceptionsForControlFlowCheckTest.java | 30 +++++++++ .../checks/avoidExceptionsForControlFlow.py | 67 +++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 src/it/test-projects/creedengo-python-plugin-test-project/src/avoidExceptionsForControlFlow.py create mode 100644 src/test/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheckTest.java create mode 100644 src/test/resources/checks/avoidExceptionsForControlFlow.py 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..34f8b77 --- /dev/null +++ b/src/it/test-projects/creedengo-python-plugin-test-project/src/avoidExceptionsForControlFlow.py @@ -0,0 +1,67 @@ +# 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 AttribteError 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) + + + +### 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/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..34f8b77 --- /dev/null +++ b/src/test/resources/checks/avoidExceptionsForControlFlow.py @@ -0,0 +1,67 @@ +# 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 AttribteError 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) + + + +### 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)) From b18b4eb444629d4f7de36bbec3a2fa1c896f55f0 Mon Sep 17 00:00:00 2001 From: Abdelkader HASSINE Date: Wed, 15 Oct 2025 21:17:04 +0200 Subject: [PATCH 4/4] treated PR review --- CHANGELOG.md | 2 ++ .../src/avoidExceptionsForControlFlow.py | 20 ++++++++++++++++++- .../AvoidExceptionsForControlFlowCheck.java | 11 ++++++++++ .../checks/avoidExceptionsForControlFlow.py | 20 ++++++++++++++++++- 4 files changed, 51 insertions(+), 2 deletions(-) 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/test-projects/creedengo-python-plugin-test-project/src/avoidExceptionsForControlFlow.py b/src/it/test-projects/creedengo-python-plugin-test-project/src/avoidExceptionsForControlFlow.py index 34f8b77..ed483a0 100644 --- 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 @@ -10,7 +10,7 @@ except IndexError: # Noncompliant {{Avoid using exceptions for control flow}} item = None -# using AttribteError for control flow +# using AttributeError for control flow try: value = obj.attribute except AttributeError: # Noncompliant {{Avoid using exceptions for control flow}} @@ -29,6 +29,24 @@ 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 ### diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheck.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheck.java index 34ce310..b545004 100644 --- a/src/main/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheck.java +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/AvoidExceptionsForControlFlowCheck.java @@ -24,6 +24,7 @@ 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; @@ -62,6 +63,16 @@ private void visitTryStatement(SubscriptionContext context) { } 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/test/resources/checks/avoidExceptionsForControlFlow.py b/src/test/resources/checks/avoidExceptionsForControlFlow.py index 34f8b77..ed483a0 100644 --- a/src/test/resources/checks/avoidExceptionsForControlFlow.py +++ b/src/test/resources/checks/avoidExceptionsForControlFlow.py @@ -10,7 +10,7 @@ except IndexError: # Noncompliant {{Avoid using exceptions for control flow}} item = None -# using AttribteError for control flow +# using AttributeError for control flow try: value = obj.attribute except AttributeError: # Noncompliant {{Avoid using exceptions for control flow}} @@ -29,6 +29,24 @@ 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 ###