001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.regex.Pattern;
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;
030
031/**
032 * <div>
033 * Restricts the number of return statements in methods, constructors and lambda expressions.
034 * Ignores specified methods ({@code equals} by default).
035 * </div>
036 *
037 * <p>
038 * <b>max</b> property will only check returns in methods and lambdas that
039 * return a specific value (Ex: 'return 1;').
040 * </p>
041 *
042 * <p>
043 * <b>maxForVoid</b> property will only check returns in methods, constructors,
044 * and lambdas that have no return type (IE 'return;'). It will only count
045 * visible return statements. Return statements not normally written, but
046 * implied, at the end of the method/constructor definition will not be taken
047 * into account. To disallow "return;" in void return type methods, use a value
048 * of 0.
049 * </p>
050 *
051 * <p>
052 * Rationale: Too many return points can mean that code is
053 * attempting to do too much or may be difficult to understand.
054 * </p>
055 *
056 * @since 3.2
057 */
058@FileStatefulCheck
059public final class ReturnCountCheck extends AbstractCheck {
060
061    /**
062     * A key is pointing to the warning message text in "messages.properties"
063     * file.
064     */
065    public static final String MSG_KEY = "return.count";
066    /**
067     * A key pointing to the warning message text in "messages.properties"
068     * file.
069     */
070    public static final String MSG_KEY_VOID = "return.countVoid";
071
072    /** Stack of method contexts. */
073    private final Deque<Context> contextStack = new ArrayDeque<>();
074
075    /** Specify method names to ignore. */
076    private Pattern format = Pattern.compile("^equals$");
077
078    /** Specify maximum allowed number of return statements in non-void methods/lambdas. */
079    private int max = 2;
080    /** Specify maximum allowed number of return statements in void methods/constructors/lambdas. */
081    private int maxForVoid = 1;
082    /** Current method context. */
083    private Context context;
084
085    @Override
086    public int[] getDefaultTokens() {
087        return new int[] {
088            TokenTypes.CTOR_DEF,
089            TokenTypes.METHOD_DEF,
090            TokenTypes.LAMBDA,
091            TokenTypes.LITERAL_RETURN,
092        };
093    }
094
095    @Override
096    public int[] getRequiredTokens() {
097        return new int[] {TokenTypes.LITERAL_RETURN};
098    }
099
100    @Override
101    public int[] getAcceptableTokens() {
102        return new int[] {
103            TokenTypes.CTOR_DEF,
104            TokenTypes.METHOD_DEF,
105            TokenTypes.LAMBDA,
106            TokenTypes.LITERAL_RETURN,
107        };
108    }
109
110    /**
111     * Setter to specify method names to ignore.
112     *
113     * @param pattern a pattern.
114     * @since 3.4
115     */
116    public void setFormat(Pattern pattern) {
117        format = pattern;
118    }
119
120    /**
121     * Setter to specify maximum allowed number of return statements
122     * in non-void methods/lambdas.
123     *
124     * @param max maximum allowed number of return statements.
125     * @since 3.2
126     */
127    public void setMax(int max) {
128        this.max = max;
129    }
130
131    /**
132     * Setter to specify maximum allowed number of return statements
133     * in void methods/constructors/lambdas.
134     *
135     * @param maxForVoid maximum allowed number of return statements for void methods.
136     * @since 6.19
137     */
138    public void setMaxForVoid(int maxForVoid) {
139        this.maxForVoid = maxForVoid;
140    }
141
142    @Override
143    public void beginTree(DetailAST rootAST) {
144        context = new Context(false);
145        contextStack.clear();
146    }
147
148    @Override
149    public void visitToken(DetailAST ast) {
150        switch (ast.getType()) {
151            case TokenTypes.CTOR_DEF,
152                 TokenTypes.METHOD_DEF -> visitMethodDef(ast);
153            case TokenTypes.LAMBDA -> visitLambda();
154            case TokenTypes.LITERAL_RETURN -> visitReturn(ast);
155            default -> throw new IllegalStateException(ast.toString());
156        }
157    }
158
159    @Override
160    public void leaveToken(DetailAST ast) {
161        switch (ast.getType()) {
162            case TokenTypes.CTOR_DEF:
163            case TokenTypes.METHOD_DEF:
164            case TokenTypes.LAMBDA:
165                leave(ast);
166                break;
167            case TokenTypes.LITERAL_RETURN:
168                // Do nothing
169                break;
170            default:
171                throw new IllegalStateException(ast.toString());
172        }
173    }
174
175    /**
176     * Creates new method context and places old one on the stack.
177     *
178     * @param ast method definition for check.
179     */
180    private void visitMethodDef(DetailAST ast) {
181        contextStack.push(context);
182        final DetailAST methodNameAST = ast.findFirstToken(TokenTypes.IDENT);
183        final boolean check = !format.matcher(methodNameAST.getText()).find();
184        context = new Context(check);
185    }
186
187    /**
188     * Checks number of return statements and restore previous context.
189     *
190     * @param ast node to leave.
191     */
192    private void leave(DetailAST ast) {
193        context.checkCount(ast);
194        context = contextStack.pop();
195    }
196
197    /**
198     * Creates new lambda context and places old one on the stack.
199     */
200    private void visitLambda() {
201        contextStack.push(context);
202        context = new Context(true);
203    }
204
205    /**
206     * Examines the return statement and tells context about it.
207     *
208     * @param ast return statement to check.
209     */
210    private void visitReturn(DetailAST ast) {
211        // we can't identify which max to use for lambdas, so we can only assign
212        // after the first return statement is seen
213        if (ast.getFirstChild().getType() == TokenTypes.SEMI) {
214            context.visitLiteralReturn(maxForVoid, Boolean.TRUE);
215        }
216        else {
217            context.visitLiteralReturn(max, Boolean.FALSE);
218        }
219    }
220
221    /**
222     * Class to encapsulate information about one method.
223     */
224    private final class Context {
225
226        /** Whether we should check this method or not. */
227        private final boolean checking;
228        /** Counter for return statements. */
229        private int count;
230        /** Maximum allowed number of return statements. */
231        private Integer maxAllowed;
232        /** Identifies if context is void. */
233        private boolean isVoidContext;
234
235        /**
236         * Creates new method context.
237         *
238         * @param checking should we check this method or not
239         */
240        private Context(boolean checking) {
241            this.checking = checking;
242        }
243
244        /**
245         * Increase the number of return statements and set context return type.
246         *
247         * @param maxAssigned Maximum allowed number of return statements.
248         * @param voidReturn Identifies if context is void.
249         */
250        public void visitLiteralReturn(int maxAssigned, Boolean voidReturn) {
251            isVoidContext = voidReturn;
252            maxAllowed = maxAssigned;
253
254            ++count;
255        }
256
257        /**
258         * Checks if number of return statements in the method are more
259         * than allowed.
260         *
261         * @param ast method def associated with this context.
262         */
263        public void checkCount(DetailAST ast) {
264            if (checking && maxAllowed != null && count > maxAllowed) {
265                if (isVoidContext) {
266                    log(ast, MSG_KEY_VOID, count, maxAllowed);
267                }
268                else {
269                    log(ast, MSG_KEY, count, maxAllowed);
270                }
271            }
272        }
273
274    }
275
276}