View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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   * <div>
37   * Restricts using
38   * <a href = "https://docs.oracle.com/javase/specs/jls/se11/html/jls-3.html#jls-3.3">
39   * Unicode escapes</a>
40   * (such as &#92;u221e). It is possible to allow using escapes for
41   * <a href="https://en.wiktionary.org/wiki/Appendix:Control_characters">
42   * non-printable, control characters</a>.
43   * Also, this check can be configured to allow using escapes
44   * if trail comment is present. By the option it is possible to
45   * allow using escapes if literal contains only them.
46   * </div>
47   *
48   * @since 5.8
49   */
50  @FileStatefulCheck
51  public class AvoidEscapedUnicodeCharactersCheck
52      extends AbstractCheck {
53  
54      /**
55       * A key is pointing to the warning message text in "messages.properties"
56       * file.
57       */
58      public static final String MSG_KEY = "forbid.escaped.unicode.char";
59  
60      /** Regular expression for Unicode chars. */
61      private static final Pattern UNICODE_REGEXP = Pattern.compile("\\\\u+[a-fA-F\\d]{4}");
62  
63      /**
64       * Regular expression Unicode control characters.
65       *
66       * @see <a href="https://en.wiktionary.org/wiki/Appendix:Control_characters">
67       *     Appendix:Control characters</a>
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       * Regular expression for all escaped chars.
84       * See <a href="https://docs.oracle.com/javase/specs/jls/se15/html/jls-3.html#jls-3.10.7">
85       * EscapeSequence</a>
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     /** Regular expression for escaped backslash. */
102     private static final Pattern ESCAPED_BACKSLASH = Pattern.compile("\\\\\\\\");
103 
104     /** Regular expression for non-printable unicode chars. */
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      * Map of Pending Violations.
178      * Key: Line number of the violation.
179      * Value: List of literal AST nodes on that line pending validation.
180      */
181     private final Map<Integer, List<DetailAST>> pendingViolations = new HashMap<>();
182 
183     /** Allow use escapes for non-printable, control characters. */
184     private boolean allowEscapesForControlCharacters;
185 
186     /** Allow use escapes if trail comment is present. */
187     private boolean allowByTailComment;
188 
189     /** Allow if all characters in literal are escaped. */
190     private boolean allowIfAllCharactersEscaped;
191 
192     /** Allow use escapes for non-printable, whitespace characters. */
193     private boolean allowNonPrintableEscapes;
194 
195     /**
196      * Creates a new {@code AvoidEscapedUnicodeCharactersCheck} instance.
197      */
198     public AvoidEscapedUnicodeCharactersCheck() {
199         // no code by default
200     }
201 
202     /**
203      * Setter to allow use escapes for non-printable, control characters.
204      *
205      * @param allow user's value.
206      * @since 5.8
207      */
208     public final void setAllowEscapesForControlCharacters(boolean allow) {
209         allowEscapesForControlCharacters = allow;
210     }
211 
212     /**
213      * Setter to allow use escapes if trail comment is present.
214      *
215      * @param allow user's value.
216      * @since 5.8
217      */
218     public final void setAllowByTailComment(boolean allow) {
219         allowByTailComment = allow;
220     }
221 
222     /**
223      * Setter to allow if all characters in literal are escaped.
224      *
225      * @param allow user's value.
226      * @since 5.8
227      */
228     public final void setAllowIfAllCharactersEscaped(boolean allow) {
229         allowIfAllCharactersEscaped = allow;
230     }
231 
232     /**
233      * Setter to allow use escapes for non-printable, whitespace characters.
234      *
235      * @param allow user's value.
236      * @since 5.8
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      * Checks if the literal has Unicode char and should be reported.
295      * If violation is found, it is added to pendingViolations.
296      *
297      * @param ast literal token.
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      * Checks if a comment clears any pending violations on the same line.
324      *
325      * @param comment comment token.
326      */
327     private void checkComment(DetailAST comment) {
328         if (isTrailingComment(comment)) {
329             pendingViolations.remove(comment.getLineNo());
330         }
331     }
332 
333     /**
334      * Checks if a comment is trailing (has no code after it on the same line).
335      *
336      * @param commentNode the comment AST node
337      * @return true if it is trailing
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      * Checks if literal has Unicode chars.
346      *
347      * @param literal String literal.
348      * @return true if literal has Unicode chars.
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      * Check if String literal contains Unicode control chars.
358      *
359      * @param literal String literal.
360      * @param pattern RegExp for valid characters.
361      * @return true, if String literal contains Unicode control chars.
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      * Count regexp matches into String literal.
373      *
374      * @param pattern pattern.
375      * @param target String literal.
376      * @return count of regexp matches.
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      * Checks if all characters in String literal is escaped.
389      *
390      * @param literal current literal.
391      * @return true if all characters in String literal is escaped.
392      */
393     private boolean isAllCharactersEscaped(String literal) {
394         return allowIfAllCharactersEscaped
395                 && ALL_ESCAPED_CHARS.matcher(literal).find();
396     }
397 
398 }