1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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   * Restricts the number of statements per line to one.
32   * <p>
33   *     Rationale: It's very difficult to read multiple statements on one line.
34   * </p>
35   * <p>
36   *     In the Java programming language, statements are the fundamental unit of
37   *     execution. All statements except blocks are terminated by a semicolon.
38   *     Blocks are denoted by open and close curly braces.
39   * </p>
40   * <p>
41   *     OneStatementPerLineCheck checks the following types of statements:
42   *     variable declaration statements, empty statements, assignment statements,
43   *     expression statements, increment statements, object creation statements,
44   *     'for loop' statements, 'break' statements, 'continue' statements,
45   *     'return' statements, import statements.
46   * </p>
47   * <p>
48   *     The following examples will be flagged as a violation:
49   * </p>
50   * <pre>
51   *     //Each line causes violation:
52   *     int var1; int var2;
53   *     var1 = 1; var2 = 2;
54   *     int var1 = 1; int var2 = 2;
55   *     var1++; var2++;
56   *     Object obj1 = new Object(); Object obj2 = new Object();
57   *     import java.io.EOFException; import java.io.BufferedReader;
58   *     ;; //two empty statements on the same line.
59   *
60   *     //Multi-line statements:
61   *     int var1 = 1
62   *     ; var2 = 2; //violation here
63   *     int o = 1, p = 2,
64   *     r = 5; int t; //violation here
65   * </pre>
66   *
67   */
68  @FileStatefulCheck
69  public final class OneStatementPerLineCheck extends AbstractCheck {
70  
71      /**
72       * A key is pointing to the warning message text in "messages.properties"
73       * file.
74       */
75      public static final String MSG_KEY = "multiple.statements.line";
76  
77      /**
78       * Counts number of semicolons in nested lambdas.
79       */
80      private final Deque<Integer> countOfSemiInLambda = new ArrayDeque<>();
81  
82      /**
83       * Hold the line-number where the last statement ended.
84       */
85      private int lastStatementEnd = -1;
86  
87      /**
88       * Hold the line-number where the last 'for-loop' statement ended.
89       */
90      private int forStatementEnd = -1;
91  
92      /**
93       * The for-header usually has 3 statements on one line, but THIS IS OK.
94       */
95      private boolean inForHeader;
96  
97      /**
98       * Holds if current token is inside lambda.
99       */
100     private boolean isInLambda;
101 
102     /**
103      * Hold the line-number where the last lambda statement ended.
104      */
105     private int lambdaStatementEnd = -1;
106 
107     @Override
108     public int[] getDefaultTokens() {
109         return getRequiredTokens();
110     }
111 
112     @Override
113     public int[] getAcceptableTokens() {
114         return getRequiredTokens();
115     }
116 
117     @Override
118     public int[] getRequiredTokens() {
119         return new int[] {
120             TokenTypes.SEMI,
121             TokenTypes.FOR_INIT,
122             TokenTypes.FOR_ITERATOR,
123             TokenTypes.LAMBDA,
124         };
125     }
126 
127     @Override
128     public void beginTree(DetailAST rootAST) {
129         inForHeader = false;
130         lastStatementEnd = -1;
131         forStatementEnd = -1;
132         isInLambda = false;
133     }
134 
135     @Override
136     public void visitToken(DetailAST ast) {
137         switch (ast.getType()) {
138             case TokenTypes.SEMI:
139                 checkIfSemicolonIsInDifferentLineThanPrevious(ast);
140                 break;
141             case TokenTypes.FOR_ITERATOR:
142                 forStatementEnd = ast.getLineNo();
143                 break;
144             case TokenTypes.LAMBDA:
145                 isInLambda = true;
146                 countOfSemiInLambda.push(0);
147                 break;
148             default:
149                 inForHeader = true;
150                 break;
151         }
152     }
153 
154     @Override
155     public void leaveToken(DetailAST ast) {
156         switch (ast.getType()) {
157             case TokenTypes.SEMI:
158                 lastStatementEnd = ast.getLineNo();
159                 forStatementEnd = -1;
160                 lambdaStatementEnd = -1;
161                 break;
162             case TokenTypes.FOR_ITERATOR:
163                 inForHeader = false;
164                 break;
165             case TokenTypes.LAMBDA:
166                 countOfSemiInLambda.pop();
167                 if (countOfSemiInLambda.isEmpty()) {
168                     isInLambda = false;
169                 }
170                 lambdaStatementEnd = ast.getLineNo();
171                 break;
172             default:
173                 break;
174         }
175     }
176 
177     /**
178      * Checks if given semicolon is in different line than previous.
179      * @param ast semicolon to check
180      */
181     private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) {
182         DetailAST currentStatement = ast;
183         final boolean hasResourcesPrevSibling =
184                 currentStatement.getPreviousSibling() != null
185                         && currentStatement.getPreviousSibling().getType() == TokenTypes.RESOURCES;
186         if (!hasResourcesPrevSibling && isMultilineStatement(currentStatement)) {
187             currentStatement = ast.getPreviousSibling();
188         }
189         if (isInLambda) {
190             int countOfSemiInCurrentLambda = countOfSemiInLambda.pop();
191             countOfSemiInCurrentLambda++;
192             countOfSemiInLambda.push(countOfSemiInCurrentLambda);
193             if (!inForHeader && countOfSemiInCurrentLambda > 1
194                     && isOnTheSameLine(currentStatement,
195                     lastStatementEnd, forStatementEnd,
196                     lambdaStatementEnd)) {
197                 log(ast, MSG_KEY);
198             }
199         }
200         else if (!inForHeader && isOnTheSameLine(currentStatement, lastStatementEnd,
201                 forStatementEnd, lambdaStatementEnd)) {
202             log(ast, MSG_KEY);
203         }
204     }
205 
206     /**
207      * Checks whether two statements are on the same line.
208      * @param ast token for the current statement.
209      * @param lastStatementEnd the line-number where the last statement ended.
210      * @param forStatementEnd the line-number where the last 'for-loop'
211      *                        statement ended.
212      * @param lambdaStatementEnd the line-number where the last lambda
213      *                        statement ended.
214      * @return true if two statements are on the same line.
215      */
216     private static boolean isOnTheSameLine(DetailAST ast, int lastStatementEnd,
217                                            int forStatementEnd, int lambdaStatementEnd) {
218         return lastStatementEnd == ast.getLineNo() && forStatementEnd != ast.getLineNo()
219                 && lambdaStatementEnd != ast.getLineNo();
220     }
221 
222     /**
223      * Checks whether statement is multiline.
224      * @param ast token for the current statement.
225      * @return true if one statement is distributed over two or more lines.
226      */
227     private static boolean isMultilineStatement(DetailAST ast) {
228         final boolean multiline;
229         if (ast.getPreviousSibling() == null) {
230             multiline = false;
231         }
232         else {
233             final DetailAST prevSibling = ast.getPreviousSibling();
234             multiline = prevSibling.getLineNo() != ast.getLineNo()
235                     && ast.getParent() != null;
236         }
237         return multiline;
238     }
239 
240 }