View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.blocks;
21  
22  import java.util.Locale;
23  
24  import javax.annotation.Nullable;
25  
26  import com.puppycrawl.tools.checkstyle.StatelessCheck;
27  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28  import com.puppycrawl.tools.checkstyle.api.DetailAST;
29  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
30  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
31  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
32  
33  /**
34   * <div>
35   * Checks for the placement of left curly braces (<code>'{'</code>) for code blocks.
36   * </div>
37   *
38   * @since 3.0
39   */
40  @StatelessCheck
41  public class LeftCurlyCheck
42      extends AbstractCheck {
43  
44      /**
45       * A key is pointing to the warning message text in "messages.properties"
46       * file.
47       */
48      public static final String MSG_KEY_LINE_NEW = "line.new";
49  
50      /**
51       * A key is pointing to the warning message text in "messages.properties"
52       * file.
53       */
54      public static final String MSG_KEY_LINE_PREVIOUS = "line.previous";
55  
56      /**
57       * A key is pointing to the warning message text in "messages.properties"
58       * file.
59       */
60      public static final String MSG_KEY_LINE_BREAK_AFTER = "line.break.after";
61  
62      /** Open curly brace literal. */
63      private static final String OPEN_CURLY_BRACE = "{";
64  
65      /** Allow to ignore enums when left curly brace policy is EOL. */
66      private boolean ignoreEnums = true;
67  
68      /**
69       * Specify the policy on placement of a left curly brace (<code>'{'</code>).
70       */
71      private LeftCurlyOption option = LeftCurlyOption.EOL;
72  
73      /**
74       * Setter to specify the policy on placement of a left curly brace (<code>'{'</code>).
75       *
76       * @param optionStr string to decode option from
77       * @throws IllegalArgumentException if unable to decode
78       * @since 3.0
79       */
80      public void setOption(String optionStr) {
81          option = LeftCurlyOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
82      }
83  
84      /**
85       * Setter to allow to ignore enums when left curly brace policy is EOL.
86       *
87       * @param ignoreEnums check's option for ignoring enums.
88       * @since 6.9
89       */
90      public void setIgnoreEnums(boolean ignoreEnums) {
91          this.ignoreEnums = ignoreEnums;
92      }
93  
94      @Override
95      public int[] getDefaultTokens() {
96          return getAcceptableTokens();
97      }
98  
99      @Override
100     public int[] getAcceptableTokens() {
101         return new int[] {
102             TokenTypes.ANNOTATION_DEF,
103             TokenTypes.CLASS_DEF,
104             TokenTypes.CTOR_DEF,
105             TokenTypes.ENUM_CONSTANT_DEF,
106             TokenTypes.ENUM_DEF,
107             TokenTypes.INTERFACE_DEF,
108             TokenTypes.LAMBDA,
109             TokenTypes.LITERAL_CASE,
110             TokenTypes.LITERAL_CATCH,
111             TokenTypes.LITERAL_DEFAULT,
112             TokenTypes.LITERAL_DO,
113             TokenTypes.LITERAL_ELSE,
114             TokenTypes.LITERAL_FINALLY,
115             TokenTypes.LITERAL_FOR,
116             TokenTypes.LITERAL_IF,
117             TokenTypes.LITERAL_SWITCH,
118             TokenTypes.LITERAL_SYNCHRONIZED,
119             TokenTypes.LITERAL_TRY,
120             TokenTypes.LITERAL_WHILE,
121             TokenTypes.METHOD_DEF,
122             TokenTypes.OBJBLOCK,
123             TokenTypes.STATIC_INIT,
124             TokenTypes.RECORD_DEF,
125             TokenTypes.COMPACT_CTOR_DEF,
126             TokenTypes.SWITCH_RULE,
127         };
128     }
129 
130     @Override
131     public int[] getRequiredTokens() {
132         return CommonUtil.EMPTY_INT_ARRAY;
133     }
134 
135     /**
136      * Visits token.
137      *
138      * @param ast the token to process
139      * @noinspection SwitchStatementWithTooManyBranches
140      * @noinspectionreason SwitchStatementWithTooManyBranches - we cannot reduce
141      *      the number of branches in this switch statement, since many tokens
142      *      require specific methods to find the first left curly
143      */
144     @Override
145     public void visitToken(DetailAST ast) {
146         final DetailAST startToken;
147         final DetailAST brace = switch (ast.getType()) {
148             case TokenTypes.CTOR_DEF, TokenTypes.METHOD_DEF, TokenTypes.COMPACT_CTOR_DEF -> {
149                 startToken = skipModifierAnnotations(ast);
150                 yield ast.findFirstToken(TokenTypes.SLIST);
151             }
152             case TokenTypes.INTERFACE_DEF, TokenTypes.CLASS_DEF, TokenTypes.ANNOTATION_DEF,
153                  TokenTypes.ENUM_DEF, TokenTypes.ENUM_CONSTANT_DEF, TokenTypes.RECORD_DEF -> {
154                 startToken = skipModifierAnnotations(ast);
155                 yield ast.findFirstToken(TokenTypes.OBJBLOCK);
156             }
157             case TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_CATCH,
158                  TokenTypes.LITERAL_SYNCHRONIZED, TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_TRY,
159                  TokenTypes.LITERAL_FINALLY, TokenTypes.LITERAL_DO,
160                  TokenTypes.LITERAL_IF, TokenTypes.STATIC_INIT, TokenTypes.LAMBDA,
161                  TokenTypes.SWITCH_RULE -> {
162                 startToken = ast;
163                 yield ast.findFirstToken(TokenTypes.SLIST);
164             }
165             case TokenTypes.LITERAL_ELSE -> {
166                 startToken = ast;
167                 yield getBraceAsFirstChild(ast);
168             }
169             case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT -> {
170                 startToken = ast;
171                 yield getBraceFromSwitchMember(ast);
172             }
173             default -> {
174                 // ATTENTION! We have default here, but we expect case TokenTypes.METHOD_DEF,
175                 // TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_DO only.
176                 // It has been done to improve coverage to 100%. I couldn't replace it with
177                 // if-else-if block because code was ugly and didn't pass pmd check.
178 
179                 startToken = ast;
180                 yield ast.findFirstToken(TokenTypes.LCURLY);
181             }
182         };
183 
184         if (brace != null) {
185             verifyBrace(brace, startToken);
186         }
187     }
188 
189     /**
190      * Gets the brace of a switch statement/ expression member.
191      *
192      * @param ast {@code DetailAST}.
193      * @return {@code DetailAST} if the first child is {@code TokenTypes.SLIST},
194      *     {@code null} otherwise.
195      */
196     @Nullable
197     private static DetailAST getBraceFromSwitchMember(DetailAST ast) {
198         final DetailAST brace;
199         final DetailAST parent = ast.getParent();
200         if (parent.getType() == TokenTypes.SWITCH_RULE) {
201             brace = parent.findFirstToken(TokenTypes.SLIST);
202         }
203         else {
204             brace = getBraceAsFirstChild(ast.getNextSibling());
205         }
206         return brace;
207     }
208 
209     /**
210      * Gets a SLIST if it is the first child of the AST.
211      *
212      * @param ast {@code DetailAST}.
213      * @return {@code DetailAST} if the first child is {@code TokenTypes.SLIST},
214      *     {@code null} otherwise.
215      */
216     @Nullable
217     private static DetailAST getBraceAsFirstChild(DetailAST ast) {
218         DetailAST brace = null;
219         if (ast != null) {
220             final DetailAST candidate = ast.getFirstChild();
221             if (candidate != null && candidate.getType() == TokenTypes.SLIST) {
222                 brace = candidate;
223             }
224         }
225         return brace;
226     }
227 
228     /**
229      * Skip all {@code TokenTypes.ANNOTATION}s to the first non-annotation.
230      *
231      * @param ast {@code DetailAST}.
232      * @return {@code DetailAST}.
233      */
234     private static DetailAST skipModifierAnnotations(DetailAST ast) {
235         DetailAST resultNode = ast;
236         final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
237 
238         if (modifiers != null) {
239             final DetailAST lastAnnotation = findLastAnnotation(modifiers);
240 
241             if (lastAnnotation != null) {
242                 if (lastAnnotation.getNextSibling() == null) {
243                     resultNode = modifiers.getNextSibling();
244                 }
245                 else {
246                     resultNode = lastAnnotation.getNextSibling();
247                 }
248             }
249         }
250         return resultNode;
251     }
252 
253     /**
254      * Find the last token of type {@code TokenTypes.ANNOTATION}
255      * under the given set of modifiers.
256      *
257      * @param modifiers {@code DetailAST}.
258      * @return {@code DetailAST} or null if there are no annotations.
259      */
260     private static DetailAST findLastAnnotation(DetailAST modifiers) {
261         DetailAST annotation = modifiers.findFirstToken(TokenTypes.ANNOTATION);
262         while (annotation != null && annotation.getNextSibling() != null
263                && annotation.getNextSibling().getType() == TokenTypes.ANNOTATION) {
264             annotation = annotation.getNextSibling();
265         }
266         return annotation;
267     }
268 
269     /**
270      * Verifies that a specified left curly brace is placed correctly
271      * according to policy.
272      *
273      * @param brace token for left curly brace
274      * @param startToken token for start of expression
275      */
276     private void verifyBrace(final DetailAST brace,
277                              final DetailAST startToken) {
278         final String braceLine = getLine(brace.getLineNo() - 1);
279 
280         // Check for being told to ignore, or have '{}' which is a special case
281         if (braceLine.length() <= brace.getColumnNo() + 1
282                 || braceLine.charAt(brace.getColumnNo() + 1) != '}') {
283             if (option == LeftCurlyOption.NL) {
284                 if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
285                     log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
286                 }
287             }
288             else if (option == LeftCurlyOption.EOL) {
289                 validateEol(brace, braceLine);
290             }
291             else if (!TokenUtil.areOnSameLine(startToken, brace)) {
292                 validateNewLinePosition(brace, startToken, braceLine);
293             }
294         }
295     }
296 
297     /**
298      * Validate EOL case.
299      *
300      * @param brace brace AST
301      * @param braceLine line content
302      */
303     private void validateEol(DetailAST brace, String braceLine) {
304         if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
305             log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
306         }
307         if (!hasLineBreakAfter(brace)) {
308             log(brace, MSG_KEY_LINE_BREAK_AFTER, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
309         }
310     }
311 
312     /**
313      * Validate token on new Line position.
314      *
315      * @param brace brace AST
316      * @param startToken start Token
317      * @param braceLine content of line with Brace
318      */
319     private void validateNewLinePosition(DetailAST brace, DetailAST startToken, String braceLine) {
320         // not on the same line
321         if (startToken.getLineNo() + 1 == brace.getLineNo()) {
322             if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
323                 log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
324             }
325             else {
326                 log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
327             }
328         }
329         else if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
330             log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
331         }
332     }
333 
334     /**
335      * Checks if left curly has line break after.
336      *
337      * @param leftCurly
338      *        Left curly token.
339      * @return
340      *        True, left curly has line break after.
341      */
342     private boolean hasLineBreakAfter(DetailAST leftCurly) {
343         DetailAST nextToken = null;
344         if (leftCurly.getType() == TokenTypes.SLIST) {
345             nextToken = leftCurly.getFirstChild();
346         }
347         else {
348             if (!ignoreEnums
349                     && leftCurly.getParent().getParent().getType() == TokenTypes.ENUM_DEF) {
350                 nextToken = leftCurly.getNextSibling();
351             }
352         }
353         return nextToken == null
354                 || nextToken.getType() == TokenTypes.RCURLY
355                 || !TokenUtil.areOnSameLine(leftCurly, nextToken);
356     }
357 }