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