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}