View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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         };
127     }
128 
129     @Override
130     public int[] getRequiredTokens() {
131         return CommonUtil.EMPTY_INT_ARRAY;
132     }
133 
134     /**
135      * Visits token.
136      *
137      * @param ast the token to process
138      * @noinspection SwitchStatementWithTooManyBranches
139      * @noinspectionreason SwitchStatementWithTooManyBranches - we cannot reduce
140      *      the number of branches in this switch statement, since many tokens
141      *      require specific methods to find the first left curly
142      */
143     @Override
144     public void visitToken(DetailAST ast) {
145         final DetailAST startToken;
146         final DetailAST brace = switch (ast.getType()) {
147             case TokenTypes.CTOR_DEF, TokenTypes.METHOD_DEF, TokenTypes.COMPACT_CTOR_DEF -> {
148                 startToken = skipModifierAnnotations(ast);
149                 yield ast.findFirstToken(TokenTypes.SLIST);
150             }
151             case TokenTypes.INTERFACE_DEF, TokenTypes.CLASS_DEF, TokenTypes.ANNOTATION_DEF,
152                  TokenTypes.ENUM_DEF, TokenTypes.ENUM_CONSTANT_DEF, TokenTypes.RECORD_DEF -> {
153                 startToken = skipModifierAnnotations(ast);
154                 yield ast.findFirstToken(TokenTypes.OBJBLOCK);
155             }
156             case TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_CATCH,
157                  TokenTypes.LITERAL_SYNCHRONIZED, TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_TRY,
158                  TokenTypes.LITERAL_FINALLY, TokenTypes.LITERAL_DO,
159                  TokenTypes.LITERAL_IF, TokenTypes.STATIC_INIT, TokenTypes.LAMBDA -> {
160                 startToken = ast;
161                 yield ast.findFirstToken(TokenTypes.SLIST);
162             }
163             case TokenTypes.LITERAL_ELSE -> {
164                 startToken = ast;
165                 yield getBraceAsFirstChild(ast);
166             }
167             case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT -> {
168                 startToken = ast;
169                 yield getBraceFromSwitchMember(ast);
170             }
171             default -> {
172                 // ATTENTION! We have default here, but we expect case TokenTypes.METHOD_DEF,
173                 // TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_DO only.
174                 // It has been done to improve coverage to 100%. I couldn't replace it with
175                 // if-else-if block because code was ugly and didn't pass pmd check.
176 
177                 startToken = ast;
178                 yield ast.findFirstToken(TokenTypes.LCURLY);
179             }
180         };
181 
182         if (brace != null) {
183             verifyBrace(brace, startToken);
184         }
185     }
186 
187     /**
188      * Gets the brace of a switch statement/ expression member.
189      *
190      * @param ast {@code DetailAST}.
191      * @return {@code DetailAST} if the first child is {@code TokenTypes.SLIST},
192      *     {@code null} otherwise.
193      */
194     @Nullable
195     private static DetailAST getBraceFromSwitchMember(DetailAST ast) {
196         final DetailAST brace;
197         final DetailAST parent = ast.getParent();
198         if (parent.getType() == TokenTypes.SWITCH_RULE) {
199             brace = parent.findFirstToken(TokenTypes.SLIST);
200         }
201         else {
202             brace = getBraceAsFirstChild(ast.getNextSibling());
203         }
204         return brace;
205     }
206 
207     /**
208      * Gets a SLIST if it is the first child of the AST.
209      *
210      * @param ast {@code DetailAST}.
211      * @return {@code DetailAST} if the first child is {@code TokenTypes.SLIST},
212      *     {@code null} otherwise.
213      */
214     @Nullable
215     private static DetailAST getBraceAsFirstChild(DetailAST ast) {
216         DetailAST brace = null;
217         if (ast != null) {
218             final DetailAST candidate = ast.getFirstChild();
219             if (candidate != null && candidate.getType() == TokenTypes.SLIST) {
220                 brace = candidate;
221             }
222         }
223         return brace;
224     }
225 
226     /**
227      * Skip all {@code TokenTypes.ANNOTATION}s to the first non-annotation.
228      *
229      * @param ast {@code DetailAST}.
230      * @return {@code DetailAST}.
231      */
232     private static DetailAST skipModifierAnnotations(DetailAST ast) {
233         DetailAST resultNode = ast;
234         final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
235 
236         if (modifiers != null) {
237             final DetailAST lastAnnotation = findLastAnnotation(modifiers);
238 
239             if (lastAnnotation != null) {
240                 if (lastAnnotation.getNextSibling() == null) {
241                     resultNode = modifiers.getNextSibling();
242                 }
243                 else {
244                     resultNode = lastAnnotation.getNextSibling();
245                 }
246             }
247         }
248         return resultNode;
249     }
250 
251     /**
252      * Find the last token of type {@code TokenTypes.ANNOTATION}
253      * under the given set of modifiers.
254      *
255      * @param modifiers {@code DetailAST}.
256      * @return {@code DetailAST} or null if there are no annotations.
257      */
258     private static DetailAST findLastAnnotation(DetailAST modifiers) {
259         DetailAST annotation = modifiers.findFirstToken(TokenTypes.ANNOTATION);
260         while (annotation != null && annotation.getNextSibling() != null
261                && annotation.getNextSibling().getType() == TokenTypes.ANNOTATION) {
262             annotation = annotation.getNextSibling();
263         }
264         return annotation;
265     }
266 
267     /**
268      * Verifies that a specified left curly brace is placed correctly
269      * according to policy.
270      *
271      * @param brace token for left curly brace
272      * @param startToken token for start of expression
273      */
274     private void verifyBrace(final DetailAST brace,
275                              final DetailAST startToken) {
276         final String braceLine = getLine(brace.getLineNo() - 1);
277 
278         // Check for being told to ignore, or have '{}' which is a special case
279         if (braceLine.length() <= brace.getColumnNo() + 1
280                 || braceLine.charAt(brace.getColumnNo() + 1) != '}') {
281             if (option == LeftCurlyOption.NL) {
282                 if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
283                     log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
284                 }
285             }
286             else if (option == LeftCurlyOption.EOL) {
287                 validateEol(brace, braceLine);
288             }
289             else if (!TokenUtil.areOnSameLine(startToken, brace)) {
290                 validateNewLinePosition(brace, startToken, braceLine);
291             }
292         }
293     }
294 
295     /**
296      * Validate EOL case.
297      *
298      * @param brace brace AST
299      * @param braceLine line content
300      */
301     private void validateEol(DetailAST brace, String braceLine) {
302         if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
303             log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
304         }
305         if (!hasLineBreakAfter(brace)) {
306             log(brace, MSG_KEY_LINE_BREAK_AFTER, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
307         }
308     }
309 
310     /**
311      * Validate token on new Line position.
312      *
313      * @param brace brace AST
314      * @param startToken start Token
315      * @param braceLine content of line with Brace
316      */
317     private void validateNewLinePosition(DetailAST brace, DetailAST startToken, String braceLine) {
318         // not on the same line
319         if (startToken.getLineNo() + 1 == brace.getLineNo()) {
320             if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
321                 log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
322             }
323             else {
324                 log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
325             }
326         }
327         else if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
328             log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
329         }
330     }
331 
332     /**
333      * Checks if left curly has line break after.
334      *
335      * @param leftCurly
336      *        Left curly token.
337      * @return
338      *        True, left curly has line break after.
339      */
340     private boolean hasLineBreakAfter(DetailAST leftCurly) {
341         DetailAST nextToken = null;
342         if (leftCurly.getType() == TokenTypes.SLIST) {
343             nextToken = leftCurly.getFirstChild();
344         }
345         else {
346             if (!ignoreEnums
347                     && leftCurly.getParent().getParent().getType() == TokenTypes.ENUM_DEF) {
348                 nextToken = leftCurly.getNextSibling();
349             }
350         }
351         return nextToken == null
352                 || nextToken.getType() == TokenTypes.RCURLY
353                 || !TokenUtil.areOnSameLine(leftCurly, nextToken);
354     }
355 
356 }