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.coding; 021 022import java.util.ArrayDeque; 023import java.util.Deque; 024 025import com.puppycrawl.tools.checkstyle.FileStatefulCheck; 026import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.TokenTypes; 029 030/** 031 * <div> 032 * Checks that there is only one statement per line. 033 * </div> 034 * 035 * <p> 036 * Rationale: It's very difficult to read multiple statements on one line. 037 * </p> 038 * 039 * <p> 040 * In the Java programming language, statements are the fundamental unit of 041 * execution. All statements except blocks are terminated by a semicolon. 042 * Blocks are denoted by open and close curly braces. 043 * </p> 044 * 045 * <p> 046 * OneStatementPerLineCheck checks the following types of statements: 047 * variable declaration statements, empty statements, import statements, 048 * assignment statements, expression statements, increment statements, 049 * object creation statements, 'for loop' statements, 'break' statements, 050 * 'continue' statements, 'return' statements, resources statements (optional). 051 * </p> 052 * <ul> 053 * <li> 054 * Property {@code treatTryResourcesAsStatement} - Enable resources processing. 055 * Type is {@code boolean}. 056 * Default value is {@code false}. 057 * </li> 058 * </ul> 059 * 060 * <p> 061 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 062 * </p> 063 * 064 * <p> 065 * Violation Message Keys: 066 * </p> 067 * <ul> 068 * <li> 069 * {@code multiple.statements.line} 070 * </li> 071 * </ul> 072 * 073 * @since 5.3 074 */ 075@FileStatefulCheck 076public final class OneStatementPerLineCheck extends AbstractCheck { 077 078 /** 079 * A key is pointing to the warning message text in "messages.properties" 080 * file. 081 */ 082 public static final String MSG_KEY = "multiple.statements.line"; 083 084 /** 085 * Counts number of semicolons in nested lambdas. 086 */ 087 private final Deque<Integer> countOfSemiInLambda = new ArrayDeque<>(); 088 089 /** 090 * Hold the line-number where the last statement ended. 091 */ 092 private int lastStatementEnd; 093 094 /** 095 * Hold the line-number where the last 'for-loop' statement ended. 096 */ 097 private int forStatementEnd; 098 099 /** 100 * The for-header usually has 3 statements on one line, but THIS IS OK. 101 */ 102 private boolean inForHeader; 103 104 /** 105 * Holds if current token is inside lambda. 106 */ 107 private boolean isInLambda; 108 109 /** 110 * Hold the line-number where the last lambda statement ended. 111 */ 112 private int lambdaStatementEnd; 113 114 /** 115 * Hold the line-number where the last resource variable statement ended. 116 */ 117 private int lastVariableResourceStatementEnd; 118 119 /** 120 * Enable resources processing. 121 */ 122 private boolean treatTryResourcesAsStatement; 123 124 /** 125 * Setter to enable resources processing. 126 * 127 * @param treatTryResourcesAsStatement user's value of treatTryResourcesAsStatement. 128 * @since 8.23 129 */ 130 public void setTreatTryResourcesAsStatement(boolean treatTryResourcesAsStatement) { 131 this.treatTryResourcesAsStatement = treatTryResourcesAsStatement; 132 } 133 134 @Override 135 public int[] getDefaultTokens() { 136 return getRequiredTokens(); 137 } 138 139 @Override 140 public int[] getAcceptableTokens() { 141 return getRequiredTokens(); 142 } 143 144 @Override 145 public int[] getRequiredTokens() { 146 return new int[] { 147 TokenTypes.SEMI, 148 TokenTypes.FOR_INIT, 149 TokenTypes.FOR_ITERATOR, 150 TokenTypes.LAMBDA, 151 }; 152 } 153 154 @Override 155 public void beginTree(DetailAST rootAST) { 156 lastStatementEnd = 0; 157 lastVariableResourceStatementEnd = 0; 158 } 159 160 @Override 161 public void visitToken(DetailAST ast) { 162 switch (ast.getType()) { 163 case TokenTypes.SEMI -> checkIfSemicolonIsInDifferentLineThanPrevious(ast); 164 case TokenTypes.FOR_ITERATOR -> forStatementEnd = ast.getLineNo(); 165 case TokenTypes.LAMBDA -> { 166 isInLambda = true; 167 countOfSemiInLambda.push(0); 168 } 169 default -> inForHeader = true; 170 } 171 } 172 173 @Override 174 public void leaveToken(DetailAST ast) { 175 switch (ast.getType()) { 176 case TokenTypes.SEMI -> { 177 lastStatementEnd = ast.getLineNo(); 178 forStatementEnd = 0; 179 lambdaStatementEnd = 0; 180 } 181 case TokenTypes.FOR_ITERATOR -> inForHeader = false; 182 case TokenTypes.LAMBDA -> { 183 countOfSemiInLambda.pop(); 184 if (countOfSemiInLambda.isEmpty()) { 185 isInLambda = false; 186 } 187 lambdaStatementEnd = ast.getLineNo(); 188 } 189 default -> { 190 // do nothing 191 } 192 } 193 } 194 195 /** 196 * Checks if given semicolon is in different line than previous. 197 * 198 * @param ast semicolon to check 199 */ 200 private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) { 201 DetailAST currentStatement = ast; 202 final DetailAST previousSibling = ast.getPreviousSibling(); 203 final boolean isUnnecessarySemicolon = previousSibling == null 204 || previousSibling.getType() == TokenTypes.RESOURCES 205 || ast.getParent().getType() == TokenTypes.COMPILATION_UNIT; 206 if (!isUnnecessarySemicolon) { 207 currentStatement = ast.getPreviousSibling(); 208 } 209 if (isInLambda) { 210 checkLambda(ast, currentStatement); 211 } 212 else if (isResource(ast.getParent())) { 213 checkResourceVariable(ast); 214 } 215 else if (!inForHeader && isOnTheSameLine(currentStatement, lastStatementEnd, 216 forStatementEnd, lambdaStatementEnd)) { 217 log(ast, MSG_KEY); 218 } 219 } 220 221 /** 222 * Checks semicolon placement in lambda. 223 * 224 * @param ast semicolon to check 225 * @param currentStatement current statement 226 */ 227 private void checkLambda(DetailAST ast, DetailAST currentStatement) { 228 int countOfSemiInCurrentLambda = countOfSemiInLambda.pop(); 229 countOfSemiInCurrentLambda++; 230 countOfSemiInLambda.push(countOfSemiInCurrentLambda); 231 if (!inForHeader && countOfSemiInCurrentLambda > 1 232 && isOnTheSameLine(currentStatement, 233 lastStatementEnd, forStatementEnd, 234 lambdaStatementEnd)) { 235 log(ast, MSG_KEY); 236 } 237 } 238 239 /** 240 * Checks that given node is a resource. 241 * 242 * @param ast semicolon to check 243 * @return true if node is a resource 244 */ 245 private static boolean isResource(DetailAST ast) { 246 return ast.getType() == TokenTypes.RESOURCES 247 || ast.getType() == TokenTypes.RESOURCE_SPECIFICATION; 248 } 249 250 /** 251 * Checks resource variable. 252 * 253 * @param currentStatement current statement 254 */ 255 private void checkResourceVariable(DetailAST currentStatement) { 256 if (treatTryResourcesAsStatement) { 257 final DetailAST nextNode = currentStatement.getNextSibling(); 258 if (currentStatement.getPreviousSibling().findFirstToken(TokenTypes.ASSIGN) != null) { 259 lastVariableResourceStatementEnd = currentStatement.getLineNo(); 260 } 261 if (nextNode.findFirstToken(TokenTypes.ASSIGN) != null 262 && nextNode.getLineNo() == lastVariableResourceStatementEnd) { 263 log(currentStatement, MSG_KEY); 264 } 265 } 266 } 267 268 /** 269 * Checks whether two statements are on the same line. 270 * 271 * @param ast token for the current statement. 272 * @param lastStatementEnd the line-number where the last statement ended. 273 * @param forStatementEnd the line-number where the last 'for-loop' 274 * statement ended. 275 * @param lambdaStatementEnd the line-number where the last lambda 276 * statement ended. 277 * @return true if two statements are on the same line. 278 */ 279 private static boolean isOnTheSameLine(DetailAST ast, int lastStatementEnd, 280 int forStatementEnd, int lambdaStatementEnd) { 281 return lastStatementEnd == ast.getLineNo() && forStatementEnd != ast.getLineNo() 282 && lambdaStatementEnd != ast.getLineNo(); 283 } 284 285}