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