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.api.AbstractCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailAST;
027import com.puppycrawl.tools.checkstyle.api.TokenTypes;
028
029/**
030 * Restricts the number of statements per line to one.
031 * <p>
032 *     Rationale: It's very difficult to read multiple statements on one line.
033 * </p>
034 * <p>
035 *     In the Java programming language, statements are the fundamental unit of
036 *     execution. All statements except blocks are terminated by a semicolon.
037 *     Blocks are denoted by open and close curly braces.
038 * </p>
039 * <p>
040 *     OneStatementPerLineCheck checks the following types of statements:
041 *     variable declaration statements, empty statements, assignment statements,
042 *     expression statements, increment statements, object creation statements,
043 *     'for loop' statements, 'break' statements, 'continue' statements,
044 *     'return' statements, import statements.
045 * </p>
046 * <p>
047 *     The following examples will be flagged as a violation:
048 * </p>
049 * <pre>
050 *     //Each line causes violation:
051 *     int var1; int var2;
052 *     var1 = 1; var2 = 2;
053 *     int var1 = 1; int var2 = 2;
054 *     var1++; var2++;
055 *     Object obj1 = new Object(); Object obj2 = new Object();
056 *     import java.io.EOFException; import java.io.BufferedReader;
057 *     ;; //two empty statements on the same line.
058 *
059 *     //Multi-line statements:
060 *     int var1 = 1
061 *     ; var2 = 2; //violation here
062 *     int o = 1, p = 2,
063 *     r = 5; int t; //violation here
064 * </pre>
065 *
066 * @author Alexander Jesse
067 * @author Oliver Burn
068 * @author Andrei Selkin
069 */
070public final class OneStatementPerLineCheck extends AbstractCheck {
071
072    /**
073     * A key is pointing to the warning message text in "messages.properties"
074     * file.
075     */
076    public static final String MSG_KEY = "multiple.statements.line";
077
078    /**
079     * Counts number of semicolons in nested lambdas.
080     */
081    private final Deque<Integer> countOfSemiInLambda = new ArrayDeque<>();
082
083    /**
084     * Hold the line-number where the last statement ended.
085     */
086    private int lastStatementEnd = -1;
087
088    /**
089     * Hold the line-number where the last 'for-loop' statement ended.
090     */
091    private int forStatementEnd = -1;
092
093    /**
094     * The for-header usually has 3 statements on one line, but THIS IS OK.
095     */
096    private boolean inForHeader;
097
098    /**
099     * Holds if current token is inside lambda.
100     */
101    private boolean isInLambda;
102
103    /**
104     * Hold the line-number where the last lambda statement ended.
105     */
106    private int lambdaStatementEnd = -1;
107
108    @Override
109    public int[] getDefaultTokens() {
110        return getAcceptableTokens();
111    }
112
113    @Override
114    public int[] getAcceptableTokens() {
115        return new int[] {
116            TokenTypes.SEMI,
117            TokenTypes.FOR_INIT,
118            TokenTypes.FOR_ITERATOR,
119            TokenTypes.LAMBDA,
120        };
121    }
122
123    @Override
124    public int[] getRequiredTokens() {
125        return getAcceptableTokens();
126    }
127
128    @Override
129    public void beginTree(DetailAST rootAST) {
130        inForHeader = false;
131        lastStatementEnd = -1;
132        forStatementEnd = -1;
133        isInLambda = false;
134    }
135
136    @Override
137    public void visitToken(DetailAST ast) {
138        switch (ast.getType()) {
139            case TokenTypes.SEMI:
140                checkIfSemicolonIsInDifferentLineThanPrevious(ast);
141                break;
142            case TokenTypes.FOR_ITERATOR:
143                forStatementEnd = ast.getLineNo();
144                break;
145            case TokenTypes.LAMBDA:
146                isInLambda = true;
147                countOfSemiInLambda.push(0);
148                break;
149            default:
150                inForHeader = true;
151                break;
152        }
153    }
154
155    @Override
156    public void leaveToken(DetailAST ast) {
157        switch (ast.getType()) {
158            case TokenTypes.SEMI:
159                lastStatementEnd = ast.getLineNo();
160                forStatementEnd = -1;
161                lambdaStatementEnd = -1;
162                break;
163            case TokenTypes.FOR_ITERATOR:
164                inForHeader = false;
165                break;
166            case TokenTypes.LAMBDA:
167                countOfSemiInLambda.pop();
168                if (countOfSemiInLambda.isEmpty()) {
169                    isInLambda = false;
170                }
171                lambdaStatementEnd = ast.getLineNo();
172                break;
173            default:
174                break;
175        }
176    }
177
178    /**
179     * Checks if given semicolon is in different line than previous.
180     * @param ast semicolon to check
181     */
182    private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) {
183        DetailAST currentStatement = ast;
184        final boolean hasResourcesPrevSibling =
185                currentStatement.getPreviousSibling() != null
186                        && currentStatement.getPreviousSibling().getType() == TokenTypes.RESOURCES;
187        if (!hasResourcesPrevSibling && isMultilineStatement(currentStatement)) {
188            currentStatement = ast.getPreviousSibling();
189        }
190        if (isInLambda) {
191            int countOfSemiInCurrentLambda = countOfSemiInLambda.pop();
192            countOfSemiInCurrentLambda++;
193            countOfSemiInLambda.push(countOfSemiInCurrentLambda);
194            if (!inForHeader && countOfSemiInCurrentLambda > 1
195                    && isOnTheSameLine(currentStatement,
196                    lastStatementEnd, forStatementEnd,
197                    lambdaStatementEnd)) {
198                log(ast, MSG_KEY);
199            }
200        }
201        else if (!inForHeader && isOnTheSameLine(currentStatement, lastStatementEnd,
202                forStatementEnd, lambdaStatementEnd)) {
203            log(ast, MSG_KEY);
204        }
205    }
206
207    /**
208     * Checks whether two statements are on the same line.
209     * @param ast token for the current statement.
210     * @param lastStatementEnd the line-number where the last statement ended.
211     * @param forStatementEnd the line-number where the last 'for-loop'
212     *                        statement ended.
213     * @param lambdaStatementEnd the line-number where the last lambda
214     *                        statement ended.
215     * @return true if two statements are on the same line.
216     */
217    private static boolean isOnTheSameLine(DetailAST ast, int lastStatementEnd,
218                                           int forStatementEnd, int lambdaStatementEnd) {
219        return lastStatementEnd == ast.getLineNo() && forStatementEnd != ast.getLineNo()
220                && lambdaStatementEnd != ast.getLineNo();
221    }
222
223    /**
224     * Checks whether statement is multiline.
225     * @param ast token for the current statement.
226     * @return true if one statement is distributed over two or more lines.
227     */
228    private static boolean isMultilineStatement(DetailAST ast) {
229        final boolean multiline;
230        if (ast.getPreviousSibling() == null) {
231            multiline = false;
232        }
233        else {
234            final DetailAST prevSibling = ast.getPreviousSibling();
235            multiline = prevSibling.getLineNo() != ast.getLineNo()
236                    && ast.getParent() != null;
237        }
238        return multiline;
239    }
240}