001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2022 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 max} - Specify the maximum number of lines allowed.
046 * Type is {@code int}.
047 * Default value is {@code 150}.
048 * </li>
049 * <li>
050 * Property {@code countEmpty} - Control whether to count empty lines and comments.
051 * Type is {@code boolean}.
052 * Default value is {@code true}.
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 * To configure the check:
069 * </p>
070 * <pre>
071 * &lt;module name="MethodLength"/&gt;
072 * </pre>
073 * <p>
074 * Example:
075 * </p>
076 * <pre>
077 * public class MyClass {
078 *   public MyClass() {  // constructor (line 1)
079 *        /&#42; line 2
080 *            ...
081 *           line 150 &#42;/
082 *   } // line 151, violation, as it is over 150
083 *
084 *   public void firstExample() { // line 1
085 *
086 *       // line 3
087 *       System.out.println("line 4");
088 *       /&#42; line 5
089 *          line 6 &#42;/
090 *   } // line 7, OK, as it is less than 150
091 *
092 *   public void secondExample() { // line 1
093 *       // line 2
094 *       System.out.println("line 3");
095 *
096 *       /&#42; line 5
097 *           ...
098 *          line 150 &#42;/
099 *   } // line 151, violation, as it is over 150
100 * }
101 * </pre>
102 * <p>
103 * To configure the check so that it accepts methods with at most 4 lines:
104 * </p>
105 * <pre>
106 * &lt;module name="MethodLength"&gt;
107 *   &lt;property name="tokens" value="METHOD_DEF"/&gt;
108 *   &lt;property name="max" value="4"/&gt;
109 * &lt;/module&gt;
110 * </pre>
111 * <p>
112 * Example:
113 * </p>
114 * <pre>
115 * public class MyTest {
116 *   public MyTest()  {            // constructor (line 1)
117 *       int var1 = 2;             // line 2
118 *       int var2 = 4;             // line 3
119 *       int sum = var1 + var2;  // line 4
120 *   } // line 5, OK, constructor is not mentioned in the tokens
121 *
122 *   public void firstMethod() { // line 1
123 *       // comment (line 2)
124 *       System.out.println("line 3");
125 *   } // line 4, OK, as it allows at most 4 lines
126 *
127 *   public void secondMethod() { // line 1
128 *       int index = 0;   // line 2
129 *       if (index &#60; 5) { // line 3
130 *           index++;     // line 4
131 *       }                // line 5
132 *   } // line 6, violation, as it is over 4 lines
133 *
134 *   public void thirdMethod() { // line 1
135 *
136 *       // comment (line 3)
137 *       System.out.println("line 4");
138 *   } // line 5, violation, as it is over 4 lines
139 * }
140 * </pre>
141 * <p>
142 * To configure the check so that it accepts methods with at most 4 lines,
143 * not counting empty lines and comments:
144 * </p>
145 * <pre>
146 * &lt;module name="MethodLength"&gt;
147 *   &lt;property name="tokens" value="METHOD_DEF"/&gt;
148 *   &lt;property name="max" value="4"/&gt;
149 *   &lt;property name="countEmpty" value="false"/&gt;
150 * &lt;/module&gt;
151 * </pre>
152 * <p>
153 * Example:
154 * </p>
155 * <pre>
156 * public class MyTest {
157 *   public MyTest()  {          // constructor (line 1)
158 *       int var1 = 2;           // line 2
159 *       int var2 = 4;           // line 3
160 *       int sum = var1 + var2;  // line 4
161 *   } // line 5, OK, constructor is not mentioned in the tokens
162 *
163 *   public void firstMethod() { // line 1
164 *       // comment - not counted as line
165 *       System.out.println("line 2");
166 *   } // line 3, OK, as it allows at most 4 lines
167 *
168 *   public void secondMethod() { // line 1
169 *       int index = 0;   // line 2
170 *       if (index &#60; 5) { // line 3
171 *           index++;     // line 4
172 *       }                // line 5
173 *   } // line 6, violation, as it is over 4 lines
174 *
175 *   public void thirdMethod() { // line 1
176 *
177 *       // comment - not counted as line
178 *       System.out.println("line 2");
179 *   } // line 3, OK, as it allows at most 4 lines
180 * }
181 * </pre>
182 * <p>
183 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
184 * </p>
185 * <p>
186 * Violation Message Keys:
187 * </p>
188 * <ul>
189 * <li>
190 * {@code maxLen.method}
191 * </li>
192 * </ul>
193 *
194 * @since 3.0
195 */
196@StatelessCheck
197public class MethodLengthCheck extends AbstractCheck {
198
199    /**
200     * A key is pointing to the warning message text in "messages.properties"
201     * file.
202     */
203    public static final String MSG_KEY = "maxLen.method";
204
205    /** Default maximum number of lines. */
206    private static final int DEFAULT_MAX_LINES = 150;
207
208    /** Control whether to count empty lines and comments. */
209    private boolean countEmpty = true;
210
211    /** Specify the maximum number of lines allowed. */
212    private int max = DEFAULT_MAX_LINES;
213
214    @Override
215    public int[] getDefaultTokens() {
216        return getAcceptableTokens();
217    }
218
219    @Override
220    public int[] getAcceptableTokens() {
221        return new int[] {
222            TokenTypes.METHOD_DEF,
223            TokenTypes.CTOR_DEF,
224            TokenTypes.COMPACT_CTOR_DEF,
225        };
226    }
227
228    @Override
229    public int[] getRequiredTokens() {
230        return CommonUtil.EMPTY_INT_ARRAY;
231    }
232
233    @Override
234    public void visitToken(DetailAST ast) {
235        final DetailAST openingBrace = ast.findFirstToken(TokenTypes.SLIST);
236        if (openingBrace != null) {
237            final int length;
238            if (countEmpty) {
239                final DetailAST closingBrace = openingBrace.findFirstToken(TokenTypes.RCURLY);
240                length = getLengthOfBlock(openingBrace, closingBrace);
241            }
242            else {
243                length = countUsedLines(openingBrace);
244            }
245            if (length > max) {
246                final String methodName = ast.findFirstToken(TokenTypes.IDENT).getText();
247                log(ast, MSG_KEY, length, max, methodName);
248            }
249        }
250    }
251
252    /**
253     * Returns length of code.
254     *
255     * @param openingBrace block opening brace
256     * @param closingBrace block closing brace
257     * @return number of lines with code for current block
258     */
259    private static int getLengthOfBlock(DetailAST openingBrace, DetailAST closingBrace) {
260        final int startLineNo = openingBrace.getLineNo();
261        final int endLineNo = closingBrace.getLineNo();
262        return endLineNo - startLineNo + 1;
263    }
264
265    /**
266     * Count number of used code lines without comments.
267     *
268     * @param ast start ast
269     * @return number of used lines of code
270     */
271    private static int countUsedLines(DetailAST ast) {
272        final Deque<DetailAST> nodes = new ArrayDeque<>();
273        nodes.add(ast);
274        final BitSet usedLines = new BitSet();
275        final int startLineNo = ast.getLineNo();
276        while (!nodes.isEmpty()) {
277            final DetailAST node = nodes.removeFirst();
278            final int lineIndex = node.getLineNo() - startLineNo;
279            // text block requires special treatment,
280            // since it is the only non-comment token that can span more than one line
281            if (node.getType() == TokenTypes.TEXT_BLOCK_LITERAL_BEGIN) {
282                final int endLineIndex = node.getLastChild().getLineNo() - startLineNo;
283                usedLines.set(lineIndex, endLineIndex + 1);
284            }
285            else {
286                usedLines.set(lineIndex);
287                Stream.iterate(
288                    node.getLastChild(), Objects::nonNull, DetailAST::getPreviousSibling
289                ).forEach(nodes::addFirst);
290            }
291        }
292        return usedLines.cardinality();
293    }
294
295    /**
296     * Setter to specify the maximum number of lines allowed.
297     *
298     * @param length the maximum length of a method.
299     */
300    public void setMax(int length) {
301        max = length;
302    }
303
304    /**
305     * Setter to control whether to count empty lines and comments.
306     *
307     * @param countEmpty whether to count empty and comments.
308     */
309    public void setCountEmpty(boolean countEmpty) {
310        this.countEmpty = countEmpty;
311    }
312
313}