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.coding;
21
22 import java.util.HashSet;
23 import java.util.Objects;
24 import java.util.Set;
25
26 import com.puppycrawl.tools.checkstyle.StatelessCheck;
27 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28 import com.puppycrawl.tools.checkstyle.api.DetailAST;
29 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
30 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
31
32 /**
33 * <div>
34 * Checks that specified symbols (by Unicode code points or ranges) are not used in code.
35 * By default, blocks common symbol ranges.
36 * </div>
37 *
38 * <p>
39 * Rationale: This check helps prevent emoji symbols and special characters in code
40 * (commonly added by AI tools), enforce coding standards, or forbid specific Unicode characters.
41 * </p>
42 *
43 * <p>
44 * Default ranges cover:
45 * </p>
46 * <ul>
47 * <li>U+2190–U+27BF: Arrows, Mathematical Operators, Box Drawing, Geometric Shapes,
48 * Miscellaneous Symbols, and Dingbats</li>
49 * <li>U+1F600–U+1F64F: Emoticons</li>
50 * <li>U+1F680–U+1F6FF: Transport and Map Symbols</li>
51 * <li>U+1F700–U+10FFFF: Alchemical Symbols and other pictographic symbols</li>
52 * </ul>
53 *
54 * <p>
55 * For a complete list of Unicode characters and ranges, see:
56 * <a href="https://en.wikipedia.org/wiki/List_of_Unicode_characters">
57 * List of Unicode characters</a>
58 * </p>
59 *
60 * <ul>
61 * <li>
62 * Property {@code symbolCodes} - Specify the symbols to check for, as Unicode code points
63 * or ranges. Format: comma-separated list of hex codes or ranges
64 * (e.g., {@code "0x2705, 0x1F600-0x1F64F"}). To allow only ASCII characters,
65 * use {@code "0x0080-0x10FFFF"}.
66 * Type is {@code java.lang.String}.
67 * Default value is {@code "0x2190-0x27BF, 0x1F600-0x1F64F, 0x1F680-0x1F6FF, 0x1F700-0x10FFFF"}.
68 * </li>
69 * </ul>
70 *
71 * @since 13.4.0
72 */
73 @StatelessCheck
74 public class IllegalSymbolCheck extends AbstractCheck {
75
76 /**
77 * A key is pointing to the warning message text in "messages.properties" file.
78 */
79 public static final String MSG_KEY = "illegal.symbol";
80
81 /** Separator used for defining ranges. */
82 private static final String RANGE_SEPARATOR = "-";
83
84 /** Default symbol codes to check for. */
85 private static final String DEFAULT_ILLEGAL_CODES =
86 "0x2190-0x27BF, 0x1F600-0x1F64F, 0x1F680-0x1F6FF, 0x1F700-0x10FFFF";
87
88 /** Set of individual Unicode code points to disallow. */
89 private final Set<Integer> singleCodePoints = new HashSet<>();
90
91 /** Set of Unicode ranges to disallow. */
92 private final Set<CodePointRange> codePointRanges = new HashSet<>();
93
94 /** Specify the symbols to check for, as Unicode code points or ranges. */
95 private String symbolCodes = DEFAULT_ILLEGAL_CODES;
96
97 /**
98 * Creates a new {@code IllegalSymbolCheck} instance.
99 */
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 }