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