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