1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2025 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 * Setter to enable resources processing.
106 *
107 * @param treatTryResourcesAsStatement user's value of treatTryResourcesAsStatement.
108 * @since 8.23
109 */
110 public void setTreatTryResourcesAsStatement(boolean treatTryResourcesAsStatement) {
111 this.treatTryResourcesAsStatement = treatTryResourcesAsStatement;
112 }
113
114 @Override
115 public int[] getDefaultTokens() {
116 return getRequiredTokens();
117 }
118
119 @Override
120 public int[] getAcceptableTokens() {
121 return getRequiredTokens();
122 }
123
124 @Override
125 public int[] getRequiredTokens() {
126 return new int[] {
127 TokenTypes.SEMI,
128 TokenTypes.FOR_INIT,
129 TokenTypes.FOR_ITERATOR,
130 TokenTypes.LAMBDA,
131 };
132 }
133
134 @Override
135 public void beginTree(DetailAST rootAST) {
136 lastStatementEnd = 0;
137 lastVariableResourceStatementEnd = 0;
138 }
139
140 @Override
141 public void visitToken(DetailAST ast) {
142 switch (ast.getType()) {
143 case TokenTypes.SEMI -> checkIfSemicolonIsInDifferentLineThanPrevious(ast);
144 case TokenTypes.FOR_ITERATOR -> forStatementEnd = ast.getLineNo();
145 case TokenTypes.LAMBDA -> {
146 isInLambda = true;
147 countOfSemiInLambda.push(0);
148 }
149 default -> inForHeader = true;
150 }
151 }
152
153 @Override
154 public void leaveToken(DetailAST ast) {
155 switch (ast.getType()) {
156 case TokenTypes.SEMI -> {
157 lastStatementEnd = ast.getLineNo();
158 forStatementEnd = 0;
159 lambdaStatementEnd = 0;
160 }
161 case TokenTypes.FOR_ITERATOR -> inForHeader = false;
162 case TokenTypes.LAMBDA -> {
163 countOfSemiInLambda.pop();
164 if (countOfSemiInLambda.isEmpty()) {
165 isInLambda = false;
166 }
167 lambdaStatementEnd = ast.getLineNo();
168 }
169 default -> {
170 // do nothing
171 }
172 }
173 }
174
175 /**
176 * Checks if given semicolon is in different line than previous.
177 *
178 * @param ast semicolon to check
179 */
180 private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) {
181 DetailAST currentStatement = ast;
182 final DetailAST previousSibling = ast.getPreviousSibling();
183 final boolean isUnnecessarySemicolon = previousSibling == null
184 || previousSibling.getType() == TokenTypes.RESOURCES
185 || ast.getParent().getType() == TokenTypes.COMPILATION_UNIT;
186 if (!isUnnecessarySemicolon) {
187 currentStatement = ast.getPreviousSibling();
188 }
189 if (isInLambda) {
190 checkLambda(ast, currentStatement);
191 }
192 else if (isResource(ast.getParent())) {
193 checkResourceVariable(ast);
194 }
195 else if (!inForHeader && isOnTheSameLine(currentStatement, lastStatementEnd,
196 forStatementEnd, lambdaStatementEnd)) {
197 log(ast, MSG_KEY);
198 }
199 }
200
201 /**
202 * Checks semicolon placement in lambda.
203 *
204 * @param ast semicolon to check
205 * @param currentStatement current statement
206 */
207 private void checkLambda(DetailAST ast, DetailAST currentStatement) {
208 int countOfSemiInCurrentLambda = countOfSemiInLambda.pop();
209 countOfSemiInCurrentLambda++;
210 countOfSemiInLambda.push(countOfSemiInCurrentLambda);
211 if (!inForHeader && countOfSemiInCurrentLambda > 1
212 && isOnTheSameLine(currentStatement,
213 lastStatementEnd, forStatementEnd,
214 lambdaStatementEnd)) {
215 log(ast, MSG_KEY);
216 }
217 }
218
219 /**
220 * Checks that given node is a resource.
221 *
222 * @param ast semicolon to check
223 * @return true if node is a resource
224 */
225 private static boolean isResource(DetailAST ast) {
226 return ast.getType() == TokenTypes.RESOURCES
227 || ast.getType() == TokenTypes.RESOURCE_SPECIFICATION;
228 }
229
230 /**
231 * Checks resource variable.
232 *
233 * @param currentStatement current statement
234 */
235 private void checkResourceVariable(DetailAST currentStatement) {
236 if (treatTryResourcesAsStatement) {
237 final DetailAST nextNode = currentStatement.getNextSibling();
238 if (currentStatement.getPreviousSibling().findFirstToken(TokenTypes.ASSIGN) != null) {
239 lastVariableResourceStatementEnd = currentStatement.getLineNo();
240 }
241 if (nextNode.findFirstToken(TokenTypes.ASSIGN) != null
242 && nextNode.getLineNo() == lastVariableResourceStatementEnd) {
243 log(currentStatement, MSG_KEY);
244 }
245 }
246 }
247
248 /**
249 * Checks whether two statements are on the same line.
250 *
251 * @param ast token for the current statement.
252 * @param lastStatementEnd the line-number where the last statement ended.
253 * @param forStatementEnd the line-number where the last 'for-loop'
254 * statement ended.
255 * @param lambdaStatementEnd the line-number where the last lambda
256 * statement ended.
257 * @return true if two statements are on the same line.
258 */
259 private static boolean isOnTheSameLine(DetailAST ast, int lastStatementEnd,
260 int forStatementEnd, int lambdaStatementEnd) {
261 return lastStatementEnd == ast.getLineNo() && forStatementEnd != ast.getLineNo()
262 && lambdaStatementEnd != ast.getLineNo();
263 }
264
265 }