001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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;
024import java.util.Set;
025
026import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.TokenTypes;
030import com.puppycrawl.tools.checkstyle.utils.NullUtil;
031import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
032
033/**
034 * <div>
035 * Checks that there is only one statement per line.
036 * </div>
037 *
038 * <p>
039 * Rationale: It's very difficult to read multiple statements on one line.
040 * </p>
041 *
042 * <p>
043 * In the Java programming language, statements are the fundamental unit of
044 * execution. All statements except blocks are terminated by a semicolon.
045 * Blocks are denoted by open and close curly braces.
046 * </p>
047 *
048 * <p>
049 * OneStatementPerLineCheck checks the following types of statements:
050 * block statements, variable declaration statements, import statements,
051 * assignment statements, expression statements, increment statements,
052 * object creation statements, 'for loop' statements, 'break' statements,
053 * 'continue' statements, 'return' statements, resources statements (optional).
054 * </p>
055 *
056 * <p>
057 * Notes:
058 * Unnecessary semicolons are ignored.
059 * </p>
060 *
061 * @since 5.3
062 */
063@FileStatefulCheck
064public final class OneStatementPerLineCheck extends AbstractCheck {
065
066    /**
067     * A key is pointing to the warning message text in "messages.properties"
068     * file.
069     */
070    public static final String MSG_KEY = "multiple.statements.line";
071
072    /** Set of valid semi parent. */
073    private static final Set<Integer> VALID_SEMI_PARENT = Set.of(
074        TokenTypes.VARIABLE_DEF,
075        TokenTypes.IMPORT,
076        TokenTypes.STATIC_IMPORT,
077        TokenTypes.MODULE_IMPORT,
078        TokenTypes.LITERAL_RETURN,
079        TokenTypes.LITERAL_BREAK,
080        TokenTypes.LITERAL_CONTINUE,
081        TokenTypes.PACKAGE_DEF,
082        TokenTypes.SUPER_CTOR_CALL,
083        TokenTypes.CTOR_CALL,
084        TokenTypes.LITERAL_ASSERT,
085        TokenTypes.LITERAL_YIELD,
086        TokenTypes.METHOD_DEF,
087        TokenTypes.ANNOTATION_FIELD_DEF
088    );
089
090    /**
091     * Stack of statement line-number for nested lambdas and anonymous classes.
092     * Used to isolate validation to the current nesting depth.
093     */
094    private final Deque<Integer> nestingScope = new ArrayDeque<>();
095
096    /**
097     * Hold the line-number where the last statement ended.
098     */
099    private int lastStatementEnd;
100
101    /**
102     * Hold the line-number where the last resource variable statement ended.
103     */
104    private int lastVariableResourceStatementEnd;
105
106    /**
107     * Enable resources processing.
108     */
109    private boolean treatTryResourcesAsStatement;
110
111    /**
112     * Hold the line number of the last statement in the current nesting scope.
113     */
114    private int lastStatementInCurrentScope;
115
116    /**
117     * The first statement of lambda or anonymous class shouldn't be violation.
118     */
119    private boolean isFirstStatementOfLambdaOrAnonymous;
120
121    /**
122     * Creates a new {@code OneStatementPerLineCheck} instance.
123     */
124    public OneStatementPerLineCheck() {
125        // no code by default
126    }
127
128    /**
129     * Setter to enable resources processing.
130     *
131     * @param treatTryResourcesAsStatement user's value of treatTryResourcesAsStatement.
132     * @since 8.23
133     */
134    public void setTreatTryResourcesAsStatement(boolean treatTryResourcesAsStatement) {
135        this.treatTryResourcesAsStatement = treatTryResourcesAsStatement;
136    }
137
138    @Override
139    public int[] getDefaultTokens() {
140        return getRequiredTokens();
141    }
142
143    @Override
144    public int[] getAcceptableTokens() {
145        return getRequiredTokens();
146    }
147
148    @Override
149    public int[] getRequiredTokens() {
150        return new int[] {
151            TokenTypes.SEMI,
152            TokenTypes.LAMBDA,
153            TokenTypes.OBJBLOCK,
154            TokenTypes.METHOD_CALL,
155            TokenTypes.CLASS_DEF,
156            TokenTypes.INTERFACE_DEF,
157            TokenTypes.ENUM_DEF,
158            TokenTypes.RECORD_DEF,
159            TokenTypes.ANNOTATION_DEF,
160        };
161    }
162
163    @Override
164    public void beginTree(DetailAST rootAST) {
165        lastStatementEnd = 0;
166        lastVariableResourceStatementEnd = lastStatementEnd;
167        lastStatementInCurrentScope = lastStatementEnd;
168    }
169
170    @Override
171    public void visitToken(DetailAST ast) {
172        switch (ast.getType()) {
173            case TokenTypes.SEMI ->
174                checkIfSemicolonIsInDifferentLineThanPrevious(ast);
175            case TokenTypes.LAMBDA, TokenTypes.OBJBLOCK -> {
176                if (ast.getType() == TokenTypes.LAMBDA
177                        || ast.getParent().getType() == TokenTypes.LITERAL_NEW) {
178                    nestingScope.push(lastStatementEnd);
179                    isFirstStatementOfLambdaOrAnonymous = true;
180                }
181            }
182            case TokenTypes.METHOD_CALL -> nestingScope.push(lastStatementEnd);
183            default -> {
184                // Expected block statements
185                DetailAST previousNode = ast.getPreviousSibling();
186                if (previousNode != null) {
187                    previousNode = getLastNestedLeafNode(previousNode);
188                    if (TokenUtil.areOnSameLine(ast, previousNode)) {
189                        logViolation(ast);
190                    }
191                }
192            }
193        }
194    }
195
196    @Override
197    public void leaveToken(DetailAST ast) {
198        switch (ast.getType()) {
199            case TokenTypes.SEMI -> {
200                if (getStatementStart(ast).getType() != TokenTypes.SEMI) {
201                    lastStatementEnd = ast.getLineNo();
202                }
203                isFirstStatementOfLambdaOrAnonymous = false;
204                lastStatementInCurrentScope = lastStatementEnd;
205            }
206            case TokenTypes.LAMBDA, TokenTypes.OBJBLOCK -> {
207                if (ast.getType() == TokenTypes.LAMBDA
208                        || ast.getParent().getType() == TokenTypes.LITERAL_NEW) {
209                    lastStatementInCurrentScope = nestingScope.pop();
210                    isFirstStatementOfLambdaOrAnonymous = false;
211                }
212            }
213            case TokenTypes.METHOD_CALL -> lastStatementInCurrentScope = nestingScope.pop();
214            default -> {
215                // do nothing
216            }
217        }
218    }
219
220    /**
221     * Checks if statement of given semicolon is in different line
222     * than previous statement.
223     *
224     * @param ast semicolon to check
225     */
226    private void checkIfSemicolonIsInDifferentLineThanPrevious(DetailAST ast) {
227        boolean validStatement = true;
228        DetailAST statementStart = getStatementStart(ast);
229        if (isResource(ast.getParent())) {
230            validStatement = checkResourceVariable(ast);
231            statementStart = NullUtil.notNull(ast.getNextSibling());
232        }
233        else if (!isFirstStatementOfLambdaOrAnonymous
234                && statementStart.getType() != TokenTypes.SEMI) {
235            validStatement = isValidStatement(statementStart);
236        }
237        if (!validStatement) {
238            logViolation(statementStart);
239        }
240    }
241
242    /**
243     * Logs a violation at the given AST node.
244     *
245     * @param violationNode token at which violation occurred.
246     */
247    private void logViolation(DetailAST violationNode) {
248        log(violationNode, MSG_KEY);
249    }
250
251    /**
252     * Checks whether the current statement is placed on a separate line
253     * from the previous statement.
254     *
255     * @param statementStart token representing the start of the current statement
256     * @return {@code true} if the current statement starts on a different line
257     *         than the previous statement; {@code false} otherwise
258     */
259    private boolean isValidStatement(DetailAST statementStart) {
260        boolean blockStatementBefore = false;
261        DetailAST previousNode = statementStart.getPreviousSibling();
262        final boolean isBlockStatement = TokenUtil.isOfType(previousNode,
263                TokenTypes.CLASS_DEF,
264                TokenTypes.INTERFACE_DEF,
265                TokenTypes.ENUM_DEF,
266                TokenTypes.RECORD_DEF,
267                TokenTypes.ANNOTATION_DEF,
268                TokenTypes.METHOD_DEF);
269        if (isBlockStatement) {
270            previousNode = getLastNestedLeafNode(previousNode);
271            blockStatementBefore = TokenUtil.areOnSameLine(statementStart, previousNode);
272        }
273        return !blockStatementBefore && statementStart.getLineNo() != lastStatementInCurrentScope;
274    }
275
276    /**
277     * Returns the starting node of the statement, or the previous statement's
278     * start if the current one is an empty statement.
279     *
280     * @param ast the SEMI token.
281     * @return the start of the associated statement or the previous sibling.
282     */
283    private static DetailAST getStatementStart(DetailAST ast) {
284        DetailAST statementStart = ast;
285        final DetailAST parent = ast.getParent();
286        final DetailAST previousSibling = ast.getPreviousSibling();
287        final boolean validPreviousSibling = previousSibling != null
288                && (previousSibling.getType() == TokenTypes.VARIABLE_DEF
289                || previousSibling.getType() == TokenTypes.EXPR
290                || previousSibling.getType() == TokenTypes.ENUM_CONSTANT_DEF)
291                && previousSibling.findFirstToken(TokenTypes.SEMI) == null;
292        if (VALID_SEMI_PARENT.contains(parent.getType())) {
293            statementStart = ast.getParent();
294        }
295        else if (validPreviousSibling) {
296            statementStart = getStartNodeInExpression(previousSibling);
297        }
298        return statementStart;
299    }
300
301    /**
302     * Checks whether the current statement represents a valid resource
303     * declaration in a try-with-resources statement.
304     *
305     * @param currentStatement the statement to check
306     * @return {@code true} if the statement is a valid resource declaration;
307     *         {@code false} otherwise.
308     */
309    private boolean checkResourceVariable(DetailAST currentStatement) {
310        boolean result = true;
311        if (treatTryResourcesAsStatement) {
312            final DetailAST nextNode = currentStatement.getNextSibling();
313            if (currentStatement.getPreviousSibling().findFirstToken(TokenTypes.ASSIGN) != null) {
314                lastVariableResourceStatementEnd = currentStatement.getLineNo();
315            }
316            result = nextNode.findFirstToken(TokenTypes.ASSIGN) == null
317                    || nextNode.getLineNo() != lastVariableResourceStatementEnd;
318        }
319        return result;
320    }
321
322    /**
323     * Finds the leftmost leaf node in the given expression's subtree.
324     *
325     * @param expression EXPR node.
326     * @return the leftmost leaf {@code DetailAST} node
327     */
328    private static DetailAST getStartNodeInExpression(DetailAST expression) {
329        DetailAST child = expression;
330        while (child.hasChildren()) {
331            child = child.getFirstChild();
332        }
333        return child;
334    }
335
336    /**
337     * Finds the last nested leaf node.
338     *
339     * @param startToken start token of statement.
340     * @return the last nested leaf {@code DetailAST} node
341     */
342    private static DetailAST getLastNestedLeafNode(DetailAST startToken) {
343        DetailAST current = startToken;
344        while (current.hasChildren()) {
345            current = current.getLastChild();
346        }
347        return current;
348    }
349
350    /**
351     * Checks that given node is a resource.
352     *
353     * @param ast semicolon to check
354     * @return true if node is a resource
355     */
356    private static boolean isResource(DetailAST ast) {
357        return ast.getType() == TokenTypes.RESOURCES;
358    }
359
360}