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.metrics;
021
022import java.util.ArrayDeque;
023import java.util.Deque;
024
025import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.api.TokenTypes;
029
030/**
031 * <p>
032 * Determines complexity of methods, classes and files by counting
033 * the Non Commenting Source Statements (NCSS). This check adheres to the
034 * <a href="http://www.kclee.de/clemens/java/javancss/#specification">specification</a>
035 * for the <a href="http://www.kclee.de/clemens/java/javancss/">JavaNCSS-Tool</a>
036 * written by <b>Chr. Clemens Lee</b>.
037 * </p>
038 * <p>
039 * Roughly said the NCSS metric is calculated by counting the source lines which are
040 * not comments, (nearly) equivalent to counting the semicolons and opening curly braces.
041 * </p>
042 * <p>
043 * The NCSS for a class is summarized from the NCSS of all its methods, the NCSS
044 * of its nested classes and the number of member variable declarations.
045 * </p>
046 * <p>
047 * The NCSS for a file is summarized from the ncss of all its top level classes,
048 * the number of imports and the package declaration.
049 * </p>
050 * <p>
051 * Rationale: Too large methods and classes are hard to read and costly to maintain.
052 * A large NCSS number often means that a method or class has too many responsibilities
053 * and/or functionalities which should be decomposed into smaller units.
054 * </p>
055 * <ul>
056 * <li>
057 * Property {@code classMaximum} - Specify the maximum allowed number of
058 * non commenting lines in a class.
059 * Type is {@code int}.
060 * Default value is {@code 1500}.
061 * </li>
062 * <li>
063 * Property {@code fileMaximum} - Specify the maximum allowed number of
064 * non commenting lines in a file including all top level and nested classes.
065 * Type is {@code int}.
066 * Default value is {@code 2000}.
067 * </li>
068 * <li>
069 * Property {@code methodMaximum} - Specify the maximum allowed number of
070 * non commenting lines in a method.
071 * Type is {@code int}.
072 * Default value is {@code 50}.
073 * </li>
074 * <li>
075 * Property {@code recordMaximum} - Specify the maximum allowed number of
076 * non commenting lines in a record.
077 * Type is {@code int}.
078 * Default value is {@code 150}.
079 * </li>
080 * </ul>
081 * <p>
082 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
083 * </p>
084 * <p>
085 * Violation Message Keys:
086 * </p>
087 * <ul>
088 * <li>
089 * {@code ncss.class}
090 * </li>
091 * <li>
092 * {@code ncss.file}
093 * </li>
094 * <li>
095 * {@code ncss.method}
096 * </li>
097 * <li>
098 * {@code ncss.record}
099 * </li>
100 * </ul>
101 *
102 * @since 3.5
103 */
104// -@cs[AbbreviationAsWordInName] We can not change it as,
105// check's name is a part of API (used in configurations).
106@FileStatefulCheck
107public class JavaNCSSCheck extends AbstractCheck {
108
109    /**
110     * A key is pointing to the warning message text in "messages.properties"
111     * file.
112     */
113    public static final String MSG_METHOD = "ncss.method";
114
115    /**
116     * A key is pointing to the warning message text in "messages.properties"
117     * file.
118     */
119    public static final String MSG_CLASS = "ncss.class";
120
121    /**
122     * A key is pointing to the warning message text in "messages.properties"
123     * file.
124     */
125    public static final String MSG_RECORD = "ncss.record";
126
127    /**
128     * A key is pointing to the warning message text in "messages.properties"
129     * file.
130     */
131    public static final String MSG_FILE = "ncss.file";
132
133    /** Default constant for max file ncss. */
134    private static final int FILE_MAX_NCSS = 2000;
135
136    /** Default constant for max file ncss. */
137    private static final int CLASS_MAX_NCSS = 1500;
138
139    /** Default constant for max record ncss. */
140    private static final int RECORD_MAX_NCSS = 150;
141
142    /** Default constant for max method ncss. */
143    private static final int METHOD_MAX_NCSS = 50;
144
145    /**
146     * Specify the maximum allowed number of non commenting lines in a file
147     * including all top level and nested classes.
148     */
149    private int fileMaximum = FILE_MAX_NCSS;
150
151    /** Specify the maximum allowed number of non commenting lines in a class. */
152    private int classMaximum = CLASS_MAX_NCSS;
153
154    /** Specify the maximum allowed number of non commenting lines in a record. */
155    private int recordMaximum = RECORD_MAX_NCSS;
156
157    /** Specify the maximum allowed number of non commenting lines in a method. */
158    private int methodMaximum = METHOD_MAX_NCSS;
159
160    /** List containing the stacked counters. */
161    private Deque<Counter> counters;
162
163    @Override
164    public int[] getDefaultTokens() {
165        return getRequiredTokens();
166    }
167
168    @Override
169    public int[] getRequiredTokens() {
170        return new int[] {
171            TokenTypes.CLASS_DEF,
172            TokenTypes.INTERFACE_DEF,
173            TokenTypes.METHOD_DEF,
174            TokenTypes.CTOR_DEF,
175            TokenTypes.INSTANCE_INIT,
176            TokenTypes.STATIC_INIT,
177            TokenTypes.PACKAGE_DEF,
178            TokenTypes.IMPORT,
179            TokenTypes.VARIABLE_DEF,
180            TokenTypes.CTOR_CALL,
181            TokenTypes.SUPER_CTOR_CALL,
182            TokenTypes.LITERAL_IF,
183            TokenTypes.LITERAL_ELSE,
184            TokenTypes.LITERAL_WHILE,
185            TokenTypes.LITERAL_DO,
186            TokenTypes.LITERAL_FOR,
187            TokenTypes.LITERAL_SWITCH,
188            TokenTypes.LITERAL_BREAK,
189            TokenTypes.LITERAL_CONTINUE,
190            TokenTypes.LITERAL_RETURN,
191            TokenTypes.LITERAL_THROW,
192            TokenTypes.LITERAL_SYNCHRONIZED,
193            TokenTypes.LITERAL_CATCH,
194            TokenTypes.LITERAL_FINALLY,
195            TokenTypes.EXPR,
196            TokenTypes.LABELED_STAT,
197            TokenTypes.LITERAL_CASE,
198            TokenTypes.LITERAL_DEFAULT,
199            TokenTypes.RECORD_DEF,
200            TokenTypes.COMPACT_CTOR_DEF,
201        };
202    }
203
204    @Override
205    public int[] getAcceptableTokens() {
206        return getRequiredTokens();
207    }
208
209    @Override
210    public void beginTree(DetailAST rootAST) {
211        counters = new ArrayDeque<>();
212
213        // add a counter for the file
214        counters.push(new Counter());
215    }
216
217    @Override
218    public void visitToken(DetailAST ast) {
219        final int tokenType = ast.getType();
220
221        if (tokenType == TokenTypes.CLASS_DEF
222            || tokenType == TokenTypes.RECORD_DEF
223            || isMethodOrCtorOrInitDefinition(tokenType)) {
224            // add a counter for this class/method
225            counters.push(new Counter());
226        }
227
228        // check if token is countable
229        if (isCountable(ast)) {
230            // increment the stacked counters
231            counters.forEach(Counter::increment);
232        }
233    }
234
235    @Override
236    public void leaveToken(DetailAST ast) {
237        final int tokenType = ast.getType();
238
239        if (isMethodOrCtorOrInitDefinition(tokenType)) {
240            // pop counter from the stack
241            final Counter counter = counters.pop();
242
243            final int count = counter.getCount();
244            if (count > methodMaximum) {
245                log(ast, MSG_METHOD, count, methodMaximum);
246            }
247        }
248        else if (tokenType == TokenTypes.CLASS_DEF) {
249            // pop counter from the stack
250            final Counter counter = counters.pop();
251
252            final int count = counter.getCount();
253            if (count > classMaximum) {
254                log(ast, MSG_CLASS, count, classMaximum);
255            }
256        }
257        else if (tokenType == TokenTypes.RECORD_DEF) {
258            // pop counter from the stack
259            final Counter counter = counters.pop();
260
261            final int count = counter.getCount();
262            if (count > recordMaximum) {
263                log(ast, MSG_RECORD, count, recordMaximum);
264            }
265        }
266    }
267
268    @Override
269    public void finishTree(DetailAST rootAST) {
270        // pop counter from the stack
271        final Counter counter = counters.pop();
272
273        final int count = counter.getCount();
274        if (count > fileMaximum) {
275            log(rootAST, MSG_FILE, count, fileMaximum);
276        }
277    }
278
279    /**
280     * Setter to specify the maximum allowed number of non commenting lines
281     * in a file including all top level and nested classes.
282     *
283     * @param fileMaximum
284     *            the maximum ncss
285     * @since 3.5
286     */
287    public void setFileMaximum(int fileMaximum) {
288        this.fileMaximum = fileMaximum;
289    }
290
291    /**
292     * Setter to specify the maximum allowed number of non commenting lines in a class.
293     *
294     * @param classMaximum
295     *            the maximum ncss
296     * @since 3.5
297     */
298    public void setClassMaximum(int classMaximum) {
299        this.classMaximum = classMaximum;
300    }
301
302    /**
303     * Setter to specify the maximum allowed number of non commenting lines in a record.
304     *
305     * @param recordMaximum
306     *            the maximum ncss
307     * @since 8.36
308     */
309    public void setRecordMaximum(int recordMaximum) {
310        this.recordMaximum = recordMaximum;
311    }
312
313    /**
314     * Setter to specify the maximum allowed number of non commenting lines in a method.
315     *
316     * @param methodMaximum
317     *            the maximum ncss
318     * @since 3.5
319     */
320    public void setMethodMaximum(int methodMaximum) {
321        this.methodMaximum = methodMaximum;
322    }
323
324    /**
325     * Checks if a token is countable for the ncss metric.
326     *
327     * @param ast
328     *            the AST
329     * @return true if the token is countable
330     */
331    private static boolean isCountable(DetailAST ast) {
332        boolean countable = true;
333
334        final int tokenType = ast.getType();
335
336        // check if an expression is countable
337        if (tokenType == TokenTypes.EXPR) {
338            countable = isExpressionCountable(ast);
339        }
340        // check if a variable definition is countable
341        else if (tokenType == TokenTypes.VARIABLE_DEF) {
342            countable = isVariableDefCountable(ast);
343        }
344        return countable;
345    }
346
347    /**
348     * Checks if a variable definition is countable.
349     *
350     * @param ast the AST
351     * @return true if the variable definition is countable, false otherwise
352     */
353    private static boolean isVariableDefCountable(DetailAST ast) {
354        boolean countable = false;
355
356        // count variable definitions only if they are direct child to a slist or
357        // object block
358        final int parentType = ast.getParent().getType();
359
360        if (parentType == TokenTypes.SLIST
361            || parentType == TokenTypes.OBJBLOCK) {
362            final DetailAST prevSibling = ast.getPreviousSibling();
363
364            // is countable if no previous sibling is found or
365            // the sibling is no COMMA.
366            // This is done because multiple assignment on one line are counted
367            // as 1
368            countable = prevSibling == null
369                    || prevSibling.getType() != TokenTypes.COMMA;
370        }
371
372        return countable;
373    }
374
375    /**
376     * Checks if an expression is countable for the ncss metric.
377     *
378     * @param ast the AST
379     * @return true if the expression is countable, false otherwise
380     */
381    private static boolean isExpressionCountable(DetailAST ast) {
382        final boolean countable;
383
384        // count expressions only if they are direct child to a slist (method
385        // body, for loop...)
386        // or direct child of label,if,else,do,while,for
387        final int parentType = ast.getParent().getType();
388        switch (parentType) {
389            case TokenTypes.SLIST:
390            case TokenTypes.LABELED_STAT:
391            case TokenTypes.LITERAL_FOR:
392            case TokenTypes.LITERAL_DO:
393            case TokenTypes.LITERAL_WHILE:
394            case TokenTypes.LITERAL_IF:
395            case TokenTypes.LITERAL_ELSE:
396                // don't count if or loop conditions
397                final DetailAST prevSibling = ast.getPreviousSibling();
398                countable = prevSibling == null
399                    || prevSibling.getType() != TokenTypes.LPAREN;
400                break;
401            default:
402                countable = false;
403                break;
404        }
405        return countable;
406    }
407
408    /**
409     * Checks if a token is a method, constructor, or compact constructor definition.
410     *
411     * @param tokenType the type of token we are checking
412     * @return true if token type is method or ctor definition, false otherwise
413     */
414    private static boolean isMethodOrCtorOrInitDefinition(int tokenType) {
415        return tokenType == TokenTypes.METHOD_DEF
416                || tokenType == TokenTypes.COMPACT_CTOR_DEF
417                || tokenType == TokenTypes.CTOR_DEF
418                || tokenType == TokenTypes.STATIC_INIT
419                || tokenType == TokenTypes.INSTANCE_INIT;
420    }
421
422    /**
423     * Class representing a counter.
424     *
425     */
426    private static final class Counter {
427
428        /** The counters internal integer. */
429        private int count;
430
431        /**
432         * Increments the counter.
433         */
434        public void increment() {
435            count++;
436        }
437
438        /**
439         * Gets the counters value.
440         *
441         * @return the counter
442         */
443        public int getCount() {
444            return count;
445        }
446
447    }
448
449}