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