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