From c6d268c34c2062dd70650a3d1c28667d2ad30530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cachet=20Cl=C3=A9ment?= Date: Tue, 19 May 2026 17:13:51 +0200 Subject: [PATCH] GCI113 implemented rule in Python --- CHANGELOG.md | 1 + .../python/integration/tests/GCIRulesIT.java | 22 ++++++ .../preferXGBoostOverRandomForestCompliant.py | 16 ++++ ...eferXGBoostOverRandomForestNonCompliant.py | 8 ++ .../python/PythonRuleRepository.java | 3 +- .../GCI113PreferXGBoostOverRandomForest.java | 76 +++++++++++++++++++ .../python/creedengo_way_profile.json | 1 + ...I113PreferXGBoostOverRandomForestTest.java | 34 +++++++++ 8 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/it/test-projects/creedengo-python-plugin-test-project/src/GCI113/preferXGBoostOverRandomForestCompliant.py create mode 100644 src/it/test-projects/creedengo-python-plugin-test-project/src/GCI113/preferXGBoostOverRandomForestNonCompliant.py create mode 100644 src/main/java/org/greencodeinitiative/creedengo/python/checks/GCI113PreferXGBoostOverRandomForest.java create mode 100644 src/test/java/org/greencodeinitiative/creedengo/python/checks/GCI113PreferXGBoostOverRandomForestTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e3dfb7b1..902d9964 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 GCI113: Prefer XGBoost over RandomForest for better energy efficiency ### 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..b84cfc3c 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 testGCI113_nonCompliant() { + String filePath = "src/GCI113/preferXGBoostOverRandomForestNonCompliant.py"; + String ruleId = "creedengo-python:GCI113"; + String ruleMsg = "Prefer XGBoost over RandomForest for better energy efficiency"; + int[] startLines = new int[]{1, 2, 5, 8, 11}; + int[] endLines = new int[]{1, 2, 5, 8, 11}; + + checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_10MIN); + } + + @Test + void testGCI113_compliant() { + String filePath = "src/GCI113/preferXGBoostOverRandomForestCompliant.py"; + String ruleId = "creedengo-python:GCI113"; + String ruleMsg = "Prefer XGBoost over RandomForest for better energy efficiency"; + int[] startLines = new int[]{}; + int[] endLines = new int[]{}; + + checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_10MIN); + } + @Test void testGCI404() { String filePath = "src/GCI404/avoidListComprehensionInIterations.py"; diff --git a/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI113/preferXGBoostOverRandomForestCompliant.py b/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI113/preferXGBoostOverRandomForestCompliant.py new file mode 100644 index 00000000..c03a20fa --- /dev/null +++ b/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI113/preferXGBoostOverRandomForestCompliant.py @@ -0,0 +1,16 @@ +from xgboost import XGBClassifier +from xgboost import XGBRegressor +from sklearn.ensemble import GradientBoostingClassifier + + +# Compliant: using XGBoost classifier +model = XGBClassifier(eval_metric="logloss") +model.fit(X_train, y_train) + +# Compliant: using XGBoost regressor +model2 = XGBRegressor(n_estimators=100) +model2.fit(X_train, y_train) + +# Compliant: using other sklearn classifiers +model3 = GradientBoostingClassifier(n_estimators=100) +model3.fit(X_train, y_train) diff --git a/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI113/preferXGBoostOverRandomForestNonCompliant.py b/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI113/preferXGBoostOverRandomForestNonCompliant.py new file mode 100644 index 00000000..fb21529b --- /dev/null +++ b/src/it/test-projects/creedengo-python-plugin-test-project/src/GCI113/preferXGBoostOverRandomForestNonCompliant.py @@ -0,0 +1,8 @@ +from sklearn.ensemble import RandomForestClassifier # Noncompliant {{Prefer XGBoost over RandomForest for better energy efficiency}} +from sklearn.ensemble import RandomForestRegressor # Noncompliant {{Prefer XGBoost over RandomForest for better energy efficiency}} + +model = RandomForestClassifier(n_estimators=100) # Noncompliant {{Prefer XGBoost over RandomForest for better energy efficiency}} + +model2 = RandomForestRegressor(n_estimators=50) # Noncompliant {{Prefer XGBoost over RandomForest for better energy efficiency}} + +model3 = sklearn.ensemble.RandomForestClassifier(n_estimators=100) # Noncompliant {{Prefer XGBoost over RandomForest for better energy efficiency}} diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java b/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java index a0bb6d86..47b662ed 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, + GCI113PreferXGBoostOverRandomForest.class ); public static final String LANGUAGE = "py"; diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/GCI113PreferXGBoostOverRandomForest.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/GCI113PreferXGBoostOverRandomForest.java new file mode 100644 index 00000000..d5d7107a --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/GCI113PreferXGBoostOverRandomForest.java @@ -0,0 +1,76 @@ +/* + * 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.Expression; +import org.sonar.plugins.python.api.tree.ImportFrom; +import org.sonar.plugins.python.api.tree.Name; +import org.sonar.plugins.python.api.tree.QualifiedExpression; +import org.sonar.plugins.python.api.tree.Tree; + +@Rule(key = "GCI113") +public class GCI113PreferXGBoostOverRandomForest extends PythonSubscriptionCheck { + + private static final String DESCRIPTION = "Prefer XGBoost over RandomForest for better energy efficiency"; + private static final String RANDOM_FOREST_CLASSIFIER = "RandomForestClassifier"; + private static final String RANDOM_FOREST_REGRESSOR = "RandomForestRegressor"; + + @Override + public void initialize(Context context) { + context.registerSyntaxNodeConsumer(Tree.Kind.IMPORT_FROM, this::checkImport); + context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::checkInstantiation); + } + + private void checkImport(SubscriptionContext context) { + ImportFrom importFrom = (ImportFrom) context.syntaxNode(); + if (importFrom.importedNames() == null) { + return; + } + importFrom.importedNames().forEach(aliasedName -> { + String importedName = aliasedName.dottedName().names().stream() + .map(Name::name) + .reduce((a, b) -> a + "." + b) + .orElse(""); + if (RANDOM_FOREST_CLASSIFIER.equals(importedName) || RANDOM_FOREST_REGRESSOR.equals(importedName)) { + context.addIssue(aliasedName, DESCRIPTION); + } + }); + } + + private void checkInstantiation(SubscriptionContext context) { + CallExpression callExpression = (CallExpression) context.syntaxNode(); + Expression callee = callExpression.callee(); + + if (callee.is(Tree.Kind.NAME)) { + Name name = (Name) callee; + if (RANDOM_FOREST_CLASSIFIER.equals(name.name()) || RANDOM_FOREST_REGRESSOR.equals(name.name())) { + context.addIssue(callExpression, DESCRIPTION); + } + } else if (callee.is(Tree.Kind.QUALIFIED_EXPR)) { + QualifiedExpression qualifiedExpr = (QualifiedExpression) callee; + String methodName = qualifiedExpr.name().name(); + if (RANDOM_FOREST_CLASSIFIER.equals(methodName) || RANDOM_FOREST_REGRESSOR.equals(methodName)) { + context.addIssue(callExpression, DESCRIPTION); + } + } + } +} 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..5d8559b7 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", + "GCI113", "GCI203", "GCI404" ] diff --git a/src/test/java/org/greencodeinitiative/creedengo/python/checks/GCI113PreferXGBoostOverRandomForestTest.java b/src/test/java/org/greencodeinitiative/creedengo/python/checks/GCI113PreferXGBoostOverRandomForestTest.java new file mode 100644 index 00000000..e6dc6afe --- /dev/null +++ b/src/test/java/org/greencodeinitiative/creedengo/python/checks/GCI113PreferXGBoostOverRandomForestTest.java @@ -0,0 +1,34 @@ +/* + * 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 GCI113PreferXGBoostOverRandomForestTest { + + @Test + void testNonCompliant() { + PythonCheckVerifier.verify(System.getProperty("testfiles.path") + "/GCI113/preferXGBoostOverRandomForestNonCompliant.py", new GCI113PreferXGBoostOverRandomForest()); + } + + @Test + void testCompliant() { + PythonCheckVerifier.verifyNoIssue(System.getProperty("testfiles.path") + "/GCI113/preferXGBoostOverRandomForestCompliant.py", new GCI113PreferXGBoostOverRandomForest()); + } +}