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" file.
132     */
133    public static final String MSG_METHOD = "ncss.method";
134
135    /**
136     * A key is pointing to the warning message text in "messages.properties" file.
137     */
138    public static final String MSG_CLASS = "ncss.class";
139
140    /**
141     * A key is pointing to the warning message text in "messages.properties" file.
142     */
143    public static final String MSG_RECORD = "ncss.record";
144
145    /**
146     * A key is pointing to the warning message text in "messages.properties" file.
147     */
148    public static final String MSG_FILE = "ncss.file";
149
150    /** Default constant for max file ncss. */
151    private static final int FILE_MAX_NCSS = 2000;
152
153    /** Default constant for max file ncss. */
154    private static final int CLASS_MAX_NCSS = 1500;
155
156    /** Default constant for max record ncss. */
157    private static final int RECORD_MAX_NCSS = 150;
158
159    /** Default constant for max method ncss. */
160    private static final int METHOD_MAX_NCSS = 50;
161
162    /**
163     * Specify the maximum allowed number of non commenting lines in a file
164     * including all top level and nested classes.
165     */
166    private int fileMaximum = FILE_MAX_NCSS;
167
168    /** Specify the maximum allowed number of non commenting lines in a class. */
169    private int classMaximum = CLASS_MAX_NCSS;
170
171    /** Specify the maximum allowed number of non commenting lines in a record. */
172    private int recordMaximum = RECORD_MAX_NCSS;
173
174    /** Specify the maximum allowed number of non commenting lines in a method. */
175    private int methodMaximum = METHOD_MAX_NCSS;
176
177    /** List containing the stacked counters. */
178    private Deque<Counter> counters;
179
180    /**
181     * Creates a new {@code JavaNCSSCheck} instance.
182     */
183    public JavaNCSSCheck() {
184        // no code by default
185    }
186
187    @Override
188    public int[] getDefaultTokens() {
189        return getRequiredTokens();
190    }
191
192    @Override
193    public int[] getRequiredTokens() {
194        return new int[] {
195            TokenTypes.CLASS_DEF,
196            TokenTypes.INTERFACE_DEF,
197            TokenTypes.METHOD_DEF,
198            TokenTypes.CTOR_DEF,
199            TokenTypes.INSTANCE_INIT,
200            TokenTypes.STATIC_INIT,
201            TokenTypes.PACKAGE_DEF,
202            TokenTypes.IMPORT,
203            TokenTypes.VARIABLE_DEF,
204            TokenTypes.CTOR_CALL,
205            TokenTypes.SUPER_CTOR_CALL,
206            TokenTypes.LITERAL_IF,
207            TokenTypes.LITERAL_ELSE,
208            TokenTypes.LITERAL_WHILE,
209            TokenTypes.LITERAL_DO,
210            TokenTypes.LITERAL_FOR,
211            TokenTypes.LITERAL_SWITCH,
212            TokenTypes.LITERAL_BREAK,
213            TokenTypes.LITERAL_CONTINUE,
214            TokenTypes.LITERAL_RETURN,
215            TokenTypes.LITERAL_THROW,
216            TokenTypes.LITERAL_SYNCHRONIZED,
217            TokenTypes.LITERAL_CATCH,
218            TokenTypes.LITERAL_FINALLY,
219            TokenTypes.EXPR,
220            TokenTypes.LABELED_STAT,
221            TokenTypes.LITERAL_CASE,
222            TokenTypes.LITERAL_DEFAULT,
223            TokenTypes.RECORD_DEF,
224            TokenTypes.COMPACT_CTOR_DEF,
225        };
226    }
227
228    @Override
229    public int[] getAcceptableTokens() {
230        return getRequiredTokens();
231    }
232
233    @Override
234    public void beginTree(DetailAST rootAST) {
235        counters = new ArrayDeque<>();
236
237        // add a counter for the file
238        counters.push(new Counter());
239    }
240
241    @Override
242    public void visitToken(DetailAST ast) {
243        final int tokenType = ast.getType();
244
245        if (tokenType == TokenTypes.CLASS_DEF
246            || tokenType == TokenTypes.RECORD_DEF
247            || isMethodOrCtorOrInitDefinition(tokenType)) {
248            // add a counter for this class/method
249            counters.push(new Counter());
250        }
251
252        // check if token is countable
253        if (isCountable(ast)) {
254            // increment the stacked counters
255            counters.forEach(Counter::increment);
256        }
257    }
258
259    @Override
260    public void leaveToken(DetailAST ast) {
261        final int tokenType = ast.getType();
262
263        if (isMethodOrCtorOrInitDefinition(tokenType)) {
264            // pop counter from the stack
265            final Counter counter = counters.pop();
266
267            final int count = counter.getCount();
268            if (count > methodMaximum) {
269                log(ast, MSG_METHOD, count, methodMaximum);
270            }
271        }
272        else if (tokenType == TokenTypes.CLASS_DEF) {
273            // pop counter from the stack
274            final Counter counter = counters.pop();
275
276            final int count = counter.getCount();
277            if (count > classMaximum) {
278                log(ast, MSG_CLASS, count, classMaximum);
279            }
280        }
281        else if (tokenType == TokenTypes.RECORD_DEF) {
282            // pop counter from the stack
283            final Counter counter = counters.pop();
284
285            final int count = counter.getCount();
286            if (count > recordMaximum) {
287                log(ast, MSG_RECORD, count, recordMaximum);
288            }
289        }
290    }
291
292    @Override
293    public void finishTree(DetailAST rootAST) {
294        // pop counter from the stack
295        final Counter counter = counters.pop();
296
297        final int count = counter.getCount();
298        if (count > fileMaximum) {
299            log(rootAST, MSG_FILE, count, fileMaximum);
300        }
301    }
302
303    /**
304     * Setter to specify the maximum allowed number of non commenting lines
305     * in a file including all top level and nested classes.
306     *
307     * @param fileMaximum
308     *            the maximum ncss
309     * @since 3.5
310     */
311    public void setFileMaximum(int fileMaximum) {
312        this.fileMaximum = fileMaximum;
313    }
314
315    /**
316     * Setter to specify the maximum allowed number of non commenting lines in a class.
317     *
318     * @param classMaximum
319     *            the maximum ncss
320     * @since 3.5
321     */
322    public void setClassMaximum(int classMaximum) {
323        this.classMaximum = classMaximum;
324    }
325
326    /**
327     * Setter to specify the maximum allowed number of non commenting lines in a record.
328     *
329     * @param recordMaximum
330     *            the maximum ncss
331     * @since 8.36
332     */
333    public void setRecordMaximum(int recordMaximum) {
334        this.recordMaximum = recordMaximum;
335    }
336
337    /**
338     * Setter to specify the maximum allowed number of non commenting lines in a method.
339     *
340     * @param methodMaximum
341     *            the maximum ncss
342     * @since 3.5
343     */
344    public void setMethodMaximum(int methodMaximum) {
345        this.methodMaximum = methodMaximum;
346    }
347
348    /**
349     * Checks if a token is countable for the ncss metric.
350     *
351     * @param ast
352     *            the AST
353     * @return true if the token is countable
354     */
355    private static boolean isCountable(DetailAST ast) {
356        boolean countable = true;
357
358        final int tokenType = ast.getType();
359
360        // check if an expression is countable
361        if (tokenType == TokenTypes.EXPR) {
362            countable = isExpressionCountable(ast);
363        }
364        // check if a variable definition is countable
365        else if (tokenType == TokenTypes.VARIABLE_DEF) {
366            countable = isVariableDefCountable(ast);
367        }
368        return countable;
369    }
370
371    /**
372     * Checks if a variable definition is countable.
373     *
374     * @param ast the AST
375     * @return true if the variable definition is countable, false otherwise
376     */
377    private static boolean isVariableDefCountable(DetailAST ast) {
378        boolean countable = false;
379
380        // count variable definitions only if they are direct child to a slist or
381        // object block
382        final int parentType = ast.getParent().getType();
383
384        if (parentType == TokenTypes.SLIST
385            || parentType == TokenTypes.OBJBLOCK) {
386            final DetailAST prevSibling = ast.getPreviousSibling();
387
388            // is countable if no previous sibling is found or
389            // the sibling is no COMMA.
390            // This is done because multiple assignment on one line are counted
391            // as 1
392            countable = prevSibling == null
393                    || prevSibling.getType() != TokenTypes.COMMA;
394        }
395
396        return countable;
397    }
398
399    /**
400     * Checks if an expression is countable for the ncss metric.
401     *
402     * @param ast the AST
403     * @return true if the expression is countable, false otherwise
404     */
405    private static boolean isExpressionCountable(DetailAST ast) {
406
407        // count expressions only if they are direct child to a slist (method
408        // body, for loop...)
409        // or direct child of label,if,else,do,while,for
410        final int parentType = ast.getParent().getType();
411        return switch (parentType) {
412            case TokenTypes.SLIST, TokenTypes.LABELED_STAT, TokenTypes.LITERAL_FOR,
413                 TokenTypes.LITERAL_DO,
414                 TokenTypes.LITERAL_WHILE, TokenTypes.LITERAL_IF, TokenTypes.LITERAL_ELSE -> {
415                // don't count if or loop conditions
416                final DetailAST prevSibling = ast.getPreviousSibling();
417                yield prevSibling == null
418                        || prevSibling.getType() != TokenTypes.LPAREN;
419            }
420            default -> false;
421        };
422    }
423
424    /**
425     * Checks if a token is a method, constructor, or compact constructor definition.
426     *
427     * @param tokenType the type of token we are checking
428     * @return true if token type is method or ctor definition, false otherwise
429     */
430    private static boolean isMethodOrCtorOrInitDefinition(int tokenType) {
431        return tokenType == TokenTypes.METHOD_DEF
432                || tokenType == TokenTypes.COMPACT_CTOR_DEF
433                || tokenType == TokenTypes.CTOR_DEF
434                || tokenType == TokenTypes.STATIC_INIT
435                || tokenType == TokenTypes.INSTANCE_INIT;
436    }
437
438    /**
439     * Class representing a counter.
440     *
441     */
442    private static final class Counter {
443
444        /** The counters internal integer. */
445        private int count;
446
447        /**
448         * Creates a new {@code Counter} instance.
449         */
450        private Counter() {
451            // no code by default
452        }
453
454        /**
455         * Increments the counter.
456         */
457        /* package */ void increment() {
458            count++;
459        }
460
461        /**
462         * Gets the counters value.
463         *
464         * @return the counter
465         */
466        /* package */ int getCount() {
467            return count;
468        }
469
470    }
471
472}