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