1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle.checks;
21
22 import java.util.ArrayList;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
30 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
31 import com.puppycrawl.tools.checkstyle.api.DetailAST;
32 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
33 import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 @FileStatefulCheck
51 public class AvoidEscapedUnicodeCharactersCheck
52 extends AbstractCheck {
53
54
55
56
57
58 public static final String MSG_KEY = "forbid.escaped.unicode.char";
59
60
61 private static final Pattern UNICODE_REGEXP = Pattern.compile("\\\\u+[a-fA-F\\d]{4}");
62
63
64
65
66
67
68
69 private static final Pattern UNICODE_CONTROL = Pattern.compile("\\\\u+"
70 + "(00[0-1][\\dA-Fa-f]"
71 + "|00[8-9][\\dA-Fa-f]"
72 + "|00[aA][dD]"
73 + "|034[fF]"
74 + "|070[fF]"
75 + "|180[eE]"
76 + "|200[b-fB-F]"
77 + "|202[a-eA-E]"
78 + "|206[0-4a-fA-F]"
79 + "|[fF]{3}[9a-bA-B]"
80 + "|[fF][eE][fF]{2})");
81
82
83
84
85
86
87 private static final Pattern ALL_ESCAPED_CHARS = Pattern.compile("^("
88 + UNICODE_REGEXP.pattern()
89 + "|\""
90 + "|'"
91 + "|\\\\"
92 + "|\\\\b"
93 + "|\\\\f"
94 + "|\\\\n"
95 + "|\\R"
96 + "|\\\\r"
97 + "|\\\\s"
98 + "|\\\\t"
99 + ")+$");
100
101
102 private static final Pattern ESCAPED_BACKSLASH = Pattern.compile("\\\\\\\\");
103
104
105 private static final Pattern NON_PRINTABLE_CHARS = Pattern.compile("\\\\u0000"
106 + "|\\\\u0009"
107 + "|\\\\u000[bB]"
108 + "|\\\\u000[cC]"
109 + "|\\\\u0020"
110 + "|\\\\u007[fF]"
111 + "|\\\\u0085"
112 + "|\\\\u009[fF]"
113 + "|\\\\u00[aA]0"
114 + "|\\\\u00[aA][dD]"
115 + "|\\\\u04[fF]9"
116 + "|\\\\u05[bB][eE]"
117 + "|\\\\u05[dD]0"
118 + "|\\\\u05[eE][aA]"
119 + "|\\\\u05[fF]3"
120 + "|\\\\u05[fF]4"
121 + "|\\\\u0600"
122 + "|\\\\u0604"
123 + "|\\\\u061[cC]"
124 + "|\\\\u06[dD]{2}"
125 + "|\\\\u06[fF]{2}"
126 + "|\\\\u070[fF]"
127 + "|\\\\u0750"
128 + "|\\\\u077[fF]"
129 + "|\\\\u0[eE]00"
130 + "|\\\\u0[eE]7[fF]"
131 + "|\\\\u1680"
132 + "|\\\\u180[eE]"
133 + "|\\\\u1[eE]00"
134 + "|\\\\u2000"
135 + "|\\\\u2001"
136 + "|\\\\u2002"
137 + "|\\\\u2003"
138 + "|\\\\u2004"
139 + "|\\\\u2005"
140 + "|\\\\u2006"
141 + "|\\\\u2007"
142 + "|\\\\u2008"
143 + "|\\\\u2009"
144 + "|\\\\u200[aA]"
145 + "|\\\\u200[fF]"
146 + "|\\\\u2025"
147 + "|\\\\u2028"
148 + "|\\\\u2029"
149 + "|\\\\u202[fF]"
150 + "|\\\\u205[fF]"
151 + "|\\\\u2064"
152 + "|\\\\u2066"
153 + "|\\\\u2067"
154 + "|\\\\u2068"
155 + "|\\\\u2069"
156 + "|\\\\u206[aA]"
157 + "|\\\\u206[fF]"
158 + "|\\\\u20[aA][fF]"
159 + "|\\\\u2100"
160 + "|\\\\u213[aA]"
161 + "|\\\\u3000"
162 + "|\\\\u[dD]800"
163 + "|\\\\u[fF]8[fF]{2}"
164 + "|\\\\u[fF][bB]50"
165 + "|\\\\u[fF][dD][fF]{2}"
166 + "|\\\\u[fF][eE]70"
167 + "|\\\\u[fF][eE][fF]{2}"
168 + "|\\\\u[fF]{2}0[eE]"
169 + "|\\\\u[fF]{2}61"
170 + "|\\\\u[fF]{2}[dD][cC]"
171 + "|\\\\u[fF]{3}9"
172 + "|\\\\u[fF]{3}[aA]"
173 + "|\\\\u[fF]{3}[bB]"
174 + "|\\\\u[fF]{4}");
175
176
177
178
179
180
181 private final Map<Integer, List<DetailAST>> pendingViolations = new HashMap<>();
182
183
184 private boolean allowEscapesForControlCharacters;
185
186
187 private boolean allowByTailComment;
188
189
190 private boolean allowIfAllCharactersEscaped;
191
192
193 private boolean allowNonPrintableEscapes;
194
195
196
197
198 public AvoidEscapedUnicodeCharactersCheck() {
199
200 }
201
202
203
204
205
206
207
208 public final void setAllowEscapesForControlCharacters(boolean allow) {
209 allowEscapesForControlCharacters = allow;
210 }
211
212
213
214
215
216
217
218 public final void setAllowByTailComment(boolean allow) {
219 allowByTailComment = allow;
220 }
221
222
223
224
225
226
227
228 public final void setAllowIfAllCharactersEscaped(boolean allow) {
229 allowIfAllCharactersEscaped = allow;
230 }
231
232
233
234
235
236
237
238 public final void setAllowNonPrintableEscapes(boolean allow) {
239 allowNonPrintableEscapes = allow;
240 }
241
242 @Override
243 public int[] getDefaultTokens() {
244 return getRequiredTokens();
245 }
246
247 @Override
248 public int[] getAcceptableTokens() {
249 return getRequiredTokens();
250 }
251
252 @Override
253 public int[] getRequiredTokens() {
254 return new int[] {
255 TokenTypes.STRING_LITERAL,
256 TokenTypes.CHAR_LITERAL,
257 TokenTypes.TEXT_BLOCK_CONTENT,
258 TokenTypes.SINGLE_LINE_COMMENT,
259 TokenTypes.BLOCK_COMMENT_BEGIN,
260 };
261 }
262
263 @Override
264 public boolean isCommentNodesRequired() {
265 return true;
266 }
267
268 @Override
269 public void beginTree(DetailAST rootAST) {
270 pendingViolations.clear();
271 }
272
273 @Override
274 public void visitToken(DetailAST ast) {
275 if (ast.getType() == TokenTypes.SINGLE_LINE_COMMENT
276 || ast.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) {
277 checkComment(ast);
278 }
279 else {
280 checkLiteral(ast);
281 }
282 }
283
284 @Override
285 public void finishTree(DetailAST rootAST) {
286 for (List<DetailAST> asts : pendingViolations.values()) {
287 for (DetailAST ast : asts) {
288 log(ast, MSG_KEY);
289 }
290 }
291 }
292
293
294
295
296
297
298
299 private void checkLiteral(DetailAST ast) {
300 final String literal =
301 CheckUtil.stripIndentAndInitialNewLineFromTextBlock(ast.getText());
302
303 if (hasUnicodeChar(literal) && !(isAllCharactersEscaped(literal)
304 || allowEscapesForControlCharacters
305 && isOnlyUnicodeValidChars(literal, UNICODE_CONTROL)
306 || allowNonPrintableEscapes
307 && isOnlyUnicodeValidChars(literal, NON_PRINTABLE_CHARS))) {
308
309 if (allowByTailComment) {
310 int lineNo = ast.getLineNo();
311 if (ast.getType() == TokenTypes.TEXT_BLOCK_CONTENT) {
312 lineNo = ast.getNextSibling().getLineNo();
313 }
314 pendingViolations.computeIfAbsent(lineNo, key -> new ArrayList<>()).add(ast);
315 }
316 else {
317 log(ast, MSG_KEY);
318 }
319 }
320 }
321
322
323
324
325
326
327 private void checkComment(DetailAST comment) {
328 if (isTrailingComment(comment)) {
329 pendingViolations.remove(comment.getLineNo());
330 }
331 }
332
333
334
335
336
337
338
339 private static boolean isTrailingComment(DetailAST commentNode) {
340 final DetailAST nextSibling = commentNode.getNextSibling();
341 return nextSibling == null || nextSibling.getLineNo() != commentNode.getLineNo();
342 }
343
344
345
346
347
348
349
350 private static boolean hasUnicodeChar(String literal) {
351 final String literalWithoutEscapedBackslashes =
352 ESCAPED_BACKSLASH.matcher(literal).replaceAll("");
353 return UNICODE_REGEXP.matcher(literalWithoutEscapedBackslashes).find();
354 }
355
356
357
358
359
360
361
362
363 private static boolean isOnlyUnicodeValidChars(String literal, Pattern pattern) {
364 final int unicodeMatchesCounter =
365 countMatches(UNICODE_REGEXP, literal);
366 final int unicodeValidMatchesCounter =
367 countMatches(pattern, literal);
368 return unicodeMatchesCounter - unicodeValidMatchesCounter == 0;
369 }
370
371
372
373
374
375
376
377
378 private static int countMatches(Pattern pattern, String target) {
379 int matcherCounter = 0;
380 final Matcher matcher = pattern.matcher(target);
381 while (matcher.find()) {
382 matcherCounter++;
383 }
384 return matcherCounter;
385 }
386
387
388
389
390
391
392
393 private boolean isAllCharactersEscaped(String literal) {
394 return allowIfAllCharactersEscaped
395 && ALL_ESCAPED_CHARS.matcher(literal).find();
396 }
397
398 }