Skip to content

Commit 381a07c

Browse files
SONARJAVA-5622 extend S7158 to work with all CharSequence not just Srings (#5196)
1 parent 05b764f commit 381a07c

5 files changed

Lines changed: 170 additions & 12 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package java.lang;
2+
3+
public class String {
4+
5+
public boolean isEmpty() {
6+
return true;
7+
}
8+
9+
int length() {
10+
return 0;
11+
}
12+
13+
public void samples(){
14+
boolean b = length() > 0; // Noncompliant
15+
}
16+
}
17+
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package checks;
2+
3+
import java.nio.CharBuffer;
4+
5+
public class CharSequenceIsEmptyCheckSample {
6+
7+
public boolean testStringBuilder(StringBuilder sb1, StringBuilder sb2) {
8+
boolean b;
9+
10+
b = sb1.length() == 0; // Noncompliant {{Use "isEmpty()" to check whether a "StringBuilder" is empty or not.}}
11+
// ^^^^^^^^^^^^^^^^^
12+
b = sb2.length() <= 0; // Noncompliant [[quickfixes=qf2]]
13+
// fix@qf2 {{Replace with "isEmpty()"}}
14+
// edit@qf2 [[sc=13;ec=26]]{{isEmpty()}}
15+
16+
17+
b = 0 >= sb1.length(); // Noncompliant
18+
b = 1 > sb2.length(); // Noncompliant
19+
20+
return b;
21+
}
22+
23+
24+
public boolean testStringBuffer(StringBuffer buf1, StringBuffer buf2) {
25+
boolean b;
26+
27+
b = buf1.length() == 0; // Noncompliant {{Use "isEmpty()" to check whether a "StringBuffer" is empty or not.}}
28+
// ^^^^^^^^^^^^^^^^^^
29+
b = buf2.length() < 1; // Noncompliant [[quickfixes=qf3]]
30+
// fix@qf3 {{Replace with "isEmpty()"}}
31+
// edit@qf3 [[sc=14;ec=26]]{{isEmpty()}}
32+
33+
34+
b = 0 != buf1.length(); // Noncompliant
35+
b = 1 <= buf2.length(); // Noncompliant
36+
37+
return b;
38+
}
39+
40+
public boolean testCharBuffer(CharBuffer cb1, CharBuffer cb2) {
41+
boolean b;
42+
43+
b = cb1.length() == 0; // Noncompliant {{Use "isEmpty()" to check whether a "CharBuffer" is empty or not.}}
44+
// ^^^^^^^^^^^^^^^^^
45+
b = cb2.length() > 0; // Noncompliant [[quickfixes=qf4]]
46+
// fix@qf4 {{Replace with "isEmpty()"}}
47+
// edit@qf4 [[sc=9;ec=9]]{{!}}
48+
// edit@qf4 [[sc=13;ec=25]]{{isEmpty()}}
49+
50+
51+
b = 0 == cb1.length(); // Noncompliant
52+
b = 0 < cb2.length(); // Noncompliant
53+
54+
return b;
55+
}
56+
57+
58+
59+
public boolean testCharSequence(CharSequence cs1, CharSequence cs2) {
60+
boolean b;
61+
62+
b = cs1.length() == 0; // Noncompliant {{Use "isEmpty()" to check whether a "CharSequence" is empty or not.}}
63+
// ^^^^^^^^^^^^^^^^^
64+
b = cs2.length() >= 1; // Noncompliant [[quickfixes=qf5]]
65+
// fix@qf5 {{Replace with "isEmpty()"}}
66+
// edit@qf5 [[sc=9;ec=9]]{{!}}
67+
// edit@qf5 [[sc=13;ec=26]]{{isEmpty()}}
68+
69+
70+
b = 0 == cs1.length(); // Noncompliant
71+
b = 1 <= cs2.length(); // Noncompliant
72+
73+
// Proper way using isEmpty
74+
b = cs1.isEmpty();
75+
b = !cs2.isEmpty();
76+
77+
return b;
78+
}
79+
80+
public void nonRelated(Foo f){
81+
boolean b = f.length() == 0;
82+
}
83+
84+
static class Foo {
85+
int length(){
86+
return 0;
87+
}
88+
}
89+
}

java-checks-test-sources/default/src/main/java/checks/StringIsEmptyCheckSample.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,6 @@ public boolean sample(String s, String t) {
9696

9797
b = 1 < 0;
9898

99-
// StringBuilder does not have `isEmpty()`
100-
StringBuilder stringBuilder = new StringBuilder();
101-
b = stringBuilder.length() == 0;
10299

103100
return b;
104101
}

java-checks/src/main/java/org/sonar/java/checks/StringIsEmptyCheck.java

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,30 @@
3232
import org.sonar.plugins.java.api.tree.BinaryExpressionTree;
3333
import org.sonar.plugins.java.api.tree.ExpressionTree;
3434
import org.sonar.plugins.java.api.tree.IdentifierTree;
35+
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
3536
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
3637
import org.sonar.plugins.java.api.tree.Tree;
3738
import org.sonar.plugins.java.api.tree.Tree.Kind;
3839

3940
/**
40-
* Implement rule that <pre>String.length() == 0</pre> should be replaced with
41-
* <pre>String.isEmpty()</pre>.
41+
* Implement rule that <pre>CharSequence.length() == 0</pre> should be replaced with
42+
* <pre>CharSequence.isEmpty()</pre>.
4243
*/
4344
@Rule(key = "S7158")
4445
public class StringIsEmptyCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor {
4546

46-
private static final MethodMatchers LENGTH_METHOD = MethodMatchers.create()
47+
private static final MethodMatchers STRING_LENGTH_METHOD = MethodMatchers.create()
4748
.ofTypes("java.lang.String")
4849
.names("length")
4950
.addWithoutParametersMatcher()
5051
.build();
5152

53+
private static final MethodMatchers CHARSEQUENCE_LENGTH_METHOD = MethodMatchers.create()
54+
.ofSubTypes("java.lang.CharSequence")
55+
.names("length")
56+
.addWithoutParametersMatcher()
57+
.build();
58+
5259
private static final Kind[] TARGETED_BINARY_OPERATOR_TREES = {
5360
Kind.EQUAL_TO,
5461
Kind.NOT_EQUAL_TO,
@@ -71,12 +78,14 @@ private static boolean isEmptinessCheck(ComparisonType comparisonType) {
7178
return comparisonType == ComparisonType.IS_EMPTY || comparisonType == ComparisonType.IS_NOT_EMPTY;
7279
}
7380

74-
// `String.isEmpty()` is available since Java 6.
81+
82+
// `String.isEmpty()` is available since Java 6, but `CharSequence.isEmpty()` since Java 15
7583
@Override
7684
public boolean isCompatibleWithJavaVersion(JavaVersion version) {
7785
return version.isJava6Compatible();
7886
}
7987

88+
8089
@Override
8190
public List<Kind> nodesToVisit() {
8291
return List.of(TARGETED_BINARY_OPERATOR_TREES);
@@ -112,7 +121,7 @@ public void visitNode(Tree tree) {
112121
.newIssue(context)
113122
.forRule(this)
114123
.onTree(tree)
115-
.withMessage("Use isEmpty() to check whether a string is empty or not.")
124+
.withMessage("Use \"isEmpty()\" to check whether a \"" + ownerName(lengthCall) + "\" is empty or not.")
116125
.withQuickFix(() -> getQuickFix(tree, lengthCall, comparisonType))
117126
.report();
118127
}
@@ -144,10 +153,20 @@ private static JavaQuickFix getQuickFix(Tree comparisonExpression, MethodInvocat
144153
return builder.build();
145154
}
146155

156+
private static String ownerName(MethodInvocationTree lengthCall) {
157+
if (lengthCall.methodSelect() instanceof MemberSelectExpressionTree sel) {
158+
return sel.expression().symbolType().name();
159+
} else {
160+
return lengthCall.methodSymbol().owner().name();
161+
}
162+
}
163+
147164
@Nullable
148-
private static MethodInvocationTree getLengthCall(ExpressionTree tree) {
149-
if (tree instanceof MethodInvocationTree mit && LENGTH_METHOD.matches(mit)) {
150-
return mit;
165+
private MethodInvocationTree getLengthCall(ExpressionTree tree) {
166+
if (tree instanceof MethodInvocationTree mit) {
167+
if (STRING_LENGTH_METHOD.matches(mit) || (context.getJavaVersion().isJava15Compatible() && CHARSEQUENCE_LENGTH_METHOD.matches(mit))) {
168+
return mit;
169+
}
151170
}
152171
return null;
153172
}

java-checks/src/test/java/org/sonar/java/checks/StringIsEmptyCheckTest.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,45 @@
2020
import org.sonar.java.checks.verifier.CheckVerifier;
2121

2222
import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;
23+
import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath;
2324

2425
class StringIsEmptyCheckTest {
2526

2627
@Test
27-
void test() {
28+
void string() {
2829
CheckVerifier.newVerifier()
2930
.onFile(mainCodeSourcesPath("checks/StringIsEmptyCheckSample.java"))
3031
.withCheck(new StringIsEmptyCheck())
3132
.verifyIssues();
3233
}
3334

35+
@Test
36+
void string_15() {
37+
CheckVerifier.newVerifier()
38+
.onFile(mainCodeSourcesPath("checks/StringIsEmptyCheckSample.java"))
39+
.withCheck(new StringIsEmptyCheck())
40+
.withJavaVersion(15)
41+
.verifyIssues();
42+
}
43+
44+
@Test
45+
void charSequence_15() {
46+
CheckVerifier.newVerifier()
47+
.onFile(mainCodeSourcesPath("checks/CharSequenceIsEmptyCheckSample.java"))
48+
.withCheck(new StringIsEmptyCheck())
49+
.withJavaVersion(15)
50+
.verifyIssues();
51+
}
52+
53+
@Test
54+
void charSequence_8() {
55+
CheckVerifier.newVerifier()
56+
.onFile(mainCodeSourcesPath("checks/CharSequenceIsEmptyCheckSample.java"))
57+
.withCheck(new StringIsEmptyCheck())
58+
.withJavaVersion(8)
59+
.verifyNoIssues();
60+
}
61+
3462
@Test
3563
void testOlderJavaVersion() {
3664
CheckVerifier.newVerifier()
@@ -39,4 +67,12 @@ void testOlderJavaVersion() {
3967
.withJavaVersion(5)
4068
.verifyNoIssues();
4169
}
70+
71+
@Test
72+
void without_explicit_receiver() {
73+
CheckVerifier.newVerifier()
74+
.onFile(nonCompilingTestSourcesPath("java/lang/String.java"))
75+
.withCheck(new StringIsEmptyCheck())
76+
.verifyIssues();
77+
}
4278
}

0 commit comments

Comments
 (0)