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