001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.whitespace; 021 022import javax.annotation.Nullable; 023 024import com.puppycrawl.tools.checkstyle.StatelessCheck; 025import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 026import com.puppycrawl.tools.checkstyle.api.DetailAST; 027import com.puppycrawl.tools.checkstyle.api.TokenTypes; 028import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 029import com.puppycrawl.tools.checkstyle.utils.NullUtil; 030import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 031 032/** 033 * <div> 034 * Checks that a whitespace is present before the opening brace of an empty body. 035 * </div> 036 * 037 * <p> 038 * This check applies to the following tokens: 039 * Methods and constructors, Classes, interfaces, enums, and records, 040 * Annotation definitions, Loops: {@code while}, {@code for}, {@code do-while}, 041 * Lambdas and anonymous classes, Static initializer blocks, 042 * Try-catch-finally statements, Synchronized blocks, Switch statements. 043 * </p> 044 * 045 * <p> 046 * A body containing only comments is considered empty by this check. 047 * </p> 048 * 049 * @since 13.9.0 050 */ 051@StatelessCheck 052public class WhitespaceBeforeEmptyBodyCheck extends AbstractCheck { 053 054 /** 055 * A key is pointing to the warning message text in "messages.properties" 056 * file. 057 */ 058 public static final String MSG_KEY = "ws.notPreceded"; 059 060 /** 061 * Creates a new {@code WhitespaceBeforeEmptyBodyCheck} instance. 062 */ 063 public WhitespaceBeforeEmptyBodyCheck() { 064 // no code by default 065 } 066 067 @Override 068 public int[] getDefaultTokens() { 069 return getAcceptableTokens(); 070 } 071 072 @Override 073 public int[] getAcceptableTokens() { 074 return new int[] { 075 TokenTypes.METHOD_DEF, 076 TokenTypes.CTOR_DEF, 077 TokenTypes.COMPACT_CTOR_DEF, 078 TokenTypes.CLASS_DEF, 079 TokenTypes.INTERFACE_DEF, 080 TokenTypes.RECORD_DEF, 081 TokenTypes.ENUM_DEF, 082 TokenTypes.ANNOTATION_DEF, 083 TokenTypes.LITERAL_NEW, 084 TokenTypes.LITERAL_DO, 085 TokenTypes.LITERAL_WHILE, 086 TokenTypes.LITERAL_FOR, 087 TokenTypes.LITERAL_IF, 088 TokenTypes.LITERAL_ELSE, 089 TokenTypes.STATIC_INIT, 090 TokenTypes.LITERAL_TRY, 091 TokenTypes.LITERAL_CATCH, 092 TokenTypes.LITERAL_FINALLY, 093 TokenTypes.LITERAL_SYNCHRONIZED, 094 TokenTypes.LITERAL_SWITCH, 095 TokenTypes.LAMBDA, 096 }; 097 } 098 099 @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}