diff --git a/src/main/java/org/greencodeinitiative/creedengo/java/checks/SpringMaxRetryableCheck.java b/src/main/java/org/greencodeinitiative/creedengo/java/checks/SpringMaxRetryableCheck.java new file mode 100644 index 00000000..a5561b12 --- /dev/null +++ b/src/main/java/org/greencodeinitiative/creedengo/java/checks/SpringMaxRetryableCheck.java @@ -0,0 +1,173 @@ +/* + * creedengo - Java language - Provides rules to reduce the environmental footprint of your Java 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.java.checks; + +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.tree.*; +import org.sonar.plugins.java.api.tree.Tree.Kind; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +// TODO DDC : rule already existing natively in SonarQube 9.9 (see java:S3012) for a part of checks +// ==> analyse / add our tag to it (?) + +@Rule(key = "GCI604") +public class SpringMaxRetryableCheck extends IssuableSubscriptionVisitor { + + public static final String MESSAGE_RULE = "Please use optimized @Retryable parameters."; + private static final long MAX_TIMEOUT = 5000; + private static final int MAX_RETRY = 3; + + public static long calculateRetryTimeout(Integer maxAttempts, Long delay, Double multiplier) { + int attempts = (maxAttempts != null) ? maxAttempts : 3; + long initialDelay = (delay != null) ? delay : 1000L; + double factor = (multiplier != null) ? multiplier : 1.0; + long total = 0; + long currentDelay = initialDelay; + + for (int i = 1; i < attempts; i++) { + total += currentDelay; + currentDelay = (long) (currentDelay * factor); + } + return total; + } + + @Override + public List nodesToVisit() { + return Collections.singletonList(Kind.ANNOTATION); + } + + private Optional extractConstantAsString(ExpressionTree tree) { + Optional asInt = tree.asConstant(Integer.class); + if (asInt.isPresent()) { + return Optional.of(String.valueOf(asInt.get())); + } + + Optional asDouble = tree.asConstant(Double.class); + return asDouble.map(String::valueOf).or(() -> tree.asConstant(String.class)); + } + + @Override + public void visitNode(Tree tree) { + AnnotationTree annotationTree = (AnnotationTree) tree; + if (!"Retryable".equals(annotationTree.symbolType().fullyQualifiedName())) { + return; + } + + List params = new ArrayList<>(); + + for (ExpressionTree argument : annotationTree.arguments()) { + if (!argument.is(Tree.Kind.ASSIGNMENT)) { + continue; + } + AssignmentExpressionTree assignmentTree = (AssignmentExpressionTree) argument; + String paramName = ((IdentifierTree) assignmentTree.variable()).name(); + ExpressionTree valueTree = assignmentTree.expression(); + + var extractedParams = extractParametersAndValues(argument, valueTree, paramName); + if (!extractedParams.isEmpty()) { + params.addAll(extractedParams); + } + } + + if (!params.isEmpty()) { + checkValues(params); + } + } + + private List extractParametersAndValues(ExpressionTree argument, ExpressionTree valueTree, String paramName) { + List params = new ArrayList<>(); + + if (valueTree.asConstant(Integer.class).isPresent()) { + int value = valueTree.asConstant(Integer.class).get(); + params.add(new ArgumentDetails(argument, paramName, String.valueOf(value))); + } else if (valueTree.is(Kind.ANNOTATION)) { + AnnotationTree nestedAnnotation = (AnnotationTree) valueTree; + for (ExpressionTree nestedArg : nestedAnnotation.arguments()) { + if (nestedArg.is(Kind.ASSIGNMENT)) { + AssignmentExpressionTree nestedAssignment = (AssignmentExpressionTree) nestedArg; + String nestedParam = ((IdentifierTree) nestedAssignment.variable()).name(); + ExpressionTree nestedValueTree = nestedAssignment.expression(); + + Optional constValue = extractConstantAsString(nestedValueTree); + constValue.ifPresent(val -> + params.add(new ArgumentDetails(nestedArg, paramName + "." + nestedParam, val)) + ); + } + } + } + + return params; + } + + void checkValues(List params) { + Integer maxAttempts = params.stream() + .filter(argumentDetails -> argumentDetails.paramName.equals("maxAttempts")) + .map(ArgumentDetails::getParamValue) + .map(Integer::valueOf) + .findFirst().orElse(null); + + Long delay = params.stream() + .filter(argumentDetails -> argumentDetails.paramName.equals("backoff.delay")) + .map(ArgumentDetails::getParamValue) + .map(Long::valueOf) + .findFirst().orElse(null); + + Double multiplier = params.stream() + .filter(argumentDetails -> argumentDetails.paramName.equals("backoff.multiplier")) + .map(ArgumentDetails::getParamValue) + .map(Double::parseDouble) + .findFirst().orElse(null); + + if (isGreaterThanMax(maxAttempts, delay, multiplier)) { + reportIssue(params.get(0).getArgument(), MESSAGE_RULE); + } + } + + public boolean isGreaterThanMax(Integer maxAttempts, Long delay, Double multiplier) { + return (calculateRetryTimeout(maxAttempts, delay, multiplier) > MAX_TIMEOUT) || maxAttempts > MAX_RETRY; + } + + class ArgumentDetails { + private final ExpressionTree argument; + private final String paramName; + private final String paramValue; + + ArgumentDetails(ExpressionTree argument, String paramName, String paramValue) { + this.argument = argument; + this.paramName = paramName; + this.paramValue = paramValue; + } + + public ExpressionTree getArgument() { + return argument; + } + + public String getParamName() { + return paramName; + } + + public String getParamValue() { + return paramValue; + } + } +} diff --git a/src/test/files/SpringMaxRetryableCheck.java b/src/test/files/SpringMaxRetryableCheck.java new file mode 100644 index 00000000..9d663383 --- /dev/null +++ b/src/test/files/SpringMaxRetryableCheck.java @@ -0,0 +1,43 @@ +/* + * creedengo - Java language - Provides rules to reduce the environmental footprint of your Java 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 . + */ +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +class TestClass { + + @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 10)) + public void springMaxRetryOK() { + } + + @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 10, multiplier = 2)) + public void springMaxRetryWithFullParamsOK() { + } + + @Retryable() + public void springMaxRetryWithoutParamsOK() { + } + + @Retryable(maxAttempts = 5, backoff = @Backoff(delay = 10, multiplier = 2)) // Noncompliant {{Please use optimized @Retryable parameters.}} + public void springMaxRetryMaxAttemptsKO() { + } + + @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 10, multiplier = 10000)) // Noncompliant {{Please use optimized @Retryable parameters.}} + public void springMaxRetryTimeOutKO() { + } +} diff --git a/src/test/java/org/greencodeinitiative/creedengo/java/checks/SpringMaxRetryableTest.java b/src/test/java/org/greencodeinitiative/creedengo/java/checks/SpringMaxRetryableTest.java new file mode 100644 index 00000000..a3d44162 --- /dev/null +++ b/src/test/java/org/greencodeinitiative/creedengo/java/checks/SpringMaxRetryableTest.java @@ -0,0 +1,36 @@ +/* + * creedengo - Java language - Provides rules to reduce the environmental footprint of your Java 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.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +class SpringMaxRetryableTest { + + /** + * @formatter:off + */ + @Test + void test() { + CheckVerifier.newVerifier() + .onFile("src/test/files/SpringMaxRetryableCheck.java") + .withCheck(new SpringMaxRetryableCheck()) + .verifyIssues(); + } + +}