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