Skip to content

Commit c032cef

Browse files
SONARJAVA-5602 implement UseIsEmptyToTestEmptinessOfStringBuilder (#5188)
1 parent e5ac95c commit c032cef

8 files changed

Lines changed: 566 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ruleKey": "S3033",
3+
"hasTruePositives": true,
4+
"falseNegatives": 0,
5+
"falsePositives": 0
6+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package java.lang;
2+
3+
public class StringBuilder {
4+
@Override
5+
public String toString() {
6+
return "";
7+
}
8+
9+
public boolean isEmpty() {
10+
return true;
11+
}
12+
13+
public void samples(){
14+
boolean b = toString().isEmpty(); // FN, we do not raise inside StringBuilder or StringBuffer
15+
}
16+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package checks;
2+
3+
public class UseIsEmptyToTestEmptinessOfStringBuilderCheckSample {
4+
5+
void noncompliantStringBuilder() {
6+
StringBuilder sb = new StringBuilder();
7+
8+
if ("".equals(sb.toString())) { // Noncompliant {{Replace "equals()" with "isEmpty()".}}
9+
// ^^^^^^^^^^^^^^^^^^^^^^^^
10+
System.out.println("Empty StringBuilder");
11+
}
12+
13+
if (sb.toString().isEmpty()) { // Noncompliant {{Replace "toString().isEmpty()" with "isEmpty()".}} [[quickfixes=qf1]]
14+
// fix@qf1 {{Replace with "isEmpty()"}}
15+
// edit@qf1 [[sc=11;ec=32]] {{.isEmpty()}}
16+
System.out.println("Empty StringBuilder");
17+
}
18+
19+
if (sb.toString().length() == 0) { // Noncompliant {{Replace "toString().length()" with "isEmpty()".}}
20+
// ^^^^^^^^^^^^^^^^^^^^^^
21+
System.out.println("Empty StringBuilder");
22+
}
23+
24+
25+
if (sb.toString().length() > 0) { // FN
26+
}
27+
28+
if (sb.toString().length() < 1) { // FN
29+
}
30+
31+
if (!"".equals(sb.toString())) { // Noncompliant
32+
System.out.println("Non-empty StringBuilder");
33+
}
34+
35+
boolean inExpression = "".equals(sb.toString()); // Noncompliant
36+
37+
if (sb.toString().equals("")) { // Noncompliant {{Replace "equals()" with "isEmpty()".}}
38+
// ^^^^^^^^^^^^^^^^^^^^^^^^
39+
System.out.println("Empty StringBuilder using equals reversed");
40+
}
41+
42+
if (!sb.toString().equals("")) { // Noncompliant
43+
System.out.println("Non-empty StringBuilder using equals reversed");
44+
}
45+
46+
}
47+
48+
void quickFix(){
49+
StringBuilder sb = new StringBuilder();
50+
var x = sb.toString().isEmpty() && false; // Noncompliant [[quickfixes=qf2]]
51+
// fix@qf2 {{Replace with "isEmpty()"}}
52+
// edit@qf2 [[sc=15;ec=36]] {{.isEmpty()}}
53+
54+
}
55+
56+
void consume(String s) {
57+
}
58+
59+
void noncompliantStringBuffer() {
60+
StringBuffer sb = new StringBuffer();
61+
62+
if ("".equals(sb.toString())) { // Noncompliant
63+
System.out.println("Empty StringBuffer");
64+
}
65+
66+
if (sb.toString().isEmpty()) { // Noncompliant
67+
System.out.println("Empty StringBuffer");
68+
}
69+
}
70+
71+
void compliantStringBuilder() {
72+
StringBuilder sb = new StringBuilder();
73+
74+
if (sb.isEmpty()) { // Compliant
75+
System.out.println("Empty StringBuilder");
76+
}
77+
78+
if (!sb.isEmpty()) { // Compliant
79+
System.out.println("Non-empty StringBuilder");
80+
}
81+
82+
boolean expression = sb.isEmpty(); // Compliant
83+
84+
if (sb.length() == 0) { // Compliant
85+
System.out.println("Empty StringBuilder");
86+
}
87+
88+
boolean isName = "name".equals(sb.toString());
89+
boolean singleLetter = sb.toString().length() == 1;
90+
boolean greaterThan = sb.toString().length() > 1;
91+
String substring = sb.toString().substring(0, 1);
92+
93+
String sbString = sb.toString();
94+
if ("".equals(sbString)) { // Compliant, we do not support variables assignment
95+
System.out.println("Empty string");
96+
}
97+
98+
if (sb.isEmpty()) { // Compliant, we do not support variables assignment
99+
System.out.println("Empty string");
100+
}
101+
}
102+
103+
void compliantStringBuffer() {
104+
StringBuffer sb = new StringBuffer();
105+
106+
if (sb.isEmpty()) { // Compliant
107+
System.out.println("Empty StringBuffer");
108+
}
109+
110+
if (!sb.isEmpty()) { // Compliant
111+
System.out.println("Non-empty StringBuffer");
112+
}
113+
}
114+
115+
void complexExpressions() {
116+
if (getStringBuilder().toString().isEmpty()) { // Noncompliant
117+
System.out.println("Empty");
118+
}
119+
120+
if ("".equals(getStringBuilder().toString())) { // Noncompliant
121+
System.out.println("Empty");
122+
}
123+
124+
StringBuilder sb = new StringBuilder();
125+
126+
if (sb.append("hello").toString().isEmpty()) { // Noncompliant
127+
System.out.println("Empty after append");
128+
}
129+
130+
if ((1 == 2 ? sb : new StringBuilder()).toString().isEmpty()) { // Noncompliant
131+
System.out.println("Empty after append");
132+
}
133+
134+
if (getStringBuilder().isEmpty()) { // Compliant
135+
System.out.println("Empty");
136+
}
137+
}
138+
139+
StringBuilder getStringBuilder() {
140+
return new StringBuilder("test");
141+
}
142+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.checks;
18+
19+
import java.util.List;
20+
import java.util.Optional;
21+
import org.sonar.check.Rule;
22+
import org.sonar.java.ast.parser.ArgumentListTreeImpl;
23+
import org.sonar.java.checks.helpers.QuickFixHelper;
24+
import org.sonar.java.checks.methods.AbstractMethodDetection;
25+
import org.sonar.java.model.LiteralUtils;
26+
import org.sonar.java.reporting.JavaQuickFix;
27+
import org.sonar.java.reporting.JavaTextEdit;
28+
import org.sonar.plugins.java.api.JavaVersion;
29+
import org.sonar.plugins.java.api.JavaVersionAwareVisitor;
30+
import org.sonar.plugins.java.api.semantic.MethodMatchers;
31+
import org.sonar.plugins.java.api.tree.BinaryExpressionTree;
32+
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
33+
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
34+
import org.sonar.plugins.java.api.tree.Tree;
35+
36+
import static org.sonar.java.model.LiteralUtils.isEmptyString;
37+
38+
@Rule(key = "S3033")
39+
public class UseIsEmptyToTestEmptinessOfStringBuilderCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor {
40+
private static final String JAVA_LANG_STRING = "java.lang.String";
41+
42+
private static final MethodMatchers TO_STRING = MethodMatchers.create()
43+
.ofTypes("java.lang.StringBuilder", "java.lang.StringBuffer")
44+
.names("toString")
45+
.withAnyParameters()
46+
.build();
47+
private static final MethodMatchers STRING_EQUALS = MethodMatchers.create()
48+
.ofTypes(JAVA_LANG_STRING)
49+
.names("equals")
50+
.withAnyParameters()
51+
.build();
52+
private static final MethodMatchers STRING_IS_EMPTY = MethodMatchers.create()
53+
.ofTypes(JAVA_LANG_STRING)
54+
.names("isEmpty")
55+
.withAnyParameters()
56+
.build();
57+
private static final MethodMatchers STRING_LENGTH = MethodMatchers.create()
58+
.ofTypes(JAVA_LANG_STRING)
59+
.names("length")
60+
.withAnyParameters()
61+
.build();
62+
63+
private static final String MESSAGE_EQUALS = "Replace \"equals()\" with \"isEmpty()\".";
64+
private static final String MESSAGE_IS_EMPTY = "Replace \"toString().isEmpty()\" with \"isEmpty()\".";
65+
private static final String MESSAGE_LENGTH = "Replace \"toString().length()\" with \"isEmpty()\".";
66+
67+
@Override
68+
protected MethodMatchers getMethodInvocationMatchers() {
69+
return TO_STRING;
70+
}
71+
72+
@Override
73+
public boolean isCompatibleWithJavaVersion(JavaVersion version) {
74+
return version.isJava15Compatible();
75+
}
76+
77+
@Override
78+
protected void onMethodInvocationFound(MethodInvocationTree toStringInvocation) {
79+
MethodInvocationTree mit = argumentSide(toStringInvocation)
80+
.or(() -> methodSelectSide(toStringInvocation))
81+
.orElse(null);
82+
83+
if (mit == null) {
84+
return;
85+
}
86+
87+
if (STRING_EQUALS.matches(mit) && isEqualsWithEmptyString(mit)) {
88+
reportIssue(mit, MESSAGE_EQUALS);
89+
} else if (STRING_IS_EMPTY.matches(mit) && toStringInvocation.methodSelect() instanceof MemberSelectExpressionTree sel) {
90+
var operator = sel.operatorToken();
91+
var edit = JavaTextEdit.replaceBetweenTree(operator, mit, ".isEmpty()");
92+
93+
QuickFixHelper.newIssue(context)
94+
.forRule(this)
95+
.onTree(mit)
96+
.withMessage(MESSAGE_IS_EMPTY)
97+
.withQuickFixes(() -> List.of(
98+
JavaQuickFix.newQuickFix("Replace with \"isEmpty()\"").addTextEdit(edit).build()
99+
))
100+
.report();
101+
} else if (STRING_LENGTH.matches(mit) && isComparedToZero(mit)) {
102+
reportIssue(mit, MESSAGE_LENGTH);
103+
}
104+
}
105+
106+
// example: "".equals(sb.toString()) and toStringInvocation=sb.toString() -> "".equals(sb.toString())
107+
private static Optional<MethodInvocationTree> argumentSide(MethodInvocationTree toStringInvocation) {
108+
return Optional.ofNullable(toStringInvocation.parent())
109+
.filter(ArgumentListTreeImpl.class::isInstance)
110+
.map(ArgumentListTreeImpl.class::cast)
111+
.map(ArgumentListTreeImpl::parent)
112+
.filter(MethodInvocationTree.class::isInstance)
113+
.map(MethodInvocationTree.class::cast);
114+
}
115+
116+
// example: sb.toString().equals("") and toStringInvocation=sb.toString() -> sb.toString().equals("")
117+
private static Optional<MethodInvocationTree> methodSelectSide(MethodInvocationTree toStringInvocation) {
118+
return Optional.ofNullable(toStringInvocation.parent())
119+
.filter(MemberSelectExpressionTree.class::isInstance)
120+
.map(MemberSelectExpressionTree.class::cast)
121+
.map(MemberSelectExpressionTree::parent)
122+
.filter(MethodInvocationTree.class::isInstance)
123+
.map(MethodInvocationTree.class::cast);
124+
}
125+
126+
private static boolean isEqualsWithEmptyString(MethodInvocationTree equalsInvocation) {
127+
Tree arg = equalsInvocation.arguments().get(0);
128+
129+
return isEmptyString(arg) ||
130+
(equalsInvocation.methodSelect() instanceof MemberSelectExpressionTree sel && isEmptyString(sel.expression()));
131+
}
132+
133+
private static boolean isComparedToZero(MethodInvocationTree lengthInvocation) {
134+
Tree parent = lengthInvocation.parent();
135+
if (parent != null && parent.is(Tree.Kind.EQUAL_TO, Tree.Kind.NOT_EQUAL_TO)) {
136+
BinaryExpressionTree binary = (BinaryExpressionTree) parent;
137+
return LiteralUtils.isZero(binary.rightOperand()) || LiteralUtils.isZero(binary.leftOperand());
138+
}
139+
return false;
140+
}
141+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.java.checks.verifier.CheckVerifier;
21+
22+
import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;
23+
import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath;
24+
25+
class UseIsEmptyToTestEmptinessOfStringBuilderCheckTest {
26+
27+
@Test
28+
void test() {
29+
CheckVerifier.newVerifier()
30+
.onFile(mainCodeSourcesPath("checks/UseIsEmptyToTestEmptinessOfStringBuilderCheckSample.java"))
31+
.withCheck(new UseIsEmptyToTestEmptinessOfStringBuilderCheck())
32+
.withJavaVersion(15)
33+
.verifyIssues();
34+
}
35+
36+
@Test
37+
void string_builder() {
38+
CheckVerifier.newVerifier()
39+
.onFile(nonCompilingTestSourcesPath("java/lang/StringBuilder.java"))
40+
.withCheck(new UseIsEmptyToTestEmptinessOfStringBuilderCheck())
41+
.withJavaVersion(15)
42+
.verifyNoIssues();
43+
}
44+
45+
@Test
46+
void no_semantic() {
47+
CheckVerifier.newVerifier()
48+
.onFile(mainCodeSourcesPath("checks/UseIsEmptyToTestEmptinessOfStringBuilderCheckSample.java"))
49+
.withCheck(new UseIsEmptyToTestEmptinessOfStringBuilderCheck())
50+
.withoutSemantic()
51+
.withJavaVersion(15)
52+
.verifyIssues();
53+
}
54+
55+
@Test
56+
void below_java_15() {
57+
CheckVerifier.newVerifier()
58+
.onFile(mainCodeSourcesPath("checks/UseIsEmptyToTestEmptinessOfStringBuilderCheckSample.java"))
59+
.withCheck(new UseIsEmptyToTestEmptinessOfStringBuilderCheck())
60+
.withJavaVersion(14)
61+
.verifyNoIssues();
62+
}
63+
64+
}

0 commit comments

Comments
 (0)