From e6b970cb76e80fce476ec896a971202a3edadd9d Mon Sep 17 00:00:00 2001 From: fabienhusse Date: Tue, 20 May 2025 19:12:52 +0200 Subject: [PATCH 1/5] :sparkles: feat: add rule to check using slots on data classes --- pom.xml | 2 +- .../src/usingSlotsOnDataClassesCompliant.py | 6 ++ .../usingSlotsOnDataClassesNonCompliant.py | 20 ++++++ .../python/PythonRuleRepository.java | 3 +- .../checks/UsingSlotsOnDataClasses.java | 69 +++++++++++++++++++ .../python/creedengo_way_profile.json | 3 +- .../checks/UsingSlotsOnDataClassesTest.java | 31 +++++++++ .../usingSlotsOnDataClassesCompliant.py | 7 ++ .../usingSlotsOnDataClassesNonCompliant.py | 20 ++++++ 9 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesCompliant.py create mode 100644 src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesNonCompliant.py create mode 100644 src/main/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClasses.java create mode 100644 src/test/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClassesTest.java create mode 100644 src/test/resources/checks/usingSlotsOnDataClassesCompliant.py create mode 100644 src/test/resources/checks/usingSlotsOnDataClassesNonCompliant.py diff --git a/pom.xml b/pom.xml index b292cfb..ebb8e65 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ 5.17.0 - 2.2.2 + main-SNAPSHOT https://repo1.maven.org/maven2 diff --git a/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesCompliant.py b/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesCompliant.py new file mode 100644 index 0000000..9fd18de --- /dev/null +++ b/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesCompliant.py @@ -0,0 +1,6 @@ +@dataclass(slots=True) +class MyClass: + def __init__(self, a, b, c): + self.a = a + self.b = b + self.c = c diff --git a/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesNonCompliant.py b/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesNonCompliant.py new file mode 100644 index 0000000..f671671 --- /dev/null +++ b/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesNonCompliant.py @@ -0,0 +1,20 @@ +@dataclass +class MyClass1: + def __init__(self, a, b, c): + self.a = a + self.b = b + self.c = c + +@dataclass() +class MyClass2: + def __init__(self, a, b, c): + self.a = a + self.b = b + self.c = c + +@dataclass(frozen=True) +class MyClass3: + def __init__(self, a, b, c): + self.a = a + self.b = b + self.c = c \ No newline at end of file diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java b/src/main/java/org/greencodeinitiative/creedengo/python/PythonRuleRepository.java index c385979..144d5e4 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, + UsingSlotsOnDataClasses.class ); public static final String LANGUAGE = "py"; diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClasses.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClasses.java new file mode 100644 index 0000000..b7642f9 --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClasses.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.*; + +@Rule(key = "EC1442") +public class UsingSlotsOnDataClasses extends PythonSubscriptionCheck { + + private static final String DECORATOR_DATA_CLASS = "dataclass"; + private static final String SLOTS_ARG = "slots"; + + public static final String DESCRIPTION = "Reduce memory footprint by using @dataclass(slots=True)"; + + @Override + public void initialize(Context context) { + context.registerSyntaxNodeConsumer(Tree.Kind.DECORATOR, this::isUsingSlots); + } + + private void isUsingSlots(SubscriptionContext ctx) { + Decorator decorator = ((Decorator) ctx.syntaxNode()); + + if (!isDecoratorDataClass(decorator)) { + return; + } + + if (decorator.arguments() != null + && decorator.arguments().arguments() != null + && decorator.arguments().arguments().stream() + .anyMatch(argument -> argument.firstToken().value().equals(SLOTS_ARG))) { + return; + } + + ctx.addIssue(decorator, DESCRIPTION); + } + + private boolean isDecoratorDataClass(Decorator decorator) { + Name name = null; + // Manage decorator detected as simple expression + if (decorator.expression().is(Tree.Kind.NAME)) { + name = (Name) decorator.expression(); + // manage decorator detected as callable expression + } else if(decorator.expression().is(Tree.Kind.CALL_EXPR)) { + CallExpression callExpression = (CallExpression) decorator.expression(); + if (callExpression.callee().is(Tree.Kind.NAME)) { + name = (Name) callExpression.callee(); + } + } + return name != null && UsingSlotsOnDataClasses.DECORATOR_DATA_CLASS.equals(name.name()); + } +} 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 671dd92..8c25757 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 @@ -11,6 +11,7 @@ "GCI74", "GCI89", "GCI203", - "GCI404" + "GCI404", + "EC1442" ] } diff --git a/src/test/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClassesTest.java b/src/test/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClassesTest.java new file mode 100644 index 0000000..db5bc61 --- /dev/null +++ b/src/test/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClassesTest.java @@ -0,0 +1,31 @@ +/* + * 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 UsingSlotsOnDataClassesTest { + + @Test + public void test_using_slots_on_data_classes() { + PythonCheckVerifier.verify("src/test/resources/checks/usingSlotsOnDataClassesNonCompliant.py", new UsingSlotsOnDataClasses()); + PythonCheckVerifier.verifyNoIssue("src/test/resources/checks/usingSlotsOnDataClassesCompliant.py", new UsingSlotsOnDataClasses()); + } + +} diff --git a/src/test/resources/checks/usingSlotsOnDataClassesCompliant.py b/src/test/resources/checks/usingSlotsOnDataClassesCompliant.py new file mode 100644 index 0000000..36cc7ab --- /dev/null +++ b/src/test/resources/checks/usingSlotsOnDataClassesCompliant.py @@ -0,0 +1,7 @@ +# COMPLIANT +@dataclass(slots=True) +class MyClass: + def __init__(self, a, b, c): + self.a = a + self.b = b + self.c = c diff --git a/src/test/resources/checks/usingSlotsOnDataClassesNonCompliant.py b/src/test/resources/checks/usingSlotsOnDataClassesNonCompliant.py new file mode 100644 index 0000000..433270b --- /dev/null +++ b/src/test/resources/checks/usingSlotsOnDataClassesNonCompliant.py @@ -0,0 +1,20 @@ +@dataclass # Noncompliant {{Reduce memory footprint by using @dataclass(slots=True)}} +class MyClass: + def __init__(self, a, b, c): + self.a = a + self.b = b + self.c = c + +@dataclass() # Noncompliant {{Reduce memory footprint by using @dataclass(slots=True)}} +class MyClass: + def __init__(self, a, b, c): + self.a = a + self.b = b + self.c = c + +@dataclass(frozen=True) # Noncompliant {{Reduce memory footprint by using @dataclass(slots=True)}} +class MyClass: + def __init__(self, a, b, c): + self.a = a + self.b = b + self.c = c \ No newline at end of file From d083e3bfbdb3b2c59dd5d5c3e9e81fab5af22eaf Mon Sep 17 00:00:00 2001 From: fabienhusse Date: Wed, 21 May 2025 11:06:43 +0200 Subject: [PATCH 2/5] :sparkles: feat: add it tests, update rule id and changelog --- CHANGELOG.md | 1 + .../python/integration/tests/GCIRulesIT.java | 26 +++++++++++++++++++ .../usingSlotsOnDataClassesNonCompliant.py | 21 +++++++-------- .../checks/UsingSlotsOnDataClasses.java | 2 +- .../python/creedengo_way_profile.json | 2 +- .../usingSlotsOnDataClassesCompliant.py | 7 +++-- .../usingSlotsOnDataClassesNonCompliant.py | 21 +++++++-------- 7 files changed, 50 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acb0422..fbb3132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- [#32](https://github.com/green-code-initiative/creedengo-challenge/issues/32) [EC1442] @dataclass(slots=True) shoud be declared on data classes ### 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 3922fc4..ca0666c 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 @@ -273,4 +273,30 @@ void testGCI203_compliant() { } + @Test + void testGCIGCI1442_compliant() { + + String filePath = "src/usingSlotsOnDataClassesCompliant.py"; + String ruleId = "creedengo-python:GCI1442"; + String ruleMsg = "Reduce memory footprint by using @dataclass(slots=True)"; + int[] startLines = new int[]{}; + int[] endLines = new int[]{}; + + checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_1MIN); + + } + + @Test + void testGCIGCI1442_nonCompliant() { + + String filePath = "src/usingSlotsOnDataClassesNonCompliant.py"; + String ruleId = "creedengo-python:GCI1442"; + String ruleMsg = "Reduce memory footprint by using @dataclass(slots=True)"; + int[] startLines = new int[]{1, 7, 13}; + int[] endLines = new int[]{1, 7, 13}; + + checkIssuesForFile(filePath, ruleId, ruleMsg, startLines, endLines, SEVERITY, TYPE, EFFORT_1MIN); + + } + } diff --git a/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesNonCompliant.py b/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesNonCompliant.py index f671671..91bc18c 100644 --- a/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesNonCompliant.py +++ b/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesNonCompliant.py @@ -1,20 +1,17 @@ @dataclass class MyClass1: - def __init__(self, a, b, c): - self.a = a - self.b = b - self.c = c + a: int + b: int + c: int @dataclass() class MyClass2: - def __init__(self, a, b, c): - self.a = a - self.b = b - self.c = c + a: int + b: int + c: int @dataclass(frozen=True) class MyClass3: - def __init__(self, a, b, c): - self.a = a - self.b = b - self.c = c \ No newline at end of file + a: int + b: int + c: int \ No newline at end of file diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClasses.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClasses.java index b7642f9..7b7b52c 100644 --- a/src/main/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClasses.java +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClasses.java @@ -22,7 +22,7 @@ import org.sonar.plugins.python.api.SubscriptionContext; import org.sonar.plugins.python.api.tree.*; -@Rule(key = "EC1442") +@Rule(key = "GCI1442") public class UsingSlotsOnDataClasses extends PythonSubscriptionCheck { private static final String DECORATOR_DATA_CLASS = "dataclass"; 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 8c25757..b94c7c2 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 @@ -12,6 +12,6 @@ "GCI89", "GCI203", "GCI404", - "EC1442" + "GCI1442" ] } diff --git a/src/test/resources/checks/usingSlotsOnDataClassesCompliant.py b/src/test/resources/checks/usingSlotsOnDataClassesCompliant.py index 36cc7ab..3cfe26b 100644 --- a/src/test/resources/checks/usingSlotsOnDataClassesCompliant.py +++ b/src/test/resources/checks/usingSlotsOnDataClassesCompliant.py @@ -1,7 +1,6 @@ # COMPLIANT @dataclass(slots=True) class MyClass: - def __init__(self, a, b, c): - self.a = a - self.b = b - self.c = c + a: int + b: int + c: int diff --git a/src/test/resources/checks/usingSlotsOnDataClassesNonCompliant.py b/src/test/resources/checks/usingSlotsOnDataClassesNonCompliant.py index 433270b..0e3044d 100644 --- a/src/test/resources/checks/usingSlotsOnDataClassesNonCompliant.py +++ b/src/test/resources/checks/usingSlotsOnDataClassesNonCompliant.py @@ -1,20 +1,17 @@ @dataclass # Noncompliant {{Reduce memory footprint by using @dataclass(slots=True)}} class MyClass: - def __init__(self, a, b, c): - self.a = a - self.b = b - self.c = c + a: int + b: int + c: int @dataclass() # Noncompliant {{Reduce memory footprint by using @dataclass(slots=True)}} class MyClass: - def __init__(self, a, b, c): - self.a = a - self.b = b - self.c = c + a: int + b: int + c: int @dataclass(frozen=True) # Noncompliant {{Reduce memory footprint by using @dataclass(slots=True)}} class MyClass: - def __init__(self, a, b, c): - self.a = a - self.b = b - self.c = c \ No newline at end of file + a: int + b: int + c: int \ No newline at end of file From 0f08288e0cbdf22d89f831afc2b59df81970dcfb Mon Sep 17 00:00:00 2001 From: fabienhusse Date: Wed, 21 May 2025 11:21:00 +0200 Subject: [PATCH 3/5] :shower: fix: py test file --- .../src/usingSlotsOnDataClassesCompliant.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesCompliant.py b/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesCompliant.py index 9fd18de..5042b3c 100644 --- a/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesCompliant.py +++ b/src/it/test-projects/creedengo-python-plugin-test-project/src/usingSlotsOnDataClassesCompliant.py @@ -1,6 +1,5 @@ @dataclass(slots=True) class MyClass: - def __init__(self, a, b, c): - self.a = a - self.b = b - self.c = c + a: int + b: int + c: int From fcca0b83a3c7775e700f0ee2d6ab44452ce9d670 Mon Sep 17 00:00:00 2001 From: fabienhusse Date: Wed, 21 May 2025 13:41:06 +0200 Subject: [PATCH 4/5] :heavy_check_mark: feat: add python version check --- .../python/checks/UsingSlotsOnDataClasses.java | 5 ++++- .../checks/UsingSlotsOnDataClassesTest.java | 11 +++++++++++ .../usingSlotsOnDataClassesCompliantV39.py | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/checks/usingSlotsOnDataClassesCompliantV39.py diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClasses.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClasses.java index 7b7b52c..0290df4 100644 --- a/src/main/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClasses.java +++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClasses.java @@ -18,6 +18,7 @@ package org.greencodeinitiative.creedengo.python.checks; import org.sonar.check.Rule; +import org.sonar.plugins.python.api.ProjectPythonVersion; import org.sonar.plugins.python.api.PythonSubscriptionCheck; import org.sonar.plugins.python.api.SubscriptionContext; import org.sonar.plugins.python.api.tree.*; @@ -32,7 +33,9 @@ public class UsingSlotsOnDataClasses extends PythonSubscriptionCheck { @Override public void initialize(Context context) { - context.registerSyntaxNodeConsumer(Tree.Kind.DECORATOR, this::isUsingSlots); + if(ProjectPythonVersion.currentVersions().stream().anyMatch(version -> version.compare(3, 10) >= 0)) { + context.registerSyntaxNodeConsumer(Tree.Kind.DECORATOR, this::isUsingSlots); + } } private void isUsingSlots(SubscriptionContext ctx) { diff --git a/src/test/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClassesTest.java b/src/test/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClassesTest.java index db5bc61..9da18b7 100644 --- a/src/test/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClassesTest.java +++ b/src/test/java/org/greencodeinitiative/creedengo/python/checks/UsingSlotsOnDataClassesTest.java @@ -18,14 +18,25 @@ package org.greencodeinitiative.creedengo.python.checks; import org.junit.Test; +import org.sonar.plugins.python.api.ProjectPythonVersion; +import org.sonar.plugins.python.api.PythonVersionUtils; import org.sonar.python.checks.utils.PythonCheckVerifier; +import java.util.Set; + public class UsingSlotsOnDataClassesTest { @Test public void test_using_slots_on_data_classes() { + ProjectPythonVersion.setCurrentVersions(PythonVersionUtils.allVersions()); PythonCheckVerifier.verify("src/test/resources/checks/usingSlotsOnDataClassesNonCompliant.py", new UsingSlotsOnDataClasses()); PythonCheckVerifier.verifyNoIssue("src/test/resources/checks/usingSlotsOnDataClassesCompliant.py", new UsingSlotsOnDataClasses()); } + @Test + public void test_using_slots_on_data_classes_python_less_than_310() { + ProjectPythonVersion.setCurrentVersions(Set.of(PythonVersionUtils.Version.V_39)); + PythonCheckVerifier.verifyNoIssue("src/test/resources/checks/usingSlotsOnDataClassesCompliantV39.py", new UsingSlotsOnDataClasses()); + } + } diff --git a/src/test/resources/checks/usingSlotsOnDataClassesCompliantV39.py b/src/test/resources/checks/usingSlotsOnDataClassesCompliantV39.py new file mode 100644 index 0000000..61d8799 --- /dev/null +++ b/src/test/resources/checks/usingSlotsOnDataClassesCompliantV39.py @@ -0,0 +1,17 @@ +@dataclass +class MyClass: + a: int + b: int + c: int + +@dataclass() +class MyClass: + a: int + b: int + c: int + +@dataclass(frozen=True) +class MyClass: + a: int + b: int + c: int \ No newline at end of file From f06defcbf0fc3b8d6df351d55d8afd1ee35dbcfd Mon Sep 17 00:00:00 2001 From: fabienhusseperso Date: Wed, 21 May 2025 15:19:24 +0200 Subject: [PATCH 5/5] :bug: fix: rules specifications version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ebb8e65..b292cfb 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ 5.17.0 - main-SNAPSHOT + 2.2.2 https://repo1.maven.org/maven2