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; 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 * 053 * @since 5.3 054 */ 055@FileStatefulCheck 056public final class OneStatementPerLineCheck extends AbstractCheck { 057 058 /** 059 * A key is pointing to the warning message text in "messages.properties" 060 * file. 061 */ 062 public static final String MSG_KEY = "multiple.statements.line"; 063 064 /** 065 * Counts number of semicolons in nested lambdas. 066 */ 067 private final Deque<Integer> countOfSemiInLambda = new ArrayDeque<>(); 068 069 /** 070 * Hold the line-number where the last statement ended. 071 */ 072 private int lastStatementEnd; 073 074 /** 075 * Hold the line-number where the last 'for-loop' statement ended. 076 */ 077 private int forStatementEnd; 078 079 /** 080 * The for-header usually has 3 statements on one line, but THIS IS OK. 081 */ 082 private boolean inForHeader; 083 084 /** 085 * Holds if current token is inside lambda. 086 */ 087 private boolean isInLambda; 088 089 /** 090 * Hold the line-number where the last lambda statement ended. 091 */ 092 private int lambdaStatementEnd; 093 094 /** 095 * Hold the line-number where the last resource variable statement ended. 096 */ 097 private int lastVariableResourceStatementEnd; 098 099 /** 100 * Enable resources processing. 101 */ 102 private boolean treatTryResourcesAsStatement; 103 104 /** 105 * Creates a new {@code OneStatementPerLineCheck} instance. 106 */ 107 public OneStatementPerLineCheck() { 108 // no code by default 109 } 110 111 /** 112 * Setter to enable resources processing. 113 * 114 * @param treatTryResourcesAsStatement user's value of treatTryResourcesAsStatement. 115 * @since 8.23 116 */ 117 public void setTreatTryResourcesAsStatement(boolean treatTryResourcesAsStatement) { 118 this.treatTryResourcesAsStatement = treatTryResourcesAsStatement; 119 } 120 121 @Override 122 public int[] getDefaultTokens() { 123 return getRequiredTokens(); 124 } 125 126 @Override 127 public int[] getAcceptableTokens() { 128 return getRequiredTokens(); 129 } 130 131 @Override 132 public int[] getRequiredTokens() { 133 return new int[] { 134 TokenTypes.SEMI, 135 TokenTypes.FOR_INIT, 136 TokenTypes.FOR_ITERATOR, 137 TokenTypes.LAMBDA, 138 }; 139 } 140 141 @Override 142 public void beginTree(DetailAST rootAST) { 143 lastStatementEnd = 0; 144 lastVariableResourceStatementEnd = 0; 145 } 146 147 @Override 148 public void visitToken(DetailAST ast) { 149 switch (ast.getType()) { 150 case TokenTypes.SEMI -> checkIfSemicolonIsInDifferentLineThanPrevious(ast); 151 case TokenTypes.FOR_ITERATOR -> forStatementEnd = ast.getLineNo(); 152 case TokenTypes.LAMBDA -> { 153 isInLambda = true; 154 countOfSemiInLambda.push(0); 155 } 156 default -> inForHeader = true; 157 } 158 } 159 160 @Override 161 public void leaveToken(DetailAST ast) { 162 switch (ast.getType()) { 163 case TokenTypes.SEMI -> { 164 lastStatementEnd = ast.getLineNo(); 165 forStatementEnd = 0; 166 lambdaStatementEnd = 0; 167 } 168 case TokenTypes.FOR_ITERATOR -> inForHeader = false; 169 case TokenTypes.LAMBDA -> { 170 countOfSemiInLambda.pop(); 171 if (countOfSemiInLambda.isEmpty()) { 172 isInLambda = false; 173 } 174 lambdaStatementEnd = ast.getLineNo(); 175 } 176 default -> { 177 // do nothing 178 } 179 } 180 } 181 182 /** 183 * Checks if given semicolon is in different line than previous. 184 * 185 * @param ast semicolon to check 186 */ 187 private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) { 188 DetailAST currentStatement = ast; 189 final DetailAST previousSibling = ast.getPreviousSibling(); 190 final boolean isUnnecessarySemicolon = previousSibling == null 191 || previousSibling.getType() == TokenTypes.RESOURCES 192 || ast.getParent().getType() == TokenTypes.COMPILATION_UNIT; 193 if (!isUnnecessarySemicolon) { 194 currentStatement = ast.getPreviousSibling(); 195 } 196 if (isInLambda) { 197 checkLambda(ast, currentStatement); 198 } 199 else if (isResource(ast.getParent())) { 200 checkResourceVariable(ast); 201 } 202 else if (!inForHeader && isOnTheSameLine(currentStatement, lastStatementEnd, 203 forStatementEnd, lambdaStatementEnd)) { 204 log(ast, MSG_KEY); 205 } 206 } 207 208 /** 209 * Checks semicolon placement in lambda. 210 * 211 * @param ast semicolon to check 212 * @param currentStatement current statement 213 */ 214 private void checkLambda(DetailAST ast, DetailAST currentStatement) { 215 int countOfSemiInCurrentLambda = countOfSemiInLambda.pop(); 216 countOfSemiInCurrentLambda++; 217 countOfSemiInLambda.push(countOfSemiInCurrentLambda); 218 if (!inForHeader && countOfSemiInCurrentLambda > 1 219 && isOnTheSameLine(currentStatement, 220 lastStatementEnd, forStatementEnd, 221 lambdaStatementEnd)) { 222 log(ast, MSG_KEY); 223 } 224 } 225 226 /** 227 * Checks that given node is a resource. 228 * 229 * @param ast semicolon to check 230 * @return true if node is a resource 231 */ 232 private static boolean isResource(DetailAST ast) { 233 return ast.getType() == TokenTypes.RESOURCES 234 || ast.getType() == TokenTypes.RESOURCE_SPECIFICATION; 235 } 236 237 /** 238 * Checks resource variable. 239 * 240 * @param currentStatement current statement 241 */ 242 private void checkResourceVariable(DetailAST currentStatement) { 243 if (treatTryResourcesAsStatement) { 244 final DetailAST nextNode = currentStatement.getNextSibling(); 245 if (currentStatement.getPreviousSibling().findFirstToken(TokenTypes.ASSIGN) != null) { 246 lastVariableResourceStatementEnd = currentStatement.getLineNo(); 247 } 248 if (nextNode.findFirstToken(TokenTypes.ASSIGN) != null 249 && nextNode.getLineNo() == lastVariableResourceStatementEnd) { 250 log(currentStatement, MSG_KEY); 251 } 252 } 253 } 254 255 /** 256 * Checks whether two statements are on the same line. 257 * 258 * @param ast token for the current statement. 259 * @param lastStatementEnd the line-number where the last statement ended. 260 * @param forStatementEnd the line-number where the last 'for-loop' 261 * statement ended. 262 * @param lambdaStatementEnd the line-number where the last lambda 263 * statement ended. 264 * @return true if two statements are on the same line. 265 */ 266 private static boolean isOnTheSameLine(DetailAST ast, int lastStatementEnd, 267 int forStatementEnd, int lambdaStatementEnd) { 268 return lastStatementEnd == ast.getLineNo() && forStatementEnd != ast.getLineNo() 269 && lambdaStatementEnd != ast.getLineNo(); 270 } 271 272}