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