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