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