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