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.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 }