Skip to content

Commit 6a63b57

Browse files
cushonError Prone Team
authored andcommitted
Discourage looping over the result of toCharArray()
PiperOrigin-RevId: 387181359
1 parent 4e91cb4 commit 6a63b57

File tree

4 files changed

+197
-0
lines changed

4 files changed

+197
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2021 The Error Prone Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.errorprone.bugpatterns;
18+
19+
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
20+
import static com.google.errorprone.matchers.Description.NO_MATCH;
21+
import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod;
22+
import static com.google.errorprone.util.ASTHelpers.getStartPosition;
23+
24+
import com.google.errorprone.BugPattern;
25+
import com.google.errorprone.VisitorState;
26+
import com.google.errorprone.fixes.SuggestedFix;
27+
import com.google.errorprone.matchers.Description;
28+
import com.google.errorprone.matchers.Matcher;
29+
import com.google.errorprone.util.ASTHelpers;
30+
import com.sun.source.tree.BlockTree;
31+
import com.sun.source.tree.EnhancedForLoopTree;
32+
import com.sun.source.tree.ExpressionTree;
33+
import com.sun.source.tree.IdentifierTree;
34+
import com.sun.source.tree.StatementTree;
35+
import com.sun.source.tree.Tree;
36+
import com.sun.source.util.TreeScanner;
37+
import java.util.List;
38+
39+
/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
40+
@BugPattern(
41+
name = "LoopOverCharArray",
42+
summary = "toCharArray allocates a new array, using charAt is more efficient",
43+
severity = WARNING)
44+
public class LoopOverCharArray extends BugChecker implements BugChecker.EnhancedForLoopTreeMatcher {
45+
46+
private static final Matcher<ExpressionTree> TO_CHAR_ARRAY =
47+
instanceMethod().onExactClass("java.lang.String").named("toCharArray");
48+
49+
@Override
50+
public Description matchEnhancedForLoop(EnhancedForLoopTree tree, VisitorState state) {
51+
if (!TO_CHAR_ARRAY.matches(tree.getExpression(), state)) {
52+
return NO_MATCH;
53+
}
54+
ExpressionTree receiver = ASTHelpers.getReceiver(tree.getExpression());
55+
if (!(receiver instanceof IdentifierTree)) {
56+
return NO_MATCH;
57+
}
58+
StatementTree body = tree.getStatement();
59+
if (!(body instanceof BlockTree)) {
60+
return NO_MATCH;
61+
}
62+
List<? extends StatementTree> statements = ((BlockTree) body).getStatements();
63+
if (statements.isEmpty()) {
64+
return NO_MATCH;
65+
}
66+
Description.Builder description = buildDescription(tree);
67+
// The fix uses `i` as the loop index variable, so give up if there's already an `i` in scope
68+
if (!alreadyDefinesIdentifier(tree)) {
69+
description.addFix(
70+
SuggestedFix.replace(
71+
getStartPosition(tree),
72+
getStartPosition(statements.get(0)),
73+
String.format(
74+
"for (int i = 0; i < %s.length(); i++) { char %s = %s.charAt(i);",
75+
state.getSourceForNode(receiver),
76+
tree.getVariable().getName(),
77+
state.getSourceForNode(receiver))));
78+
}
79+
return description.build();
80+
}
81+
82+
private static boolean alreadyDefinesIdentifier(Tree tree) {
83+
boolean[] result = {false};
84+
new TreeScanner<Void, Void>() {
85+
@Override
86+
public Void visitIdentifier(IdentifierTree node, Void unused) {
87+
if (node.getName().contentEquals("i")) {
88+
result[0] = true;
89+
}
90+
return super.visitIdentifier(node, unused);
91+
}
92+
}.scan(tree, null);
93+
return result[0];
94+
}
95+
}

core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@
195195
import com.google.errorprone.bugpatterns.LongFloatConversion;
196196
import com.google.errorprone.bugpatterns.LongLiteralLowerCaseSuffix;
197197
import com.google.errorprone.bugpatterns.LoopConditionChecker;
198+
import com.google.errorprone.bugpatterns.LoopOverCharArray;
198199
import com.google.errorprone.bugpatterns.LossyPrimitiveCompare;
199200
import com.google.errorprone.bugpatterns.MathAbsoluteRandom;
200201
import com.google.errorprone.bugpatterns.MathRoundIntLong;
@@ -852,6 +853,7 @@ public static ScannerSupplier errorChecks() {
852853
LockOnBoxedPrimitive.class,
853854
LogicalAssignment.class,
854855
LongFloatConversion.class,
856+
LoopOverCharArray.class,
855857
MathAbsoluteRandom.class,
856858
MissingCasesInEnumSwitch.class,
857859
MissingFail.class,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2021 The Error Prone Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.errorprone.bugpatterns;
18+
19+
import com.google.errorprone.BugCheckerRefactoringTestHelper;
20+
import org.junit.Test;
21+
import org.junit.runner.RunWith;
22+
import org.junit.runners.JUnit4;
23+
24+
/** {@link LoopOverCharArray}Test */
25+
@RunWith(JUnit4.class)
26+
public class LoopOverCharArrayTest {
27+
@Test
28+
public void refactor() {
29+
BugCheckerRefactoringTestHelper.newInstance(LoopOverCharArray.class, getClass())
30+
.addInputLines(
31+
"Test.java",
32+
"class T {",
33+
" void f(String s) {",
34+
" for (char c : s.toCharArray()) {",
35+
" System.err.print(c);",
36+
" }",
37+
" for (char i : s.toCharArray()) {",
38+
" System.err.print(i);",
39+
" }",
40+
" }",
41+
"}")
42+
.addOutputLines(
43+
"Test.java",
44+
"class T {",
45+
" void f(String s) {",
46+
" for (int i = 0; i < s.length(); i++) {",
47+
" char c = s.charAt(i);",
48+
" System.err.print(c);",
49+
" }",
50+
" for (char i : s.toCharArray()) {",
51+
" System.err.print(i);",
52+
" }",
53+
" }",
54+
"}")
55+
.doTest();
56+
}
57+
}

docs/bugpattern/LoopOverCharArray.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
[`String#toCharArray`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#toCharArray\(\))
2+
allocates a new array. Calling `charAt` is more efficient, because it avoids
3+
creating a new array with a copy of the character data.
4+
5+
That is, prefer this:
6+
7+
```java
8+
boolean isDigits(String string) {
9+
for (int i = 0; i < s.length(); i++) {
10+
char c = s.charAt(i);
11+
if (!Character.isDigit(c)) {
12+
return false;
13+
}
14+
}
15+
return true;
16+
}
17+
```
18+
19+
to this:
20+
21+
```java
22+
boolean isDigits(String string) {
23+
// this allocates a new char[]
24+
for (char c : string.toCharArray()) {
25+
if (!Character.isDigit(c)) {
26+
return false;
27+
}
28+
}
29+
return true;
30+
}
31+
```
32+
33+
Note that many loops over characters can be expressed using streams with
34+
[`String#chars`][chars] or [`String#codePoints`][codePoints], for example:
35+
36+
```java
37+
boolean isDigits(String string) {
38+
string.codePoints().allMatch(Character::isDigit);
39+
}
40+
```
41+
42+
[chars]: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#chars()
43+
[codePoints]: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#codePoints()

0 commit comments

Comments
 (0)