001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.ArrayDeque;
023import java.util.Deque;
024
025import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.TokenTypes;
029
030/**
031 * Restricts the number of statements per line to one.
032 * <p>
033 *     Rationale: It's very difficult to read multiple statements on one line.
034 * </p>
035 * <p>
036 *     In the Java programming language, statements are the fundamental unit of
037 *     execution. All statements except blocks are terminated by a semicolon.
038 *     Blocks are denoted by open and close curly braces.
039 * </p>
040 * <p>
041 *     OneStatementPerLineCheck checks the following types of statements:
042 *     variable declaration statements, empty statements, assignment statements,
043 *     expression statements, increment statements, object creation statements,
044 *     'for loop' statements, 'break' statements, 'continue' statements,
045 *     'return' statements, import statements.
046 * </p>
047 * <p>
048 *     The following examples will be flagged as a violation:
049 * </p>
050 * <pre>
051 *     //Each line causes violation:
052 *     int var1; int var2;
053 *     var1 = 1; var2 = 2;
054 *     int var1 = 1; int var2 = 2;
055 *     var1++; var2++;
056 *     Object obj1 = new Object(); Object obj2 = new Object();
057 *     import java.io.EOFException; import java.io.BufferedReader;
058 *     ;; //two empty statements on the same line.
059 *
060 *     //Multi-line statements:
061 *     int var1 = 1
062 *     ; var2 = 2; //violation here
063 *     int o = 1, p = 2,
064 *     r = 5; int t; //violation here
065 * </pre>
066 *
067 * @author Alexander Jesse
068 * @author Oliver Burn
069 * @author Andrei Selkin
070 */
071@FileStatefulCheck
072public final class OneStatementPerLineCheck extends AbstractCheck {
073
074    /**
075     * A key is pointing to the warning message text in "messages.properties"
076     * file.
077     */
078    public static final String MSG_KEY = "multiple.statements.line";
079
080    /**
081     * Counts number of semicolons in nested lambdas.
082     */
083    private final Deque<Integer> countOfSemiInLambda = new ArrayDeque<>();
084
085    /**
086     * Hold the line-number where the last statement ended.
087     */
088    private int lastStatementEnd = -1;
089
090    /**
091     * Hold the line-number where the last 'for-loop' statement ended.
092     */
093    private int forStatementEnd = -1;
094
095    /**
096     * The for-header usually has 3 statements on one line, but THIS IS OK.
097     */
098    private boolean inForHeader;
099
100    /**
101     * Holds if current token is inside lambda.
102     */
103    private boolean isInLambda;
104
105    /**
106     * Hold the line-number where the last lambda statement ended.
107     */
108    private int lambdaStatementEnd = -1;
109
110    @Override
111    public int[] getDefaultTokens() {
112        return getRequiredTokens();
113    }
114
115    @Override
116    public int[] getAcceptableTokens() {
117        return getRequiredTokens();
118    }
119
120    @Override
121    public int[] getRequiredTokens() {
122        return new int[] {
123            TokenTypes.SEMI,
124            TokenTypes.FOR_INIT,
125            TokenTypes.FOR_ITERATOR,
126            TokenTypes.LAMBDA,
127        };
128    }
129
130    @Override
131    public void beginTree(DetailAST rootAST) {
132        inForHeader = false;
133        lastStatementEnd = -1;
134        forStatementEnd = -1;
135        isInLambda = false;
136    }
137
138    @Override
139    public void visitToken(DetailAST ast) {
140        switch (ast.getType()) {
141            case TokenTypes.SEMI:
142                checkIfSemicolonIsInDifferentLineThanPrevious(ast);
143                break;
144            case TokenTypes.FOR_ITERATOR:
145                forStatementEnd = ast.getLineNo();
146                break;
147            case TokenTypes.LAMBDA:
148                isInLambda = true;
149                countOfSemiInLambda.push(0);
150                break;
151            default:
152                inForHeader = true;
153                break;
154        }
155    }
156
157    @Override
158    public void leaveToken(DetailAST ast) {
159        switch (ast.getType()) {
160            case TokenTypes.SEMI:
161                lastStatementEnd = ast.getLineNo();
162                forStatementEnd = -1;
163                lambdaStatementEnd = -1;
164                break;
165            case TokenTypes.FOR_ITERATOR:
166                inForHeader = false;
167                break;
168            case TokenTypes.LAMBDA:
169                countOfSemiInLambda.pop();
170                if (countOfSemiInLambda.isEmpty()) {
171                    isInLambda = false;
172                }
173                lambdaStatementEnd = ast.getLineNo();
174                break;
175            default:
176                break;
177        }
178    }
179
180    /**
181     * Checks if given semicolon is in different line than previous.
182     * @param ast semicolon to check
183     */
184    private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) {
185        DetailAST currentStatement = ast;
186        final boolean hasResourcesPrevSibling =
187                currentStatement.getPreviousSibling() != null
188                        && currentStatement.getPreviousSibling().getType() == TokenTypes.RESOURCES;
189        if (!hasResourcesPrevSibling && isMultilineStatement(currentStatement)) {
190            currentStatement = ast.getPreviousSibling();
191        }
192        if (isInLambda) {
193            int countOfSemiInCurrentLambda = countOfSemiInLambda.pop();
194            countOfSemiInCurrentLambda++;
195            countOfSemiInLambda.push(countOfSemiInCurrentLambda);
196            if (!inForHeader && countOfSemiInCurrentLambda > 1
197                    && isOnTheSameLine(currentStatement,
198                    lastStatementEnd, forStatementEnd,
199                    lambdaStatementEnd)) {
200                log(ast, MSG_KEY);
201            }
202        }
203        else if (!inForHeader && isOnTheSameLine(currentStatement, lastStatementEnd,
204                forStatementEnd, lambdaStatementEnd)) {
205            log(ast, MSG_KEY);
206        }
207    }
208
209    /**
210     * Checks whether two statements are on the same line.
211     * @param ast token for the current statement.
212     * @param lastStatementEnd the line-number where the last statement ended.
213     * @param forStatementEnd the line-number where the last 'for-loop'
214     *                        statement ended.
215     * @param lambdaStatementEnd the line-number where the last lambda
216     *                        statement ended.
217     * @return true if two statements are on the same line.
218     */
219    private static boolean isOnTheSameLine(DetailAST ast, int lastStatementEnd,
220                                           int forStatementEnd, int lambdaStatementEnd) {
221        return lastStatementEnd == ast.getLineNo() && forStatementEnd != ast.getLineNo()
222                && lambdaStatementEnd != ast.getLineNo();
223    }
224
225    /**
226     * Checks whether statement is multiline.
227     * @param ast token for the current statement.
228     * @return true if one statement is distributed over two or more lines.
229     */
230    private static boolean isMultilineStatement(DetailAST ast) {
231        final boolean multiline;
232        if (ast.getPreviousSibling() == null) {
233            multiline = false;
234        }
235        else {
236            final DetailAST prevSibling = ast.getPreviousSibling();
237            multiline = prevSibling.getLineNo() != ast.getLineNo()
238                    && ast.getParent() != null;
239        }
240        return multiline;
241    }
242}