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.site;
21
22 import java.beans.PropertyDescriptor;
23 import java.io.File;
24 import java.io.IOException;
25 import java.lang.module.ModuleDescriptor.Version;
26 import java.lang.reflect.Array;
27 import java.lang.reflect.Field;
28 import java.lang.reflect.InvocationTargetException;
29 import java.lang.reflect.ParameterizedType;
30 import java.net.URI;
31 import java.nio.charset.StandardCharsets;
32 import java.nio.file.Files;
33 import java.nio.file.Path;
34 import java.util.ArrayDeque;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.BitSet;
38 import java.util.Collection;
39 import java.util.Deque;
40 import java.util.HashMap;
41 import java.util.HashSet;
42 import java.util.LinkedHashMap;
43 import java.util.List;
44 import java.util.Locale;
45 import java.util.Map;
46 import java.util.Objects;
47 import java.util.Optional;
48 import java.util.Set;
49 import java.util.TreeSet;
50 import java.util.regex.Pattern;
51 import java.util.stream.Collectors;
52 import java.util.stream.IntStream;
53 import java.util.stream.Stream;
54
55 import javax.annotation.Nullable;
56
57 import org.apache.commons.beanutils.PropertyUtils;
58 import org.apache.maven.doxia.macro.MacroExecutionException;
59
60 import com.google.common.collect.Lists;
61 import com.puppycrawl.tools.checkstyle.Checker;
62 import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
63 import com.puppycrawl.tools.checkstyle.ModuleFactory;
64 import com.puppycrawl.tools.checkstyle.PackageNamesLoader;
65 import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
66 import com.puppycrawl.tools.checkstyle.PropertyCacheFile;
67 import com.puppycrawl.tools.checkstyle.TreeWalker;
68 import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
69 import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
70 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
71 import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
72 import com.puppycrawl.tools.checkstyle.api.BeforeExecutionFileFilter;
73 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
74 import com.puppycrawl.tools.checkstyle.api.DetailNode;
75 import com.puppycrawl.tools.checkstyle.api.Filter;
76 import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
77 import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
78 import com.puppycrawl.tools.checkstyle.checks.naming.AccessModifierOption;
79 import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpMultilineCheck;
80 import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineCheck;
81 import com.puppycrawl.tools.checkstyle.checks.regexp.RegexpSinglelineJavaCheck;
82 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
83 import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
84 import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
85
86
87
88
89 public final class SiteUtil {
90
91
92 public static final String TOKENS = "tokens";
93
94 public static final String JAVADOC_TOKENS = "javadocTokens";
95
96 public static final String DOT = ".";
97
98 public static final String COMMA_SPACE = ", ";
99
100 public static final String TOKEN_TYPES = "TokenTypes";
101
102 public static final String PATH_TO_TOKEN_TYPES =
103 "apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html";
104
105 public static final String PATH_TO_JAVADOC_TOKEN_TYPES =
106 "apidocs/com/puppycrawl/tools/checkstyle/api/JavadocTokenTypes.html";
107
108 public static final String SINCE_VERSION = "Since version";
109
110 private static final String CHECKSTYLE_ORG_URL = "https://checkstyle.org/";
111
112 private static final String CHARSET = "charset";
113
114 private static final String CURLY_BRACKETS = "{}";
115
116 private static final String FILE_EXTENSIONS = "fileExtensions";
117
118 private static final String CHECKS = "checks";
119
120 private static final String NAMING = "naming";
121
122 private static final String SRC = "src";
123
124 private static final String WHITESPACE = " ";
125
126
127 private static final Pattern SETTER_PATTERN = Pattern.compile("^Setter to ");
128
129
130 private static final Map<Class<?>, String> CLASS_TO_PARENT_MODULE = Map.ofEntries(
131 Map.entry(AbstractCheck.class, TreeWalker.class.getSimpleName()),
132 Map.entry(TreeWalkerFilter.class, TreeWalker.class.getSimpleName()),
133 Map.entry(AbstractFileSetCheck.class, Checker.class.getSimpleName()),
134 Map.entry(Filter.class, Checker.class.getSimpleName()),
135 Map.entry(BeforeExecutionFileFilter.class, Checker.class.getSimpleName())
136 );
137
138
139 private static final Set<String> CHECK_PROPERTIES =
140 getProperties(AbstractCheck.class);
141
142
143 private static final Set<String> JAVADOC_CHECK_PROPERTIES =
144 getProperties(AbstractJavadocCheck.class);
145
146
147 private static final Set<String> FILESET_PROPERTIES =
148 getProperties(AbstractFileSetCheck.class);
149
150
151
152
153 private static final String HEADER_CHECK_HEADER = "HeaderCheck.header";
154
155
156
157
158 private static final String REGEXP_HEADER_CHECK_HEADER = "RegexpHeaderCheck.header";
159
160
161 private static final Set<String> UNDOCUMENTED_PROPERTIES = Set.of(
162 "SuppressWithNearbyCommentFilter.fileContents",
163 "SuppressionCommentFilter.fileContents"
164 );
165
166
167 private static final Set<String> PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD = Set.of(
168
169 "SuppressWarningsHolder.aliasList",
170
171 HEADER_CHECK_HEADER,
172 REGEXP_HEADER_CHECK_HEADER,
173
174 "RedundantModifierCheck.jdkVersion",
175
176 "CustomImportOrderCheck.customImportOrderRules"
177 );
178
179
180 private static final Map<String, DetailNode> SUPER_CLASS_PROPERTIES_JAVADOCS =
181 new HashMap<>();
182
183
184 private static final String MAIN_FOLDER_PATH = Path.of(
185 SRC, "main", "java", "com", "puppycrawl", "tools", "checkstyle").toString();
186
187
188 private static final List<Path> MODULE_SUPER_CLASS_PATHS = List.of(
189 Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractAccessControlNameCheck.java"),
190 Path.of(MAIN_FOLDER_PATH, CHECKS, NAMING, "AbstractNameCheck.java"),
191 Path.of(MAIN_FOLDER_PATH, CHECKS, "javadoc", "AbstractJavadocCheck.java"),
192 Path.of(MAIN_FOLDER_PATH, "api", "AbstractFileSetCheck.java"),
193 Path.of(MAIN_FOLDER_PATH, CHECKS, "header", "AbstractHeaderCheck.java"),
194 Path.of(MAIN_FOLDER_PATH, CHECKS, "metrics", "AbstractClassCouplingCheck.java"),
195 Path.of(MAIN_FOLDER_PATH, CHECKS, "whitespace", "AbstractParenPadCheck.java")
196 );
197
198
199
200
201 private SiteUtil() {
202 }
203
204
205
206
207
208
209
210
211 public static Set<String> getMessageKeys(Class<?> module)
212 throws MacroExecutionException {
213 final Set<Field> messageKeyFields = getCheckMessageKeys(module);
214
215 final Set<String> messageKeys = new TreeSet<>();
216 for (Field field : messageKeyFields) {
217 messageKeys.add(getFieldValue(field, module).toString());
218 }
219 return messageKeys;
220 }
221
222
223
224
225
226
227
228
229
230
231
232
233 private static Set<Field> getCheckMessageKeys(Class<?> module)
234 throws MacroExecutionException {
235 try {
236 final Set<Field> checkstyleMessages = new HashSet<>();
237
238
239 final Field[] fields = module.getDeclaredFields();
240
241 for (Field field : fields) {
242 if (field.getName().startsWith("MSG_")) {
243 checkstyleMessages.add(field);
244 }
245 }
246
247
248 final Class<?> superModule = module.getSuperclass();
249
250 if (superModule != null) {
251 checkstyleMessages.addAll(getCheckMessageKeys(superModule));
252 }
253
254
255 if (module == RegexpMultilineCheck.class) {
256 checkstyleMessages.addAll(getCheckMessageKeys(Class
257 .forName("com.puppycrawl.tools.checkstyle.checks.regexp.MultilineDetector")));
258 }
259 else if (module == RegexpSinglelineCheck.class
260 || module == RegexpSinglelineJavaCheck.class) {
261 checkstyleMessages.addAll(getCheckMessageKeys(Class
262 .forName("com.puppycrawl.tools.checkstyle.checks.regexp.SinglelineDetector")));
263 }
264
265 return checkstyleMessages;
266 }
267 catch (ClassNotFoundException exc) {
268 final String message = String.format(Locale.ROOT, "Couldn't find class: %s",
269 module.getName());
270 throw new MacroExecutionException(message, exc);
271 }
272 }
273
274
275
276
277
278
279
280
281
282 public static Object getFieldValue(Field field, Object instance)
283 throws MacroExecutionException {
284 try {
285
286 field.trySetAccessible();
287 return field.get(instance);
288 }
289 catch (IllegalAccessException exc) {
290 throw new MacroExecutionException("Couldn't get field value", exc);
291 }
292 }
293
294
295
296
297
298
299
300
301 public static Object getModuleInstance(String moduleName) throws MacroExecutionException {
302 final ModuleFactory factory = getPackageObjectFactory();
303 try {
304 return factory.createModule(moduleName);
305 }
306 catch (CheckstyleException exc) {
307 throw new MacroExecutionException("Couldn't find class: " + moduleName, exc);
308 }
309 }
310
311
312
313
314
315
316
317 private static PackageObjectFactory getPackageObjectFactory() throws MacroExecutionException {
318 try {
319 final ClassLoader cl = ViolationMessagesMacro.class.getClassLoader();
320 final Set<String> packageNames = PackageNamesLoader.getPackageNames(cl);
321 return new PackageObjectFactory(packageNames, cl);
322 }
323 catch (CheckstyleException exc) {
324 throw new MacroExecutionException("Couldn't load checkstyle modules", exc);
325 }
326 }
327
328
329
330
331
332
333
334
335
336
337
338 public static String getNewlineAndIndentSpaces(int amountOfSpaces) {
339 return System.lineSeparator() + WHITESPACE.repeat(amountOfSpaces);
340 }
341
342
343
344
345
346
347
348
349
350 public static Path getTemplatePath(String moduleName) throws MacroExecutionException {
351 final String fileNamePattern = ".*[\\\\/]"
352 + moduleName.toLowerCase(Locale.ROOT) + "\\..*";
353 return getXdocsTemplatesFilePaths()
354 .stream()
355 .filter(path -> path.toString().matches(fileNamePattern))
356 .findFirst()
357 .orElse(null);
358 }
359
360
361
362
363
364
365
366
367
368 public static Set<Path> getXdocsTemplatesFilePaths() throws MacroExecutionException {
369 final Path directory = Path.of("src/site/xdoc");
370 try (Stream<Path> stream = Files.find(directory, Integer.MAX_VALUE,
371 (path, attr) -> {
372 return attr.isRegularFile()
373 && path.toString().endsWith(".xml.template");
374 })) {
375 return stream.collect(Collectors.toUnmodifiableSet());
376 }
377 catch (IOException ioException) {
378 throw new MacroExecutionException("Failed to find xdocs templates", ioException);
379 }
380 }
381
382
383
384
385
386
387
388
389
390 public static String getParentModule(Class<?> moduleClass)
391 throws MacroExecutionException {
392 String parentModuleName = "";
393 Class<?> parentClass = moduleClass.getSuperclass();
394
395 while (parentClass != null) {
396 parentModuleName = CLASS_TO_PARENT_MODULE.get(parentClass);
397 if (parentModuleName != null) {
398 break;
399 }
400 parentClass = parentClass.getSuperclass();
401 }
402
403
404 if (parentModuleName == null || parentModuleName.isEmpty()) {
405 final Class<?>[] interfaces = moduleClass.getInterfaces();
406 for (Class<?> interfaceClass : interfaces) {
407 parentModuleName = CLASS_TO_PARENT_MODULE.get(interfaceClass);
408 if (parentModuleName != null) {
409 break;
410 }
411 }
412 }
413
414 if (parentModuleName == null || parentModuleName.isEmpty()) {
415 final String message = String.format(Locale.ROOT,
416 "Failed to find parent module for %s", moduleClass.getSimpleName());
417 throw new MacroExecutionException(message);
418 }
419
420 return parentModuleName;
421 }
422
423
424
425
426
427
428
429
430 public static Set<String> getPropertiesForDocumentation(Class<?> clss, Object instance) {
431 final Set<String> properties =
432 getProperties(clss).stream()
433 .filter(prop -> {
434 return !isGlobalProperty(clss, prop) && !isUndocumentedProperty(clss, prop);
435 })
436 .collect(Collectors.toCollection(HashSet::new));
437 properties.addAll(getNonExplicitProperties(instance, clss));
438 return new TreeSet<>(properties);
439 }
440
441
442
443
444
445
446
447
448
449 public static DetailNode getModuleJavadoc(String moduleName, Path modulePath)
450 throws MacroExecutionException {
451
452 processModule(moduleName, modulePath);
453 return JavadocScraperResultUtil.getModuleJavadocNode();
454 }
455
456
457
458
459
460
461
462
463
464
465
466 public static Map<String, DetailNode> getPropertiesJavadocs(Set<String> properties,
467 String moduleName, Path modulePath)
468 throws MacroExecutionException {
469
470 if (SUPER_CLASS_PROPERTIES_JAVADOCS.isEmpty()) {
471 processSuperclasses();
472 }
473
474 processModule(moduleName, modulePath);
475
476 final Map<String, DetailNode> unmodifiablePropertiesJavadocs =
477 JavadocScraperResultUtil.getPropertiesJavadocNode();
478 final Map<String, DetailNode> propertiesJavadocs =
479 new LinkedHashMap<>(unmodifiablePropertiesJavadocs);
480
481 properties.forEach(property -> {
482 final DetailNode superClassPropertyJavadoc =
483 SUPER_CLASS_PROPERTIES_JAVADOCS.get(property);
484 if (superClassPropertyJavadoc != null) {
485 propertiesJavadocs.putIfAbsent(property, superClassPropertyJavadoc);
486 }
487 });
488
489 assertAllPropertySetterJavadocsAreFound(properties, moduleName, propertiesJavadocs);
490
491 return propertiesJavadocs;
492 }
493
494
495
496
497
498
499
500
501
502
503
504 private static void assertAllPropertySetterJavadocsAreFound(
505 Set<String> properties, String moduleName, Map<String, DetailNode> javadocs)
506 throws MacroExecutionException {
507 for (String property : properties) {
508 final boolean isDocumented = javadocs.containsKey(property)
509 || SUPER_CLASS_PROPERTIES_JAVADOCS.containsKey(property)
510 || TOKENS.equals(property) || JAVADOC_TOKENS.equals(property);
511 if (!isDocumented) {
512 throw new MacroExecutionException(String.format(Locale.ROOT,
513 "%s: Missing documentation for property '%s'. Check superclasses.",
514 moduleName, property));
515 }
516 }
517 }
518
519
520
521
522
523
524 private static void processSuperclasses() throws MacroExecutionException {
525 for (Path superclassPath : MODULE_SUPER_CLASS_PATHS) {
526 final Path fileNamePath = superclassPath.getFileName();
527 if (fileNamePath == null) {
528 throw new MacroExecutionException("Invalid superclass path: " + superclassPath);
529 }
530 final String superclassName = CommonUtil.getFileNameWithoutExtension(
531 fileNamePath.toString());
532 processModule(superclassName, superclassPath);
533 final Map<String, DetailNode> superclassPropertiesJavadocs =
534 JavadocScraperResultUtil.getPropertiesJavadocNode();
535 SUPER_CLASS_PROPERTIES_JAVADOCS.putAll(superclassPropertiesJavadocs);
536 }
537 }
538
539
540
541
542
543
544
545
546
547 private static void processModule(String moduleName, Path modulePath)
548 throws MacroExecutionException {
549 if (!Files.isRegularFile(modulePath)) {
550 final String message = String.format(Locale.ROOT,
551 "File %s is not a file. Please check the 'modulePath' property.", modulePath);
552 throw new MacroExecutionException(message);
553 }
554 ClassAndPropertiesSettersJavadocScraper.initialize(moduleName);
555 final Checker checker = new Checker();
556 checker.setModuleClassLoader(Checker.class.getClassLoader());
557 final DefaultConfiguration scraperCheckConfig =
558 new DefaultConfiguration(
559 ClassAndPropertiesSettersJavadocScraper.class.getName());
560 final DefaultConfiguration defaultConfiguration =
561 new DefaultConfiguration("configuration");
562 final DefaultConfiguration treeWalkerConfig =
563 new DefaultConfiguration(TreeWalker.class.getName());
564 defaultConfiguration.addProperty(CHARSET, StandardCharsets.UTF_8.name());
565 defaultConfiguration.addChild(treeWalkerConfig);
566 treeWalkerConfig.addChild(scraperCheckConfig);
567 try {
568 checker.configure(defaultConfiguration);
569 final List<File> filesToProcess = List.of(modulePath.toFile());
570 checker.process(filesToProcess);
571 checker.destroy();
572 }
573 catch (CheckstyleException checkstyleException) {
574 final String message = String.format(Locale.ROOT, "Failed processing %s", moduleName);
575 throw new MacroExecutionException(message, checkstyleException);
576 }
577 }
578
579
580
581
582
583
584
585 public static Set<String> getProperties(Class<?> clss) {
586 final Set<String> result = new TreeSet<>();
587 final PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(clss);
588
589 for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
590 if (propertyDescriptor.getWriteMethod() != null) {
591 result.add(propertyDescriptor.getName());
592 }
593 }
594
595 return result;
596 }
597
598
599
600
601
602
603
604
605
606 private static boolean isGlobalProperty(Class<?> clss, String propertyName) {
607 return AbstractCheck.class.isAssignableFrom(clss)
608 && CHECK_PROPERTIES.contains(propertyName)
609 || AbstractJavadocCheck.class.isAssignableFrom(clss)
610 && JAVADOC_CHECK_PROPERTIES.contains(propertyName)
611 || AbstractFileSetCheck.class.isAssignableFrom(clss)
612 && FILESET_PROPERTIES.contains(propertyName);
613 }
614
615
616
617
618
619
620
621
622 private static boolean isUndocumentedProperty(Class<?> clss, String propertyName) {
623 return UNDOCUMENTED_PROPERTIES.contains(clss.getSimpleName() + DOT + propertyName);
624 }
625
626
627
628
629
630
631
632
633
634 private static Set<String> getNonExplicitProperties(
635 Object instance, Class<?> clss) {
636 final Set<String> result = new TreeSet<>();
637 if (AbstractCheck.class.isAssignableFrom(clss)) {
638 final AbstractCheck check = (AbstractCheck) instance;
639
640 final int[] acceptableTokens = check.getAcceptableTokens();
641 Arrays.sort(acceptableTokens);
642 final int[] defaultTokens = check.getDefaultTokens();
643 Arrays.sort(defaultTokens);
644 final int[] requiredTokens = check.getRequiredTokens();
645 Arrays.sort(requiredTokens);
646
647 if (!Arrays.equals(acceptableTokens, defaultTokens)
648 || !Arrays.equals(acceptableTokens, requiredTokens)) {
649 result.add(TOKENS);
650 }
651 }
652
653 if (AbstractJavadocCheck.class.isAssignableFrom(clss)) {
654 final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
655 result.add("violateExecutionOnNonTightHtml");
656
657 final int[] acceptableJavadocTokens = check.getAcceptableJavadocTokens();
658 Arrays.sort(acceptableJavadocTokens);
659 final int[] defaultJavadocTokens = check.getDefaultJavadocTokens();
660 Arrays.sort(defaultJavadocTokens);
661 final int[] requiredJavadocTokens = check.getRequiredJavadocTokens();
662 Arrays.sort(requiredJavadocTokens);
663
664 if (!Arrays.equals(acceptableJavadocTokens, defaultJavadocTokens)
665 || !Arrays.equals(acceptableJavadocTokens, requiredJavadocTokens)) {
666 result.add(JAVADOC_TOKENS);
667 }
668 }
669
670 if (AbstractFileSetCheck.class.isAssignableFrom(clss)) {
671 result.add(FILE_EXTENSIONS);
672 }
673 return result;
674 }
675
676
677
678
679
680
681
682
683
684
685 public static String getPropertyDescription(
686 String propertyName, DetailNode javadoc, String moduleName)
687 throws MacroExecutionException {
688 final String description;
689 if (TOKENS.equals(propertyName)) {
690 description = "tokens to check";
691 }
692 else if (JAVADOC_TOKENS.equals(propertyName)) {
693 description = "javadoc tokens to check";
694 }
695 else {
696 final String descriptionString = SETTER_PATTERN.matcher(
697 DescriptionExtractor.getDescriptionFromJavadoc(javadoc, moduleName))
698 .replaceFirst("");
699
700 final String firstLetterCapitalized = descriptionString.substring(0, 1)
701 .toUpperCase(Locale.ROOT);
702 description = firstLetterCapitalized + descriptionString.substring(1);
703 }
704 return description;
705 }
706
707
708
709
710
711
712
713
714
715
716
717 public static String getPropertySinceVersion(String moduleName, DetailNode moduleJavadoc,
718 String propertyName, DetailNode propertyJavadoc)
719 throws MacroExecutionException {
720 final String sinceVersion;
721
722 final Optional<String> specifiedPropertyVersionInModule =
723 getSpecifiedPropertyVersionInModule(propertyName, moduleJavadoc);
724
725 if (specifiedPropertyVersionInModule.isPresent()) {
726 sinceVersion = specifiedPropertyVersionInModule.get();
727 }
728 else {
729 final String moduleSince = getSinceVersionFromJavadoc(moduleJavadoc);
730
731 if (moduleSince == null) {
732 throw new MacroExecutionException(
733 "Missing @since on module " + moduleName);
734 }
735
736 String propertySince = null;
737 if (propertyJavadoc != null) {
738 propertySince = getSinceVersionFromJavadoc(propertyJavadoc);
739 }
740
741 if (propertySince != null && isVersionAtLeast(propertySince, moduleSince)) {
742 sinceVersion = propertySince;
743 }
744 else {
745 sinceVersion = moduleSince;
746 }
747 }
748
749 return sinceVersion;
750 }
751
752
753
754
755
756
757
758
759
760 private static Optional<String> getSpecifiedPropertyVersionInModule(String propertyName,
761 DetailNode moduleJavadoc)
762 throws MacroExecutionException {
763 Optional<String> specifiedVersion = Optional.empty();
764
765 final Optional<DetailNode> propertyNodeFromModuleJavadoc =
766 getPropertyJavadocNodeInModule(propertyName, moduleJavadoc);
767
768 if (propertyNodeFromModuleJavadoc.isPresent()) {
769 final List<DetailNode> propertyModuleTextNodes = getNodesOfSpecificType(
770 propertyNodeFromModuleJavadoc.get().getChildren(), JavadocTokenTypes.TEXT);
771
772 final Optional<String> sinceVersionLine = propertyModuleTextNodes.stream()
773 .map(DetailNode::getText)
774 .filter(text -> text.startsWith(WHITESPACE + SINCE_VERSION))
775 .findFirst();
776
777 if (sinceVersionLine.isPresent()) {
778 final String sinceVersionText = sinceVersionLine.get();
779 final int sinceVersionIndex = sinceVersionText.indexOf('.') - 1;
780
781 if (sinceVersionIndex > 0) {
782 specifiedVersion = Optional.of(sinceVersionText.substring(sinceVersionIndex));
783 }
784 else {
785 throw new MacroExecutionException(sinceVersionText
786 + " has no valid version, at least one '.' is expected.");
787 }
788 }
789 }
790 else {
791 throw new MacroExecutionException("Property '" + propertyName
792 + "' is not found in module's javadoc.");
793 }
794
795 return specifiedVersion;
796 }
797
798
799
800
801
802
803
804
805 public static Optional<DetailNode> getPropertyJavadocNodeInModule(String propertyName,
806 DetailNode moduleJavadoc) {
807 final List<DetailNode> htmlElementNodes = getNodesOfSpecificType(
808 moduleJavadoc.getChildren(), JavadocTokenTypes.HTML_ELEMENT);
809
810 final List<DetailNode> ulTags = htmlElementNodes.stream()
811 .map(JavadocUtil::getFirstChild)
812 .filter(child -> {
813 final boolean isHtmlTag = child.getType() == JavadocTokenTypes.HTML_TAG;
814 final DetailNode htmlTagNameNode = JavadocUtil.findFirstToken(
815 JavadocUtil.getFirstChild(child), JavadocTokenTypes.HTML_TAG_NAME);
816
817 return isHtmlTag && "ul".equals(htmlTagNameNode.getText());
818 })
819 .toList();
820 final DetailNode[] childrenOfUlTags = ulTags.stream()
821 .flatMap(ulTag -> Arrays.stream(ulTag.getChildren()))
822 .toArray(DetailNode[]::new);
823 final List<DetailNode> innerHtmlElementsOfUlTags =
824 getNodesOfSpecificType(childrenOfUlTags, JavadocTokenTypes.HTML_ELEMENT);
825
826 final List<DetailNode> liTags = innerHtmlElementsOfUlTags.stream()
827 .map(JavadocUtil::getFirstChild)
828 .filter(tag -> tag.getType() == JavadocTokenTypes.LI)
829 .toList();
830
831 final List<DetailNode> liTagsInlineTexts = liTags.stream()
832 .map(liTag -> JavadocUtil.findFirstToken(liTag, JavadocTokenTypes.JAVADOC_INLINE_TAG))
833 .filter(Objects::nonNull)
834 .map(inlineTag -> JavadocUtil.findFirstToken(inlineTag, JavadocTokenTypes.TEXT))
835 .toList();
836
837 return liTagsInlineTexts.stream()
838 .filter(text -> text.getText().equals(propertyName))
839 .map(textNode -> textNode.getParent().getParent())
840 .findFirst();
841
842 }
843
844
845
846
847
848
849
850
851 public static List<DetailNode> getNodesOfSpecificType(DetailNode[] allNodes, int neededType) {
852 return Arrays.stream(allNodes)
853 .filter(child -> child.getType() == neededType)
854 .toList();
855 }
856
857
858
859
860
861
862
863 @Nullable
864 private static String getSinceVersionFromJavadoc(DetailNode javadoc) {
865 final DetailNode sinceJavadocTag = getSinceJavadocTag(javadoc);
866 return Optional.ofNullable(sinceJavadocTag)
867 .map(tag -> JavadocUtil.findFirstToken(tag, JavadocTokenTypes.DESCRIPTION))
868 .map(description -> JavadocUtil.findFirstToken(description, JavadocTokenTypes.TEXT))
869 .map(DetailNode::getText)
870 .orElse(null);
871 }
872
873
874
875
876
877
878
879 private static DetailNode getSinceJavadocTag(DetailNode javadoc) {
880 final DetailNode[] children = javadoc.getChildren();
881 DetailNode javadocTagWithSince = null;
882 for (final DetailNode child : children) {
883 if (child.getType() == JavadocTokenTypes.JAVADOC_TAG) {
884 final DetailNode sinceNode = JavadocUtil.findFirstToken(
885 child, JavadocTokenTypes.SINCE_LITERAL);
886 if (sinceNode != null) {
887 javadocTagWithSince = child;
888 break;
889 }
890 }
891 }
892 return javadocTagWithSince;
893 }
894
895
896
897
898
899
900
901
902
903 private static boolean isVersionAtLeast(String actualVersion,
904 String requiredVersion) {
905 final Version actualVersionParsed = Version.parse(actualVersion);
906 final Version requiredVersionParsed = Version.parse(requiredVersion);
907
908 return actualVersionParsed.compareTo(requiredVersionParsed) >= 0;
909 }
910
911
912
913
914
915
916
917
918
919
920
921 public static String getType(Field field, String propertyName,
922 String moduleName, Object instance)
923 throws MacroExecutionException {
924 final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, instance);
925 return Optional.ofNullable(field)
926 .map(nonNullField -> nonNullField.getAnnotation(XdocsPropertyType.class))
927 .map(propertyType -> propertyType.value().getDescription())
928 .orElseGet(fieldClass::getSimpleName);
929 }
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945 public static String getDefaultValue(String propertyName, Field field,
946 Object classInstance, String moduleName)
947 throws MacroExecutionException {
948 final Object value = getFieldValue(field, classInstance);
949 final Class<?> fieldClass = getFieldClass(field, propertyName, moduleName, classInstance);
950 String result = null;
951 if (CHARSET.equals(propertyName)) {
952 result = "the charset property of the parent"
953 + " <a href=\"https://checkstyle.org/config.html#Checker\">Checker</a> module";
954 }
955 else if (classInstance instanceof PropertyCacheFile) {
956 result = "null (no cache file)";
957 }
958 else if (fieldClass == boolean.class) {
959 result = value.toString();
960 }
961 else if (fieldClass == int.class) {
962 result = value.toString();
963 }
964 else if (fieldClass == int[].class) {
965 result = getIntArrayPropertyValue(value);
966 }
967 else if (fieldClass == double[].class) {
968 result = removeSquareBrackets(Arrays.toString((double[]) value).replace(".0", ""));
969 if (result.isEmpty()) {
970 result = CURLY_BRACKETS;
971 }
972 }
973 else if (fieldClass == String[].class) {
974 result = getStringArrayPropertyValue(propertyName, value);
975 }
976 else if (fieldClass == URI.class || fieldClass == String.class) {
977 if (value != null) {
978 result = '"' + value.toString() + '"';
979 }
980 }
981 else if (fieldClass == Pattern.class) {
982 if (value != null) {
983 result = '"' + value.toString().replace("\n", "\\n").replace("\t", "\\t")
984 .replace("\r", "\\r").replace("\f", "\\f") + '"';
985 }
986 }
987 else if (fieldClass == Pattern[].class) {
988 result = getPatternArrayPropertyValue(value);
989 }
990 else if (fieldClass.isEnum()) {
991 if (value != null) {
992 result = value.toString().toLowerCase(Locale.ENGLISH);
993 }
994 }
995 else if (fieldClass == AccessModifierOption[].class) {
996 result = removeSquareBrackets(Arrays.toString((Object[]) value));
997 }
998 else {
999 final String message = String.format(Locale.ROOT,
1000 "Unknown property type: %s", fieldClass.getSimpleName());
1001 throw new MacroExecutionException(message);
1002 }
1003
1004 if (result == null) {
1005 result = "null";
1006 }
1007
1008 return result;
1009 }
1010
1011
1012
1013
1014
1015
1016
1017 private static String getPatternArrayPropertyValue(Object fieldValue) {
1018 Object value = fieldValue;
1019 if (value instanceof Collection<?> collection) {
1020
1021 value = collection.stream()
1022 .map(Pattern.class::cast)
1023 .toArray(Pattern[]::new);
1024 }
1025
1026 String result = "";
1027 if (value != null && Array.getLength(value) > 0) {
1028 result = removeSquareBrackets(
1029 Arrays.stream((Pattern[]) value)
1030 .map(Pattern::pattern)
1031 .collect(Collectors.joining(COMMA_SPACE)));
1032 }
1033
1034 if (result.isEmpty()) {
1035 result = CURLY_BRACKETS;
1036 }
1037 return result;
1038 }
1039
1040
1041
1042
1043
1044
1045
1046 private static String removeSquareBrackets(String value) {
1047 return value
1048 .replace("[", "")
1049 .replace("]", "");
1050 }
1051
1052
1053
1054
1055
1056
1057
1058
1059 private static String getStringArrayPropertyValue(String propertyName, Object value) {
1060 String result;
1061 if (value == null) {
1062 result = "";
1063 }
1064 else {
1065 try (Stream<?> valuesStream = getValuesStream(value)) {
1066 result = valuesStream
1067 .map(String.class::cast)
1068 .sorted()
1069 .collect(Collectors.joining(COMMA_SPACE));
1070 }
1071 }
1072
1073 if (result.isEmpty()) {
1074 if (FILE_EXTENSIONS.equals(propertyName)) {
1075 result = "all files";
1076 }
1077 else {
1078 result = CURLY_BRACKETS;
1079 }
1080 }
1081 return result;
1082 }
1083
1084
1085
1086
1087
1088
1089
1090 private static Stream<?> getValuesStream(Object value) {
1091 final Stream<?> valuesStream;
1092 if (value instanceof Collection<?> collection) {
1093 valuesStream = collection.stream();
1094 }
1095 else {
1096 final Object[] array = (Object[]) value;
1097 valuesStream = Arrays.stream(array);
1098 }
1099 return valuesStream;
1100 }
1101
1102
1103
1104
1105
1106
1107
1108 private static String getIntArrayPropertyValue(Object value) {
1109 try (IntStream stream = getIntStream(value)) {
1110 String result = stream
1111 .mapToObj(TokenUtil::getTokenName)
1112 .sorted()
1113 .collect(Collectors.joining(COMMA_SPACE));
1114 if (result.isEmpty()) {
1115 result = CURLY_BRACKETS;
1116 }
1117 return result;
1118 }
1119 }
1120
1121
1122
1123
1124
1125
1126
1127 private static IntStream getIntStream(Object value) {
1128 final IntStream stream;
1129 if (value instanceof Collection<?> collection) {
1130 stream = collection.stream()
1131 .mapToInt(int.class::cast);
1132 }
1133 else if (value instanceof BitSet) {
1134 stream = ((BitSet) value).stream();
1135 }
1136 else {
1137 stream = Arrays.stream((int[]) value);
1138 }
1139 return stream;
1140 }
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153 private static Class<?> getFieldClass(Field field, String propertyName,
1154 String moduleName, Object instance)
1155 throws MacroExecutionException {
1156 Class<?> result = null;
1157
1158 if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD
1159 .contains(moduleName + DOT + propertyName)) {
1160 result = getPropertyClass(propertyName, instance);
1161 }
1162 if (field != null && result == null) {
1163 result = field.getType();
1164 }
1165 if (result == null) {
1166 throw new MacroExecutionException(
1167 "Could not find field " + propertyName + " in class " + moduleName);
1168 }
1169 if (field != null && (result == List.class || result == Set.class)) {
1170 final ParameterizedType type = (ParameterizedType) field.getGenericType();
1171 final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
1172
1173 if (parameterClass == Integer.class) {
1174 result = int[].class;
1175 }
1176 else if (parameterClass == String.class) {
1177 result = String[].class;
1178 }
1179 else if (parameterClass == Pattern.class) {
1180 result = Pattern[].class;
1181 }
1182 else {
1183 final String message = "Unknown parameterized type: "
1184 + parameterClass.getSimpleName();
1185 throw new MacroExecutionException(message);
1186 }
1187 }
1188 else if (result == BitSet.class) {
1189 result = int[].class;
1190 }
1191
1192 return result;
1193 }
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204 public static Class<?> getPropertyClass(String propertyName, Object instance)
1205 throws MacroExecutionException {
1206 final Class<?> result;
1207 try {
1208 final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
1209 propertyName);
1210 result = descriptor.getPropertyType();
1211 }
1212 catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exc) {
1213 throw new MacroExecutionException(exc.getMessage(), exc);
1214 }
1215 return result;
1216 }
1217
1218
1219
1220
1221
1222
1223
1224
1225 public static List<Integer> getDifference(int[] tokens, int... subtractions) {
1226 final Set<Integer> subtractionsSet = Arrays.stream(subtractions)
1227 .boxed()
1228 .collect(Collectors.toUnmodifiableSet());
1229 return Arrays.stream(tokens)
1230 .boxed()
1231 .filter(token -> !subtractionsSet.contains(token))
1232 .toList();
1233 }
1234
1235
1236
1237
1238
1239
1240
1241
1242 public static Field getField(Class<?> fieldClass, String propertyName) {
1243 Field result = null;
1244 Class<?> currentClass = fieldClass;
1245
1246 while (!Object.class.equals(currentClass)) {
1247 try {
1248 result = currentClass.getDeclaredField(propertyName);
1249 result.trySetAccessible();
1250 break;
1251 }
1252 catch (NoSuchFieldException ignored) {
1253 currentClass = currentClass.getSuperclass();
1254 }
1255 }
1256
1257 return result;
1258 }
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268 public static String getLinkToDocument(String moduleName, String document)
1269 throws MacroExecutionException {
1270 final Path templatePath = getTemplatePath(moduleName.replace("Check", ""));
1271 if (templatePath == null) {
1272 throw new MacroExecutionException(
1273 String.format(Locale.ROOT,
1274 "Could not find template for %s", moduleName));
1275 }
1276 final Path templatePathParent = templatePath.getParent();
1277 if (templatePathParent == null) {
1278 throw new MacroExecutionException("Failed to get parent path for " + templatePath);
1279 }
1280 return templatePathParent
1281 .relativize(Path.of(SRC, "site/xdoc", document))
1282 .toString()
1283 .replace(".xml", ".html")
1284 .replace('\\', '/');
1285 }
1286
1287
1288 private static final class DescriptionExtractor {
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303 private static String getDescriptionFromJavadoc(DetailNode javadoc, String moduleName)
1304 throws MacroExecutionException {
1305 boolean isInCodeLiteral = false;
1306 boolean isInHtmlElement = false;
1307 boolean isInHrefAttribute = false;
1308 final StringBuilder description = new StringBuilder(128);
1309 final Deque<DetailNode> queue = new ArrayDeque<>();
1310 final List<DetailNode> descriptionNodes = getDescriptionNodes(javadoc);
1311 Lists.reverse(descriptionNodes).forEach(queue::push);
1312
1313
1314 while (!queue.isEmpty()) {
1315 final DetailNode node = queue.pop();
1316 Lists.reverse(Arrays.asList(node.getChildren())).forEach(queue::push);
1317
1318 if (node.getType() == JavadocTokenTypes.HTML_TAG_NAME
1319 && "href".equals(node.getText())) {
1320 isInHrefAttribute = true;
1321 }
1322 if (isInHrefAttribute && node.getType() == JavadocTokenTypes.ATTR_VALUE) {
1323 final String href = node.getText();
1324 if (href.contains(CHECKSTYLE_ORG_URL)) {
1325 handleInternalLink(description, moduleName, href);
1326 }
1327 else {
1328 description.append(href);
1329 }
1330
1331 isInHrefAttribute = false;
1332 continue;
1333 }
1334 if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
1335 isInHtmlElement = true;
1336 }
1337 if (node.getType() == JavadocTokenTypes.END
1338 && node.getParent().getType() == JavadocTokenTypes.HTML_ELEMENT_END) {
1339 description.append(node.getText());
1340 isInHtmlElement = false;
1341 }
1342 if (node.getType() == JavadocTokenTypes.TEXT
1343
1344 || isInHtmlElement && node.getChildren().length == 0
1345
1346 && node.getType() != JavadocTokenTypes.LEADING_ASTERISK) {
1347 description.append(node.getText());
1348 }
1349 if (node.getType() == JavadocTokenTypes.CODE_LITERAL) {
1350 isInCodeLiteral = true;
1351 description.append("<code>");
1352 }
1353 if (isInCodeLiteral
1354 && node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG_END) {
1355 isInCodeLiteral = false;
1356 description.append("</code>");
1357 }
1358 }
1359 return description.toString().trim();
1360 }
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371 private static void handleInternalLink(StringBuilder description,
1372 String moduleName, String value)
1373 throws MacroExecutionException {
1374 String href = value;
1375 href = href.replace(CHECKSTYLE_ORG_URL, "");
1376
1377 href = href.substring(1, href.length() - 1);
1378
1379 final String relativeHref = getLinkToDocument(moduleName, href);
1380 final char doubleQuote = '\"';
1381 description.append(doubleQuote).append(relativeHref).append(doubleQuote);
1382 }
1383
1384
1385
1386
1387
1388
1389
1390 private static List<DetailNode> getDescriptionNodes(DetailNode javadoc) {
1391 final DetailNode[] children = javadoc.getChildren();
1392 final List<DetailNode> descriptionNodes = new ArrayList<>();
1393 for (final DetailNode child : children) {
1394 if (isEndOfDescription(child)) {
1395 break;
1396 }
1397 descriptionNodes.add(child);
1398 }
1399 return descriptionNodes;
1400 }
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411 private static boolean isEndOfDescription(DetailNode child) {
1412 final DetailNode nextSibling = JavadocUtil.getNextSibling(child);
1413 final DetailNode secondNextSibling = JavadocUtil.getNextSibling(nextSibling);
1414 final DetailNode thirdNextSibling = JavadocUtil.getNextSibling(secondNextSibling);
1415
1416 return child.getType() == JavadocTokenTypes.NEWLINE
1417 && nextSibling.getType() == JavadocTokenTypes.LEADING_ASTERISK
1418 && secondNextSibling.getType() == JavadocTokenTypes.NEWLINE
1419 && thirdNextSibling.getType() == JavadocTokenTypes.LEADING_ASTERISK;
1420 }
1421 }
1422 }