From f1d86deef009feb14d367532d2ded50396fbdc9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ophas=20Fournier?= Date: Mon, 19 May 2025 16:29:08 +0200 Subject: [PATCH 1/2] GCI103 DictionaryItemsUnused #Python #DLG #Build Co-authored-by: DataLabGroupe-CreditAgricole --- CHANGELOG.md | 2 + .../python/PythonRuleRepository.java | 3 +- .../python/checks/DictionaryItemsUnused.java | 128 ++++++++++++++++++ .../checks/DictionaryItemsUnusedTest.java | 29 ++++ .../resources/checks/DictionaryItemsUnused.py | 30 ++++ 5 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/greencodeinitiative/creedengo/python/checks/DictionaryItemsUnused.java create mode 100644 src/test/java/org/greencodeinitiative/creedengo/python/checks/DictionaryItemsUnusedTest.java create mode 100644 src/test/resources/checks/DictionaryItemsUnused.py diff --git a/CHANGELOG.md b/CHANGELOG.md index bea9b72..44b4d26 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 +- Add rule GCI 103 Dictionary Items Unused. A rule specifying that dictionary iteration should consider the pertinence of the element used. + ### Changed - compatibility updates for SonarQube 25.5.0 diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java b/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java index c385979..fb0efc8 100644 --- a/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java +++ b/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java @@ -40,7 +40,8 @@ public class PythonRuleRepository implements RulesDefinition, PythonCustomRuleRe AvoidFullSQLRequest.class, AvoidListComprehensionInIterations.class, DetectUnoptimizedImageFormat.class, - AvoidMultipleIfElseStatementCheck.class + AvoidMultipleIfElseStatementCheck.class, + DictionaryItemsUnused.class ); public static final String LANGUAGE = "py"; diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/DictionaryItemsUnused.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/DictionaryItemsUnused.java new file mode 100644 index 0000000..2f77974 --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/DictionaryItemsUnused.java @@ -0,0 +1,128 @@ +/* + * 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 java.util.HashMap; +import java.util.Map; + +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.ForStatement; +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 ="GCI103") +public class DictionaryItemsUnused extends PythonSubscriptionCheck { + + public static final String DESCRIPTION = "Use dict.keys() or dict.values() instead of dict.items() when only one part of the key-value pair is used"; + + private final Map itemsLoops = new HashMap<>(); + + @Override + public void initialize(Context context) { + context.registerSyntaxNodeConsumer(Tree.Kind.FOR_STMT, this::checkForLoop); + context.registerSyntaxNodeConsumer(Tree.Kind.FOR_STMT, this::finalizeCheck); + } + + private void checkForLoop(SubscriptionContext context) { + ForStatement forStmt = (ForStatement) context.syntaxNode(); + + if (forStmt.expressions().size() == 2) { + Expression keyExpr = forStmt.expressions().get(0); + Expression valueExpr = forStmt.expressions().get(1); + Expression iterable = forStmt.testExpressions().get(0); + + if (isItemsCall(iterable)) { + String key = ((Name) keyExpr).name(); + String value = ((Name) valueExpr).name(); + + ItemsLoopInfo info = new ItemsLoopInfo(key, value); + itemsLoops.put(forStmt, info); + + trackNameUsages(forStmt.body(), info); + } + } + } + + private boolean isItemsCall(Expression expr) { + if (expr.is(Tree.Kind.CALL_EXPR)) { + CallExpression callExpr = (CallExpression) expr; + if (callExpr.callee().is(Tree.Kind.QUALIFIED_EXPR)) { + QualifiedExpression qualExpr = (QualifiedExpression) callExpr.callee(); + boolean isItems = "items".equals(qualExpr.name().name()); + return isItems; + } + } + return false; + } + + private void trackNameUsages(Tree node, ItemsLoopInfo info) { + if (node instanceof Name) { + String name = ((Name) node).name(); + info.markUsage(name); + } + + for (Tree child : node.children()) { + trackNameUsages(child, info); + } + } + + private void finalizeCheck(SubscriptionContext context) { + ForStatement forStmt = (ForStatement) context.syntaxNode(); + ItemsLoopInfo info = itemsLoops.get(forStmt); + + if (info != null) { + + if (info.isOnlyOneUsed()) { + context.addIssue(forStmt.firstToken(), DESCRIPTION); + } + + itemsLoops.remove(forStmt); + } + } + + private static class ItemsLoopInfo { + final String keyVar; + final String valueVar; + boolean keyUsed = false; + boolean valueUsed = false; + + ItemsLoopInfo(String keyVar, String valueVar) { + this.keyVar = keyVar; + this.valueVar = valueVar; + } + + void markUsage(String var) { + if (var.equals(keyVar)) { + keyUsed = true; + } + if (var.equals(valueVar)) { + valueUsed = true; + } + } + + boolean isOnlyOneUsed() { + return (keyUsed && !valueUsed) || (!keyUsed && valueUsed); + } + } +} \ No newline at end of file diff --git a/src/test/java/org/greencodeinitiative/creedengo/python/checks/DictionaryItemsUnusedTest.java b/src/test/java/org/greencodeinitiative/creedengo/python/checks/DictionaryItemsUnusedTest.java new file mode 100644 index 0000000..66c404d --- /dev/null +++ b/src/test/java/org/greencodeinitiative/creedengo/python/checks/DictionaryItemsUnusedTest.java @@ -0,0 +1,29 @@ +/* + * 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.Test; +import org.sonar.python.checks.utils.PythonCheckVerifier; + +public class DictionaryItemsUnusedTest { + + @Test + public void test() { + PythonCheckVerifier.verify("src/test/resources/checks/DictionaryItemsUnused.py", new DictionaryItemsUnused()); + } +} diff --git a/src/test/resources/checks/DictionaryItemsUnused.py b/src/test/resources/checks/DictionaryItemsUnused.py new file mode 100644 index 0000000..b5b545e --- /dev/null +++ b/src/test/resources/checks/DictionaryItemsUnused.py @@ -0,0 +1,30 @@ + +for a, b in my_dict.items(): + print(a, b) + +for key, value in my_dict.items(): # Noncompliant {{Use dict.keys() or dict.values() instead of dict.items() when only one part of the key-value pair is used}} + result.append(key) + +for key, value in my_dict.items(): # Noncompliant {{Use dict.keys() or dict.values() instead of dict.items() when only one part of the key-value pair is used}} + result.append(key) + + +for key, value in my_dict.items(): # Noncompliant {{Use dict.keys() or dict.values() instead of dict.items() when only one part of the key-value pair is used}} + result.append(value) + + +for key in my_dict.keys(): + result.append(key) + + +for value in my_dict.values(): + result.append(value) + + +for item in my_dict.items(): + result.append(item) + + +entries = [] +for k, v in my_dict.items(): + entries.append((k, v)) From 7cccac649888417bba8f94d8a7b11a652e995ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ophas=20Fournier?= Date: Tue, 3 Jun 2025 09:49:11 +0200 Subject: [PATCH 2/2] GCI103 DictionaryItemsUnused add Python tests Co-authored-by: DataLabGroupe-CreditAgricole --- .../resources/checks/DictionaryItemsUnused.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/test/resources/checks/DictionaryItemsUnused.py b/src/test/resources/checks/DictionaryItemsUnused.py index b5b545e..a10dbc3 100644 --- a/src/test/resources/checks/DictionaryItemsUnused.py +++ b/src/test/resources/checks/DictionaryItemsUnused.py @@ -28,3 +28,32 @@ entries = [] for k, v in my_dict.items(): entries.append((k, v)) + +for key, value in my_dict.items(): # Noncompliant {{Use dict.keys() or dict.values() instead of dict.items() when only one part of the key-value pair is used}} + do_something_with(key) + +for k, v in my_dict.items(): # Noncompliant {{Use dict.keys() or dict.values() instead of dict.items() when only one part of the key-value pair is used}} + do_something_with(v) + +for key, value in my_dict.items(): + print(f"{key}: {value}") + +for k, v in my_dict.items(): + some_list.append((k, v)) + +for k, v in my_dict.items(): # Noncompliant {{Use dict.keys() or dict.values() instead of dict.items() when only one part of the key-value pair is used}} + used_keys.append(k) + +if True: + for k, v in my_dict.items(): + print(k) + print(v) + +copied_dict = dict(my_dict.items()) + + +for i, (k, v) in enumerate(my_dict.items()): + print(i, k, v) + + +{(k, v) for k, v in my_dict.items()}