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.whitespace;
21  
22  import javax.annotation.Nullable;
23  
24  import com.puppycrawl.tools.checkstyle.StatelessCheck;
25  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
26  import com.puppycrawl.tools.checkstyle.api.DetailAST;
27  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
28  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
29  import com.puppycrawl.tools.checkstyle.utils.NullUtil;
30  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
31  
32  /**
33   * <div>
34   * Checks that a whitespace is present before the opening brace of an empty body.
35   * </div>
36   *
37   * <p>
38   * This check applies to the following tokens:
39   * Methods and constructors, Classes, interfaces, enums, and records,
40   * Annotation definitions, Loops: {@code while}, {@code for}, {@code do-while},
41   * Lambdas and anonymous classes, Static initializer blocks,
42   * Try-catch-finally statements, Synchronized blocks, Switch statements.
43   * </p>
44   *
45   * <p>
46   * A body containing only comments is considered empty by this check.
47   * </p>
48   *
49   * @since 13.9.0
50   */
51  @StatelessCheck
52  public class WhitespaceBeforeEmptyBodyCheck extends AbstractCheck {
53  
54      /**
55       * A key is pointing to the warning message text in "messages.properties"
56       * file.
57       */
58      public static final String MSG_KEY = "ws.notPreceded";
59  
60      /**
61       * Creates a new {@code WhitespaceBeforeEmptyBodyCheck} instance.
62       */
63      public WhitespaceBeforeEmptyBodyCheck() {
64          // no code by default
65      }
66  
67      @Override
68      public int[] getDefaultTokens() {
69          return getAcceptableTokens();
70      }
71  
72      @Override
73      public int[] getAcceptableTokens() {
74          return new int[] {
75              TokenTypes.METHOD_DEF,
76              TokenTypes.CTOR_DEF,
77              TokenTypes.COMPACT_CTOR_DEF,
78              TokenTypes.CLASS_DEF,
79              TokenTypes.INTERFACE_DEF,
80              TokenTypes.RECORD_DEF,
81              TokenTypes.ENUM_DEF,
82              TokenTypes.ANNOTATION_DEF,
83              TokenTypes.LITERAL_NEW,
84              TokenTypes.LITERAL_DO,
85              TokenTypes.LITERAL_WHILE,
86              TokenTypes.LITERAL_FOR,
87              TokenTypes.LITERAL_IF,
88              TokenTypes.LITERAL_ELSE,
89              TokenTypes.STATIC_INIT,
90              TokenTypes.LITERAL_TRY,
91              TokenTypes.LITERAL_CATCH,
92              TokenTypes.LITERAL_FINALLY,
93              TokenTypes.LITERAL_SYNCHRONIZED,
94              TokenTypes.LITERAL_SWITCH,
95              TokenTypes.LAMBDA,
96          };
97      }
98  
99      @Override
100     public int[] getRequiredTokens() {
101         return CommonUtil.EMPTY_INT_ARRAY;
102     }
103 
104     @Override
105     public void visitToken(DetailAST ast) {
106         final DetailAST lcurly = findOpeningBrace(ast);
107         if (lcurly != null && isBodyEmpty(lcurly) && !precededByWhiteSpace(lcurly)) {
108             log(lcurly, MSG_KEY, lcurly.getText());
109         }
110     }
111 
112     /**
113      * Returns left curly token for an ast.
114      *
115      * @param ast the AST to get brace token for
116      * @return the left curly token, or null if the construct has no-body
117      */
118     @Nullable
119     private static DetailAST findOpeningBrace(DetailAST ast) {
120         final int astType = ast.getType();
121         return switch (astType) {
122             case TokenTypes.CLASS_DEF,
123                  TokenTypes.INTERFACE_DEF,
124                  TokenTypes.ENUM_DEF,
125                  TokenTypes.RECORD_DEF,
126                  TokenTypes.ANNOTATION_DEF,
127                  TokenTypes.LITERAL_NEW -> {
128                 final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
129                 DetailAST lcurly = null;
130                 if (objBlock != null) {
131                     lcurly = objBlock.findFirstToken(TokenTypes.LCURLY);
132                 }
133                 yield lcurly;
134             }
135             case TokenTypes.LITERAL_SWITCH -> ast.findFirstToken(TokenTypes.LCURLY);
136             default -> ast.findFirstToken(TokenTypes.SLIST);
137         };
138     }
139 
140     /**
141      * Checks if the opening brace is preceded by whitespace.
142      *
143      * @param lcurly token representing the opening brace
144      * @return true if preceded by whitespace
145      */
146     private static boolean precededByWhiteSpace(DetailAST lcurly) {
147         DetailAST previousToken = lcurly.getPreviousSibling();
148         if (previousToken != null) {
149             previousToken = descendToLeafNode(previousToken);
150         }
151         if (previousToken == null
152                 || previousToken.getParent().getType() == TokenTypes.LAMBDA) {
153             previousToken = NullUtil.notNull(lcurly.getParent());
154         }
155         if (previousToken.getType() == TokenTypes.OBJBLOCK) {
156             previousToken = NullUtil.notNull(previousToken.getPreviousSibling());
157             previousToken = descendToLeafNode(previousToken);
158         }
159 
160         return !TokenUtil.areOnSameLine(lcurly, previousToken)
161                 || isSpaceBetweenTokens(previousToken, lcurly);
162     }
163 
164     /**
165      * Return the last nested child of the token.
166      *
167      * @param token the ast token
168      * @return nested child
169      */
170     private static DetailAST descendToLeafNode(DetailAST token) {
171         DetailAST current = token;
172         while (current.hasChildren()) {
173             current = current.getLastChild();
174         }
175         return current;
176     }
177 
178     /**
179      * Checks if the body is empty.
180      *
181      * @param lcurly the token representing the opening brace
182      * @return true if the body is empty
183      */
184     private static boolean isBodyEmpty(DetailAST lcurly) {
185         final DetailAST nextSibling = lcurly.getNextSibling();
186         final DetailAST firstChild = lcurly.getFirstChild();
187         return nextSibling != null && nextSibling.getType() == TokenTypes.RCURLY
188                 || firstChild != null && firstChild.getType() == TokenTypes.RCURLY;
189     }
190 
191     /**
192      * Checks if space exists between first and second token.
193      *
194      * @param firstToken token in ast which is behind second token
195      * @param secondToken token next to firstToken
196      * @return true if space exists between first and second tokens
197      */
198     private static boolean isSpaceBetweenTokens(DetailAST firstToken, DetailAST secondToken) {
199         String tokenText = firstToken.getText();
200         if (firstToken.getType() == TokenTypes.STATIC_INIT) {
201             tokenText = "static";
202         }
203         final int firstLastColumn = firstToken.getColumnNo()
204                 + tokenText.length() - 1;
205         return secondToken.getColumnNo() - firstLastColumn > 1;
206     }
207 
208 }