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" file.
056     */
057    public static final String MSG_KEY = "ws.notPreceded";
058
059    /**
060     * Creates a new {@code WhitespaceBeforeEmptyBodyCheck} instance.
061     */
062    public WhitespaceBeforeEmptyBodyCheck() {
063        // no code by default
064    }
065
066    @Override
067    public int[] getDefaultTokens() {
068        return getAcceptableTokens();
069    }
070
071    @Override
072    public int[] getAcceptableTokens() {
073        return new int[] {
074            TokenTypes.METHOD_DEF,
075            TokenTypes.CTOR_DEF,
076            TokenTypes.COMPACT_CTOR_DEF,
077            TokenTypes.CLASS_DEF,
078            TokenTypes.INTERFACE_DEF,
079            TokenTypes.RECORD_DEF,
080            TokenTypes.ENUM_DEF,
081            TokenTypes.ANNOTATION_DEF,
082            TokenTypes.LITERAL_NEW,
083            TokenTypes.LITERAL_DO,
084            TokenTypes.LITERAL_WHILE,
085            TokenTypes.LITERAL_FOR,
086            TokenTypes.LITERAL_IF,
087            TokenTypes.LITERAL_ELSE,
088            TokenTypes.STATIC_INIT,
089            TokenTypes.LITERAL_TRY,
090            TokenTypes.LITERAL_CATCH,
091            TokenTypes.LITERAL_FINALLY,
092            TokenTypes.LITERAL_SYNCHRONIZED,
093            TokenTypes.LITERAL_SWITCH,
094            TokenTypes.LAMBDA,
095        };
096    }
097
098    @Override
099    public int[] getRequiredTokens() {
100        return CommonUtil.EMPTY_INT_ARRAY;
101    }
102
103    @Override
104    public void visitToken(DetailAST ast) {
105        final DetailAST lcurly = findOpeningBrace(ast);
106        if (lcurly != null && isBodyEmpty(lcurly) && !precededByWhiteSpace(lcurly)) {
107            log(lcurly, MSG_KEY, lcurly.getText());
108        }
109    }
110
111    /**
112     * Returns left curly token for an ast.
113     *
114     * @param ast the AST to get brace token for
115     * @return the left curly token, or null if the construct has no-body
116     */
117    @Nullable
118    private static DetailAST findOpeningBrace(DetailAST ast) {
119        final int astType = ast.getType();
120        return switch (astType) {
121            case TokenTypes.CLASS_DEF,
122                 TokenTypes.INTERFACE_DEF,
123                 TokenTypes.ENUM_DEF,
124                 TokenTypes.RECORD_DEF,
125                 TokenTypes.ANNOTATION_DEF,
126                 TokenTypes.LITERAL_NEW -> {
127                final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
128                DetailAST lcurly = null;
129                if (objBlock != null) {
130                    lcurly = objBlock.findFirstToken(TokenTypes.LCURLY);
131                }
132                yield lcurly;
133            }
134            case TokenTypes.LITERAL_SWITCH -> ast.findFirstToken(TokenTypes.LCURLY);
135            default -> ast.findFirstToken(TokenTypes.SLIST);
136        };
137    }
138
139    /**
140     * Checks if the opening brace is preceded by whitespace.
141     *
142     * @param lcurly token representing the opening brace
143     * @return true if preceded by whitespace
144     */
145    private static boolean precededByWhiteSpace(DetailAST lcurly) {
146        DetailAST previousToken = lcurly.getPreviousSibling();
147        if (previousToken != null) {
148            previousToken = descendToLeafNode(previousToken);
149        }
150        if (previousToken == null
151                || previousToken.getParent().getType() == TokenTypes.LAMBDA) {
152            previousToken = NullUtil.notNull(lcurly.getParent());
153        }
154        if (previousToken.getType() == TokenTypes.OBJBLOCK) {
155            previousToken = NullUtil.notNull(previousToken.getPreviousSibling());
156            previousToken = descendToLeafNode(previousToken);
157        }
158
159        return !TokenUtil.areOnSameLine(lcurly, previousToken)
160                || isSpaceBetweenTokens(previousToken, lcurly);
161    }
162
163    /**
164     * Return the last nested child of the token.
165     *
166     * @param token the ast token
167     * @return nested child
168     */
169    private static DetailAST descendToLeafNode(DetailAST token) {
170        DetailAST current = token;
171        while (current.hasChildren()) {
172            current = current.getLastChild();
173        }
174        return current;
175    }
176
177    /**
178     * Checks if the body is empty.
179     *
180     * @param lcurly the token representing the opening brace
181     * @return true if the body is empty
182     */
183    private static boolean isBodyEmpty(DetailAST lcurly) {
184        final DetailAST nextSibling = lcurly.getNextSibling();
185        final DetailAST firstChild = lcurly.getFirstChild();
186        return nextSibling != null && nextSibling.getType() == TokenTypes.RCURLY
187                || firstChild != null && firstChild.getType() == TokenTypes.RCURLY;
188    }
189
190    /**
191     * Checks if space exists between first and second token.
192     *
193     * @param firstToken token in ast which is behind second token
194     * @param secondToken token next to firstToken
195     * @return true if space exists between first and second tokens
196     */
197    private static boolean isSpaceBetweenTokens(DetailAST firstToken, DetailAST secondToken) {
198        String tokenText = firstToken.getText();
199        if (firstToken.getType() == TokenTypes.STATIC_INIT) {
200            tokenText = "static";
201        }
202        final int firstLastColumn = firstToken.getColumnNo()
203                + tokenText.length() - 1;
204        return secondToken.getColumnNo() - firstLastColumn > 1;
205    }
206
207}