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