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