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.coding; 021 022import java.util.ArrayDeque; 023import java.util.Deque; 024import java.util.Set; 025 026import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 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.NullUtil; 031import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 032 033/** 034 * <div> 035 * Checks that there is only one statement per line. 036 * </div> 037 * 038 * <p> 039 * Rationale: It's very difficult to read multiple statements on one line. 040 * </p> 041 * 042 * <p> 043 * In the Java programming language, statements are the fundamental unit of 044 * execution. All statements except blocks are terminated by a semicolon. 045 * Blocks are denoted by open and close curly braces. 046 * </p> 047 * 048 * <p> 049 * OneStatementPerLineCheck checks the following types of statements: 050 * block statements, variable declaration statements, import statements, 051 * assignment statements, expression statements, increment statements, 052 * object creation statements, 'for loop' statements, 'break' statements, 053 * 'continue' statements, 'return' statements, resources statements (optional). 054 * </p> 055 * 056 * <p> 057 * Notes: 058 * Unnecessary semicolons are ignored. 059 * </p> 060 * 061 * @since 5.3 062 */ 063@FileStatefulCheck 064public final class OneStatementPerLineCheck extends AbstractCheck { 065 066 /** 067 * A key is pointing to the warning message text in "messages.properties" 068 * file. 069 */ 070 public static final String MSG_KEY = "multiple.statements.line"; 071 072 /** Set of valid semi parent. */ 073 private static final Set<Integer> VALID_SEMI_PARENT = Set.of( 074 TokenTypes.VARIABLE_DEF, 075 TokenTypes.IMPORT, 076 TokenTypes.STATIC_IMPORT, 077 TokenTypes.MODULE_IMPORT, 078 TokenTypes.LITERAL_RETURN, 079 TokenTypes.LITERAL_BREAK, 080 TokenTypes.LITERAL_CONTINUE, 081 TokenTypes.PACKAGE_DEF, 082 TokenTypes.SUPER_CTOR_CALL, 083 TokenTypes.CTOR_CALL, 084 TokenTypes.LITERAL_ASSERT, 085 TokenTypes.LITERAL_YIELD, 086 TokenTypes.METHOD_DEF, 087 TokenTypes.ANNOTATION_FIELD_DEF 088 ); 089 090 /** 091 * Stack of statement line-number for nested lambdas and anonymous classes. 092 * Used to isolate validation to the current nesting depth. 093 */ 094 private final Deque<Integer> nestingScope = new ArrayDeque<>(); 095 096 /** 097 * Hold the line-number where the last statement ended. 098 */ 099 private int lastStatementEnd; 100 101 /** 102 * Hold the line-number where the last resource variable statement ended. 103 */ 104 private int lastVariableResourceStatementEnd; 105 106 /** 107 * Enable resources processing. 108 */ 109 private boolean treatTryResourcesAsStatement; 110 111 /** 112 * Hold the line number of the last statement in the current nesting scope. 113 */ 114 private int lastStatementInCurrentScope; 115 116 /** 117 * The first statement of lambda or anonymous class shouldn't be violation. 118 */ 119 private boolean isFirstStatementOfLambdaOrAnonymous; 120 121 /** 122 * Creates a new {@code OneStatementPerLineCheck} instance. 123 */ 124 public OneStatementPerLineCheck() { 125 // no code by default 126 } 127 128 /** 129 * Setter to enable resources processing. 130 * 131 * @param treatTryResourcesAsStatement user's value of treatTryResourcesAsStatement. 132 * @since 8.23 133 */ 134 public void setTreatTryResourcesAsStatement(boolean treatTryResourcesAsStatement) { 135 this.treatTryResourcesAsStatement = treatTryResourcesAsStatement; 136 } 137 138 @Override 139 public int[] getDefaultTokens() { 140 return getRequiredTokens(); 141 } 142 143 @Override 144 public int[] getAcceptableTokens() { 145 return getRequiredTokens(); 146 } 147 148 @Override 149 public int[] getRequiredTokens() { 150 return new int[] { 151 TokenTypes.SEMI, 152 TokenTypes.LAMBDA, 153 TokenTypes.OBJBLOCK, 154 TokenTypes.METHOD_CALL, 155 TokenTypes.CLASS_DEF, 156 TokenTypes.INTERFACE_DEF, 157 TokenTypes.ENUM_DEF, 158 TokenTypes.RECORD_DEF, 159 TokenTypes.ANNOTATION_DEF, 160 }; 161 } 162 163 @Override 164 public void beginTree(DetailAST rootAST) { 165 lastStatementEnd = 0; 166 lastVariableResourceStatementEnd = lastStatementEnd; 167 lastStatementInCurrentScope = lastStatementEnd; 168 } 169 170 @Override 171 public void visitToken(DetailAST ast) { 172 switch (ast.getType()) { 173 case TokenTypes.SEMI -> 174 checkIfSemicolonIsInDifferentLineThanPrevious(ast); 175 case TokenTypes.LAMBDA, TokenTypes.OBJBLOCK -> { 176 if (ast.getType() == TokenTypes.LAMBDA 177 || ast.getParent().getType() == TokenTypes.LITERAL_NEW) { 178 nestingScope.push(lastStatementEnd); 179 isFirstStatementOfLambdaOrAnonymous = true; 180 } 181 } 182 case TokenTypes.METHOD_CALL -> nestingScope.push(lastStatementEnd); 183 default -> { 184 // Expected block statements 185 DetailAST previousNode = ast.getPreviousSibling(); 186 if (previousNode != null) { 187 previousNode = getLastNestedLeafNode(previousNode); 188 if (TokenUtil.areOnSameLine(ast, previousNode)) { 189 logViolation(ast); 190 } 191 } 192 } 193 } 194 } 195 196 @Override 197 public void leaveToken(DetailAST ast) { 198 switch (ast.getType()) { 199 case TokenTypes.SEMI -> { 200 if (getStatementStart(ast).getType() != TokenTypes.SEMI) { 201 lastStatementEnd = ast.getLineNo(); 202 } 203 isFirstStatementOfLambdaOrAnonymous = false; 204 lastStatementInCurrentScope = lastStatementEnd; 205 } 206 case TokenTypes.LAMBDA, TokenTypes.OBJBLOCK -> { 207 if (ast.getType() == TokenTypes.LAMBDA 208 || ast.getParent().getType() == TokenTypes.LITERAL_NEW) { 209 lastStatementInCurrentScope = nestingScope.pop(); 210 isFirstStatementOfLambdaOrAnonymous = false; 211 } 212 } 213 case TokenTypes.METHOD_CALL -> lastStatementInCurrentScope = nestingScope.pop(); 214 default -> { 215 // do nothing 216 } 217 } 218 } 219 220 /** 221 * Checks if statement of given semicolon is in different line 222 * than previous statement. 223 * 224 * @param ast semicolon to check 225 */ 226 private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) { 227 boolean validStatement = true; 228 DetailAST statementStart = getStatementStart(ast); 229 if (isResource(ast.getParent())) { 230 validStatement = checkResourceVariable(ast); 231 statementStart = NullUtil.notNull(ast.getNextSibling()); 232 } 233 else if (!isFirstStatementOfLambdaOrAnonymous 234 && statementStart.getType() != TokenTypes.SEMI) { 235 validStatement = isValidStatement(statementStart); 236 } 237 if (!validStatement) { 238 logViolation(statementStart); 239 } 240 } 241 242 /** 243 * Logs a violation at the given AST node. 244 * 245 * @param violationNode token at which violation occurred. 246 */ 247 private void logViolation(DetailAST violationNode) { 248 log(violationNode, MSG_KEY); 249 } 250 251 /** 252 * Checks whether the current statement is placed on a separate line 253 * from the previous statement. 254 * 255 * @param statementStart token representing the start of the current statement 256 * @return {@code true} if the current statement starts on a different line 257 * than the previous statement; {@code false} otherwise 258 */ 259 private boolean isValidStatement(DetailAST statementStart) { 260 boolean blockStatementBefore = false; 261 DetailAST previousNode = statementStart.getPreviousSibling(); 262 final boolean isBlockStatement = TokenUtil.isOfType(previousNode, 263 TokenTypes.CLASS_DEF, 264 TokenTypes.INTERFACE_DEF, 265 TokenTypes.ENUM_DEF, 266 TokenTypes.RECORD_DEF, 267 TokenTypes.ANNOTATION_DEF, 268 TokenTypes.METHOD_DEF); 269 if (isBlockStatement) { 270 previousNode = getLastNestedLeafNode(previousNode); 271 blockStatementBefore = TokenUtil.areOnSameLine(statementStart, previousNode); 272 } 273 return !blockStatementBefore && statementStart.getLineNo() != lastStatementInCurrentScope; 274 } 275 276 /** 277 * Returns the starting node of the statement, or the previous statement's 278 * start if the current one is an empty statement. 279 * 280 * @param ast the SEMI token. 281 * @return the start of the associated statement or the previous sibling. 282 */ 283 private static DetailAST getStatementStart(DetailAST ast) { 284 DetailAST statementStart = ast; 285 final DetailAST parent = ast.getParent(); 286 final DetailAST previousSibling = ast.getPreviousSibling(); 287 final boolean validPreviousSibling = previousSibling != null 288 && (previousSibling.getType() == TokenTypes.VARIABLE_DEF 289 || previousSibling.getType() == TokenTypes.EXPR 290 || previousSibling.getType() == TokenTypes.ENUM_CONSTANT_DEF) 291 && previousSibling.findFirstToken(TokenTypes.SEMI) == null; 292 if (VALID_SEMI_PARENT.contains(parent.getType())) { 293 statementStart = ast.getParent(); 294 } 295 else if (validPreviousSibling) { 296 statementStart = getStartNodeInExpression(previousSibling); 297 } 298 return statementStart; 299 } 300 301 /** 302 * Checks whether the current statement represents a valid resource 303 * declaration in a try-with-resources statement. 304 * 305 * @param currentStatement the statement to check 306 * @return {@code true} if the statement is a valid resource declaration; 307 * {@code false} otherwise. 308 */ 309 private boolean checkResourceVariable(DetailAST currentStatement) { 310 boolean result = true; 311 if (treatTryResourcesAsStatement) { 312 final DetailAST nextNode = currentStatement.getNextSibling(); 313 if (currentStatement.getPreviousSibling().findFirstToken(TokenTypes.ASSIGN) != null) { 314 lastVariableResourceStatementEnd = currentStatement.getLineNo(); 315 } 316 result = nextNode.findFirstToken(TokenTypes.ASSIGN) == null 317 || nextNode.getLineNo() != lastVariableResourceStatementEnd; 318 } 319 return result; 320 } 321 322 /** 323 * Finds the leftmost leaf node in the given expression's subtree. 324 * 325 * @param expression EXPR node. 326 * @return the leftmost leaf {@code DetailAST} node 327 */ 328 private static DetailAST getStartNodeInExpression(DetailAST expression) { 329 DetailAST child = expression; 330 while (child.hasChildren()) { 331 child = child.getFirstChild(); 332 } 333 return child; 334 } 335 336 /** 337 * Finds the last nested leaf node. 338 * 339 * @param startToken start token of statement. 340 * @return the last nested leaf {@code DetailAST} node 341 */ 342 private static DetailAST getLastNestedLeafNode(DetailAST startToken) { 343 DetailAST current = startToken; 344 while (current.hasChildren()) { 345 current = current.getLastChild(); 346 } 347 return current; 348 } 349 350 /** 351 * Checks that given node is a resource. 352 * 353 * @param ast semicolon to check 354 * @return true if node is a resource 355 */ 356 private static boolean isResource(DetailAST ast) { 357 return ast.getType() == TokenTypes.RESOURCES; 358 } 359 360}