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 }