001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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 */
068@FileStatefulCheck
069public final class OneStatementPerLineCheck extends AbstractCheck {
070
071    /**
072     * A key is pointing to the warning message text in "messages.properties"
073     * file.
074     */
075    public static final String MSG_KEY = "multiple.statements.line";
076
077    /**
078     * Counts number of semicolons in nested lambdas.
079     */
080    private final Deque<Integer> countOfSemiInLambda = new ArrayDeque<>();
081
082    /**
083     * Hold the line-number where the last statement ended.
084     */
085    private int lastStatementEnd = -1;
086
087    /**
088     * Hold the line-number where the last 'for-loop' statement ended.
089     */
090    private int forStatementEnd = -1;
091
092    /**
093     * The for-header usually has 3 statements on one line, but THIS IS OK.
094     */
095    private boolean inForHeader;
096
097    /**
098     * Holds if current token is inside lambda.
099     */
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}