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  
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 }