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  
11  /**
12   * Checks node/closing brackets to be the last symbols on the line.
13   *
14   * <p>This is how a correct bracket structure should look like:</p>
15   *
16   * <pre>
17   * String text = String.format(
18   *   "some text: %s",
19   *   new Foo().with(
20   *     "abc",
21   *     "foo"
22   *   )
23   * );
24   * </pre>
25   *
26   * <p>The motivation for such formatting is simple - we want to see the entire
27   * block as fast as possible. When you look at a block of code you should be
28   * able to see where it starts and where it ends. In exactly the same way
29   * we organize curled brackets.</p>
30   *
31   * <p>In other words, when you open a bracket and can't close it at the same
32   * line - you should leave it as the last symbol at this line.</p>
33   *
34   * @since 0.3
35   */
36  @SuppressWarnings("PMD.GodClass")
37  public final class BracketsStructureCheck extends AbstractCheck {
38  
39      /**
40       * Default constructor.
41       */
42      public BracketsStructureCheck() {
43          // nothing to initialize
44      }
45  
46      @Override
47      public int[] getDefaultTokens() {
48          return new int[] {
49              TokenTypes.LITERAL_NEW,
50              TokenTypes.METHOD_CALL,
51              TokenTypes.RESOURCE_SPECIFICATION,
52              TokenTypes.ANNOTATION,
53          };
54      }
55  
56      @Override
57      public int[] getAcceptableTokens() {
58          return this.getDefaultTokens();
59      }
60  
61      @Override
62      public int[] getRequiredTokens() {
63          return this.getDefaultTokens();
64      }
65  
66      @Override
67      public void visitToken(final DetailAST ast) {
68          if (ast.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
69              this.checkResources(ast);
70          } else if (ast.getType() == TokenTypes.ANNOTATION) {
71              this.checkAnnotation(ast);
72          } else if (ast.getType() == TokenTypes.METHOD_CALL
73              || ast.getType() == TokenTypes.LITERAL_NEW) {
74              this.checkParams(ast);
75          } else {
76              final DetailAST brackets = ast.findFirstToken(TokenTypes.LPAREN);
77              if (brackets != null) {
78                  this.checkParams(brackets);
79              }
80          }
81      }
82  
83      private void checkParams(final DetailAST node) {
84          final DetailAST closing = node.findFirstToken(TokenTypes.RPAREN);
85          if (closing != null) {
86              this.checkLines(node, node.getLineNo(), closing.getLineNo());
87          }
88      }
89  
90      private void checkLines(final DetailAST node, final int start,
91          final int end) {
92          if (start != end) {
93              final DetailAST elist = node.findFirstToken(TokenTypes.ELIST);
94              final int pline = BracketsStructureCheck.firstParamLine(elist);
95              if (pline == start) {
96                  this.log(start, "Parameters should start on a new line");
97              }
98              this.checkExpressionList(elist, end);
99          }
100     }
101 
102     private static int firstParamLine(final DetailAST elist) {
103         DetailAST leaf = elist;
104         while (leaf.getFirstChild() != null) {
105             leaf = leaf.getFirstChild();
106         }
107         final int line;
108         if (leaf.equals(elist)) {
109             line = elist.getLineNo();
110         } else {
111             line = leaf.getLineNo();
112         }
113         return line;
114     }
115 
116     private void checkExpressionList(final DetailAST elist, final int end) {
117         if (elist.getChildCount() > 0) {
118             DetailAST last = elist.getLastChild();
119             while (last.getChildCount() > 0) {
120                 last = last.getLastChild();
121             }
122             final int lline = last.getLineNo();
123             if (lline == end) {
124                 this.log(lline, "Closing bracket should be on a new line");
125             }
126         }
127     }
128 
129     private void checkAnnotation(final DetailAST node) {
130         final DetailAST opening = node.findFirstToken(TokenTypes.LPAREN);
131         final DetailAST closing = node.findFirstToken(TokenTypes.RPAREN);
132         if (opening != null && closing != null
133             && opening.getLineNo() != closing.getLineNo()) {
134             final DetailAST first = opening.getNextSibling();
135             final DetailAST last = closing.getPreviousSibling();
136             final DetailAST rcurly =
137                 BracketsStructureCheck.arrayInitRcurly(first, last);
138             if (rcurly == null) {
139                 this.checkBoundsStart(first, opening);
140                 this.checkBoundsEnd(last, closing);
141             } else if (first.getLineNo() != rcurly.getLineNo()) {
142                 this.checkArrayBounds(first, rcurly);
143             }
144         }
145     }
146 
147     private static DetailAST arrayInitRcurly(final DetailAST first,
148         final DetailAST last) {
149         DetailAST rcurly = null;
150         if (first != null && first.equals(last)
151             && first.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
152             final DetailAST candidate = first.getLastChild();
153             if (candidate != null
154                 && candidate.getType() == TokenTypes.RCURLY) {
155                 rcurly = candidate;
156             }
157         }
158         return rcurly;
159     }
160 
161     private void checkArrayBounds(final DetailAST array,
162         final DetailAST rcurly) {
163         final DetailAST inner = array.getFirstChild();
164         if (!inner.equals(rcurly)) {
165             this.checkBoundsStart(inner, array);
166         }
167         this.checkBoundsEnd(rcurly.getPreviousSibling(), rcurly);
168     }
169 
170     private void checkBoundsStart(final DetailAST first,
171         final DetailAST start) {
172         if (first != null && first.getLineNo() == start.getLineNo()) {
173             this.log(
174                 first.getLineNo(),
175                 "Parameters should start on a new line"
176             );
177         }
178     }
179 
180     private void checkBoundsEnd(final DetailAST last, final DetailAST end) {
181         DetailAST leaf = last;
182         while (leaf != null && leaf.getChildCount() > 0) {
183             leaf = leaf.getLastChild();
184         }
185         if (leaf != null && leaf.getLineNo() == end.getLineNo()) {
186             this.log(
187                 leaf.getLineNo(),
188                 "Closing bracket should be on a new line"
189             );
190         }
191     }
192 
193     private void checkResources(final DetailAST node) {
194         final DetailAST opening = node.findFirstToken(TokenTypes.LPAREN);
195         final DetailAST closing = node.findFirstToken(TokenTypes.RPAREN);
196         if (opening != null && closing != null
197             && opening.getLineNo() != closing.getLineNo()) {
198             this.checkResourceBody(node, opening, closing);
199         }
200     }
201 
202     private void checkResourceBody(final DetailAST node,
203         final DetailAST opening, final DetailAST closing) {
204         final DetailAST resources = node.findFirstToken(TokenTypes.RESOURCES);
205         if (resources != null) {
206             if (resources.getLineNo() == opening.getLineNo()) {
207                 this.log(
208                     resources.getLineNo(),
209                     "Parameters should start on a new line"
210                 );
211             }
212             DetailAST last = resources.getLastChild();
213             while (last != null && last.getChildCount() > 0) {
214                 last = last.getLastChild();
215             }
216             if (last != null && last.getLineNo() == closing.getLineNo()) {
217                 this.log(
218                     last.getLineNo(),
219                     "Closing bracket should be on a new line"
220                 );
221             }
222         }
223     }
224 }