diff --git a/CHANGELOG.md b/CHANGELOG.md
index f97f08d..5435c0c 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 105 on String Concatentation. This rule maybe apply to other rule
+
### 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..7aebcbf 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,
+ StringConcatenation.class
);
public static final String LANGUAGE = "py";
diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/StringConcatenation.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/StringConcatenation.java
new file mode 100644
index 0000000..8b31f00
--- /dev/null
+++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/StringConcatenation.java
@@ -0,0 +1,91 @@
+/*
+ * 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.Name;
+import org.sonar.plugins.python.api.tree.CompoundAssignmentStatement;
+import org.sonar.plugins.python.api.tree.Tree;
+import org.sonar.plugins.python.api.tree.Expression;
+import org.sonar.plugins.python.api.tree.AssignmentStatement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+
+@Rule(key="GCI105")
+public class StringConcatenation extends PythonSubscriptionCheck {
+
+ private final List stringVariables = new ArrayList<>();
+
+ public static final String DESCRIPTION = "Concatenation of strings should be done using f-strings or str.join()";
+
+
+ @Override
+ public void initialize(Context context) {
+ context.registerSyntaxNodeConsumer(Tree.Kind.STRING_ELEMENT, this::findStringVariable);
+
+ context.registerSyntaxNodeConsumer(Tree.Kind.COMPOUND_ASSIGNMENT, this::checkAssignment);
+
+ }
+
+ private void findStringVariable(SubscriptionContext context) {
+ Tree node = context.syntaxNode();
+ if (node.is(Tree.Kind.STRING_ELEMENT)) {
+ Tree current = node;
+ while (current != null && !current.is(Tree.Kind.ASSIGNMENT_STMT)) {
+ current = current.parent();
+ }
+
+ if (current != null && current.is(Tree.Kind.ASSIGNMENT_STMT)) {
+ AssignmentStatement assignment = (AssignmentStatement) current;
+
+
+ if (!assignment.assignedValue().is(Tree.Kind.LIST_LITERAL) &&
+ !assignment.assignedValue().is(Tree.Kind.LIST_COMPREHENSION)) {
+
+ String variableName = Utils.getVariableName(context);
+ if (variableName != null) {
+ stringVariables.add(variableName);
+ }
+ }
+ }
+ }
+}
+
+ private void checkAssignment(SubscriptionContext context) {
+
+ CompoundAssignmentStatement compoundAssignment = (CompoundAssignmentStatement) context.syntaxNode();
+ if (compoundAssignment.compoundAssignmentToken().value().equals("+=")) {
+ Expression lhsExpression = compoundAssignment.lhsExpression();
+ if (lhsExpression.is(Tree.Kind.NAME)) {
+ String variableName = ((Name) lhsExpression).name();
+ if (stringVariables.contains(variableName)) {
+ context.addIssue(lhsExpression.firstToken(), DESCRIPTION);
+ }
+ }}}
+
+
+
+
+
+}
diff --git a/src/main/java/org/greencodeinitiative/creedengo/python/checks/Utils.java b/src/main/java/org/greencodeinitiative/creedengo/python/checks/Utils.java
new file mode 100644
index 0000000..44f2183
--- /dev/null
+++ b/src/main/java/org/greencodeinitiative/creedengo/python/checks/Utils.java
@@ -0,0 +1,102 @@
+/*
+ * 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.plugins.python.api.SubscriptionContext;
+import org.sonar.plugins.python.api.symbols.Symbol;
+import org.sonar.plugins.python.api.tree.Argument;
+import org.sonar.plugins.python.api.tree.AssignmentStatement;
+import org.sonar.plugins.python.api.tree.Tree;
+import org.sonar.plugins.python.api.tree.RegularArgument;
+import org.sonar.plugins.python.api.tree.Name;
+import org.sonar.plugins.python.api.tree.CallExpression;
+import org.sonar.plugins.python.api.tree.Expression;
+
+import javax.annotation.CheckForNull;
+import java.util.List;
+import java.util.Objects;
+
+public class Utils {
+
+ private static boolean hasKeyword(Argument argument, String keyword) {
+ if (!argument.is(new Tree.Kind[] {Tree.Kind.REGULAR_ARGUMENT})) {
+ return false;
+ } else {
+ Name keywordArgument = ((RegularArgument) argument).keywordArgument();
+ return keywordArgument != null && keywordArgument.name().equals(keyword);
+ }
+ }
+
+ @CheckForNull
+ public static RegularArgument nthArgumentOrKeyword(int argPosition, String keyword, List arguments) {
+ for (int i = 0; i < arguments.size(); ++i) {
+ Argument argument = (Argument) arguments.get(i);
+ if (hasKeyword(argument, keyword)) {
+ return (RegularArgument) argument;
+ }
+
+ if (argument.is(new Tree.Kind[] {Tree.Kind.REGULAR_ARGUMENT})) {
+ RegularArgument regularArgument = (RegularArgument) argument;
+ if (regularArgument.keywordArgument() == null && argPosition == i) {
+ return regularArgument;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static String getQualifiedName(CallExpression callExpression) {
+ Symbol symbol = callExpression.calleeSymbol();
+
+ return symbol != null && symbol.fullyQualifiedName() != null ? symbol.fullyQualifiedName() : "";
+ }
+
+ public static String getMethodName(CallExpression callExpression) {
+ Symbol symbol = callExpression.calleeSymbol();
+ return symbol != null && symbol.name() != null ? symbol.name() : "";
+ }
+
+ public static List getArgumentsFromCall(CallExpression callExpression) {
+ try {
+ return Objects.requireNonNull(callExpression.argumentList()).arguments();
+ } catch (NullPointerException e) {
+ return List.of();
+ }
+ }
+
+ public static String getVariableName(SubscriptionContext context) {
+ Tree node = context.syntaxNode();
+ Tree current = node;
+ while (current != null && !current.is(Tree.Kind.ASSIGNMENT_STMT)) {
+ current = current.parent();
+ }
+ if (current != null && current.is(Tree.Kind.ASSIGNMENT_STMT)) {
+ AssignmentStatement assignment = (AssignmentStatement) current;
+ if (!assignment.lhsExpressions().isEmpty() && !assignment.lhsExpressions().get(0).expressions().isEmpty()) {
+ Expression leftExpr = assignment.lhsExpressions().get(0).expressions().get(0);
+ if (leftExpr.is(Tree.Kind.NAME)) {
+ Name variableName = (Name) leftExpr;
+ return variableName.name();
+ }
+ }
+
+ }
+ return null;
+ }
+}
diff --git a/src/test/java/org/greencodeinitiative/creedengo/python/checks/StringConcatenationTest.java b/src/test/java/org/greencodeinitiative/creedengo/python/checks/StringConcatenationTest.java
new file mode 100644
index 0000000..ed887ab
--- /dev/null
+++ b/src/test/java/org/greencodeinitiative/creedengo/python/checks/StringConcatenationTest.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 StringConcatenationTest {
+
+ @Test
+ public void test() {
+ PythonCheckVerifier.verify("src/test/resources/checks/stringConcatenation.py", new StringConcatenation());
+ }
+}
diff --git a/src/test/resources/checks/stringConcatenation.py b/src/test/resources/checks/stringConcatenation.py
new file mode 100644
index 0000000..a7d86b3
--- /dev/null
+++ b/src/test/resources/checks/stringConcatenation.py
@@ -0,0 +1,38 @@
+text = "hello"
+following_words = ["world", "I", "am", "a", "string", "concatenation"]
+
+for word in following_words:
+ text += word # Noncompliant {{Concatenation of strings should be done using f-strings or str.join()}}
+
+text = "init"
+text += " add this" # Noncompliant {{Concatenation of strings should be done using f-strings or str.join()}}
+
+text += [word for word in following_words] # Noncompliant {{Concatenation of strings should be done using f-strings or str.join()}}
+
+
+result = " ".join([text] + following_words)
+
+
+final = f"{text} {' '.join(following_words)}"
+
+
+def build_string(base, parts):
+ return f"{base} {' '.join(parts)}"
+
+mylist = []
+mylist += [1, 2, 3] # Compliant
+
+
+count = 0
+count += 1 # Compliant
+
+
+msg = "start"
+if True:
+ msg += " continued" # Noncompliant {{Concatenation of strings should be done using f-strings or str.join()}}
+
+
+def get_text():
+ return "function text"
+
+text += get_text() # Noncompliant {{Concatenation of strings should be done using f-strings or str.join()}}