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.blocks;
021
022import java.util.Arrays;
023import java.util.Locale;
024import java.util.Optional;
025
026import com.puppycrawl.tools.checkstyle.StatelessCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.TokenTypes;
030import com.puppycrawl.tools.checkstyle.utils.CodePointUtil;
031import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
032
033/**
034 * <div>
035 * Checks for empty blocks.
036 * </div>
037 *
038 * <p>
039 * This check does not validate sequential blocks. This check does not violate fallthrough.
040 * </p>
041 *
042 * <p>
043 * NOTE: This check processes LITERAL_CASE and LITERAL_DEFAULT separately.
044 * Verification empty block is done for single nearest {@code case} or {@code default}.
045 * </p>
046 *
047 * @since 3.0
048 */
049@StatelessCheck
050public class EmptyBlockCheck
051    extends AbstractCheck {
052
053    /**
054     * A key is pointing to the warning message text in "messages.properties"
055     * file.
056     */
057    public static final String MSG_KEY_BLOCK_NO_STATEMENT = "block.noStatement";
058
059    /**
060     * A key is pointing to the warning message text in "messages.properties"
061     * file.
062     */
063    public static final String MSG_KEY_BLOCK_EMPTY = "block.empty";
064
065    /** Specify the policy on block contents. */
066    private BlockOption option = BlockOption.STATEMENT;
067
068    /**
069     * Creates a new {@code EmptyBlockCheck} instance.
070     */
071    public EmptyBlockCheck() {
072        // no code by default
073    }
074
075    /**
076     * Setter to specify the policy on block contents.
077     *
078     * @param optionStr string to decode option from
079     * @throws IllegalArgumentException if unable to decode
080     * @since 3.0
081     */
082    public void setOption(String optionStr) {
083        option = BlockOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
084    }
085
086    @Override
087    public int[] getDefaultTokens() {
088        return new int[] {
089            TokenTypes.LITERAL_WHILE,
090            TokenTypes.LITERAL_TRY,
091            TokenTypes.LITERAL_FINALLY,
092            TokenTypes.LITERAL_DO,
093            TokenTypes.LITERAL_IF,
094            TokenTypes.LITERAL_ELSE,
095            TokenTypes.LITERAL_FOR,
096            TokenTypes.INSTANCE_INIT,
097            TokenTypes.STATIC_INIT,
098            TokenTypes.LITERAL_SWITCH,
099            TokenTypes.LITERAL_SYNCHRONIZED,
100        };
101    }
102
103    @Override
104    public int[] getAcceptableTokens() {
105        return new int[] {
106            TokenTypes.LITERAL_WHILE,
107            TokenTypes.LITERAL_TRY,
108            TokenTypes.LITERAL_CATCH,
109            TokenTypes.LITERAL_FINALLY,
110            TokenTypes.LITERAL_DO,
111            TokenTypes.LITERAL_IF,
112            TokenTypes.LITERAL_ELSE,
113            TokenTypes.LITERAL_FOR,
114            TokenTypes.INSTANCE_INIT,
115            TokenTypes.STATIC_INIT,
116            TokenTypes.LITERAL_SWITCH,
117            TokenTypes.LITERAL_SYNCHRONIZED,
118            TokenTypes.LITERAL_CASE,
119            TokenTypes.LITERAL_DEFAULT,
120            TokenTypes.ARRAY_INIT,
121        };
122    }
123
124    @Override
125    public int[] getRequiredTokens() {
126        return CommonUtil.EMPTY_INT_ARRAY;
127    }
128
129    @Override
130    public void visitToken(DetailAST ast) {
131        final Optional<DetailAST> leftCurly = getLeftCurly(ast);
132        if (leftCurly.isPresent()) {
133            final DetailAST leftCurlyAST = leftCurly.orElseThrow();
134            if (option == BlockOption.STATEMENT) {
135                final boolean emptyBlock;
136                if (leftCurlyAST.getType() == TokenTypes.LCURLY) {
137                    final DetailAST nextSibling = leftCurlyAST.getNextSibling();
138                    emptyBlock = nextSibling.getType() != TokenTypes.CASE_GROUP
139                            && nextSibling.getType() != TokenTypes.SWITCH_RULE;
140                }
141                else {
142                    emptyBlock = leftCurlyAST.getChildCount() <= 1;
143                }
144                if (emptyBlock) {
145                    log(leftCurlyAST,
146                        MSG_KEY_BLOCK_NO_STATEMENT);
147                }
148            }
149            else if (!hasText(leftCurlyAST)) {
150                log(leftCurlyAST,
151                    MSG_KEY_BLOCK_EMPTY,
152                    ast.getText());
153            }
154        }
155    }
156
157    /**
158     * Checks if SLIST token contains any text.
159     *
160     * @param slistAST a {@code DetailAST} value
161     * @return whether the SLIST token contains any text.
162     */
163    private boolean hasText(final DetailAST slistAST) {
164        final DetailAST rightCurly = slistAST.findFirstToken(TokenTypes.RCURLY);
165        final DetailAST rcurlyAST;
166
167        if (rightCurly == null) {
168            rcurlyAST = slistAST.getParent().findFirstToken(TokenTypes.RCURLY);
169        }
170        else {
171            rcurlyAST = rightCurly;
172        }
173        final int slistLineNo = slistAST.getLineNo();
174        final int slistColNo = slistAST.getColumnNo();
175        final int rcurlyLineNo = rcurlyAST.getLineNo();
176        final int rcurlyColNo = rcurlyAST.getColumnNo();
177        boolean returnValue = false;
178        if (slistLineNo == rcurlyLineNo) {
179            // Handle braces on the same line
180            final int[] txt = Arrays.copyOfRange(getLineCodePoints(slistLineNo - 1),
181                    slistColNo + 1, rcurlyColNo);
182
183            if (!CodePointUtil.isBlank(txt)) {
184                returnValue = true;
185            }
186        }
187        else {
188            final int[] codePointsFirstLine = getLineCodePoints(slistLineNo - 1);
189            final int[] firstLine = Arrays.copyOfRange(codePointsFirstLine,
190                    slistColNo + 1, codePointsFirstLine.length);
191            final int[] codePointsLastLine = getLineCodePoints(rcurlyLineNo - 1);
192            final int[] lastLine = Arrays.copyOfRange(codePointsLastLine, 0, rcurlyColNo);
193            // check if all lines are also only whitespace
194            returnValue = !(CodePointUtil.isBlank(firstLine) && CodePointUtil.isBlank(lastLine))
195                    || !checkIsAllLinesAreWhitespace(slistLineNo, rcurlyLineNo);
196        }
197        return returnValue;
198    }
199
200    /**
201     * Checks is all lines from 'lineFrom' to 'lineTo' (exclusive)
202     * contain whitespaces only.
203     *
204     * @param lineFrom
205     *            check from this line number
206     * @param lineTo
207     *            check to this line numbers
208     * @return true if lines contain only whitespaces
209     */
210    private boolean checkIsAllLinesAreWhitespace(int lineFrom, int lineTo) {
211        boolean result = true;
212        for (int i = lineFrom; i < lineTo - 1; i++) {
213            if (!CodePointUtil.isBlank(getLineCodePoints(i))) {
214                result = false;
215                break;
216            }
217        }
218        return result;
219    }
220
221    /**
222     * Calculates the left curly corresponding to the block to be checked.
223     *
224     * @param ast a {@code DetailAST} value
225     * @return the left curly corresponding to the block to be checked
226     */
227    private static Optional<DetailAST> getLeftCurly(DetailAST ast) {
228        final DetailAST parent = ast.getParent();
229        final int parentType = parent.getType();
230        final Optional<DetailAST> leftCurly;
231
232        if (parentType == TokenTypes.SWITCH_RULE) {
233            // get left curly of a case or default that is in switch rule
234            leftCurly = Optional.ofNullable(parent.findFirstToken(TokenTypes.SLIST));
235        }
236        else if (parentType == TokenTypes.CASE_GROUP) {
237            // get left curly of a case or default that is in switch statement
238            leftCurly = Optional.ofNullable(ast.getNextSibling())
239                         .map(DetailAST::getFirstChild)
240                         .filter(node -> node.getType() == TokenTypes.SLIST);
241        }
242        else if (ast.findFirstToken(TokenTypes.SLIST) != null) {
243            // we have a left curly that is part of a statement list, but not in a case or default
244            leftCurly = Optional.of(ast.findFirstToken(TokenTypes.SLIST));
245        }
246        else {
247            // get the first left curly that we can find, if it is present
248            leftCurly = Optional.ofNullable(ast.findFirstToken(TokenTypes.LCURLY));
249        }
250        return leftCurly;
251    }
252
253}