Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions MUTATORS.generated.MD
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,16 @@ isDraft

languageLevel: jdk1.8

### [ConsecutiveLiteralAppends](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/ConsecutiveLiteralAppends.java)

PMD: [ConsecutiveLiteralAppends](https://pmd.github.io/pmd/pmd_rules_java_performance.html#consecutiveliteralappends)

Cleanthat own ID: ConsecutiveLiteralAppends

isDraft

languageLevel: jdk1.5

### [CreateTempFilesUsingNio](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/CreateTempFilesUsingNio.java)

Sonar: [RSPEC-2976](https://rules.sonarsource.com/java/RSPEC-2976)
Expand Down
3 changes: 3 additions & 0 deletions MUTATORS_BY_TAG.generated.MD
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- [AvoidUncheckedExceptionsInSignatures](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/AvoidUncheckedExceptionsInSignatures.java)
- [CastMathOperandsBeforeAssignement](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/CastMathOperandsBeforeAssignement.java)
- [CollectionToOptional](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/CollectionToOptional.java)
- [ConsecutiveLiteralAppends](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/ConsecutiveLiteralAppends.java)
- [CreateTempFilesUsingNio](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/CreateTempFilesUsingNio.java)
- [EmptyControlStatement](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/EmptyControlStatement.java)
- [EnumsWithoutEquals](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/EnumsWithoutEquals.java)
Expand Down Expand Up @@ -158,6 +159,7 @@
- [AvoidMultipleUnaryOperators](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/AvoidMultipleUnaryOperators.java)
- [AvoidUncheckedExceptionsInSignatures](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/AvoidUncheckedExceptionsInSignatures.java)
- [ComparisonWithNaN](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/ComparisonWithNaN.java)
- [ConsecutiveLiteralAppends](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/ConsecutiveLiteralAppends.java)
- [LiteralsFirstInComparisons](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/LiteralsFirstInComparisons.java)
- [PrimitiveWrapperInstantiation](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/PrimitiveWrapperInstantiation.java)
- [SimplifyStartsWith](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/SimplifyStartsWith.java)
Expand Down Expand Up @@ -318,6 +320,7 @@
## With JDK 1.5

- [AppendCharacterWithChar](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/AppendCharacterWithChar.java)
- [ConsecutiveLiteralAppends](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/ConsecutiveLiteralAppends.java)
- [EnumsWithoutEquals](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/EnumsWithoutEquals.java)
- [PrimitiveWrapperInstantiation](java/src/main/java/eu/solven/cleanthat/engine/java/refactorer/mutators/PrimitiveWrapperInstantiation.java)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright 2023-2025 Benoit Lacelle - SOLVEN
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.solven.cleanthat.engine.java.refactorer.mutators;

import java.util.Optional;
import java.util.Set;

import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.google.common.collect.ImmutableSet;

import eu.solven.cleanthat.engine.java.IJdkVersionConstants;
import eu.solven.cleanthat.engine.java.refactorer.AJavaparserExprMutator;
import eu.solven.cleanthat.engine.java.refactorer.NodeAndSymbolSolver;
import eu.solven.cleanthat.engine.java.refactorer.helpers.MethodCallExprHelpers;
import eu.solven.cleanthat.engine.java.refactorer.meta.IMutatorDescriber;

/**
* Switch builder.append("string").append("builder") to builder.append("stringbuilder")
*
* @author Balazs Glatz
*/
public class ConsecutiveLiteralAppends extends AJavaparserExprMutator implements IMutatorDescriber {

private static final String METHOD_APPEND = "append";

@Override
public String minimalJavaVersion() {
return IJdkVersionConstants.JDK_5;
}

@Override
public boolean isPerformanceImprovment() {
return true;
}

@Override
public Set<String> getTags() {
return ImmutableSet.of("String");
}

@Override
public Optional<String> getPmdId() {
return Optional.of("ConsecutiveLiteralAppends");
}

@Override
public String pmdUrl() {
return "https://pmd.github.io/pmd/pmd_rules_java_performance.html#consecutiveliteralappends";
}

@Override
protected boolean processExpression(NodeAndSymbolSolver<Expression> expression) {
Expression node = expression.getNode();
if (!isMethodCall(node)) {
return false;
}

var methodCall = node.asMethodCallExpr();
if (!isAppendMethodWithSingleParam(methodCall)) {
return false;
}

String argument = getStringValue(methodCall.getArgument(0));
if (argument == null) {
return false;
}

Optional<Expression> scope = methodCall.getScope();
if (!isMethodCallOnAppendable(expression, scope)) {
return false;
}

var previousMethodCall = scope.get().asMethodCallExpr();
if (!isAppendMethodWithSingleParam(previousMethodCall)) {
return false;
}

String previousArgument = getStringValue(previousMethodCall.getArgument(0));
if (previousArgument == null) {
return false;
}

if (!isAppendableScope(expression, previousMethodCall.getScope())) {
return false;
}

var newArgument = new StringLiteralExpr(previousArgument + argument);
var replacement =
new MethodCallExpr(previousMethodCall.getScope().get(), METHOD_APPEND, NodeList.nodeList(newArgument));

return tryReplace(expression, replacement);
}

private static boolean isMethodCall(Expression node) {
return node instanceof MethodCallExpr;
}

private static boolean isMethodCallOnAppendable(NodeAndSymbolSolver<Expression> expression, Optional<Expression> scope) {
return scope.isPresent() && scope.get() instanceof MethodCallExpr && isAppendableScope(expression, scope);
}

private static boolean isAppendMethodWithSingleParam(MethodCallExpr methodCall) {
return METHOD_APPEND.equals(methodCall.getNameAsString()) && methodCall.getArguments().size() == 1;
}

private static boolean isAppendableScope(NodeAndSymbolSolver<Expression> expression, Optional<Expression> scope) {
return MethodCallExprHelpers.scopeHasRequiredType(expression.editNode(scope), Appendable.class);
}

private static String getStringValue(Expression argument) {
if (argument.isStringLiteralExpr()) {
return argument.asStringLiteralExpr().getValue();
}
if (argument.isCharLiteralExpr()) {
Comment thread
blacelle marked this conversation as resolved.
return argument.asCharLiteralExpr().getValue();
}
if (argument.isIntegerLiteralExpr()) {
return argument.asIntegerLiteralExpr().asNumber().toString();
}
if (argument.isLongLiteralExpr()) {
return argument.asLongLiteralExpr().asNumber().toString();
}
if (argument.isDoubleLiteralExpr()) {
return String.valueOf(argument.asDoubleLiteralExpr().asDouble());
}
if (argument.isUnaryExpr()) {
return argument.asUnaryExpr().toString();
}
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package eu.solven.cleanthat.engine.java.refactorer.cases.do_not_format_me;

import eu.solven.cleanthat.engine.java.refactorer.annotations.CompareMethods;
import eu.solven.cleanthat.engine.java.refactorer.annotations.UnmodifiedMethod;
import eu.solven.cleanthat.engine.java.refactorer.meta.IJavaparserAstMutator;
import eu.solven.cleanthat.engine.java.refactorer.mutators.ConsecutiveLiteralAppends;
import eu.solven.cleanthat.engine.java.refactorer.test.AJavaparserRefactorerCases;
import org.junit.Ignore;

public class TestConsecutiveLiteralAppendsCases extends AJavaparserRefactorerCases {

@Override
public IJavaparserAstMutator getTransformer() {
return new ConsecutiveLiteralAppends();
}

@UnmodifiedMethod
public static class FakeAppend {
private void append(char c) { }

public void pre() {
append('/');
}
}

@UnmodifiedMethod
public static class FakeChainedAppend {
private StringBuilder append(String string) {
return new StringBuilder();
}

public void pre() {
append("fakeAppend").append("realAppend");
}
}

@UnmodifiedMethod
public static class UnchainedLiteral {
public Object pre(StringBuilder builder) {
return builder.append("first");
}
}

@UnmodifiedMethod
public static class UnchainedVariable {
public Object pre(StringBuilder builder, String first) {
return builder.append(first);
}
}

@CompareMethods
public static class TwoLiterals {
public Object pre(StringBuilder builder) {
return builder.append("app").append("end");
}

public Object post(StringBuilder builder) {
return builder.append("append");
}
}

@Ignore("Not yet implemented")
@CompareMethods
public static class ThreeLiterals {
public Object pre(StringBuilder builder) {
return builder.append("app").append("end").append("ed");
}

public Object post(StringBuilder builder) {
return builder.append("appended");
}
}

@CompareMethods
public static class TwoChars {
public Object pre(StringBuilder builder) {
return builder.append('a').append('b');
}

public Object post(StringBuilder builder) {
return builder.append("ab");
}
}

@UnmodifiedMethod
public static class TwoVariables {
public Object pre(StringBuilder builder, String first, String second) {
return builder.append(first).append(second);
}
}

@CompareMethods
public static class CharAndString {
public Object pre(StringBuilder builder) {
return builder.append('a').append("ppend");
}

public Object post(StringBuilder builder) {
return builder.append("append");
}
}

@CompareMethods
public static class StringAndChar {
public Object pre(StringBuilder builder) {
return builder.append("map").append('s');
}

public Object post(StringBuilder builder) {
return builder.append("maps");
}
}

@UnmodifiedMethod
public static class VariableAndLiteral {
public Object pre(StringBuilder builder, String first) {
return builder.append(first).append("second");
}
}

@UnmodifiedMethod
public static class LiteralAndVariable {
public Object pre(StringBuilder builder, String second) {
return builder.append("first").append(second);
}
}

@CompareMethods
public static class TwoIntegers {
public Object pre(StringBuilder builder) {
return builder.append(1).append(2);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about :

  • builder.append(1).append(2);
  • builder.append(123).append(456);
  • builder.append(2147483647).append(1);
  • builder.append(1).append(-2);
  • builder.append(0x1).append(0x2);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, added them!

}

public Object post(StringBuilder builder) {
return builder.append("12");
Comment thread
blacelle marked this conversation as resolved.
}
}

@CompareMethods
public static class NonSingleDigitIntegers {
public Object pre(StringBuilder builder) {
return builder.append(123).append(456);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case is UnmodifiedMethod. Is it for specific reason, or just it feels like a rare/not-very relevant case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I think I just didn't think about it, but thanks for pointing out, I'll try to make it work!

Copy link
Copy Markdown
Contributor Author

@gbq6 gbq6 Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have adjusted it to support numbers, but only non-negatives yet. Planning to extend it with those as well, but in the meantime if you have any test cases that you suggest I'd appreciate it.

}

public Object post(StringBuilder builder) {
return builder.append("123456");
}
}

@CompareMethods
public static class IntegerOverflow {
public Object pre(StringBuilder builder) {
return builder.append(2147483647).append(1);
}

public Object post(StringBuilder builder) {
return builder.append("21474836471");
}
}

@CompareMethods
public static class Doubles {
public Object pre(StringBuilder builder) {
return builder.append(1.2).append(3.4);
}

public Object post(StringBuilder builder) {
return builder.append("1.23.4");
}
}

@CompareMethods
public static class NegativeInteger {
public Object pre(StringBuilder builder) {
return builder.append(1).append(-2);
}

public Object post(StringBuilder builder) {
return builder.append("1-2");
}
}

@CompareMethods
public static class HexadecimalIntegers {
public Object pre(StringBuilder builder) {
return builder.append(0x1).append(0x2);
}

public Object post(StringBuilder builder) {
return builder.append("12");
}
}

@UnmodifiedMethod
public static class CastIntegers {
public Object pre(StringBuilder builder) {
return builder.append((char) 1).append((char) 2);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public final class MutatorsScanner {
(Class<? extends IMutator>) Class.forName(PACKAGE_MUTATORS + "CollectionIndexOfToContains"),
(Class<? extends IMutator>) Class.forName(PACKAGE_MUTATORS + "CollectionToOptional"),
(Class<? extends IMutator>) Class.forName(PACKAGE_MUTATORS + "ComparisonWithNaN"),
(Class<? extends IMutator>) Class.forName(PACKAGE_MUTATORS + "ConsecutiveLiteralAppends"),
(Class<? extends IMutator>) Class.forName(PACKAGE_MUTATORS + "CreateTempFilesUsingNio"),
(Class<? extends IMutator>) Class.forName(PACKAGE_MUTATORS + "EmptyControlStatement"),
(Class<? extends IMutator>) Class.forName(PACKAGE_MUTATORS + "EnumsWithoutEquals"),
Expand Down