001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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 * <ul>
047 * <li>
048 * Property {@code option} - Specify the policy on block contents.
049 * Type is {@code com.puppycrawl.tools.checkstyle.checks.blocks.BlockOption}.
050 * Default value is {@code statement}.
051 * </li>
052 * <li>
053 * Property {@code tokens} - tokens to check
054 * Type is {@code java.lang.String[]}.
055 * Validation type is {@code tokenSet}.
056 * Default value is:
057 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_WHILE">
058 * LITERAL_WHILE</a>,
059 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_TRY">
060 * LITERAL_TRY</a>,
061 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_FINALLY">
062 * LITERAL_FINALLY</a>,
063 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_DO">
064 * LITERAL_DO</a>,
065 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_IF">
066 * LITERAL_IF</a>,
067 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_ELSE">
068 * LITERAL_ELSE</a>,
069 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_FOR">
070 * LITERAL_FOR</a>,
071 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#INSTANCE_INIT">
072 * INSTANCE_INIT</a>,
073 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#STATIC_INIT">
074 * STATIC_INIT</a>,
075 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_SWITCH">
076 * LITERAL_SWITCH</a>,
077 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#LITERAL_SYNCHRONIZED">
078 * LITERAL_SYNCHRONIZED</a>.
079 * </li>
080 * </ul>
081 *
082 * <p>
083 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
084 * </p>
085 *
086 * <p>
087 * Violation Message Keys:
088 * </p>
089 * <ul>
090 * <li>
091 * {@code block.empty}
092 * </li>
093 * <li>
094 * {@code block.noStatement}
095 * </li>
096 * </ul>
097 *
098 * @since 3.0
099 */
100@StatelessCheck
101public class EmptyBlockCheck
102    extends AbstractCheck {
103
104    /**
105     * A key is pointing to the warning message text in "messages.properties"
106     * file.
107     */
108    public static final String MSG_KEY_BLOCK_NO_STATEMENT = "block.noStatement";
109
110    /**
111     * A key is pointing to the warning message text in "messages.properties"
112     * file.
113     */
114    public static final String MSG_KEY_BLOCK_EMPTY = "block.empty";
115
116    /** Specify the policy on block contents. */
117    private BlockOption option = BlockOption.STATEMENT;
118
119    /**
120     * Setter to specify the policy on block contents.
121     *
122     * @param optionStr string to decode option from
123     * @throws IllegalArgumentException if unable to decode
124     * @since 3.0
125     */
126    public void setOption(String optionStr) {
127        option = BlockOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
128    }
129
130    @Override
131    public int[] getDefaultTokens() {
132        return new int[] {
133            TokenTypes.LITERAL_WHILE,
134            TokenTypes.LITERAL_TRY,
135            TokenTypes.LITERAL_FINALLY,
136            TokenTypes.LITERAL_DO,
137            TokenTypes.LITERAL_IF,
138            TokenTypes.LITERAL_ELSE,
139            TokenTypes.LITERAL_FOR,
140            TokenTypes.INSTANCE_INIT,
141            TokenTypes.STATIC_INIT,
142            TokenTypes.LITERAL_SWITCH,
143            TokenTypes.LITERAL_SYNCHRONIZED,
144        };
145    }
146
147    @Override
148    public int[] getAcceptableTokens() {
149        return new int[] {
150            TokenTypes.LITERAL_WHILE,
151            TokenTypes.LITERAL_TRY,
152            TokenTypes.LITERAL_CATCH,
153            TokenTypes.LITERAL_FINALLY,
154            TokenTypes.LITERAL_DO,
155            TokenTypes.LITERAL_IF,
156            TokenTypes.LITERAL_ELSE,
157            TokenTypes.LITERAL_FOR,
158            TokenTypes.INSTANCE_INIT,
159            TokenTypes.STATIC_INIT,
160            TokenTypes.LITERAL_SWITCH,
161            TokenTypes.LITERAL_SYNCHRONIZED,
162            TokenTypes.LITERAL_CASE,
163            TokenTypes.LITERAL_DEFAULT,
164            TokenTypes.ARRAY_INIT,
165        };
166    }
167
168    @Override
169    public int[] getRequiredTokens() {
170        return CommonUtil.EMPTY_INT_ARRAY;
171    }
172
173    @Override
174    public void visitToken(DetailAST ast) {
175        final Optional<DetailAST> leftCurly = getLeftCurly(ast);
176        if (leftCurly.isPresent()) {
177            final DetailAST leftCurlyAST = leftCurly.orElseThrow();
178            if (option == BlockOption.STATEMENT) {
179                final boolean emptyBlock;
180                if (leftCurlyAST.getType() == TokenTypes.LCURLY) {
181                    final DetailAST nextSibling = leftCurlyAST.getNextSibling();
182                    emptyBlock = nextSibling.getType() != TokenTypes.CASE_GROUP
183                            && nextSibling.getType() != TokenTypes.SWITCH_RULE;
184                }
185                else {
186                    emptyBlock = leftCurlyAST.getChildCount() <= 1;
187                }
188                if (emptyBlock) {
189                    log(leftCurlyAST,
190                        MSG_KEY_BLOCK_NO_STATEMENT);
191                }
192            }
193            else if (!hasText(leftCurlyAST)) {
194                log(leftCurlyAST,
195                    MSG_KEY_BLOCK_EMPTY,
196                    ast.getText());
197            }
198        }
199    }
200
201    /**
202     * Checks if SLIST token contains any text.
203     *
204     * @param slistAST a {@code DetailAST} value
205     * @return whether the SLIST token contains any text.
206     */
207    private boolean hasText(final DetailAST slistAST) {
208        final DetailAST rightCurly = slistAST.findFirstToken(TokenTypes.RCURLY);
209        final DetailAST rcurlyAST;
210
211        if (rightCurly == null) {
212            rcurlyAST = slistAST.getParent().findFirstToken(TokenTypes.RCURLY);
213        }
214        else {
215            rcurlyAST = rightCurly;
216        }
217        final int slistLineNo = slistAST.getLineNo();
218        final int slistColNo = slistAST.getColumnNo();
219        final int rcurlyLineNo = rcurlyAST.getLineNo();
220        final int rcurlyColNo = rcurlyAST.getColumnNo();
221        boolean returnValue = false;
222        if (slistLineNo == rcurlyLineNo) {
223            // Handle braces on the same line
224            final int[] txt = Arrays.copyOfRange(getLineCodePoints(slistLineNo - 1),
225                    slistColNo + 1, rcurlyColNo);
226
227            if (!CodePointUtil.isBlank(txt)) {
228                returnValue = true;
229            }
230        }
231        else {
232            final int[] codePointsFirstLine = getLineCodePoints(slistLineNo - 1);
233            final int[] firstLine = Arrays.copyOfRange(codePointsFirstLine,
234                    slistColNo + 1, codePointsFirstLine.length);
235            final int[] codePointsLastLine = getLineCodePoints(rcurlyLineNo - 1);
236            final int[] lastLine = Arrays.copyOfRange(codePointsLastLine, 0, rcurlyColNo);
237            // check if all lines are also only whitespace
238            returnValue = !(CodePointUtil.isBlank(firstLine) && CodePointUtil.isBlank(lastLine))
239                    || !checkIsAllLinesAreWhitespace(slistLineNo, rcurlyLineNo);
240        }
241        return returnValue;
242    }
243
244    /**
245     * Checks is all lines from 'lineFrom' to 'lineTo' (exclusive)
246     * contain whitespaces only.
247     *
248     * @param lineFrom
249     *            check from this line number
250     * @param lineTo
251     *            check to this line numbers
252     * @return true if lines contain only whitespaces
253     */
254    private boolean checkIsAllLinesAreWhitespace(int lineFrom, int lineTo) {
255        boolean result = true;
256        for (int i = lineFrom; i < lineTo - 1; i++) {
257            if (!CodePointUtil.isBlank(getLineCodePoints(i))) {
258                result = false;
259                break;
260            }
261        }
262        return result;
263    }
264
265    /**
266     * Calculates the left curly corresponding to the block to be checked.
267     *
268     * @param ast a {@code DetailAST} value
269     * @return the left curly corresponding to the block to be checked
270     */
271    private static Optional<DetailAST> getLeftCurly(DetailAST ast) {
272        final DetailAST parent = ast.getParent();
273        final int parentType = parent.getType();
274        final Optional<DetailAST> leftCurly;
275
276        if (parentType == TokenTypes.SWITCH_RULE) {
277            // get left curly of a case or default that is in switch rule
278            leftCurly = Optional.ofNullable(parent.findFirstToken(TokenTypes.SLIST));
279        }
280        else if (parentType == TokenTypes.CASE_GROUP) {
281            // get left curly of a case or default that is in switch statement
282            leftCurly = Optional.ofNullable(ast.getNextSibling())
283                         .map(DetailAST::getFirstChild)
284                         .filter(node -> node.getType() == TokenTypes.SLIST);
285        }
286        else if (ast.findFirstToken(TokenTypes.SLIST) != null) {
287            // we have a left curly that is part of a statement list, but not in a case or default
288            leftCurly = Optional.of(ast.findFirstToken(TokenTypes.SLIST));
289        }
290        else {
291            // get the first left curly that we can find, if it is present
292            leftCurly = Optional.ofNullable(ast.findFirstToken(TokenTypes.LCURLY));
293        }
294        return leftCurly;
295    }
296
297}