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.CommonUtil; 032import com.puppycrawl.tools.checkstyle.utils.NullUtil; 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 case TokenTypes.OBJBLOCK -> { 183 startToken = ast; 184 DetailAST braceToken = null; 185 if (ast.getParent().getType() == TokenTypes.LITERAL_NEW) { 186 braceToken = ast; 187 } 188 yield braceToken; 189 } 190 default -> { 191 // only expected DEFAULT Token is LITERAL_SWITCH 192 startToken = ast; 193 yield ast.findFirstToken(TokenTypes.LCURLY); 194 } 195 }; 196 197 if (brace != null) { 198 verifyBrace(brace, startToken); 199 } 200 } 201 202 /** 203 * Gets the brace of a switch statement/ expression member. 204 * 205 * @param ast {@code DetailAST}. 206 * @return {@code DetailAST} if the first child is {@code TokenTypes.SLIST}, 207 * {@code null} otherwise. 208 */ 209 @Nullable 210 private static DetailAST getBraceFromSwitchMember(DetailAST ast) { 211 final DetailAST brace; 212 final DetailAST parent = ast.getParent(); 213 if (parent.getType() == TokenTypes.SWITCH_RULE) { 214 brace = parent.findFirstToken(TokenTypes.SLIST); 215 } 216 else { 217 brace = getBraceAsFirstChild(ast.getNextSibling()); 218 } 219 return brace; 220 } 221 222 /** 223 * Gets a SLIST if it is the first child of the AST. 224 * 225 * @param ast {@code DetailAST}. 226 * @return {@code DetailAST} if the first child is {@code TokenTypes.SLIST}, 227 * {@code null} otherwise. 228 */ 229 @Nullable 230 private static DetailAST getBraceAsFirstChild(DetailAST ast) { 231 DetailAST brace = null; 232 if (ast != null) { 233 final DetailAST candidate = ast.getFirstChild(); 234 if (candidate != null && candidate.getType() == TokenTypes.SLIST) { 235 brace = candidate; 236 } 237 } 238 return brace; 239 } 240 241 /** 242 * Skip all {@code TokenTypes.ANNOTATION}s to the first non-annotation. 243 * 244 * @param ast {@code DetailAST}. 245 * @return {@code DetailAST} or null if there are no annotations. 246 */ 247 private static DetailAST skipModifierAnnotations(DetailAST ast) { 248 DetailAST resultNode = ast; 249 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS); 250 251 if (modifiers != null) { 252 resultNode = findLastAnnotation(modifiers) 253 .map(annotation -> { 254 final DetailAST nextNode; 255 if (annotation.getNextSibling() == null) { 256 nextNode = modifiers.getNextSibling(); 257 } 258 else { 259 nextNode = annotation.getNextSibling(); 260 } 261 return nextNode; 262 }) 263 .orElse(resultNode); 264 } 265 return resultNode; 266 } 267 268 /** 269 * Find the last token of type {@code TokenTypes.ANNOTATION} 270 * under the given set of modifiers. 271 * 272 * @param modifiers {@code DetailAST}. 273 * @return Optional containing the last annotation, if found. 274 */ 275 private static Optional<DetailAST> findLastAnnotation(DetailAST modifiers) { 276 DetailAST annotation = modifiers.findFirstToken(TokenTypes.ANNOTATION); 277 while (annotation != null && annotation.getNextSibling() != null 278 && annotation.getNextSibling().getType() == TokenTypes.ANNOTATION) { 279 annotation = annotation.getNextSibling(); 280 } 281 return Optional.ofNullable(annotation); 282 } 283 284 /** 285 * Verifies that a specified left curly brace is placed correctly 286 * according to policy. 287 * 288 * @param brace token for left curly brace 289 * @param startToken token for start of expression 290 */ 291 private void verifyBrace(final DetailAST brace, 292 final DetailAST startToken) { 293 final String braceLine = getLine(brace.getLineNo() - 1); 294 295 // Check for being told to ignore, or have '{}' which is a special case 296 if (braceLine.length() <= brace.getColumnNo() + 1 297 || braceLine.charAt(brace.getColumnNo() + 1) != '}') { 298 if (option == LeftCurlyOption.NL) { 299 if (!CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) { 300 log(brace, MSG_KEY_LINE_NEW, OPEN_CURLY_BRACE, brace.getColumnNo() + 1); 301 } 302 } 303 else if (option == LeftCurlyOption.EOL) { 304 validateEol(startToken, brace, braceLine); 305 } 306 else if (!TokenUtil.areOnSameLine(startToken, brace)) { 307 validateNewLinePosition(brace, startToken, braceLine); 308 } 309 } 310 } 311 312 /** 313 * Validate EOL case. 314 * 315 * @param startToken token for start of expression. 316 * @param brace brace AST 317 * @param braceLine line content 318 */ 319 private void validateEol(DetailAST startToken, DetailAST brace, String braceLine) { 320 if (CommonUtil.hasWhitespaceBefore(brace.getColumnNo(), braceLine)) { 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 content of line with Brace 334 */ 335 private void validateNewLinePosition(DetailAST brace, DetailAST startToken, String braceLine) { 336 // not on the same line 337 if (startToken.getLineNo() + 1 == brace.getLineNo()) { 338 if (CommonUtil.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 (!CommonUtil.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}