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