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.coding; 021 022import java.util.HashSet; 023import java.util.Objects; 024import java.util.Set; 025 026import com.puppycrawl.tools.checkstyle.StatelessCheck; 027import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 028import com.puppycrawl.tools.checkstyle.api.DetailAST; 029import com.puppycrawl.tools.checkstyle.api.TokenTypes; 030import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 031 032/** 033 * <div> 034 * Checks that specified symbols (by Unicode code points or ranges) are not used in code. 035 * By default, blocks common symbol ranges. 036 * </div> 037 * 038 * <p> 039 * Rationale: This check helps prevent emoji symbols and special characters in code 040 * (commonly added by AI tools), enforce coding standards, or forbid specific Unicode characters. 041 * </p> 042 * 043 * <p> 044 * Default ranges cover: 045 * </p> 046 * <ul> 047 * <li>U+2190–U+27BF: Arrows, Mathematical Operators, Box Drawing, Geometric Shapes, 048 * Miscellaneous Symbols, and Dingbats</li> 049 * <li>U+1F600–U+1F64F: Emoticons</li> 050 * <li>U+1F680–U+1F6FF: Transport and Map Symbols</li> 051 * <li>U+1F700–U+10FFFF: Alchemical Symbols and other pictographic symbols</li> 052 * </ul> 053 * 054 * <p> 055 * For a complete list of Unicode characters and ranges, see: 056 * <a href="https://en.wikipedia.org/wiki/List_of_Unicode_characters"> 057 * List of Unicode characters</a> 058 * </p> 059 * 060 * <ul> 061 * <li> 062 * Property {@code symbolCodes} - Specify the symbols to check for, as Unicode code points 063 * or ranges. Format: comma-separated list of hex codes or ranges 064 * (e.g., {@code "0x2705, 0x1F600-0x1F64F"}). To allow only ASCII characters, 065 * use {@code "0x0080-0x10FFFF"}. 066 * Type is {@code java.lang.String}. 067 * Default value is {@code "0x2190-0x27BF, 0x1F600-0x1F64F, 0x1F680-0x1F6FF, 0x1F700-0x10FFFF"}. 068 * </li> 069 * </ul> 070 * 071 * @since 13.4.0 072 */ 073@StatelessCheck 074public class IllegalSymbolCheck extends AbstractCheck { 075 076 /** 077 * A key is pointing to the warning message text in "messages.properties" file. 078 */ 079 public static final String MSG_KEY = "illegal.symbol"; 080 081 /** Separator used for defining ranges. */ 082 private static final String RANGE_SEPARATOR = "-"; 083 084 /** Default symbol codes to check for. */ 085 private static final String DEFAULT_ILLEGAL_CODES = 086 "0x2190-0x27BF, 0x1F600-0x1F64F, 0x1F680-0x1F6FF, 0x1F700-0x10FFFF"; 087 088 /** Set of individual Unicode code points to disallow. */ 089 private final Set<Integer> singleCodePoints = new HashSet<>(); 090 091 /** Set of Unicode ranges to disallow. */ 092 private final Set<CodePointRange> codePointRanges = new HashSet<>(); 093 094 /** Specify the symbols to check for, as Unicode code points or ranges. */ 095 private String symbolCodes = DEFAULT_ILLEGAL_CODES; 096 097 /** 098 * Creates a new {@code IllegalSymbolCheck} instance. 099 */ 100 public IllegalSymbolCheck() { 101 // no code by default 102 } 103 104 /** 105 * Setter to specify the symbols to check for. 106 * 107 * @param symbols the symbols specification 108 * @throws IllegalArgumentException if the format is invalid 109 * @since 13.4.0 110 */ 111 public void setSymbolCodes(String symbols) { 112 symbolCodes = Objects.requireNonNullElse(symbols, ""); 113 parseSymbolCodes(); 114 } 115 116 /** 117 * Initializes the check after all properties are set. 118 * 119 * <p> 120 * Ensures that the {@code symbolCodes} property is parsed 121 * for both default configuration and custom user configuration. 122 * </p> 123 * 124 * @throws IllegalArgumentException if the configured symbol format is invalid 125 */ 126 @Override 127 public void init() { 128 parseSymbolCodes(); 129 } 130 131 @Override 132 public int[] getDefaultTokens() { 133 return new int[] { 134 TokenTypes.COMMENT_CONTENT, 135 }; 136 } 137 138 @Override 139 public int[] getAcceptableTokens() { 140 return new int[] { 141 TokenTypes.COMMENT_CONTENT, 142 TokenTypes.STRING_LITERAL, 143 TokenTypes.CHAR_LITERAL, 144 TokenTypes.TEXT_BLOCK_CONTENT, 145 TokenTypes.IDENT, 146 }; 147 } 148 149 @Override 150 public int[] getRequiredTokens() { 151 return CommonUtil.EMPTY_INT_ARRAY; 152 } 153 154 @Override 155 public boolean isCommentNodesRequired() { 156 return true; 157 } 158 159 @Override 160 public void visitToken(DetailAST ast) { 161 ast.getText().codePoints() 162 .filter(this::isIllegalSymbol) 163 .findFirst() 164 .ifPresent(codePoint -> log(ast, MSG_KEY, Character.toString(codePoint))); 165 } 166 167 /** 168 * Parses the configured symbolCodes string into singleCodePoints and codePointRanges. 169 * 170 * @throws IllegalArgumentException if format is invalid 171 */ 172 private void parseSymbolCodes() { 173 for (String part : symbolCodes.split(",", -1)) { 174 final String trimmed = part.trim(); 175 if (!trimmed.isEmpty()) { 176 try { 177 if (trimmed.contains(RANGE_SEPARATOR)) { 178 parseRange(trimmed); 179 } 180 else { 181 singleCodePoints.add(parseCodePoint(trimmed)); 182 } 183 } 184 catch (NumberFormatException exception) { 185 throw new IllegalArgumentException( 186 "Invalid symbol code format: " + trimmed, exception); 187 } 188 } 189 } 190 } 191 192 /** 193 * Determines whether a code point is illegal. 194 * 195 * @param codePoint Unicode code point 196 * @return true if illegal; false otherwise 197 */ 198 private boolean isIllegalSymbol(int codePoint) { 199 boolean illegal = singleCodePoints.contains(codePoint); 200 201 for (CodePointRange range : codePointRanges) { 202 if (range.contains(codePoint)) { 203 illegal = true; 204 break; 205 } 206 } 207 208 return illegal; 209 } 210 211 /** 212 * Parses and stores a Unicode range. 213 * 214 * @param rangeStr range definition string (already trimmed by caller) 215 * @throws IllegalArgumentException if format is invalid 216 */ 217 private void parseRange(String rangeStr) { 218 final String[] parts = rangeStr.split(RANGE_SEPARATOR, -1); 219 if (parts.length != 2 220 || CommonUtil.isBlank(parts[0]) 221 || CommonUtil.isBlank(parts[1])) { 222 throw new IllegalArgumentException( 223 "Invalid range format: " + rangeStr); 224 } 225 226 final int start = parseCodePoint(parts[0].trim()); 227 final int end = parseCodePoint(parts[1].trim()); 228 229 if (start > end) { 230 throw new IllegalArgumentException( 231 "Range start must be <= end: " + rangeStr); 232 } 233 234 codePointRanges.add(new CodePointRange(start, end)); 235 } 236 237 /** 238 * Parses a Unicode code point from a trimmed string. 239 * Supported formats: {@code 0x1234}, {@code \\u1234}, {@code U+1234}, or plain hex. 240 * 241 * @param str input string (already trimmed by caller) 242 * @return parsed code point 243 * @throws NumberFormatException if invalid format 244 */ 245 private static int parseCodePoint(String str) { 246 final int hexRadix = 16; 247 final int result; 248 249 final boolean hasPrefix = 250 str.startsWith("\\u") 251 || str.startsWith("0x") 252 || str.startsWith("0X") 253 || str.startsWith("U+") 254 || str.startsWith("u+"); 255 256 if (hasPrefix) { 257 result = Integer.parseInt(str.substring(2), hexRadix); 258 } 259 else { 260 result = Integer.parseInt(str, hexRadix); 261 } 262 return result; 263 } 264 265 /** 266 * Represents a Unicode code point range. 267 * 268 * @param start range start (inclusive) 269 * @param end range end (inclusive) 270 */ 271 private record CodePointRange(int start, int end) { 272 273 /** 274 * Checks if code point is within range. 275 * 276 * @param codePoint code point to test 277 * @return true if within range; false otherwise 278 */ 279 private boolean contains(int codePoint) { 280 return codePoint >= start && codePoint <= end; 281 } 282 } 283 284}