diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f7fc9..998fb37 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 +- [#26](https://github.com/green-code-initiative/ecoCode-python/issues/26) [EC89] Avoid unlimited cache + ### Changed ### Deleted diff --git a/pom.xml b/pom.xml index 554966f..9ceee8d 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ 5.3.1 - 1.4.7 + 1.6.0 2.5.0.1358 diff --git a/src/main/java/fr/greencodeinitiative/python/PythonRuleRepository.java b/src/main/java/fr/greencodeinitiative/python/PythonRuleRepository.java index c9ba42d..fb3b20f 100644 --- a/src/main/java/fr/greencodeinitiative/python/PythonRuleRepository.java +++ b/src/main/java/fr/greencodeinitiative/python/PythonRuleRepository.java @@ -60,6 +60,7 @@ public List checkClasses() { AvoidGlobalVariableInFunctionCheck.class, AvoidSQLRequestInLoop.class, AvoidTryCatchWithFileOpenedCheck.class, + AvoidUnlimitedCache.class, AvoidUnoptimizedVectorImagesCheck.class, NoFunctionCallWhenDeclaringForLoop.class, AvoidFullSQLRequest.class, diff --git a/src/main/java/fr/greencodeinitiative/python/checks/AvoidUnlimitedCache.java b/src/main/java/fr/greencodeinitiative/python/checks/AvoidUnlimitedCache.java new file mode 100644 index 0000000..1f62d87 --- /dev/null +++ b/src/main/java/fr/greencodeinitiative/python/checks/AvoidUnlimitedCache.java @@ -0,0 +1,90 @@ +/* + * ecoCode - Python language - Provides rules to reduce the environmental footprint of your Python programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * 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 fr.greencodeinitiative.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.Decorator; +import org.sonar.plugins.python.api.tree.FunctionDef; +import org.sonar.plugins.python.api.tree.Name; +import org.sonar.plugins.python.api.tree.RegularArgument; +import org.sonar.plugins.python.api.tree.Tree; + +@Rule(key = "EC89") +public class AvoidUnlimitedCache extends PythonSubscriptionCheck { + + public static final String DESCRIPTION = "Do not set cache size to unlimited"; + + public static final String LRU_CACHE = "lru_cache"; + public static final String MAX_SIZE_ARGUMENT = "maxsize"; + + public static final String CACHE = "cache"; + + @Override + public void initialize(Context context) { + // Check function decorators + context.registerSyntaxNodeConsumer(Tree.Kind.FUNCDEF, this::checkFunction); + } + + private void checkFunction(SubscriptionContext ctx) { + FunctionDef function = (FunctionDef) ctx.syntaxNode(); + + function.decorators().forEach(decorator -> { + // If decorator is @cache + if (isCacheDecorator(decorator)) { + ctx.addIssue(decorator, AvoidUnlimitedCache.DESCRIPTION); + // If decorator is @lru_cache + } else if (isLruCacheDecorator(decorator) + && decorator.arguments() != null + && decorator.arguments().arguments() != null + ) { + decorator.arguments().arguments().forEach(arg -> { + RegularArgument regArg = (RegularArgument) arg; + if (MAX_SIZE_ARGUMENT.equals(regArg.keywordArgument().name()) && regArg.expression().is(Tree.Kind.NONE)) { + ctx.addIssue(decorator, AvoidUnlimitedCache.DESCRIPTION); + } + }); + } + }); + } + + private boolean isCacheDecorator(Decorator decorator) { + return isDecorator(decorator, CACHE); + } + + private boolean isLruCacheDecorator(Decorator decorator) { + return isDecorator(decorator, LRU_CACHE); + } + + private boolean isDecorator(Decorator decorator, String expression) { + Name name = null; + // Manage decarator 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 && expression.equals(name.name()); + } +} diff --git a/src/test/java/fr/greencodeinitiative/python/PythonRuleRepositoryTest.java b/src/test/java/fr/greencodeinitiative/python/PythonRuleRepositoryTest.java index ca37f45..77db56f 100644 --- a/src/test/java/fr/greencodeinitiative/python/PythonRuleRepositoryTest.java +++ b/src/test/java/fr/greencodeinitiative/python/PythonRuleRepositoryTest.java @@ -78,7 +78,7 @@ void testMetadata() { @Test void testRegistredRules() { - assertThat(repository.rules()).hasSize(11); + assertThat(repository.rules()).hasSize(12); } @Test diff --git a/src/test/java/fr/greencodeinitiative/python/checks/AvoidUnlimitedCacheTest.java b/src/test/java/fr/greencodeinitiative/python/checks/AvoidUnlimitedCacheTest.java new file mode 100644 index 0000000..e99f092 --- /dev/null +++ b/src/test/java/fr/greencodeinitiative/python/checks/AvoidUnlimitedCacheTest.java @@ -0,0 +1,29 @@ +/* + * ecoCode - Python language - Provides rules to reduce the environmental footprint of your Python programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * 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 fr.greencodeinitiative.python.checks; + +import org.junit.Test; +import org.sonar.python.checks.utils.PythonCheckVerifier; + +public class AvoidUnlimitedCacheTest { + @Test + public void test() { + PythonCheckVerifier.verify("src/test/resources/checks/avoidUnlimitedCacheNonCompliant.py", new AvoidUnlimitedCache()); + PythonCheckVerifier.verifyNoIssue("src/test/resources/checks/avoidUnlimitedCacheCompliant.py", new AvoidUnlimitedCache()); + } +} diff --git a/src/test/resources/checks/avoidUnlimitedCacheCompliant.py b/src/test/resources/checks/avoidUnlimitedCacheCompliant.py new file mode 100644 index 0000000..aa37e6b --- /dev/null +++ b/src/test/resources/checks/avoidUnlimitedCacheCompliant.py @@ -0,0 +1,16 @@ +from functools import lru_cache + + +@lru_cache +def cached_function(): + print('a') + + +@lru_cache() +def cached_function_a(): + print('a') + + +@lru_cache(maxsize=30) +def cached_function_b(): + print('b') diff --git a/src/test/resources/checks/avoidUnlimitedCacheNonCompliant.py b/src/test/resources/checks/avoidUnlimitedCacheNonCompliant.py new file mode 100644 index 0000000..a05c706 --- /dev/null +++ b/src/test/resources/checks/avoidUnlimitedCacheNonCompliant.py @@ -0,0 +1,22 @@ +from functools import cache +from functools import lru_cache + + +class A: + @cache # Noncompliant {{Do not set cache size to unlimited}} + def cached_method_a(self): + print('a') + + @lru_cache(maxsize=None) # Noncompliant {{Do not set cache size to unlimited}} + def cached_method_b(self): + print('b') + + +@cache # Noncompliant {{Do not set cache size to unlimited}} +def cached_function(): + print('a') + + +@lru_cache(maxsize=None) # Noncompliant {{Do not set cache size to unlimited}} +def cached_method(): + print('b')