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;
024import java.util.Set;
025
026import javax.annotation.Nullable;
027
028import com.puppycrawl.tools.checkstyle.StatelessCheck;
029import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
030import com.puppycrawl.tools.checkstyle.api.DetailAST;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
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            default -> {
183                // ATTENTION! We have default here, but we expect case TokenTypes.METHOD_DEF,
184                // TokenTypes.LITERAL_FOR, TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_DO only.
185                // It has been done to improve coverage to 100%. I couldn't replace it with
186                // if-else-if block because code was ugly and didn't pass pmd check.
187
188                startToken = ast;
189                yield ast.findFirstToken(TokenTypes.LCURLY);
190            }
191        };
192
193        if (brace != null && !isBraceVerifiedByParent(ast)) {
194            verifyBrace(brace, startToken);
195        }
196    }
197
198    /**
199     * Checks whether the brace of the given token is verified through its parent
200     * as well. An {@code OBJBLOCK} shares its brace with the type definition it
201     * belongs to, and the case label of an arrow switch shares its brace with the
202     * {@code SWITCH_RULE}. Without this the same brace is reported twice.
203     *
204     * @param ast the token being visited
205     * @return {@code true} if the parent already verifies the same brace
206     */
207    private boolean isBraceVerifiedByParent(DetailAST ast) {
208        return TokenUtil.isOfType(ast,
209                    TokenTypes.OBJBLOCK, TokenTypes.LITERAL_CASE, TokenTypes.LITERAL_DEFAULT)
210                && isConfigured(ast.getParent());
211    }
212
213    /**
214     * Checks whether the token type of the given node is configured for this check.
215     *
216     * @param ast the token to check
217     * @return {@code true} if this check visits that token type
218     */
219    private boolean isConfigured(DetailAST ast) {
220        final Set<String> configuredTokens = getTokenNames();
221        final boolean result;
222        if (configuredTokens.isEmpty()) {
223            result = TokenUtil.isOfType(ast, getDefaultTokens());
224        }
225        else {
226            result = configuredTokens.contains(TokenUtil.getTokenName(ast.getType()));
227        }
228        return result;
229    }
230
231    /**
232     * Gets the brace of a switch statement/ expression member.
233     *
234     * @param ast {@code DetailAST}.
235     * @return {@code DetailAST} if the first child is {@code TokenTypes.SLIST},
236     *     {@code null} otherwise.
237     */
238    @Nullable
239    private static DetailAST getBraceFromSwitchMember(DetailAST ast) {
240        final DetailAST brace;
241        final DetailAST parent = ast.getParent();
242        if (parent.getType() == TokenTypes.SWITCH_RULE) {
243            brace = parent.findFirstToken(TokenTypes.SLIST);
244        }
245        else {
246            brace = getBraceAsFirstChild(ast.getNextSibling());
247        }
248        return brace;
249    }
250
251    /**
252     * Gets a SLIST if it is the first child of the AST.
253     *
254     * @param ast {@code DetailAST}.
255     * @return {@code DetailAST} if the first child is {@code TokenTypes.SLIST},
256     *     {@code null} otherwise.
257     */
258    @Nullable
259    private static DetailAST getBraceAsFirstChild(DetailAST ast) {
260        DetailAST brace = null;
261        if (ast != null) {
262            final DetailAST candidate = ast.getFirstChild();
263            if (candidate != null && candidate.getType() == TokenTypes.SLIST) {
264                brace = candidate;
265            }
266        }
267        return brace;
268    }
269
270    /**
271     * Skip all {@code TokenTypes.ANNOTATION}s to the first non-annotation.
272     *
273     * @param ast {@code DetailAST}.
274     * @return {@code DetailAST} or null if there are no annotations.
275     */
276    private static DetailAST skipModifierAnnotations(DetailAST ast) {
277        DetailAST resultNode = ast;
278        final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
279
280        if (modifiers != null) {
281            resultNode = findLastAnnotation(modifiers)
282                    .map(annotation -> {
283                        final DetailAST nextNode;
284                        if (annotation.getNextSibling() == null) {
285                            nextNode = modifiers.getNextSibling();
286                        }
287                        else {
288                            nextNode = annotation.getNextSibling();
289                        }
290                        return nextNode;
291                    })
292                    .orElse(resultNode);
293        }
294        return resultNode;
295    }
296
297    /**
298     * Find the last token of type {@code TokenTypes.ANNOTATION}
299     * under the given set of modifiers.
300     *
301     * @param modifiers {@code DetailAST}.
302     * @return Optional containing the last annotation, if found.
303     */
304    private static Optional<DetailAST> findLastAnnotation(DetailAST modifiers) {
305        DetailAST annotation = modifiers.findFirstToken(TokenTypes.ANNOTATION);
306        while (annotation != null && annotation.getNextSibling() != null
307               && annotation.getNextSibling().getType() == TokenTypes.ANNOTATION) {
308            annotation = annotation.getNextSibling();
309        }
310        return Optional.ofNullable(annotation);
311    }
312
313    /**
314     * Verifies that a specified left curly brace is placed correctly
315     * according to policy.
316     *
317     * @param brace token for left curly brace
318     * @param startToken token for start of expression
319     */
320    private void verifyBrace(final DetailAST brace,
321                             final DetailAST startToken) {
322        final String braceLine = getLine(brace.getLineNo() - 1);
323
324        // Check for being told to ignore, or have '{}' which is a special case
325        if (braceLine.length() <= brace.getColumnNo() + 1
326                || braceLine.charAt(brace.getColumnNo() + 1) != '}') {
327            if (option == LeftCurlyOption.NL) {
328                if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
329                    log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
330                }
331            }
332            else if (option == LeftCurlyOption.EOL) {
333                validateEol(brace, braceLine);
334            }
335            else if (!TokenUtil.areOnSameLine(startToken, brace)) {
336                validateNewLinePosition(brace, startToken, braceLine);
337            }
338        }
339    }
340
341    /**
342     * Validate EOL case.
343     *
344     * @param brace brace AST
345     * @param braceLine line content
346     */
347    private void validateEol(DetailAST brace, String braceLine) {
348        if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
349            log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
350        }
351        if (!hasLineBreakAfter(brace)) {
352            log(brace, MSG_KEY_LINE_BREAK_AFTER, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
353        }
354    }
355
356    /**
357     * Validate token on new Line position.
358     *
359     * @param brace brace AST
360     * @param startToken start Token
361     * @param braceLine content of line with Brace
362     */
363    private void validateNewLinePosition(DetailAST brace, DetailAST startToken, String braceLine) {
364        // not on the same line
365        if (startToken.getLineNo() + 1 == brace.getLineNo()) {
366            if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
367                log(brace, MSG_KEY_LINE_PREVIOUS, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
368            }
369            else {
370                log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
371            }
372        }
373        else if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) {
374            log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1);
375        }
376    }
377
378    /**
379     * Checks if left curly has line break after.
380     *
381     * @param leftCurly
382     *        Left curly token.
383     * @return
384     *        True, left curly has line break after.
385     */
386    private boolean hasLineBreakAfter(DetailAST leftCurly) {
387        DetailAST nextToken = null;
388        if (leftCurly.getType() == TokenTypes.SLIST) {
389            nextToken = leftCurly.getFirstChild();
390        }
391        else if (!ignoreEnums) {
392            if (leftCurly.getParent().getParent().getType() == TokenTypes.ENUM_DEF) {
393                nextToken = leftCurly.getNextSibling();
394            }
395            else if (leftCurly.getParent().getType() == TokenTypes.ENUM_DEF) {
396                // the brace of the enum body is the first child of its OBJBLOCK
397                nextToken = leftCurly.getFirstChild().getNextSibling();
398            }
399        }
400        return nextToken == null
401                || nextToken.getType() == TokenTypes.RCURLY
402                || !TokenUtil.areOnSameLine(leftCurly, nextToken);
403    }
404
405}