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