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   * <ul>
53   * <li>
54   * Property {@code treatTryResourcesAsStatement} - Enable resources processing.
55   * Type is {@code boolean}.
56   * Default value is {@code false}.
57   * </li>
58   * </ul>
59   *
60   * <p>
61   * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
62   * </p>
63   *
64   * <p>
65   * Violation Message Keys:
66   * </p>
67   * <ul>
68   * <li>
69   * {@code multiple.statements.line}
70   * </li>
71   * </ul>
72   *
73   * @since 5.3
74   */
75  @FileStatefulCheck
76  public final class OneStatementPerLineCheck extends AbstractCheck {
77  
78      /**
79       * A key is pointing to the warning message text in "messages.properties"
80       * file.
81       */
82      public static final String MSG_KEY = "multiple.statements.line";
83  
84      /**
85       * Counts number of semicolons in nested lambdas.
86       */
87      private final Deque<Integer> countOfSemiInLambda = new ArrayDeque<>();
88  
89      /**
90       * Hold the line-number where the last statement ended.
91       */
92      private int lastStatementEnd;
93  
94      /**
95       * Hold the line-number where the last 'for-loop' statement ended.
96       */
97      private int forStatementEnd;
98  
99      /**
100      * The for-header usually has 3 statements on one line, but THIS IS OK.
101      */
102     private boolean inForHeader;
103 
104     /**
105      * Holds if current token is inside lambda.
106      */
107     private boolean isInLambda;
108 
109     /**
110      * Hold the line-number where the last lambda statement ended.
111      */
112     private int lambdaStatementEnd;
113 
114     /**
115      * Hold the line-number where the last resource variable statement ended.
116      */
117     private int lastVariableResourceStatementEnd;
118 
119     /**
120      * Enable resources processing.
121      */
122     private boolean treatTryResourcesAsStatement;
123 
124     /**
125      * Setter to enable resources processing.
126      *
127      * @param treatTryResourcesAsStatement user's value of treatTryResourcesAsStatement.
128      * @since 8.23
129      */
130     public void setTreatTryResourcesAsStatement(boolean treatTryResourcesAsStatement) {
131         this.treatTryResourcesAsStatement = treatTryResourcesAsStatement;
132     }
133 
134     @Override
135     public int[] getDefaultTokens() {
136         return getRequiredTokens();
137     }
138 
139     @Override
140     public int[] getAcceptableTokens() {
141         return getRequiredTokens();
142     }
143 
144     @Override
145     public int[] getRequiredTokens() {
146         return new int[] {
147             TokenTypes.SEMI,
148             TokenTypes.FOR_INIT,
149             TokenTypes.FOR_ITERATOR,
150             TokenTypes.LAMBDA,
151         };
152     }
153 
154     @Override
155     public void beginTree(DetailAST rootAST) {
156         lastStatementEnd = 0;
157         lastVariableResourceStatementEnd = 0;
158     }
159 
160     @Override
161     public void visitToken(DetailAST ast) {
162         switch (ast.getType()) {
163             case TokenTypes.SEMI:
164                 checkIfSemicolonIsInDifferentLineThanPrevious(ast);
165                 break;
166             case TokenTypes.FOR_ITERATOR:
167                 forStatementEnd = ast.getLineNo();
168                 break;
169             case TokenTypes.LAMBDA:
170                 isInLambda = true;
171                 countOfSemiInLambda.push(0);
172                 break;
173             default:
174                 inForHeader = true;
175                 break;
176         }
177     }
178 
179     @Override
180     public void leaveToken(DetailAST ast) {
181         switch (ast.getType()) {
182             case TokenTypes.SEMI:
183                 lastStatementEnd = ast.getLineNo();
184                 forStatementEnd = 0;
185                 lambdaStatementEnd = 0;
186                 break;
187             case TokenTypes.FOR_ITERATOR:
188                 inForHeader = false;
189                 break;
190             case TokenTypes.LAMBDA:
191                 countOfSemiInLambda.pop();
192                 if (countOfSemiInLambda.isEmpty()) {
193                     isInLambda = false;
194                 }
195                 lambdaStatementEnd = ast.getLineNo();
196                 break;
197             default:
198                 break;
199         }
200     }
201 
202     /**
203      * Checks if given semicolon is in different line than previous.
204      *
205      * @param ast semicolon to check
206      */
207     private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) {
208         DetailAST currentStatement = ast;
209         final DetailAST previousSibling = ast.getPreviousSibling();
210         final boolean isUnnecessarySemicolon = previousSibling == null
211             || previousSibling.getType() == TokenTypes.RESOURCES
212             || ast.getParent().getType() == TokenTypes.COMPILATION_UNIT;
213         if (!isUnnecessarySemicolon) {
214             currentStatement = ast.getPreviousSibling();
215         }
216         if (isInLambda) {
217             checkLambda(ast, currentStatement);
218         }
219         else if (isResource(ast.getParent())) {
220             checkResourceVariable(ast);
221         }
222         else if (!inForHeader && isOnTheSameLine(currentStatement, lastStatementEnd,
223                 forStatementEnd, lambdaStatementEnd)) {
224             log(ast, MSG_KEY);
225         }
226     }
227 
228     /**
229      * Checks semicolon placement in lambda.
230      *
231      * @param ast semicolon to check
232      * @param currentStatement current statement
233      */
234     private void checkLambda(DetailAST ast, DetailAST currentStatement) {
235         int countOfSemiInCurrentLambda = countOfSemiInLambda.pop();
236         countOfSemiInCurrentLambda++;
237         countOfSemiInLambda.push(countOfSemiInCurrentLambda);
238         if (!inForHeader && countOfSemiInCurrentLambda > 1
239                 && isOnTheSameLine(currentStatement,
240                 lastStatementEnd, forStatementEnd,
241                 lambdaStatementEnd)) {
242             log(ast, MSG_KEY);
243         }
244     }
245 
246     /**
247      * Checks that given node is a resource.
248      *
249      * @param ast semicolon to check
250      * @return true if node is a resource
251      */
252     private static boolean isResource(DetailAST ast) {
253         return ast.getType() == TokenTypes.RESOURCES
254                  || ast.getType() == TokenTypes.RESOURCE_SPECIFICATION;
255     }
256 
257     /**
258      * Checks resource variable.
259      *
260      * @param currentStatement current statement
261      */
262     private void checkResourceVariable(DetailAST currentStatement) {
263         if (treatTryResourcesAsStatement) {
264             final DetailAST nextNode = currentStatement.getNextSibling();
265             if (currentStatement.getPreviousSibling().findFirstToken(TokenTypes.ASSIGN) != null) {
266                 lastVariableResourceStatementEnd = currentStatement.getLineNo();
267             }
268             if (nextNode.findFirstToken(TokenTypes.ASSIGN) != null
269                 && nextNode.getLineNo() == lastVariableResourceStatementEnd) {
270                 log(currentStatement, MSG_KEY);
271             }
272         }
273     }
274 
275     /**
276      * Checks whether two statements are on the same line.
277      *
278      * @param ast token for the current statement.
279      * @param lastStatementEnd the line-number where the last statement ended.
280      * @param forStatementEnd the line-number where the last 'for-loop'
281      *                        statement ended.
282      * @param lambdaStatementEnd the line-number where the last lambda
283      *                        statement ended.
284      * @return true if two statements are on the same line.
285      */
286     private static boolean isOnTheSameLine(DetailAST ast, int lastStatementEnd,
287                                            int forStatementEnd, int lambdaStatementEnd) {
288         return lastStatementEnd == ast.getLineNo() && forStatementEnd != ast.getLineNo()
289                 && lambdaStatementEnd != ast.getLineNo();
290     }
291 
292 }