View Javadoc
1   /*
2    * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3    * SPDX-License-Identifier: MIT
4    */
5   package com.qulice.checkstyle;
6   
7   import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8   import com.puppycrawl.tools.checkstyle.api.DetailAST;
9   import com.puppycrawl.tools.checkstyle.api.TokenTypes;
10  import java.util.regex.Matcher;
11  import java.util.regex.Pattern;
12  
13  /**
14   * Prohibits hard-coded line separator escape sequences inside string literals.
15   *
16   * <p>The following constructs are prohibited, because {@code \n} and
17   * {@code \r} are OS dependent line separators:
18   *
19   * <pre>
20   * String a = "first\nsecond";
21   * System.out.println("line\r\n");
22   * </pre>
23   *
24   * <p>These strings should be rewritten using
25   * {@link System#lineSeparator()} or {@link String#format(String, Object[])}
26   * with the {@code %n} directive, for example:
27   *
28   * <pre>
29   * String a = "first" + System.lineSeparator() + "second";
30   * System.out.println(String.format("line%n"));
31   * </pre>
32   *
33   * @since 0.24
34   */
35  public final class ProhibitLineSeparatorInStringsCheck extends AbstractCheck {
36  
37      /**
38       * Matches one or more backslashes followed by {@code n} or {@code r};
39       * only runs with an odd backslash count denote an actual escape sequence.
40       */
41      private static final Pattern ESCAPE = Pattern.compile("\\\\+[rn]");
42  
43      @Override
44      public int[] getDefaultTokens() {
45          return new int[] {TokenTypes.STRING_LITERAL};
46      }
47  
48      @Override
49      public int[] getAcceptableTokens() {
50          return this.getDefaultTokens();
51      }
52  
53      @Override
54      public int[] getRequiredTokens() {
55          return this.getDefaultTokens();
56      }
57  
58      @Override
59      public void visitToken(final DetailAST ast) {
60          if (ProhibitLineSeparatorInStringsCheck.hasLineSeparator(ast.getText())) {
61              this.log(
62                  ast,
63                  "OS-dependent line separator in string literal, use System.lineSeparator() or String.format(\"%n\")"
64              );
65          }
66      }
67  
68      /**
69       * Checks if the literal source text contains an unescaped
70       * {@code \n} or {@code \r} escape sequence.
71       * @param text Raw source text of the string literal, including quotes
72       *  (e.g. {@code "\n"} or {@code "a\\nb"})
73       * @return True if the literal embeds a line separator escape
74       */
75      private static boolean hasLineSeparator(final String text) {
76          final Matcher matcher =
77              ProhibitLineSeparatorInStringsCheck.ESCAPE.matcher(text);
78          boolean found = false;
79          while (matcher.find()) {
80              if ((matcher.end() - matcher.start() - 1) % 2 == 1) {
81                  found = true;
82                  break;
83              }
84          }
85          return found;
86      }
87  }