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}