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