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}