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