1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2024 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.coding;
21
22 import java.util.ArrayDeque;
23 import java.util.Deque;
24
25 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
26 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
27 import com.puppycrawl.tools.checkstyle.api.DetailAST;
28 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
29
30 /**
31 * <p>
32 * Checks that there is only one statement per line.
33 * </p>
34 * <p>
35 * Rationale: It's very difficult to read multiple statements on one line.
36 * </p>
37 * <p>
38 * In the Java programming language, statements are the fundamental unit of
39 * execution. All statements except blocks are terminated by a semicolon.
40 * Blocks are denoted by open and close curly braces.
41 * </p>
42 * <p>
43 * OneStatementPerLineCheck checks the following types of statements:
44 * variable declaration statements, empty statements, import statements,
45 * assignment statements, expression statements, increment statements,
46 * object creation statements, 'for loop' statements, 'break' statements,
47 * 'continue' statements, 'return' statements, resources statements (optional).
48 * </p>
49 * <ul>
50 * <li>
51 * Property {@code treatTryResourcesAsStatement} - Enable resources processing.
52 * Type is {@code boolean}.
53 * Default value is {@code false}.
54 * </li>
55 * </ul>
56 * <p>
57 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
58 * </p>
59 * <p>
60 * Violation Message Keys:
61 * </p>
62 * <ul>
63 * <li>
64 * {@code multiple.statements.line}
65 * </li>
66 * </ul>
67 *
68 * @since 5.3
69 */
70 @FileStatefulCheck
71 public final class OneStatementPerLineCheck extends AbstractCheck {
72
73 /**
74 * A key is pointing to the warning message text in "messages.properties"
75 * file.
76 */
77 public static final String MSG_KEY = "multiple.statements.line";
78
79 /**
80 * Counts number of semicolons in nested lambdas.
81 */
82 private final Deque<Integer> countOfSemiInLambda = new ArrayDeque<>();
83
84 /**
85 * Hold the line-number where the last statement ended.
86 */
87 private int lastStatementEnd;
88
89 /**
90 * Hold the line-number where the last 'for-loop' statement ended.
91 */
92 private int forStatementEnd;
93
94 /**
95 * The for-header usually has 3 statements on one line, but THIS IS OK.
96 */
97 private boolean inForHeader;
98
99 /**
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 }