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;
21  
22  import static com.google.common.truth.Truth.assertWithMessage;
23  import static com.puppycrawl.tools.checkstyle.checks.naming.AbstractNameCheck.MSG_INVALID_PATTERN;
24  import static com.puppycrawl.tools.checkstyle.internal.utils.TestUtil.getExpectedThrowable;
25  import static org.mockito.ArgumentMatchers.any;
26  import static org.mockito.Mockito.CALLS_REAL_METHODS;
27  import static org.mockito.Mockito.doThrow;
28  import static org.mockito.Mockito.mock;
29  
30  import java.io.ByteArrayOutputStream;
31  import java.io.File;
32  import java.io.Writer;
33  import java.nio.charset.StandardCharsets;
34  import java.nio.file.Files;
35  import java.nio.file.StandardCopyOption;
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.Collection;
39  import java.util.Collections;
40  import java.util.HashMap;
41  import java.util.HashSet;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.Objects;
45  import java.util.Set;
46  import java.util.UUID;
47  import java.util.regex.Matcher;
48  import java.util.regex.Pattern;
49  
50  import org.junit.jupiter.api.Test;
51  import org.junit.jupiter.api.io.TempDir;
52  import org.mockito.MockedConstruction;
53  import org.mockito.MockedStatic;
54  import org.mockito.Mockito;
55  import org.mockito.internal.util.Checks;
56  
57  import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
58  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
59  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
60  import com.puppycrawl.tools.checkstyle.api.Configuration;
61  import com.puppycrawl.tools.checkstyle.api.Context;
62  import com.puppycrawl.tools.checkstyle.api.DetailAST;
63  import com.puppycrawl.tools.checkstyle.api.FileContents;
64  import com.puppycrawl.tools.checkstyle.api.FileText;
65  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
66  import com.puppycrawl.tools.checkstyle.checks.NoCodeInFileCheck;
67  import com.puppycrawl.tools.checkstyle.checks.coding.EmptyStatementCheck;
68  import com.puppycrawl.tools.checkstyle.checks.coding.HiddenFieldCheck;
69  import com.puppycrawl.tools.checkstyle.checks.design.OneTopLevelClassCheck;
70  import com.puppycrawl.tools.checkstyle.checks.indentation.CommentsIndentationCheck;
71  import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocPackageCheck;
72  import com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocParagraphCheck;
73  import com.puppycrawl.tools.checkstyle.checks.naming.ConstantNameCheck;
74  import com.puppycrawl.tools.checkstyle.checks.naming.MemberNameCheck;
75  import com.puppycrawl.tools.checkstyle.checks.naming.ParameterNameCheck;
76  import com.puppycrawl.tools.checkstyle.checks.naming.TypeNameCheck;
77  import com.puppycrawl.tools.checkstyle.checks.whitespace.WhitespaceAfterCheck;
78  import com.puppycrawl.tools.checkstyle.checks.whitespace.WhitespaceAroundCheck;
79  import com.puppycrawl.tools.checkstyle.filters.SuppressWithNearbyCommentFilter;
80  import com.puppycrawl.tools.checkstyle.filters.SuppressionXpathFilter;
81  import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
82  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
83  
84  /**
85   * TreeWalkerTest.
86   *
87   * @noinspection ClassWithTooManyDependencies because we are less strict with tests.
88   * @noinspectionreason ClassWithTooManyDependencies - complex tests require a
89   *      large number of imports
90   */
91  public class TreeWalkerTest extends AbstractModuleTestSupport {
92  
93      @TempDir
94      public File temporaryFolder;
95  
96      @Override
97      public String getPackageLocation() {
98          return "com/puppycrawl/tools/checkstyle/treewalker";
99      }
100 
101     @Test
102     public void testProperFileExtension() throws Exception {
103         final String path = getPath("InputTreeWalkerProperFileExtension.java");
104         final String[] expected = {
105             "15:27: " + getCheckMessage(ConstantNameCheck.class,
106                         MSG_INVALID_PATTERN, "k", "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$"),
107         };
108         verifyWithInlineConfigParserTwice(path, expected);
109     }
110 
111     /**
112      * This test is needed for 100% coverage.
113      * The Pitest reports some conditions as redundant, for example:
114      * <pre>
115      *     if (!collection.isEmpty()) { // This may be omitted.
116      *         Object value = doSomeHardJob();
117      *         for (Item item : collection) {
118      *             item.accept(value);
119      *         }
120      *     }
121      * </pre>
122      * But we really want to avoid calls to {@code doSomeHardJob} method.
123      * To make this condition mandatory, we need to broke one branch.
124      * In this case, mocking {@code TreeWalkerAuditEvent} will cause
125      * {@code getFilteredViolations} to fail. This prevents the condition
126      * <pre>
127      *     if (filters.isEmpty())
128      * </pre>
129      * in {@link TreeWalker#processFiltered(File, FileText)} to survive with Pitest mutations.
130      *
131      * @throws Exception if an error occurs
132      */
133     @Test
134     public void testNoAuditEventsWithoutFilters() throws Exception {
135         final String[] expected = {
136             "10:1: " + getCheckMessage(OneTopLevelClassCheck.class,
137                     OneTopLevelClassCheck.MSG_KEY, "InputTreeWalkerInner"),
138         };
139         try (MockedConstruction<TreeWalkerAuditEvent> mocked =
140                  Mockito.mockConstruction(TreeWalkerAuditEvent.class, (mock, context) -> {
141                      throw new CheckstyleException("No audit events expected");
142                  })) {
143             verifyWithInlineConfigParserTwice(getPath("InputTreeWalker.java"), expected);
144         }
145     }
146 
147     /**
148      * This test is needed for 100% coverage. The method {@link Mockito#mockStatic} is used to
149      * ensure that the {@code if (!ordinaryChecks.isEmpty())} condition cannot be removed.
150      */
151     @Test
152     public void testConditionRequiredWithoutOrdinaryChecks() throws Exception {
153         final String[] expected = {
154             "10:5: " + getCheckMessage(JavadocParagraphCheck.class,
155                     JavadocParagraphCheck.MSG_REDUNDANT_PARAGRAPH),
156         };
157         final String path = getPath("InputTreeWalkerJavadoc.java");
158         final DetailAST mockAst = mock();
159         final DetailAST realAst = JavaParser.parseFile(new File(path),
160                 JavaParser.Options.WITH_COMMENTS);
161         // Ensure that there is no calls to walk(..., AstState.ORDINARY)
162         doThrow(IllegalStateException.class).when(mockAst).getFirstChild();
163         try (MockedStatic<JavaParser> parser = Mockito.mockStatic(JavaParser.class)) {
164             parser.when(() -> JavaParser.parse(any(FileContents.class))).thenReturn(mockAst);
165             // This will re-enable walk(..., AstState.WITH_COMMENTS)
166             parser.when(() -> JavaParser.appendHiddenCommentNodes(mockAst)).thenReturn(realAst);
167 
168             verifyWithInlineConfigParserTwice(path, expected);
169         }
170     }
171 
172     /**
173      * This test is needed for 100% coverage. The method {@link Mockito#mockStatic} is used to
174      * ensure that the {@code if (!commentChecks.isEmpty())} condition cannot be removed.
175      */
176     @Test
177     public void testConditionRequiredWithoutCommentChecks() throws Exception {
178         final String[] expected = {
179             "10:1: " + getCheckMessage(OneTopLevelClassCheck.class,
180                     OneTopLevelClassCheck.MSG_KEY, "InputTreeWalkerInner"),
181         };
182         try (MockedStatic<JavaParser> parser =
183                      Mockito.mockStatic(JavaParser.class, CALLS_REAL_METHODS)) {
184             // Ensure that there is no calls to walk(..., AstState.WITH_COMMENTS)
185             parser.when(() -> JavaParser.appendHiddenCommentNodes(any(DetailAST.class)))
186                     .thenThrow(IllegalStateException.class);
187 
188             verifyWithInlineConfigParserTwice(getPath("InputTreeWalker.java"), expected);
189         }
190     }
191 
192     @Test
193     public void testImproperFileExtension() throws Exception {
194         final String regularFilePath = getPath("InputTreeWalkerImproperFileExtension.java");
195         final File originalFile = new File(regularFilePath);
196         final File tempFile = new File(temporaryFolder, "file.pdf");
197         Files.copy(originalFile.toPath(), tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
198         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
199         verifyWithInlineConfigParserTwice(tempFile.getPath(), expected);
200     }
201 
202     @Test
203     public void testAcceptableTokens() {
204         final DefaultConfiguration checkConfig =
205             createModuleConfig(HiddenFieldCheck.class);
206         checkConfig.addProperty("tokens", "VARIABLE_DEF, ENUM_DEF, CLASS_DEF, METHOD_DEF,"
207                 + "IMPORT");
208         final CheckstyleException exc =
209                 getExpectedThrowable(CheckstyleException.class, () -> {
210                     execute(checkConfig, getPath("InputTreeWalker.java"));
211                 }, "CheckstyleException is expected");
212         final String errorMsg = exc.getMessage();
213         final Pattern expected = Pattern.compile(Pattern.quote("cannot initialize module"
214                 + " com.puppycrawl.tools.checkstyle.TreeWalker - Token ")
215                 + "\"(ENUM_DEF|CLASS_DEF|METHOD_DEF|IMPORT)\""
216                 + Pattern.quote(" was not found in Acceptable tokens list in check"
217                 + " com.puppycrawl.tools.checkstyle.checks.coding.HiddenFieldCheck"));
218         final Matcher errorMsgMatcher = expected.matcher(errorMsg);
219         assertWithMessage("Failure for: %s", errorMsg)
220             .that(errorMsgMatcher.matches())
221             .isTrue();
222     }
223 
224     @Test
225     public void testOnEmptyFile() throws Exception {
226         final DefaultConfiguration checkConfig = createModuleConfig(HiddenFieldCheck.class);
227         final String uniqueFileName = "file_" + UUID.randomUUID() + ".java";
228         final File emptyFile = new File(temporaryFolder, uniqueFileName);
229         emptyFile.createNewFile();
230         execute(checkConfig, emptyFile.getPath());
231         final long fileSize = Files.size(emptyFile.toPath());
232         assertWithMessage("File should be empty")
233                 .that(fileSize)
234                 .isEqualTo(0);
235     }
236 
237     @Test
238     public void testWithCheckNotHavingTreeWalkerAsParent() {
239         final DefaultConfiguration checkConfig = createModuleConfig(JavadocPackageCheck.class);
240 
241         final String uniqueFileName = "junit_" + UUID.randomUUID() + ".java";
242         final File filePath = new File(temporaryFolder, uniqueFileName);
243         final CheckstyleException exception =
244                 getExpectedThrowable(CheckstyleException.class, () -> {
245                     execute(createTreeWalkerConfig(checkConfig), filePath.toString());
246                 }, "CheckstyleException is expected");
247         assertWithMessage("Error message is unexpected")
248             .that(exception.getMessage())
249             .contains("TreeWalker is not allowed as a parent of");
250     }
251 
252     @Test
253     public void testSetupChildExceptions() {
254         final TreeWalker treeWalker = new TreeWalker();
255         final PackageObjectFactory factory = new PackageObjectFactory(
256                 new HashSet<>(), Thread.currentThread().getContextClassLoader());
257         treeWalker.setModuleFactory(factory);
258 
259         final Configuration config = new DefaultConfiguration("java.lang.String");
260         final CheckstyleException exc =
261                 getExpectedThrowable(CheckstyleException.class, () -> {
262                     treeWalker.setupChild(config);
263                 }, "Exception is expected");
264         assertWithMessage("Error message is not expected")
265             .that(exc.getMessage())
266             .isEqualTo("TreeWalker is not allowed as a parent of java.lang.String "
267                 + "Please review 'Parent Module' section for this Check in "
268                 + "web documentation if Check is standard.");
269     }
270 
271     @Test
272     public void testSettersForParameters() throws Exception {
273         final TreeWalker treeWalker = new TreeWalker();
274         final DefaultConfiguration config = new DefaultConfiguration("default config");
275         treeWalker.setTabWidth(1);
276         treeWalker.configure(config);
277 
278         final int tabWidth = TestUtil.getInternalState(treeWalker, "tabWidth", Integer.class);
279         assertWithMessage("Invalid setter result")
280             .that(tabWidth)
281             .isEqualTo(1);
282         final Object configuration = TestUtil.getInternalState(treeWalker, "configuration",
283                 Object.class);
284         assertWithMessage("Invalid configuration")
285             .that(configuration)
286             .isEqualTo(config);
287     }
288 
289     @Test
290     public void testForInvalidCheckImplementation() {
291         final DefaultConfiguration checkConfig = createModuleConfig(BadJavaDocCheck.class);
292         final String uniqueFileName = "file_" + UUID.randomUUID() + ".java";
293         final File pathToEmptyFile = new File(temporaryFolder, uniqueFileName);
294 
295         final CheckstyleException exc =
296                 getExpectedThrowable(CheckstyleException.class, () -> {
297                     execute(checkConfig, pathToEmptyFile.toString());
298                 }, "Exception is expected");
299         assertWithMessage("Error message is unexpected")
300             .that(exc.getMessage())
301             .isEqualTo("cannot initialize module com.puppycrawl.tools.checkstyle."
302                     + "TreeWalker - Check 'com.puppycrawl.tools.checkstyle."
303                     + "TreeWalkerTest$BadJavaDocCheck' waits for comment type token "
304                     + "('SINGLE_LINE_COMMENT') and should override "
305                     + "'isCommentNodesRequired()' method to return 'true'");
306         assertWithMessage("Error message is unexpected")
307             .that(exc.getMessage())
308             .contains("isCommentNodesRequired");
309     }
310 
311     @Test
312     public void testProcessNonJavaFiles() throws Exception {
313         final TreeWalker treeWalker = new TreeWalker();
314         final PackageObjectFactory factory = new PackageObjectFactory(
315             new HashSet<>(), Thread.currentThread().getContextClassLoader());
316         treeWalker.setModuleFactory(factory);
317         treeWalker.configure(new DefaultConfiguration("default config"));
318         final DefaultConfiguration childConfig = createModuleConfig(JavadocParagraphCheck.class);
319         treeWalker.setupChild(childConfig);
320         final File file = new File("input.java");
321         final List<String> lines =
322             new ArrayList<>(Arrays.asList("package com.puppycrawl.tools.checkstyle;", "",
323                 "error public class InputTreeWalkerFileWithViolation {}"));
324         final FileText fileText = new FileText(file, lines);
325         treeWalker.setFileContents(new FileContents(fileText));
326         final CheckstyleException exc =
327                 getExpectedThrowable(CheckstyleException.class, () -> {
328                     treeWalker.processFiltered(file, fileText);
329                 }, "Exception expected");
330         assertWithMessage("Invalid exception message")
331             .that(exc.getMessage())
332             .isEqualTo("IllegalStateException occurred while parsing file input.java.");
333     }
334 
335     @Test
336     public void testProcessNonJavaFilesWithoutException() throws Exception {
337         final TreeWalker treeWalker = new TreeWalker();
338         treeWalker.setTabWidth(1);
339         treeWalker.configure(new DefaultConfiguration("default config"));
340         final File file = new File(getPath("InputTreeWalkerNotJava.xml"));
341         final FileText fileText = new FileText(file, StandardCharsets.ISO_8859_1.name());
342         treeWalker.processFiltered(file, fileText);
343         final Collection<Checks> checks =
344                 TestUtil.getInternalStateCollectionChecks(treeWalker, "ordinaryChecks");
345         assertWithMessage("No checks -> No parsing")
346             .that(checks)
347             .isEmpty();
348     }
349 
350     @Test
351     public void testWithCacheWithNoViolation() throws Exception {
352         final String path = getPath("InputTreeWalkerWithCacheWithNoViolation.java");
353         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
354         verifyWithInlineConfigParserTwice(path, expected);
355     }
356 
357     @Test
358     public void testProcessWithParserThrowable() throws Exception {
359         final TreeWalker treeWalker = new TreeWalker();
360         treeWalker.configure(createModuleConfig(TypeNameCheck.class));
361         final PackageObjectFactory factory = new PackageObjectFactory(
362             new HashSet<>(), Thread.currentThread().getContextClassLoader());
363         treeWalker.setModuleFactory(factory);
364         treeWalker.setupChild(createModuleConfig(TypeNameCheck.class));
365         final File file = new File(temporaryFolder, "file.java");
366         final List<String> lines = new ArrayList<>();
367         lines.add(" classD a {} ");
368         final FileText fileText = new FileText(file, lines);
369         treeWalker.setFileContents(new FileContents(fileText));
370         final CheckstyleException exception =
371                 getExpectedThrowable(CheckstyleException.class, () -> {
372                     treeWalker.processFiltered(file, fileText);
373                 }, "Exception is expected");
374         assertWithMessage("Error message is unexpected")
375             .that(exception.getMessage())
376             .contains("occurred while parsing file");
377     }
378 
379     @Test
380     public void testProcessWithRecognitionException() throws Exception {
381         final TreeWalker treeWalker = new TreeWalker();
382         treeWalker.configure(createModuleConfig(TypeNameCheck.class));
383         final PackageObjectFactory factory = new PackageObjectFactory(
384             new HashSet<>(), Thread.currentThread().getContextClassLoader());
385         treeWalker.setModuleFactory(factory);
386         treeWalker.setupChild(createModuleConfig(TypeNameCheck.class));
387         final File file = new File(temporaryFolder, "file.java");
388         final List<String> lines = new ArrayList<>();
389         lines.add(" class a%$# {} ");
390         final FileText fileText = new FileText(file, lines);
391         treeWalker.setFileContents(new FileContents(fileText));
392         final CheckstyleException exception =
393                 getExpectedThrowable(CheckstyleException.class, () -> {
394                     treeWalker.processFiltered(file, fileText);
395                 }, "Exception is expected");
396         assertWithMessage("Error message is unexpected")
397             .that(exception.getMessage())
398             .contains("IllegalStateException occurred while parsing file");
399     }
400 
401     @Test
402     public void testRequiredTokenIsEmptyIntArray() throws Exception {
403         final File file = new File(temporaryFolder, "file.java");
404         try (Writer writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) {
405             final String configComment =
406                     """
407                     /*
408                     com.puppycrawl.tools.checkstyle.TreeWalkerTest\
409                     $RequiredTokenIsEmptyIntArray
410 
411                     */
412                     """;
413             writer.write(configComment);
414         }
415         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
416         verifyWithInlineConfigParserTwice(file.getPath(), expected);
417     }
418 
419     @Test
420     public void testBehaviourWithZeroChecks() throws Exception {
421         final TreeWalker treeWalker = new TreeWalker();
422         final PackageObjectFactory factory = new PackageObjectFactory(
423                 new HashSet<>(), Thread.currentThread().getContextClassLoader());
424         treeWalker.setModuleFactory(factory);
425         // create file that should throw exception
426         final File file = new File(temporaryFolder, "file.java");
427         final FileText fileText = new FileText(file, new ArrayList<>());
428 
429         treeWalker.processFiltered(file, fileText);
430         final Collection<Checks> checks =
431                 TestUtil.getInternalStateCollectionChecks(treeWalker, "ordinaryChecks");
432         assertWithMessage("No checks -> No parsing")
433             .that(checks)
434             .isEmpty();
435     }
436 
437     @Test
438     public void testBehaviourWithOrdinaryAndCommentChecks() throws Exception {
439         final TreeWalker treeWalker = new TreeWalker();
440         treeWalker.configure(createModuleConfig(TypeNameCheck.class));
441         treeWalker.configure(createModuleConfig(CommentsIndentationCheck.class));
442         final PackageObjectFactory factory = new PackageObjectFactory(
443                 new HashSet<>(), Thread.currentThread().getContextClassLoader());
444         treeWalker.setModuleFactory(factory);
445         treeWalker.setupChild(createModuleConfig(TypeNameCheck.class));
446         treeWalker.setupChild(createModuleConfig(CommentsIndentationCheck.class));
447         final File file = new File(temporaryFolder, "file.java");
448         final List<String> lines = new ArrayList<>();
449         lines.add(" class a%$# {} ");
450         final FileText fileText = new FileText(file, lines);
451         treeWalker.setFileContents(new FileContents(fileText));
452 
453         final CheckstyleException exception =
454                 getExpectedThrowable(CheckstyleException.class, () -> {
455                     treeWalker.processFiltered(file, fileText);
456                 }, "file is not compilable, exception is expected");
457         final String message = "IllegalStateException occurred while parsing file";
458         assertWithMessage("Error message is unexpected")
459             .that(exception.getMessage())
460             .contains(message);
461     }
462 
463     @Test
464     public void testSetupChild() throws Exception {
465         final TreeWalker treeWalker = new TreeWalker();
466         final PackageObjectFactory factory = new PackageObjectFactory(
467                 new HashSet<>(), Thread.currentThread().getContextClassLoader());
468         treeWalker.setModuleFactory(factory);
469         treeWalker.setTabWidth(99);
470         treeWalker.finishLocalSetup();
471 
472         final Configuration config = new DefaultConfiguration(
473                 XpathFileGeneratorAstFilter.class.getName());
474 
475         treeWalker.setupChild(config);
476 
477         final Set<TreeWalkerFilter> filters =
478                 TestUtil.getInternalStateSetTreeWalkerFilter(treeWalker, "filters");
479         final int tabWidth = TestUtil.getInternalState(filters.iterator().next(),
480                 "tabWidth", Integer.class);
481 
482         assertWithMessage("expected tab width")
483             .that(tabWidth)
484             .isEqualTo(99);
485     }
486 
487     @Test
488     public void testBehaviourWithChecksAndFilters() throws Exception {
489 
490         final String[] expected = {
491             "22:17: " + getCheckMessage(MemberNameCheck.class, "name.invalidPattern", "P",
492                     "^[a-z][a-zA-Z0-9]*$"),
493             "17:17: " + getCheckMessage(MemberNameCheck.class, "name.invalidPattern", "I",
494                     "^[a-z][a-zA-Z0-9]*$"),
495         };
496 
497         verifyWithInlineConfigParserTwice(
498                 getPath("InputTreeWalkerSuppressionCommentFilter.java"),
499                 expected);
500     }
501 
502     @Test
503     public void testMultiCheckOrder() throws Exception {
504 
505         final String[] expected = {
506             "29:9: " + getCheckMessage(WhitespaceAfterCheck.class, "ws.notFollowed", "if"),
507             "29:9: " + getCheckMessage(WhitespaceAroundCheck.class, "ws.notFollowed", "if"),
508         };
509 
510         verifyWithInlineConfigParserTwice(
511                 getPath("InputTreeWalkerMultiCheckOrder.java"),
512                 expected);
513     }
514 
515     @Test
516     public void testMultiCheckOfSameTypeNoIdResultsInOrderingByHash() throws Exception {
517 
518         final String[] expected = {
519             "17:28: " + getCheckMessage(ParameterNameCheck.class,
520                     "name.invalidPattern", "V2", "^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"),
521             "19:25: " + getCheckMessage(ParameterNameCheck.class,
522                     "name.invalidPattern", "b", "^[a-z][a-z0-9][a-zA-Z0-9]*$"),
523         };
524 
525         verifyWithInlineConfigParserTwice(
526                 getPath("InputTreeWalkerMultiCheckOrder2.java"),
527                 expected);
528     }
529 
530     @Test
531     public void testFinishLocalSetupFullyInitialized() {
532         final TreeWalker treeWalker = new TreeWalker();
533         treeWalker.setSeverity("error");
534         treeWalker.setTabWidth(100);
535         treeWalker.finishLocalSetup();
536 
537         final Context context = TestUtil.getInternalState(treeWalker, "childContext",
538                 Context.class);
539         assertWithMessage("Severity differs from expected")
540             .that(context.get("severity"))
541             .isEqualTo("error");
542         assertWithMessage("Tab width differs from expected")
543             .that(context.get("tabWidth"))
544             .isEqualTo(String.valueOf(100));
545     }
546 
547     @Test
548     public void testCheckInitIsCalledInTreeWalker() throws Exception {
549         final DefaultConfiguration checkConfig =
550                 createModuleConfig(VerifyInitCheck.class);
551         final String uniqueFileName = "file_" + UUID.randomUUID() + ".pdf";
552         final File file = new File(temporaryFolder, uniqueFileName);
553         execute(checkConfig, file.getPath());
554         assertWithMessage("Init was not called")
555                 .that(VerifyInitCheck.isInitWasCalled())
556                 .isTrue();
557     }
558 
559     @Test
560     public void testCheckDestroyIsCalledInTreeWalker() throws Exception {
561         VerifyDestroyCheck.resetDestroyWasCalled();
562         final DefaultConfiguration checkConfig =
563                 createModuleConfig(VerifyDestroyCheck.class);
564         final String uniqueFileName = "file_" + UUID.randomUUID() + ".pdf";
565         final File file = new File(temporaryFolder, uniqueFileName);
566         execute(checkConfig, file.getPath());
567         assertWithMessage("Destroy was not called")
568                 .that(VerifyDestroyCheck.isDestroyWasCalled())
569                 .isTrue();
570     }
571 
572     @Test
573     public void testCommentCheckDestroyIsCalledInTreeWalker() throws Exception {
574         VerifyDestroyCheck.resetDestroyWasCalled();
575         final DefaultConfiguration checkConfig =
576                 createModuleConfig(VerifyDestroyCommentCheck.class);
577         final String uniqueFileName = "file_" + UUID.randomUUID() + ".pdf";
578         final File file = new File(temporaryFolder, uniqueFileName);
579         execute(checkConfig, file.getPath());
580         assertWithMessage("Destroy was not called")
581                 .that(VerifyDestroyCheck.isDestroyWasCalled())
582                 .isTrue();
583     }
584 
585     @Test
586     public void testCacheWhenFileExternalResourceContentDoesNotChange() throws Exception {
587         final DefaultConfiguration filterConfig = createModuleConfig(SuppressionXpathFilter.class);
588         filterConfig.addProperty("file", getPath("InputTreeWalkerSuppressionXpathFilter.xml"));
589         final DefaultConfiguration treeWalkerConfig = createModuleConfig(TreeWalker.class);
590         treeWalkerConfig.addChild(filterConfig);
591 
592         final DefaultConfiguration checkerConfig = createRootConfig(treeWalkerConfig);
593         final String uniqueFileName1 = "junit_" + UUID.randomUUID() + ".java";
594         final File cacheFile = new File(temporaryFolder, uniqueFileName1);
595         checkerConfig.addProperty("cacheFile", cacheFile.getPath());
596 
597         final String uniqueFileName2 = "file_" + UUID.randomUUID() + ".java";
598         final File filePath = new File(temporaryFolder, uniqueFileName2);
599 
600         execute(checkerConfig, filePath.toString());
601         // One more time to use cache.
602         execute(checkerConfig, filePath.toString());
603 
604         assertWithMessage("External resource is not present in cache")
605                 .that(Files.readString(cacheFile.toPath()))
606                 .contains("InputTreeWalkerSuppressionXpathFilter.xml");
607     }
608 
609     @Test
610     public void testTreeWalkerFilterAbsolutePath() throws Exception {
611         // test is only valid when relative paths are given
612         final String filePath = "src/test/resources/" + getPackageLocation()
613                 + "/InputTreeWalkerSuppressionXpathFilterAbsolute.java";
614 
615         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
616         verifyWithInlineConfigParserTwice(filePath, expected);
617     }
618 
619     @Test
620     public void testExternalResourceFiltersWithNoExternalResource() throws Exception {
621         final DefaultConfiguration checkConfig = createModuleConfig(EmptyStatementCheck.class);
622         final DefaultConfiguration filterConfig =
623                 createModuleConfig(SuppressWithNearbyCommentFilter.class);
624         final DefaultConfiguration treeWalkerConfig = createModuleConfig(TreeWalker.class);
625         treeWalkerConfig.addChild(checkConfig);
626         treeWalkerConfig.addChild(filterConfig);
627 
628         final DefaultConfiguration checkerConfig = createRootConfig(treeWalkerConfig);
629         final String uniqueFileName1 = "junit_" + UUID.randomUUID() + ".java";
630         final File cacheFile = new File(temporaryFolder, uniqueFileName1);
631         checkerConfig.addProperty("cacheFile", cacheFile.getPath());
632         final String uniqueFileName2 = "junit_" + UUID.randomUUID() + ".java";
633         final File filePath = new File(temporaryFolder, uniqueFileName2);
634 
635         execute(checkerConfig, filePath.toString());
636 
637         final long cacheSize = Files.size(cacheFile.toPath());
638         assertWithMessage("cacheFile should not be empty")
639                 .that(cacheSize)
640                 .isNotEqualTo(0);
641     }
642 
643     /**
644      * This test is checking that Checks execution ordered by name.
645      *
646      * @throws Exception if file is not found
647      */
648     @Test
649     public void testOrderOfCheckExecution() throws Exception {
650 
651         final DefaultConfiguration configuration1 = createModuleConfig(AaCheck.class);
652         configuration1.addProperty("id", "2");
653         final DefaultConfiguration configuration2 = createModuleConfig(BbCheck.class);
654         configuration2.addProperty("id", "1");
655 
656         final DefaultConfiguration treeWalkerConfig = createModuleConfig(TreeWalker.class);
657         treeWalkerConfig.addChild(configuration2);
658         treeWalkerConfig.addChild(configuration1);
659 
660         final List<File> files =
661                 Collections.singletonList(new File(getPath("InputTreeWalker2.java")));
662         final Checker checker = createChecker(treeWalkerConfig);
663 
664         final CheckstyleException exception =
665                 getExpectedThrowable(CheckstyleException.class, () -> {
666                     checker.process(files);
667                 }, "exception is expected");
668         assertWithMessage("wrong order of Check executions")
669             .that(exception.getCause().getMessage())
670             .isEqualTo(AaCheck.class.toString());
671     }
672 
673     @Test
674     public void testCreateNewCheckSortedSetOrdersByIdBeforeHashCode() throws Exception {
675         TestCheck.clearExecutionOrder();
676 
677         // Create two checks of same class with IDs where:
678         // - ID "alpha" < "beta" alphabetically
679         // - hashCode beta(1) < alpha(2) - opposite order!
680         final DefaultConfiguration config1 = createModuleConfig(TestCheck.class);
681         config1.addProperty("id", "alpha");
682 
683         final DefaultConfiguration config2 = createModuleConfig(TestCheck.class);
684         config2.addProperty("id", "beta");
685 
686         final DefaultConfiguration treeWalkerConfig = createModuleConfig(TreeWalker.class);
687         // Add in reverse alphabetical order - beta first, then alpha
688         treeWalkerConfig.addChild(config2);
689         treeWalkerConfig.addChild(config1);
690 
691         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
692         verify(createChecker(treeWalkerConfig), getPath("InputTreeWalker2.java"), expected);
693 
694         final List<String> executionOrder = TestCheck.getExecutionOrder();
695         // With proper sorting (by ID, not hashCode):
696         // "alpha" < "beta" alphabetically, so alpha executes first
697         // Without proper sorting: beta(hash=1) < alpha(hash=2), so beta executes first
698         assertWithMessage("Checks should be sorted by ID (alpha before beta), not by hashCode")
699                 .that(executionOrder)
700                 .containsExactly("alpha", "beta")
701                 .inOrder();
702     }
703 
704     @Test
705     public void testSkipFileOnJavaParseExceptionTrue() throws Exception {
706         final DefaultConfiguration config = createModuleConfig(TreeWalker.class);
707         config.addProperty("skipFileOnJavaParseException", "true");
708         config.addProperty("javaParseExceptionSeverity", "ignore");
709         config.addChild(createModuleConfig(ConstantNameCheck.class));
710 
711         final File[] files = {
712             new File(getNonCompilablePath("InputTreeWalkerSkipParsingException.java")),
713             new File(getPath("InputTreeWalkerProperFileExtension.java")),
714             new File(getNonCompilablePath("InputTreeWalkerSkipParsingException2.java")),
715         };
716 
717         final Checker checker = createChecker(config);
718         final Map<String, List<String>> expectedViolation = new HashMap<>();
719         expectedViolation.put(getPath("InputTreeWalkerProperFileExtension.java"),
720                 Collections.singletonList(
721                         "15:27: " + getCheckMessage(ConstantNameCheck.class,
722                         MSG_INVALID_PATTERN, "k", "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$")));
723         verify(checker, files, expectedViolation);
724     }
725 
726     /**
727      * This test method is not using the regular {@code verify} methods
728      * because it is testing the behavior of {@code TreeWalker} when it should
729      * not skip files with parse exception. Instead, it should throw {@code exception}.
730      * The test verifies this behavior by attempting to process files
731      * that are known to cause a Java parse exception and asserting that
732      * the expected {@code exception} is indeed thrown.
733      *
734      */
735     @Test
736     public void testSkipFileOnJavaParseExceptionFalse() throws Exception {
737         final DefaultConfiguration config = createModuleConfig(TreeWalker.class);
738         config.addProperty("skipFileOnJavaParseException", "false");
739         config.addChild(createModuleConfig(ConstantNameCheck.class));
740 
741         final String[] files = {
742             getNonCompilablePath("InputTreeWalkerSkipParsingException2.java"),
743             getPath("InputTreeWalkerProperFileExtension.java"),
744             getNonCompilablePath("InputTreeWalkerSkipParsingException.java"),
745         };
746         final Exception ex = getExpectedThrowable(CheckstyleException.class,
747                 () -> execute(config, files),
748                 "Exception is expected");
749         assertWithMessage("Error message is unexpected")
750                 .that(ex.getMessage())
751                 .contains("Exception was thrown while processing");
752     }
753 
754     @Test
755     public void testSkipFileOnJavaParseExceptionConfigSeverityIgnore() throws Exception {
756         final String path =
757                 getNonCompilablePath(
758                         "InputTreeWalkerSkipParsingExceptionConfigSeverityIgnore.java");
759         final String[] expected = CommonUtil.EMPTY_STRING_ARRAY;
760         verifyWithInlineXmlConfig(path, expected);
761     }
762 
763     @Test
764     public void testSkipFileOnJavaParseExceptionConfigSeverityDefault() throws Exception {
765         final String path =
766                 getNonCompilablePath(
767                         "InputTreeWalkerSkipParsingExceptionConfigSeverityDefault.java");
768         final String[] expected = {
769             "1: " + getCheckMessage(TreeWalker.PARSE_EXCEPTION_MSG, "IllegalStateException")
770                   + " occurred while parsing file " + path + ".",
771         };
772         verifyWithInlineXmlConfig(path, expected);
773     }
774 
775     @Test
776     public void testSkipFileOnJavaParseExceptionSkipChecks() throws Exception {
777         final DefaultConfiguration config = createModuleConfig(TreeWalker.class);
778         config.addProperty("skipFileOnJavaParseException", "true");
779         config.addProperty("javaParseExceptionSeverity", "ignore");
780         config.addChild(createModuleConfig(NoCodeInFileCheck.class));
781 
782         final Checker checker = createChecker(config);
783 
784         final File[] files = {
785             new File(getNonCompilablePath("InputTreeWalkerSkipParsingException.java")),
786             new File(getPath("InputTreeWalkerProperFileExtension.java")),
787             new File(getNonCompilablePath("InputTreeWalkerSkipParsingException2.java")),
788         };
789         final Map<String, List<String>> expectedViolation = new HashMap<>();
790         expectedViolation.put(getPath("InputTreeWalkerProperFileExtension.java"),
791                 new ArrayList<>());
792 
793         verify(checker, files, expectedViolation);
794     }
795 
796     @Test
797     public void testJavaParseExceptionSeverityDefaultError() throws Exception {
798         final DefaultConfiguration config = createModuleConfig(TreeWalker.class);
799         config.addProperty("skipFileOnJavaParseException", "true");
800         config.addChild(createModuleConfig(NoCodeInFileCheck.class));
801 
802         final Checker checker = createChecker(config);
803 
804         final File[] files = {
805             new File(getNonCompilablePath("InputTreeWalkerSkipParsingException.java")),
806             new File(getPath("InputTreeWalkerProperFileExtension.java")),
807         };
808 
809         final Map<String, List<String>> expectedViolation = new HashMap<>();
810 
811         expectedViolation.put(getPath("InputTreeWalkerProperFileExtension.java"),
812                 new ArrayList<>());
813         expectedViolation.put(getNonCompilablePath("InputTreeWalkerSkipParsingException.java"),
814                 List.of("1: Java specific (TreeWalker-based) modules are skipped due to an "
815                         + "exception during parsing - "
816                         + "IllegalStateException occurred while parsing file "
817                         + getNonCompilablePath("InputTreeWalkerSkipParsingException.java") + "."));
818 
819         verify(checker, files, expectedViolation);
820     }
821 
822     @Test
823     public void testJavaParseExceptionSeverityDefault() throws Exception {
824         final String inputFile = "InputTreeWalkerSkipParsingExceptionConfigSeverityDefault.java";
825         final String expectedInfoFile = "InputTreeWalkerExpectedInfo.txt";
826         final String expectedErrorFile = "InputTreeWalkerExpectedError.txt";
827 
828         final ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
829         final ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
830         final DefaultLogger dl = new DefaultLogger(infoStream, OutputStreamOptions.CLOSE,
831                 errorStream, OutputStreamOptions.CLOSE);
832 
833         verifyWithInlineConfigParserAndDefaultLogger(
834                 getNonCompilablePath(inputFile),
835                 getPath(expectedInfoFile),
836                 getPath(expectedErrorFile),
837                 dl, infoStream, errorStream);
838     }
839 
840     public static class BadJavaDocCheck extends AbstractCheck {
841 
842         @Override
843         public int[] getDefaultTokens() {
844             return getAcceptableTokens();
845         }
846 
847         @Override
848         public int[] getAcceptableTokens() {
849             return new int[] {TokenTypes.SINGLE_LINE_COMMENT};
850         }
851 
852         @Override
853         public int[] getRequiredTokens() {
854             return getAcceptableTokens();
855         }
856 
857     }
858 
859     public static class VerifyInitCheck extends AbstractCheck {
860 
861         private static boolean initWasCalled;
862 
863         @Override
864         public int[] getDefaultTokens() {
865             return CommonUtil.EMPTY_INT_ARRAY;
866         }
867 
868         @Override
869         public int[] getAcceptableTokens() {
870             return getDefaultTokens();
871         }
872 
873         @Override
874         public int[] getRequiredTokens() {
875             return getDefaultTokens();
876         }
877 
878         @Override
879         public void init() {
880             super.init();
881             initWasCalled = true;
882         }
883 
884         public static boolean isInitWasCalled() {
885             return initWasCalled;
886         }
887 
888     }
889 
890     public static class VerifyDestroyCheck extends AbstractCheck {
891 
892         private static boolean destroyWasCalled;
893 
894         @Override
895         public int[] getDefaultTokens() {
896             return CommonUtil.EMPTY_INT_ARRAY;
897         }
898 
899         @Override
900         public int[] getAcceptableTokens() {
901             return getDefaultTokens();
902         }
903 
904         @Override
905         public int[] getRequiredTokens() {
906             return getDefaultTokens();
907         }
908 
909         @Override
910         public void destroy() {
911             super.destroy();
912             destroyWasCalled = true;
913         }
914 
915         public static void resetDestroyWasCalled() {
916             destroyWasCalled = false;
917         }
918 
919         public static boolean isDestroyWasCalled() {
920             return destroyWasCalled;
921         }
922 
923     }
924 
925     public static class VerifyDestroyCommentCheck extends VerifyDestroyCheck {
926 
927         @Override
928         public boolean isCommentNodesRequired() {
929             return true;
930         }
931 
932     }
933 
934     public static class AaCheck extends AbstractCheck {
935 
936         @Override
937         public int[] getDefaultTokens() {
938             return new int[0];
939         }
940 
941         @Override
942         public int[] getAcceptableTokens() {
943             return new int[0];
944         }
945 
946         @Override
947         public int[] getRequiredTokens() {
948             return new int[0];
949         }
950 
951         @Override
952         public void beginTree(DetailAST rootAST) {
953             throw new IllegalStateException(AaCheck.class.toString());
954         }
955 
956     }
957 
958     public static class BbCheck extends AbstractCheck {
959 
960         @Override
961         public int[] getDefaultTokens() {
962             return new int[0];
963         }
964 
965         @Override
966         public int[] getAcceptableTokens() {
967             return new int[0];
968         }
969 
970         @Override
971         public int[] getRequiredTokens() {
972             return new int[0];
973         }
974 
975         @Override
976         public void beginTree(DetailAST rootAST) {
977             throw new IllegalStateException(BbCheck.class.toString());
978         }
979 
980     }
981 
982     public static class RequiredTokenIsEmptyIntArray extends AbstractCheck {
983 
984         @Override
985         public int[] getRequiredTokens() {
986             return CommonUtil.EMPTY_INT_ARRAY;
987         }
988 
989         @Override
990         public int[] getDefaultTokens() {
991             return new int[] {TokenTypes.ANNOTATION};
992         }
993 
994         @Override
995         public int[] getAcceptableTokens() {
996             return CommonUtil.EMPTY_INT_ARRAY;
997         }
998 
999     }
1000 
1001     /**
1002      * Test check with controllable ID and hashCode for deterministic sorting tests.
1003      * Tracks execution order to verify sorting.
1004      */
1005     public static class TestCheck extends AbstractCheck {
1006         private static final List<String> EXECUTION_ORDER = new ArrayList<>();
1007 
1008         @Override
1009         public void beginTree(DetailAST rootAST) {
1010             EXECUTION_ORDER.add(getId());
1011         }
1012 
1013         @Override
1014         public boolean equals(Object obj) {
1015             if (this == obj) {
1016                 return true;
1017             }
1018             if (obj == null || getClass() != obj.getClass()) {
1019                 return false;
1020             }
1021             final TestCheck other = (TestCheck) obj;
1022             return Objects.equals(getId(), other.getId());
1023         }
1024 
1025         @Override
1026         public int hashCode() {
1027             return Objects.hash(getId());
1028         }
1029 
1030         @Override
1031         public int[] getDefaultTokens() {
1032             return CommonUtil.EMPTY_INT_ARRAY;
1033         }
1034 
1035         @Override
1036         public int[] getAcceptableTokens() {
1037             return CommonUtil.EMPTY_INT_ARRAY;
1038         }
1039 
1040         @Override
1041         public int[] getRequiredTokens() {
1042             return CommonUtil.EMPTY_INT_ARRAY;
1043         }
1044 
1045         /**
1046          * Clears the execution order tracking list.
1047          * Used to reset state between tests.
1048          */
1049         /* package */ static void clearExecutionOrder() {
1050             EXECUTION_ORDER.clear();
1051         }
1052 
1053         /**
1054          * Gets a copy of the execution order list.
1055          * Used by tests to verify check execution order.
1056          *
1057          * @return copy of execution order list
1058          */
1059         /* package */ static List<String> getExecutionOrder() {
1060             return new ArrayList<>(EXECUTION_ORDER);
1061         }
1062     }
1063 
1064 }