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