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. Moreover, two such closing
22   * bracket lines may not follow each other at the very same indentation,
23   * since every one of them closes exactly one more nesting level and thus
24   * must be shallower than the one above it.
25   * All other cases must cause a failure.
26   *
27   * @since 0.3
28   */
29  public final class CascadeIndentationCheck extends AbstractFileSetCheck {
30  
31      /**
32       * Default constructor.
33       */
34      public CascadeIndentationCheck() {
35          // nothing to initialize
36      }
37  
38      @Override
39      public void processFiltered(final File file, final FileText lines) {
40          int previous = 0;
41          boolean closer = false;
42          for (int pos = 0; pos < lines.size(); pos += 1) {
43              final String line = lines.get(pos);
44              final int current = CascadeIndentationCheck.indentation(line);
45              if (CascadeIndentationCheck.inCommentBlock(line)
46                  || line.isEmpty()) {
47                  continue;
48              }
49              final String message = CascadeIndentationCheck.violation(
50                  line, current, previous, closer
51              );
52              if (!message.isEmpty()) {
53                  this.log(pos + 1, message);
54              }
55              previous = current;
56              closer = CascadeIndentationCheck.isClosingBracketLine(line);
57          }
58      }
59  
60      private static String violation(final String line, final int current,
61          final int previous, final boolean closer) {
62          final String message;
63          if (current > previous
64              && current != previous + 4) {
65              message = String.format(
66                  new Joined(
67                      "",
68                      "Indentation (%d) must be same or ",
69                      "less than previous line (%d), or ",
70                      "bigger by exactly 4"
71                  ).toString(),
72                  current,
73                  previous
74              );
75          } else if (closer && current > previous) {
76              message = String.format(
77                  new Joined(
78                      "",
79                      "Indentation (%d) must not be greater ",
80                      "than the closing bracket line (%d)"
81                  ).toString(),
82                  current,
83                  previous
84              );
85          } else if (closer && current == previous
86              && CascadeIndentationCheck.isClosingBracketLine(line)) {
87              message = String.format(
88                  new Joined(
89                      "",
90                      "Indentation (%d) of this closing bracket line must be ",
91                      "less than the one of the closing bracket line above it"
92                  ).toString(),
93                  current
94              );
95          } else {
96              message = "";
97          }
98          return message;
99      }
100 
101     private static boolean isClosingBracketLine(final String line) {
102         final String trimmed = line.trim();
103         boolean result = !trimmed.isEmpty()
104             && CascadeIndentationCheck.isClosingBracket(trimmed.charAt(0));
105         for (int idx = 0; result && idx < trimmed.length(); idx += 1) {
106             result = CascadeIndentationCheck.isAllowedTail(trimmed.charAt(idx));
107         }
108         return result;
109     }
110 
111     private static boolean isClosingBracket(final char chr) {
112         return chr == ')' || chr == ']' || chr == '}';
113     }
114 
115     private static boolean isAllowedTail(final char chr) {
116         return CascadeIndentationCheck.isClosingBracket(chr)
117             || chr == ';' || chr == ','
118             || Character.isWhitespace(chr);
119     }
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     private static int indentation(final String line) {
131         int result = 0;
132         for (int pos = 0; pos < line.length(); pos += 1) {
133             if (!Character.isWhitespace(line.charAt(pos))) {
134                 break;
135             }
136             result += 1;
137         }
138         return result;
139     }
140 }