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