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