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