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 &#92;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}