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