001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 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; 029import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 030 031/** 032 * <p> 033 * Checks that there is only one statement per line. 034 * </p> 035 * <p> 036 * Rationale: It's very difficult to read multiple statements on one line. 037 * </p> 038 * <p> 039 * In the Java programming language, statements are the fundamental unit of 040 * execution. All statements except blocks are terminated by a semicolon. 041 * Blocks are denoted by open and close curly braces. 042 * </p> 043 * <p> 044 * OneStatementPerLineCheck checks the following types of statements: 045 * variable declaration statements, empty statements, import statements, 046 * assignment statements, expression statements, increment statements, 047 * object creation statements, 'for loop' statements, 'break' statements, 048 * 'continue' statements, 'return' statements, resources statements (optional). 049 * </p> 050 * <ul> 051 * <li> 052 * Property {@code treatTryResourcesAsStatement} - Enable resources processing. 053 * Type is {@code boolean}. 054 * Default value is {@code false}. 055 * </li> 056 * </ul> 057 * <p> 058 * An example of how to configure this Check: 059 * </p> 060 * <pre> 061 * <module name="OneStatementPerLine"/> 062 * </pre> 063 * <p> 064 * The following examples will be flagged as a violation: 065 * </p> 066 * <pre> 067 * //Each line causes violation: 068 * int var1; int var2; 069 * var1 = 1; var2 = 2; 070 * int var1 = 1; int var2 = 2; 071 * var1++; var2++; 072 * Object obj1 = new Object(); Object obj2 = new Object(); 073 * import java.io.EOFException; import java.io.BufferedReader; 074 * ;; //two empty statements on the same line. 075 * 076 * //Multi-line statements: 077 * int var1 = 1 078 * ; var2 = 2; //violation here 079 * int o = 1, p = 2, 080 * r = 5; int t; //violation here 081 * </pre> 082 * <p> 083 * An example of how to configure the check to treat resources 084 * in a try statement as statements to require them on their own line: 085 * </p> 086 * <pre> 087 * <module name="OneStatementPerLine"> 088 * <property name="treatTryResourcesAsStatement" value="true"/> 089 * </module> 090 * </pre> 091 * <p> 092 * Note: resource declarations can contain variable definitions 093 * and variable references (from java9). 094 * When property "treatTryResourcesAsStatement" is enabled, 095 * this check is only applied to variable definitions. 096 * If there are one or more variable references 097 * and one variable definition on the same line in resources declaration, 098 * there is no violation. 099 * The following examples will illustrate difference: 100 * </p> 101 * <pre> 102 * OutputStream s1 = new PipedOutputStream(); 103 * OutputStream s2 = new PipedOutputStream(); 104 * // only one statement(variable definition) with two variable references 105 * try (s1; s2; OutputStream s3 = new PipedOutputStream();) // OK 106 * {} 107 * // two statements with variable definitions 108 * try (Reader r = new PipedReader(); s2; Reader s3 = new PipedReader() // violation 109 * ) {} 110 * </pre> 111 * <p> 112 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 113 * </p> 114 * <p> 115 * Violation Message Keys: 116 * </p> 117 * <ul> 118 * <li> 119 * {@code multiple.statements.line} 120 * </li> 121 * </ul> 122 * 123 * @since 5.3 124 */ 125@FileStatefulCheck 126public final class OneStatementPerLineCheck extends AbstractCheck { 127 128 /** 129 * A key is pointing to the warning message text in "messages.properties" 130 * file. 131 */ 132 public static final String MSG_KEY = "multiple.statements.line"; 133 134 /** 135 * Counts number of semicolons in nested lambdas. 136 */ 137 private final Deque<Integer> countOfSemiInLambda = new ArrayDeque<>(); 138 139 /** 140 * Hold the line-number where the last statement ended. 141 */ 142 private int lastStatementEnd = -1; 143 144 /** 145 * Hold the line-number where the last 'for-loop' statement ended. 146 */ 147 private int forStatementEnd = -1; 148 149 /** 150 * The for-header usually has 3 statements on one line, but THIS IS OK. 151 */ 152 private boolean inForHeader; 153 154 /** 155 * Holds if current token is inside lambda. 156 */ 157 private boolean isInLambda; 158 159 /** 160 * Hold the line-number where the last lambda statement ended. 161 */ 162 private int lambdaStatementEnd = -1; 163 164 /** 165 * Hold the line-number where the last resource variable statement ended. 166 */ 167 private int lastVariableResourceStatementEnd = -1; 168 169 /** 170 * Enable resources processing. 171 */ 172 private boolean treatTryResourcesAsStatement; 173 174 /** 175 * Setter to enable resources processing. 176 * 177 * @param treatTryResourcesAsStatement user's value of treatTryResourcesAsStatement. 178 */ 179 public void setTreatTryResourcesAsStatement(boolean treatTryResourcesAsStatement) { 180 this.treatTryResourcesAsStatement = treatTryResourcesAsStatement; 181 } 182 183 @Override 184 public int[] getDefaultTokens() { 185 return getRequiredTokens(); 186 } 187 188 @Override 189 public int[] getAcceptableTokens() { 190 return getRequiredTokens(); 191 } 192 193 @Override 194 public int[] getRequiredTokens() { 195 return new int[] { 196 TokenTypes.SEMI, 197 TokenTypes.FOR_INIT, 198 TokenTypes.FOR_ITERATOR, 199 TokenTypes.LAMBDA, 200 }; 201 } 202 203 @Override 204 public void beginTree(DetailAST rootAST) { 205 inForHeader = false; 206 lastStatementEnd = -1; 207 forStatementEnd = -1; 208 isInLambda = false; 209 lastVariableResourceStatementEnd = -1; 210 } 211 212 @Override 213 public void visitToken(DetailAST ast) { 214 switch (ast.getType()) { 215 case TokenTypes.SEMI: 216 checkIfSemicolonIsInDifferentLineThanPrevious(ast); 217 break; 218 case TokenTypes.FOR_ITERATOR: 219 forStatementEnd = ast.getLineNo(); 220 break; 221 case TokenTypes.LAMBDA: 222 isInLambda = true; 223 countOfSemiInLambda.push(0); 224 break; 225 default: 226 inForHeader = true; 227 break; 228 } 229 } 230 231 @Override 232 public void leaveToken(DetailAST ast) { 233 switch (ast.getType()) { 234 case TokenTypes.SEMI: 235 lastStatementEnd = ast.getLineNo(); 236 forStatementEnd = -1; 237 lambdaStatementEnd = -1; 238 break; 239 case TokenTypes.FOR_ITERATOR: 240 inForHeader = false; 241 break; 242 case TokenTypes.LAMBDA: 243 countOfSemiInLambda.pop(); 244 if (countOfSemiInLambda.isEmpty()) { 245 isInLambda = false; 246 } 247 lambdaStatementEnd = ast.getLineNo(); 248 break; 249 default: 250 break; 251 } 252 } 253 254 /** 255 * Checks if given semicolon is in different line than previous. 256 * 257 * @param ast semicolon to check 258 */ 259 private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) { 260 DetailAST currentStatement = ast; 261 final boolean hasResourcesPrevSibling = 262 currentStatement.getPreviousSibling() != null 263 && currentStatement.getPreviousSibling().getType() == TokenTypes.RESOURCES; 264 if (!hasResourcesPrevSibling && isMultilineStatement(currentStatement)) { 265 currentStatement = ast.getPreviousSibling(); 266 } 267 if (isInLambda) { 268 checkLambda(ast, currentStatement); 269 } 270 else if (isResource(ast.getParent())) { 271 checkResourceVariable(ast); 272 } 273 else if (!inForHeader && isOnTheSameLine(currentStatement, lastStatementEnd, 274 forStatementEnd, lambdaStatementEnd)) { 275 log(ast, MSG_KEY); 276 } 277 } 278 279 private void checkLambda(DetailAST ast, DetailAST currentStatement) { 280 int countOfSemiInCurrentLambda = countOfSemiInLambda.pop(); 281 countOfSemiInCurrentLambda++; 282 countOfSemiInLambda.push(countOfSemiInCurrentLambda); 283 if (!inForHeader && countOfSemiInCurrentLambda > 1 284 && isOnTheSameLine(currentStatement, 285 lastStatementEnd, forStatementEnd, 286 lambdaStatementEnd)) { 287 log(ast, MSG_KEY); 288 } 289 } 290 291 private static boolean isResource(DetailAST ast) { 292 return ast != null 293 && (ast.getType() == TokenTypes.RESOURCES 294 || ast.getType() == TokenTypes.RESOURCE_SPECIFICATION); 295 } 296 297 private void checkResourceVariable(DetailAST currentStatement) { 298 if (treatTryResourcesAsStatement) { 299 final DetailAST nextNode = currentStatement.getNextSibling(); 300 if (currentStatement.getPreviousSibling().findFirstToken(TokenTypes.ASSIGN) != null) { 301 lastVariableResourceStatementEnd = currentStatement.getLineNo(); 302 } 303 if (nextNode.findFirstToken(TokenTypes.ASSIGN) != null 304 && nextNode.getLineNo() == lastVariableResourceStatementEnd) { 305 log(currentStatement, MSG_KEY); 306 } 307 } 308 } 309 310 /** 311 * Checks whether two statements are on the same line. 312 * 313 * @param ast token for the current statement. 314 * @param lastStatementEnd the line-number where the last statement ended. 315 * @param forStatementEnd the line-number where the last 'for-loop' 316 * statement ended. 317 * @param lambdaStatementEnd the line-number where the last lambda 318 * statement ended. 319 * @return true if two statements are on the same line. 320 */ 321 private static boolean isOnTheSameLine(DetailAST ast, int lastStatementEnd, 322 int forStatementEnd, int lambdaStatementEnd) { 323 return lastStatementEnd == ast.getLineNo() && forStatementEnd != ast.getLineNo() 324 && lambdaStatementEnd != ast.getLineNo(); 325 } 326 327 /** 328 * Checks whether statement is multiline. 329 * 330 * @param ast token for the current statement. 331 * @return true if one statement is distributed over two or more lines. 332 */ 333 private static boolean isMultilineStatement(DetailAST ast) { 334 final boolean multiline; 335 if (ast.getPreviousSibling() == null) { 336 multiline = false; 337 } 338 else { 339 final DetailAST prevSibling = ast.getPreviousSibling(); 340 multiline = !TokenUtil.areOnSameLine(prevSibling, ast) 341 && ast.getParent() != null; 342 } 343 return multiline; 344 } 345 346}