View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.NPathComplexityCheck.MSG_KEY;
24  
25  import java.io.File;
26  import java.math.BigInteger;
27  import java.util.Collection;
28  import java.util.Optional;
29  import java.util.SortedSet;
30  
31  import org.antlr.v4.runtime.CommonToken;
32  import org.junit.jupiter.api.Test;
33  
34  import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
35  import com.puppycrawl.tools.checkstyle.DetailAstImpl;
36  import com.puppycrawl.tools.checkstyle.JavaParser;
37  import com.puppycrawl.tools.checkstyle.api.Context;
38  import com.puppycrawl.tools.checkstyle.api.DetailAST;
39  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
40  import com.puppycrawl.tools.checkstyle.api.Violation;
41  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
42  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
43  
44  // -@cs[AbbreviationAsWordInName] Can't change check name
45  public class NPathComplexityCheckTest extends AbstractModuleTestSupport {
46  
47      @Override
48      protected String getPackageLocation() {
49          return "com/puppycrawl/tools/checkstyle/checks/metrics/npathcomplexity";
50      }
51  
52      @Test
53      public void testCalculation() throws Exception {
54          final String[] expected = {
55              "12:5: " + getCheckMessage(MSG_KEY, 2, 0),
56              "17:17: " + getCheckMessage(MSG_KEY, 2, 0),
57              "29:5: " + getCheckMessage(MSG_KEY, 10, 0),
58              "42:5: " + getCheckMessage(MSG_KEY, 3, 0),
59              "52:5: " + getCheckMessage(MSG_KEY, 7, 0),
60              "70:5: " + getCheckMessage(MSG_KEY, 3, 0),
61              "83:5: " + getCheckMessage(MSG_KEY, 3, 0),
62              "95:5: " + getCheckMessage(MSG_KEY, 3, 0),
63              "111:13: " + getCheckMessage(MSG_KEY, 2, 0),
64              "120:5: " + getCheckMessage(MSG_KEY, 48, 0),
65              "130:5: " + getCheckMessage(MSG_KEY, 1, 0),
66              "131:5: " + getCheckMessage(MSG_KEY, 1, 0),
67              "137:17: " + getCheckMessage(MSG_KEY, 3, 0),
68              "151:21: " + getCheckMessage(MSG_KEY, 3, 0),
69          };
70  
71          verifyWithInlineConfigParser(
72                  getPath("InputNPathComplexityDefault.java"), expected);
73      }
74  
75      @Test
76      public void testCalculation2() throws Exception {
77          final String[] expected = {
78              "12:5: " + getCheckMessage(MSG_KEY, 5, 0),
79              "18:5: " + getCheckMessage(MSG_KEY, 5, 0),
80              "25:5: " + getCheckMessage(MSG_KEY, 4, 0),
81              "40:5: " + getCheckMessage(MSG_KEY, 4, 0),
82              "56:5: " + getCheckMessage(MSG_KEY, 6, 0),
83              "72:5: " + getCheckMessage(MSG_KEY, 15, 0),
84              "97:5: " + getCheckMessage(MSG_KEY, 11, 0),
85              "107:5: " + getCheckMessage(MSG_KEY, 8, 0),
86              "120:5: " + getCheckMessage(MSG_KEY, 120, 0),
87              "132:5: " + getCheckMessage(MSG_KEY, 6, 0),
88              "142:5: " + getCheckMessage(MSG_KEY, 21, 0),
89              "155:5: " + getCheckMessage(MSG_KEY, 35, 0),
90              "163:5: " + getCheckMessage(MSG_KEY, 25, 0),
91              "178:5: " + getCheckMessage(MSG_KEY, 2, 0),
92          };
93  
94          verifyWithInlineConfigParser(
95                  getPath("InputNPathComplexity.java"), expected);
96      }
97  
98      @Test
99      public void testCalculation3() throws Exception {
100         final String[] expected = {
101             "11:5: " + getCheckMessage(MSG_KEY, 64, 0),
102         };
103 
104         verifyWithInlineConfigParser(
105                 getNonCompilablePath("InputNPathComplexityDefault2.java"), expected);
106     }
107 
108     @Test
109     public void testIntegerOverflow() throws Exception {
110 
111         final long largerThanMaxInt = 3_486_784_401L;
112 
113         final String[] expected = {
114             "20:5: " + getCheckMessage(MSG_KEY, largerThanMaxInt, 0),
115         };
116 
117         verifyWithInlineConfigParser(
118                 getPath("InputNPathComplexityOverflow.java"), expected);
119     }
120 
121     @Test
122     @SuppressWarnings("unchecked")
123     public void testStatefulFieldsClearedOnBeginTree1() {
124         final DetailAstImpl ast = new DetailAstImpl();
125         ast.setType(TokenTypes.LITERAL_ELSE);
126 
127         final NPathComplexityCheck check = new NPathComplexityCheck();
128         assertWithMessage("Stateful field is not cleared after beginTree")
129                 .that(TestUtil.isStatefulFieldClearedDuringBeginTree(check, ast, "rangeValues",
130                         rangeValues -> ((Collection<Context>) rangeValues).isEmpty()))
131                 .isTrue();
132         assertWithMessage("Stateful field is not cleared after beginTree")
133                 .that(TestUtil.isStatefulFieldClearedDuringBeginTree(check, ast, "expressionValues",
134                         expressionValues -> ((Collection<Context>) expressionValues).isEmpty()))
135                 .isTrue();
136         assertWithMessage("Stateful field is not cleared after beginTree")
137                 .that(TestUtil.isStatefulFieldClearedDuringBeginTree(check, ast, "branchVisited",
138                         branchVisited -> !(boolean) branchVisited))
139                 .isTrue();
140         assertWithMessage("Stateful field is not cleared after beginTree")
141                 .that(TestUtil.isStatefulFieldClearedDuringBeginTree(check, ast,
142                         "currentRangeValue",
143                         currentRangeValue -> currentRangeValue.equals(BigInteger.ZERO)))
144                 .isTrue();
145     }
146 
147     @Test
148     @SuppressWarnings("unchecked")
149     public void testStatefulFieldsClearedOnBeginTree2() {
150         final DetailAstImpl ast = new DetailAstImpl();
151         ast.setType(TokenTypes.LITERAL_RETURN);
152         ast.setLineNo(5);
153         final DetailAstImpl child = new DetailAstImpl();
154         child.setType(TokenTypes.SEMI);
155         ast.addChild(child);
156 
157         final NPathComplexityCheck check = new NPathComplexityCheck();
158         assertWithMessage("Stateful field is not cleared after beginTree")
159                 .that(TestUtil.isStatefulFieldClearedDuringBeginTree(check, ast, "afterValues",
160                         isAfterValues -> ((Collection<Context>) isAfterValues).isEmpty()))
161                 .isTrue();
162     }
163 
164     @Test
165     public void testStatefulFieldsClearedOnBeginTree3() throws Exception {
166         final NPathComplexityCheck check = new NPathComplexityCheck();
167         final Optional<DetailAST> question = TestUtil.findTokenInAstByPredicate(
168             JavaParser.parseFile(new File(getPath("InputNPathComplexity.java")),
169                 JavaParser.Options.WITHOUT_COMMENTS),
170             ast -> ast.getType() == TokenTypes.QUESTION);
171 
172         assertWithMessage("Ast should contain QUESTION")
173                 .that(question.isPresent())
174                 .isTrue();
175 
176         assertWithMessage("State is not cleared on beginTree")
177                 .that(TestUtil.isStatefulFieldClearedDuringBeginTree(check, question.orElseThrow(),
178                         "processingTokenEnd", processingTokenEnd -> {
179                             return TestUtil.<Integer>getInternalState(processingTokenEnd,
180                                     "endLineNo") == 0
181                                     && TestUtil.<Integer>getInternalState(processingTokenEnd,
182                                             "endColumnNo") == 0;
183                         }))
184                 .isTrue();
185     }
186 
187     @Test
188     public void testDefaultConfiguration() throws Exception {
189         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
190         verifyWithInlineConfigParser(
191                 getPath("InputNPathComplexityDefault2.java"), expected);
192     }
193 
194     @Test
195     public void testNpathComplexityRecords() throws Exception {
196         final int max = 1;
197 
198         final String[] expected = {
199             "15:5: " + getCheckMessage(MSG_KEY, 3, max),
200             "25:9: " + getCheckMessage(MSG_KEY, 2, max),
201             "30:21: " + getCheckMessage(MSG_KEY, 2, max),
202             "44:9: " + getCheckMessage(MSG_KEY, 3, max),
203         };
204 
205         verifyWithInlineConfigParser(
206                 getNonCompilablePath("InputNPathComplexityRecords.java"), expected);
207     }
208 
209     @Test
210     public void testNpathComplexitySwitchExpression() throws Exception {
211 
212         final int max = 1;
213 
214         final String[] expected = {
215             "12:5: " + getCheckMessage(MSG_KEY, 5, max),
216             "29:5: " + getCheckMessage(MSG_KEY, 5, max),
217             "44:5: " + getCheckMessage(MSG_KEY, 6, max),
218             "60:5: " + getCheckMessage(MSG_KEY, 6, max),
219         };
220 
221         verifyWithInlineConfigParser(
222                 getNonCompilablePath("InputNPathComplexityCheckSwitchExpression.java"),
223             expected);
224     }
225 
226     @Test
227     public void testBranchVisited() throws Exception {
228 
229         final String[] expected = {
230             "13:3: " + getCheckMessage(MSG_KEY, 37, 20),
231         };
232 
233         verifyWithInlineConfigParser(
234                 getPath("InputNPathComplexityCheckBranchVisited.java"),
235             expected);
236     }
237 
238     @Test
239     public void testCount() throws Exception {
240 
241         final String[] expected = {
242             "11:5: " + getCheckMessage(MSG_KEY, 30, 20),
243             "22:5: " + getCheckMessage(MSG_KEY, 72, 20),
244             "67:5: " + getCheckMessage(MSG_KEY, 23, 20),
245         };
246 
247         verifyWithInlineConfigParser(
248                 getPath("InputNPathComplexityCheckCount.java"),
249             expected);
250     }
251 
252     @Test
253     public void testPatternMatchingForSwitch() throws Exception {
254 
255         final String[] expected = {
256             "14:5: " + getCheckMessage(MSG_KEY, 3, 1),
257             "23:5: " + getCheckMessage(MSG_KEY, 3, 1),
258             "32:5: " + getCheckMessage(MSG_KEY, 3, 1),
259             "41:5: " + getCheckMessage(MSG_KEY, 3, 1),
260             "50:5: " + getCheckMessage(MSG_KEY, 5, 1),
261             "59:5: " + getCheckMessage(MSG_KEY, 5, 1),
262             "68:5: " + getCheckMessage(MSG_KEY, 4, 1),
263             "76:5: " + getCheckMessage(MSG_KEY, 4, 1),
264             "86:5: " + getCheckMessage(MSG_KEY, 3, 1),
265             "95:5: " + getCheckMessage(MSG_KEY, 3, 1),
266         };
267 
268         verifyWithInlineConfigParser(
269                 getNonCompilablePath("InputNPathComplexityPatternMatchingForSwitch.java"),
270             expected);
271 
272     }
273 
274     @Test
275     public void testWhenExpression() throws Exception {
276 
277         final String[] expected = {
278             "14:5: " + getCheckMessage(MSG_KEY, 3, 1),
279             "20:5: " + getCheckMessage(MSG_KEY, 3, 1),
280             "28:5: " + getCheckMessage(MSG_KEY, 3, 1),
281             "36:5: " + getCheckMessage(MSG_KEY, 4, 1),
282             "44:5: " + getCheckMessage(MSG_KEY, 4, 1),
283             "52:5: " + getCheckMessage(MSG_KEY, 5, 1),
284             "60:5: " + getCheckMessage(MSG_KEY, 7, 1),
285             "69:5: " + getCheckMessage(MSG_KEY, 5, 1),
286             "77:5: " + getCheckMessage(MSG_KEY, 5, 1),
287             "85:5: " + getCheckMessage(MSG_KEY, 6, 1),
288         };
289 
290         verifyWithInlineConfigParser(
291                 getNonCompilablePath("InputNPathComplexityWhenExpression.java"),
292             expected);
293 
294     }
295 
296     @Test
297     public void testGetAcceptableTokens() {
298         final NPathComplexityCheck npathComplexityCheckObj = new NPathComplexityCheck();
299         final int[] actual = npathComplexityCheckObj.getAcceptableTokens();
300         final int[] expected = {
301             TokenTypes.CTOR_DEF,
302             TokenTypes.METHOD_DEF,
303             TokenTypes.STATIC_INIT,
304             TokenTypes.INSTANCE_INIT,
305             TokenTypes.LITERAL_WHILE,
306             TokenTypes.LITERAL_DO,
307             TokenTypes.LITERAL_FOR,
308             TokenTypes.LITERAL_IF,
309             TokenTypes.LITERAL_ELSE,
310             TokenTypes.LITERAL_SWITCH,
311             TokenTypes.CASE_GROUP,
312             TokenTypes.LITERAL_TRY,
313             TokenTypes.LITERAL_CATCH,
314             TokenTypes.QUESTION,
315             TokenTypes.LITERAL_RETURN,
316             TokenTypes.LITERAL_DEFAULT,
317             TokenTypes.COMPACT_CTOR_DEF,
318             TokenTypes.SWITCH_RULE,
319             TokenTypes.LITERAL_WHEN,
320         };
321         assertWithMessage("Acceptable tokens should not be null")
322             .that(actual)
323             .isNotNull();
324         assertWithMessage("Invalid acceptable tokens")
325             .that(actual)
326             .isEqualTo(expected);
327     }
328 
329     @Test
330     public void testGetRequiredTokens() {
331         final NPathComplexityCheck npathComplexityCheckObj = new NPathComplexityCheck();
332         final int[] actual = npathComplexityCheckObj.getRequiredTokens();
333         final int[] expected = {
334             TokenTypes.CTOR_DEF,
335             TokenTypes.METHOD_DEF,
336             TokenTypes.STATIC_INIT,
337             TokenTypes.INSTANCE_INIT,
338             TokenTypes.LITERAL_WHILE,
339             TokenTypes.LITERAL_DO,
340             TokenTypes.LITERAL_FOR,
341             TokenTypes.LITERAL_IF,
342             TokenTypes.LITERAL_ELSE,
343             TokenTypes.LITERAL_SWITCH,
344             TokenTypes.CASE_GROUP,
345             TokenTypes.LITERAL_TRY,
346             TokenTypes.LITERAL_CATCH,
347             TokenTypes.QUESTION,
348             TokenTypes.LITERAL_RETURN,
349             TokenTypes.LITERAL_DEFAULT,
350             TokenTypes.COMPACT_CTOR_DEF,
351             TokenTypes.SWITCH_RULE,
352             TokenTypes.LITERAL_WHEN,
353         };
354         assertWithMessage("Required tokens should not be null")
355             .that(actual)
356             .isNotNull();
357         assertWithMessage("Invalid required tokens")
358             .that(actual)
359             .isEqualTo(expected);
360     }
361 
362     @Test
363     public void testDefaultHooks() {
364         final NPathComplexityCheck npathComplexityCheckObj = new NPathComplexityCheck();
365         final DetailAstImpl ast = new DetailAstImpl();
366         ast.initialize(new CommonToken(TokenTypes.INTERFACE_DEF, "interface"));
367 
368         npathComplexityCheckObj.visitToken(ast);
369         final SortedSet<Violation> violations1 = npathComplexityCheckObj.getViolations();
370 
371         assertWithMessage("No exception violations expected")
372             .that(violations1)
373             .isEmpty();
374 
375         npathComplexityCheckObj.leaveToken(ast);
376         final SortedSet<Violation> violations2 = npathComplexityCheckObj.getViolations();
377 
378         assertWithMessage("No exception violations expected")
379             .that(violations2)
380             .isEmpty();
381     }
382 
383     /**
384      * This must be a reflection test as it is too difficult to hit normally and
385      * the responsible code can't be removed without failing tests.
386      * TokenEnd is only used for processingTokenEnd and it is only set during visitConditional
387      * and visitUnitaryOperator. For it to be the same line/column, it must be the exact same
388      * token or a token who has the same line/column as it's child and we visit. We never
389      * visit the same token twice and we are only visiting on very specific tokens.
390      * The line can't be removed or reworked as other tests fail, and regression shows us no
391      * use cases to create a UT for.
392      *
393      * @throws Exception if there is an error.
394      */
395     @Test
396     public void testTokenEndIsAfterSameLineColumn() throws Exception {
397         final NPathComplexityCheck check = new NPathComplexityCheck();
398         final Object tokenEnd = TestUtil.getInternalState(check, "processingTokenEnd");
399         final DetailAstImpl token = new DetailAstImpl();
400         token.setLineNo(0);
401         token.setColumnNo(0);
402 
403         assertWithMessage("isAfter must be true for same line/column")
404                 .that(TestUtil.<Boolean>invokeMethod(tokenEnd, "isAfter", token))
405                 .isTrue();
406     }
407 
408     @Test
409     public void testVisitTokenBeforeExpressionRange() {
410         // Create first ast
411         final DetailAstImpl astIf = mockAST(TokenTypes.LITERAL_IF, "if", 2, 2);
412         final DetailAstImpl astIfLeftParen = mockAST(TokenTypes.LPAREN, "(", 3, 3);
413         astIf.addChild(astIfLeftParen);
414         final DetailAstImpl astIfTrue =
415                 mockAST(TokenTypes.LITERAL_TRUE, "true", 3, 3);
416         astIf.addChild(astIfTrue);
417         final DetailAstImpl astIfRightParen = mockAST(TokenTypes.RPAREN, ")", 4, 4);
418         astIf.addChild(astIfRightParen);
419         // Create ternary ast
420         final DetailAstImpl astTernary = mockAST(TokenTypes.QUESTION, "?", 1, 1);
421         final DetailAstImpl astTernaryTrue =
422                 mockAST(TokenTypes.LITERAL_TRUE, "true", 1, 2);
423         astTernary.addChild(astTernaryTrue);
424 
425         final NPathComplexityCheck npathComplexityCheckObj = new NPathComplexityCheck();
426         npathComplexityCheckObj.beginTree(null);
427 
428         // visiting first ast, set expressionSpatialRange to [2,2 - 4,4]
429         npathComplexityCheckObj.visitToken(astIf);
430         final SortedSet<Violation> violations1 = npathComplexityCheckObj.getViolations();
431 
432         assertWithMessage("No exception violations expected")
433             .that(violations1)
434             .isEmpty();
435 
436         // visiting ternary, it lies before expressionSpatialRange
437         npathComplexityCheckObj.visitToken(astTernary);
438         final SortedSet<Violation> violations2 = npathComplexityCheckObj.getViolations();
439 
440         assertWithMessage("No exception violations expected")
441             .that(violations2)
442             .isEmpty();
443     }
444 
445     /**
446      * Creates MOCK lexical token and returns AST node for this token.
447      *
448      * @param tokenType type of token
449      * @param tokenText text of token
450      * @param tokenRow token position in a file (row)
451      * @param tokenColumn token position in a file (column)
452      * @return AST node for the token
453      */
454     private static DetailAstImpl mockAST(final int tokenType, final String tokenText,
455             final int tokenRow, final int tokenColumn) {
456         final CommonToken tokenImportSemi = new CommonToken(tokenType, tokenText);
457         tokenImportSemi.setLine(tokenRow);
458         tokenImportSemi.setCharPositionInLine(tokenColumn);
459         final DetailAstImpl astSemi = new DetailAstImpl();
460         astSemi.initialize(tokenImportSemi);
461         return astSemi;
462     }
463 
464 }