001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.Locale;
023
024import javax.annotation.Nullable;
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.CommonUtil;
031import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
032
033/**
034 * <div>
035 * Checks for the placement of left curly braces (<code>'{'</code>) for code blocks.
036 * </div>
037 *
038 *
039 * @since 3.0
040 */
041@StatelessCheck
042public class LeftCurlyCheck
043    extends AbstractCheck {
044
045    /**
046     * A key is pointing to the warning message text in "messages.properties"
047     * file.
048     */
049    public static final String MSG_KEY_LINE_NEW = "line.new";
050
051    /**
052     * A key is pointing to the warning message text in "messages.properties"
053     * file.
054     */
055    public static final String MSG_KEY_LINE_PREVIOUS = "line.previous";
056
057    /**
058     * A key is pointing to the warning message text in "messages.properties"
059     * file.
060     */
061    public static final String MSG_KEY_LINE_BREAK_AFTER = "line.break.after";
062
063    /** Open curly brace literal. */
064    private static final String OPEN_CURLY_BRACE = "{";
065
066    /** Allow to ignore enums when left curly brace policy is EOL. */
067    private boolean ignoreEnums = true;
068
069    /**
070     * Specify the policy on placement of a left curly brace (<code>'{'</code>).
071     */
072    private LeftCurlyOption option = LeftCurlyOption.EOL;
073
074    /**
075     * Setter to specify the policy on placement of a left curly brace (<code>'{'</code>).
076     *
077     * @param optionStr string to decode option from
078     * @throws IllegalArgumentException if unable to decode
079     * @since 3.0
080     */
081    public void setOption(String optionStr) {
082        option = LeftCurlyOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
083    }
084
085    /**
086     * Setter to allow to ignore enums when left curly brace policy is EOL.
087     *
088     * @param ignoreEnums check's option for ignoring enums.
089     * @since 6.9
090     */
091    public void setIgnoreEnums(boolean ignoreEnums) {
092        this.ignoreEnums = ignoreEnums;
093    }
094
095    @Override
096    public int[] getDefaultTokens() {
097        return getAcceptableTokens();
098    }
099
100    @Override
101    public int[] getAcceptableTokens() {
102        return new int[] {
103            TokenTypes.ANNOTATION_DEF,
104            TokenTypes.CLASS_DEF,
105            TokenTypes.CTOR_DEF,
106            TokenTypes.ENUM_CONSTANT_DEF,
107            TokenTypes.ENUM_DEF,
108            TokenTypes.INTERFACE_DEF,
109            TokenTypes.LAMBDA,
110            TokenTypes.LITERAL_CASE,
111            TokenTypes.LITERAL_CATCH,
112            TokenTypes.LITERAL_DEFAULT,
113            TokenTypes.LITERAL_DO,
114            TokenTypes.LITERAL_ELSE,
115            TokenTypes.LITERAL_FINALLY,
116            TokenTypes.LITERAL_FOR,
117            TokenTypes.LITERAL_IF,
118            TokenTypes.LITERAL_SWITCH,
119            TokenTypes.LITERAL_SYNCHRONIZED,
120            TokenTypes.LITERAL_TRY,
121            TokenTypes.LITERAL_WHILE,
122            TokenTypes.METHOD_DEF,
123            TokenTypes.OBJBLOCK,
124            TokenTypes.STATIC_INIT,
125            TokenTypes.RECORD_DEF,
126            TokenTypes.COMPACT_CTOR_DEF,
127        };
128    }
129
130    @Override
131    public int[] getRequiredTokens() {
132        return CommonUtil.EMPTY_INT_ARRAY;
133    }
134
135    /**
136     * Visits token.
137     *
138     * @param ast the token to process
139     * @noinspection SwitchStatementWithTooManyBranches
140     * @noinspectionreason SwitchStatementWithTooManyBranches - we cannot reduce
141     *      the number of branches in this switch statement, since many tokens
142     *      require specific methods to find the first left curly
143     */
144    @Override
145    public void visitToken(DetailAST ast) {
146        final DetailAST startToken;
147        final DetailAST brace = switch (ast.getType()) {
148            case TokenTypes.CTOR_DEF, TokenTypes.METHOD_DEF, TokenTypes.COMPACT_CTOR_DEF -> {
149                startToken = skipModifierAnnotations(ast);
150                yield ast.findFirstToken(TokenTypes.SLIST);
151            }
152            case TokenTypes.INTERFACE_DEF, TokenTypes.CLASS_DEF, TokenTypes.ANNOTATION_DEF,
153                 TokenTypes.ENUM_DEF, TokenTypes.ENUM_CONSTANT_DEF, TokenTypes.RECORD_DEF -> {
154                startToken = skipModifierAnnotations(ast);
155                yield ast.findFirstToken(TokenTypes.OBJBLOCK);
156            }
157            case TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_CATCH,
158                 TokenTypes.LITERAL_SYNCHRONIZED, TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_TRY,
159                 TokenTypes.LITERAL_FINALLY, TokenTypes.LITERAL_DO,
160                 TokenTypes.LITERAL_IF, TokenTypes.STATIC_INIT, TokenTypes.LAMBDA -> {
161                startToken = ast;
162                yield ast.findFirstToken(TokenTypes.SLIST);
163            }
164            case TokenTypes.LITERAL_ELSE -> {
165                startToken = ast;
166                yield getBraceAsFirstChild(ast);
167            }
168            case TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT -> {
169                startToken = ast;
170                yield getBraceFromSwitchMember(ast);
171            }
172            default -> {
173                // ATTENTION! We have default here, but we expect case TokenTypes.METHOD_DEF,
174                // TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_DO only.
175                // It has been done to improve coverage to 100%. I couldn't replace it with
176                // if-else-if block because code was ugly and didn't pass pmd check.
177
178                startToken = ast;
179                yield ast.findFirstToken(TokenTypes.LCURLY);
180            }
181        };
182
183        if (brace != null) {
184            verifyBrace(brace, startToken);
185        }
186    }
187
188    /**
189     * Gets the brace of a switch statement/ expression member.
190     *
191     * @param ast {@code DetailAST}.
192     * @return {@code DetailAST} if the first child is {@code TokenTypes.SLIST},
193     *     {@code null} otherwise.
194     */
195    @Nullable
196    private static DetailAST getBraceFromSwitchMember(DetailAST ast) {
197        final DetailAST brace;
198        final DetailAST parent = ast.getParent();
199        if (parent.getType() == TokenTypes.SWITCH_RULE) {
200            brace = parent.findFirstToken(TokenTypes.SLIST);
201        }
202        else {
203            brace = getBraceAsFirstChild(ast.getNextSibling());
204        }
205        return brace;
206    }
207
208    /**
209     * Gets a SLIST if it is the first child of the AST.
210     *
211     * @param ast {@code DetailAST}.
212     * @return {@code DetailAST} if the first child is {@code TokenTypes.SLIST},
213     *     {@code null} otherwise.
214     */
215    @Nullable
216    private static DetailAST getBraceAsFirstChild(DetailAST ast) {
217        DetailAST brace = null;
218        if (ast != null) {
219            final DetailAST candidate = ast.getFirstChild();
220            if (candidate != null && candidate.getType() == TokenTypes.SLIST) {
221                brace = candidate;
222            }
223        }
224        return brace;
225    }
226
227    /**
228     * Skip all {@code TokenTypes.ANNOTATION}s to the first non-annotation.
229     *
230     * @param ast {@code DetailAST}.
231     * @return {@code DetailAST}.
232     */
233    private static DetailAST skipModifierAnnotations(DetailAST ast) {
234        DetailAST resultNode = ast;
235        final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
236
237        if (modifiers != null) {
238            final DetailAST lastAnnotation = findLastAnnotation(modifiers);
239
240            if (lastAnnotation != null) {
241                if (lastAnnotation.getNextSibling() == null) {
242                    resultNode = modifiers.getNextSibling();
243                }
244                else {
245                    resultNode = lastAnnotation.getNextSibling();
246                }
247            }
248        }
249        return resultNode;
250    }
251
252    /**
253     * Find the last token of type {@code TokenTypes.ANNOTATION}
254     * under the given set of modifiers.
255     *
256     * @param modifiers {@code DetailAST}.
257     * @return {@code DetailAST} or null if there are no annotations.
258     */
259    private static DetailAST findLastAnnotation(DetailAST modifiers) {
260        DetailAST annotation = modifiers.findFirstToken(TokenTypes.ANNOTATION);
261        while (annotation != null && annotation.getNextSibling() != null
262               && annotation.getNextSibling().getType() == TokenTypes.ANNOTATION) {
263            annotation = annotation.getNextSibling();
264        }
265        return annotation;
266    }
267
268    /**
269     * Verifies that a specified left curly brace is placed correctly
270     * according to policy.
271     *
272     * @param brace token for left curly brace
273     * @param startToken token for start of expression
274     */
275    private void verifyBrace(final DetailAST brace,
276                             final DetailAST startToken) {
277        final String braceLine = getLine(brace.getLineNo() - 1);
278
279        // Check for being told to ignore, or have '{}' which is a special case
280        if (braceLine.length() <= brace.getColumnNo() + 1
281                || braceLine.charAt(brace.getColumnNo() + 1) != '}') {
282            if (option == LeftCurlyOption.NL) {
283                if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
284                    log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
285                }
286            }
287            else if (option == LeftCurlyOption.EOL) {
288                validateEol(brace, braceLine);
289            }
290            else if (!TokenUtil.areOnSameLine(startToken, brace)) {
291                validateNewLinePosition(brace, startToken, braceLine);
292            }
293        }
294    }
295
296    /**
297     * Validate EOL case.
298     *
299     * @param brace brace AST
300     * @param braceLine line content
301     */
302    private void validateEol(DetailAST brace, String braceLine) {
303        if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
304            log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
305        }
306        if (!hasLineBreakAfter(brace)) {
307            log(brace, MSG_KEY_LINE_BREAK_AFTER, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
308        }
309    }
310
311    /**
312     * Validate token on new Line position.
313     *
314     * @param brace brace AST
315     * @param startToken start Token
316     * @param braceLine content of line with Brace
317     */
318    private void validateNewLinePosition(DetailAST brace, DetailAST startToken, String braceLine) {
319        // not on the same line
320        if (startToken.getLineNo() + 1 == brace.getLineNo()) {
321            if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
322                log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
323            }
324            else {
325                log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
326            }
327        }
328        else if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
329            log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
330        }
331    }
332
333    /**
334     * Checks if left curly has line break after.
335     *
336     * @param leftCurly
337     *        Left curly token.
338     * @return
339     *        True, left curly has line break after.
340     */
341    private boolean hasLineBreakAfter(DetailAST leftCurly) {
342        DetailAST nextToken = null;
343        if (leftCurly.getType() == TokenTypes.SLIST) {
344            nextToken = leftCurly.getFirstChild();
345        }
346        else {
347            if (!ignoreEnums
348                    && leftCurly.getParent().getParent().getType() == TokenTypes.ENUM_DEF) {
349                nextToken = leftCurly.getNextSibling();
350            }
351        }
352        return nextToken == null
353                || nextToken.getType() == TokenTypes.RCURLY
354                || !TokenUtil.areOnSameLine(leftCurly, nextToken);
355    }
356
357}