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.AbstractFileSetCheck;
8 import com.puppycrawl.tools.checkstyle.api.FileText;
9 import java.io.File;
10 import org.cactoos.text.Joined;
11
12 /**
13 * Make sure each line indentation is either:
14 * <ul>
15 * <li>the same as previous one or less
16 * <li>bigger than previous by exactly 4
17 * </ul>
18 * Also, if the previous non-empty line consists only of closing brackets
19 * (and optional trailing semicolon or comma), the current line indentation
20 * must not be greater than that of the closing bracket line, since the
21 * expression has been already terminated.
22 * All other cases must cause a failure.
23 * @since 0.3
24 */
25 public final class CascadeIndentationCheck extends AbstractFileSetCheck {
26
27 /**
28 * Exact indentation increase difference.
29 */
30 private static final int LINE_INDENT_DIFF = 4;
31
32 @Override
33 public void processFiltered(final File file, final FileText lines) {
34 int previous = 0;
35 boolean closer = false;
36 for (int pos = 0; pos < lines.size(); pos += 1) {
37 final String line = lines.get(pos);
38 final int current = CascadeIndentationCheck.indentation(line);
39 if (CascadeIndentationCheck.inCommentBlock(line)
40 || line.isEmpty()) {
41 continue;
42 }
43 if (current > previous
44 && current != previous
45 + CascadeIndentationCheck.LINE_INDENT_DIFF) {
46 this.log(
47 pos + 1,
48 String.format(
49 new Joined(
50 "",
51 "Indentation (%d) must be same or ",
52 "less than previous line (%d), or ",
53 "bigger by exactly 4"
54 ).toString(),
55 current,
56 previous
57 )
58 );
59 } else if (closer && current > previous) {
60 this.log(
61 pos + 1,
62 String.format(
63 new Joined(
64 "",
65 "Indentation (%d) must not be greater ",
66 "than the closing bracket line (%d)"
67 ).toString(),
68 current,
69 previous
70 )
71 );
72 }
73 previous = current;
74 closer = CascadeIndentationCheck.isClosingBracketLine(line);
75 }
76 }
77
78 /**
79 * Tells whether the line consists only of closing brackets, optionally
80 * followed by a comma or a semicolon and surrounding whitespace.
81 * @param line Input line
82 * @return True if the line is a standalone closing bracket line
83 */
84 private static boolean isClosingBracketLine(final String line) {
85 final String trimmed = line.trim();
86 boolean result = !trimmed.isEmpty()
87 && CascadeIndentationCheck.isClosingBracket(trimmed.charAt(0));
88 for (int idx = 0; result && idx < trimmed.length(); idx += 1) {
89 result = CascadeIndentationCheck.isAllowedTail(trimmed.charAt(idx));
90 }
91 return result;
92 }
93
94 /**
95 * Tells whether the character is a closing bracket.
96 * @param chr Character
97 * @return True if it is one of ')', ']', '}'
98 */
99 private static boolean isClosingBracket(final char chr) {
100 return chr == ')' || chr == ']' || chr == '}';
101 }
102
103 /**
104 * Tells whether a character is allowed inside a standalone
105 * closing-bracket line (closing bracket, comma, semicolon or
106 * whitespace).
107 * @param chr Character
108 * @return True if the character is allowed
109 */
110 private static boolean isAllowedTail(final char chr) {
111 return CascadeIndentationCheck.isClosingBracket(chr)
112 || chr == ';' || chr == ','
113 || Character.isWhitespace(chr);
114 }
115
116 /**
117 * Checks if the line belongs to a comment block.
118 * @param line Input
119 * @return True if the line belongs to a comment block
120 */
121 private static boolean inCommentBlock(final String line) {
122 final String trimmed = line.trim();
123 return !trimmed.isEmpty()
124 && (trimmed.charAt(0) == '*'
125 || trimmed.startsWith("/*")
126 || trimmed.startsWith("*/")
127 );
128 }
129
130 /**
131 * Calculates indentation of a line.
132 * @param line Input line
133 * @return Indentation of the given line
134 */
135 private static int indentation(final String line) {
136 int result = 0;
137 for (int pos = 0; pos < line.length(); pos += 1) {
138 if (!Character.isWhitespace(line.charAt(pos))) {
139 break;
140 }
141 result += 1;
142 }
143 return result;
144 }
145 }