1
2
3
4
5 package com.qulice.checkstyle;
6
7 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8 import com.puppycrawl.tools.checkstyle.api.DetailAST;
9 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
10 import java.util.Optional;
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 public final class SimpleStringSplitCheck extends AbstractCheck {
49
50
51
52
53 public SimpleStringSplitCheck() {
54
55 }
56
57 @Override
58 public int[] getDefaultTokens() {
59 return new int[] {TokenTypes.METHOD_CALL};
60 }
61
62 @Override
63 public int[] getAcceptableTokens() {
64 return this.getDefaultTokens();
65 }
66
67 @Override
68 public int[] getRequiredTokens() {
69 return this.getDefaultTokens();
70 }
71
72 @Override
73 public void visitToken(final DetailAST ast) {
74 final Optional<DetailAST> literal =
75 SimpleStringSplitCheck.regexLiteral(ast);
76 final Optional<String> regex = literal.flatMap(
77 node -> SimpleStringSplitCheck.decode(node.getText())
78 );
79 if (regex.isPresent()
80 && !SimpleStringSplitCheck.optimized(regex.get())) {
81 this.log(
82 literal.get(),
83 "String.split regex is not JDK-optimized, use a precompiled java.util.regex.Pattern"
84 );
85 }
86 }
87
88 private static Optional<DetailAST> regexLiteral(final DetailAST call) {
89 Optional<DetailAST> result = Optional.empty();
90 if (SimpleStringSplitCheck.isSplitCall(call)) {
91 result = SimpleStringSplitCheck.firstLiteralArg(call);
92 }
93 return result;
94 }
95
96 private static boolean isSplitCall(final DetailAST call) {
97 final DetailAST dot = call.getFirstChild();
98 final boolean result;
99 if (dot == null || dot.getType() != TokenTypes.DOT) {
100 result = false;
101 } else {
102 final DetailAST name = dot.getLastChild();
103 result = name != null && name.getType() == TokenTypes.IDENT
104 && "split".equals(name.getText());
105 }
106 return result;
107 }
108
109 private static Optional<DetailAST> firstLiteralArg(final DetailAST call) {
110 final DetailAST elist = call.findFirstToken(TokenTypes.ELIST);
111 Optional<DetailAST> result = Optional.empty();
112 if (elist != null && SimpleStringSplitCheck.isOneOrTwoArgs(elist)) {
113 final DetailAST expr = elist.findFirstToken(TokenTypes.EXPR);
114 if (expr != null && SimpleStringSplitCheck.isLoneStringLiteral(expr)) {
115 result = Optional.of(expr.getFirstChild());
116 }
117 }
118 return result;
119 }
120
121 private static boolean isOneOrTwoArgs(final DetailAST elist) {
122 final int args = elist.getChildCount(TokenTypes.EXPR);
123 return args == 1 || args == 2;
124 }
125
126 private static boolean isLoneStringLiteral(final DetailAST expr) {
127 return expr.getChildCount() == 1
128 && expr.getFirstChild().getType() == TokenTypes.STRING_LITERAL;
129 }
130
131 private static boolean optimized(final String regex) {
132 final boolean result;
133 final int len = regex.length();
134 if (len == 1) {
135 result = ".$|()[{^?*+\\".indexOf(regex.charAt(0)) < 0;
136 } else if (len == 2 && regex.charAt(0) == '\\') {
137 result = !SimpleStringSplitCheck.isAsciiAlphanumeric(regex.charAt(1));
138 } else {
139 result = false;
140 }
141 return result;
142 }
143
144 private static boolean isAsciiAlphanumeric(final char chr) {
145 return SimpleStringSplitCheck.isAsciiDigit(chr)
146 || SimpleStringSplitCheck.isAsciiLetter(chr);
147 }
148
149 private static boolean isAsciiDigit(final char chr) {
150 return chr >= '0' && chr <= '9';
151 }
152
153 private static boolean isAsciiLetter(final char chr) {
154 return chr >= 'a' && chr <= 'z'
155 || chr >= 'A' && chr <= 'Z';
156 }
157
158 private static Optional<String> decode(final String text) {
159 final String body = text.substring(1, text.length() - 1);
160 final StringBuilder out = new StringBuilder(body.length());
161 int idx = 0;
162 boolean failed = false;
163 while (idx < body.length() && !failed) {
164 final int advance = SimpleStringSplitCheck.step(body, idx, out);
165 if (advance < 0) {
166 failed = true;
167 } else {
168 idx += advance;
169 }
170 }
171 final Optional<String> result;
172 if (failed) {
173 result = Optional.empty();
174 } else {
175 result = Optional.of(out.toString());
176 }
177 return result;
178 }
179
180 private static int step(
181 final String body, final int idx, final StringBuilder out
182 ) {
183 final char chr = body.charAt(idx);
184 final int advance;
185 if (chr == '\\') {
186 advance = SimpleStringSplitCheck.handleEscape(body, idx, out);
187 } else {
188 out.append(chr);
189 advance = 1;
190 }
191 return advance;
192 }
193
194 private static int handleEscape(
195 final String body, final int idx, final StringBuilder out
196 ) {
197 final int advance;
198 if (idx + 1 >= body.length()) {
199 advance = -1;
200 } else {
201 final int decoded = SimpleStringSplitCheck.escape(
202 body.charAt(idx + 1)
203 );
204 if (decoded < 0) {
205 advance = -1;
206 } else {
207 out.append((char) decoded);
208 advance = 2;
209 }
210 }
211 return advance;
212 }
213
214 private static int escape(final char chr) {
215 return switch (chr) {
216 case 'n' -> '\n';
217 case 't' -> '\t';
218 case 'r' -> '\r';
219 case 'b' -> '\b';
220 case 'f' -> '\f';
221 case 's' -> ' ';
222 case '\'' -> '\'';
223 case '"' -> '"';
224 case '\\' -> '\\';
225 default -> -1;
226 };
227 }
228 }