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