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