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