Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ruleKey": "S3063",
"hasTruePositives": true,
"falseNegatives": 0,
"falsePositives": 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package checks.unused;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

public class UnusedStringBuilderCheckSample {

public void usedTerminalMethod() {
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append("!");
System.out.println(sb.toString());
}

public void usedPassedAsArgument() {
StringBuilder sb = new StringBuilder();
sb.append("Hello!");
System.out.println(sb);
}

public String usedButInitializedSeparately() {
// FN, because it is not implemented.
StringBuilder sb;
sb = new StringBuilder();
sb.append("Hello");
sb.append("!");
return sb.toString();
}

public void unused() {
StringBuilder sb = new StringBuilder(); // Noncompliant {{Consume or remove this unused StringBuilder}}
// ^^
sb.append("Hello");
sb.append("!");
System.out.println("Hello!");
}

public void unusedNoOperationsAtAll() {
StringBuilder sb = new StringBuilder(); // Noncompliant {{Consume or remove this unused StringBuilder}}
// ^^
}

public String usedTerminalSubstring() {
StringBuilder sb = new StringBuilder();
sb.append("Hello!");
return sb.substring(2);
}

public StringBuilder usedReturned() {
StringBuilder sb = new StringBuilder();
sb.append("returned");
return sb;
}

public StringBuilder usedChainedReturned() {
StringBuilder sb = new StringBuilder();
return sb.append("returned");
}

public void usedChainedTwicePrinted() {
StringBuilder sb = new StringBuilder();
System.out.println(sb.append("one").append("two"));
}

public String usedChainedTwiceReturned() {
StringBuilder sb = new StringBuilder();
return sb.append("one").append("two").toString();
}

private void passedAsArg(StringBuilder fromOutside) {
// Considered used, because the caller can invoke a terminal operation.
fromOutside.append("good");
}

private void retrievedWithoutCallingTheConstructor(Supplier<StringBuilder> supplier) {
// Considered used, because the caller can invoke a terminal operation.
StringBuilder sb = supplier.get();
sb.append("good");
}

private void assignedLhs(Supplier<StringBuilder> supplier) {
// Considered used, because it is assigned.
StringBuilder sb = new StringBuilder();
sb = supplier.get();
sb.append("assignment");
}

private void assignedRhs(Supplier<StringBuilder> supplier) {
// FP, because sb escapes analysis, but we do not handle it.
StringBuilder sb = new StringBuilder(); // Noncompliant {{Consume or remove this unused StringBuilder}}
// ^^
sb.append("assignment");
StringBuilder sb2 = sb;
System.out.println(sb2);
}

private String unusedConstructorWithArgument() {
StringBuilder sb = new StringBuilder("Hello"); // Noncompliant {{Consume or remove this unused StringBuilder}}
// ^^
sb.append("!");
return "Hello!";
}

public void usedStringBufferTerminalMethod() {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("Hello");
stringBuffer.append("!");
System.out.println(stringBuffer.toString());
}

public void unusedStringBuffer() {
StringBuffer stringBuffer = new StringBuffer(); // Noncompliant {{Consume or remove this unused StringBuffer}}
// ^^^^^^^^^^^^
stringBuffer.append("Hello");
stringBuffer.append("!");
System.out.println("Hello!");
}


private void unrelated() {
List<Integer> list = new ArrayList<>();
list.add(5);
}

String usedManyChainedCalls() {
StringBuilder sb = new StringBuilder();
return sb.append("a").append("b").toString();
}

String unusedManyChainedCalls() {
StringBuilder sb = new StringBuilder(); // Noncompliant {{Consume or remove this unused StringBuilder}}
// ^^
sb.append("a").append("b");
return "ab";
}

String unusedManyChainedCallsAssign() {
StringBuilder sb = new StringBuilder(); // Noncompliant {{Consume or remove this unused StringBuilder}}
// ^^
StringBuilder sb2 = sb.append("a").append("b");
return "ab";
}

static class UnusedField1 {
// FN, because it is not implemented (initialization in the constructor).
StringBuilder stringBuilder;

UnusedField1() {
this.stringBuilder = new StringBuilder();
}

void appendHello() {
stringBuilder.append("Hello");
}
}

static class UnusedFieldPrivate {
private StringBuilder stringBuilder = new StringBuilder(); // Noncompliant
// ^^^^^^^^^^^^^

void appendHello() {
stringBuilder.append("Hello");
}
}

static class UnusedFieldProtected {
// It can be used in subclass.
protected StringBuilder stringBuilder = new StringBuilder();

void appendHello() {
stringBuilder.append("Hello");
}
}

static class UsedFieldPrivate {
private StringBuilder stringBuilder = new StringBuilder(); // Compliant

void appendHello() {
stringBuilder.append("Hello");
}

void print() {
System.out.println(stringBuilder);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* SonarQube Java
* Copyright (C) 2012-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* 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 Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.java.checks.unused;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import org.sonar.check.Rule;
import org.sonar.java.ast.parser.ArgumentListTreeImpl;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.NewClassTree;
import org.sonar.plugins.java.api.tree.ReturnStatementTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;

/**
* Check that `StringBuilder` and `StringBuffer` instances are consumed by calling `toString()` or in a similar way.
*/
@Rule(key = "S3063")
public class UnusedStringBuilderCheck extends IssuableSubscriptionVisitor {
private static final Set<String> TERMINAL_METHODS = Set.of("toString", "substring");

@Override
public List<Tree.Kind> nodesToVisit() {
return List.of(Tree.Kind.VARIABLE);
}

@Override
public void visitNode(Tree tree) {
if (tree instanceof VariableTree variableTree) {
Symbol symbol = variableTree.symbol();
String typeName = getStringBuilderOrStringBuffer(symbol.type());

// Exclude non-local variables with non-private visibility,
// because they can be changed in a way that is hard to track.
if (typeName != null && isInitializedByConstructor(variableTree.initializer()) &&
isLocalOrPrivate(symbol) &&
symbol.usages().stream().noneMatch(UnusedStringBuilderCheck::isUsedInAssignment) &&
symbol.usages().stream().noneMatch(UnusedStringBuilderCheck::isConsumed)) {
reportIssue(variableTree.simpleName(), "Consume or remove this unused %s".formatted(typeName));
}
}
}

/**
* Returns `"StringBuilder"` or `"StringBuffer"` if the variable is one of these types.
* This is to both check the type and extract the name for use in the issue message.
*/
private static @Nullable String getStringBuilderOrStringBuffer(Type type) {
if (type.is("java.lang.StringBuilder")) {
return "StringBuilder";
} else if (type.is("java.lang.StringBuffer")) {
return "StringBuffer";
}
return null;
}

/**
* Verify that the initializer is a call to a constructor, for example `new StringBuilder()`,
* and not a method call or missing, which happens when the variable is a parameter.
*/
private static boolean isInitializedByConstructor(@Nullable ExpressionTree initializer) {
return initializer instanceof NewClassTree;
}

private static boolean isLocalOrPrivate(Symbol symbol) {
return symbol.isLocalVariable() || symbol.isPrivate();
}

/**
* True if the argument is the LHS of an assignment.
*
* Ideally, we should also check the RHS, to account for variables escaping the analysis.
*/
private static boolean isUsedInAssignment(@Nullable Tree tree) {
return Optional.ofNullable(tree)
.map(Tree::parent)
.filter(AssignmentExpressionTree.class::isInstance)
.isPresent();
}

/**
* True if a tree, representing a variable, is consumed by calling `toString()`, returning it,
* passing it as an argument, etc.
*/
private static boolean isConsumed(Tree tree) {
Tree parent = tree.parent();
if (parent instanceof MemberSelectExpressionTree mset) {
if (TERMINAL_METHODS.contains(mset.identifier().name())) {
return true;
} else {
// Handle chained method calls, for example `return sb.append("text")`.
return Optional.ofNullable(parent.parent())
.filter(UnusedStringBuilderCheck::isConsumed)
.isPresent();
}
} else if (parent instanceof ReturnStatementTree || parent instanceof ArgumentListTreeImpl) {
return true;
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* SonarQube Java
* Copyright (C) 2012-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* 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 Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.java.checks.unused;

import org.junit.jupiter.api.Test;
import org.sonar.java.checks.verifier.CheckVerifier;

import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;

class UnusedStringBuilderCheckTest {
@Test
void test() {
CheckVerifier.newVerifier()
.onFile(mainCodeSourcesPath("checks/unused/UnusedStringBuilderCheckSample.java"))
.withCheck(new UnusedStringBuilderCheck())
.verifyIssues();
}

@Test
void test_without_semantics() {
CheckVerifier.newVerifier()
.onFile(mainCodeSourcesPath("checks/unused/UnusedStringBuilderCheckSample.java"))
.withCheck(new UnusedStringBuilderCheck())
.withoutSemantic()
.verifyIssues();
}
}
Loading