001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks; 021 022import java.util.ArrayList; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Map; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 028 029import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 030import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 031import com.puppycrawl.tools.checkstyle.api.DetailAST; 032import com.puppycrawl.tools.checkstyle.api.TokenTypes; 033import com.puppycrawl.tools.checkstyle.utils.CheckUtil; 034 035/** 036 * <div> 037 * Restricts using 038 * <a href = "https://docs.oracle.com/javase/specs/jls/se11/html/jls-3.html#jls-3.3"> 039 * Unicode escapes</a> (such as \u221e). It is possible to allow using escapes for 040 * <a href="https://en.wiktionary.org/wiki/Appendix:Control_characters"> 041 * non-printable, control characters</a>. 042 * Also, this check can be configured to allow using escapes 043 * if trail comment is present. By the option it is possible to 044 * allow using escapes if literal contains only them. 045 * </div> 046 * 047 * @since 5.8 048 */ 049@FileStatefulCheck 050public class AvoidEscapedUnicodeCharactersCheck extends AbstractCheck { 051 052 /** 053 * A key is pointing to the warning message text in "messages.properties" file. 054 */ 055 public static final String MSG_KEY = "forbid.escaped.unicode.char"; 056 057 /** Regular expression for Unicode chars. */ 058 private static final Pattern UNICODE_REGEXP = Pattern.compile("\\\\u+[a-fA-F\\d]{4}"); 059 060 /** 061 * Regular expression Unicode control characters. 062 * 063 * @see <a href="https://en.wiktionary.org/wiki/Appendix:Control_characters"> 064 * Appendix:Control characters</a> 065 */ 066 private static final Pattern UNICODE_CONTROL = Pattern.compile("\\\\u+" 067 + "(00[0-1][\\dA-Fa-f]" 068 + "|00[8-9][\\dA-Fa-f]" 069 + "|00[aA][dD]" 070 + "|034[fF]" 071 + "|070[fF]" 072 + "|180[eE]" 073 + "|200[b-fB-F]" 074 + "|202[a-eA-E]" 075 + "|206[0-4a-fA-F]" 076 + "|[fF]{3}[9a-bA-B]" 077 + "|[fF][eE][fF]{2})"); 078 079 /** 080 * Regular expression for all escaped chars. 081 * See <a href="https://docs.oracle.com/javase/specs/jls/se15/html/jls-3.html#jls-3.10.7"> 082 * EscapeSequence</a> 083 */ 084 private static final Pattern ALL_ESCAPED_CHARS = Pattern.compile("^(" 085 + UNICODE_REGEXP.pattern() 086 + "|\"" 087 + "|'" 088 + "|\\\\" 089 + "|\\\\b" 090 + "|\\\\f" 091 + "|\\\\n" 092 + "|\\R" 093 + "|\\\\r" 094 + "|\\\\s" 095 + "|\\\\t" 096 + ")+$"); 097 098 /** Regular expression for escaped backslash. */ 099 private static final Pattern ESCAPED_BACKSLASH = Pattern.compile("\\\\\\\\"); 100 101 /** Regular expression for non-printable unicode chars. */ 102 private static final Pattern NON_PRINTABLE_CHARS = Pattern.compile("\\\\u0000" 103 + "|\\\\u0009" 104 + "|\\\\u000[bB]" 105 + "|\\\\u000[cC]" 106 + "|\\\\u0020" 107 + "|\\\\u007[fF]" 108 + "|\\\\u0085" 109 + "|\\\\u009[fF]" 110 + "|\\\\u00[aA]0" 111 + "|\\\\u00[aA][dD]" 112 + "|\\\\u04[fF]9" 113 + "|\\\\u05[bB][eE]" 114 + "|\\\\u05[dD]0" 115 + "|\\\\u05[eE][aA]" 116 + "|\\\\u05[fF]3" 117 + "|\\\\u05[fF]4" 118 + "|\\\\u0600" 119 + "|\\\\u0604" 120 + "|\\\\u061[cC]" 121 + "|\\\\u06[dD]{2}" 122 + "|\\\\u06[fF]{2}" 123 + "|\\\\u070[fF]" 124 + "|\\\\u0750" 125 + "|\\\\u077[fF]" 126 + "|\\\\u0[eE]00" 127 + "|\\\\u0[eE]7[fF]" 128 + "|\\\\u1680" 129 + "|\\\\u180[eE]" 130 + "|\\\\u1[eE]00" 131 + "|\\\\u2000" 132 + "|\\\\u2001" 133 + "|\\\\u2002" 134 + "|\\\\u2003" 135 + "|\\\\u2004" 136 + "|\\\\u2005" 137 + "|\\\\u2006" 138 + "|\\\\u2007" 139 + "|\\\\u2008" 140 + "|\\\\u2009" 141 + "|\\\\u200[aA]" 142 + "|\\\\u200[fF]" 143 + "|\\\\u2025" 144 + "|\\\\u2028" 145 + "|\\\\u2029" 146 + "|\\\\u202[fF]" 147 + "|\\\\u205[fF]" 148 + "|\\\\u2064" 149 + "|\\\\u2066" 150 + "|\\\\u2067" 151 + "|\\\\u2068" 152 + "|\\\\u2069" 153 + "|\\\\u206[aA]" 154 + "|\\\\u206[fF]" 155 + "|\\\\u20[aA][fF]" 156 + "|\\\\u2100" 157 + "|\\\\u213[aA]" 158 + "|\\\\u3000" 159 + "|\\\\u[dD]800" 160 + "|\\\\u[fF]8[fF]{2}" 161 + "|\\\\u[fF][bB]50" 162 + "|\\\\u[fF][dD][fF]{2}" 163 + "|\\\\u[fF][eE]70" 164 + "|\\\\u[fF][eE][fF]{2}" 165 + "|\\\\u[fF]{2}0[eE]" 166 + "|\\\\u[fF]{2}61" 167 + "|\\\\u[fF]{2}[dD][cC]" 168 + "|\\\\u[fF]{3}9" 169 + "|\\\\u[fF]{3}[aA]" 170 + "|\\\\u[fF]{3}[bB]" 171 + "|\\\\u[fF]{4}"); 172 173 /** 174 * Map of Pending Violations. 175 * Key: Line number of the violation. 176 * Value: List of literal AST nodes on that line pending validation. 177 */ 178 private final Map<Integer, List<DetailAST>> pendingViolations = new HashMap<>(); 179 180 /** Allow use escapes for non-printable, control characters. */ 181 private boolean allowEscapesForControlCharacters; 182 183 /** Allow use escapes if trail comment is present. */ 184 private boolean allowByTailComment; 185 186 /** Allow if all characters in literal are escaped. */ 187 private boolean allowIfAllCharactersEscaped; 188 189 /** Allow use escapes for non-printable, whitespace characters. */ 190 private boolean allowNonPrintableEscapes; 191 192 /** 193 * Creates a new {@code AvoidEscapedUnicodeCharactersCheck} instance. 194 */ 195 public AvoidEscapedUnicodeCharactersCheck() { 196 // no code by default 197 } 198 199 /** 200 * Setter to allow use escapes for non-printable, control characters. 201 * 202 * @param allow user's value. 203 * @since 5.8 204 */ 205 public final void setAllowEscapesForControlCharacters(boolean allow) { 206 allowEscapesForControlCharacters = allow; 207 } 208 209 /** 210 * Setter to allow use escapes if trail comment is present. 211 * 212 * @param allow user's value. 213 * @since 5.8 214 */ 215 public final void setAllowByTailComment(boolean allow) { 216 allowByTailComment = allow; 217 } 218 219 /** 220 * Setter to allow if all characters in literal are escaped. 221 * 222 * @param allow user's value. 223 * @since 5.8 224 */ 225 public final void setAllowIfAllCharactersEscaped(boolean allow) { 226 allowIfAllCharactersEscaped = allow; 227 } 228 229 /** 230 * Setter to allow use escapes for non-printable, whitespace characters. 231 * 232 * @param allow user's value. 233 * @since 5.8 234 */ 235 public final void setAllowNonPrintableEscapes(boolean allow) { 236 allowNonPrintableEscapes = allow; 237 } 238 239 @Override 240 public int[] getDefaultTokens() { 241 return getRequiredTokens(); 242 } 243 244 @Override 245 public int[] getAcceptableTokens() { 246 return getRequiredTokens(); 247 } 248 249 @Override 250 public int[] getRequiredTokens() { 251 return new int[] { 252 TokenTypes.STRING_LITERAL, 253 TokenTypes.CHAR_LITERAL, 254 TokenTypes.TEXT_BLOCK_CONTENT, 255 TokenTypes.SINGLE_LINE_COMMENT, 256 TokenTypes.BLOCK_COMMENT_BEGIN, 257 }; 258 } 259 260 @Override 261 public boolean isCommentNodesRequired() { 262 return true; 263 } 264 265 @Override 266 public void beginTree(DetailAST rootAST) { 267 pendingViolations.clear(); 268 } 269 270 @Override 271 public void visitToken(DetailAST ast) { 272 if (ast.getType() == TokenTypes.SINGLE_LINE_COMMENT 273 || ast.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) { 274 checkComment(ast); 275 } 276 else { 277 checkLiteral(ast); 278 } 279 } 280 281 @Override 282 public void finishTree(DetailAST rootAST) { 283 for (List<DetailAST> asts : pendingViolations.values()) { 284 for (DetailAST ast : asts) { 285 log(ast, MSG_KEY); 286 } 287 } 288 } 289 290 /** 291 * Checks if the literal has Unicode char and should be reported. 292 * If violation is found, it is added to pendingViolations. 293 * 294 * @param ast literal token. 295 */ 296 private void checkLiteral(DetailAST ast) { 297 final String literal = 298 CheckUtil.stripIndentAndInitialNewLineFromTextBlock(ast.getText()); 299 300 if (hasUnicodeChar(literal) && !(isAllCharactersEscaped(literal) 301 || allowEscapesForControlCharacters 302 && isOnlyUnicodeValidChars(literal, UNICODE_CONTROL) 303 || allowNonPrintableEscapes 304 && isOnlyUnicodeValidChars(literal, NON_PRINTABLE_CHARS))) { 305 306 if (allowByTailComment) { 307 int lineNo = ast.getLineNo(); 308 if (ast.getType() == TokenTypes.TEXT_BLOCK_CONTENT) { 309 lineNo = ast.getNextSibling().getLineNo(); 310 } 311 pendingViolations.computeIfAbsent(lineNo, key -> new ArrayList<>()).add(ast); 312 } 313 else { 314 log(ast, MSG_KEY); 315 } 316 } 317 } 318 319 /** 320 * Checks if a comment clears any pending violations on the same line. 321 * 322 * @param comment comment token. 323 */ 324 private void checkComment(DetailAST comment) { 325 if (isTrailingComment(comment)) { 326 pendingViolations.remove(comment.getLineNo()); 327 } 328 } 329 330 /** 331 * Checks if a comment is trailing (has no code after it on the same line). 332 * 333 * @param commentNode the comment AST node 334 * @return true if it is trailing 335 */ 336 private static boolean isTrailingComment(DetailAST commentNode) { 337 final DetailAST nextSibling = commentNode.getNextSibling(); 338 return nextSibling == null || nextSibling.getLineNo() != commentNode.getLineNo(); 339 } 340 341 /** 342 * Checks if literal has Unicode chars. 343 * 344 * @param literal String literal. 345 * @return true if literal has Unicode chars. 346 */ 347 private static boolean hasUnicodeChar(String literal) { 348 final String literalWithoutEscapedBackslashes = 349 ESCAPED_BACKSLASH.matcher(literal).replaceAll(""); 350 return UNICODE_REGEXP.matcher(literalWithoutEscapedBackslashes).find(); 351 } 352 353 /** 354 * Check if String literal contains Unicode control chars. 355 * 356 * @param literal String literal. 357 * @param pattern RegExp for valid characters. 358 * @return true, if String literal contains Unicode control chars. 359 */ 360 private static boolean isOnlyUnicodeValidChars(String literal, Pattern pattern) { 361 final int unicodeMatchesCounter = 362 countMatches(UNICODE_REGEXP, literal); 363 final int unicodeValidMatchesCounter = 364 countMatches(pattern, literal); 365 return unicodeMatchesCounter - unicodeValidMatchesCounter == 0; 366 } 367 368 /** 369 * Count regexp matches into String literal. 370 * 371 * @param pattern pattern. 372 * @param target String literal. 373 * @return count of regexp matches. 374 */ 375 private static int countMatches(Pattern pattern, String target) { 376 int matcherCounter = 0; 377 final Matcher matcher = pattern.matcher(target); 378 while (matcher.find()) { 379 matcherCounter++; 380 } 381 return matcherCounter; 382 } 383 384 /** 385 * Checks if all characters in String literal is escaped. 386 * 387 * @param literal current literal. 388 * @return true if all characters in String literal is escaped. 389 */ 390 private boolean isAllCharactersEscaped(String literal) { 391 return allowIfAllCharactersEscaped 392 && ALL_ESCAPED_CHARS.matcher(literal).find(); 393 } 394 395}