001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2024 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: 164 checkIfSemicolonIsInDifferentLineThanPrevious(ast); 165 break; 166 case TokenTypes.FOR_ITERATOR: 167 forStatementEnd = ast.getLineNo(); 168 break; 169 case TokenTypes.LAMBDA: 170 isInLambda = true; 171 countOfSemiInLambda.push(0); 172 break; 173 default: 174 inForHeader = true; 175 break; 176 } 177 } 178 179 @Override 180 public void leaveToken(DetailAST ast) { 181 switch (ast.getType()) { 182 case TokenTypes.SEMI: 183 lastStatementEnd = ast.getLineNo(); 184 forStatementEnd = 0; 185 lambdaStatementEnd = 0; 186 break; 187 case TokenTypes.FOR_ITERATOR: 188 inForHeader = false; 189 break; 190 case TokenTypes.LAMBDA: 191 countOfSemiInLambda.pop(); 192 if (countOfSemiInLambda.isEmpty()) { 193 isInLambda = false; 194 } 195 lambdaStatementEnd = ast.getLineNo(); 196 break; 197 default: 198 break; 199 } 200 } 201 202 /** 203 * Checks if given semicolon is in different line than previous. 204 * 205 * @param ast semicolon to check 206 */ 207 private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) { 208 DetailAST currentStatement = ast; 209 final DetailAST previousSibling = ast.getPreviousSibling(); 210 final boolean isUnnecessarySemicolon = previousSibling == null 211 || previousSibling.getType() == TokenTypes.RESOURCES 212 || ast.getParent().getType() == TokenTypes.COMPILATION_UNIT; 213 if (!isUnnecessarySemicolon) { 214 currentStatement = ast.getPreviousSibling(); 215 } 216 if (isInLambda) { 217 checkLambda(ast, currentStatement); 218 } 219 else if (isResource(ast.getParent())) { 220 checkResourceVariable(ast); 221 } 222 else if (!inForHeader && isOnTheSameLine(currentStatement, lastStatementEnd, 223 forStatementEnd, lambdaStatementEnd)) { 224 log(ast, MSG_KEY); 225 } 226 } 227 228 /** 229 * Checks semicolon placement in lambda. 230 * 231 * @param ast semicolon to check 232 * @param currentStatement current statement 233 */ 234 private void checkLambda(DetailAST ast, DetailAST currentStatement) { 235 int countOfSemiInCurrentLambda = countOfSemiInLambda.pop(); 236 countOfSemiInCurrentLambda++; 237 countOfSemiInLambda.push(countOfSemiInCurrentLambda); 238 if (!inForHeader && countOfSemiInCurrentLambda > 1 239 && isOnTheSameLine(currentStatement, 240 lastStatementEnd, forStatementEnd, 241 lambdaStatementEnd)) { 242 log(ast, MSG_KEY); 243 } 244 } 245 246 /** 247 * Checks that given node is a resource. 248 * 249 * @param ast semicolon to check 250 * @return true if node is a resource 251 */ 252 private static boolean isResource(DetailAST ast) { 253 return ast.getType() == TokenTypes.RESOURCES 254 || ast.getType() == TokenTypes.RESOURCE_SPECIFICATION; 255 } 256 257 /** 258 * Checks resource variable. 259 * 260 * @param currentStatement current statement 261 */ 262 private void checkResourceVariable(DetailAST currentStatement) { 263 if (treatTryResourcesAsStatement) { 264 final DetailAST nextNode = currentStatement.getNextSibling(); 265 if (currentStatement.getPreviousSibling().findFirstToken(TokenTypes.ASSIGN) != null) { 266 lastVariableResourceStatementEnd = currentStatement.getLineNo(); 267 } 268 if (nextNode.findFirstToken(TokenTypes.ASSIGN) != null 269 && nextNode.getLineNo() == lastVariableResourceStatementEnd) { 270 log(currentStatement, MSG_KEY); 271 } 272 } 273 } 274 275 /** 276 * Checks whether two statements are on the same line. 277 * 278 * @param ast token for the current statement. 279 * @param lastStatementEnd the line-number where the last statement ended. 280 * @param forStatementEnd the line-number where the last 'for-loop' 281 * statement ended. 282 * @param lambdaStatementEnd the line-number where the last lambda 283 * statement ended. 284 * @return true if two statements are on the same line. 285 */ 286 private static boolean isOnTheSameLine(DetailAST ast, int lastStatementEnd, 287 int forStatementEnd, int lambdaStatementEnd) { 288 return lastStatementEnd == ast.getLineNo() && forStatementEnd != ast.getLineNo() 289 && lambdaStatementEnd != ast.getLineNo(); 290 } 291 292}