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