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.net.URLEncoder;
35 import java.nio.charset.StandardCharsets;
36 import java.nio.file.Files;
37 import java.nio.file.Path;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.BitSet;
41 import java.util.Collection;
42 import java.util.Collections;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.Iterator;
46 import java.util.List;
47 import java.util.Locale;
48 import java.util.Map;
49 import java.util.NoSuchElementException;
50 import java.util.Optional;
51 import java.util.Properties;
52 import java.util.Set;
53 import java.util.TreeSet;
54 import java.util.regex.Matcher;
55 import java.util.regex.Pattern;
56 import java.util.stream.Collectors;
57 import java.util.stream.IntStream;
58 import java.util.stream.Stream;
59
60 import javax.xml.parsers.DocumentBuilder;
61 import javax.xml.parsers.DocumentBuilderFactory;
62
63 import org.apache.commons.beanutils.PropertyUtils;
64 import org.junit.jupiter.api.Test;
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.XdocUtil;
87 import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
88 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
89
90
91
92
93 public class XdocsPagesTest {
94
95 private static final Path SITE_PATH = Path.of("src/site/site.xml");
96 private static final Path CHECKSTYLE_JS_PATH = Path.of(
97 "src/site/resources/js/checkstyle.js");
98
99 private static final Path AVAILABLE_CHECKS_PATH = Path.of("src/site/xdoc/checks.xml");
100 private static final Path AVAILABLE_FILE_FILTERS_PATH = Path.of(
101 "src/site/xdoc/filefilters/index.xml");
102 private static final Path AVAILABLE_FILTERS_PATH = Path.of("src/site/xdoc/filters/index.xml");
103
104 private static final Pattern VERSION = Pattern.compile("\\d+\\.\\d+(\\.\\d+)?");
105
106 private static final Pattern DESCRIPTION_VERSION = Pattern
107 .compile("^Since Checkstyle \\d+\\.\\d+(\\.\\d+)?");
108
109 private static final Pattern END_OF_SENTENCE = Pattern.compile("(.*?\\.)\\s", Pattern.DOTALL);
110
111
112 private static final Pattern EXAMPLE_ID_PATTERN =
113 Pattern.compile("^((?:Example|UseCase)\\d+)-config$");
114
115
116 private static final Pattern TAG_PATTERN = Pattern.compile("</?(?!code\\b)[a-zA-Z][^>]*>");
117
118 private static final List<String> XML_FILESET_LIST = List.of(
119 "TreeWalker",
120 "name=\"Checker\"",
121 "name=\"Header\"",
122 "name=\"LineLength\"",
123 "name=\"Translation\"",
124 "name=\"SeverityMatchFilter\"",
125 "name=\"SuppressWithNearbyTextFilter\"",
126 "name=\"SuppressWithPlainTextCommentFilter\"",
127 "name=\"SuppressionFilter\"",
128 "name=\"SuppressionSingleFilter\"",
129 "name=\"SuppressWarningsFilter\"",
130 "name=\"BeforeExecutionExclusionFileFilter\"",
131 "name=\"RegexpHeader\"",
132 "name=\"MultiFileRegexpHeader\"",
133 "name=\"RegexpOnFilename\"",
134 "name=\"RegexpSingleline\"",
135 "name=\"RegexpMultiline\"",
136 "name=\"JavadocPackage\"",
137 "name=\"LineEnding\"",
138 "name=\"NewlineAtEndOfFile\"",
139 "name=\"OrderedProperties\"",
140 "name=\"UniqueProperties\"",
141 "name=\"FileLength\"",
142 "name=\"FileTabCharacter\""
143 );
144
145 private static final Set<String> CHECK_PROPERTIES = getProperties(AbstractCheck.class);
146 private static final Set<String> JAVADOC_CHECK_PROPERTIES =
147 getProperties(AbstractJavadocCheck.class);
148 private static final Set<String> FILESET_PROPERTIES = getProperties(AbstractFileSetCheck.class);
149
150 private static final Set<String> UNDOCUMENTED_PROPERTIES = Set.of(
151 "Checker.classLoader",
152 "Checker.classloader",
153 "Checker.moduleClassLoader",
154 "Checker.moduleFactory",
155 "TreeWalker.classLoader",
156 "TreeWalker.moduleFactory",
157 "TreeWalker.cacheFile",
158 "TreeWalker.upChild",
159 "SuppressWithNearbyCommentFilter.fileContents",
160 "SuppressionCommentFilter.fileContents"
161 );
162
163 private static final Set<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Set.of(
164
165 "SuppressWarningsHolder.aliasList",
166
167 "Header.header",
168 "RegexpHeader.header",
169
170 "RedundantModifier.jdkVersion",
171
172 "CustomImportOrder.customImportOrderRules"
173 );
174
175 private static final Set<String> SUN_MODULES = Collections.unmodifiableSet(
176 CheckUtil.getConfigSunStyleModules());
177
178
179
180 private static final Set<String> IGNORED_SUN_MODULES = Set.of(
181 "ArrayTypeStyle",
182 "AvoidNestedBlocks",
183 "AvoidStarImport",
184 "ConstantName",
185 "DesignForExtension",
186 "EmptyBlock",
187 "EmptyForIteratorPad",
188 "EmptyStatement",
189 "EqualsHashCode",
190 "FileLength",
191 "FileTabCharacter",
192 "FinalClass",
193 "FinalParameters",
194 "GenericWhitespace",
195 "HiddenField",
196 "HideUtilityClassConstructor",
197 "IllegalImport",
198 "IllegalInstantiation",
199 "InnerAssignment",
200 "InterfaceIsType",
201 "JavadocMethod",
202 "JavadocPackage",
203 "JavadocType",
204 "JavadocVariable",
205 "LeftCurly",
206 "LineLength",
207 "LocalFinalVariableName",
208 "LocalVariableName",
209 "MagicNumber",
210 "MemberName",
211 "MethodLength",
212 "MethodName",
213 "MethodParamPad",
214 "MissingJavadocMethod",
215 "MissingSwitchDefault",
216 "ModifierOrder",
217 "NeedBraces",
218 "NewlineAtEndOfFile",
219 "NoWhitespaceAfter",
220 "NoWhitespaceBefore",
221 "OperatorWrap",
222 "PackageName",
223 "ParameterName",
224 "ParameterNumber",
225 "ParenPad",
226 "RedundantImport",
227 "RedundantModifier",
228 "RegexpSingleline",
229 "RightCurly",
230 "SimplifyBooleanExpression",
231 "SimplifyBooleanReturn",
232 "StaticVariableName",
233 "TodoComment",
234 "Translation",
235 "TypecastParenPad",
236 "TypeName",
237 "UnusedImports",
238 "UpperEll",
239 "VisibilityModifier",
240 "WhitespaceAfter",
241 "WhitespaceAround"
242 );
243
244 private static final Set<String> GOOGLE_MODULES = Collections.unmodifiableSet(
245 CheckUtil.getConfigGoogleStyleModules());
246
247
248 private static final Set<String> IGNORED_GOOGLE_MODULES = Set.of(
249 "RegexpSingleline"
250 );
251
252 private static final Set<String> OPENJDK_MODULES = Collections.unmodifiableSet(
253 CheckUtil.getConfigOpenJdkStyleModules());
254
255 private static final Set<String> DOC_COMMENTS_MODULES = Collections.unmodifiableSet(
256 CheckUtil.getConfigDocCommentsStyleModules());
257
258
259
260
261
262
263
264 private static final Set<String> ALLOWED_EXAMPLES_WITHOUT_SEPARATOR = Set.of(
265 "newlineatendoffile.xml.template:Example4:Example6"
266 );
267
268 private static final Set<String> NON_MODULE_XDOC = Set.of(
269 "config-system-properties.xml",
270 "sponsoring.xml",
271 "consulting.xml",
272 "index.xml",
273 "extending.xml",
274 "contributing.xml",
275 "running.xml",
276 "checks.xml",
277 "property-types.xml",
278 "google-style.xml",
279 "openjdk-style.xml",
280 "sun-style.xml",
281 "doc-comments-style.xml",
282 "style-configs.xml",
283 "writing-filters.xml",
284 "writing-filefilters.xml",
285 "eclipse.xml",
286 "netbeans.xml",
287 "idea.xml",
288 "beginning-development.xml",
289 "writing-checks.xml",
290 "config.xml",
291 "report-issue.xml",
292 "result-reports.xml",
293 "xpath.xml",
294 "google_style.xml",
295 "openjdk_style.xml",
296 "sun_style.xml",
297 "property_types.xml",
298 "releasenotes.xml",
299 "report_issue.xml",
300 "result_reports.xml",
301 "style_configs.xml",
302 "writingchecks.xml",
303 "writingfilefilters.xml",
304 "writingfilters.xml",
305 "writingjavadocchecks.xml",
306 "writinglisteners.xml",
307 "anttask.xml",
308 "beginning_development.xml",
309 "doc_comments_style.xml"
310 );
311
312 private static final String NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH =
313 " names must be in alphabetical order at " + SITE_PATH;
314
315 @Test
316 public void testAllChecksPresentOnAvailableChecksPage() throws Exception {
317 final String availableChecks = Files.readString(AVAILABLE_CHECKS_PATH);
318
319 CheckUtil.getSimpleNames(CheckUtil.getCheckstyleChecks())
320 .forEach(checkName -> {
321 if (!isPresent(availableChecks, checkName)) {
322 assertWithMessage(
323 "%s is not correctly listed on Available Checks page - add it to %s",
324 checkName, AVAILABLE_CHECKS_PATH).fail();
325 }
326 });
327 }
328
329 private static boolean isPresent(String availableChecks, String checkName) {
330 final String linkPattern = String.format(Locale.ROOT,
331 "(?s).*<a href=\"[^\"]+#%1$s\">([\\r\\n\\s])*%1$s([\\r\\n\\s])*</a>.*",
332 checkName);
333 return availableChecks.matches(linkPattern);
334 }
335
336 @Test
337 public void testAllConfigsHaveLinkInSite() throws Exception {
338 final String siteContent = Files.readString(SITE_PATH);
339
340 for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
341 final String expectedFile = path.toString()
342 .replace(".xml", ".html")
343 .replaceAll("\\\\", "/")
344 .replaceAll("src[\\\\/]site[\\\\/]xdoc[\\\\/]", "");
345 final boolean isConfigHtmlFile = Pattern.matches("config_[a-z]+.html", expectedFile);
346 final boolean isChecksIndexHtmlFile = "checks/index.html".equals(expectedFile);
347 final boolean isOldReleaseNotes = path.toString().contains("release-notes-");
348 final boolean isInnerPage = "report-issue.html".equals(expectedFile);
349 final boolean isRedirectStub = Set.of(
350 "google_style.html",
351 "openjdk_style.html",
352 "sun_style.html",
353 "property_types.html",
354 "releasenotes.html",
355 "report_issue.html",
356 "result_reports.html",
357 "style_configs.html",
358 "writingchecks.html",
359 "writingfilefilters.html",
360 "writingfilters.html",
361 "writingjavadocchecks.html",
362 "writinglisteners.html",
363 "anttask.html",
364 "beginning_development.html",
365 "doc_comments_style.html"
366 ).contains(expectedFile);
367
368 if (!isConfigHtmlFile && !isChecksIndexHtmlFile
369 && !isOldReleaseNotes && !isInnerPage && !isRedirectStub) {
370 final String expectedLink = String.format(Locale.ROOT, "href=\"%s\"", expectedFile);
371 assertWithMessage("Expected to find link to '%s' in %s", expectedLink, SITE_PATH)
372 .that(siteContent)
373 .contains(expectedLink);
374 }
375 }
376 }
377
378 @Test
379 public void testAllModulesPageInSyncWithModuleSummaries() throws Exception {
380 validateModulesSyncWithTheirSummaries(AVAILABLE_CHECKS_PATH,
381 (Path path) -> {
382 final String fileName = path.getFileName().toString();
383 return isNonModulePage(fileName) || !path.toString().contains("checks");
384 });
385
386 validateModulesSyncWithTheirSummaries(AVAILABLE_FILTERS_PATH,
387 (Path path) -> {
388 final String fileName = path.getFileName().toString();
389 return isNonModulePage(fileName)
390 || path.toString().contains("checks")
391 || path.toString().contains("filefilters");
392 });
393
394 validateModulesSyncWithTheirSummaries(AVAILABLE_FILE_FILTERS_PATH,
395 (Path path) -> {
396 final String fileName = path.getFileName().toString();
397 return isNonModulePage(fileName) || !path.toString().contains("filefilters");
398 });
399 }
400
401 private static void validateModulesSyncWithTheirSummaries(Path availablePagePath,
402 PredicateProcess skipPredicate)
403 throws Exception {
404 for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
405 if (skipPredicate.hasFit(path)) {
406 continue;
407 }
408
409 final String fileName = path.getFileName().toString();
410 final Map<String, String> summaries = readSummaries(availablePagePath);
411 final NodeList subsectionSources = getTagSourcesNode(path, "subsection");
412
413 for (int position = 0; position < subsectionSources.getLength(); position++) {
414 final Node subsection = subsectionSources.item(position);
415 final String subsectionName = XmlUtil.getNameAttributeOfNode(subsection);
416 if (!"Description".equals(subsectionName)) {
417 continue;
418 }
419
420 final String moduleName = XmlUtil.getNameAttributeOfNode(
421 subsection.getParentNode());
422 final Matcher matcher = END_OF_SENTENCE.matcher(subsection.getTextContent());
423 assertWithMessage(
424 "The first sentence of the \"Description\" subsection for "
425 + "the module %s in the file \"%s\" should end with a period",
426 moduleName, fileName)
427 .that(matcher.find())
428 .isTrue();
429
430 final String firstSentence = XmlUtil.sanitizeXml(matcher.group(1));
431
432 assertWithMessage(
433 "The summary for module %s in the file \"%s\" "
434 + "should match the first sentence of "
435 + "the \"Description\" subsection for this module in the file \"%s\"",
436 moduleName, availablePagePath, fileName)
437 .that(summaries.get(moduleName))
438 .isEqualTo(firstSentence);
439 }
440 }
441 }
442
443 @Test
444 public void testCategoryIndexPageTableInSyncWithAllChecksPageTable() throws Exception {
445 final Map<String, String> summaries = readSummaries(AVAILABLE_CHECKS_PATH);
446 for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
447 final String fileName = path.getFileName().toString();
448 if (!"index.xml".equals(fileName)
449
450
451
452 || path.getParent().toString().contains("filters")) {
453 continue;
454 }
455
456 final NodeList sources = getTagSourcesNode(path, "tr");
457
458 for (int position = 0; position < sources.getLength(); position++) {
459 final Node tableRow = sources.item(position);
460 final Iterator<Node> cells = XmlUtil
461 .findChildElementsByTag(tableRow, "td").iterator();
462 final String checkName = XmlUtil.sanitizeXml(cells.next().getTextContent());
463 final String description = XmlUtil.sanitizeXml(cells.next().getTextContent());
464 assertWithMessage(
465 "The summary for check %s in the file \"%s\" "
466 + "should match the summary for this check in the file \"%s\"",
467 checkName, path, AVAILABLE_CHECKS_PATH)
468 .that(description)
469 .isEqualTo(summaries.get(checkName));
470 }
471 }
472 }
473
474 @Test
475 public void testAllFiltersIndexPageTable() throws Exception {
476 validateFilterTypeIndexPage(AVAILABLE_FILTERS_PATH);
477 validateFilterTypeIndexPage(AVAILABLE_FILE_FILTERS_PATH);
478 }
479
480 private static void validateFilterTypeIndexPage(Path availablePath)
481 throws Exception {
482 final NodeList tableRowSources = getTagSourcesNode(availablePath, "tr");
483
484 for (int position = 0; position < tableRowSources.getLength(); position++) {
485 final Node tableRow = tableRowSources.item(position);
486 final Iterator<Node> tdCells = XmlUtil
487 .findChildElementsByTag(tableRow, "td").iterator();
488
489 assertWithMessage("Filter name cell at row %s in %s should exist", position + 1,
490 availablePath)
491 .that(tdCells.hasNext())
492 .isTrue();
493 final Node nameCell = tdCells.next();
494 final String filterName = XmlUtil.sanitizeXml(nameCell.getTextContent().trim());
495
496 assertWithMessage("Description cell for %s in index.xml should exist", filterName)
497 .that(tdCells.hasNext())
498 .isTrue();
499
500 assertWithMessage("Filter name at row %s in %s should not be empty", position + 1,
501 availablePath)
502 .that(filterName)
503 .isNotEmpty();
504
505 final Node descriptionCell = tdCells.next();
506 final String description = XmlUtil.sanitizeXml(
507 descriptionCell.getTextContent().trim());
508
509 assertWithMessage("Filter description for %s in %s should not be empty", filterName,
510 availablePath)
511 .that(description)
512 .isNotEmpty();
513
514 assertWithMessage("Filter description for %s in %s should end with a period",
515 filterName, availablePath)
516 .that(description.charAt(description.length() - 1))
517 .isEqualTo('.');
518 }
519 }
520
521 private static NodeList getTagSourcesNode(Path availablePath, String tagName)
522 throws Exception {
523 final String input = Files.readString(availablePath);
524 final Document document = XmlUtil.getRawXml(
525 availablePath.toString(), input, input);
526
527 return document.getElementsByTagName(tagName);
528 }
529
530 @Test
531 public void testAlphabetOrderInNames() throws Exception {
532 final NodeList nodes = getTagSourcesNode(SITE_PATH, "item");
533
534 for (int nodeIndex = 0; nodeIndex < nodes.getLength(); nodeIndex++) {
535 final Node current = nodes.item(nodeIndex);
536
537 if ("Checks".equals(XmlUtil.getNameAttributeOfNode(current))) {
538 final List<String> groupNames = getNames(current);
539 final List<String> groupNamesSorted = groupNames.stream()
540 .sorted()
541 .toList();
542
543 assertWithMessage("Group%s", NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH)
544 .that(groupNames)
545 .containsExactlyElementsIn(groupNamesSorted)
546 .inOrder();
547
548 Node groupNode = current.getFirstChild();
549 int index = 0;
550 final int totalGroups = XmlUtil.getChildrenElements(current).size();
551 while (index < totalGroups) {
552 if ("item".equals(groupNode.getNodeName())) {
553 final List<String> checkNames = getNames(groupNode);
554 final List<String> checkNamesSorted = checkNames.stream()
555 .sorted()
556 .toList();
557 assertWithMessage("Check%s", NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH)
558 .that(checkNames)
559 .containsExactlyElementsIn(checkNamesSorted)
560 .inOrder();
561 index++;
562 }
563 groupNode = groupNode.getNextSibling();
564 }
565 }
566 if ("Filters".equals(XmlUtil.getNameAttributeOfNode(current))) {
567 final List<String> filterNames = getNames(current);
568 final List<String> filterNamesSorted = filterNames.stream()
569 .sorted()
570 .toList();
571 assertWithMessage("Filter%s", NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH)
572 .that(filterNames)
573 .containsExactlyElementsIn(filterNamesSorted)
574 .inOrder();
575 }
576 if ("File Filters".equals(XmlUtil.getNameAttributeOfNode(current))) {
577 final List<String> fileFilterNames = getNames(current);
578 final List<String> fileFilterNamesSorted = fileFilterNames.stream()
579 .sorted()
580 .toList();
581 assertWithMessage("File Filter%s", NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH)
582 .that(fileFilterNames)
583 .containsExactlyElementsIn(fileFilterNamesSorted)
584 .inOrder();
585 }
586 }
587 }
588
589 @Test
590 public void testAlphabetOrderAtIndexPages() throws Exception {
591 final Path allChecks = Path.of("src/site/xdoc/checks.xml");
592 validateOrder(allChecks, "Check");
593
594 final String[] groupNames = {"annotation", "blocks", "design",
595 "coding", "header", "imports", "javadoc", "metrics",
596 "misc", "modifier", "naming", "regexp", "sizes", "whitespace"};
597 for (String name : groupNames) {
598 final Path checks = Path.of("src/site/xdoc/checks/" + name + "/index.xml");
599 validateOrder(checks, "Check");
600 }
601 validateOrder(AVAILABLE_FILTERS_PATH, "Filter");
602
603 final Path fileFilters = Path.of("src/site/xdoc/filefilters/index.xml");
604 validateOrder(fileFilters, "File Filter");
605 }
606
607 public static void validateOrder(Path path, String name) throws Exception {
608 final NodeList nodes = getTagSourcesNode(path, "div");
609
610 for (int nodeIndex = 0; nodeIndex < nodes.getLength(); nodeIndex++) {
611 final Node current = nodes.item(nodeIndex);
612 final List<String> names = getNamesFromIndexPage(current);
613 final List<String> namesSorted = names.stream()
614 .sorted()
615 .toList();
616
617 assertWithMessage("%s%s%s", name, NAMES_MUST_BE_IN_ALPHABETICAL_ORDER_SITE_PATH, path)
618 .that(names)
619 .containsExactlyElementsIn(namesSorted)
620 .inOrder();
621 }
622 }
623
624 private static List<String> getNamesFromIndexPage(Node node) {
625 final List<String> result = new ArrayList<>();
626 final Set<Node> children = XmlUtil.findChildElementsByTag(node, "a");
627
628 Node current = node.getFirstChild();
629 Node treeNode = current;
630 boolean getFirstChild = false;
631 int index = 0;
632 while (current != null && index < children.size()) {
633 if ("tr".equals(current.getNodeName())) {
634 treeNode = current.getNextSibling();
635 }
636 if ("a".equals(current.getNodeName())) {
637 final String name = current.getFirstChild().getTextContent()
638 .replace(" ", "").replace("\n", "");
639 result.add(name);
640 current = treeNode;
641 getFirstChild = false;
642 index++;
643 }
644 else if (getFirstChild) {
645 current = current.getFirstChild();
646 getFirstChild = false;
647 }
648 else {
649 current = current.getNextSibling();
650 getFirstChild = true;
651 }
652 }
653 return result;
654 }
655
656 private static List<String> getNames(Node node) {
657 final Set<Node> children = XmlUtil.getChildrenElements(node);
658 final List<String> result = new ArrayList<>();
659 Node current = node.getFirstChild();
660 int index = 0;
661 while (index < children.size()) {
662 if ("item".equals(current.getNodeName())) {
663 final String name = XmlUtil.getNameAttributeOfNode(current);
664 result.add(name);
665 index++;
666 }
667 current = current.getNextSibling();
668 }
669 return result;
670 }
671
672 private static Map<String, String> readSummaries(Path availablePath) throws Exception {
673 final NodeList rows = getTagSourcesNode(availablePath, "tr");
674 final Map<String, String> result = new HashMap<>();
675
676 for (int position = 0; position < rows.getLength(); position++) {
677 final Node row = rows.item(position);
678 final Iterator<Node> cells = XmlUtil.findChildElementsByTag(row, "td").iterator();
679 final String name = XmlUtil.sanitizeXml(cells.next().getTextContent());
680 final String summary = XmlUtil.sanitizeXml(cells.next().getTextContent());
681
682 result.put(name, summary);
683 }
684
685 return result;
686 }
687
688 @Test
689 public void testAllSubSections() throws Exception {
690 for (Path path : XdocUtil.getXdocsFilePaths()) {
691 final String fileName = path.getFileName().toString();
692 final NodeList subSections = getTagSourcesNode(path, "subsection");
693
694 for (int position = 0; position < subSections.getLength(); position++) {
695 final Node subSection = subSections.item(position);
696 final Node name = subSection.getAttributes().getNamedItem("name");
697 assertWithMessage("All sub-sections in '%s' must have a name", fileName)
698 .that(name)
699 .isNotNull();
700 final Node id = subSection.getAttributes().getNamedItem("id");
701 assertWithMessage("All sub-sections in '%s' must have an id", fileName)
702 .that(id)
703 .isNotNull();
704
705
706 String sectionName = XmlUtil.getNameAttributeOfNode(subSection.getParentNode());
707 final String nameString = name.getNodeValue();
708 final String subsectionId = id.getNodeValue();
709 final String expectedId;
710 if ("google-style.xml".equals(fileName)) {
711 sectionName = "Google";
712 expectedId = (sectionName + "_" + nameString).replace(' ', '_');
713 }
714 else if ("sun-style.xml".equals(fileName)) {
715 sectionName = "Sun";
716 expectedId = (sectionName + "_" + nameString).replace(' ', '_');
717 }
718 else if ("openjdk-style.xml".equals(fileName)) {
719 sectionName = "OpenJDK";
720 expectedId = (sectionName + "_" + nameString).replace(' ', '_');
721 }
722 else if ("doc-comments-style.xml".equals(fileName)) {
723 sectionName = "Documentation Comments";
724 expectedId = (sectionName + "_" + nameString).replace(' ', '_');
725 }
726 else if (sectionName.isEmpty()) {
727 expectedId = nameString.replace(' ', '_');
728 }
729 else {
730 expectedId = (sectionName + "_" + nameString).replace(' ', '_');
731 }
732 assertWithMessage("%s sub-section %s for section %s must match", fileName,
733 nameString, sectionName)
734 .that(subsectionId)
735 .isEqualTo(expectedId);
736 }
737 }
738 }
739
740 @Test
741 public void testAllXmlExamples() throws Exception {
742 for (Path path : XdocUtil.getXdocsFilePaths()) {
743 final String fileName = path.getFileName().toString();
744 final NodeList sources = getTagSourcesNode(path, "source");
745
746 for (int position = 0; position < sources.getLength(); position++) {
747 final String unserializedSource = sources.item(position).getTextContent()
748 .replace("...", "").trim();
749
750 if (unserializedSource.length() > 1 && (unserializedSource.charAt(0) != '<'
751 || unserializedSource.charAt(unserializedSource.length() - 1) != '>'
752
753 || unserializedSource.contains("<!"))) {
754 continue;
755 }
756
757 final String code = buildXml(unserializedSource);
758
759 XmlUtil.getRawXml(fileName, code, unserializedSource);
760
761
762 assertWithMessage("Xml is invalid, old or has outdated structure")
763 .that(fileName.startsWith("ant-task")
764 || fileName.startsWith("release-notes")
765 || fileName.startsWith("writing-javadoc-checks")
766 || isValidCheckstyleXml(fileName, code, unserializedSource))
767 .isTrue();
768 }
769 }
770 }
771
772 private static String buildXml(String unserializedSource) throws IOException {
773
774 String code = unserializedSource
775
776 .replace("target/cachefile", "target/cachefile-test");
777
778 if (!hasFileSetClass(code)) {
779 code = "<module name=\"TreeWalker\">\n" + code + "\n</module>";
780 }
781 if (!code.contains("name=\"Checker\"")) {
782 code = "<module name=\"Checker\">\n" + code + "\n</module>";
783 }
784 if (!code.startsWith("<?xml")) {
785 final String dtdPath = new File(
786 "src/main/resources/com/puppycrawl/tools/checkstyle/configuration_1_3.dtd")
787 .getCanonicalPath();
788
789 code = "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC "
790 + "\"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\" \"" + dtdPath
791 + "\">\n" + code;
792 }
793 return code;
794 }
795
796 private static boolean hasFileSetClass(String xml) {
797 boolean found = false;
798
799 for (String find : XML_FILESET_LIST) {
800 if (xml.contains(find)) {
801 found = true;
802 break;
803 }
804 }
805
806 return found;
807 }
808
809 private static boolean isValidCheckstyleXml(String fileName, String code,
810 String unserializedSource)
811 throws IOException, CheckstyleException {
812
813 if (!code.contains("com.mycompany") && !code.contains("checkstyle-packages")
814 && !code.contains("MethodLimit") && !code.contains("<suppress ")
815 && !code.contains("<suppress-xpath ")
816 && !code.contains("<import-control ")
817 && !unserializedSource.startsWith("<property ")
818 && !unserializedSource.startsWith("<taskdef ")) {
819
820 try {
821 final Properties properties = new Properties();
822
823 properties.setProperty("checkstyle.header.file",
824 new File("config/java.header").getCanonicalPath());
825 properties.setProperty("config.folder",
826 new File("config").getCanonicalPath());
827
828 final PropertiesExpander expander = new PropertiesExpander(properties);
829 final Configuration config = ConfigurationLoader.loadConfiguration(new InputSource(
830 new StringReader(code)), expander, IgnoredModulesOptions.EXECUTE);
831 final Checker checker = new Checker();
832
833 try {
834 final ClassLoader moduleClassLoader = Checker.class.getClassLoader();
835 checker.setModuleClassLoader(moduleClassLoader);
836 checker.configure(config);
837 }
838 finally {
839 checker.destroy();
840 }
841 }
842 catch (CheckstyleException exc) {
843 throw new CheckstyleException(fileName + " has invalid Checkstyle xml: "
844 + unserializedSource, exc);
845 }
846 }
847 return true;
848 }
849
850 @Test
851 public void testAllCheckSections() throws Exception {
852 final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
853
854 for (Path path : XdocUtil.getXdocsConfigFilePaths(XdocUtil.getXdocsFilePaths())) {
855 final String fileName = path.getFileName().toString();
856
857 if (isNonModulePage(fileName)) {
858 continue;
859 }
860
861 final NodeList sources = getTagSourcesNode(path, "section");
862 String lastSectionName = null;
863
864 for (int position = 0; position < sources.getLength(); position++) {
865 final Node section = sources.item(position);
866 final String sectionName = XmlUtil.getNameAttributeOfNode(section);
867
868 if ("Content".equals(sectionName) || "Overview".equals(sectionName)) {
869 assertWithMessage("%s section '%s' should be first", fileName, sectionName)
870 .that(lastSectionName)
871 .isNull();
872 continue;
873 }
874
875 assertWithMessage(
876 "%s section '%s' shouldn't end with 'Check'", fileName, sectionName)
877 .that(sectionName.endsWith("Check"))
878 .isFalse();
879 if (lastSectionName != null) {
880 assertWithMessage("%s section '%s' is out of order compared to '%s'", fileName,
881 sectionName, lastSectionName)
882 .that(sectionName.toLowerCase(Locale.ENGLISH).compareTo(
883 lastSectionName.toLowerCase(Locale.ENGLISH)) >= 0)
884 .isTrue();
885 }
886
887 validateCheckSection(moduleFactory, fileName, sectionName, section);
888
889 lastSectionName = sectionName;
890 }
891 }
892 }
893
894 public static boolean isNonModulePage(String fileName) {
895 return NON_MODULE_XDOC.contains(fileName)
896 || fileName.startsWith("release-notes")
897 || Pattern.matches("config_[a-z]+.xml", fileName);
898 }
899
900 @Test
901 public void testAllCheckSectionsEx() throws Exception {
902 final ModuleFactory moduleFactory = TestUtil.getPackageObjectFactory();
903
904 final Path path = Path.of(XdocUtil.DIRECTORY_PATH + "/config.xml");
905 final String fileName = path.getFileName().toString();
906
907 final NodeList sources = getTagSourcesNode(path, "section");
908
909 for (int position = 0; position < sources.getLength(); position++) {
910 final Node section = sources.item(position);
911 final String sectionName = XmlUtil.getNameAttributeOfNode(section);
912
913 if (!"Checker".equals(sectionName) && !"TreeWalker".equals(sectionName)) {
914 continue;
915 }
916
917 validateCheckSection(moduleFactory, fileName, sectionName, section);
918 }
919 }
920
921 private static void validateCheckSection(ModuleFactory moduleFactory, String fileName,
922 String sectionName, Node section) throws Exception {
923 final Object instance;
924
925 try {
926 instance = moduleFactory.createModule(sectionName);
927 }
928 catch (CheckstyleException exc) {
929 throw new CheckstyleException(fileName + " couldn't find class: " + sectionName, exc);
930 }
931
932 int subSectionPos = 0;
933 for (Node subSection : XmlUtil.getChildrenElements(section)) {
934 if (subSectionPos == 0 && "p".equals(subSection.getNodeName())) {
935 validateSinceDescriptionSection(fileName, sectionName, subSection);
936 continue;
937 }
938 if ("div".equals(subSection.getNodeName())) {
939 continue;
940 }
941
942 final String subSectionName = XmlUtil.getNameAttributeOfNode(subSection);
943
944
945 if ("Notes".equals(subSectionName)
946 || "Rule Description".equals(subSectionName)
947 || "Metadata".equals(subSectionName)) {
948 continue;
949 }
950
951
952 if (subSectionPos == 1 && !"Properties".equals(subSectionName)) {
953 validatePropertySection(fileName, sectionName, null, instance);
954 subSectionPos++;
955 }
956 if (subSectionPos == 3 && !"Use Cases".equals(subSectionName)) {
957 subSectionPos++;
958 }
959 if (subSectionPos == 5 && !"Violation Messages".equals(subSectionName)) {
960 validateViolationSection(fileName, sectionName, null, instance);
961 subSectionPos++;
962 }
963
964 assertWithMessage("%s section '%s' should be in order", fileName, sectionName)
965 .that(subSectionName)
966 .isEqualTo(getSubSectionName(subSectionPos));
967
968 switch (subSectionPos) {
969 case 0 -> validateDescriptionSection(fileName, sectionName, subSection);
970 case 1 -> validatePropertySection(fileName, sectionName, subSection, instance);
971 case 4 -> validateUsageExample(fileName, sectionName, subSection);
972 case 5 -> validateViolationSection(fileName, sectionName, subSection, instance);
973 case 6 -> validateFullyQualifiedNameSection(
974 fileName, sectionName, subSection, instance);
975 case 7 -> validateParentSection(fileName, sectionName, subSection);
976 default -> {
977
978 }
979 }
980
981 subSectionPos++;
982 }
983
984 if ("Checker".equals(sectionName)) {
985 assertWithMessage("%s section '%s' should contain up to 'Package' sub-section",
986 fileName, sectionName)
987 .that(subSectionPos)
988 .isGreaterThan(6);
989 }
990 else {
991 assertWithMessage("%s section '%s' should contain up to 'Parent' sub-section", fileName,
992 sectionName)
993 .that(subSectionPos)
994 .isGreaterThan(7);
995 }
996 }
997
998 private static void validateSinceDescriptionSection(String fileName, String sectionName,
999 Node subSection) {
1000 assertWithMessage(
1001 "%s section '%s' should have a valid version at the start of the description like:\n%s",
1002 fileName, sectionName, DESCRIPTION_VERSION.pattern())
1003 .that(DESCRIPTION_VERSION.matcher(subSection.getTextContent().trim()).find())
1004 .isTrue();
1005 }
1006
1007 private static Object getSubSectionName(int subSectionPos) {
1008 return switch (subSectionPos) {
1009 case 0 -> "Description";
1010 case 1 -> "Properties";
1011 case 2 -> "Examples";
1012 case 3 -> "Use Cases";
1013 case 4 -> "Example of Usage";
1014 case 5 -> "Violation Messages";
1015 case 6 -> "Fully Qualified Name";
1016 case 7 -> "Parent Module";
1017 default -> null;
1018 };
1019 }
1020
1021 private static void validateDescriptionSection(String fileName, String sectionName,
1022 Node subSection) {
1023 if ("config-filters.xml".equals(fileName) && "SuppressionXpathFilter".equals(sectionName)) {
1024 validateListOfSuppressionXpathFilterIncompatibleChecks(subSection);
1025 }
1026 }
1027
1028 private static void validateListOfSuppressionXpathFilterIncompatibleChecks(Node subSection) {
1029 assertWithMessage(
1030 "Incompatible check list should match XpathRegressionTest.INCOMPATIBLE_CHECK_NAMES")
1031 .that(getListById(subSection, "SuppressionXpathFilter_IncompatibleChecks"))
1032 .isEqualTo(XpathRegressionTest.INCOMPATIBLE_CHECK_NAMES);
1033 final Set<String> suppressionXpathFilterJavadocChecks = getListById(subSection,
1034 "SuppressionXpathFilter_JavadocChecks");
1035 assertWithMessage(
1036 "Javadoc check list should match XpathRegressionTest.INCOMPATIBLE_JAVADOC_CHECK_NAMES")
1037 .that(suppressionXpathFilterJavadocChecks)
1038 .isEqualTo(XpathRegressionTest.INCOMPATIBLE_JAVADOC_CHECK_NAMES);
1039 }
1040
1041 private static void validatePropertySection(String fileName, String sectionName,
1042 Node subSection, Object instance) throws Exception {
1043 final Set<String> properties = getProperties(instance.getClass());
1044 final Class<?> clss = instance.getClass();
1045
1046 fixCapturedProperties(sectionName, instance, clss, properties);
1047
1048 if (subSection != null) {
1049 assertWithMessage("%s section '%s' should have no properties to show", fileName,
1050 sectionName)
1051 .that(properties)
1052 .isNotEmpty();
1053
1054 final Set<Node> nodes = XmlUtil.getChildrenElements(subSection);
1055 assertWithMessage("%s section '%s' subsection 'Properties' should have one child node",
1056 fileName, sectionName)
1057 .that(nodes)
1058 .hasSize(1);
1059
1060 final Node div = nodes.iterator().next();
1061 assertWithMessage("%s section '%s' subsection 'Properties' has unexpected child node",
1062 fileName, sectionName)
1063 .that(div.getNodeName())
1064 .isEqualTo("div");
1065 final String wrapperMessage = String.format(Locale.ROOT,
1066 "%s section '%s' subsection 'Properties'"
1067 + " wrapping div for table needs the class 'wrapper'",
1068 fileName, sectionName);
1069 assertWithMessage(wrapperMessage)
1070 .that(div.hasAttributes())
1071 .isTrue();
1072 assertWithMessage(wrapperMessage)
1073 .that(div.getAttributes().getNamedItem("class").getNodeValue())
1074 .isNotNull();
1075 assertWithMessage(wrapperMessage)
1076 .that(div.getAttributes().getNamedItem("class").getNodeValue())
1077 .contains("wrapper");
1078
1079 final Node table = XmlUtil.getFirstChildElement(div);
1080 assertWithMessage("%s section '%s' subsection 'Properties' has unexpected child node",
1081 fileName, sectionName)
1082 .that(table.getNodeName())
1083 .isEqualTo("table");
1084
1085 validatePropertySectionPropertiesOrder(fileName, sectionName, table, properties);
1086
1087 validatePropertySectionProperties(fileName, sectionName, table, instance,
1088 properties);
1089 }
1090
1091 assertWithMessage(
1092 "%s section '%s' should show properties: %s", fileName, sectionName, properties)
1093 .that(properties)
1094 .isEmpty();
1095 }
1096
1097 private static void validatePropertySectionPropertiesOrder(String fileName, String sectionName,
1098 Node table, Set<String> properties) {
1099 final Set<Node> rows = XmlUtil.getChildrenElements(table);
1100 final List<String> orderedPropertyNames = new ArrayList<>(properties);
1101 final List<String> tablePropertyNames = new ArrayList<>();
1102
1103
1104 if (orderedPropertyNames.contains("javadocTokens")) {
1105 orderedPropertyNames.remove("javadocTokens");
1106 orderedPropertyNames.add("javadocTokens");
1107 }
1108 if (orderedPropertyNames.contains("tokens")) {
1109 orderedPropertyNames.remove("tokens");
1110 orderedPropertyNames.add("tokens");
1111 }
1112
1113 rows
1114 .stream()
1115
1116 .skip(1)
1117 .forEach(row -> {
1118 final List<Node> columns = new ArrayList<>(XmlUtil.getChildrenElements(row));
1119 assertWithMessage("%s section '%s' should have the requested columns", fileName,
1120 sectionName)
1121 .that(columns)
1122 .hasSize(5);
1123
1124 final String propertyName = columns.getFirst().getTextContent();
1125 tablePropertyNames.add(propertyName);
1126 });
1127
1128 assertWithMessage("%s section '%s' should have properties in the requested order", fileName,
1129 sectionName)
1130 .that(tablePropertyNames)
1131 .isEqualTo(orderedPropertyNames);
1132 }
1133
1134 private static void fixCapturedProperties(String sectionName, Object instance, Class<?> clss,
1135 Set<String> properties) {
1136
1137 if (hasParentModule(sectionName)) {
1138 if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
1139 properties.removeAll(JAVADOC_CHECK_PROPERTIES);
1140
1141
1142 properties.add("violateExecutionOnNonTightHtml");
1143 }
1144 else if (AbstractCheck.class.isAssignableFrom(clss)) {
1145 properties.removeAll(CHECK_PROPERTIES);
1146 }
1147 }
1148 if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
1149 properties.removeAll(FILESET_PROPERTIES);
1150
1151
1152 properties.add("fileExtensions");
1153 }
1154
1155
1156 new HashSet<>(properties).stream()
1157 .filter(prop -> UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + "." + prop))
1158 .forEach(properties::remove);
1159
1160 if (AbstractCheck.class.isAssignableFrom(clss)) {
1161 final AbstractCheck check = (AbstractCheck) instance;
1162
1163 final int[] acceptableTokens = check.getAcceptableTokens();
1164 Arrays.sort(acceptableTokens);
1165 final int[] defaultTokens = check.getDefaultTokens();
1166 Arrays.sort(defaultTokens);
1167 final int[] requiredTokens = check.getRequiredTokens();
1168 Arrays.sort(requiredTokens);
1169
1170 if (!Arrays.equals(acceptableTokens, defaultTokens)
1171 || !Arrays.equals(acceptableTokens, requiredTokens)) {
1172 properties.add("tokens");
1173 }
1174 }
1175
1176 if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
1177 final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
1178
1179 final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens();
1180 Arrays.sort(acceptableJavadocTokens);
1181 final int[] defaultJavadocTokens = check.getDefaultJavadocTokens();
1182 Arrays.sort(defaultJavadocTokens);
1183 final int[] requiredJavadocTokens = check.getRequiredJavadocTokens();
1184 Arrays.sort(requiredJavadocTokens);
1185
1186 if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens)
1187 || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) {
1188 properties.add("javadocTokens");
1189 }
1190 }
1191 }
1192
1193 private static void validatePropertySectionProperties(String fileName, String sectionName,
1194 Node table, Object instance, Set<String> properties) throws Exception {
1195 boolean skip = true;
1196 boolean didJavadocTokens = false;
1197 boolean didTokens = false;
1198
1199 for (Node row : XmlUtil.getChildrenElements(table)) {
1200 final List<Node> columns = new ArrayList<>(XmlUtil.getChildrenElements(row));
1201
1202 assertWithMessage("%s section '%s' should have the requested columns", fileName,
1203 sectionName)
1204 .that(columns)
1205 .hasSize(5);
1206
1207 if (skip) {
1208 assertWithMessage("%s section '%s' should have the specific title", fileName,
1209 sectionName)
1210 .that(columns.getFirst().getTextContent())
1211 .isEqualTo("name");
1212 assertWithMessage("%s section '%s' should have the specific title", fileName,
1213 sectionName)
1214 .that(columns.get(1).getTextContent())
1215 .isEqualTo("description");
1216 assertWithMessage("%s section '%s' should have the specific title", fileName,
1217 sectionName)
1218 .that(columns.get(2).getTextContent())
1219 .isEqualTo("type");
1220 assertWithMessage("%s section '%s' should have the specific title", fileName,
1221 sectionName)
1222 .that(columns.get(3).getTextContent())
1223 .isEqualTo("default value");
1224 assertWithMessage("%s section '%s' should have the specific title", fileName,
1225 sectionName)
1226 .that(columns.get(4).getTextContent())
1227 .isEqualTo("since");
1228
1229 skip = false;
1230 continue;
1231 }
1232
1233 assertWithMessage("%s section '%s' should have token properties last", fileName,
1234 sectionName)
1235 .that(didTokens)
1236 .isFalse();
1237
1238 final String propertyName = columns.getFirst().getTextContent();
1239 assertWithMessage("%s section '%s' should not contain the property: %s", fileName,
1240 sectionName, propertyName)
1241 .that(properties.remove(propertyName))
1242 .isTrue();
1243
1244 if ("tokens".equals(propertyName)) {
1245 final AbstractCheck check = (AbstractCheck) instance;
1246 validatePropertySectionPropertyTokens(fileName, sectionName, check, columns);
1247 didTokens = true;
1248 }
1249 else if ("javadocTokens".equals(propertyName)) {
1250 final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
1251 validatePropertySectionPropertyJavadocTokens(fileName, sectionName, check, columns);
1252 didJavadocTokens = true;
1253 }
1254 else {
1255 assertWithMessage(
1256 "%s section '%s' should have javadoc token properties"
1257 + " next to last, before tokens",
1258 fileName, sectionName)
1259 .that(didJavadocTokens)
1260 .isFalse();
1261
1262 validatePropertySectionPropertyEx(fileName, sectionName, instance, columns,
1263 propertyName);
1264 }
1265
1266 assertWithMessage("%s section '%s' should have a version for %s",
1267 fileName, sectionName, propertyName)
1268 .that(columns.get(4).getTextContent().trim())
1269 .isNotEmpty();
1270 assertWithMessage("%s section '%s' should have a valid version for %s",
1271 fileName, sectionName, propertyName)
1272 .that(columns.get(4).getTextContent().trim())
1273 .matches(VERSION);
1274 }
1275 }
1276
1277 private static void validatePropertySectionPropertyEx(String fileName, String sectionName,
1278 Object instance, List<Node> columns, String propertyName) throws Exception {
1279 assertWithMessage("%s section '%s' should have a description for %s",
1280 fileName, sectionName, propertyName)
1281 .that(columns.get(1).getTextContent().trim())
1282 .isNotEmpty();
1283 assertWithMessage("%s section '%s' should have a description for %s"
1284 + " that starts with uppercase character",
1285 fileName, sectionName, propertyName)
1286 .that(Character.isUpperCase(columns.get(1).getTextContent().trim().charAt(0)))
1287 .isTrue();
1288
1289 final String actualTypeName = columns.get(2).getTextContent().replace("\n", "")
1290 .replace("\r", "").replaceAll(" +", " ").trim();
1291
1292 assertWithMessage(
1293 "%s section '%s' should have a type for %s", fileName, sectionName, propertyName)
1294 .that(actualTypeName)
1295 .isNotEmpty();
1296
1297 final Field field = getField(instance.getClass(), propertyName);
1298 final Class<?> fieldClass = getFieldClass(fileName, sectionName, instance, field,
1299 propertyName);
1300
1301 final String expectedTypeName = Optional.ofNullable(field)
1302 .map(nonNullField -> nonNullField.getAnnotation(XdocsPropertyType.class))
1303 .map(propertyType -> propertyType.value().getDescription())
1304 .map(XdocsPagesTest::simplifyTypeName)
1305 .orElseGet(fieldClass::getSimpleName);
1306 final String expectedValue = getModulePropertyExpectedValue(sectionName, propertyName,
1307 field, fieldClass, instance);
1308
1309 assertWithMessage("%s section '%s' should have the type for %s", fileName, sectionName,
1310 propertyName)
1311 .that(actualTypeName)
1312 .isEqualTo(expectedTypeName);
1313
1314 if (expectedValue != null) {
1315 final String actualValue = columns.get(3).getTextContent().trim()
1316 .replaceAll("\\s+", " ")
1317 .replaceAll("\\s,", ",");
1318
1319 assertWithMessage("%s section '%s' should have the value for %s", fileName, sectionName,
1320 propertyName)
1321 .that(actualValue)
1322 .isEqualTo(expectedValue);
1323 }
1324 }
1325
1326 private static String simplifyTypeName(String fullTypeName) {
1327 final int separatorIndex = Math.max(fullTypeName.lastIndexOf('$'),
1328 fullTypeName.lastIndexOf('.'));
1329 return fullTypeName.substring(separatorIndex + 1);
1330 }
1331
1332 private static void validatePropertySectionPropertyTokens(String fileName, String sectionName,
1333 AbstractCheck check, List<Node> columns) {
1334 assertWithMessage("%s section '%s' should have the basic token description", fileName,
1335 sectionName)
1336 .that(columns.get(1).getTextContent())
1337 .isEqualTo("tokens to check");
1338
1339 final String acceptableTokenText = columns.get(2).getTextContent().trim();
1340 String expectedAcceptableTokenText = "subset of tokens "
1341 + CheckUtil.getTokenText(check.getAcceptableTokens(),
1342 check.getRequiredTokens());
1343 if (isAllTokensAcceptable(check)) {
1344 expectedAcceptableTokenText = "set of any supported tokens";
1345 }
1346 assertWithMessage("%s section '%s' should have all the acceptable tokens", fileName,
1347 sectionName)
1348 .that(acceptableTokenText
1349 .replaceAll("\\s+", " ")
1350 .replaceAll("\\s,", ",")
1351 .replaceAll("\\s\\.", "."))
1352 .isEqualTo(expectedAcceptableTokenText);
1353 assertWithMessage(
1354 "%s's acceptable token section: %s should have ',' & '.' "
1355 + "at beginning of the next corresponding lines.",
1356 fileName, sectionName)
1357 .that(isInvalidTokenPunctuation(acceptableTokenText))
1358 .isFalse();
1359
1360 final String defaultTokenText = columns.get(3).getTextContent().trim();
1361 final String expectedDefaultTokenText = CheckUtil.getTokenText(check.getDefaultTokens(),
1362 check.getRequiredTokens());
1363 if (expectedDefaultTokenText.isEmpty()) {
1364 assertWithMessage("Empty tokens should have 'empty' string in xdoc")
1365 .that(defaultTokenText)
1366 .isEqualTo("empty");
1367 }
1368 else {
1369 assertWithMessage("%s section '%s' should have all the default tokens", fileName,
1370 sectionName)
1371 .that(defaultTokenText
1372 .replaceAll("\\s+", " ")
1373 .replaceAll("\\s,", ",")
1374 .replaceAll("\\s\\.", "."))
1375 .isEqualTo(expectedDefaultTokenText);
1376 assertWithMessage(
1377 "%s's default token section: %s should have ',' or '.' "
1378 + "at beginning of the next corresponding lines.",
1379 fileName, sectionName)
1380 .that(isInvalidTokenPunctuation(defaultTokenText))
1381 .isFalse();
1382 }
1383
1384 }
1385
1386 private static boolean isAllTokensAcceptable(AbstractCheck check) {
1387 return Arrays.equals(check.getAcceptableTokens(), TokenUtil.getAllTokenIds());
1388 }
1389
1390 private static void validatePropertySectionPropertyJavadocTokens(String fileName,
1391 String sectionName, AbstractJavadocCheck check, List<Node> columns) {
1392 assertWithMessage("%s section '%s' should have the basic token javadoc description",
1393 fileName, sectionName)
1394 .that(columns.get(1).getTextContent())
1395 .isEqualTo("javadoc tokens to check");
1396
1397 final String acceptableTokenText = columns.get(2).getTextContent().trim();
1398 assertWithMessage("%s section '%s' should have all the acceptable javadoc tokens", fileName,
1399 sectionName)
1400 .that(acceptableTokenText
1401 .replaceAll("\\s+", " ")
1402 .replaceAll("\\s,", ",")
1403 .replaceAll("\\s\\.", "."))
1404 .isEqualTo("subset of javadoc tokens "
1405 + CheckUtil.getJavadocTokenText(check.getAcceptableJavadocTokens(),
1406 check.getRequiredJavadocTokens()));
1407 assertWithMessage(
1408 "%s's acceptable javadoc token section: %s should have ',' & '.' "
1409 + "at beginning of the next corresponding lines.",
1410 fileName, sectionName)
1411 .that(isInvalidTokenPunctuation(acceptableTokenText))
1412 .isFalse();
1413
1414 final String defaultTokenText = columns.get(3).getTextContent().trim();
1415 assertWithMessage("%s section '%s' should have all the default javadoc tokens", fileName,
1416 sectionName)
1417 .that(defaultTokenText
1418 .replaceAll("\\s+", " ")
1419 .replaceAll("\\s,", ",")
1420 .replaceAll("\\s\\.", "."))
1421 .isEqualTo(CheckUtil.getJavadocTokenText(check.getDefaultJavadocTokens(),
1422 check.getRequiredJavadocTokens()));
1423 assertWithMessage(
1424 "%s's default javadoc token section: %s should have ',' & '.' "
1425 + "at beginning of the next corresponding lines.",
1426 fileName, sectionName)
1427 .that(isInvalidTokenPunctuation(defaultTokenText))
1428 .isFalse();
1429 }
1430
1431 private static boolean isInvalidTokenPunctuation(String tokenText) {
1432 return Pattern.compile("\\w,").matcher(tokenText).find()
1433 || Pattern.compile("\\w\\.").matcher(tokenText).find();
1434 }
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449 private static String getModulePropertyExpectedValue(String sectionName, String propertyName,
1450 Field field, Class<?> fieldClass, Object instance) throws Exception {
1451 String result = null;
1452
1453 if (field != null) {
1454 final Object value = field.get(instance);
1455
1456 if ("Checker".equals(sectionName) && "localeCountry".equals(propertyName)) {
1457 result = "default locale country for the Java Virtual Machine";
1458 }
1459 else if ("Checker".equals(sectionName) && "localeLanguage".equals(propertyName)) {
1460 result = "default locale language for the Java Virtual Machine";
1461 }
1462 else if ("Checker".equals(sectionName) && "charset".equals(propertyName)) {
1463 result = "UTF-8";
1464 }
1465 else if ("charset".equals(propertyName)) {
1466 result = "the charset property of the parent"
1467 + " <a href=\"https://checkstyle.org/config.html#Checker\">Checker</a> module";
1468 }
1469 else if ("PropertyCacheFile".equals(fieldClass.getSimpleName())) {
1470 result = "null (no cache file)";
1471 }
1472 else if (fieldClass == boolean.class) {
1473 result = value.toString();
1474 }
1475 else if (fieldClass == int.class) {
1476 result = value.toString();
1477 }
1478 else if (fieldClass == int[].class) {
1479 result = getIntArrayPropertyValue(value);
1480 }
1481 else if (fieldClass == double[].class) {
1482 result = Arrays.toString((double[]) value).replace("[", "").replace("]", "")
1483 .replace(".0", "");
1484 if (result.isEmpty()) {
1485 result = "{}";
1486 }
1487 }
1488 else if (fieldClass == String[].class) {
1489 final boolean preserveOrder = hasPreserveOrderAnnotation(field);
1490 result = getStringArrayPropertyValue(propertyName, value, preserveOrder);
1491 }
1492 else if (fieldClass == URI.class || fieldClass == String.class) {
1493 if (value != null) {
1494 result = value.toString();
1495 }
1496 }
1497 else if (fieldClass == Pattern.class) {
1498 if (value != null) {
1499 result = value.toString().replace("\n", "\\n").replace("\t", "\\t")
1500 .replace("\r", "\\r").replace("\f", "\\f");
1501 }
1502 }
1503 else if (fieldClass == Pattern[].class) {
1504 result = getPatternArrayPropertyValue(value);
1505 }
1506 else if (fieldClass.isEnum()) {
1507 if (value != null) {
1508 result = value.toString().toLowerCase(Locale.ENGLISH);
1509 }
1510 }
1511 else if (fieldClass == AccessModifierOption[].class) {
1512 result = Arrays.toString((Object[]) value).replace("[", "").replace("]", "");
1513 }
1514 else {
1515 assertWithMessage("Unknown property type: %s", fieldClass.getSimpleName()).fail();
1516 }
1517
1518 if (result == null) {
1519 result = "null";
1520 }
1521 }
1522
1523 return result;
1524 }
1525
1526 private static boolean hasPreserveOrderAnnotation(Field field) {
1527 return field != null && field.isAnnotationPresent(PreserveOrder.class);
1528 }
1529
1530
1531
1532
1533
1534
1535
1536 private static String getPatternArrayPropertyValue(Object fieldValue) {
1537 Object value = fieldValue;
1538 String result;
1539 if (value instanceof Collection<?> collection) {
1540 final Pattern[] newArray = new Pattern[collection.size()];
1541 final Iterator<?> iterator = collection.iterator();
1542 int index = 0;
1543
1544 while (iterator.hasNext()) {
1545 final Object next = iterator.next();
1546 newArray[index] = (Pattern) next;
1547 index++;
1548 }
1549
1550 value = newArray;
1551 }
1552
1553 if (value != null && Array.getLength(value) > 0) {
1554 final String[] newArray = new String[Array.getLength(value)];
1555
1556 for (int i = 0; i < newArray.length; i++) {
1557 newArray[i] = ((Pattern) Array.get(value, i)).pattern();
1558 }
1559
1560 result = Arrays.toString(newArray).replace("[", "").replace("]", "");
1561 }
1562 else {
1563 result = "";
1564 }
1565
1566 if (result.isEmpty()) {
1567 result = "{}";
1568 }
1569 return result;
1570 }
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580 private static String getStringArrayPropertyValue(String propertyName, Object value,
1581 boolean preserveOrder) {
1582 String result;
1583 if (value == null) {
1584 result = "";
1585 }
1586 else {
1587 final Stream<?> valuesStream;
1588 if (value instanceof Collection<?> collection) {
1589 valuesStream = collection.stream();
1590 }
1591 else {
1592 final Object[] array = (Object[]) value;
1593 valuesStream = Arrays.stream(array);
1594 }
1595
1596 Stream<String> stringStream = valuesStream.map(String.class::cast);
1597
1598 if (!preserveOrder) {
1599 stringStream = stringStream.sorted();
1600 }
1601
1602 result = stringStream.collect(Collectors.joining(", "));
1603
1604 }
1605
1606 if (result.isEmpty()) {
1607 if ("fileExtensions".equals(propertyName)) {
1608 result = "all files";
1609 }
1610 else {
1611 result = "{}";
1612 }
1613 }
1614 return result;
1615 }
1616
1617
1618
1619
1620
1621
1622
1623 private static String getIntArrayPropertyValue(Object value) {
1624 final IntStream stream = switch (value) {
1625 case null -> throw new IllegalArgumentException("value is null");
1626 case Collection<?> collection -> collection.stream()
1627 .mapToInt(number -> (int) number);
1628 case BitSet set -> set.stream();
1629 default -> Arrays.stream((int[]) value);
1630 };
1631 String result = stream
1632 .mapToObj(TokenUtil::getTokenName)
1633 .sorted()
1634 .collect(Collectors.joining(", "));
1635 if (result.isEmpty()) {
1636 result = "{}";
1637 }
1638 return result;
1639 }
1640
1641
1642
1643
1644
1645
1646
1647
1648 private static Field getField(Class<?> fieldClass, String propertyName) {
1649 Field result = null;
1650 Class<?> currentClass = fieldClass;
1651
1652 while (!Object.class.equals(currentClass)) {
1653 try {
1654 result = currentClass.getDeclaredField(propertyName);
1655 result.trySetAccessible();
1656 break;
1657 }
1658 catch (NoSuchFieldException ignored) {
1659 currentClass = currentClass.getSuperclass();
1660 }
1661 }
1662
1663 return result;
1664 }
1665
1666 private static Class<?> getFieldClass(String fileName, String sectionName, Object instance,
1667 Field field, String propertyName) throws Exception {
1668 Class<?> result = null;
1669
1670 if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD.contains(sectionName + "." + propertyName)) {
1671 final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
1672 propertyName);
1673 result = descriptor.getPropertyType();
1674 }
1675 if (field != null && result == null) {
1676 result = field.getType();
1677 }
1678 if (result == null) {
1679 assertWithMessage(
1680 "%s section '%s' could not find field %s", fileName, sectionName, propertyName)
1681 .fail();
1682 }
1683 if (field != null && (result == List.class || result == Set.class)) {
1684 final ParameterizedType type = (ParameterizedType) field.getGenericType();
1685 final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
1686
1687 if (parameterClass == Integer.class) {
1688 result = int[].class;
1689 }
1690 else if (parameterClass == String.class) {
1691 result = String[].class;
1692 }
1693 else if (parameterClass == Pattern.class) {
1694 result = Pattern[].class;
1695 }
1696 else {
1697 assertWithMessage("Unknown parameterized type: %s", parameterClass.getSimpleName())
1698 .fail();
1699 }
1700 }
1701 else if (result == BitSet.class) {
1702 result = int[].class;
1703 }
1704
1705 return result;
1706 }
1707
1708 private static Set<String> getListById(Node subSection, String id) {
1709 Set<String> result = null;
1710 final Node node = XmlUtil.findChildElementById(subSection, id);
1711 if (node != null) {
1712 result = XmlUtil.getChildrenElements(node)
1713 .stream()
1714 .map(Node::getTextContent)
1715 .collect(Collectors.toUnmodifiableSet());
1716 }
1717 return result;
1718 }
1719
1720 private static void validateViolationSection(String fileName, String sectionName,
1721 Node subSection,
1722 Object instance) throws Exception {
1723 final Class<?> clss = instance.getClass();
1724 final Set<Field> fields = CheckUtil.getCheckMessagesWithDeepScan(clss);
1725 final Set<String> list = new TreeSet<>();
1726
1727 for (Field field : fields) {
1728
1729 field.trySetAccessible();
1730
1731 list.add(field.get(null).toString());
1732 }
1733
1734 final StringBuilder expectedText = new StringBuilder(120);
1735
1736 for (String s : list) {
1737 expectedText.append(s)
1738 .append('\n');
1739 }
1740
1741 if (!expectedText.isEmpty()) {
1742 expectedText.append(
1743 """
1744 All messages can be customized if the default message doesn't suit you.
1745 Please see the documentation to learn how to.
1746 """);
1747 }
1748
1749 if (subSection == null) {
1750 assertWithMessage("%s section '%s' should have the expected error keys", fileName,
1751 sectionName)
1752 .that(expectedText.toString())
1753 .isEqualTo("");
1754 }
1755 else {
1756 final String subsectionTextContent = subSection.getTextContent()
1757 .replaceAll("\n\\s+", "\n")
1758 .replaceAll("\\s+", " ")
1759 .trim();
1760 assertWithMessage("%s section '%s' should have the expected error keys", fileName,
1761 sectionName)
1762 .that(subsectionTextContent)
1763 .isEqualTo(expectedText.toString().replace("\n", " ").trim());
1764
1765 for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) {
1766 final String url = node.getAttributes().getNamedItem("href").getTextContent();
1767 final String linkText = node.getTextContent().trim();
1768 final String expectedUrl;
1769
1770 if ("see the documentation".equals(linkText)) {
1771 expectedUrl = "../../config.html#Custom_messages";
1772 }
1773 else {
1774 final String query = "path:src/main/resources/"
1775 + clss.getPackage().getName().replace('.', '/')
1776 + " path:**/messages*.properties repo:checkstyle/checkstyle \""
1777 + linkText + "\"";
1778 expectedUrl = "https://github.com/search?q="
1779 + URLEncoder.encode(query, StandardCharsets.UTF_8);
1780 }
1781
1782 assertWithMessage("%s section '%s' should have matching url for '%s'", fileName,
1783 sectionName, linkText)
1784 .that(url)
1785 .isEqualTo(expectedUrl);
1786 }
1787 }
1788 }
1789
1790 private static void validateUsageExample(String fileName, String sectionName, Node subSection) {
1791 final String text = subSection.getTextContent()
1792 .replace("Checkstyle Style", "")
1793 .replace("Google Style", "")
1794 .replace("Sun Style", "")
1795 .replace("OpenJDK Style", "")
1796 .replace("Documentation Comments Style", "")
1797 .replace("Checkstyle's Import Control Config", "")
1798 .trim();
1799
1800 assertWithMessage("%s section '%s' has unknown text in 'Example of Usage': %s", fileName,
1801 sectionName, text)
1802 .that(text)
1803 .isEmpty();
1804
1805 boolean hasCheckstyle = false;
1806 boolean hasGoogle = false;
1807 boolean hasSun = false;
1808 boolean hasOpenjdk = false;
1809 boolean hasDocComments = false;
1810
1811 for (Node node : XmlUtil.findChildElementsByTag(subSection, "a")) {
1812 final String url = node.getAttributes().getNamedItem("href").getTextContent();
1813 final String linkText = node.getTextContent().trim();
1814 String expectedUrl = null;
1815
1816 if ("Checkstyle Style".equals(linkText)) {
1817 hasCheckstyle = true;
1818 expectedUrl = "https://github.com/search?q="
1819 + "path%3Aconfig%20path%3A**%2Fcheckstyle-checks.xml+"
1820 + "repo%3Acheckstyle%2Fcheckstyle+" + sectionName;
1821 }
1822 else if ("Google Style".equals(linkText)) {
1823 hasGoogle = true;
1824 expectedUrl = getExpectedStyleGuideUrl("google_checks.xml")
1825 + sectionName;
1826
1827 assertWithMessage(
1828 "%s section '%s' should be in google_checks.xml"
1829 + " or not reference 'Google Style'",
1830 fileName, sectionName)
1831 .that(GOOGLE_MODULES)
1832 .contains(sectionName);
1833 }
1834 else if ("Sun Style".equals(linkText)) {
1835 hasSun = true;
1836 expectedUrl = getExpectedStyleGuideUrl("sun_checks.xml")
1837 + sectionName;
1838
1839 assertWithMessage(
1840 "%s section '%s' should be in sun_checks.xml or not reference 'Sun Style'",
1841 fileName, sectionName)
1842 .that(SUN_MODULES)
1843 .contains(sectionName);
1844 }
1845 else if ("OpenJDK Style".equals(linkText)) {
1846 hasOpenjdk = true;
1847 expectedUrl = getExpectedStyleGuideUrl("openjdk_checks.xml")
1848 + sectionName;
1849 assertWithMessage(
1850 "%s section '%s' should be in openjdk_checks.xml "
1851 + "or not reference 'OpenJDK Style'",
1852 fileName, sectionName)
1853 .that(OPENJDK_MODULES)
1854 .contains(sectionName);
1855 }
1856 else if ("Documentation Comments Style".equals(linkText)) {
1857 hasDocComments = true;
1858 expectedUrl = getExpectedStyleGuideUrl("doc_comments_checks.xml")
1859 + sectionName;
1860 assertWithMessage(
1861 "%s section '%s' should be in doc_comments_checks.xml "
1862 + "or not reference 'Documentation Comments Style'",
1863 fileName, sectionName)
1864 .that(DOC_COMMENTS_MODULES)
1865 .contains(sectionName);
1866 }
1867 else if ("Checkstyle's Import Control Config".equals(linkText)) {
1868 expectedUrl = "https://github.com/checkstyle/checkstyle/blob/master/config/"
1869 + "import-control.xml";
1870 }
1871
1872 assertWithMessage("%s section '%s' should have matching url", fileName, sectionName)
1873 .that(url)
1874 .isEqualTo(expectedUrl);
1875 }
1876
1877 assertWithMessage("%s section '%s' should have a checkstyle section", fileName, sectionName)
1878 .that(hasCheckstyle)
1879 .isTrue();
1880 assertWithMessage("%s section '%s' should have a google section since it is in it's config",
1881 fileName, sectionName)
1882 .that(hasGoogle
1883 || !GOOGLE_MODULES.contains(sectionName)
1884 || IGNORED_GOOGLE_MODULES.contains(sectionName))
1885 .isTrue();
1886 assertWithMessage("%s section '%s' should have a sun section since it is in it's config",
1887 fileName, sectionName)
1888 .that(hasSun || !SUN_MODULES.contains(sectionName))
1889 .isTrue();
1890 assertWithMessage("%s section '%s' should have an openjdk section since "
1891 + "it is in its config",
1892 fileName, sectionName)
1893 .that(hasOpenjdk || !OPENJDK_MODULES.contains(sectionName))
1894 .isTrue();
1895 assertWithMessage("%s section '%s' should have a documentation comments section since "
1896 + "it is in its config",
1897 fileName, sectionName)
1898 .that(hasDocComments || !DOC_COMMENTS_MODULES.contains(sectionName))
1899 .isTrue();
1900 }
1901
1902 private static void validateFullyQualifiedNameSection(String fileName, String sectionName,
1903 Node subSection, Object instance) {
1904 final String fullyQualifiedName = subSection.getTextContent()
1905 .replaceAll("\\s+", "");
1906
1907 assertWithMessage("%s section '%s' should have matching fully qualified name",
1908 fileName, sectionName)
1909 .that(fullyQualifiedName)
1910 .contains(instance.getClass().getName());
1911 }
1912
1913 private static void validateParentSection(String fileName, String sectionName,
1914 Node subSection) {
1915 final String expected;
1916
1917 if (!"TreeWalker".equals(sectionName) && hasParentModule(sectionName)) {
1918 expected = "TreeWalker";
1919 }
1920 else {
1921 expected = "Checker";
1922 }
1923
1924 assertWithMessage("%s section '%s' should have matching parent", fileName, sectionName)
1925 .that(subSection.getTextContent().trim())
1926 .isEqualTo(expected);
1927 }
1928
1929 private static boolean hasParentModule(String sectionName) {
1930 final String search = "\"" + sectionName + "\"";
1931 boolean result = true;
1932
1933 for (String find : XML_FILESET_LIST) {
1934 if (find.contains(search)) {
1935 result = false;
1936 break;
1937 }
1938 }
1939
1940 return result;
1941 }
1942
1943 private static Set<String> getProperties(Class<?> clss) {
1944 final Set<String> result = new TreeSet<>();
1945 final PropertyDescriptor[] map = PropertyUtils.getPropertyDescriptors(clss);
1946
1947 for (PropertyDescriptor p : map) {
1948 if (p.getWriteMethod() != null) {
1949 result.add(p.getName());
1950 }
1951 }
1952
1953 return result;
1954 }
1955
1956 private static boolean shouldSkipStyleFile(String fileName, String styleName) {
1957 return "doc_comments".equals(styleName) || "openjdk".equals(styleName)
1958 || "google_style.xml".equals(fileName) || "openjdk_style.xml".equals(fileName)
1959 || "sun_style.xml".equals(fileName) || "doc_comments_style.xml".equals(fileName);
1960 }
1961
1962 @Test
1963 public void testAllStyleRules() throws Exception {
1964 for (Path path : XdocUtil.getXdocsStyleFilePaths(XdocUtil.getXdocsFilePaths())) {
1965 final String fileName = path.getFileName().toString();
1966 final String styleName = fileName.substring(0, fileName.lastIndexOf('_'));
1967 if (shouldSkipStyleFile(fileName, styleName)) {
1968 continue;
1969 }
1970 final NodeList sources = getTagSourcesNode(path, "tr");
1971
1972 final Set<String> styleChecks = switch (styleName) {
1973 case "google" -> {
1974 final Set<String> checks = new HashSet<>(GOOGLE_MODULES);
1975 checks.removeAll(IGNORED_GOOGLE_MODULES);
1976 yield checks;
1977 }
1978 case "sun" -> {
1979 final Set<String> checks = new HashSet<>(SUN_MODULES);
1980 checks.removeAll(IGNORED_SUN_MODULES);
1981 yield checks;
1982 }
1983 case null -> {
1984 assertWithMessage("Style name is unexpectedly null")
1985 .fail();
1986 yield null;
1987 }
1988 default -> {
1989 assertWithMessage("Missing modules list for style file '%s'", fileName)
1990 .fail();
1991 yield null;
1992 }
1993 };
1994
1995 String lastRuleName = null;
1996 String[] lastRuleNumberParts = null;
1997
1998 for (int position = 0; position < sources.getLength(); position++) {
1999 final Node row = sources.item(position);
2000 final List<Node> columns = new ArrayList<>(
2001 XmlUtil.findChildElementsByTag(row, "td"));
2002
2003 if (columns.isEmpty()) {
2004 continue;
2005 }
2006
2007 final String ruleName = columns.get(1).getTextContent().trim();
2008 lastRuleNumberParts = validateRuleNameOrder(
2009 fileName, lastRuleName, lastRuleNumberParts, ruleName);
2010
2011 if (!"--".equals(ruleName)) {
2012 validateStyleAnchors(XmlUtil.findChildElementsByTag(columns.getFirst(), "a"),
2013 fileName, ruleName);
2014 }
2015
2016 validateStyleModules(XmlUtil.findChildElementsByTag(columns.get(2), "a"),
2017 XmlUtil.findChildElementsByTag(columns.get(3), "a"), styleChecks, styleName,
2018 ruleName);
2019
2020 lastRuleName = ruleName;
2021 }
2022
2023 removeCommonUndocumentedModules(styleChecks);
2024 assertWithMessage(
2025 "%s requires the following check(s) to appear: %s", fileName, styleChecks)
2026 .that(styleChecks)
2027 .isEmpty();
2028 }
2029 }
2030
2031 private static String[] validateRuleNameOrder(String fileName, String lastRuleName,
2032 String[] lastRuleNumberParts, String ruleName) {
2033 final String[] ruleNumberParts = ruleName.split(" ", 2)[0].split("\\.");
2034
2035 if (lastRuleName != null) {
2036 final int ruleNumberPartsAmount = ruleNumberParts.length;
2037 final int lastRuleNumberPartsAmount = lastRuleNumberParts.length;
2038 final String outOfOrderReason = fileName + " rule '" + ruleName
2039 + "' is out of order compared to '" + lastRuleName + "'";
2040 boolean lastRuleNumberPartWasEqual = false;
2041 int partIndex;
2042 for (partIndex = 0; partIndex < ruleNumberPartsAmount; partIndex++) {
2043 if (lastRuleNumberPartsAmount <= partIndex) {
2044
2045
2046 break;
2047 }
2048
2049 final String ruleNumberPart = ruleNumberParts[partIndex];
2050 final String lastRuleNumberPart = lastRuleNumberParts[partIndex];
2051 final boolean ruleNumberPartsAreNumeric = IntStream.concat(
2052 ruleNumberPart.chars(),
2053 lastRuleNumberPart.chars()
2054 ).allMatch(Character::isDigit);
2055
2056 if (ruleNumberPartsAreNumeric) {
2057 final int numericRuleNumberPart = parseInt(ruleNumberPart);
2058 final int numericLastRuleNumberPart = parseInt(lastRuleNumberPart);
2059 assertWithMessage(outOfOrderReason)
2060 .that(numericRuleNumberPart)
2061 .isAtLeast(numericLastRuleNumberPart);
2062 }
2063 else {
2064 assertWithMessage(outOfOrderReason)
2065 .that(ruleNumberPart.compareToIgnoreCase(lastRuleNumberPart))
2066 .isAtLeast(0);
2067 }
2068 lastRuleNumberPartWasEqual = ruleNumberPart.equalsIgnoreCase(lastRuleNumberPart);
2069 if (!lastRuleNumberPartWasEqual) {
2070
2071
2072 break;
2073 }
2074 }
2075 if (ruleNumberPartsAmount == partIndex && lastRuleNumberPartWasEqual) {
2076 if (lastRuleNumberPartsAmount == partIndex) {
2077 assertWithMessage("%s rule '%s' and rule '%s' have the same rule number",
2078 fileName, ruleName, lastRuleName).fail();
2079 }
2080 else {
2081 assertWithMessage(outOfOrderReason).fail();
2082 }
2083 }
2084 }
2085
2086 return ruleNumberParts;
2087 }
2088
2089 private static void validateStyleAnchors(Set<Node> anchors, String fileName, String ruleName) {
2090 assertWithMessage("%s rule '%s' must have two row anchors", fileName, ruleName)
2091 .that(anchors)
2092 .hasSize(2);
2093
2094 final int space = ruleName.indexOf(' ');
2095 assertWithMessage(
2096 "%s rule '%s' must have have a space between the rule's number and the rule's name",
2097 fileName, ruleName)
2098 .that(space)
2099 .isNotEqualTo(-1);
2100
2101 final String ruleNumber = ruleName.substring(0, space);
2102
2103 int position = 1;
2104
2105 for (Node anchor : anchors) {
2106 final String actualUrl;
2107 final String expectedUrl;
2108
2109 if (position == 1) {
2110 actualUrl = XmlUtil.getNameAttributeOfNode(anchor);
2111 expectedUrl = "a" + ruleNumber;
2112 }
2113 else {
2114 actualUrl = anchor.getAttributes().getNamedItem("href").getTextContent();
2115 expectedUrl = "#" + ruleNumber;
2116 }
2117
2118 assertWithMessage("%s rule '%s' anchor %s should have matching name/url", fileName,
2119 ruleName, position)
2120 .that(actualUrl)
2121 .isEqualTo(expectedUrl);
2122
2123 position++;
2124 }
2125 }
2126
2127 private static void validateStyleModules(Set<Node> checks, Set<Node> configs,
2128 Set<String> styleChecks, String styleName, String ruleName) {
2129 final Iterator<Node> itrChecks = checks.iterator();
2130 final Iterator<Node> itrConfigs = configs.iterator();
2131 final boolean isGoogleDocumentation = "google".equals(styleName);
2132 final boolean isSunDocumentation = "sun".equals(styleName);
2133
2134 if (isGoogleDocumentation || isSunDocumentation) {
2135 validateChapterWiseTesting(itrChecks, itrConfigs, styleChecks, styleName, ruleName);
2136 }
2137 else {
2138 validateModuleWiseTesting(itrChecks, itrConfigs, styleChecks, styleName, ruleName);
2139 }
2140
2141 assertWithMessage("%s_style.xml rule '%s' has too many configs", styleName, ruleName)
2142 .that(itrConfigs.hasNext())
2143 .isFalse();
2144 }
2145
2146 private static void validateModuleWiseTesting(Iterator<Node> itrChecks,
2147 Iterator<Node> itrConfigs, Set<String> styleChecks, String styleName, String ruleName) {
2148 while (itrChecks.hasNext()) {
2149 final Node module = itrChecks.next();
2150 final String moduleName = module.getTextContent().trim();
2151 final String href = module.getAttributes().getNamedItem("href").getTextContent();
2152 final boolean moduleIsCheck = href.startsWith("checks/");
2153
2154 if (!moduleIsCheck) {
2155 continue;
2156 }
2157
2158 assertWithMessage("%s_style.xml rule '%s' module '%s' shouldn't end with 'Check'",
2159 styleName, ruleName, moduleName)
2160 .that(moduleName.endsWith("Check"))
2161 .isFalse();
2162
2163 styleChecks.remove(moduleName);
2164
2165 for (String configName : new String[] {"config", "test"}) {
2166 Node config = null;
2167
2168 try {
2169 config = itrConfigs.next();
2170 }
2171 catch (NoSuchElementException ignore) {
2172 assertWithMessage(
2173 "%s_style.xml rule '%s' module '%s' is missing the config link: %s",
2174 styleName, ruleName, moduleName, configName).fail();
2175 }
2176
2177 assertWithMessage(
2178 "%s_style.xml rule '%s' module '%s' has mismatched config/test links",
2179 styleName, ruleName, moduleName)
2180 .that(config.getTextContent().trim())
2181 .isEqualTo(configName);
2182
2183 final String configUrl = config.getAttributes().getNamedItem("href")
2184 .getTextContent();
2185
2186 if ("config".equals(configName)) {
2187 final String expectedUrl = getExpectedStyleGuideUrl(styleName + "_checks.xml")
2188 + moduleName;
2189
2190 assertWithMessage(
2191 "%s_style.xml rule '%s' module '%s' should have matching %s url", styleName,
2192 ruleName, moduleName, configName)
2193 .that(configUrl)
2194 .isEqualTo(expectedUrl);
2195 }
2196 else if ("test".equals(configName)) {
2197 assertWithMessage(
2198 "%s_style.xml rule '%s' module '%s' should have matching %s url", styleName,
2199 ruleName, moduleName, configName)
2200 .that(configUrl)
2201 .startsWith("https://github.com/checkstyle/checkstyle/"
2202 + "blob/master/src/it/java/com/" + styleName
2203 + "/checkstyle/test/");
2204 assertWithMessage(
2205 "%s_style.xml rule '%s' module '%s' should have matching %s url", styleName,
2206 ruleName, moduleName, configName)
2207 .that(configUrl)
2208 .endsWith("/" + moduleName + "Test.java");
2209
2210 assertWithMessage(
2211 "%s_style.xml rule '%s' module '%s' should have a test that exists",
2212 styleName, ruleName, moduleName)
2213 .that(new File(configUrl.substring(53).replace('/',
2214 File.separatorChar)).exists())
2215 .isTrue();
2216 }
2217 }
2218 }
2219 }
2220
2221 private static void validateChapterWiseTesting(Iterator<Node> itrChecks,
2222 Iterator<Node> itrSample, Set<String> styleChecks, String styleName, String ruleName) {
2223 boolean hasChecks = false;
2224 final Set<String> usedModules = new HashSet<>();
2225
2226 while (itrChecks.hasNext()) {
2227 final Node module = itrChecks.next();
2228 final String moduleName = module.getTextContent().trim();
2229 final String href = module.getAttributes().getNamedItem("href").getTextContent();
2230 final boolean moduleIsCheck = href.startsWith("checks/");
2231
2232 final String partialConfigUrl = "https://github.com/search?q="
2233 + "path%3Asrc%2Fmain%2Fresources%20path%3A**%2F" + styleName;
2234
2235 if (!moduleIsCheck) {
2236 if (href.startsWith(partialConfigUrl)) {
2237 assertWithMessage(
2238 "%s_style.xml rule '%s' module '%s' has too many config links",
2239 styleName, ruleName, moduleName).fail();
2240 }
2241 continue;
2242 }
2243
2244 hasChecks = true;
2245
2246 final Node idAttr = module.getAttributes().getNamedItem("id");
2247 String moduleId = "";
2248 if (idAttr != null) {
2249 moduleId = idAttr.getTextContent();
2250 }
2251 final String moduleKey;
2252 if (moduleId.isEmpty()) {
2253 moduleKey = moduleName;
2254 }
2255 else {
2256 moduleKey = moduleName + "#" + moduleId;
2257 }
2258
2259 assertWithMessage(
2260 "Module ids should be unique. Duplicate id '%s' was found for "
2261 + "module '%s' in rule '%s' of style guide '%s_style.xml'",
2262 moduleId, moduleName, ruleName, styleName)
2263 .that(usedModules)
2264 .doesNotContain(moduleKey);
2265
2266 usedModules.add(moduleKey);
2267
2268 assertWithMessage("%s_style.xml rule '%s' module '%s' shouldn't end with 'Check'",
2269 styleName, ruleName, moduleName)
2270 .that(moduleName.endsWith("Check"))
2271 .isFalse();
2272
2273 styleChecks.remove(moduleName);
2274
2275 if (itrChecks.hasNext()) {
2276 final Node config = itrChecks.next();
2277
2278 final String configUrl = config.getAttributes()
2279 .getNamedItem("href").getTextContent();
2280
2281 final String expectedUrl =
2282 partialConfigUrl + "_checks.xml+repo%3Acheckstyle%2Fcheckstyle+" + moduleName;
2283
2284 if (moduleId.isEmpty()) {
2285 assertWithMessage(
2286 "%s_style.xml rule '%s' module '%s' should have matching config url",
2287 styleName, ruleName, moduleName)
2288 .that(configUrl)
2289 .isEqualTo(expectedUrl);
2290 }
2291 else {
2292 final String expectedUrlWithId = expectedUrl + "+" + moduleId;
2293 assertWithMessage(
2294 "%s_style.xml rule '%s' module '%s' should have matching config url",
2295 styleName, ruleName, moduleName)
2296 .that(configUrl)
2297 .isEqualTo(expectedUrlWithId);
2298 }
2299 }
2300 else {
2301 assertWithMessage("%s_style.xml rule '%s' module '%s' is missing the config link",
2302 styleName, ruleName, moduleName).fail();
2303 }
2304 }
2305
2306 if (itrSample.hasNext()) {
2307 assertWithMessage("%s_style.xml rule '%s' should have checks if it has sample links",
2308 styleName, ruleName)
2309 .that(hasChecks)
2310 .isTrue();
2311
2312 final Node sample = itrSample.next();
2313 final String inputFolderUrl = sample.getAttributes().getNamedItem("href")
2314 .getTextContent();
2315 final String extractedChapterNumber = getExtractedChapterNumber(ruleName);
2316 final String extractedSectionNumber = getExtractedSectionNumber(ruleName);
2317
2318 assertWithMessage("%s_style.xml rule '%s' rule '' should have matching sample url",
2319 styleName, ruleName)
2320 .that(inputFolderUrl)
2321 .startsWith("https://github.com/checkstyle/checkstyle/"
2322 + "tree/master/src/it/resources/com/" + styleName
2323 + "/checkstyle/test/");
2324
2325 assertWithMessage("%s_style.xml rule '%s' should have matching sample url",
2326 styleName, ruleName)
2327 .that(inputFolderUrl)
2328 .containsMatch(
2329 "/chapter" + extractedChapterNumber
2330 + "\\D[^/]+/rule" + extractedSectionNumber + "\\D");
2331
2332 assertWithMessage(
2333 "%s_style.xml rule '%s' should have a inputs test folder that exists", styleName,
2334 ruleName)
2335 .that(new File(inputFolderUrl.substring(53).replace('/',
2336 File.separatorChar)).exists())
2337 .isTrue();
2338
2339 assertWithMessage("%s_style.xml rule '%s' has too many samples link", styleName,
2340 ruleName)
2341 .that(itrSample.hasNext())
2342 .isFalse();
2343 }
2344 else {
2345 assertWithMessage("%s_style.xml rule '%s' is missing sample link", styleName, ruleName)
2346 .that(hasChecks)
2347 .isFalse();
2348 }
2349 }
2350
2351 private static String getExpectedStyleGuideUrl(String styleGuideName) {
2352 return "https://github.com/search?q=path%3Asrc%2Fmain%2Fresources%20path%3A**%2F"
2353 + styleGuideName
2354 + "+repo%3Acheckstyle%2Fcheckstyle+";
2355 }
2356
2357 private static String getExtractedChapterNumber(String ruleName) {
2358 final Pattern pattern = Pattern.compile("^\\d+");
2359 final Matcher matcher = pattern.matcher(ruleName);
2360 matcher.find();
2361 return matcher.group();
2362 }
2363
2364 private static String getExtractedSectionNumber(String ruleName) {
2365 final Pattern pattern = Pattern.compile("^\\d+(\\.\\d+)*");
2366 final Matcher matcher = pattern.matcher(ruleName);
2367 matcher.find();
2368 return matcher.group().replaceAll("\\.", "");
2369 }
2370
2371 @Test
2372 public void testDocCommentsStyleRules() throws Exception {
2373 final Path path = Path.of("src/site/xdoc/doc-comments-style.xml");
2374 final NodeList sources = getTagSourcesNode(path, "tr");
2375 final Set<String> styleChecks = new HashSet<>(DOC_COMMENTS_MODULES);
2376
2377 for (int position = 0; position < sources.getLength(); position++) {
2378 final Node row = sources.item(position);
2379 final List<Node> columns = new ArrayList<>(
2380 XmlUtil.findChildElementsByTag(row, "td"));
2381
2382 if (columns.isEmpty()) {
2383 continue;
2384 }
2385
2386 final String ruleName = columns.get(1).getTextContent().trim();
2387
2388 validateDocCommentsStyleModules(XmlUtil.findChildElementsByTag(columns.get(2), "a"),
2389 XmlUtil.findChildElementsByTag(columns.get(3), "a"), styleChecks, ruleName);
2390 }
2391
2392 removeCommonUndocumentedModules(styleChecks);
2393 assertWithMessage(
2394 "doc-comments-style.xml requires the following check(s) to appear: %s",
2395 styleChecks)
2396 .that(styleChecks)
2397 .isEmpty();
2398 }
2399
2400 private static void validateDocCommentsStyleModules(Set<Node> checks, Set<Node> samples,
2401 Set<String> styleChecks, String ruleName) {
2402 final Iterator<Node> itrChecks = checks.iterator();
2403 boolean hasChecks = false;
2404 final Set<String> usedModules = new HashSet<>();
2405
2406 while (itrChecks.hasNext()) {
2407 final Node module = itrChecks.next();
2408 final String moduleName = module.getTextContent().trim();
2409 final String href = module.getAttributes().getNamedItem("href").getTextContent();
2410 final boolean moduleIsCheck = href.startsWith("checks/");
2411
2412 final String partialConfigUrl = getExpectedStyleGuideUrl("doc_comments_checks.xml");
2413
2414 if (!moduleIsCheck) {
2415 if (href.startsWith(partialConfigUrl)) {
2416 assertWithMessage(
2417 "doc-comments-style.xml rule '%s' module '%s' has too many config links",
2418 ruleName, moduleName).fail();
2419 }
2420 continue;
2421 }
2422
2423 hasChecks = true;
2424
2425 assertWithMessage(
2426 "The module '%s' in the rule '%s' of the style guide 'doc-comments-style.xml'"
2427 + " should not appear more than once in the section.",
2428 moduleName, ruleName)
2429 .that(usedModules)
2430 .doesNotContain(moduleName);
2431
2432 usedModules.add(moduleName);
2433
2434 assertWithMessage("doc-comments-style.xml rule '%s' module '%s' shouldn't end"
2435 + " with 'Check'", ruleName, moduleName)
2436 .that(moduleName.endsWith("Check"))
2437 .isFalse();
2438
2439 styleChecks.remove(moduleName);
2440
2441 if (itrChecks.hasNext()) {
2442 final Node config = itrChecks.next();
2443
2444 final String configUrl = config.getAttributes()
2445 .getNamedItem("href").getTextContent();
2446
2447 final String expectedUrl = partialConfigUrl + moduleName;
2448
2449 assertWithMessage(
2450 "doc-comments-style.xml rule '%s' module '%s' should have matching config url",
2451 ruleName, moduleName)
2452 .that(configUrl)
2453 .isEqualTo(expectedUrl);
2454 }
2455 else {
2456 assertWithMessage("doc-comments-style.xml rule '%s' module '%s' is missing the"
2457 + " config link", ruleName, moduleName).fail();
2458 }
2459 }
2460
2461 validateDocCommentsStyleSamples(samples.iterator(), hasChecks, ruleName);
2462 }
2463
2464 private static void validateDocCommentsStyleSamples(Iterator<Node> itrSample,
2465 boolean hasChecks, String ruleName) {
2466 if (itrSample.hasNext()) {
2467 assertWithMessage("doc-comments-style.xml rule '%s' should have checks if it has"
2468 + " sample links", ruleName)
2469 .that(hasChecks)
2470 .isTrue();
2471
2472 final Node sample = itrSample.next();
2473 final String inputFolderUrl = sample.getAttributes().getNamedItem("href")
2474 .getTextContent();
2475
2476 assertWithMessage("doc-comments-style.xml rule '%s' should have matching sample url",
2477 ruleName)
2478 .that(inputFolderUrl)
2479 .startsWith("https://github.com/checkstyle/checkstyle/"
2480 + "tree/master/src/it/resources/com/doccomments/checkstyle/test/");
2481
2482 assertWithMessage(
2483 "doc-comments-style.xml rule '%s' should have a inputs test folder that exists",
2484 ruleName)
2485 .that(new File(inputFolderUrl.substring(53).replace('/',
2486 File.separatorChar)).exists())
2487 .isTrue();
2488
2489 assertWithMessage("doc-comments-style.xml rule '%s' has too many samples link",
2490 ruleName)
2491 .that(itrSample.hasNext())
2492 .isFalse();
2493 }
2494 else {
2495 assertWithMessage("doc-comments-style.xml rule '%s' is missing sample link", ruleName)
2496 .that(hasChecks)
2497 .isFalse();
2498 }
2499 }
2500
2501 @Test
2502 public void testOpenJdkStyleRules() throws Exception {
2503 final Path path = Path.of("src/site/xdoc/openjdk-style.xml");
2504 final NodeList source = getTagSourcesNode(path, "tr");
2505 final Set<String> styleChecks = new HashSet<>(OPENJDK_MODULES);
2506
2507 for (int position = 0; position < source.getLength(); position++) {
2508 final Node row = source.item(position);
2509 final List<Node> columns = new ArrayList<>(
2510 XmlUtil.findChildElementsByTag(row, "td"));
2511
2512 if (columns.isEmpty()) {
2513 continue;
2514 }
2515 final String ruleName = columns.get(1).getTextContent().trim();
2516
2517 if (!"--".equals(ruleName)) {
2518 validateStyleAnchorsForOpenjdk(
2519 XmlUtil.findChildElementsByTag(columns.getFirst(), "a"),
2520 "openjdk_checks.xml", columns.get(1));
2521 }
2522
2523 validateOpenJdkStyleModules(XmlUtil.findChildElementsByTag(columns.get(2), "a"),
2524 XmlUtil.findChildElementsByTag(columns.get(3), "a"), styleChecks, ruleName);
2525 }
2526
2527 removeCommonUndocumentedModules(styleChecks);
2528 assertWithMessage(
2529 "openjdk-style.xml requires the following check(s) to appear: %s", styleChecks)
2530 .that(styleChecks)
2531 .isEmpty();
2532 }
2533
2534 private static void validateOpenJdkStyleModules(Set<Node> checks, Set<Node> samples,
2535 Set<String> styleChecks, String ruleName) {
2536 final Iterator<Node> itrChecks = checks.iterator();
2537 boolean hasChecks = false;
2538 final Set<String> usedModules = new HashSet<>();
2539
2540 while (itrChecks.hasNext()) {
2541 final Node module = itrChecks.next();
2542 final String moduleName = module.getTextContent().trim();
2543 final String href = module.getAttributes().getNamedItem("href").getTextContent();
2544 final boolean moduleIsCheck = href.startsWith("checks/");
2545
2546 final String partialConfigUrl = getExpectedStyleGuideUrl("openjdk_checks.xml");
2547
2548 if (!moduleIsCheck) {
2549 if (href.startsWith(partialConfigUrl)) {
2550 assertWithMessage(
2551 "openjdk-style.xml rule '%s' module '%s' has too many config links",
2552 ruleName, moduleName).fail();
2553 }
2554 continue;
2555 }
2556
2557 hasChecks = true;
2558
2559 assertWithMessage(
2560 "The module '%s' in the rule '%s' of the style guide 'openjdk-style.xml'"
2561 + " should not appear more than once in the section.",
2562 moduleName, ruleName)
2563 .that(usedModules)
2564 .doesNotContain(moduleName);
2565
2566 usedModules.add(moduleName);
2567
2568 assertWithMessage("openjdk-style.xml rule '%s' module '%s' shouldn't end"
2569 + " with 'Check'", ruleName, moduleName)
2570 .that(moduleName.endsWith("Check"))
2571 .isFalse();
2572
2573 styleChecks.remove(moduleName);
2574
2575 if (itrChecks.hasNext()) {
2576 final Node config = itrChecks.next();
2577
2578 final String configUrl = config.getAttributes()
2579 .getNamedItem("href").getTextContent();
2580
2581 final String expectedUrl = partialConfigUrl + moduleName;
2582
2583 assertWithMessage(
2584 "openjdk-style.xml rule '%s' module '%s' should have matching config url",
2585 ruleName, moduleName)
2586 .that(configUrl)
2587 .isEqualTo(expectedUrl);
2588 }
2589 else {
2590 assertWithMessage("openjdk-style.xml rule '%s' module '%s' is missing the"
2591 + " config link", ruleName, moduleName).fail();
2592 }
2593 }
2594
2595 validateOpenJdkStyleSamples(samples.iterator(), hasChecks, ruleName);
2596 }
2597
2598 private static void validateOpenJdkStyleSamples(Iterator<Node> itrSample,
2599 boolean hasChecks, String ruleName) {
2600 if (itrSample.hasNext()) {
2601 assertWithMessage("openjdk-style.xml rule '%s' should have checks if it has"
2602 + " sample links", ruleName)
2603 .that(hasChecks)
2604 .isTrue();
2605
2606 final Node sample = itrSample.next();
2607 final String inputFolderUrl = sample.getAttributes().getNamedItem("href")
2608 .getTextContent();
2609
2610 assertWithMessage("openjdk-style.xml rule '%s' should have matching sample url",
2611 ruleName)
2612 .that(inputFolderUrl)
2613 .startsWith("https://github.com/checkstyle/checkstyle/"
2614 + "tree/master/src/it/resources/com/openjdk/checkstyle/test/");
2615
2616 assertWithMessage(
2617 "openjdk-style.xml rule '%s' should have a inputs test folder that exists",
2618 ruleName)
2619 .that(new File(inputFolderUrl.substring(53).replace('/',
2620 File.separatorChar)).exists())
2621 .isTrue();
2622
2623 assertWithMessage("openjdk-style.xml rule '%s' has too many samples link",
2624 ruleName)
2625 .that(itrSample.hasNext())
2626 .isFalse();
2627 }
2628 else {
2629 assertWithMessage("openjdk-style.xml rule '%s' is missing sample link", ruleName)
2630 .that(hasChecks)
2631 .isFalse();
2632 }
2633 }
2634
2635 private static void validateStyleAnchorsForOpenjdk(Set<Node> anchors,
2636 String fileName, Node ruleColumn) {
2637
2638 final String ruleName = ruleColumn.getTextContent().trim();
2639 assertWithMessage("%s rule '%s' must have two row anchors", fileName, ruleName)
2640 .that(anchors)
2641 .hasSize(2);
2642
2643 final Node ruleAnchor = XmlUtil.findChildElementsByTag(ruleColumn, "a")
2644 .iterator().next();
2645 final String ruleHref = ruleAnchor.getAttributes()
2646 .getNamedItem("href").getTextContent();
2647
2648 final String anchorUrl = ruleHref.substring(ruleHref.indexOf('#') + 1);
2649
2650 int position = 1;
2651
2652 for (Node anchor : anchors) {
2653 final String actualUrl;
2654 final String expectedUrl;
2655
2656 if (position == 1) {
2657 actualUrl = XmlUtil.getNameAttributeOfNode(anchor);
2658 expectedUrl = anchorUrl;
2659 }
2660 else {
2661 actualUrl = anchor.getAttributes().getNamedItem("href").getTextContent();
2662 expectedUrl = "#" + anchorUrl;
2663 }
2664
2665 assertWithMessage("%s rule '%s' anchor %s should have matching name/url", fileName,
2666 ruleName, position)
2667 .that(actualUrl)
2668 .isEqualTo(expectedUrl);
2669
2670 position++;
2671 }
2672 }
2673
2674
2675
2676
2677
2678
2679
2680 private static void removeCommonUndocumentedModules(Set<String> styleChecks) {
2681 styleChecks.remove("BeforeExecutionExclusionFileFilter");
2682 styleChecks.remove("SuppressionFilter");
2683 styleChecks.remove("SuppressionXpathFilter");
2684 styleChecks.remove("SuppressionXpathSingleFilter");
2685 styleChecks.remove("TreeWalker");
2686 styleChecks.remove("Checker");
2687 styleChecks.remove("SuppressWithNearbyCommentFilter");
2688 styleChecks.remove("SuppressionCommentFilter");
2689 styleChecks.remove("SuppressWarningsFilter");
2690 styleChecks.remove("SuppressWarningsHolder");
2691 styleChecks.remove("SuppressWithNearbyTextFilter");
2692 styleChecks.remove("SuppressWithPlainTextCommentFilter");
2693 }
2694
2695 @Test
2696 public void testAllExampleMacrosHaveParagraphWithIdBeforeThem() throws Exception {
2697 for (Path path : XdocUtil.getXdocsTemplatesFilePaths()) {
2698 final String fileName = path.getFileName().toString();
2699 final NodeList sources = getTagSourcesNode(path, "macro");
2700
2701 for (int position = 0; position < sources.getLength(); position++) {
2702 final Node macro = sources.item(position);
2703 final String macroName = macro.getAttributes()
2704 .getNamedItem("name").getTextContent();
2705
2706 if (!"example".equals(macroName)) {
2707 continue;
2708 }
2709
2710 final Node precedingParagraph = getPrecedingParagraph(macro);
2711 assertWithMessage("%s: paragraph before example macro should have an id attribute",
2712 fileName)
2713 .that(precedingParagraph.hasAttributes())
2714 .isTrue();
2715
2716 final Node idAttribute = precedingParagraph.getAttributes().getNamedItem("id");
2717 assertWithMessage("%s: paragraph before example macro should have an id attribute",
2718 fileName)
2719 .that(idAttribute)
2720 .isNotNull();
2721
2722 validatePrecedingParagraphId(macro, fileName, idAttribute);
2723 }
2724 }
2725 }
2726
2727 private static void validatePrecedingParagraphId(
2728 Node macro, String fileName, Node idAttribute) {
2729 String exampleName = "";
2730 String exampleType = "";
2731 final NodeList params = macro.getChildNodes();
2732 for (int paramPosition = 0; paramPosition < params.getLength(); paramPosition++) {
2733 final Node item = params.item(paramPosition);
2734
2735 if (!"param".equals(item.getNodeName())) {
2736 continue;
2737 }
2738
2739 final String paramName = item.getAttributes()
2740 .getNamedItem("name").getTextContent();
2741 final String paramValue = item.getAttributes()
2742 .getNamedItem("value").getTextContent();
2743 if ("path".equals(paramName)) {
2744 exampleName = paramValue.substring(paramValue.lastIndexOf('/') + 1,
2745 paramValue.lastIndexOf('.'));
2746 }
2747 else if ("type".equals(paramName)) {
2748 exampleType = paramValue;
2749 }
2750 }
2751
2752 final String id = idAttribute.getTextContent();
2753 final String expectedId = String.format(Locale.ROOT, "%s-%s", exampleName,
2754 exampleType);
2755 if (expectedId.startsWith("package-info")) {
2756 assertWithMessage(
2757 "%s: paragraph before example macro should have the expected id value", fileName)
2758 .that(id)
2759 .endsWith(expectedId);
2760 }
2761 else {
2762 assertWithMessage(
2763 "%s: paragraph before example macro should have the expected id value", fileName)
2764 .that(id)
2765 .isEqualTo(expectedId);
2766 }
2767 }
2768
2769 private static Node getPrecedingParagraph(Node macro) {
2770 Node precedingNode = macro.getPreviousSibling();
2771 while (!"p".equals(precedingNode.getNodeName())) {
2772 precedingNode = precedingNode.getPreviousSibling();
2773 }
2774 return precedingNode;
2775 }
2776
2777 @Test
2778 public void validateExampleSectionSeparation() throws Exception {
2779 final List<Path> templates = collectAllXmlTemplatesUnderSrcSite();
2780
2781 for (final Path template : templates) {
2782 final Document doc = parseXmlToDomDocument(template);
2783 final NodeList subsectionList = doc.getElementsByTagName("subsection");
2784
2785 for (int index = 0; index < subsectionList.getLength(); index++) {
2786 final Element subsection = (Element) subsectionList.item(index);
2787 final String subSectionName = subsection.getAttribute("name");
2788
2789 if (!"Examples".equals(subSectionName) && !"Use Cases".equals(subSectionName)) {
2790 continue;
2791 }
2792
2793 final NodeList children = subsection.getChildNodes();
2794 String lastExampleIdPrefix = null;
2795 boolean separatorSeen = false;
2796
2797 for (int childIndex = 0; childIndex < children.getLength(); childIndex++) {
2798 final Node child = children.item(childIndex);
2799 if (child.getNodeType() != Node.ELEMENT_NODE) {
2800 continue;
2801 }
2802
2803 final Element element = (Element) child;
2804 if ("hr".equals(element.getTagName())
2805 && "example-separator".equals(element.getAttribute("class"))) {
2806 separatorSeen = true;
2807 continue;
2808 }
2809
2810 final String currentId = element.getAttribute("id");
2811 if (currentId != null && (currentId.startsWith("Example")
2812 || currentId.startsWith("UseCase"))) {
2813 final String currentExPrefix = getExamplePrefix(currentId);
2814 if (lastExampleIdPrefix != null
2815 && !lastExampleIdPrefix.equals(currentExPrefix)) {
2816 final boolean isSeparated = separatorSeen
2817 || isSeparatorSuppressed(template, lastExampleIdPrefix,
2818 currentExPrefix);
2819 assertWithMessage(
2820 "Missing <hr class=\"example-separator\"/> "
2821 + "between %s and %s in file: %s",
2822 lastExampleIdPrefix, currentExPrefix, template)
2823 .that(isSeparated)
2824 .isTrue();
2825 separatorSeen = false;
2826 }
2827 lastExampleIdPrefix = currentExPrefix;
2828 }
2829 }
2830 }
2831 }
2832 }
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843 private static boolean isSeparatorSuppressed(Path template, String previousExamplePrefix,
2844 String currentExamplePrefix) {
2845 final String key = template.getFileName() + ":" + previousExamplePrefix
2846 + ":" + currentExamplePrefix;
2847 return ALLOWED_EXAMPLES_WITHOUT_SEPARATOR.contains(key);
2848 }
2849
2850 private static List<Path> collectAllXmlTemplatesUnderSrcSite() throws IOException {
2851 final Path root = Path.of("src/site/xdoc");
2852 try (Stream<Path> walk = Files.walk(root)) {
2853 return walk
2854 .filter(path -> path.getFileName().toString().endsWith(".xml.template"))
2855 .collect(toImmutableList());
2856 }
2857 }
2858
2859 private static Document parseXmlToDomDocument(Path template) throws Exception {
2860 final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
2861 dbFactory.setNamespaceAware(true);
2862 final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
2863 final Document doc = dBuilder.parse(template.toFile());
2864 doc.getDocumentElement().normalize();
2865 return doc;
2866 }
2867
2868 private static String getExamplePrefix(String id) {
2869 final int dash = id.indexOf('-');
2870 final String result;
2871 if (dash == -1) {
2872 result = id;
2873 }
2874 else {
2875 result = id.substring(0, dash);
2876 }
2877 return result;
2878 }
2879
2880 @Test
2881 public void testAllOldReleaseNotesHaveRedirectInCheckstyleJs() throws Exception {
2882 final String checkstyleJsContent = Files.readString(CHECKSTYLE_JS_PATH);
2883 for (Path path : XdocUtil.getXdocsFilePaths()) {
2884 if (!path.toString().contains("release-notes-old-")) {
2885 continue;
2886 }
2887 final String fileNameWithoutExtension =
2888 path.getFileName().toString().replace(".xml", "");
2889 final String expectedRedirect = String.format(Locale.ROOT,
2890 "window.location.replace(`./%s.html", fileNameWithoutExtension);
2891 assertWithMessage(String.format(
2892 Locale.ROOT,
2893 "Missing redirect for %s: expected '%s...' in %s",
2894 fileNameWithoutExtension,
2895 expectedRedirect,
2896 CHECKSTYLE_JS_PATH))
2897 .that(checkstyleJsContent)
2898 .contains(expectedRedirect);
2899 }
2900 }
2901
2902 @Test
2903 public void testAllXdocsModulesTemplatesHaveSinceMacroAtTheBeginning() throws Exception {
2904 for (Path path : XdocUtil.getXdocsTemplatesFilePaths()) {
2905 final String fileName = path.getFileName().toString();
2906
2907 if (isNonModulePage(fileName.replace(".template", ""))) {
2908 continue;
2909 }
2910
2911 final NodeList sources = getTagSourcesNode(path, "section");
2912 final Node section = sources.item(0);
2913 final String sectionName = section.getNodeName();
2914 final Node firstChild = XmlUtil.getFirstChildElement(section);
2915 assertWithMessage(
2916 "%s first child of section %s should be a <macro> tag", fileName, sectionName)
2917 .that(firstChild.getNodeName())
2918 .isEqualTo("macro");
2919 assertWithMessage(
2920 "%s first child of section %s should be a <macro> tag with name 'since'", fileName,
2921 sectionName)
2922 .that(firstChild.getAttributes().getNamedItem("name").getTextContent())
2923 .isEqualTo("since");
2924 }
2925 }
2926
2927 @Test
2928 public void testUseCasesSectionExistsWhenUseCaseIdsPresent() throws Exception {
2929 final List<Path> templates = collectAllXmlTemplatesUnderSrcSite();
2930 final List<Path> violations = new ArrayList<>();
2931
2932 for (final Path template : templates) {
2933 final Document doc = parseXmlToDomDocument(template);
2934
2935 if (hasAnyUseCaseId(doc) && !hasUseCasesSubsection(doc)) {
2936 violations.add(template);
2937 }
2938 }
2939
2940 final String message;
2941 if (violations.isEmpty()) {
2942 message = "";
2943 }
2944 else {
2945 final StringBuilder builder = new StringBuilder(256);
2946 builder.append("Found ")
2947 .append(violations.size())
2948 .append(" template(s) with 'UseCase' ids but no "
2949 + "<subsection name=\"Use Cases\" .../> to hold them:\n");
2950 for (Path violation : violations) {
2951 builder.append(" ").append(violation).append('\n');
2952 }
2953 message = builder.toString();
2954 }
2955
2956 assertWithMessage(message)
2957 .that(violations)
2958 .isEmpty();
2959 }
2960
2961 @Test
2962 public void testAllExampleAndUseCaseParagraphsHaveDescriptiveText() throws Exception {
2963 final List<Path> templates = collectAllXmlTemplatesUnderSrcSite();
2964
2965 assertWithMessage("Expected to find at least one xdoc template under src/site")
2966 .that(templates)
2967 .isNotEmpty();
2968
2969 final List<String> failures = new ArrayList<>();
2970
2971 for (final Path template : templates) {
2972 final String content = Files.readString(template);
2973 final String fileName = template.getFileName().toString();
2974
2975 failures.addAll(validateTocExtractableDescriptions(fileName, content));
2976 }
2977
2978 assertWithMessage("TOC-extractable description problems found:\n%s",
2979 String.join("\n", failures))
2980 .that(failures)
2981 .isEmpty();
2982 }
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995 private static List<String> validateTocExtractableDescriptions(String fileName,
2996 String content) throws Exception {
2997 final Document doc = parseXml(content);
2998 final Set<String> matchedIds = new HashSet<>();
2999 final List<String> failures = new ArrayList<>();
3000 final NodeList paragraphs = doc.getElementsByTagName("p");
3001
3002 for (int index = 0; index < paragraphs.getLength(); index++) {
3003 final Element paragraph = (Element) paragraphs.item(index);
3004 final Matcher idMatcher = EXAMPLE_ID_PATTERN.matcher(paragraph.getAttribute("id"));
3005
3006 if (!idMatcher.matches()) {
3007 continue;
3008 }
3009
3010 final Element nextElement = nextSiblingElement(paragraph);
3011 if (nextElement == null
3012 || !"macro".equals(nextElement.getTagName())
3013 || !"example".equals(nextElement.getAttribute("name"))
3014 || !hasPathParam(nextElement)) {
3015 continue;
3016 }
3017
3018 final String exampleId = idMatcher.group(1);
3019 final String strippedText = TAG_PATTERN.matcher(paragraph.getTextContent())
3020 .replaceAll("")
3021 .replaceAll("\\s+", " ")
3022 .trim();
3023
3024 if ("Notes:".equals(strippedText)) {
3025 matchedIds.add(exampleId);
3026 continue;
3027 }
3028
3029 if (strippedText.isEmpty()) {
3030 failures.add(String.format(Locale.ROOT,
3031 "%s: description paragraph for '%s-config' must have non-empty text "
3032 + "so TocMacro can extract a TOC title from it",
3033 fileName, exampleId));
3034 }
3035
3036 matchedIds.add(exampleId);
3037 }
3038
3039 final Set<String> unmatchedIds = new TreeSet<>(findAllExampleAndUseCaseIds(content));
3040 unmatchedIds.removeAll(matchedIds);
3041
3042 if (!unmatchedIds.isEmpty()) {
3043 failures.add(String.format(Locale.ROOT,
3044 "%s: the following Example/UseCase ids have a config paragraph that "
3045 + "TocMacro's extraction pattern cannot match (paragraph must "
3046 + "immediately precede a <macro name=\"example\"> with a 'path' "
3047 + "param): %s",
3048 fileName, unmatchedIds));
3049 }
3050 return failures;
3051 }
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063 private static Set<String> findAllExampleAndUseCaseIds(String content) throws Exception {
3064 final Document doc = parseXml(content);
3065 final Set<String> result = new TreeSet<>();
3066 final NodeList paragraphs = doc.getElementsByTagName("p");
3067
3068 for (int index = 0; index < paragraphs.getLength(); index++) {
3069 final Element paragraph = (Element) paragraphs.item(index);
3070 final Matcher idMatcher = EXAMPLE_ID_PATTERN.matcher(paragraph.getAttribute("id"));
3071 if (idMatcher.matches()) {
3072 result.add(idMatcher.group(1));
3073 }
3074 }
3075
3076 return result;
3077 }
3078
3079
3080
3081
3082
3083
3084
3085
3086 private static Document parseXml(String content) throws Exception {
3087 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
3088 factory.setNamespaceAware(false);
3089 final DocumentBuilder builder = factory.newDocumentBuilder();
3090 return builder.parse(new InputSource(new StringReader(content)));
3091 }
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101 private static Element nextSiblingElement(Node node) {
3102 Node sibling = node.getNextSibling();
3103 Element result = null;
3104 while (sibling != null) {
3105 if (sibling.getNodeType() == Node.ELEMENT_NODE) {
3106 final Element element = (Element) sibling;
3107
3108 if (!"ul".equals(element.getTagName())) {
3109 result = element;
3110 break;
3111 }
3112 }
3113 sibling = sibling.getNextSibling();
3114 }
3115 return result;
3116 }
3117
3118
3119
3120
3121
3122
3123
3124
3125 private static boolean hasPathParam(Element macroElement) {
3126 final NodeList params = macroElement.getElementsByTagName("param");
3127 boolean result = false;
3128 for (int index = 0; index < params.getLength(); index++) {
3129 final Element param = (Element) params.item(index);
3130 if ("path".equals(param.getAttribute("name"))) {
3131 result = true;
3132 break;
3133 }
3134 }
3135 return result;
3136 }
3137
3138 private static boolean hasAnyUseCaseId(Document doc) {
3139 final NodeList allParagraphElements = doc.getElementsByTagName("p");
3140 boolean found = false;
3141
3142 for (int index = 0; !found && index < allParagraphElements.getLength(); index++) {
3143 final Element element = (Element) allParagraphElements.item(index);
3144 final String id = element.getAttribute("id");
3145 if (id != null && id.startsWith("UseCase")) {
3146 found = true;
3147 }
3148 }
3149
3150 return found;
3151 }
3152
3153 private static boolean hasUseCasesSubsection(Document doc) {
3154 final NodeList subsections = doc.getElementsByTagName("subsection");
3155 boolean found = false;
3156
3157 for (int index = 0; !found && index < subsections.getLength(); index++) {
3158 final Element subsection = (Element) subsections.item(index);
3159 if ("Use Cases".equals(subsection.getAttribute("name"))) {
3160 found = true;
3161 }
3162 }
3163
3164 return found;
3165 }
3166
3167 @FunctionalInterface
3168 private interface PredicateProcess {
3169 boolean hasFit(Path path);
3170 }
3171
3172 }