001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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 * <p>
061 * Here is a breakdown of what exactly is counted and not counted:
062 * </p>
063 * <div class="wrapper">
064 * <table>
065 * <caption>JavaNCSS metrics</caption>
066 * <thead><tr><th>Structure</th><th>NCSS Count</th><th>Notes</th></tr></thead>
067 * <tbody>
068 * <tr><td>Package declaration</td><td>1</td>
069 * <td>Counted at the terminating semicolon.</td></tr>
070 * <tr><td>Import declaration</td><td>1</td>
071 * <td>Each single, static, or wildcard import counts as 1.</td></tr>
072 * <tr><td>Class, Interface, Annotation ({@code @interface})</td><td>1</td>
073 * <td>Counted at the opening curly brace of the body.</td></tr>
074 * <tr><td>Method, Constructor</td><td>1</td>
075 * <td>Counted at the declaration.</td></tr>
076 * <tr><td>Static initializer, Instance initializer</td><td>1</td>
077 * <td>Both {@code static {}} and bare {@code {}} initializer blocks count as 1.</td></tr>
078 * <tr><td>Annotation type member</td><td>1</td>
079 * <td>Each method-like member declaration inside {@code @interface} counts as 1.
080 * A standalone {@code ;} inside {@code @interface} also counts as 1.</td></tr>
081 * <tr><td>Variable declaration</td><td>1</td>
082 * <td>1 per statement regardless of how many variables are declared on that line.
083 * {@code int x, y;} counts as 1.</td></tr>
084 * <tr><td>{@code if}</td><td>1</td>
085 * <td>The {@code if} keyword counts as 1.</td></tr>
086 * <tr><td>{@code else}, {@code else if}</td><td>1</td>
087 * <td>The {@code else} keyword counts as 1, separate from the {@code if} count.</td></tr>
088 * <tr><td>{@code while}, {@code do}, {@code for}</td><td>1</td>
089 * <td>The keyword header counts as 1.</td></tr>
090 * <tr><td>{@code switch}</td><td>1</td>
091 * <td>The {@code switch} keyword counts as 1.</td></tr>
092 * <tr><td>{@code case}, {@code default}</td><td>1</td>
093 * <td>Every case and default label adds 1.</td></tr>
094 * <tr><td>{@code try}</td><td>0</td>
095 * <td>The {@code try} keyword itself does not count.</td></tr>
096 * <tr><td>{@code catch}</td><td>1</td>
097 * <td>Each catch block counts as 1.</td></tr>
098 * <tr><td>{@code finally}</td><td>1</td>
099 * <td>The finally block counts as 1.</td></tr>
100 * <tr><td>{@code synchronized}</td><td>1</td>
101 * <td>The synchronized statement counts as 1.</td></tr>
102 * <tr><td>{@code return}, {@code break}, {@code continue}, {@code throw}</td><td>1</td>
103 * <td>Each counts as 1.</td></tr>
104 * <tr><td>{@code assert}</td><td>1</td>
105 * <td>Each assert statement counts as 1, with or without a message expression.</td></tr>
106 * <tr><td>Labeled statement</td><td>1</td>
107 * <td>{@code label: statement} counts as 1.</td></tr>
108 * <tr><td>Explicit constructor invocation</td><td>1</td>
109 * <td>{@code this()} or {@code super()} calls inside a constructor body
110 * each count as 1.</td></tr>
111 * <tr><td>Expression statements (assignments, method calls)</td><td>1</td>
112 * <td>Statement-level expressions terminated by {@code ;} count as 1.
113 * A method call inside a {@code return} does not add an extra count.</td></tr>
114 * <tr><td>Empty blocks {}</td><td>0</td>
115 * <td>Empty curly braces do not increase the count.</td></tr>
116 * <tr><td>Empty statements ;</td><td>0</td>
117 * <td>Standalone semicolons outside of {@code @interface} do not increase
118 * the count.</td></tr>
119 * </tbody>
120 * </table>
121 * </div>
122 *
123 * @since 3.5
124 */
125// -@cs[AbbreviationAsWordInName] We can not change it as,
126// check's name is a part of API (used in configurations).
127@FileStatefulCheck
128public class JavaNCSSCheck extends AbstractCheck {
129
130    /**
131     * A key is pointing to the warning message text in "messages.properties"
132     * file.
133     */
134    public static final String MSG_METHOD = "ncss.method";
135
136    /**
137     * A key is pointing to the warning message text in "messages.properties"
138     * file.
139     */
140    public static final String MSG_CLASS = "ncss.class";
141
142    /**
143     * A key is pointing to the warning message text in "messages.properties"
144     * file.
145     */
146    public static final String MSG_RECORD = "ncss.record";
147
148    /**
149     * A key is pointing to the warning message text in "messages.properties"
150     * file.
151     */
152    public static final String MSG_FILE = "ncss.file";
153
154    /** Default constant for max file ncss. */
155    private static final int FILE_MAX_NCSS = 2000;
156
157    /** Default constant for max file ncss. */
158    private static final int CLASS_MAX_NCSS = 1500;
159
160    /** Default constant for max record ncss. */
161    private static final int RECORD_MAX_NCSS = 150;
162
163    /** Default constant for max method ncss. */
164    private static final int METHOD_MAX_NCSS = 50;
165
166    /**
167     * Specify the maximum allowed number of non commenting lines in a file
168     * including all top level and nested classes.
169     */
170    private int fileMaximum = FILE_MAX_NCSS;
171
172    /** Specify the maximum allowed number of non commenting lines in a class. */
173    private int classMaximum = CLASS_MAX_NCSS;
174
175    /** Specify the maximum allowed number of non commenting lines in a record. */
176    private int recordMaximum = RECORD_MAX_NCSS;
177
178    /** Specify the maximum allowed number of non commenting lines in a method. */
179    private int methodMaximum = METHOD_MAX_NCSS;
180
181    /** List containing the stacked counters. */
182    private Deque<Counter> counters;
183
184    @Override
185    public int[] getDefaultTokens() {
186        return getRequiredTokens();
187    }
188
189    @Override
190    public int[] getRequiredTokens() {
191        return new int[] {
192            TokenTypes.CLASS_DEF,
193            TokenTypes.INTERFACE_DEF,
194            TokenTypes.METHOD_DEF,
195            TokenTypes.CTOR_DEF,
196            TokenTypes.INSTANCE_INIT,
197            TokenTypes.STATIC_INIT,
198            TokenTypes.PACKAGE_DEF,
199            TokenTypes.IMPORT,
200            TokenTypes.VARIABLE_DEF,
201            TokenTypes.CTOR_CALL,
202            TokenTypes.SUPER_CTOR_CALL,
203            TokenTypes.LITERAL_IF,
204            TokenTypes.LITERAL_ELSE,
205            TokenTypes.LITERAL_WHILE,
206            TokenTypes.LITERAL_DO,
207            TokenTypes.LITERAL_FOR,
208            TokenTypes.LITERAL_SWITCH,
209            TokenTypes.LITERAL_BREAK,
210            TokenTypes.LITERAL_CONTINUE,
211            TokenTypes.LITERAL_RETURN,
212            TokenTypes.LITERAL_THROW,
213            TokenTypes.LITERAL_SYNCHRONIZED,
214            TokenTypes.LITERAL_CATCH,
215            TokenTypes.LITERAL_FINALLY,
216            TokenTypes.EXPR,
217            TokenTypes.LABELED_STAT,
218            TokenTypes.LITERAL_CASE,
219            TokenTypes.LITERAL_DEFAULT,
220            TokenTypes.RECORD_DEF,
221            TokenTypes.COMPACT_CTOR_DEF,
222        };
223    }
224
225    @Override
226    public int[] getAcceptableTokens() {
227        return getRequiredTokens();
228    }
229
230    @Override
231    public void beginTree(DetailAST rootAST) {
232        counters = new ArrayDeque<>();
233
234        // add a counter for the file
235        counters.push(new Counter());
236    }
237
238    @Override
239    public void visitToken(DetailAST ast) {
240        final int tokenType = ast.getType();
241
242        if (tokenType == TokenTypes.CLASS_DEF
243            || tokenType == TokenTypes.RECORD_DEF
244            || isMethodOrCtorOrInitDefinition(tokenType)) {
245            // add a counter for this class/method
246            counters.push(new Counter());
247        }
248
249        // check if token is countable
250        if (isCountable(ast)) {
251            // increment the stacked counters
252            counters.forEach(Counter::increment);
253        }
254    }
255
256    @Override
257    public void leaveToken(DetailAST ast) {
258        final int tokenType = ast.getType();
259
260        if (isMethodOrCtorOrInitDefinition(tokenType)) {
261            // pop counter from the stack
262            final Counter counter = counters.pop();
263
264            final int count = counter.getCount();
265            if (count > methodMaximum) {
266                log(ast, MSG_METHOD, count, methodMaximum);
267            }
268        }
269        else if (tokenType == TokenTypes.CLASS_DEF) {
270            // pop counter from the stack
271            final Counter counter = counters.pop();
272
273            final int count = counter.getCount();
274            if (count > classMaximum) {
275                log(ast, MSG_CLASS, count, classMaximum);
276            }
277        }
278        else if (tokenType == TokenTypes.RECORD_DEF) {
279            // pop counter from the stack
280            final Counter counter = counters.pop();
281
282            final int count = counter.getCount();
283            if (count > recordMaximum) {
284                log(ast, MSG_RECORD, count, recordMaximum);
285            }
286        }
287    }
288
289    @Override
290    public void finishTree(DetailAST rootAST) {
291        // pop counter from the stack
292        final Counter counter = counters.pop();
293
294        final int count = counter.getCount();
295        if (count > fileMaximum) {
296            log(rootAST, MSG_FILE, count, fileMaximum);
297        }
298    }
299
300    /**
301     * Setter to specify the maximum allowed number of non commenting lines
302     * in a file including all top level and nested classes.
303     *
304     * @param fileMaximum
305     *            the maximum ncss
306     * @since 3.5
307     */
308    public void setFileMaximum(int fileMaximum) {
309        this.fileMaximum = fileMaximum;
310    }
311
312    /**
313     * Setter to specify the maximum allowed number of non commenting lines in a class.
314     *
315     * @param classMaximum
316     *            the maximum ncss
317     * @since 3.5
318     */
319    public void setClassMaximum(int classMaximum) {
320        this.classMaximum = classMaximum;
321    }
322
323    /**
324     * Setter to specify the maximum allowed number of non commenting lines in a record.
325     *
326     * @param recordMaximum
327     *            the maximum ncss
328     * @since 8.36
329     */
330    public void setRecordMaximum(int recordMaximum) {
331        this.recordMaximum = recordMaximum;
332    }
333
334    /**
335     * Setter to specify the maximum allowed number of non commenting lines in a method.
336     *
337     * @param methodMaximum
338     *            the maximum ncss
339     * @since 3.5
340     */
341    public void setMethodMaximum(int methodMaximum) {
342        this.methodMaximum = methodMaximum;
343    }
344
345    /**
346     * Checks if a token is countable for the ncss metric.
347     *
348     * @param ast
349     *            the AST
350     * @return true if the token is countable
351     */
352    private static boolean isCountable(DetailAST ast) {
353        boolean countable = true;
354
355        final int tokenType = ast.getType();
356
357        // check if an expression is countable
358        if (tokenType == TokenTypes.EXPR) {
359            countable = isExpressionCountable(ast);
360        }
361        // check if a variable definition is countable
362        else if (tokenType == TokenTypes.VARIABLE_DEF) {
363            countable = isVariableDefCountable(ast);
364        }
365        return countable;
366    }
367
368    /**
369     * Checks if a variable definition is countable.
370     *
371     * @param ast the AST
372     * @return true if the variable definition is countable, false otherwise
373     */
374    private static boolean isVariableDefCountable(DetailAST ast) {
375        boolean countable = false;
376
377        // count variable definitions only if they are direct child to a slist or
378        // object block
379        final int parentType = ast.getParent().getType();
380
381        if (parentType == TokenTypes.SLIST
382            || parentType == TokenTypes.OBJBLOCK) {
383            final DetailAST prevSibling = ast.getPreviousSibling();
384
385            // is countable if no previous sibling is found or
386            // the sibling is no COMMA.
387            // This is done because multiple assignment on one line are counted
388            // as 1
389            countable = prevSibling == null
390                    || prevSibling.getType() != TokenTypes.COMMA;
391        }
392
393        return countable;
394    }
395
396    /**
397     * Checks if an expression is countable for the ncss metric.
398     *
399     * @param ast the AST
400     * @return true if the expression is countable, false otherwise
401     */
402    private static boolean isExpressionCountable(DetailAST ast) {
403
404        // count expressions only if they are direct child to a slist (method
405        // body, for loop...)
406        // or direct child of label,if,else,do,while,for
407        final int parentType = ast.getParent().getType();
408        return switch (parentType) {
409            case TokenTypes.SLIST, TokenTypes.LABELED_STAT, TokenTypes.LITERAL_FOR,
410                 TokenTypes.LITERAL_DO,
411                 TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_IF, TokenTypes.LITERAL_ELSE -> {
412                // don't count if or loop conditions
413                final DetailAST prevSibling = ast.getPreviousSibling();
414                yield prevSibling == null
415                        || prevSibling.getType() != TokenTypes.LPAREN;
416            }
417            default -> false;
418        };
419    }
420
421    /**
422     * Checks if a token is a method, constructor, or compact constructor definition.
423     *
424     * @param tokenType the type of token we are checking
425     * @return true if token type is method or ctor definition, false otherwise
426     */
427    private static boolean isMethodOrCtorOrInitDefinition(int tokenType) {
428        return tokenType == TokenTypes.METHOD_DEF
429                || tokenType == TokenTypes.COMPACT_CTOR_DEF
430                || tokenType == TokenTypes.CTOR_DEF
431                || tokenType == TokenTypes.STATIC_INIT
432                || tokenType == TokenTypes.INSTANCE_INIT;
433    }
434
435    /**
436     * Class representing a counter.
437     *
438     */
439    private static final class Counter {
440
441        /** The counters internal integer. */
442        private int count;
443
444        /**
445         * Increments the counter.
446         */
447        /* package */ void increment() {
448            count++;
449        }
450
451        /**
452         * Gets the counters value.
453         *
454         * @return the counter
455         */
456        /* package */ int getCount() {
457            return count;
458        }
459
460    }
461
462}