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  import java.util.Optional;
24  import java.util.Set;
25  
26  import javax.annotation.Nullable;
27  
28  import com.puppycrawl.tools.checkstyle.StatelessCheck;
29  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
30  import com.puppycrawl.tools.checkstyle.api.DetailAST;
31  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
32  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
33  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
34  
35  /**
36   * <div>
37   * Checks for the placement of left curly braces (<code>'{'</code>) for code blocks.
38   * </div>
39   *
40   * @since 3.0
41   */
42  @StatelessCheck
43  public class LeftCurlyCheck
44      extends AbstractCheck {
45  
46      /**
47       * A key is pointing to the warning message text in "messages.properties"
48       * file.
49       */
50      public static final String MSG_KEY_LINE_NEW = "line.new";
51  
52      /**
53       * A key is pointing to the warning message text in "messages.properties"
54       * file.
55       */
56      public static final String MSG_KEY_LINE_PREVIOUS = "line.previous";
57  
58      /**
59       * A key is pointing to the warning message text in "messages.properties"
60       * file.
61       */
62      public static final String MSG_KEY_LINE_BREAK_AFTER = "line.break.after";
63  
64      /** Open curly brace literal. */
65      private static final String OPEN_CURLY_BRACE = "{";
66  
67      /** Allow to ignore enums when left curly brace policy is EOL. */
68      private boolean ignoreEnums = true;
69  
70      /**
71       * Specify the policy on placement of a left curly brace (<code>'{'</code>).
72       */
73      private LeftCurlyOption option = LeftCurlyOption.EOL;
74  
75      /**
76       * Creates a new {@code LeftCurlyCheck} instance.
77       */
78      public LeftCurlyCheck() {
79          // no code by default
80      }
81  
82      /**
83       * Setter to specify the policy on placement of a left curly brace (<code>'{'</code>).
84       *
85       * @param optionStr string to decode option from
86       * @throws IllegalArgumentException if unable to decode
87       * @since 3.0
88       */
89      public void setOption(String optionStr) {
90          option = LeftCurlyOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
91      }
92  
93      /**
94       * Setter to allow to ignore enums when left curly brace policy is EOL.
95       *
96       * @param ignoreEnums check's option for ignoring enums.
97       * @since 6.9
98       */
99      public void setIgnoreEnums(boolean ignoreEnums) {
100         this.ignoreEnums = ignoreEnums;
101     }
102 
103     @Override
104     public int[] getDefaultTokens() {
105         return getAcceptableTokens();
106     }
107 
108     @Override
109     public int[] getAcceptableTokens() {
110         return new int[] {
111             TokenTypes.ANNOTATION_DEF,
112             TokenTypes.CLASS_DEF,
113             TokenTypes.CTOR_DEF,
114             TokenTypes.ENUM_CONSTANT_DEF,
115             TokenTypes.ENUM_DEF,
116             TokenTypes.INTERFACE_DEF,
117             TokenTypes.LAMBDA,
118             TokenTypes.LITERAL_CASE,
119             TokenTypes.LITERAL_CATCH,
120             TokenTypes.LITERAL_DEFAULT,
121             TokenTypes.LITERAL_DO,
122             TokenTypes.LITERAL_ELSE,
123             TokenTypes.LITERAL_FINALLY,
124             TokenTypes.LITERAL_FOR,
125             TokenTypes.LITERAL_IF,
126             TokenTypes.LITERAL_SWITCH,
127             TokenTypes.LITERAL_SYNCHRONIZED,
128             TokenTypes.LITERAL_TRY,
129             TokenTypes.LITERAL_WHILE,
130             TokenTypes.METHOD_DEF,
131             TokenTypes.OBJBLOCK,
132             TokenTypes.STATIC_INIT,
133             TokenTypes.RECORD_DEF,
134             TokenTypes.COMPACT_CTOR_DEF,
135             TokenTypes.SWITCH_RULE,
136         };
137     }
138 
139     @Override
140     public int[] getRequiredTokens() {
141         return CommonUtil.EMPTY_INT_ARRAY;
142     }
143 
144     /**
145      * Visits token.
146      *
147      * @param ast the token to process
148      * @noinspection SwitchStatementWithTooManyBranches
149      * @noinspectionreason SwitchStatementWithTooManyBranches - we cannot reduce
150      *      the number of branches in this switch statement, since many tokens
151      *      require specific methods to find the first left curly
152      */
153     @Override
154     public void visitToken(DetailAST ast) {
155         final DetailAST startToken;
156         final DetailAST brace = switch (ast.getType()) {
157             case TokenTypes.CTOR_DEF, TokenTypes.METHOD_DEF, TokenTypes.COMPACT_CTOR_DEF -> {
158                 startToken = skipModifierAnnotations(ast);
159                 yield ast.findFirstToken(TokenTypes.SLIST);
160             }
161             case TokenTypes.INTERFACE_DEF, TokenTypes.CLASS_DEF, TokenTypes.ANNOTATION_DEF,
162                  TokenTypes.ENUM_DEF, TokenTypes.ENUM_CONSTANT_DEF, TokenTypes.RECORD_DEF -> {
163                 startToken = skipModifierAnnotations(ast);
164                 yield ast.findFirstToken(TokenTypes.OBJBLOCK);
165             }
166             case TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_CATCH,
167                  TokenTypes.LITERAL_SYNCHRONIZED, TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_TRY,
168                  TokenTypes.LITERAL_FINALLY, TokenTypes.LITERAL_DO,
169                  TokenTypes.LITERAL_IF, TokenTypes.STATIC_INIT, TokenTypes.LAMBDA,
170                  TokenTypes.SWITCH_RULE -> {
171                 startToken = ast;
172                 yield ast.findFirstToken(TokenTypes.SLIST);
173             }
174             case TokenTypes.LITERAL_ELSE -> {
175                 startToken = ast;
176                 yield getBraceAsFirstChild(ast);
177             }
178             case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT -> {
179                 startToken = ast;
180                 yield getBraceFromSwitchMember(ast);
181             }
182             default -> {
183                 // ATTENTION! We have default here, but we expect case TokenTypes.METHOD_DEF,
184                 // TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_DO only.
185                 // It has been done to improve coverage to 100%. I couldn't replace it with
186                 // if-else-if block because code was ugly and didn't pass pmd check.
187 
188                 startToken = ast;
189                 yield ast.findFirstToken(TokenTypes.LCURLY);
190             }
191         };
192 
193         if (brace != null && !isBraceVerifiedByParent(ast)) {
194             verifyBrace(brace, startToken);
195         }
196     }
197 
198     /**
199      * Checks whether the brace of the given token is verified through its parent
200      * as well. An {@code OBJBLOCK} shares its brace with the type definition it
201      * belongs to, and the case label of an arrow switch shares its brace with the
202      * {@code SWITCH_RULE}. Without this the same brace is reported twice.
203      *
204      * @param ast the token being visited
205      * @return {@code true} if the parent already verifies the same brace
206      */
207     private boolean isBraceVerifiedByParent(DetailAST ast) {
208         return TokenUtil.isOfType(ast,
209                     TokenTypes.OBJBLOCK, TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT)
210                 && isConfigured(ast.getParent());
211     }
212 
213     /**
214      * Checks whether the token type of the given node is configured for this check.
215      *
216      * @param ast the token to check
217      * @return {@code true} if this check visits that token type
218      */
219     private boolean isConfigured(DetailAST ast) {
220         final Set<String> configuredTokens = getTokenNames();
221         final boolean result;
222         if (configuredTokens.isEmpty()) {
223             result = TokenUtil.isOfType(ast, getDefaultTokens());
224         }
225         else {
226             result = configuredTokens.contains(TokenUtil.getTokenName(ast.getType()));
227         }
228         return result;
229     }
230 
231     /**
232      * Gets the brace of a switch statement/ expression member.
233      *
234      * @param ast {@code DetailAST}.
235      * @return {@code DetailAST} if the first child is {@code TokenTypes.SLIST},
236      *     {@code null} otherwise.
237      */
238     @Nullable
239     private static DetailAST getBraceFromSwitchMember(DetailAST ast) {
240         final DetailAST brace;
241         final DetailAST parent = ast.getParent();
242         if (parent.getType() == TokenTypes.SWITCH_RULE) {
243             brace = parent.findFirstToken(TokenTypes.SLIST);
244         }
245         else {
246             brace = getBraceAsFirstChild(ast.getNextSibling());
247         }
248         return brace;
249     }
250 
251     /**
252      * Gets a SLIST if it is the first child of the AST.
253      *
254      * @param ast {@code DetailAST}.
255      * @return {@code DetailAST} if the first child is {@code TokenTypes.SLIST},
256      *     {@code null} otherwise.
257      */
258     @Nullable
259     private static DetailAST getBraceAsFirstChild(DetailAST ast) {
260         DetailAST brace = null;
261         if (ast != null) {
262             final DetailAST candidate = ast.getFirstChild();
263             if (candidate != null && candidate.getType() == TokenTypes.SLIST) {
264                 brace = candidate;
265             }
266         }
267         return brace;
268     }
269 
270     /**
271      * Skip all {@code TokenTypes.ANNOTATION}s to the first non-annotation.
272      *
273      * @param ast {@code DetailAST}.
274      * @return {@code DetailAST} or null if there are no annotations.
275      */
276     private static DetailAST skipModifierAnnotations(DetailAST ast) {
277         DetailAST resultNode = ast;
278         final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
279 
280         if (modifiers != null) {
281             resultNode = findLastAnnotation(modifiers)
282                     .map(annotation -> {
283                         final DetailAST nextNode;
284                         if (annotation.getNextSibling() == null) {
285                             nextNode = modifiers.getNextSibling();
286                         }
287                         else {
288                             nextNode = annotation.getNextSibling();
289                         }
290                         return nextNode;
291                     })
292                     .orElse(resultNode);
293         }
294         return resultNode;
295     }
296 
297     /**
298      * Find the last token of type {@code TokenTypes.ANNOTATION}
299      * under the given set of modifiers.
300      *
301      * @param modifiers {@code DetailAST}.
302      * @return Optional containing the last annotation, if found.
303      */
304     private static Optional<DetailAST> findLastAnnotation(DetailAST modifiers) {
305         DetailAST annotation = modifiers.findFirstToken(TokenTypes.ANNOTATION);
306         while (annotation != null && annotation.getNextSibling() != null
307                && annotation.getNextSibling().getType() == TokenTypes.ANNOTATION) {
308             annotation = annotation.getNextSibling();
309         }
310         return Optional.ofNullable(annotation);
311     }
312 
313     /**
314      * Verifies that a specified left curly brace is placed correctly
315      * according to policy.
316      *
317      * @param brace token for left curly brace
318      * @param startToken token for start of expression
319      */
320     private void verifyBrace(final DetailAST brace,
321                              final DetailAST startToken) {
322         final String braceLine = getLine(brace.getLineNo() - 1);
323 
324         // Check for being told to ignore, or have '{}' which is a special case
325         if (braceLine.length() <= brace.getColumnNo() + 1
326                 || braceLine.charAt(brace.getColumnNo() + 1) != '}') {
327             if (option == LeftCurlyOption.NL) {
328                 if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
329                     log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
330                 }
331             }
332             else if (option == LeftCurlyOption.EOL) {
333                 validateEol(brace, braceLine);
334             }
335             else if (!TokenUtil.areOnSameLine(startToken, brace)) {
336                 validateNewLinePosition(brace, startToken, braceLine);
337             }
338         }
339     }
340 
341     /**
342      * Validate EOL case.
343      *
344      * @param brace brace AST
345      * @param braceLine line content
346      */
347     private void validateEol(DetailAST brace, String braceLine) {
348         if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
349             log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
350         }
351         if (!hasLineBreakAfter(brace)) {
352             log(brace, MSG_KEY_LINE_BREAK_AFTER, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
353         }
354     }
355 
356     /**
357      * Validate token on new Line position.
358      *
359      * @param brace brace AST
360      * @param startToken start Token
361      * @param braceLine content of line with Brace
362      */
363     private void validateNewLinePosition(DetailAST brace, DetailAST startToken, String braceLine) {
364         // not on the same line
365         if (startToken.getLineNo() + 1 == brace.getLineNo()) {
366             if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
367                 log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
368             }
369             else {
370                 log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
371             }
372         }
373         else if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
374             log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
375         }
376     }
377 
378     /**
379      * Checks if left curly has line break after.
380      *
381      * @param leftCurly
382      *        Left curly token.
383      * @return
384      *        True, left curly has line break after.
385      */
386     private boolean hasLineBreakAfter(DetailAST leftCurly) {
387         DetailAST nextToken = null;
388         if (leftCurly.getType() == TokenTypes.SLIST) {
389             nextToken = leftCurly.getFirstChild();
390         }
391         else if (!ignoreEnums) {
392             if (leftCurly.getParent().getParent().getType() == TokenTypes.ENUM_DEF) {
393                 nextToken = leftCurly.getNextSibling();
394             }
395             else if (leftCurly.getParent().getType() == TokenTypes.ENUM_DEF) {
396                 // the brace of the enum body is the first child of its OBJBLOCK
397                 nextToken = leftCurly.getFirstChild().getNextSibling();
398             }
399         }
400         return nextToken == null
401                 || nextToken.getType() == TokenTypes.RCURLY
402                 || !TokenUtil.areOnSameLine(leftCurly, nextToken);
403     }
404 
405 }