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
1097
1098 private static IntStream getIntStream(Object value) {
1099 final IntStream stream;
1100 if (value instanceof Collection<?> collection) {
1101 stream = collection.stream()
1102 .mapToInt(int.class::cast);
1103 }
1104 else if (value instanceof BitSet set) {
1105 stream = set.stream();
1106 }
1107 else {
1108 stream = Arrays.stream((int[]) value);
1109 }
1110 return stream;
1111 }
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125 public static Class<?> getFieldClass(Field field, String propertyName,
1126 String moduleName, Object instance)
1127 throws MacroExecutionException {
1128 Class<?> result = null;
1129
1130 if (PROPERTIES_ALLOWED_GET_TYPES_FROM_METHOD
1131 .contains(moduleName + DOT + propertyName)) {
1132 result = getPropertyClass(propertyName, instance);
1133 }
1134 if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) {
1135 result = String[].class;
1136 }
1137 if (field != null && result == null) {
1138 result = field.getType();
1139 }
1140
1141 if (result == null) {
1142 throw new MacroExecutionException(
1143 "Could not find field " + propertyName + " in class " + moduleName);
1144 }
1145
1146 if (field != null && (result == List.class || result == Set.class)) {
1147 final ParameterizedType type = (ParameterizedType) field.getGenericType();
1148 final Class<?> parameterClass = (Class<?>) type.getActualTypeArguments()[0];
1149
1150 if (parameterClass == Integer.class) {
1151 result = int[].class;
1152 }
1153 else if (parameterClass == String.class) {
1154 result = String[].class;
1155 }
1156 else if (parameterClass == Pattern.class) {
1157 result = Pattern[].class;
1158 }
1159 else {
1160 final String message = "Unknown parameterized type: "
1161 + parameterClass.getSimpleName();
1162 throw new MacroExecutionException(message);
1163 }
1164 }
1165 else if (result == BitSet.class) {
1166 result = int[].class;
1167 }
1168
1169 return result;
1170 }
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181 public static Class<?> getPropertyClass(String propertyName, Object instance)
1182 throws MacroExecutionException {
1183 final Class<?> result;
1184 try {
1185 final PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(instance,
1186 propertyName);
1187 result = descriptor.getPropertyType();
1188 }
1189 catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exc) {
1190 throw new MacroExecutionException("Failed to retrieve property type", exc);
1191 }
1192 return result;
1193 }
1194
1195
1196
1197
1198
1199
1200
1201
1202 public static List<Integer> getDifference(int[] tokens, int... subtractions) {
1203 final Set<Integer> subtractionsSet = Arrays.stream(subtractions)
1204 .boxed()
1205 .collect(Collectors.toUnmodifiableSet());
1206 return Arrays.stream(tokens)
1207 .boxed()
1208 .filter(token -> !subtractionsSet.contains(token))
1209 .toList();
1210 }
1211
1212
1213
1214
1215
1216
1217
1218
1219 public static Field getField(Class<?> fieldClass, String propertyName) {
1220 Field result = null;
1221 Class<?> currentClass = fieldClass;
1222
1223 while (!Object.class.equals(currentClass)) {
1224 try {
1225 result = currentClass.getDeclaredField(propertyName);
1226 result.trySetAccessible();
1227 break;
1228 }
1229 catch (NoSuchFieldException ignored) {
1230 currentClass = currentClass.getSuperclass();
1231 }
1232 }
1233
1234 return result;
1235 }
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245 public static String getLinkToDocument(String moduleName, String document)
1246 throws MacroExecutionException {
1247 final Path templatePath = getTemplatePath(FINAL_CHECK.matcher(moduleName).replaceAll(""));
1248 if (templatePath == null) {
1249 throw new MacroExecutionException(
1250 String.format(Locale.ROOT,
1251 "Could not find template for %s", moduleName));
1252 }
1253 final Path templatePathParent = templatePath.getParent();
1254 if (templatePathParent == null) {
1255 throw new MacroExecutionException("Failed to get parent path for " + templatePath);
1256 }
1257 return templatePathParent
1258 .relativize(Path.of(SRC, "site/xdoc", document))
1259 .toString()
1260 .replace(".xml", ".html")
1261 .replace('\\', '/');
1262 }
1263
1264
1265
1266
1267
1268
1269
1270
1271 public static List<Path> getTemplatesThatContainPropertiesMacro()
1272 throws CheckstyleException, MacroExecutionException {
1273 final List<Path> result = new ArrayList<>();
1274 final Set<Path> templatesPaths = getXdocsTemplatesFilePaths();
1275 for (Path templatePath: templatesPaths) {
1276 final String content = getFileContents(templatePath);
1277 final String propertiesMacroDefinition = "<macro name=\"properties\"";
1278 if (content.contains(propertiesMacroDefinition)) {
1279 result.add(templatePath);
1280 }
1281 }
1282 return result;
1283 }
1284
1285
1286
1287
1288
1289
1290
1291
1292 private static String getFileContents(Path pathToFile) throws CheckstyleException {
1293 final String content;
1294 try {
1295 content = Files.readString(pathToFile);
1296 }
1297 catch (IOException ioException) {
1298 final String message = String.format(Locale.ROOT, "Failed to read file: %s",
1299 pathToFile);
1300 throw new CheckstyleException(message, ioException);
1301 }
1302 return content;
1303 }
1304
1305
1306
1307
1308
1309
1310
1311 public static String getModuleName(File file) {
1312 final String fullFileName = file.getName();
1313 return CommonUtil.getFileNameWithoutExtension(fullFileName);
1314 }
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329 private static String getDescriptionFromJavadocForXdoc(DetailNode javadoc, String moduleName)
1330 throws MacroExecutionException {
1331 boolean isInCodeLiteral = false;
1332 boolean isInHtmlElement = false;
1333 boolean isInHrefAttribute = false;
1334 final StringBuilder description = new StringBuilder(128);
1335 final List<DetailNode> descriptionNodes = getFirstJavadocParagraphNodes(javadoc);
1336 DetailNode node = descriptionNodes.getFirst();
1337 final DetailNode endNode = descriptionNodes.getLast();
1338
1339 while (node != null) {
1340 if (node.getType() == JavadocCommentsTokenTypes.TAG_ATTR_NAME
1341 && "href".equals(node.getText())) {
1342 isInHrefAttribute = true;
1343 }
1344 if (isInHrefAttribute && node.getType()
1345 == JavadocCommentsTokenTypes.ATTRIBUTE_VALUE) {
1346 final String href = node.getText();
1347 if (href.contains(CHECKSTYLE_ORG_URL)) {
1348 DescriptionExtractor.handleInternalLink(description, moduleName, href);
1349 }
1350 else {
1351 description.append(href);
1352 }
1353
1354 isInHrefAttribute = false;
1355 }
1356 else {
1357 if (node.getType() == JavadocCommentsTokenTypes.HTML_ELEMENT) {
1358 isInHtmlElement = true;
1359 }
1360 if (node.getType() == JavadocCommentsTokenTypes.TAG_CLOSE
1361 && node.getParent().getType() == JavadocCommentsTokenTypes.HTML_TAG_END) {
1362 description.append(node.getText());
1363 isInHtmlElement = false;
1364 }
1365 if (node.getType() == JavadocCommentsTokenTypes.TEXT
1366
1367 || isInHtmlElement && node.getFirstChild() == null
1368
1369 && node.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK) {
1370 if (isInCodeLiteral) {
1371 description.append(node.getText().trim());
1372 }
1373 else {
1374 description.append(node.getText());
1375 }
1376 }
1377 if (node.getType() == JavadocCommentsTokenTypes.TAG_NAME
1378 && node.getParent().getType()
1379 == JavadocCommentsTokenTypes.CODE_INLINE_TAG) {
1380 isInCodeLiteral = true;
1381 description.append("<code>");
1382 }
1383 if (isInCodeLiteral
1384 && node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_END) {
1385 isInCodeLiteral = false;
1386 description.append("</code>");
1387 }
1388
1389 }
1390
1391 DetailNode toVisit = node.getFirstChild();
1392 while (node != endNode && toVisit == null) {
1393 toVisit = node.getNextSibling();
1394 node = node.getParent();
1395 }
1396
1397 node = toVisit;
1398 }
1399
1400 return description.toString().trim();
1401 }
1402
1403
1404
1405
1406
1407
1408
1409 public static String getFirstParagraphFromJavadoc(DetailNode javadoc) {
1410 final String result;
1411 final List<DetailNode> firstParagraphNodes = getFirstJavadocParagraphNodes(javadoc);
1412 if (firstParagraphNodes.isEmpty()) {
1413 result = "";
1414 }
1415 else {
1416 final DetailNode startNode = firstParagraphNodes.getFirst();
1417 final DetailNode endNode = firstParagraphNodes.getLast();
1418 result = JavadocMetadataScraperUtil.constructSubTreeText(startNode, endNode);
1419 }
1420 return result;
1421 }
1422
1423
1424
1425
1426
1427
1428
1429 public static List<DetailNode> getFirstJavadocParagraphNodes(DetailNode javadoc) {
1430 final List<DetailNode> firstParagraphNodes = new ArrayList<>();
1431
1432 for (DetailNode child = javadoc.getFirstChild();
1433 child != null; child = child.getNextSibling()) {
1434 if (isEndOfFirstJavadocParagraph(child)) {
1435 break;
1436 }
1437 firstParagraphNodes.add(child);
1438 }
1439 return firstParagraphNodes;
1440 }
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451 public static boolean isEndOfFirstJavadocParagraph(DetailNode child) {
1452 final DetailNode nextSibling = child.getNextSibling();
1453 final DetailNode secondNextSibling = nextSibling.getNextSibling();
1454 final DetailNode thirdNextSibling = secondNextSibling.getNextSibling();
1455
1456 return child.getType() == JavadocCommentsTokenTypes.NEWLINE
1457 && nextSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK
1458 && secondNextSibling.getType() == JavadocCommentsTokenTypes.NEWLINE
1459 && thirdNextSibling.getType() == JavadocCommentsTokenTypes.LEADING_ASTERISK;
1460 }
1461
1462
1463
1464
1465
1466
1467
1468 public static String simplifyTypeName(String fullTypeName) {
1469 final int simplifiedStartIndex;
1470
1471 if (fullTypeName.contains("$")) {
1472 simplifiedStartIndex = fullTypeName.lastIndexOf('$') + 1;
1473 }
1474 else {
1475 simplifiedStartIndex = fullTypeName.lastIndexOf('.') + 1;
1476 }
1477
1478 return fullTypeName.substring(simplifiedStartIndex);
1479 }
1480
1481
1482 private static final class DescriptionExtractor {
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493 private static void handleInternalLink(StringBuilder description,
1494 String moduleName, String value)
1495 throws MacroExecutionException {
1496 String href = value;
1497 href = href.replace(CHECKSTYLE_ORG_URL, "");
1498
1499 href = href.substring(1, href.length() - 1);
1500
1501 final String relativeHref = getLinkToDocument(moduleName, href);
1502 final char doubleQuote = '\"';
1503 description.append(doubleQuote).append(relativeHref).append(doubleQuote);
1504 }
1505 }
1506 }