View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.metrics;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  import static com.puppycrawl.tools.checkstyle.checks.metrics.ClassFanOutComplexityCheck.MSG_KEY;
24  import static com.puppycrawl.tools.checkstyle.internal.utils.TestUtil.getExpectedThrowable;
25  
26  import java.io.File;
27  import java.util.Collection;
28  import java.util.Map;
29  import java.util.Optional;
30  
31  import org.junit.jupiter.api.Test;
32  
33  import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
34  import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
35  import com.puppycrawl.tools.checkstyle.JavaParser;
36  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
37  import com.puppycrawl.tools.checkstyle.api.DetailAST;
38  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
39  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
40  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
41  
42  public class ClassFanOutComplexityCheckTest extends AbstractModuleTestSupport {
43  
44      @Override
45      public String getPackageLocation() {
46          return "com/puppycrawl/tools/checkstyle/checks/metrics/classfanoutcomplexity";
47      }
48  
49      @Test
50      public void test() throws Exception {
51  
52          final String[] expected = {
53              "27:1: " + getCheckMessage(MSG_KEY, 3, 0),
54              "59:1: " + getCheckMessage(MSG_KEY, 1, 0),
55          };
56  
57          verifyWithInlineConfigParser(
58                  getPath("InputClassFanOutComplexity.java"), expected);
59      }
60  
61      @Test
62      public void testExcludedPackagesDirectPackages() throws Exception {
63          final String[] expected = {
64              "29:1: " + getCheckMessage(MSG_KEY, 2, 0),
65          };
66  
67          verifyWithInlineConfigParser(
68                  getPath("InputClassFanOutComplexityExcludedPackagesDirectPackages.java"), expected);
69      }
70  
71      @Test
72      public void testExcludedPackagesCommonPackages() throws Exception {
73          final String[] expected = {
74              "28:1: " + getCheckMessage(MSG_KEY, 2, 0),
75              "32:5: " + getCheckMessage(MSG_KEY, 2, 0),
76              "38:1: " + getCheckMessage(MSG_KEY, 1, 0),
77          };
78          verifyWithInlineConfigParser(
79                  getPath("InputClassFanOutComplexityExcludedPackagesCommonPackage.java"), expected);
80      }
81  
82      @Test
83      public void testExcludedPackagesCommonPackagesWithEndingDot() {
84          final DefaultConfiguration checkConfig =
85              createModuleConfig(ClassFanOutComplexityCheck.class);
86  
87          checkConfig.addProperty("max", "0");
88          checkConfig.addProperty("excludedPackages",
89              "com.puppycrawl.tools.checkstyle.checks.metrics.inputs.a.");
90  
91          final CheckstyleException exc =
92                  getExpectedThrowable(CheckstyleException.class,
93                          () -> createChecker(checkConfig));
94          assertWithMessage("Invalid exception message")
95              .that(exc.getMessage())
96              .isEqualTo("cannot initialize module com.puppycrawl.tools.checkstyle.TreeWalker - "
97                  + "cannot initialize module com.puppycrawl.tools.checkstyle.checks."
98                  + "metrics.ClassFanOutComplexityCheck");
99          assertWithMessage("Invalid exception message,")
100             .that(exc.getCause().getCause().getCause().getCause().getMessage())
101             .isEqualTo("the following values are not valid identifiers: ["
102                         + "com.puppycrawl.tools.checkstyle.checks.metrics.inputs.a.]");
103     }
104 
105     @Test
106     public void testExcludedPackagesAllIgnored() throws Exception {
107         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
108         verifyWithInlineConfigParser(
109                 getPath("InputClassFanOutComplexityExcludedPackagesAllIgnored.java"), expected);
110     }
111 
112     @Test
113     public void test15() throws Exception {
114 
115         final String[] expected = {
116             "29:1: " + getCheckMessage(MSG_KEY, 1, 0),
117         };
118 
119         verifyWithInlineConfigParser(
120                 getPath("InputClassFanOutComplexity15Extensions.java"), expected);
121     }
122 
123     @Test
124     public void testDefaultConfiguration() throws Exception {
125         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
126         verifyWithInlineConfigParser(
127                 getPath("InputClassFanOutComplexity2.java"), expected);
128     }
129 
130     @Test
131     public void testGetAcceptableTokens() {
132         final ClassFanOutComplexityCheck classFanOutComplexityCheckObj =
133             new ClassFanOutComplexityCheck();
134         final int[] actual = classFanOutComplexityCheckObj.getAcceptableTokens();
135         final int[] expected = {
136             TokenTypes.PACKAGE_DEF,
137             TokenTypes.IMPORT,
138             TokenTypes.CLASS_DEF,
139             TokenTypes.EXTENDS_CLAUSE,
140             TokenTypes.IMPLEMENTS_CLAUSE,
141             TokenTypes.ANNOTATION,
142             TokenTypes.INTERFACE_DEF,
143             TokenTypes.ENUM_DEF,
144             TokenTypes.TYPE,
145             TokenTypes.LITERAL_NEW,
146             TokenTypes.LITERAL_THROWS,
147             TokenTypes.ANNOTATION_DEF,
148             TokenTypes.RECORD_DEF,
149         };
150         assertWithMessage("Acceptable tokens should not be null")
151             .that(actual)
152             .isNotNull();
153         assertWithMessage("Invalid acceptable tokens")
154             .that(actual)
155             .isEqualTo(expected);
156     }
157 
158     @Test
159     public void testRegularExpression() throws Exception {
160 
161         final String[] expected = {
162             "44:1: " + getCheckMessage(MSG_KEY, 2, 0),
163             "76:1: " + getCheckMessage(MSG_KEY, 1, 0),
164         };
165 
166         verifyWithInlineConfigParser(
167                 getPath("InputClassFanOutComplexity3.java"), expected);
168     }
169 
170     @Test
171     public void testEmptyRegularExpression() throws Exception {
172 
173         final String[] expected = {
174             "44:1: " + getCheckMessage(MSG_KEY, 3, 0),
175             "76:1: " + getCheckMessage(MSG_KEY, 1, 0),
176         };
177 
178         verifyWithInlineConfigParser(
179                 getPath("InputClassFanOutComplexity4.java"), expected);
180     }
181 
182     @Test
183     public void testWithMultiDimensionalArray() throws Exception {
184 
185         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
186         verifyWithInlineConfigParser(
187                 getPath("InputClassFanOutComplexityMultiDimensionalArray.java"), expected);
188     }
189 
190     @Test
191     public void testPackageName() throws Exception {
192 
193         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
194         verifyWithInlineConfigParser(
195                 getPath("InputClassFanOutComplexityPackageName.java"), expected);
196     }
197 
198     @Test
199     public void testExtends() throws Exception {
200         final String[] expected = {
201             "23:1: " + getCheckMessage(MSG_KEY, 1, 0),
202         };
203         verifyWithInlineConfigParser(
204                 getPath("InputClassFanOutComplexityExtends.java"), expected);
205     }
206 
207     @Test
208     public void testImplements() throws Exception {
209         final String[] expected = {
210             "23:1: " + getCheckMessage(MSG_KEY, 1, 0),
211         };
212         verifyWithInlineConfigParser(
213                 getPath("InputClassFanOutComplexityImplements.java"), expected);
214     }
215 
216     @Test
217     public void testAnnotation() throws Exception {
218         final String[] expected = {
219             "29:1: " + getCheckMessage(MSG_KEY, 2, 0),
220             "45:5: " + getCheckMessage(MSG_KEY, 2, 0),
221             "54:5: " + getCheckMessage(MSG_KEY, 3, 0),
222             "64:5: " + getCheckMessage(MSG_KEY, 2, 0),
223             "79:1: " + getCheckMessage(MSG_KEY, 1, 0),
224             "99:1: " + getCheckMessage(MSG_KEY, 1, 0),
225             "102:1: " + getCheckMessage(MSG_KEY, 1, 0),
226         };
227         verifyWithInlineConfigParser(
228                 getPath("InputClassFanOutComplexityAnnotations.java"), expected);
229     }
230 
231     @Test
232     public void testImplementsAndNestedCount() throws Exception {
233         final String[] expected = {
234             "26:1: " + getCheckMessage(MSG_KEY, 3, 0),
235         };
236         verifyWithInlineConfigParser(
237                 getPath("InputClassFanOutComplexityImplementsAndNestedCount.java"), expected);
238     }
239 
240     @Test
241     public void testClassFanOutComplexityRecords() throws Exception {
242         final String[] expected = {
243             "32:1: " + getCheckMessage(MSG_KEY, 4, 2),
244             "53:1: " + getCheckMessage(MSG_KEY, 4, 2),
245         };
246         verifyWithInlineConfigParser(
247                 getPath("InputClassFanOutComplexityRecords.java"), expected);
248     }
249 
250     @Test
251     public void testClassFanOutComplexityIgnoreVar() throws Exception {
252         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
253         verifyWithInlineConfigParser(
254                 getPath("InputClassFanOutComplexityVar.java"), expected);
255     }
256 
257     @Test
258     public void testClassFanOutComplexityRemoveIncorrectAnnotationToken() throws Exception {
259         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
260         verifyWithInlineConfigParser(
261                 getNonCompilablePath(
262                   "InputClassFanOutComplexityRemoveIncorrectAnnotationToken.java"), expected);
263     }
264 
265     @Test
266     public void testClassFanOutComplexityRemoveIncorrectTypeParameter() throws Exception {
267         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
268         verifyWithInlineConfigParser(
269                 getPath("InputClassFanOutComplexityRemoveIncorrectTypeParameter.java"), expected);
270     }
271 
272     @Test
273     public void testClassFanOutComplexityMultiCatchBitwiseOr() throws Exception {
274         final String[] expected = {
275             "27:1: " + getCheckMessage(MSG_KEY, 5, 4),
276         };
277 
278         verifyWithInlineConfigParser(
279                 getPath("InputClassFanOutComplexityMultiCatchBitwiseOr.java"), expected);
280     }
281 
282     @Test
283     public void testThrows() throws Exception {
284         final String[] expected = {
285             "25:1: " + getCheckMessage(MSG_KEY, 2, 0),
286         };
287 
288         verifyWithInlineConfigParser(
289                 getPath("InputClassFanOutComplexityThrows.java"), expected);
290     }
291 
292     @Test
293     public void testSealedClasses() throws Exception {
294         final String[] expected = {
295             "25:1: " + getCheckMessage(MSG_KEY, 2, 0),
296             "32:1: " + getCheckMessage(MSG_KEY, 1, 0),
297         };
298 
299         verifyWithInlineConfigParser(
300                 getPath("InputClassFanOutComplexitySealedClasses.java"),
301                 expected);
302     }
303 
304     /**
305      * We cannot reproduce situation when visitToken is called and leaveToken is not.
306      * So, we have to use reflection to be sure that even in such situation
307      * state of the field will be cleared.
308      *
309      * @throws Exception when code tested throws exception
310      */
311     @SuppressWarnings("unchecked")
312     @Test
313     public void testClearStateImportedClassPackages() throws Exception {
314         final ClassFanOutComplexityCheck check = new ClassFanOutComplexityCheck();
315         final DetailAST root = JavaParser.parseFile(
316                 new File(getPath("InputClassFanOutComplexity.java")),
317                 JavaParser.Options.WITHOUT_COMMENTS);
318         final Optional<DetailAST> importAst = TestUtil.findTokenInAstByPredicate(root,
319             ast -> ast.getType() == TokenTypes.IMPORT);
320 
321         assertWithMessage("Ast should contain IMPORT")
322                 .that(importAst.isPresent())
323                 .isTrue();
324         assertWithMessage("State is not cleared on beginTree")
325                 .that(TestUtil.isStatefulFieldClearedDuringBeginTree(check,
326                     importAst.orElseThrow(), "importedClassPackages",
327                     importedClssPackage -> ((Map<String, String>) importedClssPackage).isEmpty()))
328                 .isTrue();
329     }
330 
331     /**
332      * We cannot reproduce situation when visitToken is called and leaveToken is not.
333      * So, we have to use reflection to be sure that even in such situation
334      * state of the field will be cleared.
335      *
336      * @throws Exception when code tested throws exception
337      */
338     @Test
339     public void testClearStateClassContexts() throws Exception {
340         final ClassFanOutComplexityCheck check = new ClassFanOutComplexityCheck();
341         final DetailAST root = JavaParser.parseFile(
342                 new File(getPath("InputClassFanOutComplexity.java")),
343                 JavaParser.Options.WITHOUT_COMMENTS);
344         final Optional<DetailAST> classDef = TestUtil.findTokenInAstByPredicate(root,
345             ast -> ast.getType() == TokenTypes.CLASS_DEF);
346 
347         assertWithMessage("Ast should contain CLASS_DEF")
348                 .that(classDef.isPresent())
349                 .isTrue();
350         assertWithMessage("State is not cleared on beginTree")
351                 .that(TestUtil.isStatefulFieldClearedDuringBeginTree(check, classDef.orElseThrow(),
352                         "classesContexts",
353                         classContexts -> ((Collection<?>) classContexts).size() == 1))
354                 .isTrue();
355     }
356 
357     /**
358      * We cannot reproduce situation when visitToken is called and leaveToken is not.
359      * So, we have to use reflection to be sure that even in such situation
360      * state of the field will be cleared.
361      *
362      * @throws Exception when code tested throws exception
363      */
364     @Test
365     public void testClearStatePackageName() throws Exception {
366         final ClassFanOutComplexityCheck check = new ClassFanOutComplexityCheck();
367         final DetailAST root = JavaParser.parseFile(
368                 new File(getPath("InputClassFanOutComplexity.java")),
369                 JavaParser.Options.WITHOUT_COMMENTS);
370         final Optional<DetailAST> packageDef = TestUtil.findTokenInAstByPredicate(root,
371             ast -> ast.getType() == TokenTypes.PACKAGE_DEF);
372 
373         assertWithMessage("Ast should contain PACKAGE_DEF")
374                 .that(packageDef.isPresent())
375                 .isTrue();
376         assertWithMessage("State is not cleared on beginTree")
377                 .that(TestUtil.isStatefulFieldClearedDuringBeginTree(check,
378                         packageDef.orElseThrow(), "packageName",
379                         packageName -> ((CharSequence) packageName).isEmpty()))
380                 .isTrue();
381     }
382 
383     /**
384      * Test [ClassName]::new expression. Such expression will be processed as a
385      * normal declaration of a new instance. We need to make sure this does not
386      *
387      * @throws Exception when code tested throws exception
388      */
389     @Test
390     public void testLambdaNew() throws Exception {
391         final String[] expected = {
392             "29:1: " + getCheckMessage(MSG_KEY, 2, 0),
393         };
394         verifyWithInlineConfigParser(
395                 getPath("InputClassFanOutComplexityLambdaNew.java"), expected);
396     }
397 
398 }