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 java.util.Locale;
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;
029
030/**
031 * <div>
032 * Checks the empty line right after the opening brace and right before the closing
033 * brace of a block.
034 * </div>
035 *
036 * <p>
037 * {@code topSeparator} controls the line after the opening brace and
038 * {@code bottomSeparator} controls the line before the closing brace.
039 * Both accept the following values:
040 * </p>
041 * <ul>
042 * <li>{@code empty_line} - exactly one empty line is required;</li>
043 * <li>{@code empty_line_allowed} - not validated, an empty line may be present or absent;</li>
044 * <li>{@code no_empty_line} - an empty line is forbidden.</li>
045 * </ul>
046 *
047 * @since 14.4.0
048 */
049@StatelessCheck
050public class EmptyLineWrappingInBlockCheck extends AbstractCheck {
051
052    /**
053     * A key is pointing to the warning message empty.line.wrapping.top.no in "messages.properties"
054     * file.
055     */
056    public static final String MSG_EMPTY_LINE_WRAPPING_TOP_NO = "empty.line.wrapping.top.no";
057
058    /**
059     * A key is pointing to the warning message empty.line.wrapping.top.one in "messages.properties"
060     * file.
061     */
062    public static final String MSG_EMPTY_LINE_WRAPPING_TOP_ONE = "empty.line.wrapping.top.one";
063
064    /**
065     * A key is pointing to the warning message empty.line.wrapping.bottom.no in
066     * "messages.properties" file.
067     */
068    public static final String MSG_EMPTY_LINE_WRAPPING_BOTTOM_NO = "empty.line.wrapping.bottom.no";
069
070    /**
071     * A key is pointing to the warning message empty.line.wrapping.bottom.one in
072     * "messages.properties" file.
073     */
074    public static final String MSG_EMPTY_LINE_WRAPPING_BOTTOM_ONE =
075            "empty.line.wrapping.bottom.one";
076
077    /** Policy for empty lines after the opening brace of the block. */
078    private SeparatorOption topSeparator = SeparatorOption.EMPTY_LINE;
079
080    /** Policy for empty lines before the closing brace of the block. */
081    private SeparatorOption bottomSeparator = SeparatorOption.EMPTY_LINE;
082
083    /**
084     * Creates a new {@code EmptyLineWrappingInBlockCheck} instance.
085     */
086    public EmptyLineWrappingInBlockCheck() {
087        // no code by default
088    }
089
090    /**
091     * Specify the policy for the empty line after the opening brace.
092     *
093     * @param value one of {@code empty_line}, {@code empty_line_allowed}, {@code no_empty_line}
094     * @throws IllegalArgumentException if the value is not recognized
095     * @since 14.4.0
096     */
097    public void setTopSeparator(String value) {
098        topSeparator = SeparatorOption.valueOf(value.trim().toUpperCase(Locale.ENGLISH));
099    }
100
101    /**
102     * Specify the policy for the empty line before the closing brace.
103     *
104     * @param value one of {@code empty_line}, {@code empty_line_allowed}, {@code no_empty_line}
105     * @throws IllegalArgumentException if the value is not recognized
106     * @since 14.4.0
107     */
108    public void setBottomSeparator(String value) {
109        bottomSeparator = SeparatorOption.valueOf(value.trim().toUpperCase(Locale.ENGLISH));
110    }
111
112    /**
113     * Returns the default token set that this check validates.
114     *
115     * @return the default tokens
116     */
117    @Override
118    public int[] getDefaultTokens() {
119        return new int[] {
120            TokenTypes.CLASS_DEF,
121            TokenTypes.INTERFACE_DEF,
122            TokenTypes.ANNOTATION_DEF,
123            TokenTypes.ENUM_DEF,
124            TokenTypes.ENUM_CONSTANT_DEF,
125        };
126    }
127
128    /**
129     * Returns the set of tokens that can be configured for this check.
130     *
131     * @return the acceptable tokens
132     */
133    @Override
134    public int[] getAcceptableTokens() {
135        return new int[] {
136            TokenTypes.ANNOTATION_ARRAY_INIT,
137            TokenTypes.ANNOTATION_DEF,
138            TokenTypes.ARRAY_INIT,
139            TokenTypes.CASE_GROUP,
140            TokenTypes.CLASS_DEF,
141            TokenTypes.CTOR_DEF,
142            TokenTypes.ENUM_CONSTANT_DEF,
143            TokenTypes.ENUM_DEF,
144            TokenTypes.INSTANCE_INIT,
145            TokenTypes.INTERFACE_DEF,
146            TokenTypes.LAMBDA,
147            TokenTypes.LITERAL_CASE,
148            TokenTypes.LITERAL_CATCH,
149            TokenTypes.LITERAL_DEFAULT,
150            TokenTypes.LITERAL_DO,
151            TokenTypes.LITERAL_ELSE,
152            TokenTypes.LITERAL_FINALLY,
153            TokenTypes.LITERAL_FOR,
154            TokenTypes.LITERAL_IF,
155            TokenTypes.LITERAL_NEW,
156            TokenTypes.LITERAL_SWITCH,
157            TokenTypes.LITERAL_SYNCHRONIZED,
158            TokenTypes.LITERAL_TRY,
159            TokenTypes.LITERAL_WHILE,
160            TokenTypes.METHOD_DEF,
161            TokenTypes.SLIST,
162            TokenTypes.STATIC_INIT,
163        };
164    }
165
166    /**
167     * Returns the tokens that this check must be registered for.
168     *
169     * @return the empty array as this check has no required tokens
170     */
171    @Override
172    public int[] getRequiredTokens() {
173        return CommonUtil.EMPTY_INT_ARRAY;
174    }
175
176    /**
177     * Called to process a token. Validates empty line wrapping after the opening brace
178     * and before the closing brace of the block according to the configured policies.
179     *
180     * @param ast the token to process
181     */
182    @Override
183    public void visitToken(DetailAST ast) {
184        final DetailAST leftCurly = findLeftBrace(ast);
185        if (leftCurly != null) {
186            final DetailAST rightCurly = findRightBrace(leftCurly);
187            if (topSeparator == SeparatorOption.NO_EMPTY_LINE
188                    && hasBlankLineAfter(leftCurly, rightCurly)) {
189                log(leftCurly, MSG_EMPTY_LINE_WRAPPING_TOP_NO, leftCurly.getText());
190            }
191            else if (topSeparator == SeparatorOption.EMPTY_LINE
192                    && !hasOneEmptyLineAfter(leftCurly, rightCurly)) {
193                log(leftCurly, MSG_EMPTY_LINE_WRAPPING_TOP_ONE, leftCurly.getText());
194            }
195            if (bottomSeparator == SeparatorOption.NO_EMPTY_LINE
196                    && hasBlankLineBefore(leftCurly, rightCurly)) {
197                log(rightCurly, MSG_EMPTY_LINE_WRAPPING_BOTTOM_NO, rightCurly.getText());
198            }
199            else if (bottomSeparator == SeparatorOption.EMPTY_LINE
200                    && !hasOneEmptyLineBefore(leftCurly, rightCurly)) {
201                log(rightCurly, MSG_EMPTY_LINE_WRAPPING_BOTTOM_ONE, rightCurly.getText());
202            }
203        }
204    }
205
206    /**
207     * Finds the left (opening) brace of the block for the given AST node.
208     * The result depends on the token type (e.g. SLIST for statements,
209     * OBJBLOCK for type bodies, or the node itself for array inits).
210     *
211     * @param ast the AST node representing a block or block-containing construct
212     * @return the AST of the opening brace, or null if not found
213     */
214    private static DetailAST findLeftBrace(DetailAST ast) {
215        DetailAST leftBrace = null;
216        switch (ast.getType()) {
217            case TokenTypes.ARRAY_INIT, TokenTypes.ANNOTATION_ARRAY_INIT -> leftBrace = ast;
218            case TokenTypes.INTERFACE_DEF, TokenTypes.CLASS_DEF, TokenTypes.ANNOTATION_DEF,
219                TokenTypes.ENUM_DEF, TokenTypes.ENUM_CONSTANT_DEF, TokenTypes.LITERAL_NEW -> {
220                final DetailAST objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
221                if (objBlock != null) {
222                    leftBrace = objBlock.getFirstChild();
223                }
224            }
225            case TokenTypes.SLIST -> {
226                if (ast.getParent().getType() == TokenTypes.SLIST) {
227                    leftBrace = ast;
228                }
229            }
230            case TokenTypes.CASE_GROUP ->
231                leftBrace = ast.getLastChild().findFirstToken(TokenTypes.SLIST);
232            case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT ->
233                leftBrace = ast.getParent().getLastChild().findFirstToken(TokenTypes.SLIST);
234            default ->
235                leftBrace = ast.findFirstToken(TokenTypes.SLIST);
236        }
237        return leftBrace;
238    }
239
240    /**
241     * Finds the right (closing) brace of the block for the given opening brace.
242     * The result depends on the token type: for SLIST/array-init the closing brace
243     * is the last child; for OBJBLOCK etc. it is the last child of the parent.
244     *
245     * @param leftBrace the opening brace AST, non-null
246     * @return the closing brace AST
247     */
248    private static DetailAST findRightBrace(DetailAST leftBrace) {
249        return switch (leftBrace.getType()) {
250            case TokenTypes.ARRAY_INIT, TokenTypes.ANNOTATION_ARRAY_INIT, TokenTypes.SLIST ->
251                leftBrace.getLastChild();
252            default -> leftBrace.getParent().getLastChild();
253        };
254    }
255
256    /**
257     * Checks whether the line immediately after the opening brace is blank.
258     *
259     * @param leftCurly the opening brace token
260     * @param rightCurly the closing brace token
261     * @return true if the braces are on different lines and the next line is blank
262     */
263    private boolean hasBlankLineAfter(DetailAST leftCurly, DetailAST rightCurly) {
264        return leftCurly.getLineNo() != rightCurly.getLineNo()
265                && CommonUtil.isBlank(getLine(leftCurly.getLineNo()));
266    }
267
268    /**
269     * Checks whether there is exactly one empty line after the opening brace.
270     * Used when topSeparator is EMPTY_LINE. Two or more consecutive empty lines are rejected.
271     *
272     * @param leftCurly the opening brace token
273     * @param rightCurly the closing brace token
274     * @return true if the next line is blank and the line after that is not blank
275     */
276    private boolean hasOneEmptyLineAfter(DetailAST leftCurly, DetailAST rightCurly) {
277        boolean result = false;
278        final int nextLineIndex = leftCurly.getLineNo();
279        if (leftCurly.getLineNo() != rightCurly.getLineNo()
280                && CommonUtil.isBlank(getLine(nextLineIndex))) {
281            final int lineAfterNext = nextLineIndex + 1;
282            result = !CommonUtil.isBlank(getLine(lineAfterNext));
283        }
284        return result;
285    }
286
287    /**
288     * Checks whether the line immediately before the closing brace is blank.
289     * Used when bottomSeparator is NO_EMPTY_LINE. Always false for same-line braces.
290     *
291     * @param leftCurly the opening brace token
292     * @param rightCurly the closing brace token
293     * @return true if the braces are on different lines and the previous line is blank
294     */
295    private boolean hasBlankLineBefore(DetailAST leftCurly, DetailAST rightCurly) {
296        return leftCurly.getLineNo() != rightCurly.getLineNo()
297                && CommonUtil.isBlank(getLine(rightCurly.getLineNo() - 2));
298    }
299
300    /**
301     * Checks whether there is exactly one empty line before the closing brace.
302     * Used when bottomSeparator is EMPTY_LINE. Two or more consecutive empty lines are rejected.
303     *
304     * @param leftCurly the opening brace token
305     * @param rightCurly the closing brace token
306     * @return true if the previous line is blank and the line before that is not blank
307     */
308    private boolean hasOneEmptyLineBefore(DetailAST leftCurly, DetailAST rightCurly) {
309        boolean result = false;
310        final int prevLineIndex = rightCurly.getLineNo() - 2;
311        if (leftCurly.getLineNo() != rightCurly.getLineNo()
312                && CommonUtil.isBlank(getLine(prevLineIndex))) {
313            result = !CommonUtil.isBlank(getLine(prevLineIndex - 1));
314        }
315        return result;
316    }
317
318}