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