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