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