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