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