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