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:
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.
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.
33   *
34   * @since 0.3
35   */
36  @SuppressWarnings({"PMD.GodClass", "PMD.TooManyMethods"})
37  public final class BracketsStructureCheck extends AbstractCheck {
38  
39      @Override
40      public int[] getDefaultTokens() {
41          return new int[] {
42              TokenTypes.LITERAL_NEW,
43              TokenTypes.METHOD_CALL,
44              TokenTypes.RESOURCE_SPECIFICATION,
45              TokenTypes.ANNOTATION,
46          };
47      }
48  
49      @Override
50      public int[] getAcceptableTokens() {
51          return this.getDefaultTokens();
52      }
53  
54      @Override
55      public int[] getRequiredTokens() {
56          return this.getDefaultTokens();
57      }
58  
59      @Override
60      public void visitToken(final DetailAST ast) {
61          if (ast.getType() == TokenTypes.RESOURCE_SPECIFICATION) {
62              this.checkResources(ast);
63          } else if (ast.getType() == TokenTypes.ANNOTATION) {
64              this.checkAnnotation(ast);
65          } else if (ast.getType() == TokenTypes.METHOD_CALL
66              || ast.getType() == TokenTypes.LITERAL_NEW) {
67              this.checkParams(ast);
68          } else {
69              final DetailAST brackets = ast.findFirstToken(TokenTypes.LPAREN);
70              if (brackets != null) {
71                  this.checkParams(brackets);
72              }
73          }
74      }
75  
76      /**
77       * Checks params statement to satisfy the rule.
78       * @param node Tree node, containing method call statement
79       */
80      private void checkParams(final DetailAST node) {
81          final DetailAST closing = node.findFirstToken(TokenTypes.RPAREN);
82          if (closing != null) {
83              this.checkLines(node, node.getLineNo(), closing.getLineNo());
84          }
85      }
86  
87      /**
88       * Checks params statement to satisfy the rule.
89       * @param node Tree node, containing method call statement
90       * @param start First line
91       * @param end Final line
92       */
93      private void checkLines(final DetailAST node, final int start,
94          final int end) {
95          if (start != end) {
96              final DetailAST elist = node.findFirstToken(TokenTypes.ELIST);
97              final int pline = BracketsStructureCheck.firstParamLine(elist);
98              if (pline == start) {
99                  this.log(start, "Parameters should start on a new line");
100             }
101             this.checkExpressionList(elist, end);
102         }
103     }
104 
105     /**
106      * Returns the line number of the first actual token inside the
107      * expression list. ELIST itself reports {@code lparen + 1} regardless
108      * of where the first parameter actually is, so we descend to the
109      * leftmost leaf to find the real position.
110      * @param elist Tree node, containing the expression list
111      * @return Line number of the first parameter token
112      */
113     private static int firstParamLine(final DetailAST elist) {
114         DetailAST leaf = elist;
115         while (leaf.getFirstChild() != null) {
116             leaf = leaf.getFirstChild();
117         }
118         final int line;
119         if (leaf.equals(elist)) {
120             line = elist.getLineNo();
121         } else {
122             line = leaf.getLineNo();
123         }
124         return line;
125     }
126 
127     /**
128      * Checks expression list if closing bracket is on new line.
129      * @param elist Tree node, containing expression list
130      * @param end Final line
131      */
132     private void checkExpressionList(final DetailAST elist, final int end) {
133         if (elist.getChildCount() > 0) {
134             DetailAST last = elist.getLastChild();
135             while (last.getChildCount() > 0) {
136                 last = last.getLastChild();
137             }
138             final int lline = last.getLineNo();
139             if (lline == end) {
140                 this.log(lline, "Closing bracket should be on a new line");
141             }
142         }
143     }
144 
145     /**
146      * Checks annotation with multi-line parameter list.
147      * @param node Tree node, containing the ANNOTATION
148      */
149     private void checkAnnotation(final DetailAST node) {
150         final DetailAST opening = node.findFirstToken(TokenTypes.LPAREN);
151         final DetailAST closing = node.findFirstToken(TokenTypes.RPAREN);
152         if (opening != null && closing != null
153             && opening.getLineNo() != closing.getLineNo()) {
154             final DetailAST first = opening.getNextSibling();
155             final DetailAST last = closing.getPreviousSibling();
156             final DetailAST rcurly =
157                 BracketsStructureCheck.arrayInitRcurly(first, last);
158             if (rcurly == null) {
159                 this.checkBoundsStart(first, opening);
160                 this.checkBoundsEnd(last, closing);
161             } else if (first.getLineNo() != rcurly.getLineNo()) {
162                 this.checkArrayBounds(first, rcurly);
163             }
164         }
165     }
166 
167     /**
168      * Returns the closing curly of an annotation array initializer when
169      * the annotation contains exactly one such child, otherwise null.
170      * @param first First child after the LPAREN
171      * @param last Last child before the RPAREN
172      * @return RCURLY token or null
173      */
174     private static DetailAST arrayInitRcurly(final DetailAST first,
175         final DetailAST last) {
176         DetailAST rcurly = null;
177         if (first != null && first.equals(last)
178             && first.getType() == TokenTypes.ANNOTATION_ARRAY_INIT) {
179             final DetailAST candidate = first.getLastChild();
180             if (candidate != null
181                 && candidate.getType() == TokenTypes.RCURLY) {
182                 rcurly = candidate;
183             }
184         }
185         return rcurly;
186     }
187 
188     /**
189      * Logs at the first/last content of a multi-line annotation array
190      * initializer. The empty initializer {@code {}} has no content
191      * before the closing curly, so only the end is checked.
192      * @param array The ANNOTATION_ARRAY_INIT token
193      * @param rcurly The closing RCURLY of that initializer
194      */
195     private void checkArrayBounds(final DetailAST array,
196         final DetailAST rcurly) {
197         final DetailAST inner = array.getFirstChild();
198         if (!inner.equals(rcurly)) {
199             this.checkBoundsStart(inner, array);
200         }
201         this.checkBoundsEnd(rcurly.getPreviousSibling(), rcurly);
202     }
203 
204     /**
205      * Logs a violation when the first content sits on the same line as
206      * the opening bracket.
207      * @param first First content token (may be null)
208      * @param start Opening bracket token
209      */
210     private void checkBoundsStart(final DetailAST first,
211         final DetailAST start) {
212         if (first != null && first.getLineNo() == start.getLineNo()) {
213             this.log(
214                 first.getLineNo(),
215                 "Parameters should start on a new line"
216             );
217         }
218     }
219 
220     /**
221      * Logs a violation when the last content sits on the same line as
222      * the closing bracket.
223      * @param last Last content token (may be null)
224      * @param end Closing bracket token
225      */
226     private void checkBoundsEnd(final DetailAST last, final DetailAST end) {
227         DetailAST leaf = last;
228         while (leaf != null && leaf.getChildCount() > 0) {
229             leaf = leaf.getLastChild();
230         }
231         if (leaf != null && leaf.getLineNo() == end.getLineNo()) {
232             this.log(
233                 leaf.getLineNo(),
234                 "Closing bracket should be on a new line"
235             );
236         }
237     }
238 
239     /**
240      * Checks resources of try-with-resources statement.
241      * @param node Tree node, containing the RESOURCE_SPECIFICATION
242      */
243     private void checkResources(final DetailAST node) {
244         final DetailAST opening = node.findFirstToken(TokenTypes.LPAREN);
245         final DetailAST closing = node.findFirstToken(TokenTypes.RPAREN);
246         if (opening != null && closing != null
247             && opening.getLineNo() != closing.getLineNo()) {
248             this.checkResourceBody(node, opening, closing);
249         }
250     }
251 
252     /**
253      * Checks RESOURCES body inside a multiline try-with-resources.
254      * @param node Tree node with the RESOURCE_SPECIFICATION
255      * @param opening The opening LPAREN token
256      * @param closing The closing RPAREN token
257      */
258     private void checkResourceBody(final DetailAST node,
259         final DetailAST opening, final DetailAST closing) {
260         final DetailAST resources = node.findFirstToken(TokenTypes.RESOURCES);
261         if (resources != null) {
262             if (resources.getLineNo() == opening.getLineNo()) {
263                 this.log(
264                     resources.getLineNo(),
265                     "Parameters should start on a new line"
266                 );
267             }
268             DetailAST last = resources.getLastChild();
269             while (last != null && last.getChildCount() > 0) {
270                 last = last.getLastChild();
271             }
272             if (last != null && last.getLineNo() == closing.getLineNo()) {
273                 this.log(
274                     last.getLineNo(),
275                     "Closing bracket should be on a new line"
276                 );
277             }
278         }
279     }
280 }