1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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.puppycrawl.tools.checkstyle.checks.metrics.NPathComplexityCheck.MSG_KEY;
23  
24  import java.io.File;
25  import java.util.Collection;
26  import java.util.Optional;
27  import java.util.SortedSet;
28  
29  import org.junit.Assert;
30  import org.junit.Test;
31  
32  import antlr.CommonHiddenStreamToken;
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.Context;
37  import com.puppycrawl.tools.checkstyle.api.DetailAST;
38  import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
39  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
40  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
41  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
42  
43  // -@cs[AbbreviationAsWordInName] Can't change check name
44  public class NPathComplexityCheckTest extends AbstractModuleTestSupport {
45  
46      @Override
47      protected String getPackageLocation() {
48          return "com/puppycrawl/tools/checkstyle/checks/metrics/npathcomplexity";
49      }
50  
51      @Test
52      public void testCalculation() throws Exception {
53          final DefaultConfiguration checkConfig =
54              createModuleConfig(NPathComplexityCheck.class);
55  
56          checkConfig.addAttribute("max", "0");
57          final String[] expected = {
58              "5:5: " + getCheckMessage(MSG_KEY, 2, 0),
59              "10:17: " + getCheckMessage(MSG_KEY, 2, 0),
60              "22:5: " + getCheckMessage(MSG_KEY, 10, 0),
61              "35:5: " + getCheckMessage(MSG_KEY, 3, 0),
62              "45:5: " + getCheckMessage(MSG_KEY, 7, 0),
63              "63:5: " + getCheckMessage(MSG_KEY, 3, 0),
64              "76:5: " + getCheckMessage(MSG_KEY, 3, 0),
65              "88:5: " + getCheckMessage(MSG_KEY, 3, 0),
66              "104:13: " + getCheckMessage(MSG_KEY, 2, 0),
67              "113:5: " + getCheckMessage(MSG_KEY, 48, 0),
68              "123:5: " + getCheckMessage(MSG_KEY, 1, 0),
69              "124:5: " + getCheckMessage(MSG_KEY, 1, 0),
70              "130:17: " + getCheckMessage(MSG_KEY, 3, 0),
71              "144:21: " + getCheckMessage(MSG_KEY, 3, 0),
72          };
73  
74          verify(checkConfig, getPath("InputNPathComplexityDefault.java"), expected);
75      }
76  
77      @Test
78      public void testCalculation2() throws Exception {
79          final DefaultConfiguration checkConfig =
80              createModuleConfig(NPathComplexityCheck.class);
81  
82          checkConfig.addAttribute("max", "0");
83          final String[] expected = {
84              "5:5: " + getCheckMessage(MSG_KEY, 5, 0),
85              "11:5: " + getCheckMessage(MSG_KEY, 5, 0),
86              "18:5: " + getCheckMessage(MSG_KEY, 4, 0),
87              "33:5: " + getCheckMessage(MSG_KEY, 4, 0),
88              "49:5: " + getCheckMessage(MSG_KEY, 6, 0),
89              "65:5: " + getCheckMessage(MSG_KEY, 15, 0),
90              "90:5: " + getCheckMessage(MSG_KEY, 11, 0),
91              "100:5: " + getCheckMessage(MSG_KEY, 8, 0),
92              "113:5: " + getCheckMessage(MSG_KEY, 120, 0),
93              "125:5: " + getCheckMessage(MSG_KEY, 6, 0),
94              "135:5: " + getCheckMessage(MSG_KEY, 21, 0),
95              "148:5: " + getCheckMessage(MSG_KEY, 35, 0),
96              "156:5: " + getCheckMessage(MSG_KEY, 25, 0),
97              "171:5: " + getCheckMessage(MSG_KEY, 2, 0),
98          };
99  
100         verify(checkConfig, getPath("InputNPathComplexity.java"), expected);
101     }
102 
103     @Test
104     public void testCalculation3() throws Exception {
105         final DefaultConfiguration checkConfig =
106             createModuleConfig(NPathComplexityCheck.class);
107 
108         checkConfig.addAttribute("max", "0");
109         final String[] expected = {
110             "4:5: " + getCheckMessage(MSG_KEY, 64, 0),
111         };
112 
113         verify(checkConfig, getNonCompilablePath("InputNPathComplexityDefault2.java"), expected);
114     }
115 
116     @Test
117     public void testIntegerOverflow() throws Exception {
118         final DefaultConfiguration checkConfig =
119             createModuleConfig(NPathComplexityCheck.class);
120 
121         checkConfig.addAttribute("max", "0");
122 
123         final long largerThanMaxInt = 3_486_784_401L;
124 
125         final String[] expected = {
126             "13:5: " + getCheckMessage(MSG_KEY, largerThanMaxInt, 0),
127         };
128 
129         verify(checkConfig, getPath("InputNPathComplexityOverflow.java"), expected);
130     }
131 
132     @Test
133     @SuppressWarnings("unchecked")
134     public void testStatefulFieldsClearedOnBeginTree1() throws Exception {
135         final DetailAST ast = new DetailAST();
136         ast.setType(TokenTypes.LITERAL_ELSE);
137 
138         final NPathComplexityCheck check = new NPathComplexityCheck();
139         Assert.assertTrue("Stateful field is not cleared after beginTree",
140             TestUtil.isStatefulFieldClearedDuringBeginTree(check, ast, "rangeValues",
141                 rangeValues -> ((Collection<Context>) rangeValues).isEmpty()));
142         Assert.assertTrue("Stateful field is not cleared after beginTree",
143             TestUtil.isStatefulFieldClearedDuringBeginTree(check, ast, "expressionValues",
144                 expressionValues -> ((Collection<Context>) expressionValues).isEmpty()));
145     }
146 
147     @Test
148     @SuppressWarnings("unchecked")
149     public void testStatefulFieldsClearedOnBeginTree2() throws Exception {
150         final DetailAST ast = new DetailAST();
151         ast.setType(TokenTypes.LITERAL_RETURN);
152         ast.setLineNo(5);
153         final DetailAST child = new DetailAST();
154         child.setType(TokenTypes.SEMI);
155         ast.addChild(child);
156 
157         final NPathComplexityCheck check = new NPathComplexityCheck();
158         Assert.assertTrue("Stateful field is not cleared after beginTree",
159             TestUtil.isStatefulFieldClearedDuringBeginTree(check, ast, "afterValues",
160                 isAfterValues -> ((Collection<Context>) isAfterValues).isEmpty()));
161     }
162 
163     @Test
164     public void testStatefulFieldsClearedOnBeginTree3() throws Exception {
165         final NPathComplexityCheck check = new NPathComplexityCheck();
166         final Optional<DetailAST> question = TestUtil.findTokenInAstByPredicate(
167             JavaParser.parseFile(new File(getPath("InputNPathComplexity.java")),
168                 JavaParser.Options.WITHOUT_COMMENTS),
169             ast -> ast.getType() == TokenTypes.QUESTION);
170 
171         Assert.assertTrue("Ast should contain QUESTION", question.isPresent());
172 
173         Assert.assertTrue("State is not cleared on beginTree",
174             TestUtil.isStatefulFieldClearedDuringBeginTree(
175                 check,
176                 question.get(),
177                 "processingTokenEnd",
178                 processingTokenEnd -> {
179                     try {
180                         return (Integer) TestUtil.getClassDeclaredField(
181                             processingTokenEnd.getClass(), "endLineNo").get(
182                             processingTokenEnd) == 0
183                             && (Integer) TestUtil.getClassDeclaredField(
184                                 processingTokenEnd.getClass(), "endColumnNo").get(
185                                 processingTokenEnd) == 0;
186                     }
187                     catch (IllegalAccessException | NoSuchFieldException ex) {
188                         throw new IllegalStateException(ex);
189                     }
190                 }));
191     }
192 
193     @Test
194     public void testDefaultConfiguration() throws Exception {
195         final DefaultConfiguration checkConfig =
196             createModuleConfig(NPathComplexityCheck.class);
197 
198         createChecker(checkConfig);
199         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
200         verify(checkConfig, getPath("InputNPathComplexityDefault.java"), expected);
201     }
202 
203     @Test
204     public void testGetAcceptableTokens() {
205         final NPathComplexityCheck npathComplexityCheckObj = new NPathComplexityCheck();
206         final int[] actual = npathComplexityCheckObj.getAcceptableTokens();
207         final int[] expected = {
208             TokenTypes.CTOR_DEF,
209             TokenTypes.METHOD_DEF,
210             TokenTypes.STATIC_INIT,
211             TokenTypes.INSTANCE_INIT,
212             TokenTypes.LITERAL_WHILE,
213             TokenTypes.LITERAL_DO,
214             TokenTypes.LITERAL_FOR,
215             TokenTypes.LITERAL_IF,
216             TokenTypes.LITERAL_ELSE,
217             TokenTypes.LITERAL_SWITCH,
218             TokenTypes.CASE_GROUP,
219             TokenTypes.LITERAL_TRY,
220             TokenTypes.LITERAL_CATCH,
221             TokenTypes.QUESTION,
222             TokenTypes.LITERAL_RETURN,
223             TokenTypes.LITERAL_DEFAULT,
224         };
225         Assert.assertNotNull("Acceptable tokens should not be null", actual);
226         Assert.assertArrayEquals("Invalid acceptable tokens", expected, actual);
227     }
228 
229     @Test
230     public void testGetRequiredTokens() {
231         final NPathComplexityCheck npathComplexityCheckObj = new NPathComplexityCheck();
232         final int[] actual = npathComplexityCheckObj.getRequiredTokens();
233         final int[] expected = {
234             TokenTypes.CTOR_DEF,
235             TokenTypes.METHOD_DEF,
236             TokenTypes.STATIC_INIT,
237             TokenTypes.INSTANCE_INIT,
238             TokenTypes.LITERAL_WHILE,
239             TokenTypes.LITERAL_DO,
240             TokenTypes.LITERAL_FOR,
241             TokenTypes.LITERAL_IF,
242             TokenTypes.LITERAL_ELSE,
243             TokenTypes.LITERAL_SWITCH,
244             TokenTypes.CASE_GROUP,
245             TokenTypes.LITERAL_TRY,
246             TokenTypes.LITERAL_CATCH,
247             TokenTypes.QUESTION,
248             TokenTypes.LITERAL_RETURN,
249             TokenTypes.LITERAL_DEFAULT,
250         };
251         Assert.assertNotNull("Required tokens should not be null", actual);
252         Assert.assertArrayEquals("Invalid required tokens", expected, actual);
253     }
254 
255     @Test
256     public void testDefaultHooks() {
257         final NPathComplexityCheck npathComplexityCheckObj = new NPathComplexityCheck();
258         final DetailAST ast = new DetailAST();
259         ast.initialize(new CommonHiddenStreamToken(TokenTypes.INTERFACE_DEF, "interface"));
260 
261         npathComplexityCheckObj.visitToken(ast);
262         final SortedSet<LocalizedMessage> messages1 = npathComplexityCheckObj.getMessages();
263 
264         Assert.assertEquals("No exception messages expected", 0, messages1.size());
265 
266         npathComplexityCheckObj.leaveToken(ast);
267         final SortedSet<LocalizedMessage> messages2 = npathComplexityCheckObj.getMessages();
268 
269         Assert.assertEquals("No exception messages expected", 0, messages2.size());
270     }
271 
272     /**
273      * This must be a reflection test as it is too difficult to hit normally and
274      * the responsible code can't be removed without failing tests.
275      * TokenEnd is only used for processingTokenEnd and it is only set during visitConditional
276      * and visitUnitaryOperator. For it to be the same line/column, it must be the exact same
277      * token or a token who has the same line/column as it's child and we visit. We never
278      * visit the same token twice and we are only visiting on very specific tokens.
279      * The line can't be removed or reworked as other tests fail, and regression shows us no
280      * use cases to create a UT for.
281      * @throws Exception if there is an error.
282      */
283     @Test
284     public void testTokenEndIsAfterSameLineColumn() throws Exception {
285         final NPathComplexityCheck check = new NPathComplexityCheck();
286         final Object tokenEnd = TestUtil.getClassDeclaredField(NPathComplexityCheck.class,
287                 "processingTokenEnd").get(check);
288         final DetailAST token = new DetailAST();
289         token.setLineNo(0);
290         token.setColumnNo(0);
291 
292         Assert.assertTrue("isAfter must be true for same line/column",
293                 (Boolean) TestUtil.getClassDeclaredMethod(tokenEnd.getClass(), "isAfter")
294                     .invoke(tokenEnd, token));
295     }
296 
297     @Test
298     public void testVisitTokenBeforeExpressionRange() {
299         // Create first ast
300         final DetailAST astIf = mockAST(TokenTypes.LITERAL_IF, "if", "mockfile", 2, 2);
301         final DetailAST astIfLeftParen = mockAST(TokenTypes.LPAREN, "(", "mockfile", 3, 3);
302         astIf.addChild(astIfLeftParen);
303         final DetailAST astIfTrue =
304                 mockAST(TokenTypes.LITERAL_TRUE, "true", "mockfile", 3, 3);
305         astIf.addChild(astIfTrue);
306         final DetailAST astIfRightParen = mockAST(TokenTypes.RPAREN, ")", "mockfile", 4, 4);
307         astIf.addChild(astIfRightParen);
308         // Create ternary ast
309         final DetailAST astTernary = mockAST(TokenTypes.QUESTION, "?", "mockfile", 1, 1);
310         final DetailAST astTernaryTrue =
311                 mockAST(TokenTypes.LITERAL_TRUE, "true", "mockfile", 1, 2);
312         astTernary.addChild(astTernaryTrue);
313 
314         final NPathComplexityCheck npathComplexityCheckObj = new NPathComplexityCheck();
315 
316         // visiting first ast, set expressionSpatialRange to [2,2 - 4,4]
317         npathComplexityCheckObj.visitToken(astIf);
318         final SortedSet<LocalizedMessage> messages1 = npathComplexityCheckObj.getMessages();
319 
320         Assert.assertEquals("No exception messages expected", 0, messages1.size());
321 
322         //visiting ternary, it lies before expressionSpatialRange
323         npathComplexityCheckObj.visitToken(astTernary);
324         final SortedSet<LocalizedMessage> messages2 = npathComplexityCheckObj.getMessages();
325 
326         Assert.assertEquals("No exception messages expected", 0, messages2.size());
327     }
328 
329     /**
330      * Creates MOCK lexical token and returns AST node for this token.
331      * @param tokenType type of token
332      * @param tokenText text of token
333      * @param tokenFileName file name of token
334      * @param tokenRow token position in a file (row)
335      * @param tokenColumn token position in a file (column)
336      * @return AST node for the token
337      */
338     private static DetailAST mockAST(final int tokenType, final String tokenText,
339             final String tokenFileName, final int tokenRow, final int tokenColumn) {
340         final CommonHiddenStreamToken tokenImportSemi = new CommonHiddenStreamToken();
341         tokenImportSemi.setType(tokenType);
342         tokenImportSemi.setText(tokenText);
343         tokenImportSemi.setLine(tokenRow);
344         tokenImportSemi.setColumn(tokenColumn);
345         tokenImportSemi.setFilename(tokenFileName);
346         final DetailAST astSemi = new DetailAST();
347         astSemi.initialize(tokenImportSemi);
348         return astSemi;
349     }
350 
351 }