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.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 }