View Javadoc
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  import java.util.Set;
25  
26  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
27  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28  import com.puppycrawl.tools.checkstyle.api.DetailAST;
29  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
30  import com.puppycrawl.tools.checkstyle.utils.NullUtil;
31  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
32  
33  /**
34   * <div>
35   * Checks that there is only one statement per line.
36   * </div>
37   *
38   * <p>
39   * Rationale: It's very difficult to read multiple statements on one line.
40   * </p>
41   *
42   * <p>
43   * In the Java programming language, statements are the fundamental unit of
44   * execution. All statements except blocks are terminated by a semicolon.
45   * Blocks are denoted by open and close curly braces.
46   * </p>
47   *
48   * <p>
49   * OneStatementPerLineCheck checks the following types of statements:
50   * block statements, variable declaration statements, import statements,
51   * assignment statements, expression statements, increment statements,
52   * object creation statements, 'for loop' statements, 'break' statements,
53   * 'continue' statements, 'return' statements, resources statements (optional).
54   * </p>
55   *
56   * <p>
57   * Notes:
58   * Unnecessary semicolons are ignored.
59   * </p>
60   *
61   * @since 5.3
62   */
63  @FileStatefulCheck
64  public final class OneStatementPerLineCheck extends AbstractCheck {
65  
66      /**
67       * A key is pointing to the warning message text in "messages.properties"
68       * file.
69       */
70      public static final String MSG_KEY = "multiple.statements.line";
71  
72      /** Set of valid semi parent. */
73      private static final Set<Integer> VALID_SEMI_PARENT = Set.of(
74          TokenTypes.VARIABLE_DEF,
75          TokenTypes.IMPORT,
76          TokenTypes.STATIC_IMPORT,
77          TokenTypes.MODULE_IMPORT,
78          TokenTypes.LITERAL_RETURN,
79          TokenTypes.LITERAL_BREAK,
80          TokenTypes.LITERAL_CONTINUE,
81          TokenTypes.PACKAGE_DEF,
82          TokenTypes.SUPER_CTOR_CALL,
83          TokenTypes.CTOR_CALL,
84          TokenTypes.LITERAL_ASSERT,
85          TokenTypes.LITERAL_YIELD,
86          TokenTypes.METHOD_DEF,
87          TokenTypes.ANNOTATION_FIELD_DEF
88      );
89  
90      /**
91       * Stack of statement line-number for nested lambdas and anonymous classes.
92       * Used to isolate validation to the current nesting depth.
93       */
94      private final Deque<Integer> nestingScope = new ArrayDeque<>();
95  
96      /**
97       * Hold the line-number where the last statement ended.
98       */
99      private int lastStatementEnd;
100 
101     /**
102      * Hold the line-number where the last resource variable statement ended.
103      */
104     private int lastVariableResourceStatementEnd;
105 
106     /**
107      * Enable resources processing.
108      */
109     private boolean treatTryResourcesAsStatement;
110 
111     /**
112      * Hold the line number of the last statement in the current nesting scope.
113      */
114     private int lastStatementInCurrentScope;
115 
116     /**
117      * The first statement of lambda or anonymous class shouldn't be violation.
118      */
119     private boolean isFirstStatementOfLambdaOrAnonymous;
120 
121     /**
122      * Creates a new {@code OneStatementPerLineCheck} instance.
123      */
124     public OneStatementPerLineCheck() {
125         // no code by default
126     }
127 
128     /**
129      * Setter to enable resources processing.
130      *
131      * @param treatTryResourcesAsStatement user's value of treatTryResourcesAsStatement.
132      * @since 8.23
133      */
134     public void setTreatTryResourcesAsStatement(boolean treatTryResourcesAsStatement) {
135         this.treatTryResourcesAsStatement = treatTryResourcesAsStatement;
136     }
137 
138     @Override
139     public int[] getDefaultTokens() {
140         return getRequiredTokens();
141     }
142 
143     @Override
144     public int[] getAcceptableTokens() {
145         return getRequiredTokens();
146     }
147 
148     @Override
149     public int[] getRequiredTokens() {
150         return new int[] {
151             TokenTypes.SEMI,
152             TokenTypes.LAMBDA,
153             TokenTypes.OBJBLOCK,
154             TokenTypes.METHOD_CALL,
155             TokenTypes.CLASS_DEF,
156             TokenTypes.INTERFACE_DEF,
157             TokenTypes.ENUM_DEF,
158             TokenTypes.RECORD_DEF,
159             TokenTypes.ANNOTATION_DEF,
160         };
161     }
162 
163     @Override
164     public void beginTree(DetailAST rootAST) {
165         lastStatementEnd = 0;
166         lastVariableResourceStatementEnd = lastStatementEnd;
167         lastStatementInCurrentScope = lastStatementEnd;
168     }
169 
170     @Override
171     public void visitToken(DetailAST ast) {
172         switch (ast.getType()) {
173             case TokenTypes.SEMI ->
174                 checkIfSemicolonIsInDifferentLineThanPrevious(ast);
175             case TokenTypes.LAMBDA, TokenTypes.OBJBLOCK -> {
176                 if (ast.getType() == TokenTypes.LAMBDA
177                         || ast.getParent().getType() == TokenTypes.LITERAL_NEW) {
178                     nestingScope.push(lastStatementEnd);
179                     isFirstStatementOfLambdaOrAnonymous = true;
180                 }
181             }
182             case TokenTypes.METHOD_CALL -> nestingScope.push(lastStatementEnd);
183             default -> {
184                 // Expected block statements
185                 DetailAST previousNode = ast.getPreviousSibling();
186                 if (previousNode != null) {
187                     previousNode = getLastNestedLeafNode(previousNode);
188                     if (TokenUtil.areOnSameLine(ast, previousNode)) {
189                         logViolation(ast);
190                     }
191                 }
192             }
193         }
194     }
195 
196     @Override
197     public void leaveToken(DetailAST ast) {
198         switch (ast.getType()) {
199             case TokenTypes.SEMI -> {
200                 if (getStatementStart(ast).getType() != TokenTypes.SEMI) {
201                     lastStatementEnd = ast.getLineNo();
202                 }
203                 isFirstStatementOfLambdaOrAnonymous = false;
204                 lastStatementInCurrentScope = lastStatementEnd;
205             }
206             case TokenTypes.LAMBDA, TokenTypes.OBJBLOCK -> {
207                 if (ast.getType() == TokenTypes.LAMBDA
208                         || ast.getParent().getType() == TokenTypes.LITERAL_NEW) {
209                     lastStatementInCurrentScope = nestingScope.pop();
210                     isFirstStatementOfLambdaOrAnonymous = false;
211                 }
212             }
213             case TokenTypes.METHOD_CALL -> lastStatementInCurrentScope = nestingScope.pop();
214             default -> {
215                 // do nothing
216             }
217         }
218     }
219 
220     /**
221      * Checks if statement of given semicolon is in different line
222      * than previous statement.
223      *
224      * @param ast semicolon to check
225      */
226     private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) {
227         boolean validStatement = true;
228         DetailAST statementStart = getStatementStart(ast);
229         if (isResource(ast.getParent())) {
230             validStatement = checkResourceVariable(ast);
231             statementStart = NullUtil.notNull(ast.getNextSibling());
232         }
233         else if (!isFirstStatementOfLambdaOrAnonymous
234                 && statementStart.getType() != TokenTypes.SEMI) {
235             validStatement = isValidStatement(statementStart);
236         }
237         if (!validStatement) {
238             logViolation(statementStart);
239         }
240     }
241 
242     /**
243      * Logs a violation at the given AST node.
244      *
245      * @param violationNode token at which violation occurred.
246      */
247     private void logViolation(DetailAST violationNode) {
248         log(violationNode, MSG_KEY);
249     }
250 
251     /**
252      * Checks whether the current statement is placed on a separate line
253      * from the previous statement.
254      *
255      * @param statementStart token representing the start of the current statement
256      * @return {@code true} if the current statement starts on a different line
257      *         than the previous statement; {@code false} otherwise
258      */
259     private boolean isValidStatement(DetailAST statementStart) {
260         boolean blockStatementBefore = false;
261         DetailAST previousNode = statementStart.getPreviousSibling();
262         final boolean isBlockStatement = TokenUtil.isOfType(previousNode,
263                 TokenTypes.CLASS_DEF,
264                 TokenTypes.INTERFACE_DEF,
265                 TokenTypes.ENUM_DEF,
266                 TokenTypes.RECORD_DEF,
267                 TokenTypes.ANNOTATION_DEF,
268                 TokenTypes.METHOD_DEF);
269         if (isBlockStatement) {
270             previousNode = getLastNestedLeafNode(previousNode);
271             blockStatementBefore = TokenUtil.areOnSameLine(statementStart, previousNode);
272         }
273         return !blockStatementBefore && statementStart.getLineNo() != lastStatementInCurrentScope;
274     }
275 
276     /**
277      * Returns the starting node of the statement, or the previous statement's
278      * start if the current one is an empty statement.
279      *
280      * @param ast the SEMI token.
281      * @return the start of the associated statement or the previous sibling.
282      */
283     private static DetailAST getStatementStart(DetailAST ast) {
284         DetailAST statementStart = ast;
285         final DetailAST parent = ast.getParent();
286         final DetailAST previousSibling = ast.getPreviousSibling();
287         final boolean validPreviousSibling = previousSibling != null
288                 && (previousSibling.getType() == TokenTypes.VARIABLE_DEF
289                 || previousSibling.getType() == TokenTypes.EXPR
290                 || previousSibling.getType() == TokenTypes.ENUM_CONSTANT_DEF)
291                 && previousSibling.findFirstToken(TokenTypes.SEMI) == null;
292         if (VALID_SEMI_PARENT.contains(parent.getType())) {
293             statementStart = ast.getParent();
294         }
295         else if (validPreviousSibling) {
296             statementStart = getStartNodeInExpression(previousSibling);
297         }
298         return statementStart;
299     }
300 
301     /**
302      * Checks whether the current statement represents a valid resource
303      * declaration in a try-with-resources statement.
304      *
305      * @param currentStatement the statement to check
306      * @return {@code true} if the statement is a valid resource declaration;
307      *         {@code false} otherwise.
308      */
309     private boolean checkResourceVariable(DetailAST currentStatement) {
310         boolean result = true;
311         if (treatTryResourcesAsStatement) {
312             final DetailAST nextNode = currentStatement.getNextSibling();
313             if (currentStatement.getPreviousSibling().findFirstToken(TokenTypes.ASSIGN) != null) {
314                 lastVariableResourceStatementEnd = currentStatement.getLineNo();
315             }
316             result = nextNode.findFirstToken(TokenTypes.ASSIGN) == null
317                     || nextNode.getLineNo() != lastVariableResourceStatementEnd;
318         }
319         return result;
320     }
321 
322     /**
323      * Finds the leftmost leaf node in the given expression's subtree.
324      *
325      * @param expression EXPR node.
326      * @return the leftmost leaf {@code DetailAST} node
327      */
328     private static DetailAST getStartNodeInExpression(DetailAST expression) {
329         DetailAST child = expression;
330         while (child.hasChildren()) {
331             child = child.getFirstChild();
332         }
333         return child;
334     }
335 
336     /**
337      * Finds the last nested leaf node.
338      *
339      * @param startToken start token of statement.
340      * @return the last nested leaf {@code DetailAST} node
341      */
342     private static DetailAST getLastNestedLeafNode(DetailAST startToken) {
343         DetailAST current = startToken;
344         while (current.hasChildren()) {
345             current = current.getLastChild();
346         }
347         return current;
348     }
349 
350     /**
351      * Checks that given node is a resource.
352      *
353      * @param ast semicolon to check
354      * @return true if node is a resource
355      */
356     private static boolean isResource(DetailAST ast) {
357         return ast.getType() == TokenTypes.RESOURCES;
358     }
359 
360 }