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