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