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