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 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 final boolean preserveOrder = hasPreserveOrderAnnotation(field);
951 result = getStringArrayPropertyValue(value, preserveOrder);
952 }
953 else if (fieldClass == Pattern.class) {
954 if (value != null) {
955 result = value.toString().replace("\n", "\\n").replace("\t", "\\t")
956 .replace("\r", "\\r").replace("\f", "\\f");
957 }
958 }
959 else if (fieldClass == Pattern[].class) {
960 result = getPatternArrayPropertyValue(value);
961 }
962 else if (fieldClass.isEnum()) {
963 if (value != null) {
964 result = value.toString().toLowerCase(Locale.ENGLISH);
965 }
966 }
967 else if (fieldClass == AccessModifierOption[].class) {
968 result = removeSquareBrackets(Arrays.toString((Object[]) value));
969 }
970
971 if (result == null) {
972 result = "null";
973 }
974
975 return result;
976 }
977
978
979
980
981
982
983
984 private static boolean hasPreserveOrderAnnotation(Field field) {
985 return field != null && field.isAnnotationPresent(PreserveOrder.class);
986 }
987
988
989
990
991
992
993
994 private static String getPatternArrayPropertyValue(Object fieldValue) {
995 Object value = fieldValue;
996 if (value instanceof Collection<?> collection) {
997 value = collection.stream()
998 .map(Pattern.class::cast)
999 .toArray(Pattern[]::new);
1000 }
1001
1002 String result = "";
1003 if (value != null && Array.getLength(value) > 0) {
1004 result = removeSquareBrackets(
1005 Arrays.stream((Pattern[]) value)
1006 .map(Pattern::pattern)
1007 .collect(Collectors.joining(COMMA_SPACE)));
1008 }
1009
1010 return result;
1011 }
1012
1013
1014
1015
1016
1017
1018
1019 private static String removeSquareBrackets(String value) {
1020 return value
1021 .replace("[", "")
1022 .replace("]", "");
1023 }
1024
1025
1026
1027
1028
1029
1030
1031
1032 private static String getStringArrayPropertyValue(Object value, boolean preserveOrder) {
1033 final String result;
1034 if (value == null) {
1035 result = "";
1036 }
1037 else {
1038 try (Stream<?> valuesStream = getValuesStream(value)) {
1039 final List<String> stringList = valuesStream
1040 .map(String.class::cast)
1041 .collect(Collectors.toCollection(ArrayList<String>::new));
1042
1043 if (preserveOrder) {
1044 result = String.join(COMMA_SPACE, stringList);
1045 }
1046 else {
1047 result = stringList.stream()
1048 .sorted()
1049 .collect(Collectors.joining(COMMA_SPACE));
1050 }
1051 }
1052 }
1053 return result;
1054 }
1055
1056
1057
1058
1059
1060
1061
1062 private static Stream<?> getValuesStream(Object value) {
1063 final Stream<?> valuesStream;
1064 if (value instanceof Collection<?> collection) {
1065 valuesStream = collection.stream();
1066 }
1067 else {
1068 final Object[] array = (Object[]) value;
1069 valuesStream = Arrays.stream(array);
1070 }
1071 return valuesStream;
1072 }
1073
1074
1075
1076
1077
1078
1079
1080 private static String getIntArrayPropertyValue(Object value) {
1081 try (IntStream stream = getIntStream(value)) {
1082 return stream
1083 .mapToObj(TokenUtil::getTokenName)
1084 .sorted()
1085 .collect(Collectors.joining(COMMA_SPACE));
1086 }
1087 }
1088
1089
1090
1091
1092
1093
1094
1095
1096 private static IntStream getIntStream(Object value) {
1097 return switch (value) {
1098 case null -> throw new IllegalArgumentException("value is null");
1099 case Collection<?> collection -> collection.stream()
1100 .mapToInt(int.class::cast);
1101 case BitSet set -> set.stream();
1102 default -> Arrays.stream((int[]) value);
1103 };
1104 }
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118 public static Class<?> getFieldClass(Field field, String propertyName,
1119 String moduleName, Object instance)
1120 throws MacroExecutionException {
1121 Class<?> result = null;
1122
1123 if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD
1124 .contains(moduleName + DOT + propertyName)) {
1125 result = getPropertyClass(propertyName, instance);
1126 }
1127 if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) {
1128 result = String[].class;
1129 }
1130 if (field != null && result == null) {
1131 result = field.getType();
1132 }
1133
1134 if (result == null) {
1135 throw new MacroExecutionException(
1136 "Could not find field " + propertyName + " in class " + moduleName);
1137 }
1138
1139 if (field != null && (result == List.class || result == Set.class)) {
1140 final ParameterizedType type = (ParameterizedType) field.getGenericType();
1141 final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
1142
1143 if (parameterClass == Integer.class) {
1144 result = int[].class;
1145 }
1146 else if (parameterClass == String.class) {
1147 result = String[].class;
1148 }
1149 else if (parameterClass == Pattern.class) {
1150 result = Pattern[].class;
1151 }
1152 else {
1153 final String message = "Unknown parameterized type: "
1154 + parameterClass.getSimpleName();
1155 throw new MacroExecutionException(message);
1156 }
1157 }
1158 else if (result == BitSet.class) {
1159 result = int[].class;
1160 }
1161
1162 return result;
1163 }
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174 public static Class<?> getPropertyClass(String propertyName, Object instance)
1175 throws MacroExecutionException {
1176 final Class<?> result;
1177 try {
1178 final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
1179 propertyName);
1180 result = descriptor.getPropertyType();
1181 }
1182 catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exc) {
1183 throw new MacroExecutionException("Failed to retrieve property type", exc);
1184 }
1185 return result;
1186 }
1187
1188
1189
1190
1191
1192
1193
1194
1195 public static List<Integer> getDifference(int[] tokens, int... subtractions) {
1196 final Set<Integer> subtractionsSet = Arrays.stream(subtractions)
1197 .boxed()
1198 .collect(Collectors.toUnmodifiableSet());
1199 return Arrays.stream(tokens)
1200 .boxed()
1201 .filter(token -> !subtractionsSet.contains(token))
1202 .toList();
1203 }
1204
1205
1206
1207
1208
1209
1210
1211
1212 public static Field getField(Class<?> fieldClass, String propertyName) {
1213 Field result = null;
1214 Class<?> currentClass = fieldClass;
1215
1216 while (!Object.class.equals(currentClass)) {
1217 try {
1218 result = currentClass.getDeclaredField(propertyName);
1219 result.trySetAccessible();
1220 break;
1221 }
1222 catch (NoSuchFieldException ignored) {
1223 currentClass = currentClass.getSuperclass();
1224 }
1225 }
1226
1227 return result;
1228 }
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238 public static String getLinkToDocument(String moduleName, String document)
1239 throws MacroExecutionException {
1240 final Path templatePath = getTemplatePath(FINAL_CHECK.matcher(moduleName).replaceAll(""));
1241 if (templatePath == null) {
1242 throw new MacroExecutionException(
1243 String.format(Locale.ROOT,
1244 "Could not find template for %s", moduleName));
1245 }
1246 final Path templatePathParent = templatePath.getParent();
1247 if (templatePathParent == null) {
1248 throw new MacroExecutionException("Failed to get parent path for " + templatePath);
1249 }
1250 return templatePathParent
1251 .relativize(Path.of(SRC, "site/xdoc", document))
1252 .toString()
1253 .replace(".xml", ".html")
1254 .replace('\\', '/');
1255 }
1256
1257
1258
1259
1260
1261
1262
1263
1264 public static List<Path> getTemplatesThatContainPropertiesMacro()
1265 throws CheckstyleException, MacroExecutionException {
1266 final List<Path> result = new ArrayList<>();
1267 final Set<Path> templatesPaths = getXdocsTemplatesFilePaths();
1268 for (Path templatePath: templatesPaths) {
1269 final String content = getFileContents(templatePath);
1270 final String propertiesMacroDefinition = "<macro name=\"properties\"";
1271 if (content.contains(propertiesMacroDefinition)) {
1272 result.add(templatePath);
1273 }
1274 }
1275 return result;
1276 }
1277
1278
1279
1280
1281
1282
1283
1284
1285 private static String getFileContents(Path pathToFile) throws CheckstyleException {
1286 final String content;
1287 try {
1288 content = Files.readString(pathToFile);
1289 }
1290 catch (IOException ioException) {
1291 final String message = String.format(Locale.ROOT, "Failed to read file: %s",
1292 pathToFile);
1293 throw new CheckstyleException(message, ioException);
1294 }
1295 return content;
1296 }
1297
1298
1299
1300
1301
1302
1303
1304 public static String getModuleName(File file) {
1305 final String fullFileName = file.getName();
1306 return CommonUtil.getFileNameWithoutExtension(fullFileName);
1307 }
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322 private static String getDescriptionFromJavadocForXdoc(DetailNode javadoc, String moduleName)
1323 throws MacroExecutionException {
1324 boolean isInCodeLiteral = false;
1325 boolean isInLiteralTag = false;
1326 boolean isInHtmlElement = false;
1327 boolean isInHrefAttribute = false;
1328 final StringBuilder description = new StringBuilder(128);
1329 final List<DetailNode> descriptionNodes = getFirstJavadocParagraphNodes(javadoc);
1330 DetailNode node = descriptionNodes.getFirst();
1331 final DetailNode endNode = descriptionNodes.getLast();
1332
1333 while (node != null) {
1334 if (node.getType() == JavadocCommentsTokenTypes.TAG_ATTR_NAME
1335 && "href".equals(node.getText())) {
1336 isInHrefAttribute = true;
1337 }
1338 if (isInHrefAttribute && node.getType()
1339 == JavadocCommentsTokenTypes.ATTRIBUTE_VALUE) {
1340 final String href = node.getText();
1341 if (href.contains(CHECKSTYLE_ORG_URL)) {
1342 DescriptionExtractor.handleInternalLink(description, moduleName, href);
1343 }
1344 else {
1345 description.append(href);
1346 }
1347
1348 isInHrefAttribute = false;
1349 }
1350 else {
1351 if (node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) {
1352 isInHtmlElement = true;
1353 }
1354 if (node.getType() == JavadocCommentsTokenTypes.TAG_CLOSE
1355 && node.getParent().getType() == JavadocCommentsTokenTypes.HTML_TAG_END) {
1356 description.append(node.getText());
1357 isInHtmlElement = false;
1358 }
1359 if (isTextContent(node, isInHtmlElement)) {
1360 if (isInCodeLiteral || isInLiteralTag) {
1361 description.append(node.getText().trim()
1362 .replace("&", "&")
1363 .replace("<", "<")
1364 .replace(">", ">"));
1365 }
1366 else {
1367 description.append(node.getText());
1368 }
1369 }
1370 if (node.getType() == JavadocCommentsTokenTypes.TAG_NAME
1371 && node.getParent().getType()
1372 == JavadocCommentsTokenTypes.CODE_INLINE_TAG) {
1373 isInCodeLiteral = true;
1374 description.append("<code>");
1375 }
1376 if (isInCodeLiteral
1377 && node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_END) {
1378 isInCodeLiteral = false;
1379 description.append("</code>");
1380 }
1381 if (node.getType() == JavadocCommentsTokenTypes.TAG_NAME
1382 && node.getParent().getType()
1383 == JavadocCommentsTokenTypes.LITERAL_INLINE_TAG) {
1384 isInLiteralTag = true;
1385 }
1386 if (isInLiteralTag
1387 && node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_END) {
1388 isInLiteralTag = false;
1389 }
1390
1391 }
1392
1393 DetailNode toVisit = node.getFirstChild();
1394 while (node != endNode && toVisit == null) {
1395 toVisit = node.getNextSibling();
1396 node = node.getParent();
1397 }
1398
1399 node = toVisit;
1400 }
1401
1402 return description.toString().trim();
1403 }
1404
1405
1406
1407
1408
1409
1410
1411
1412 private static boolean isTextContent(DetailNode node, boolean isInHtmlElement) {
1413 return node.getType() == JavadocCommentsTokenTypes.TEXT
1414 || isInHtmlElement && node.getFirstChild() == null
1415 && node.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK;
1416 }
1417
1418
1419
1420
1421
1422
1423
1424 public static String getFirstParagraphFromJavadoc(DetailNode javadoc) {
1425 final String result;
1426 final List<DetailNode> firstParagraphNodes = getFirstJavadocParagraphNodes(javadoc);
1427 if (firstParagraphNodes.isEmpty()) {
1428 result = "";
1429 }
1430 else {
1431 final DetailNode startNode = firstParagraphNodes.getFirst();
1432 final DetailNode endNode = firstParagraphNodes.getLast();
1433 result = JavadocMetadataScraperUtil.constructSubTreeText(startNode, endNode);
1434 }
1435 return result;
1436 }
1437
1438
1439
1440
1441
1442
1443
1444 public static List<DetailNode> getFirstJavadocParagraphNodes(DetailNode javadoc) {
1445 final List<DetailNode> firstParagraphNodes = new ArrayList<>();
1446
1447 for (DetailNode child = javadoc.getFirstChild();
1448 child != null; child = child.getNextSibling()) {
1449 if (isEndOfFirstJavadocParagraph(child)) {
1450 break;
1451 }
1452 firstParagraphNodes.add(child);
1453 }
1454 return firstParagraphNodes;
1455 }
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466 public static boolean isEndOfFirstJavadocParagraph(DetailNode child) {
1467 final DetailNode nextSibling = child.getNextSibling();
1468 final DetailNode secondNextSibling = nextSibling.getNextSibling();
1469 final DetailNode thirdNextSibling = secondNextSibling.getNextSibling();
1470
1471 return child.getType() == JavadocCommentsTokenTypes.NEWLINE
1472 && nextSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK
1473 && secondNextSibling.getType() == JavadocCommentsTokenTypes.NEWLINE
1474 && thirdNextSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK;
1475 }
1476
1477
1478
1479
1480
1481
1482
1483 public static String simplifyTypeName(String fullTypeName) {
1484 final int simplifiedStartIndex;
1485
1486 if (fullTypeName.contains("$")) {
1487 simplifiedStartIndex = fullTypeName.lastIndexOf('$') + 1;
1488 }
1489 else {
1490 simplifiedStartIndex = fullTypeName.lastIndexOf('.') + 1;
1491 }
1492
1493 return fullTypeName.substring(simplifiedStartIndex);
1494 }
1495
1496
1497 private static final class DescriptionExtractor {
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508 private static void handleInternalLink(StringBuilder description,
1509 String moduleName, String value)
1510 throws MacroExecutionException {
1511 String href = value;
1512 href = href.replace(CHECKSTYLE_ORG_URL, "");
1513
1514 href = href.substring(1, href.length() - 1);
1515
1516 final String relativeHref = getLinkToDocument(moduleName, href);
1517 final char doubleQuote = '\"';
1518 description.append(doubleQuote).append(relativeHref).append(doubleQuote);
1519 }
1520 }
1521 }