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