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