1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 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 * <div>
32 * Checks that there is only one statement per line.
33 * </div>
34 *
35 * <p>
36 * Rationale: It's very difficult to read multiple statements on one line.
37 * </p>
38 *
39 * <p>
40 * In the Java programming language, statements are the fundamental unit of
41 * execution. All statements except blocks are terminated by a semicolon.
42 * Blocks are denoted by open and close curly braces.
43 * </p>
44 *
45 * <p>
46 * OneStatementPerLineCheck checks the following types of statements:
47 * variable declaration statements, empty statements, import statements,
48 * assignment statements, expression statements, increment statements,
49 * object creation statements, 'for loop' statements, 'break' statements,
50 * 'continue' statements, 'return' statements, resources statements (optional).
51 * </p>
52 *
53 * @since 5.3
54 */
55 @FileStatefulCheck
56 public final class OneStatementPerLineCheck extends AbstractCheck {
57
58 /**
59 * A key is pointing to the warning message text in "messages.properties"
60 * file.
61 */
62 public static final String MSG_KEY = "multiple.statements.line";
63
64 /**
65 * Counts number of semicolons in nested lambdas.
66 */
67 private final Deque<Integer> countOfSemiInLambda = new ArrayDeque<>();
68
69 /**
70 * Hold the line-number where the last statement ended.
71 */
72 private int lastStatementEnd;
73
74 /**
75 * Hold the line-number where the last 'for-loop' statement ended.
76 */
77 private int forStatementEnd;
78
79 /**
80 * The for-header usually has 3 statements on one line, but THIS IS OK.
81 */
82 private boolean inForHeader;
83
84 /**
85 * Holds if current token is inside lambda.
86 */
87 private boolean isInLambda;
88
89 /**
90 * Hold the line-number where the last lambda statement ended.
91 */
92 private int lambdaStatementEnd;
93
94 /**
95 * Hold the line-number where the last resource variable statement ended.
96 */
97 private int lastVariableResourceStatementEnd;
98
99 /**
100 * Enable resources processing.
101 */
102 private boolean treatTryResourcesAsStatement;
103
104 /**
105 * Creates a new {@code OneStatementPerLineCheck} instance.
106 */
107 public OneStatementPerLineCheck() {
108 // no code by default
109 }
110
111 /**
112 * Setter to enable resources processing.
113 *
114 * @param treatTryResourcesAsStatement user's value of treatTryResourcesAsStatement.
115 * @since 8.23
116 */
117 public void setTreatTryResourcesAsStatement(boolean treatTryResourcesAsStatement) {
118 this.treatTryResourcesAsStatement = treatTryResourcesAsStatement;
119 }
120
121 @Override
122 public int[] getDefaultTokens() {
123 return getRequiredTokens();
124 }
125
126 @Override
127 public int[] getAcceptableTokens() {
128 return getRequiredTokens();
129 }
130
131 @Override
132 public int[] getRequiredTokens() {
133 return new int[] {
134 TokenTypes.SEMI,
135 TokenTypes.FOR_INIT,
136 TokenTypes.FOR_ITERATOR,
137 TokenTypes.LAMBDA,
138 };
139 }
140
141 @Override
142 public void beginTree(DetailAST rootAST) {
143 lastStatementEnd = 0;
144 lastVariableResourceStatementEnd = 0;
145 }
146
147 @Override
148 public void visitToken(DetailAST ast) {
149 switch (ast.getType()) {
150 case TokenTypes.SEMI -> checkIfSemicolonIsInDifferentLineThanPrevious(ast);
151 case TokenTypes.FOR_ITERATOR -> forStatementEnd = ast.getLineNo();
152 case TokenTypes.LAMBDA -> {
153 isInLambda = true;
154 countOfSemiInLambda.push(0);
155 }
156 default -> inForHeader = true;
157 }
158 }
159
160 @Override
161 public void leaveToken(DetailAST ast) {
162 switch (ast.getType()) {
163 case TokenTypes.SEMI -> {
164 lastStatementEnd = ast.getLineNo();
165 forStatementEnd = 0;
166 lambdaStatementEnd = 0;
167 }
168 case TokenTypes.FOR_ITERATOR -> inForHeader = false;
169 case TokenTypes.LAMBDA -> {
170 countOfSemiInLambda.pop();
171 if (countOfSemiInLambda.isEmpty()) {
172 isInLambda = false;
173 }
174 lambdaStatementEnd = ast.getLineNo();
175 }
176 default -> {
177 // do nothing
178 }
179 }
180 }
181
182 /**
183 * Checks if given semicolon is in different line than previous.
184 *
185 * @param ast semicolon to check
186 */
187 private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) {
188 DetailAST currentStatement = ast;
189 final DetailAST previousSibling = ast.getPreviousSibling();
190 final boolean isUnnecessarySemicolon = previousSibling == null
191 || previousSibling.getType() == TokenTypes.RESOURCES
192 || ast.getParent().getType() == TokenTypes.COMPILATION_UNIT;
193 if (!isUnnecessarySemicolon) {
194 currentStatement = ast.getPreviousSibling();
195 }
196 if (isInLambda) {
197 checkLambda(ast, currentStatement);
198 }
199 else if (isResource(ast.getParent())) {
200 checkResourceVariable(ast);
201 }
202 else if (!inForHeader && isOnTheSameLine(currentStatement, lastStatementEnd,
203 forStatementEnd, lambdaStatementEnd)) {
204 log(ast, MSG_KEY);
205 }
206 }
207
208 /**
209 * Checks semicolon placement in lambda.
210 *
211 * @param ast semicolon to check
212 * @param currentStatement current statement
213 */
214 private void checkLambda(DetailAST ast, DetailAST currentStatement) {
215 int countOfSemiInCurrentLambda = countOfSemiInLambda.pop();
216 countOfSemiInCurrentLambda++;
217 countOfSemiInLambda.push(countOfSemiInCurrentLambda);
218 if (!inForHeader && countOfSemiInCurrentLambda > 1
219 && isOnTheSameLine(currentStatement,
220 lastStatementEnd, forStatementEnd,
221 lambdaStatementEnd)) {
222 log(ast, MSG_KEY);
223 }
224 }
225
226 /**
227 * Checks that given node is a resource.
228 *
229 * @param ast semicolon to check
230 * @return true if node is a resource
231 */
232 private static boolean isResource(DetailAST ast) {
233 return ast.getType() == TokenTypes.RESOURCES
234 || ast.getType() == TokenTypes.RESOURCE_SPECIFICATION;
235 }
236
237 /**
238 * Checks resource variable.
239 *
240 * @param currentStatement current statement
241 */
242 private void checkResourceVariable(DetailAST currentStatement) {
243 if (treatTryResourcesAsStatement) {
244 final DetailAST nextNode = currentStatement.getNextSibling();
245 if (currentStatement.getPreviousSibling().findFirstToken(TokenTypes.ASSIGN) != null) {
246 lastVariableResourceStatementEnd = currentStatement.getLineNo();
247 }
248 if (nextNode.findFirstToken(TokenTypes.ASSIGN) != null
249 && nextNode.getLineNo() == lastVariableResourceStatementEnd) {
250 log(currentStatement, MSG_KEY);
251 }
252 }
253 }
254
255 /**
256 * Checks whether two statements are on the same line.
257 *
258 * @param ast token for the current statement.
259 * @param lastStatementEnd the line-number where the last statement ended.
260 * @param forStatementEnd the line-number where the last 'for-loop'
261 * statement ended.
262 * @param lambdaStatementEnd the line-number where the last lambda
263 * statement ended.
264 * @return true if two statements are on the same line.
265 */
266 private static boolean isOnTheSameLine(DetailAST ast, int lastStatementEnd,
267 int forStatementEnd, int lambdaStatementEnd) {
268 return lastStatementEnd == ast.getLineNo() && forStatementEnd != ast.getLineNo()
269 && lambdaStatementEnd != ast.getLineNo();
270 }
271
272 }