1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle.internal;
21
22 import static com.google.common.truth.Truth.assertWithMessage;
23 import static java.lang.Integer.parseInt;
24
25 import java.beans.PropertyDescriptor;
26 import java.io.File;
27 import java.io.IOException;
28 import java.io.StringReader;
29 import java.lang.reflect.Array;
30 import java.lang.reflect.Field;
31 import java.lang.reflect.ParameterizedType;
32 import java.net.URI;
33 import java.nio.file.Files;
34 import java.nio.file.Path;
35 import java.nio.file.Paths;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.BitSet;
39 import java.util.Collection;
40 import java.util.Collections;
41 import java.util.HashMap;
42 import java.util.HashSet;
43 import java.util.Iterator;
44 import java.util.List;
45 import java.util.Locale;
46 import java.util.Map;
47 import java.util.NoSuchElementException;
48 import java.util.Optional;
49 import java.util.Properties;
50 import java.util.Set;
51 import java.util.TreeSet;
52 import java.util.regex.Matcher;
53 import java.util.regex.Pattern;
54 import java.util.stream.Collectors;
55 import java.util.stream.IntStream;
56 import java.util.stream.Stream;
57
58 import org.apache.commons.beanutils.PropertyUtils;
59 import org.junit.jupiter.api.BeforeAll;
60 import org.junit.jupiter.api.Test;
61 import org.w3c.dom.Document;
62 import org.w3c.dom.Node;
63 import org.w3c.dom.NodeList;
64 import org.xml.sax.InputSource;
65
66 import com.puppycrawl.tools.checkstyle.Checker;
67 import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
68 import com.puppycrawl.tools.checkstyle.ConfigurationLoader.IgnoredModulesOptions;
69 import com.puppycrawl.tools.checkstyle.ModuleFactory;
70 import com.puppycrawl.tools.checkstyle.PropertiesExpander;
71 import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
72 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
73 import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
74 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
75 import com.puppycrawl.tools.checkstyle.api.Configuration;
76 import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
77 import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
78 import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;
79 import com.puppycrawl.tools.checkstyle.internal.utils.TestUtil;
80 import com.puppycrawl.tools.checkstyle.internal.utils.XdocGenerator;
81 import com.puppycrawl.tools.checkstyle.internal.utils.XdocUtil;
82 import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
83 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
84
85
86
87
88
89
90
91 public class XdocsPagesTest {
92 private static final Path SITE_PATH = Paths.get("src/site/site.xml");
93
94 private static final Path AVAILABLE_CHECKS_PATH = Paths.get("src/xdocs/checks.xml");
95 private static final String LINK_TEMPLATE =
96 "(?s).*<a href=\"[^\"]+#%1$s\">([\\r\\n\\s])*%1$s([\\r\\n\\s])*</a>.*";
97
98 private static final Pattern VERSION = Pattern.compile("\\d+\\.\\d+(\\.\\d+)?");
99
100 private static final Pattern DESCRIPTION_VERSION = Pattern
101 .compile("^Since Checkstyle \\d+\\.\\d+(\\.\\d+)?");
102
103 private static final List<String> XML_FILESET_LIST = List.of(
104 "TreeWalker",
105 "name=\"Checker\"",
106 "name=\"Header\"",
107 "name=\"LineLength\"",
108 "name=\"Translation\"",
109 "name=\"SeverityMatchFilter\"",
110 "name=\"SuppressWithNearbyTextFilter\"",
111 "name=\"SuppressWithPlainTextCommentFilter\"",
112 "name=\"SuppressionFilter\"",
113 "name=\"SuppressionSingleFilter\"",
114 "name=\"SuppressWarningsFilter\"",
115 "name=\"BeforeExecutionExclusionFileFilter\"",
116 "name=\"RegexpHeader\"",
117 "name=\"RegexpOnFilename\"",
118 "name=\"RegexpSingleline\"",
119 "name=\"RegexpMultiline\"",
120 "name=\"JavadocPackage\"",
121 "name=\"NewlineAtEndOfFile\"",
122 "name=\"OrderedProperties\"",
123 "name=\"UniqueProperties\"",
124 "name=\"FileLength\"",
125 "name=\"FileTabCharacter\""
126 );
127
128 private static final Set<String> CHECK_PROPERTIES = getProperties(AbstractCheck.class);
129 private static final Set<String> JAVADOC_CHECK_PROPERTIES =
130 getProperties(AbstractJavadocCheck.class);
131 private static final Set<String> FILESET_PROPERTIES = getProperties(AbstractFileSetCheck.class);
132
133 private static final Set<String> UNDOCUMENTED_PROPERTIES = Set.of(
134 "Checker.classLoader",
135 "Checker.classloader",
136 "Checker.moduleClassLoader",
137 "Checker.moduleFactory",
138 "TreeWalker.classLoader",
139 "TreeWalker.moduleFactory",
140 "TreeWalker.cacheFile",
141 "TreeWalker.upChild",
142 "SuppressWithNearbyCommentFilter.fileContents",
143 "SuppressionCommentFilter.fileContents"
144 );
145
146 private static final Set<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Set.of(
147
148 "SuppressWarningsHolder.aliasList",
149
150 "Header.header",
151 "RegexpHeader.header",
152
153 "CustomImportOrder.customImportOrderRules"
154 );
155
156 private static final Set<String> SUN_MODULES = Collections.unmodifiableSet(
157 CheckUtil.getConfigSunStyleModules());
158
159
160
161 private static final Set<String> IGNORED_SUN_MODULES = Set.of(
162 "ArrayTypeStyle",
163 "AvoidNestedBlocks",
164 "AvoidStarImport",
165 "ConstantName",
166 "DesignForExtension",
167 "EmptyBlock",
168 "EmptyForIteratorPad",
169 "EmptyStatement",
170 "EqualsHashCode",
171 "FileLength",
172 "FileTabCharacter",
173 "FinalClass",
174 "FinalParameters",
175 "GenericWhitespace",
176 "HiddenField",
177 "HideUtilityClassConstructor",
178 "IllegalImport",
179 "IllegalInstantiation",
180 "InnerAssignment",
181 "InterfaceIsType",
182 "JavadocMethod",
183 "JavadocPackage",
184 "JavadocStyle",
185 "JavadocType",
186 "JavadocVariable",
187 "LeftCurly",
188 "LineLength",
189 "LocalFinalVariableName",
190 "LocalVariableName",
191 "MagicNumber",
192 "MemberName",
193 "MethodLength",
194 "MethodName",
195 "MethodParamPad",
196 "MissingJavadocMethod",
197 "MissingSwitchDefault",
198 "ModifierOrder",
199 "NeedBraces",
200 "NewlineAtEndOfFile",
201 "NoWhitespaceAfter",
202 "NoWhitespaceBefore",
203 "OperatorWrap",
204 "PackageName",
205 "ParameterName",
206 "ParameterNumber",
207 "ParenPad",
208 "RedundantImport",
209 "RedundantModifier",
210 "RegexpSingleline",
211 "RightCurly",
212 "SimplifyBooleanExpression",
213 "SimplifyBooleanReturn",
214 "StaticVariableName",
215 "TodoComment",
216 "Translation",
217 "TypecastParenPad",
218 "TypeName",
219 "UnusedImports",
220 "UpperEll",
221 "VisibilityModifier",
222 "WhitespaceAfter",
223 "WhitespaceAround"
224 );
225
226 private static final Set<String> GOOGLE_MODULES = Collections.unmodifiableSet(
227 CheckUtil.getConfigGoogleStyleModules());
228
229
230
231
232
233
234
235
236 @BeforeAll
237 public static void generateXdocContent() throws Exception {
238 XdocGenerator.generateXdocContent();
239 }
240
241 @Test
242 public void testAllChecksPresentOnAvailableChecksPage() throws Exception {
243 final String availableChecks = Files.readString(AVAILABLE_CHECKS_PATH);
244
245 CheckUtil.getSimpleNames(CheckUtil.getCheckstyleChecks())
246 .stream()
247 .filter(checkName -> {
248 return !"JavadocMetadataScraper".equals(checkName)
249 && !"ClassAndPropertiesSettersJavadocScraper".equals(checkName);
250 })
251 .forEach(checkName -> {
252 if (!isPresent(availableChecks, checkName)) {
253 assertWithMessage(
254 checkName + " is not correctly listed on Available Checks page"
255 + " - add it to " + AVAILABLE_CHECKS_PATH).fail();
256 }
257 });
258 }
259
260 private static boolean isPresent(String availableChecks, String checkName) {
261 final String linkPattern = String.format(Locale.ROOT, LINK_TEMPLATE, checkName);
262 return availableChecks.matches(linkPattern);
263 }
264
265 @Test
266 public void testAllConfigsHaveLinkInSite() throws Exception {
267 final String siteContent = Files.readString(SITE_PATH);
268
269 for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
270 final String expectedFile = path.toString()
271 .replace(".xml", ".html")
272 .replaceAll("\\\\", "/")
273 .replaceAll("src[\\\\/]xdocs[\\\\/]", "");
274 final String expectedLink = String.format(Locale.ROOT, "href=\"%s\"", expectedFile);
275 assertWithMessage("Expected to find link to '" + expectedLink + "' in " + SITE_PATH)
276 .that(siteContent)
277 .contains(expectedLink);
278 }
279 }
280
281 @Test
282 public void testAllChecksPageInSyncWithChecksSummaries() throws Exception {
283 final Pattern endOfSentence = Pattern.compile("(.*?\\.)\\s", Pattern.DOTALL);
284 final Map<String, String> summaries = readSummaries();
285
286 for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
287 final String fileName = path.getFileName().toString();
288 if ("config_system_properties.xml".equals(fileName)
289 || path.toString().contains("filefilters")
290 || path.toString().contains("filters")) {
291 continue;
292 }
293
294 final String input = Files.readString(path);
295 final Document document = XmlUtil.getRawXml(fileName, input, input);
296 final NodeList sources = document.getElementsByTagName("subsection");
297
298 for (int position = 0; position < sources.getLength(); position++) {
299 final Node section = sources.item(position);
300 final String sectionName = XmlUtil.getNameAttributeOfNode(section);
301 if (!"Description".equals(sectionName)) {
302 continue;
303 }
304
305 final String checkName = XmlUtil.getNameAttributeOfNode(section.getParentNode());
306 final Matcher matcher = endOfSentence.matcher(section.getTextContent());
307 assertWithMessage(
308 "The first sentence of the \"Description\" subsection for the check "
309 + checkName + " in the file \"" + fileName + "\" should end with a period")
310 .that(matcher.find())
311 .isTrue();
312 final String firstSentence = XmlUtil.sanitizeXml(matcher.group(1));
313 assertWithMessage("The summary for check " + checkName
314 + " in the file \"" + AVAILABLE_CHECKS_PATH + "\""
315 + " should match the first sentence of the \"Description\" subsection"
316 + " for this check in the file \"" + fileName + "\"")
317 .that(summaries.get(checkName))
318 .isEqualTo(firstSentence);
319 }
320 }
321 }
322
323 @Test
324 public void testCategoryIndexPageTableInSyncWithAllChecksPageTable() throws Exception {
325 final Map<String, String> summaries = readSummaries();
326 for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
327 final String fileName = path.getFileName().toString();
328 if (!"index.xml".equals(fileName)
329 || path.getParent().toString().contains("filters")) {
330 continue;
331 }
332
333 final String input = Files.readString(path);
334 final Document document = XmlUtil.getRawXml(fileName, input, input);
335 final NodeList sources = document.getElementsByTagName("tr");
336
337 for (int position = 0; position < sources.getLength(); position++) {
338 final Node tableRow = sources.item(position);
339 final Iterator<Node> cells = XmlUtil
340 .findChildElementsByTag(tableRow, "td").iterator();
341 final String checkName = XmlUtil.sanitizeXml(cells.next().getTextContent());
342 final String description = XmlUtil.sanitizeXml(cells.next().getTextContent());
343 assertWithMessage("The summary for check " + checkName
344 + " in the file \"" + path + "\""
345 + " should match the summary"
346 + " for this check in the file \"" + AVAILABLE_CHECKS_PATH + "\"")
347 .that(description)
348 .isEqualTo(summaries.get(checkName));
349 }
350 }
351 }
352
353 private static Map<String, String> readSummaries() throws Exception {
354 final String fileName = AVAILABLE_CHECKS_PATH.getFileName().toString();
355 final String input = Files.readString(AVAILABLE_CHECKS_PATH);
356 final Document document = XmlUtil.getRawXml(fileName, input, input);
357 final NodeList rows = document.getElementsByTagName("tr");
358 final Map<String, String> result = new HashMap<>();
359
360 for (int position = 0; position < rows.getLength(); position++) {
361 final Node row = rows.item(position);
362 final Iterator<Node> cells = XmlUtil.findChildElementsByTag(row, "td").iterator();
363 final String name = XmlUtil.sanitizeXml(cells.next().getTextContent());
364 final String summary = XmlUtil.sanitizeXml(cells.next().getTextContent());
365
366 result.put(name, summary);
367 }
368
369 return result;
370 }
371
372 @Test
373 public void testAllSubSections() throws Exception {
374 for (Path path : XdocUtil.getXdocsFilePaths()) {
375 final String input = Files.readString(path);
376 final String fileName = path.getFileName().toString();
377
378 final Document document = XmlUtil.getRawXml(fileName, input, input);
379 final NodeList subSections = document.getElementsByTagName("subsection");
380
381 for (int position = 0; position < subSections.getLength(); position++) {
382 final Node subSection = subSections.item(position);
383 final Node name = subSection.getAttributes().getNamedItem("name");
384
385 assertWithMessage("All sub-sections in '" + fileName + "' must have a name")
386 .that(name)
387 .isNotNull();
388
389 final Node id = subSection.getAttributes().getNamedItem("id");
390
391 assertWithMessage("All sub-sections in '" + fileName + "' must have an id")
392 .that(id)
393 .isNotNull();
394
395 final String sectionName;
396 final String nameString = name.getNodeValue();
397 final String idString = id.getNodeValue();
398 final String expectedId;
399
400 if ("google_style.xml".equals(fileName)) {
401 sectionName = "Google";
402 expectedId = (sectionName + " " + nameString).replace(' ', '_');
403 }
404 else if ("sun_style.xml".equals(fileName)) {
405 sectionName = "Sun";
406 expectedId = (sectionName + " " + nameString).replace(' ', '_');
407 }
408 else if (path.toString().contains("filters")
409 || path.toString().contains("checks")) {
410
411
412 sectionName = XmlUtil.getNameAttributeOfNode(subSection.getParentNode());
413 expectedId = nameString.replace(' ', '_');
414 }
415 else {
416 sectionName = XmlUtil.getNameAttributeOfNode(subSection.getParentNode());
417 expectedId = (sectionName + " " + nameString).replace(' ', '_');
418 }
419
420 assertWithMessage(fileName + " sub-section " + nameString + " for section "
421 + sectionName + " must match")
422 .that(idString)
423 .isEqualTo(expectedId);
424 }
425 }
426 }
427
428 @Test
429 public void testAllXmlExamples() throws Exception {
430 for (Path path : XdocUtil.getXdocsFilePaths()) {
431 final String input = Files.readString(path);
432 final String fileName = path.getFileName().toString();
433
434 final Document document = XmlUtil.getRawXml(fileName, input, input);
435 final NodeList sources = document.getElementsByTagName("source");
436
437 for (int position = 0; position < sources.getLength(); position++) {
438 final String unserializedSource = sources.item(position).getTextContent()
439 .replace("...", "").trim();
440
441 if (unserializedSource.length() > 1 && (unserializedSource.charAt(0) != '<'
442 || unserializedSource.charAt(unserializedSource.length() - 1) != '>'
443
444 || unserializedSource.contains("<!"))) {
445 continue;
446 }
447
448 final String code = buildXml(unserializedSource);
449
450 XmlUtil.getRawXml(fileName, code, unserializedSource);
451
452
453 assertWithMessage("Xml is invalid, old or has outdated structure")
454 .that(fileName.startsWith("anttask")
455 || fileName.startsWith("releasenotes")
456 || fileName.startsWith("writingjavadocchecks")
457 || isValidCheckstyleXml(fileName, code, unserializedSource))
458 .isTrue();
459 }
460 }
461 }
462
463 private static String buildXml(String unserializedSource) throws IOException {
464
465 String code = unserializedSource
466
467 .replace("target/cachefile", "target/cachefile-test");
468
469 if (!hasFileSetClass(code)) {
470 code = "<module name=\"TreeWalker\">\n" + code + "\n</module>";
471 }
472 if (!code.contains("name=\"Checker\"")) {
473 code = "<module name=\"Checker\">\n" + code + "\n</module>";
474 }
475 if (!code.startsWith("<?xml")) {
476 final String dtdPath = new File(
477 "src/main/resources/com/puppycrawl/tools/checkstyle/configuration_1_3.dtd")
478 .getCanonicalPath();
479
480 code = "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC "
481 + "\"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\" \"" + dtdPath
482 + "\">\n" + code;
483 }
484 return code;
485 }
486
487 private static boolean hasFileSetClass(String xml) {
488 boolean found = false;
489
490 for (String find : XML_FILESET_LIST) {
491 if (xml.contains(find)) {
492 found = true;
493 break;
494 }
495 }
496
497 return found;
498 }
499
500 private static boolean isValidCheckstyleXml(String fileName, String code,
501 String unserializedSource)
502 throws IOException, CheckstyleException {
503
504 if (!code.contains("com.mycompany") && !code.contains("checkstyle-packages")
505 && !code.contains("MethodLimit") && !code.contains("<suppress ")
506 && !code.contains("<suppress-xpath ")
507 && !code.contains("<import-control ")
508 && !unserializedSource.startsWith("<property ")
509 && !unserializedSource.startsWith("<taskdef ")) {
510
511 try {
512 final Properties properties = new Properties();
513
514 properties.setProperty("checkstyle.header.file",
515 new File("config/java.header").getCanonicalPath());
516
517 final PropertiesExpander expander = new PropertiesExpander(properties);
518 final Configuration config = ConfigurationLoader.loadConfiguration(new InputSource(
519 new StringReader(code)), expander, IgnoredModulesOptions.EXECUTE);
520 final Checker checker = new Checker();
521
522 try {
523 final ClassLoader moduleClassLoader = Checker.class.getClassLoader();
524 checker.setModuleClassLoader(moduleClassLoader);
525 checker.configure(config);
526 }
527 finally {
528 checker.destroy();
529 }
530 }
531 catch (CheckstyleException ex) {
532 throw new CheckstyleException(fileName + " has invalid Checkstyle xml ("
533 + ex.getMessage() + "): " + unserializedSource, ex);
534 }
535 }
536 return true;
537 }
538
539 @Test
540 public void testAllCheckSections() throws Exception {
541 final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
542
543 for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
544 final String fileName = path.getFileName().toString();
545
546 if ("config_system_properties.xml".equals(fileName)
547 || "index.xml".equals(fileName)) {
548 continue;
549 }
550
551 final String input = Files.readString(path);
552 final Document document = XmlUtil.getRawXml(fileName, input, input);
553 final NodeList sources = document.getElementsByTagName("section");
554 String lastSectionName = null;
555
556 for (int position = 0; position < sources.getLength(); position++) {
557 final Node section = sources.item(position);
558 final String sectionName = XmlUtil.getNameAttributeOfNode(section);
559
560 if ("Content".equals(sectionName) || "Overview".equals(sectionName)) {
561 assertWithMessage(fileName + " section '" + sectionName + "' should be first")
562 .that(lastSectionName)
563 .isNull();
564 continue;
565 }
566
567 assertWithMessage(
568 fileName + " section '" + sectionName + "' shouldn't end with 'Check'")
569 .that(sectionName.endsWith("Check"))
570 .isFalse();
571 if (lastSectionName != null) {
572 assertWithMessage(fileName + " section '" + sectionName
573 + "' is out of order compared to '" + lastSectionName + "'")
574 .that(sectionName.toLowerCase(Locale.ENGLISH).compareTo(
575 lastSectionName.toLowerCase(Locale.ENGLISH)) >= 0)
576 .isTrue();
577 }
578
579 validateCheckSection(moduleFactory, fileName, sectionName, section);
580
581 lastSectionName = sectionName;
582 }
583 }
584 }
585
586
587
588
589
590
591
592
593 @Test
594 public void testAllCheckSectionsEx() throws Exception {
595 final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
596
597 final Path path = Paths.get(XdocUtil.DIRECTORY_PATH + "/config.xml");
598 final String fileName = path.getFileName().toString();
599
600 final String input = Files.readString(path);
601 final Document document = XmlUtil.getRawXml(fileName, input, input);
602 final NodeList sources = document.getElementsByTagName("section");
603
604 for (int position = 0; position < sources.getLength(); position++) {
605 final Node section = sources.item(position);
606 final String sectionName = XmlUtil.getNameAttributeOfNode(section);
607
608 if (!"Checker".equals(sectionName) && !"TreeWalker".equals(sectionName)) {
609 continue;
610 }
611
612 validateCheckSection(moduleFactory, fileName, sectionName, section);
613 }
614 }
615
616 private static void validateCheckSection(ModuleFactory moduleFactory, String fileName,
617 String sectionName, Node section) throws Exception {
618 final Object instance;
619
620 try {
621 instance = moduleFactory.createModule(sectionName);
622 }
623 catch (CheckstyleException ex) {
624 throw new CheckstyleException(fileName + " couldn't find class: " + sectionName, ex);
625 }
626
627 int subSectionPos = 0;
628 for (Node subSection : XmlUtil.getChildrenElements(section)) {
629 if (subSectionPos == 0 && "p".equals(subSection.getNodeName())) {
630 validateSinceDescriptionSection(fileName, sectionName, subSection);
631 continue;
632 }
633
634 final String subSectionName = XmlUtil.getNameAttributeOfNode(subSection);
635
636
637 if ("Notes".equals(subSectionName)
638 || "Rule Description".equals(subSectionName)
639 || "Metadata".equals(subSectionName)) {
640 continue;
641 }
642
643
644 if (subSectionPos == 1 && !"Properties".equals(subSectionName)) {
645 validatePropertySection(fileName, sectionName, null, instance);
646 subSectionPos++;
647 }
648 if (subSectionPos == 4 && !"Violation Messages".equals(subSectionName)) {
649 validateViolationSection(fileName, sectionName, null, instance);
650 subSectionPos++;
651 }
652
653 assertWithMessage(fileName + " section '" + sectionName + "' should be in order")
654 .that(subSectionName)
655 .isEqualTo(getSubSectionName(subSectionPos));
656
657 switch (subSectionPos) {
658 case 0:
659 validateDescriptionSection(fileName, sectionName, subSection);
660 break;
661 case 1:
662 validatePropertySection(fileName, sectionName, subSection, instance);
663 break;
664 case 3:
665 validateUsageExample(fileName, sectionName, subSection);
666 break;
667 case 4:
668 validateViolationSection(fileName, sectionName, subSection, instance);
669 break;
670 case 5:
671 validatePackageSection(fileName, sectionName, subSection, instance);
672 break;
673 case 6:
674 validateParentSection(fileName, sectionName, subSection);
675 break;
676 case 2:
677 default:
678 break;
679 }
680
681 subSectionPos++;
682 }
683
684 if ("Checker".equals(sectionName)) {
685 assertWithMessage(fileName + " section '" + sectionName
686 + "' should contain up to 'Package' sub-section")
687 .that(subSectionPos)
688 .isGreaterThan(5);
689 }
690 else {
691 assertWithMessage(fileName + " section '" + sectionName
692 + "' should contain up to 'Parent' sub-section")
693 .that(subSectionPos)
694 .isGreaterThan(6);
695 }
696 }
697
698 private static void validateSinceDescriptionSection(String fileName, String sectionName,
699 Node subSection) {
700 assertWithMessage(fileName + " section '" + sectionName
701 + "' should have a valid version at the start of the description like:\n"
702 + DESCRIPTION_VERSION.pattern())
703 .that(DESCRIPTION_VERSION.matcher(subSection.getTextContent().trim()).find())
704 .isTrue();
705 }
706
707 private static Object getSubSectionName(int subSectionPos) {
708 final String result;
709
710 switch (subSectionPos) {
711 case 0:
712 result = "Description";
713 break;
714 case 1:
715 result = "Properties";
716 break;
717 case 2:
718 result = "Examples";
719 break;
720 case 3:
721 result = "Example of Usage";
722 break;
723 case 4:
724 result = "Violation Messages";
725 break;
726 case 5:
727 result = "Package";
728 break;
729 case 6:
730 result = "Parent Module";
731 break;
732 default:
733 result = null;
734 break;
735 }
736
737 return result;
738 }
739
740 private static void validateDescriptionSection(String fileName, String sectionName,
741 Node subSection) {
742 if ("config_filters.xml".equals(fileName) && "SuppressionXpathFilter".equals(sectionName)) {
743 validateListOfSuppressionXpathFilterIncompatibleChecks(subSection);
744 }
745 }
746
747 private static void validateListOfSuppressionXpathFilterIncompatibleChecks(Node subSection) {
748 assertWithMessage(
749 "Incompatible check list should match XpathRegressionTest.INCOMPATIBLE_CHECK_NAMES")
750 .that(getListById(subSection, "SuppressionXpathFilter_IncompatibleChecks"))
751 .isEqualTo(XpathRegressionTest.INCOMPATIBLE_CHECK_NAMES);
752 final Set<String> suppressionXpathFilterJavadocChecks = getListById(subSection,
753 "SuppressionXpathFilter_JavadocChecks");
754 assertWithMessage(
755 "Javadoc check list should match XpathRegressionTest.INCOMPATIBLE_JAVADOC_CHECK_NAMES")
756 .that(suppressionXpathFilterJavadocChecks)
757 .isEqualTo(XpathRegressionTest.INCOMPATIBLE_JAVADOC_CHECK_NAMES);
758 }
759
760 private static void validatePropertySection(String fileName, String sectionName,
761 Node subSection, Object instance) throws Exception {
762 final Set<String> properties = getProperties(instance.getClass());
763 final Class<?> clss = instance.getClass();
764
765 fixCapturedProperties(sectionName, instance, clss, properties);
766
767 if (subSection != null) {
768 assertWithMessage(fileName + " section '" + sectionName
769 + "' should have no properties to show")
770 .that(properties)
771 .isNotEmpty();
772
773 final Set<Node> nodes = XmlUtil.getChildrenElements(subSection);
774 assertWithMessage(fileName + " section '" + sectionName
775 + "' subsection 'Properties' should have one child node")
776 .that(nodes)
777 .hasSize(1);
778
779 final Node div = nodes.iterator().next();
780 assertWithMessage(fileName + " section '" + sectionName
781 + "' subsection 'Properties' has unexpected child node")
782 .that(div.getNodeName())
783 .isEqualTo("div");
784 final String wrapperMessage = fileName + " section '" + sectionName
785 + "' subsection 'Properties' wrapping div for table needs the"
786 + " class 'wrapper'";
787 assertWithMessage(wrapperMessage)
788 .that(div.hasAttributes())
789 .isTrue();
790 assertWithMessage(wrapperMessage)
791 .that(div.getAttributes().getNamedItem("class").getNodeValue())
792 .isNotNull();
793 assertWithMessage(wrapperMessage)
794 .that(div.getAttributes().getNamedItem("class").getNodeValue())
795 .contains("wrapper");
796
797 final Node table = XmlUtil.getFirstChildElement(div);
798 assertWithMessage(fileName + " section '" + sectionName
799 + "' subsection 'Properties' has unexpected child node")
800 .that(table.getNodeName())
801 .isEqualTo("table");
802
803 validatePropertySectionPropertiesOrder(fileName, sectionName, table, properties);
804
805 validatePropertySectionProperties(fileName, sectionName, table, instance,
806 properties);
807 }
808
809 assertWithMessage(
810 fileName + " section '" + sectionName + "' should show properties: " + properties)
811 .that(properties)
812 .isEmpty();
813 }
814
815 private static void validatePropertySectionPropertiesOrder(String fileName, String sectionName,
816 Node table, Set<String> properties) {
817 final Set<Node> rows = XmlUtil.getChildrenElements(table);
818 final List<String> orderedPropertyNames = new ArrayList<>(properties);
819 final List<String> tablePropertyNames = new ArrayList<>();
820
821
822 if (orderedPropertyNames.contains("javadocTokens")) {
823 orderedPropertyNames.remove("javadocTokens");
824 orderedPropertyNames.add("javadocTokens");
825 }
826 if (orderedPropertyNames.contains("tokens")) {
827 orderedPropertyNames.remove("tokens");
828 orderedPropertyNames.add("tokens");
829 }
830
831 rows
832 .stream()
833
834 .skip(1)
835 .forEach(row -> {
836 final List<Node> columns = new ArrayList<>(XmlUtil.getChildrenElements(row));
837 assertWithMessage(fileName + " section '" + sectionName
838 + "' should have the requested columns")
839 .that(columns)
840 .hasSize(5);
841
842 final String propertyName = columns.get(0).getTextContent();
843 tablePropertyNames.add(propertyName);
844 });
845
846 assertWithMessage(fileName + " section '" + sectionName
847 + "' should have properties in the requested order")
848 .that(tablePropertyNames)
849 .isEqualTo(orderedPropertyNames);
850 }
851
852 private static void fixCapturedProperties(String sectionName, Object instance, Class<?> clss,
853 Set<String> properties) {
854
855 if (hasParentModule(sectionName)) {
856 if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
857 properties.removeAll(JAVADOC_CHECK_PROPERTIES);
858
859
860 properties.add("violateExecutionOnNonTightHtml");
861 }
862 else if (AbstractCheck.class.isAssignableFrom(clss)) {
863 properties.removeAll(CHECK_PROPERTIES);
864 }
865 }
866 if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
867 properties.removeAll(FILESET_PROPERTIES);
868
869
870 properties.add("fileExtensions");
871 }
872
873
874 new HashSet<>(properties).stream()
875 .filter(prop -> UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + "." + prop))
876 .forEach(properties::remove);
877
878 if (AbstractCheck.class.isAssignableFrom(clss)) {
879 final AbstractCheck check = (AbstractCheck) instance;
880
881 final int[] acceptableTokens = check.getAcceptableTokens();
882 Arrays.sort(acceptableTokens);
883 final int[] defaultTokens = check.getDefaultTokens();
884 Arrays.sort(defaultTokens);
885 final int[] requiredTokens = check.getRequiredTokens();
886 Arrays.sort(requiredTokens);
887
888 if (!Arrays.equals(acceptableTokens, defaultTokens)
889 || !Arrays.equals(acceptableTokens, requiredTokens)) {
890 properties.add("tokens");
891 }
892 }
893
894 if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
895 final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
896
897 final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens();
898 Arrays.sort(acceptableJavadocTokens);
899 final int[] defaultJavadocTokens = check.getDefaultJavadocTokens();
900 Arrays.sort(defaultJavadocTokens);
901 final int[] requiredJavadocTokens = check.getRequiredJavadocTokens();
902 Arrays.sort(requiredJavadocTokens);
903
904 if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens)
905 || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) {
906 properties.add("javadocTokens");
907 }
908 }
909 }
910
911 private static void validatePropertySectionProperties(String fileName, String sectionName,
912 Node table, Object instance, Set<String> properties) throws Exception {
913 boolean skip = true;
914 boolean didJavadocTokens = false;
915 boolean didTokens = false;
916
917 for (Node row : XmlUtil.getChildrenElements(table)) {
918 final List<Node> columns = new ArrayList<>(XmlUtil.getChildrenElements(row));
919
920 assertWithMessage(fileName + " section '" + sectionName
921 + "' should have the requested columns")
922 .that(columns)
923 .hasSize(5);
924
925 if (skip) {
926 assertWithMessage(fileName + " section '" + sectionName
927 + "' should have the specific title")
928 .that(columns.get(0).getTextContent())
929 .isEqualTo("name");
930 assertWithMessage(fileName + " section '" + sectionName
931 + "' should have the specific title")
932 .that(columns.get(1).getTextContent())
933 .isEqualTo("description");
934 assertWithMessage(fileName + " section '" + sectionName
935 + "' should have the specific title")
936 .that(columns.get(2).getTextContent())
937 .isEqualTo("type");
938 assertWithMessage(fileName + " section '" + sectionName
939 + "' should have the specific title")
940 .that(columns.get(3).getTextContent())
941 .isEqualTo("default value");
942 assertWithMessage(fileName + " section '" + sectionName
943 + "' should have the specific title")
944 .that(columns.get(4).getTextContent())
945 .isEqualTo("since");
946
947 skip = false;
948 continue;
949 }
950
951 assertWithMessage(fileName + " section '" + sectionName
952 + "' should have token properties last")
953 .that(didTokens)
954 .isFalse();
955
956 final String propertyName = columns.get(0).getTextContent();
957 assertWithMessage(fileName + " section '" + sectionName
958 + "' should not contain the property: " + propertyName)
959 .that(properties.remove(propertyName))
960 .isTrue();
961
962 if ("tokens".equals(propertyName)) {
963 final AbstractCheck check = (AbstractCheck) instance;
964 validatePropertySectionPropertyTokens(fileName, sectionName, check, columns);
965 didTokens = true;
966 }
967 else if ("javadocTokens".equals(propertyName)) {
968 final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
969 validatePropertySectionPropertyJavadocTokens(fileName, sectionName, check, columns);
970 didJavadocTokens = true;
971 }
972 else {
973 assertWithMessage(fileName + " section '" + sectionName
974 + "' should have javadoc token properties next to last, before tokens")
975 .that(didJavadocTokens)
976 .isFalse();
977
978 validatePropertySectionPropertyEx(fileName, sectionName, instance, columns,
979 propertyName);
980 }
981
982 assertWithMessage("%s section '%s' should have a version for %s",
983 fileName, sectionName, propertyName)
984 .that(columns.get(4).getTextContent().trim())
985 .isNotEmpty();
986 assertWithMessage("%s section '%s' should have a valid version for %s",
987 fileName, sectionName, propertyName)
988 .that(columns.get(4).getTextContent().trim())
989 .matches(VERSION);
990 }
991 }
992
993 private static void validatePropertySectionPropertyEx(String fileName, String sectionName,
994 Object instance, List<Node> columns, String propertyName) throws Exception {
995 assertWithMessage("%s section '%s' should have a description for %s",
996 fileName, sectionName, propertyName)
997 .that(columns.get(1).getTextContent().trim())
998 .isNotEmpty();
999 assertWithMessage("%s section '%s' should have a description for %s"
1000 + " that starts with uppercase character",
1001 fileName, sectionName, propertyName)
1002 .that(Character.isUpperCase(columns.get(1).getTextContent().trim().charAt(0)))
1003 .isTrue();
1004
1005 final String actualTypeName = columns.get(2).getTextContent().replace("\n", "")
1006 .replace("\r", "").replaceAll(" +", " ").trim();
1007
1008 assertWithMessage(
1009 fileName + " section '" + sectionName + "' should have a type for " + propertyName)
1010 .that(actualTypeName)
1011 .isNotEmpty();
1012
1013 final Field field = getField(instance.getClass(), propertyName);
1014 final Class<?> fieldClass = getFieldClass(fileName, sectionName, instance, field,
1015 propertyName);
1016
1017 final String expectedTypeName = Optional.ofNullable(field)
1018 .map(nonNullField -> nonNullField.getAnnotation(XdocsPropertyType.class))
1019 .map(propertyType -> propertyType.value().getDescription())
1020 .orElse(fieldClass.getSimpleName());
1021 final String expectedValue = getModulePropertyExpectedValue(sectionName, propertyName,
1022 field, fieldClass, instance);
1023
1024 assertWithMessage(fileName + " section '" + sectionName
1025 + "' should have the type for " + propertyName)
1026 .that(actualTypeName)
1027 .isEqualTo(expectedTypeName);
1028
1029 if (expectedValue != null) {
1030 final String actualValue = columns.get(3).getTextContent().trim()
1031 .replaceAll("\\s+", " ")
1032 .replaceAll("\\s,", ",");
1033
1034 assertWithMessage(fileName + " section '" + sectionName
1035 + "' should have the value for " + propertyName)
1036 .that(actualValue)
1037 .isEqualTo(expectedValue);
1038 }
1039 }
1040
1041 private static void validatePropertySectionPropertyTokens(String fileName, String sectionName,
1042 AbstractCheck check, List<Node> columns) {
1043 assertWithMessage(fileName + " section '" + sectionName
1044 + "' should have the basic token description")
1045 .that(columns.get(1).getTextContent())
1046 .isEqualTo("tokens to check");
1047
1048 final String acceptableTokenText = columns.get(2).getTextContent().trim();
1049 String expectedAcceptableTokenText = "subset of tokens "
1050 + CheckUtil.getTokenText(check.getAcceptableTokens(),
1051 check.getRequiredTokens());
1052 if (isAllTokensAcceptable(check)) {
1053 expectedAcceptableTokenText = "set of any supported tokens";
1054 }
1055 assertWithMessage(fileName + " section '" + sectionName
1056 + "' should have all the acceptable tokens")
1057 .that(acceptableTokenText
1058 .replaceAll("\\s+", " ")
1059 .replaceAll("\\s,", ",")
1060 .replaceAll("\\s\\.", "."))
1061 .isEqualTo(expectedAcceptableTokenText);
1062 assertWithMessage(fileName + "'s acceptable token section: " + sectionName
1063 + "should have ',' & '.' at beginning of the next corresponding lines.")
1064 .that(isInvalidTokenPunctuation(acceptableTokenText))
1065 .isFalse();
1066
1067 final String defaultTokenText = columns.get(3).getTextContent().trim();
1068 final String expectedDefaultTokenText = CheckUtil.getTokenText(check.getDefaultTokens(),
1069 check.getRequiredTokens());
1070 if (expectedDefaultTokenText.isEmpty()) {
1071 assertWithMessage("Empty tokens should have 'empty' string in xdoc")
1072 .that(defaultTokenText)
1073 .isEqualTo("empty");
1074 }
1075 else {
1076 assertWithMessage(fileName + " section '" + sectionName
1077 + "' should have all the default tokens")
1078 .that(defaultTokenText
1079 .replaceAll("\\s+", " ")
1080 .replaceAll("\\s,", ",")
1081 .replaceAll("\\s\\.", "."))
1082 .isEqualTo(expectedDefaultTokenText);
1083 assertWithMessage(fileName + "'s default token section: " + sectionName
1084 + "should have ',' or '.' at beginning of the next corresponding lines.")
1085 .that(isInvalidTokenPunctuation(defaultTokenText))
1086 .isFalse();
1087 }
1088
1089 }
1090
1091 private static boolean isAllTokensAcceptable(AbstractCheck check) {
1092 return Arrays.equals(check.getAcceptableTokens(), TokenUtil.getAllTokenIds());
1093 }
1094
1095 private static void validatePropertySectionPropertyJavadocTokens(String fileName,
1096 String sectionName, AbstractJavadocCheck check, List<Node> columns) {
1097 assertWithMessage(fileName + " section '" + sectionName
1098 + "' should have the basic token javadoc description")
1099 .that(columns.get(1).getTextContent())
1100 .isEqualTo("javadoc tokens to check");
1101
1102 final String acceptableTokenText = columns.get(2).getTextContent().trim();
1103 assertWithMessage(fileName + " section '" + sectionName
1104 + "' should have all the acceptable javadoc tokens")
1105 .that(acceptableTokenText
1106 .replaceAll("\\s+", " ")
1107 .replaceAll("\\s,", ",")
1108 .replaceAll("\\s\\.", "."))
1109 .isEqualTo("subset of javadoc tokens "
1110 + CheckUtil.getJavadocTokenText(check.getAcceptableJavadocTokens(),
1111 check.getRequiredJavadocTokens()));
1112 assertWithMessage(fileName + "'s acceptable javadoc token section: " + sectionName
1113 + "should have ',' & '.' at beginning of the next corresponding lines.")
1114 .that(isInvalidTokenPunctuation(acceptableTokenText))
1115 .isFalse();
1116
1117 final String defaultTokenText = columns.get(3).getTextContent().trim();
1118 assertWithMessage(fileName + " section '" + sectionName
1119 + "' should have all the default javadoc tokens")
1120 .that(defaultTokenText
1121 .replaceAll("\\s+", " ")
1122 .replaceAll("\\s,", ",")
1123 .replaceAll("\\s\\.", "."))
1124 .isEqualTo(CheckUtil.getJavadocTokenText(check.getDefaultJavadocTokens(),
1125 check.getRequiredJavadocTokens()));
1126 assertWithMessage(fileName + "'s default javadoc token section: " + sectionName
1127 + "should have ',' & '.' at beginning of the next corresponding lines.")
1128 .that(isInvalidTokenPunctuation(defaultTokenText))
1129 .isFalse();
1130 }
1131
1132 private static boolean isInvalidTokenPunctuation(String tokenText) {
1133 return Pattern.compile("\\w,").matcher(tokenText).find()
1134 || Pattern.compile("\\w\\.").matcher(tokenText).find();
1135 }
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150 private static String getModulePropertyExpectedValue(String sectionName, String propertyName,
1151 Field field, Class<?> fieldClass, Object instance) throws Exception {
1152 String result = null;
1153
1154 if (field != null) {
1155 final Object value = field.get(instance);
1156
1157 if ("Checker".equals(sectionName) && "localeCountry".equals(propertyName)) {
1158 result = "default locale country for the Java Virtual Machine";
1159 }
1160 else if ("Checker".equals(sectionName) && "localeLanguage".equals(propertyName)) {
1161 result = "default locale language for the Java Virtual Machine";
1162 }
1163 else if ("Checker".equals(sectionName) && "charset".equals(propertyName)) {
1164 result = "UTF-8";
1165 }
1166 else if ("charset".equals(propertyName)) {
1167 result = "the charset property of the parent"
1168 + " <a href=\"https://checkstyle.org/config.html#Checker\">Checker</a> module";
1169 }
1170 else if ("PropertyCacheFile".equals(fieldClass.getSimpleName())) {
1171 result = "null (no cache file)";
1172 }
1173 else if (fieldClass == boolean.class) {
1174 result = value.toString();
1175 }
1176 else if (fieldClass == int.class) {
1177 result = value.toString();
1178 }
1179 else if (fieldClass == int[].class) {
1180 result = getIntArrayPropertyValue(value);
1181 }
1182 else if (fieldClass == double[].class) {
1183 result = Arrays.toString((double[]) value).replace("[", "").replace("]", "")
1184 .replace(".0", "");
1185 if (result.isEmpty()) {
1186 result = "{}";
1187 }
1188 }
1189 else if (fieldClass == String[].class) {
1190 result = getStringArrayPropertyValue(propertyName, value);
1191 }
1192 else if (fieldClass == URI.class || fieldClass == String.class) {
1193 if (value != null) {
1194 result = '"' + value.toString() + '"';
1195 }
1196 }
1197 else if (fieldClass == Pattern.class) {
1198 if (value != null) {
1199 result = '"' + value.toString().replace("\n", "\\n").replace("\t", "\\t")
1200 .replace("\r", "\\r").replace("\f", "\\f") + '"';
1201 }
1202 }
1203 else if (fieldClass == Pattern[].class) {
1204 result = getPatternArrayPropertyValue(value);
1205 }
1206 else if (fieldClass.isEnum()) {
1207 if (value != null) {
1208 result = value.toString().toLowerCase(Locale.ENGLISH);
1209 }
1210 }
1211 else if (fieldClass == AccessModifierOption[].class) {
1212 result = Arrays.toString((Object[]) value).replace("[", "").replace("]", "");
1213 }
1214 else {
1215 assertWithMessage("Unknown property type: " + fieldClass.getSimpleName()).fail();
1216 }
1217
1218 if (result == null) {
1219 result = "null";
1220 }
1221 }
1222
1223 return result;
1224 }
1225
1226
1227
1228
1229
1230
1231
1232 private static String getPatternArrayPropertyValue(Object fieldValue) {
1233 Object value = fieldValue;
1234 String result;
1235 if (value instanceof Collection) {
1236 final Collection<?> collection = (Collection<?>) value;
1237 final Pattern[] newArray = new Pattern[collection.size()];
1238 final Iterator<?> iterator = collection.iterator();
1239 int index = 0;
1240
1241 while (iterator.hasNext()) {
1242 final Object next = iterator.next();
1243 newArray[index] = (Pattern) next;
1244 index++;
1245 }
1246
1247 value = newArray;
1248 }
1249
1250 if (value != null && Array.getLength(value) > 0) {
1251 final String[] newArray = new String[Array.getLength(value)];
1252
1253 for (int i = 0; i < newArray.length; i++) {
1254 newArray[i] = ((Pattern) Array.get(value, i)).pattern();
1255 }
1256
1257 result = Arrays.toString(newArray).replace("[", "").replace("]", "");
1258 }
1259 else {
1260 result = "";
1261 }
1262
1263 if (result.isEmpty()) {
1264 result = "{}";
1265 }
1266 return result;
1267 }
1268
1269
1270
1271
1272
1273
1274
1275
1276 private static String getStringArrayPropertyValue(String propertyName, Object value) {
1277 String result;
1278 if (value == null) {
1279 result = "";
1280 }
1281 else {
1282 final Stream<?> valuesStream;
1283 if (value instanceof Collection) {
1284 final Collection<?> collection = (Collection<?>) value;
1285 valuesStream = collection.stream();
1286 }
1287 else {
1288 final Object[] array = (Object[]) value;
1289 valuesStream = Arrays.stream(array);
1290 }
1291 result = valuesStream
1292 .map(String.class::cast)
1293 .sorted()
1294 .collect(Collectors.joining(", "));
1295 }
1296
1297 if (result.isEmpty()) {
1298 if ("fileExtensions".equals(propertyName)) {
1299 result = "all files";
1300 }
1301 else {
1302 result = "{}";
1303 }
1304 }
1305 return result;
1306 }
1307
1308
1309
1310
1311
1312
1313
1314 private static String getIntArrayPropertyValue(Object value) {
1315 final IntStream stream;
1316 if (value instanceof Collection) {
1317 final Collection<?> collection = (Collection<?>) value;
1318 stream = collection.stream()
1319 .mapToInt(number -> (int) number);
1320 }
1321 else if (value instanceof BitSet) {
1322 stream = ((BitSet) value).stream();
1323 }
1324 else {
1325 stream = Arrays.stream((int[]) value);
1326 }
1327 String result = stream
1328 .mapToObj(TokenUtil::getTokenName)
1329 .sorted()
1330 .collect(Collectors.joining(", "));
1331 if (result.isEmpty()) {
1332 result = "{}";
1333 }
1334 return result;
1335 }
1336
1337
1338
1339
1340
1341
1342
1343
1344 private static Field getField(Class<?> fieldClass, String propertyName) {
1345 Field result = null;
1346 Class<?> currentClass = fieldClass;
1347
1348 while (!Object.class.equals(currentClass)) {
1349 try {
1350 result = currentClass.getDeclaredField(propertyName);
1351 result.trySetAccessible();
1352 break;
1353 }
1354 catch (NoSuchFieldException ignored) {
1355 currentClass = currentClass.getSuperclass();
1356 }
1357 }
1358
1359 return result;
1360 }
1361
1362 private static Class<?> getFieldClass(String fileName, String sectionName, Object instance,
1363 Field field, String propertyName) throws Exception {
1364 Class<?> result = null;
1365
1366 if (field != null) {
1367 result = field.getType();
1368 }
1369 if (result == null) {
1370 assertWithMessage(
1371 fileName + " section '" + sectionName + "' could not find field "
1372 + propertyName)
1373 .that(PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD)
1374 .contains(sectionName + "." + propertyName);
1375
1376 final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
1377 propertyName);
1378 result = descriptor.getPropertyType();
1379 }
1380 if (result == List.class || result == Set.class) {
1381 final ParameterizedType type = (ParameterizedType) field.getGenericType();
1382 final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
1383
1384 if (parameterClass == Integer.class) {
1385 result = int[].class;
1386 }
1387 else if (parameterClass == String.class) {
1388 result = String[].class;
1389 }
1390 else if (parameterClass == Pattern.class) {
1391 result = Pattern[].class;
1392 }
1393 else {
1394 assertWithMessage("Unknown parameterized type: " + parameterClass.getSimpleName())
1395 .fail();
1396 }
1397 }
1398 else if (result == BitSet.class) {
1399 result = int[].class;
1400 }
1401
1402 return result;
1403 }
1404
1405 private static Set<String> getListById(Node subSection, String id) {
1406 Set<String> result = null;
1407 final Node node = XmlUtil.findChildElementById(subSection, id);
1408 if (node != null) {
1409 result = XmlUtil.getChildrenElements(node)
1410 .stream()
1411 .map(Node::getTextContent)
1412 .collect(Collectors.toUnmodifiableSet());
1413 }
1414 return result;
1415 }
1416
1417 private static void validateViolationSection(String fileName, String sectionName,
1418 Node subSection,
1419 Object instance) throws Exception {
1420 final Class<?> clss = instance.getClass();
1421 final Set<Field> fields = CheckUtil.getCheckMessages(clss, true);
1422 final Set<String> list = new TreeSet<>();
1423
1424 for (Field field : fields) {
1425
1426 field.trySetAccessible();
1427
1428 list.add(field.get(null).toString());
1429 }
1430
1431 final StringBuilder expectedText = new StringBuilder(120);
1432
1433 for (String s : list) {
1434 expectedText.append(s);
1435 expectedText.append('\n');
1436 }
1437
1438 if (expectedText.length() > 0) {
1439 expectedText.append("All messages can be customized if the default message doesn't "
1440 + "suit you.\nPlease see the documentation to learn how to.");
1441 }
1442
1443 if (subSection == null) {
1444 assertWithMessage(fileName + " section '" + sectionName
1445 + "' should have the expected error keys")
1446 .that(expectedText.toString())
1447 .isEqualTo("");
1448 }
1449 else {
1450 final String subsectionTextContent = subSection.getTextContent()
1451 .replaceAll("\n\\s+", "\n")
1452 .replaceAll("\\s+", " ")
1453 .trim();
1454 assertWithMessage(fileName + " section '" + sectionName
1455 + "' should have the expected error keys")
1456 .that(subsectionTextContent)
1457 .isEqualTo(expectedText.toString().replaceAll("\n", " ").trim());
1458
1459 for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) {
1460 final String url = node.getAttributes().getNamedItem("href").getTextContent();
1461 final String linkText = node.getTextContent().trim();
1462 final String expectedUrl;
1463
1464 if ("see the documentation".equals(linkText)) {
1465 expectedUrl = "../../config.html#Custom_messages";
1466 }
1467 else {
1468 expectedUrl = "https://github.com/search?q="
1469 + "path%3Asrc%2Fmain%2Fresources%2F"
1470 + clss.getPackage().getName().replace(".", "%2F")
1471 + "%20path%3A**%2Fmessages*.properties+repo%3Acheckstyle%2F"
1472 + "checkstyle+%22" + linkText + "%22";
1473 }
1474
1475 assertWithMessage(fileName + " section '" + sectionName
1476 + "' should have matching url for '" + linkText + "'")
1477 .that(url)
1478 .isEqualTo(expectedUrl);
1479 }
1480 }
1481 }
1482
1483 private static void validateUsageExample(String fileName, String sectionName, Node subSection) {
1484 final String text = subSection.getTextContent().replace("Checkstyle Style", "")
1485 .replace("Google Style", "").replace("Sun Style", "").trim();
1486
1487 assertWithMessage(fileName + " section '" + sectionName
1488 + "' has unknown text in 'Example of Usage': " + text)
1489 .that(text)
1490 .isEmpty();
1491
1492 boolean hasCheckstyle = false;
1493 boolean hasGoogle = false;
1494 boolean hasSun = false;
1495
1496 for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) {
1497 final String url = node.getAttributes().getNamedItem("href").getTextContent();
1498 final String linkText = node.getTextContent().trim();
1499 String expectedUrl = null;
1500
1501 if ("Checkstyle Style".equals(linkText)) {
1502 hasCheckstyle = true;
1503 expectedUrl = "https://github.com/search?q="
1504 + "path%3Aconfig%20path%3A**%2Fcheckstyle-checks.xml+"
1505 + "repo%3Acheckstyle%2Fcheckstyle+" + sectionName;
1506 }
1507 else if ("Google Style".equals(linkText)) {
1508 hasGoogle = true;
1509 expectedUrl = "https://github.com/search?q="
1510 + "path%3Asrc%2Fmain%2Fresources%20path%3A**%2Fgoogle_checks.xml+"
1511 + "repo%3Acheckstyle%2Fcheckstyle+"
1512 + sectionName;
1513
1514 assertWithMessage(fileName + " section '" + sectionName
1515 + "' should be in google_checks.xml or not reference 'Google Style'")
1516 .that(GOOGLE_MODULES)
1517 .contains(sectionName);
1518 }
1519 else if ("Sun Style".equals(linkText)) {
1520 hasSun = true;
1521 expectedUrl = "https://github.com/search?q="
1522 + "path%3Asrc%2Fmain%2Fresources%20path%3A**%2Fsun_checks.xml+"
1523 + "repo%3Acheckstyle%2Fcheckstyle+"
1524 + sectionName;
1525
1526 assertWithMessage(fileName + " section '" + sectionName
1527 + "' should be in sun_checks.xml or not reference 'Sun Style'")
1528 .that(SUN_MODULES)
1529 .contains(sectionName);
1530 }
1531
1532 assertWithMessage(fileName + " section '" + sectionName
1533 + "' should have matching url")
1534 .that(url)
1535 .isEqualTo(expectedUrl);
1536 }
1537
1538 assertWithMessage(fileName + " section '" + sectionName
1539 + "' should have a checkstyle section")
1540 .that(hasCheckstyle)
1541 .isTrue();
1542 assertWithMessage(fileName + " section '" + sectionName
1543 + "' should have a google section since it is in it's config")
1544 .that(hasGoogle || !GOOGLE_MODULES.contains(sectionName))
1545 .isTrue();
1546 assertWithMessage(fileName + " section '" + sectionName
1547 + "' should have a sun section since it is in it's config")
1548 .that(hasSun || !SUN_MODULES.contains(sectionName))
1549 .isTrue();
1550 }
1551
1552 private static void validatePackageSection(String fileName, String sectionName,
1553 Node subSection, Object instance) {
1554 assertWithMessage(fileName + " section '" + sectionName
1555 + "' should have matching package")
1556 .that(subSection.getTextContent().trim())
1557 .isEqualTo(instance.getClass().getPackage().getName());
1558 }
1559
1560 private static void validateParentSection(String fileName, String sectionName,
1561 Node subSection) {
1562 final String expected;
1563
1564 if (!"TreeWalker".equals(sectionName) && hasParentModule(sectionName)) {
1565 expected = "TreeWalker";
1566 }
1567 else {
1568 expected = "Checker";
1569 }
1570
1571 assertWithMessage(fileName + " section '" + sectionName + "' should have matching parent")
1572 .that(subSection.getTextContent().trim())
1573 .isEqualTo(expected);
1574 }
1575
1576 private static boolean hasParentModule(String sectionName) {
1577 final String search = "\"" + sectionName + "\"";
1578 boolean result = true;
1579
1580 for (String find : XML_FILESET_LIST) {
1581 if (find.contains(search)) {
1582 result = false;
1583 break;
1584 }
1585 }
1586
1587 return result;
1588 }
1589
1590 private static Set<String> getProperties(Class<?> clss) {
1591 final Set<String> result = new TreeSet<>();
1592 final PropertyDescriptor[] map = PropertyUtils.getPropertyDescriptors(clss);
1593
1594 for (PropertyDescriptor p : map) {
1595 if (p.getWriteMethod() != null) {
1596 result.add(p.getName());
1597 }
1598 }
1599
1600 return result;
1601 }
1602
1603 @Test
1604 public void testAllStyleRules() throws Exception {
1605 for (Path path : XdocUtil.getXdocsStyleFilePaths(XdocUtil.getXdocsFilePaths())) {
1606 final String fileName = path.getFileName().toString();
1607 final String styleName = fileName.substring(0, fileName.lastIndexOf('_'));
1608 final String input = Files.readString(path);
1609 final Document document = XmlUtil.getRawXml(fileName, input, input);
1610 final NodeList sources = document.getElementsByTagName("tr");
1611
1612 final Set<String> styleChecks;
1613 switch (styleName) {
1614 case "google":
1615 styleChecks = new HashSet<>(GOOGLE_MODULES);
1616 break;
1617
1618 case "sun":
1619 styleChecks = new HashSet<>(SUN_MODULES);
1620 styleChecks.removeAll(IGNORED_SUN_MODULES);
1621 break;
1622
1623 default:
1624 assertWithMessage("Missing modules list for style file '" + fileName + "'")
1625 .fail();
1626 styleChecks = null;
1627 }
1628
1629 String lastRuleName = null;
1630 String[] lastRuleNumberParts = null;
1631
1632 for (int position = 0; position < sources.getLength(); position++) {
1633 final Node row = sources.item(position);
1634 final List<Node> columns = new ArrayList<>(
1635 XmlUtil.findChildElementsByTag(row, "td"));
1636
1637 if (columns.isEmpty()) {
1638 continue;
1639 }
1640
1641 final String ruleName = columns.get(1).getTextContent().trim();
1642 lastRuleNumberParts = validateRuleNameOrder(
1643 fileName, lastRuleName, lastRuleNumberParts, ruleName);
1644
1645 if (!"--".equals(ruleName)) {
1646 validateStyleAnchors(XmlUtil.findChildElementsByTag(columns.get(0), "a"),
1647 fileName, ruleName);
1648 }
1649
1650 validateStyleModules(XmlUtil.findChildElementsByTag(columns.get(2), "a"),
1651 XmlUtil.findChildElementsByTag(columns.get(3), "a"), styleChecks, styleName,
1652 ruleName);
1653
1654 lastRuleName = ruleName;
1655 }
1656
1657
1658 styleChecks.remove("BeforeExecutionExclusionFileFilter");
1659 styleChecks.remove("SuppressionFilter");
1660 styleChecks.remove("SuppressionXpathFilter");
1661 styleChecks.remove("SuppressionXpathSingleFilter");
1662 styleChecks.remove("TreeWalker");
1663 styleChecks.remove("Checker");
1664 styleChecks.remove("SuppressWithNearbyCommentFilter");
1665 styleChecks.remove("SuppressionCommentFilter");
1666 styleChecks.remove("SuppressWarningsFilter");
1667 styleChecks.remove("SuppressWarningsHolder");
1668
1669 assertWithMessage(
1670 fileName + " requires the following check(s) to appear: " + styleChecks)
1671 .that(styleChecks)
1672 .isEmpty();
1673 }
1674 }
1675
1676 private static String[] validateRuleNameOrder(String fileName, String lastRuleName,
1677 String[] lastRuleNumberParts, String ruleName) {
1678 final String[] ruleNumberParts = ruleName.split(" ", 2)[0].split("\\.");
1679
1680 if (lastRuleName != null) {
1681 final int ruleNumberPartsAmount = ruleNumberParts.length;
1682 final int lastRuleNumberPartsAmount = lastRuleNumberParts.length;
1683 final String outOfOrderReason = fileName + " rule '" + ruleName
1684 + "' is out of order compared to '" + lastRuleName + "'";
1685 boolean lastRuleNumberPartWasEqual = false;
1686 int partIndex;
1687 for (partIndex = 0; partIndex < ruleNumberPartsAmount; partIndex++) {
1688 if (lastRuleNumberPartsAmount <= partIndex) {
1689
1690
1691 break;
1692 }
1693
1694 final String ruleNumberPart = ruleNumberParts[partIndex];
1695 final String lastRuleNumberPart = lastRuleNumberParts[partIndex];
1696 final boolean ruleNumberPartsAreNumeric = IntStream.concat(
1697 ruleNumberPart.chars(),
1698 lastRuleNumberPart.chars()
1699 ).allMatch(Character::isDigit);
1700
1701 if (ruleNumberPartsAreNumeric) {
1702 final int numericRuleNumberPart = parseInt(ruleNumberPart);
1703 final int numericLastRuleNumberPart = parseInt(lastRuleNumberPart);
1704 assertWithMessage(outOfOrderReason)
1705 .that(numericRuleNumberPart)
1706 .isAtLeast(numericLastRuleNumberPart);
1707 }
1708 else {
1709 assertWithMessage(outOfOrderReason)
1710 .that(ruleNumberPart.compareToIgnoreCase(lastRuleNumberPart))
1711 .isAtLeast(0);
1712 }
1713 lastRuleNumberPartWasEqual = ruleNumberPart.equalsIgnoreCase(lastRuleNumberPart);
1714 if (!lastRuleNumberPartWasEqual) {
1715
1716
1717 break;
1718 }
1719 }
1720 if (ruleNumberPartsAmount == partIndex && lastRuleNumberPartWasEqual) {
1721 if (lastRuleNumberPartsAmount == partIndex) {
1722 assertWithMessage(fileName + " rule '" + ruleName + "' and rule '"
1723 + lastRuleName + "' have the same rule number").fail();
1724 }
1725 else {
1726 assertWithMessage(outOfOrderReason).fail();
1727 }
1728 }
1729 }
1730
1731 return ruleNumberParts;
1732 }
1733
1734 private static void validateStyleAnchors(Set<Node> anchors, String fileName, String ruleName) {
1735 assertWithMessage(fileName + " rule '" + ruleName + "' must have two row anchors")
1736 .that(anchors)
1737 .hasSize(2);
1738
1739 final int space = ruleName.indexOf(' ');
1740 assertWithMessage(fileName + " rule '" + ruleName
1741 + "' must have have a space between the rule's number and the rule's name")
1742 .that(space)
1743 .isNotEqualTo(-1);
1744
1745 final String ruleNumber = ruleName.substring(0, space);
1746
1747 int position = 1;
1748
1749 for (Node anchor : anchors) {
1750 final String actualUrl;
1751 final String expectedUrl;
1752
1753 if (position == 1) {
1754 actualUrl = XmlUtil.getNameAttributeOfNode(anchor);
1755 expectedUrl = ruleNumber;
1756 }
1757 else {
1758 actualUrl = anchor.getAttributes().getNamedItem("href").getTextContent();
1759 expectedUrl = "#" + ruleNumber;
1760 }
1761
1762 assertWithMessage(fileName + " rule '" + ruleName + "' anchor "
1763 + position + " should have matching name/url")
1764 .that(actualUrl)
1765 .isEqualTo(expectedUrl);
1766
1767 position++;
1768 }
1769 }
1770
1771 private static void validateStyleModules(Set<Node> checks, Set<Node> configs,
1772 Set<String> styleChecks, String styleName, String ruleName) {
1773 final Iterator<Node> itrChecks = checks.iterator();
1774 final Iterator<Node> itrConfigs = configs.iterator();
1775
1776 while (itrChecks.hasNext()) {
1777 final Node module = itrChecks.next();
1778 final String moduleName = module.getTextContent().trim();
1779 final String href = module.getAttributes().getNamedItem("href").getTextContent();
1780
1781 final boolean moduleIsConfig = href.startsWith("config_");
1782 final boolean moduleIsCheck = href.startsWith("checks/");
1783
1784 if (!moduleIsConfig && !moduleIsCheck) {
1785 continue;
1786 }
1787
1788 assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '" + moduleName
1789 + "' shouldn't end with 'Check'")
1790 .that(moduleName.endsWith("Check"))
1791 .isFalse();
1792
1793 styleChecks.remove(moduleName);
1794
1795 for (String configName : new String[] {"config", "test"}) {
1796 Node config = null;
1797
1798 try {
1799 config = itrConfigs.next();
1800 }
1801 catch (NoSuchElementException ignore) {
1802 assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
1803 + moduleName + "' is missing the config link: " + configName).fail();
1804 }
1805
1806 assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
1807 + moduleName + "' has mismatched config/test links")
1808 .that(config.getTextContent().trim())
1809 .isEqualTo(configName);
1810
1811 final String configUrl = config.getAttributes().getNamedItem("href")
1812 .getTextContent();
1813
1814 if ("config".equals(configName)) {
1815 final String expectedUrl = "https://github.com/search?q="
1816 + "path%3Asrc%2Fmain%2Fresources%20path%3A**%2F" + styleName
1817 + "_checks.xml+repo%3Acheckstyle%2Fcheckstyle+" + moduleName;
1818
1819 assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
1820 + moduleName + "' should have matching " + configName + " url")
1821 .that(configUrl)
1822 .isEqualTo(expectedUrl);
1823 }
1824 else if ("test".equals(configName)) {
1825 assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
1826 + moduleName + "' should have matching " + configName + " url")
1827 .that(configUrl)
1828 .startsWith("https://github.com/checkstyle/checkstyle/"
1829 + "blob/master/src/it/java/com/" + styleName
1830 + "/checkstyle/test/");
1831 assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
1832 + moduleName + "' should have matching " + configName + " url")
1833 .that(configUrl)
1834 .endsWith("/" + moduleName + "Test.java");
1835
1836 assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' module '"
1837 + moduleName + "' should have a test that exists")
1838 .that(new File(configUrl.substring(53).replace('/',
1839 File.separatorChar)).exists())
1840 .isTrue();
1841 }
1842 }
1843 }
1844
1845 assertWithMessage(styleName + "_style.xml rule '" + ruleName + "' has too many configs")
1846 .that(itrConfigs.hasNext())
1847 .isFalse();
1848 }
1849
1850 @Test
1851 public void testAllExampleMacrosHaveParagraphWithIdBeforeThem() throws Exception {
1852 for (Path path : XdocUtil.getXdocsTemplatesFilePaths()) {
1853 final String fileName = path.getFileName().toString();
1854 final String input = Files.readString(path);
1855 final Document document = XmlUtil.getRawXml(fileName, input, input);
1856 final NodeList sources = document.getElementsByTagName("macro");
1857
1858 for (int position = 0; position < sources.getLength(); position++) {
1859 final Node macro = sources.item(position);
1860 final String macroName = macro.getAttributes()
1861 .getNamedItem("name").getTextContent();
1862
1863 if (!"example".equals(macroName)) {
1864 continue;
1865 }
1866
1867 final Node precedingParagraph = getPrecedingParagraph(macro);
1868 assertWithMessage(fileName
1869 + ": paragraph before example macro should have an id attribute")
1870 .that(precedingParagraph.hasAttributes())
1871 .isTrue();
1872
1873 final Node idAttribute = precedingParagraph.getAttributes().getNamedItem("id");
1874 assertWithMessage(fileName
1875 + ": paragraph before example macro should have an id attribute")
1876 .that(idAttribute)
1877 .isNotNull();
1878
1879 validatePrecedingParagraphId(macro, fileName, idAttribute);
1880 }
1881 }
1882 }
1883
1884 private static void validatePrecedingParagraphId(
1885 Node macro, String fileName, Node idAttribute) {
1886 String exampleName = "";
1887 String exampleType = "";
1888 final NodeList params = macro.getChildNodes();
1889 for (int paramPosition = 0; paramPosition < params.getLength(); paramPosition++) {
1890 final Node item = params.item(paramPosition);
1891
1892 if (!"param".equals(item.getNodeName())) {
1893 continue;
1894 }
1895
1896 final String paramName = item.getAttributes()
1897 .getNamedItem("name").getTextContent();
1898 final String paramValue = item.getAttributes()
1899 .getNamedItem("value").getTextContent();
1900 if ("path".equals(paramName)) {
1901 exampleName = paramValue.substring(paramValue.lastIndexOf('/') + 1,
1902 paramValue.lastIndexOf('.'));
1903 }
1904 else if ("type".equals(paramName)) {
1905 exampleType = paramValue;
1906 }
1907 }
1908
1909 final String id = idAttribute.getTextContent();
1910 final String expectedId = String.format(Locale.ROOT, "%s-%s", exampleName,
1911 exampleType);
1912 assertWithMessage(fileName
1913 + ": paragraph before example macro should have the expected id value")
1914 .that(id)
1915 .isEqualTo(expectedId);
1916 }
1917
1918 private static Node getPrecedingParagraph(Node macro) {
1919 Node precedingNode = macro.getPreviousSibling();
1920 while (!"p".equals(precedingNode.getNodeName())) {
1921 precedingNode = precedingNode.getPreviousSibling();
1922 }
1923 return precedingNode;
1924 }
1925 }