001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 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.sizes;
021
022import java.util.ArrayDeque;
023import java.util.BitSet;
024import java.util.Deque;
025import java.util.Objects;
026import java.util.stream.Stream;
027
028import com.puppycrawl.tools.checkstyle.StatelessCheck;
029import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
030import com.puppycrawl.tools.checkstyle.api.DetailAST;
031import com.puppycrawl.tools.checkstyle.api.TokenTypes;
032import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
033
034/**
035 * <p>
036 * Checks for long methods and constructors.
037 * </p>
038 * <p>
039 * Rationale: If a method becomes very long it is hard to understand.
040 * Therefore, long methods should usually be refactored into several
041 * individual methods that focus on a specific task.
042 * </p>
043 * <ul>
044 * <li>
045 * Property {@code countEmpty} - Control whether to count empty lines and comments.
046 * Type is {@code boolean}.
047 * Default value is {@code true}.
048 * </li>
049 * <li>
050 * Property {@code max} - Specify the maximum number of lines allowed.
051 * Type is {@code int}.
052 * Default value is {@code 150}.
053 * </li>
054 * <li>
055 * Property {@code tokens} - tokens to check
056 * Type is {@code java.lang.String[]}.
057 * Validation type is {@code tokenSet}.
058 * Default value is:
059 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#METHOD_DEF">
060 * METHOD_DEF</a>,
061 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#CTOR_DEF">
062 * CTOR_DEF</a>,
063 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#COMPACT_CTOR_DEF">
064 * COMPACT_CTOR_DEF</a>.
065 * </li>
066 * </ul>
067 * <p>
068 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
069 * </p>
070 * <p>
071 * Violation Message Keys:
072 * </p>
073 * <ul>
074 * <li>
075 * {@code maxLen.method}
076 * </li>
077 * </ul>
078 *
079 * @since 3.0
080 */
081@StatelessCheck
082public class MethodLengthCheck extends AbstractCheck {
083
084    /**
085     * A key is pointing to the warning message text in "messages.properties"
086     * file.
087     */
088    public static final String MSG_KEY = "maxLen.method";
089
090    /** Default maximum number of lines. */
091    private static final int DEFAULT_MAX_LINES = 150;
092
093    /** Control whether to count empty lines and comments. */
094    private boolean countEmpty = true;
095
096    /** Specify the maximum number of lines allowed. */
097    private int max = DEFAULT_MAX_LINES;
098
099    @Override
100    public int[] getDefaultTokens() {
101        return getAcceptableTokens();
102    }
103
104    @Override
105    public int[] getAcceptableTokens() {
106        return new int[] {
107            TokenTypes.METHOD_DEF,
108            TokenTypes.CTOR_DEF,
109            TokenTypes.COMPACT_CTOR_DEF,
110        };
111    }
112
113    @Override
114    public int[] getRequiredTokens() {
115        return CommonUtil.EMPTY_INT_ARRAY;
116    }
117
118    @Override
119    public void visitToken(DetailAST ast) {
120        final DetailAST openingBrace = ast.findFirstToken(TokenTypes.SLIST);
121        if (openingBrace != null) {
122            final int length;
123            if (countEmpty) {
124                final DetailAST closingBrace = openingBrace.findFirstToken(TokenTypes.RCURLY);
125                length = getLengthOfBlock(openingBrace, closingBrace);
126            }
127            else {
128                length = countUsedLines(openingBrace);
129            }
130            if (length > max) {
131                final String methodName = ast.findFirstToken(TokenTypes.IDENT).getText();
132                log(ast, MSG_KEY, length, max, methodName);
133            }
134        }
135    }
136
137    /**
138     * Returns length of code.
139     *
140     * @param openingBrace block opening brace
141     * @param closingBrace block closing brace
142     * @return number of lines with code for current block
143     */
144    private static int getLengthOfBlock(DetailAST openingBrace, DetailAST closingBrace) {
145        final int startLineNo = openingBrace.getLineNo();
146        final int endLineNo = closingBrace.getLineNo();
147        return endLineNo - startLineNo + 1;
148    }
149
150    /**
151     * Count number of used code lines without comments.
152     *
153     * @param ast start ast
154     * @return number of used lines of code
155     */
156    private static int countUsedLines(DetailAST ast) {
157        final Deque<DetailAST> nodes = new ArrayDeque<>();
158        nodes.add(ast);
159        final BitSet usedLines = new BitSet();
160        while (!nodes.isEmpty()) {
161            final DetailAST node = nodes.removeFirst();
162            final int lineIndex = node.getLineNo();
163            // text block requires special treatment,
164            // since it is the only non-comment token that can span more than one line
165            if (node.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN) {
166                final int endLineIndex = node.getLastChild().getLineNo();
167                usedLines.set(lineIndex, endLineIndex + 1);
168            }
169            else {
170                usedLines.set(lineIndex);
171                Stream.iterate(
172                    node.getLastChild(), Objects::nonNull, DetailAST::getPreviousSibling
173                ).forEach(nodes::addFirst);
174            }
175        }
176        return usedLines.cardinality();
177    }
178
179    /**
180     * Setter to specify the maximum number of lines allowed.
181     *
182     * @param length the maximum length of a method.
183     * @since 3.0
184     */
185    public void setMax(int length) {
186        max = length;
187    }
188
189    /**
190     * Setter to control whether to count empty lines and comments.
191     *
192     * @param countEmpty whether to count empty and comments.
193     * @since 3.2
194     */
195    public void setCountEmpty(boolean countEmpty) {
196        this.countEmpty = countEmpty;
197    }
198
199}