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