code: ///////////////////////////////////////////////////////////////////////////////////////////////
  // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
  // Copyright (C) 2001-2023 the original author or authors.
  //
  // This library is free software; you can redistribute it and/or
  // modify it under the terms of the GNU Lesser General Public
  // License as published by the Free Software Foundation; either
  // version 2.1 of the License, or (at your option) any later version.
  //
  // This library is distributed in the hope that it will be useful,
  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  // Lesser General Public License for more details.
  //
  // You should have received a copy of the GNU Lesser General Public
  // License along with this library; if not, write to the Free Software
  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  ///////////////////////////////////////////////////////////////////////////////////////////////
  
  package com.puppycrawl.tools.checkstyle.checks;
  
  import static com.google.common.truth.Truth.assertWithMessage;
  import static com.puppycrawl.tools.checkstyle.checks.AvoidEscapedUnicodeCharactersCheck.MSG_KEY;
  
  import java.util.Arrays;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  import java.util.stream.IntStream;
  
  import org.junit.jupiter.api.Test;
  
  import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
  
  public class AvoidEscapedUnicodeCharactersCheckTest extends AbstractModuleTestSupport {
  
      // C0 (ASCII and derivatives)
      // https://en.wiktionary.org/wiki/Appendix:Control_characters#C0_.28ASCII_and_derivatives.29
      private static final int[] C0_CONTROL_CHARACTER_INDICES = {
          0x0000,
          0x0001,
          0x0002,
          0x0003,
          0x0004,
          0x0005,
          0x0006,
          0x0007,
          0x0008,
          0x0009,
          0x000a,
          0x000b,
          0x000c,
          0x000d,
          0x000e,
          0x000f,
          0x0010,
          0x0011,
          0x0012,
          0x0013,
          0x0014,
          0x0015,
          0x0016,
          0x0017,
          0x0018,
          0x0019,
          0x001a,
          0x001b,
          0x001c,
          0x001d,
          0x001e,
          0x001f,
      };
  
      // C1 set
      // https://en.wiktionary.org/wiki/Appendix:Control_characters#C1_set
      private static final int[] C1_CONTROL_CHARACTER_INDICES = {
          0x0080,
          0x0081,
          0x0082,
          0x0083,
          0x0084,
          0x0085,
          0x0086,
          0x0087,
          0x0088,
          0x0089,
          0x008a,
          0x008b,
          0x008c,
          0x008d,
          0x008e,
          0x008f,
          0x0090,
          0x0091,
          0x0092,
          0x0093,
          0x0094,
          0x0095,
          0x0096,
          0x0097,
          0x0098,
          0x0099,
          0x009a,
          0x009b,
          0x009c,
          0x009d,
          0x009e,
          0x009f,
      };
  
      // Other control characters which do not occur in the C0 or C1 sets
      // https://en.wiktionary.org/wiki/Appendix:Control_characters#Unicode_control_characters
      private static final int[] OTHER_CONTROL_CHARACTER_INDICES = {
          0x00ad,
          0x034f,
          0x070f,
          0x180e,
          0x200b,
          0x200c,
          0x200d,
          0x200e,
          0x200f,
          0x202a,
          0x202b,
          0x202c,
          0x202d,
          0x202e,
          0x2060,
          0x2061,
          0x2062,
          0x2063,
          0x2064,
          0x206a,
          0x206b,
          0x206c,
          0x206d,
          0x206e,
          0x206f,
          0xfeff,
          0xfff9,
          0xfffa,
          0xfffb,
      };
  
      @Override
      protected String getPackageLocation() {
          return "com/puppycrawl/tools/checkstyle/checks/avoidescapedunicodecharacters";
      }
  
      @Test
      public void testGetRequiredTokens() {
          final AvoidEscapedUnicodeCharactersCheck checkObj =
              new AvoidEscapedUnicodeCharactersCheck();
          final int[] expected = {
              TokenTypes.STRING_LITERAL,
              TokenTypes.CHAR_LITERAL,
              TokenTypes.TEXT_BLOCK_CONTENT,
          };
          assertWithMessage("Required tokens differ from expected")
              .that(checkObj.getRequiredTokens())
              .isEqualTo(expected);
      }
  
      @Test
      public void testDefault() throws Exception {
          final String[] expected = {
              "17:38: " + getCheckMessage(MSG_KEY),
              "19:38: " + getCheckMessage(MSG_KEY),
              "21:38: " + getCheckMessage(MSG_KEY),
              "25:38: " + getCheckMessage(MSG_KEY),
              "26:38: " + getCheckMessage(MSG_KEY),
              "30:24: " + getCheckMessage(MSG_KEY),
              "34:36: " + getCheckMessage(MSG_KEY),
              "35:36: " + getCheckMessage(MSG_KEY),
              "37:24: " + getCheckMessage(MSG_KEY),
              "41:38: " + getCheckMessage(MSG_KEY),
              "42:38: " + getCheckMessage(MSG_KEY),
              "43:38: " + getCheckMessage(MSG_KEY),
              "44:47: " + getCheckMessage(MSG_KEY),
              "52:32: " + getCheckMessage(MSG_KEY),
              "69:35: " + getCheckMessage(MSG_KEY),
              "70:35: " + getCheckMessage(MSG_KEY),
              "71:35: " + getCheckMessage(MSG_KEY),
              "72:35: " + getCheckMessage(MSG_KEY),
              "82:24: " + getCheckMessage(MSG_KEY),
              "83:24: " + getCheckMessage(MSG_KEY),
              "84:24: " + getCheckMessage(MSG_KEY),
              "85:24: " + getCheckMessage(MSG_KEY),
              "86:24: " + getCheckMessage(MSG_KEY),
              "87:24: " + getCheckMessage(MSG_KEY),
              "89:24: " + getCheckMessage(MSG_KEY),
              "92:31: " + getCheckMessage(MSG_KEY),
              "92:48: " + getCheckMessage(MSG_KEY),
              "96:38: " + getCheckMessage(MSG_KEY),
              "97:38: " + getCheckMessage(MSG_KEY),
              "98:38: " + getCheckMessage(MSG_KEY),
              "99:38: " + getCheckMessage(MSG_KEY),
              "102:31: " + getCheckMessage(MSG_KEY),
              "102:45: " + getCheckMessage(MSG_KEY),
              "103:34: " + getCheckMessage(MSG_KEY),
              "104:46: " + getCheckMessage(MSG_KEY),
              "108:38: " + getCheckMessage(MSG_KEY),
              "114:38: " + getCheckMessage(MSG_KEY),
              "116:46: " + getCheckMessage(MSG_KEY),
              "117:55: " + getCheckMessage(MSG_KEY),
              "118:46: " + getCheckMessage(MSG_KEY),
              "119:55: " + getCheckMessage(MSG_KEY),
              "120:46: " + getCheckMessage(MSG_KEY),
              "121:55: " + getCheckMessage(MSG_KEY),
              "122:46: " + getCheckMessage(MSG_KEY),
              "123:55: " + getCheckMessage(MSG_KEY),
              "124:46: " + getCheckMessage(MSG_KEY),
              "125:55: " + getCheckMessage(MSG_KEY),
              "126:48: " + getCheckMessage(MSG_KEY),
              "127:57: " + getCheckMessage(MSG_KEY),
          };
          verifyWithInlineConfigParser(
                  getPath("InputAvoidEscapedUnicodeCharacters.java"), expected);
      }
  
      @Test
      public void testAllowEscapesForControlCharacterSet() throws Exception {
          final String[] expected = {
              "17:38: " + getCheckMessage(MSG_KEY),
              "19:38: " + getCheckMessage(MSG_KEY),
              "21:38: " + getCheckMessage(MSG_KEY),
              "25:38: " + getCheckMessage(MSG_KEY),
              "26:38: " + getCheckMessage(MSG_KEY),
              "34:36: " + getCheckMessage(MSG_KEY),
              "35:36: " + getCheckMessage(MSG_KEY),
              "41:38: " + getCheckMessage(MSG_KEY),
              "42:38: " + getCheckMessage(MSG_KEY),
              "43:38: " + getCheckMessage(MSG_KEY),
              "44:47: " + getCheckMessage(MSG_KEY),
              "52:32: " + getCheckMessage(MSG_KEY),
              "69:35: " + getCheckMessage(MSG_KEY),
              "70:35: " + getCheckMessage(MSG_KEY),
              "71:35: " + getCheckMessage(MSG_KEY),
              "72:35: " + getCheckMessage(MSG_KEY),
              "83:24: " + getCheckMessage(MSG_KEY),
              "84:24: " + getCheckMessage(MSG_KEY),
              "85:24: " + getCheckMessage(MSG_KEY),
              "86:24: " + getCheckMessage(MSG_KEY),
              "87:24: " + getCheckMessage(MSG_KEY),
              "89:24: " + getCheckMessage(MSG_KEY),
              "92:31: " + getCheckMessage(MSG_KEY),
              "92:48: " + getCheckMessage(MSG_KEY),
              "96:38: " + getCheckMessage(MSG_KEY),
              "97:38: " + getCheckMessage(MSG_KEY),
              "98:38: " + getCheckMessage(MSG_KEY),
              "99:38: " + getCheckMessage(MSG_KEY),
              "102:45: " + getCheckMessage(MSG_KEY),
              "104:46: " + getCheckMessage(MSG_KEY),
              "108:38: " + getCheckMessage(MSG_KEY),
              "114:38: " + getCheckMessage(MSG_KEY),
              "116:46: " + getCheckMessage(MSG_KEY),
              "117:55: " + getCheckMessage(MSG_KEY),
              "118:46: " + getCheckMessage(MSG_KEY),
              "119:55: " + getCheckMessage(MSG_KEY),
              "120:46: " + getCheckMessage(MSG_KEY),
              "121:55: " + getCheckMessage(MSG_KEY),
              "122:46: " + getCheckMessage(MSG_KEY),
              "123:55: " + getCheckMessage(MSG_KEY),
              "124:46: " + getCheckMessage(MSG_KEY),
              "125:55: " + getCheckMessage(MSG_KEY),
              "126:48: " + getCheckMessage(MSG_KEY),
              "127:57: " + getCheckMessage(MSG_KEY),
          };
          verifyWithInlineConfigParser(
                  getPath("InputAvoidEscapedUnicodeCharacters1.java"), expected);
      }
  
      @Test
      public void testAllowByTailComment() throws Exception {
          final String[] expected = {
              "17:38: " + getCheckMessage(MSG_KEY),
              "25:38: " + getCheckMessage(MSG_KEY),
              "34:36: " + getCheckMessage(MSG_KEY),
              "41:38: " + getCheckMessage(MSG_KEY),
              "43:38: " + getCheckMessage(MSG_KEY),
              "44:47: " + getCheckMessage(MSG_KEY),
              "69:35: " + getCheckMessage(MSG_KEY),
              "71:35: " + getCheckMessage(MSG_KEY),
              "73:35: " + getCheckMessage(MSG_KEY),
              "75:35: " + getCheckMessage(MSG_KEY),
              "86:24: " + getCheckMessage(MSG_KEY),
              "88:24: " + getCheckMessage(MSG_KEY),
              "90:24: " + getCheckMessage(MSG_KEY),
              "92:24: " + getCheckMessage(MSG_KEY),
              "94:24: " + getCheckMessage(MSG_KEY),
              "97:24: " + getCheckMessage(MSG_KEY),
              "101:31: " + getCheckMessage(MSG_KEY),
              "101:48: " + getCheckMessage(MSG_KEY),
              "111:31: " + getCheckMessage(MSG_KEY),
              "111:45: " + getCheckMessage(MSG_KEY),
              "117:38: " + getCheckMessage(MSG_KEY),
              "123:38: " + getCheckMessage(MSG_KEY),
              "125:46: " + getCheckMessage(MSG_KEY),
              "128:46: " + getCheckMessage(MSG_KEY),
              "131:46: " + getCheckMessage(MSG_KEY),
              "134:46: " + getCheckMessage(MSG_KEY),
              "137:46: " + getCheckMessage(MSG_KEY),
              "140:48: " + getCheckMessage(MSG_KEY),
          };
          verifyWithInlineConfigParser(
                  getPath("InputAvoidEscapedUnicodeCharacters2.java"), expected);
      }
  
      @Test
      public void testAllowAllCharactersEscaped() throws Exception {
          final String[] expected = {
              "17:38: " + getCheckMessage(MSG_KEY),
              "19:38: " + getCheckMessage(MSG_KEY),
              "21:38: " + getCheckMessage(MSG_KEY),
              "25:38: " + getCheckMessage(MSG_KEY),
              "26:38: " + getCheckMessage(MSG_KEY),
              "41:38: " + getCheckMessage(MSG_KEY),
              "42:38: " + getCheckMessage(MSG_KEY),
              "43:38: " + getCheckMessage(MSG_KEY),
              "52:32: " + getCheckMessage(MSG_KEY),
              "96:38: " + getCheckMessage(MSG_KEY),
              "97:38: " + getCheckMessage(MSG_KEY),
              "98:38: " + getCheckMessage(MSG_KEY),
              "99:38: " + getCheckMessage(MSG_KEY),
              "108:38: " + getCheckMessage(MSG_KEY),
              "116:46: " + getCheckMessage(MSG_KEY),
              "117:55: " + getCheckMessage(MSG_KEY),
              "118:46: " + getCheckMessage(MSG_KEY),
              "119:55: " + getCheckMessage(MSG_KEY),
              "120:46: " + getCheckMessage(MSG_KEY),
              "121:55: " + getCheckMessage(MSG_KEY),
              "122:46: " + getCheckMessage(MSG_KEY),
              "123:55: " + getCheckMessage(MSG_KEY),
              "124:46: " + getCheckMessage(MSG_KEY),
              "125:55: " + getCheckMessage(MSG_KEY),
              "126:48: " + getCheckMessage(MSG_KEY),
              "127:57: " + getCheckMessage(MSG_KEY),
          };
          verifyWithInlineConfigParser(
                  getPath("InputAvoidEscapedUnicodeCharacters3.java"), expected);
      }
  
      @Test
      public void allowNonPrintableEscapes() throws Exception {
          final String[] expected = {
              "17:38: " + getCheckMessage(MSG_KEY),
              "19:38: " + getCheckMessage(MSG_KEY),
              "21:38: " + getCheckMessage(MSG_KEY),
              "25:38: " + getCheckMessage(MSG_KEY),
              "26:38: " + getCheckMessage(MSG_KEY),
              "34:36: " + getCheckMessage(MSG_KEY),
              "35:36: " + getCheckMessage(MSG_KEY),
              "41:38: " + getCheckMessage(MSG_KEY),
              "42:38: " + getCheckMessage(MSG_KEY),
              "43:38: " + getCheckMessage(MSG_KEY),
              "44:47: " + getCheckMessage(MSG_KEY),
              "52:32: " + getCheckMessage(MSG_KEY),
              "96:38: " + getCheckMessage(MSG_KEY),
              "97:38: " + getCheckMessage(MSG_KEY),
              "98:38: " + getCheckMessage(MSG_KEY),
              "99:38: " + getCheckMessage(MSG_KEY),
              "103:34: " + getCheckMessage(MSG_KEY),
              "104:46: " + getCheckMessage(MSG_KEY),
              "108:38: " + getCheckMessage(MSG_KEY),
              "114:38: " + getCheckMessage(MSG_KEY),
              "116:46: " + getCheckMessage(MSG_KEY),
              "117:55: " + getCheckMessage(MSG_KEY),
              "118:46: " + getCheckMessage(MSG_KEY),
              "119:55: " + getCheckMessage(MSG_KEY),
              "120:46: " + getCheckMessage(MSG_KEY),
              "121:55: " + getCheckMessage(MSG_KEY),
              "122:46: " + getCheckMessage(MSG_KEY),
              "123:55: " + getCheckMessage(MSG_KEY),
              "124:46: " + getCheckMessage(MSG_KEY),
              "125:55: " + getCheckMessage(MSG_KEY),
              "126:48: " + getCheckMessage(MSG_KEY),
              "127:57: " + getCheckMessage(MSG_KEY),
          };
          verifyWithInlineConfigParser(
                  getPath("InputAvoidEscapedUnicodeCharacters4.java"), expected);
      }
  
      @Test
      public void testAllowByTailCommentWithEmoji() throws Exception {
          final String[] expected = {
              "15:24: " + getCheckMessage(MSG_KEY),
              "18:24: " + getCheckMessage(MSG_KEY),
              "22:30: " + getCheckMessage(MSG_KEY),
              "32:18: " + getCheckMessage(MSG_KEY),
              "35:18: " + getCheckMessage(MSG_KEY),
              "37:18: " + getCheckMessage(MSG_KEY),
              "40:18: " + getCheckMessage(MSG_KEY),
          };
          verifyWithInlineConfigParser(
                  getPath("InputAvoidEscapedUnicodeCharacters5.java"), expected);
      }
  
      @Test
      public void testAvoidEscapedUnicodeCharactersTextBlocksAllowByComment() throws Exception {
          final String[] expected = {
              "18:30: " + getCheckMessage(MSG_KEY),
              "20:30: " + getCheckMessage(MSG_KEY),
              "22:30: " + getCheckMessage(MSG_KEY),
              "25:39: " + getCheckMessage(MSG_KEY),
              "30:33: " + getCheckMessage(MSG_KEY),
              "33:33: " + getCheckMessage(MSG_KEY),
              "36:33: " + getCheckMessage(MSG_KEY),
              "41:42: " + getCheckMessage(MSG_KEY),
          };
          verifyWithInlineConfigParser(
                  getNonCompilablePath(
                  "InputAvoidEscapedUnicodeCharactersTextBlocksAllowByComment.java"),
              expected);
      }
  
      @Test
      public void testAvoidEscapedUnicodeCharactersTextBlocks() throws Exception {
          final String[] expected = {
              "17:30: " + getCheckMessage(MSG_KEY),
              "18:30: " + getCheckMessage(MSG_KEY),
              "19:30: " + getCheckMessage(MSG_KEY),
              "20:39: " + getCheckMessage(MSG_KEY),
              "24:33: " + getCheckMessage(MSG_KEY),
              "26:33: " + getCheckMessage(MSG_KEY),
              "28:33: " + getCheckMessage(MSG_KEY),
              "30:42: " + getCheckMessage(MSG_KEY),
          };
          verifyWithInlineConfigParser(
                  getNonCompilablePath("InputAvoidEscapedUnicodeCharactersTextBlocks.java"),
              expected);
      }
  
      @Test
      public void testAvoidEscapedUnicodeCharactersEscapedS() throws Exception {
          final String[] expected = {
              "17:21: " + getCheckMessage(MSG_KEY),
              "18:22: " + getCheckMessage(MSG_KEY),
              "27:39: " + getCheckMessage(MSG_KEY),
              "30:39: " + getCheckMessage(MSG_KEY),
              "33:39: " + getCheckMessage(MSG_KEY),
              "36:22: " + getCheckMessage(MSG_KEY),
          };
          verifyWithInlineConfigParser(
                  getNonCompilablePath("InputAvoidEscapedUnicodeCharactersEscapedS.java"),
                  expected);
      }
  
      @Test
      public void testGetAcceptableTokens() {
          final AvoidEscapedUnicodeCharactersCheck check = new AvoidEscapedUnicodeCharactersCheck();
          final int[] actual = check.getAcceptableTokens();
          final int[] expected = {
              TokenTypes.STRING_LITERAL,
              TokenTypes.CHAR_LITERAL,
              TokenTypes.TEXT_BLOCK_CONTENT,
          };
          assertWithMessage("Acceptable tokens differ from expected")
              .that(actual)
              .isEqualTo(expected);
      }
  
      @Test
      public void testAllowEscapesForControlCharacterSetForAllCharacters() throws Exception {
  
          final int indexOfStartLineInInputFile = 16;
          final String message = getCheckMessage(MSG_KEY);
          final String[] expected = IntStream.rangeClosed(0, 0xffff)
                  .parallel()
                  .filter(val -> !isControlCharacter(val))
                  .mapToObj(msg -> indexOfStartLineInInputFile + msg + ":54: " + message)
                  .toArray(String[]::new);
          verifyWithInlineConfigParser(
                  getPath("InputAvoidEscapedUnicodeCharactersAllEscapedUnicodeCharacters.java"),
                  expected);
      }
  
      /**
       * Method countMatches is used only inside isOnlyUnicodeValidChars method, and when
       * pitest mutates 316:13 countMatches++ to countMatches-- it makes no difference for
       * isOnlyUnicodeValidChars method as it applies countMatches to both cases in comparison.
       * It is possible to kill mutation in countMatches method by changing code in
       * isOnlyUnicodeValidChars, but it creates new uncoverable mutations and makes code harder
       * to understand.
       *
       * @throws Exception when code tested throws some exception
       */
      @Test
      public void testCountMatches() throws Exception {
          final AvoidEscapedUnicodeCharactersCheck check = new AvoidEscapedUnicodeCharactersCheck();
          final int actual = TestUtil.invokeMethod(check, "countMatches",
                  Pattern.compile("\\\\u[a-fA-F\\d]{4}"), "\\u1234");
          assertWithMessage("Unexpected matches count")
              .that(actual)
              .isEqualTo(1);
      }
  
      /**
       * Testing, that all elements in the constant NON_PRINTABLE_CHARS are sorted.
       * This is very convenient for the sake of maintainability.
       */
      @Test
      public void testNonPrintableCharsAreSorted() {
          String expression = TestUtil.<Pattern>getInternalStaticState(
                  AvoidEscapedUnicodeCharactersCheck.class, "NON_PRINTABLE_CHARS").pattern();
  
          // Replacing expressions like "\\u000[bB]" with "\\u000B"
          final String[] charExpressions = {"Aa", "Bb", "Cc", "Dd", "Ee", "Ff"};
          for (String charExpression : charExpressions) {
              final String regex = "\\[[" + charExpression + "]{2}]";
              expression = expression.replaceAll(regex, charExpression.substring(0, 1));
          }
  
          // Replacing duplications like "\\uF{3}9" with "\\uFFF9"
          for (int i = 4; i > 1; i--) {
              final String regex = "([A-F])\\{" + i + "}";
              String replacement = "$1$1{" + (i - 1) + "}";
              if (i == 2) {
                  replacement = "$1$1";
              }
              expression = expression.replaceAll(regex, replacement);
          }
  
          // Verifying character order
          final String[] expressionParts = expression.split("\\|");
          final Pattern unicodeCharPattern = Pattern.compile("^\\\\\\\\u[\\dA-F]{4}$");
          String lastChar = null;
          for (int i = 0; i < expressionParts.length; i++) {
              final String currentChar = expressionParts[i];
              final Matcher matcher = unicodeCharPattern.matcher(currentChar);
              if (!matcher.matches()) {
                  final String message = "Character '" + currentChar + "' (at position " + i
                          + ") doesn't match the pattern";
                  assertWithMessage(message)
                          .that(matcher.matches())
                          .isTrue();
              }
              if (lastChar != null) {
                  final String message = "Character '" + lastChar + "' should be after '"
                          + currentChar + "', position: " + i;
                  assertWithMessage(message)
                          .that(lastChar.compareTo(currentChar) < 0)
                          .isTrue();
              }
              lastChar = currentChar;
          }
      }
  
      private static boolean isControlCharacter(final int character) {
          return Arrays.binarySearch(C0_CONTROL_CHARACTER_INDICES, character) >= 0
                  || Arrays.binarySearch(C1_CONTROL_CHARACTER_INDICES, character) >= 0
                  || Arrays.binarySearch(OTHER_CONTROL_CHARACTER_INDICES, character) >= 0;
      }
  
  }
  