View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.site;
21  
22  import java.lang.reflect.Field;
23  import java.nio.file.Path;
24  import java.nio.file.Paths;
25  import java.util.Arrays;
26  import java.util.Collections;
27  import java.util.LinkedList;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.Map;
31  import java.util.Set;
32  import java.util.regex.Pattern;
33  import java.util.stream.Collectors;
34  
35  import org.apache.maven.doxia.macro.AbstractMacro;
36  import org.apache.maven.doxia.macro.Macro;
37  import org.apache.maven.doxia.macro.MacroExecutionException;
38  import org.apache.maven.doxia.macro.MacroRequest;
39  import org.apache.maven.doxia.module.xdoc.XdocSink;
40  import org.apache.maven.doxia.sink.Sink;
41  import org.codehaus.plexus.component.annotations.Component;
42  
43  import com.puppycrawl.tools.checkstyle.PropertyType;
44  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
45  import com.puppycrawl.tools.checkstyle.api.DetailNode;
46  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
47  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
48  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
49  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
50  
51  /**
52   * A macro that inserts a table of properties for the given checkstyle module.
53   */
54  @Component(role = Macro.class, hint = "properties")
55  public class PropertiesMacro extends AbstractMacro {
56  
57      /**
58       * Constant value for cases when tokens set is empty.
59       */
60      public static final String EMPTY = "empty";
61  
62      /** Set of properties not inherited from the base token configuration. */
63      public static final Set<String> NON_BASE_TOKEN_PROPERTIES = Collections.unmodifiableSet(
64              Arrays.stream(new String[] {
65                  "AtclauseOrder - target",
66                  "DescendantToken - limitedTokens",
67                  "IllegalType - memberModifiers",
68                  "MagicNumber - constantWaiverParentToken",
69                  "MultipleStringLiterals - ignoreOccurrenceContext",
70              }).collect(Collectors.toSet()));
71  
72      /** The precompiled pattern for a comma followed by a space. */
73      private static final Pattern COMMA_SPACE_PATTERN = Pattern.compile(", ");
74  
75      /** The precompiled pattern for a Check. */
76      private static final Pattern CHECK_PATTERN = Pattern.compile("Check$");
77  
78      /** The string '{}'. */
79      private static final String CURLY_BRACKET = "{}";
80  
81      /** Represents the relative path to the property types XML. */
82      private static final String PROPERTY_TYPES_XML = "property_types.xml";
83  
84      /** Represents the format string for constructing URLs with two placeholders. */
85      private static final String URL_F = "%s#%s";
86  
87      /** Reflects start of a code segment. */
88      private static final String CODE_START = "<code>";
89  
90      /** Reflects end of a code segment. */
91      private static final String CODE_END = "</code>";
92  
93      /**
94       * This property is used to change the existing properties for javadoc.
95       * Tokens always present at the end of all properties.
96       */
97      private static final String TOKENS_PROPERTY = SiteUtil.TOKENS;
98  
99      /** The name of the current module being processed. */
100     private static String currentModuleName = "";
101 
102     /** The file of the current module being processed. */
103     private static Path currentModulePath = Paths.get("");
104 
105     @Override
106     public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
107         // until https://github.com/checkstyle/checkstyle/issues/13426
108         if (!(sink instanceof XdocSink)) {
109             throw new MacroExecutionException("Expected Sink to be an XdocSink.");
110         }
111 
112         final String modulePath = (String) request.getParameter("modulePath");
113 
114         configureGlobalProperties(modulePath);
115 
116         writePropertiesTable((XdocSink) sink);
117     }
118 
119     /**
120      * Configures the global properties for the current module.
121      *
122      * @param modulePath the path of the current module processed.
123      * @throws MacroExecutionException if the module path is invalid.
124      */
125     private static void configureGlobalProperties(String modulePath)
126             throws MacroExecutionException {
127         final Path modulePathObj = Paths.get(modulePath);
128         currentModulePath = modulePathObj;
129         final Path fileNamePath = modulePathObj.getFileName();
130 
131         if (fileNamePath == null) {
132             throw new MacroExecutionException(
133                 "Invalid modulePath '" + modulePath + "': No file name present.");
134         }
135 
136         currentModuleName = CommonUtil.getFileNameWithoutExtension(
137             fileNamePath.toString());
138     }
139 
140     /**
141      * Writes the properties table for the given module. Expects that the module has been processed
142      * with the ClassAndPropertiesSettersJavadocScraper before calling this method.
143      *
144      * @param sink the sink to write to.
145      * @throws MacroExecutionException if an error occurs during writing.
146      */
147     private static void writePropertiesTable(XdocSink sink)
148             throws MacroExecutionException {
149         sink.table();
150         sink.setInsertNewline(false);
151         sink.tableRows(null, false);
152         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_12);
153         writeTableHeaderRow(sink);
154         writeTablePropertiesRows(sink);
155         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_10);
156         sink.tableRows_();
157         sink.table_();
158         sink.setInsertNewline(true);
159     }
160 
161     /**
162      * Writes the table header row with 5 columns - name, description, type, default value, since.
163      *
164      * @param sink sink to write to.
165      */
166     private static void writeTableHeaderRow(Sink sink) {
167         sink.tableRow();
168         writeTableHeaderCell(sink, "name");
169         writeTableHeaderCell(sink, "description");
170         writeTableHeaderCell(sink, "type");
171         writeTableHeaderCell(sink, "default value");
172         writeTableHeaderCell(sink, "since");
173         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_12);
174         sink.tableRow_();
175     }
176 
177     /**
178      * Writes a table header cell with the given text.
179      *
180      * @param sink sink to write to.
181      * @param text the text to write.
182      */
183     private static void writeTableHeaderCell(Sink sink, String text) {
184         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14);
185         sink.tableHeaderCell();
186         sink.text(text);
187         sink.tableHeaderCell_();
188     }
189 
190     /**
191      * Writes the rows of the table with the 5 columns - name, description, type, default value,
192      * since. Each row corresponds to a property of the module.
193      *
194      * @param sink sink to write to.
195      * @throws MacroExecutionException if an error occurs during writing.
196      */
197     private static void writeTablePropertiesRows(Sink sink)
198             throws MacroExecutionException {
199         final Object instance = SiteUtil.getModuleInstance(currentModuleName);
200         final Class<?> clss = instance.getClass();
201 
202         final Set<String> properties = SiteUtil.getPropertiesForDocumentation(clss, instance);
203         final Map<String, DetailNode> propertiesJavadocs = SiteUtil
204                 .getPropertiesJavadocs(properties, currentModuleName, currentModulePath);
205 
206         final List<String> orderedProperties = orderProperties(properties);
207 
208         for (String property : orderedProperties) {
209             try {
210                 final DetailNode propertyJavadoc = propertiesJavadocs.get(property);
211                 final DetailNode currentModuleJavadoc =
212                     SiteUtil.getModuleJavadoc(currentModuleName, currentModulePath);
213                 writePropertyRow(sink, property, propertyJavadoc, instance, currentModuleJavadoc);
214             }
215             // -@cs[IllegalCatch] we need to get details in wrapping exception
216             catch (Exception exc) {
217                 final String message = String.format(Locale.ROOT,
218                         "Exception while handling moduleName: %s propertyName: %s",
219                         currentModuleName, property);
220                 throw new MacroExecutionException(message, exc);
221             }
222         }
223     }
224 
225     /**
226      * Reorder properties to always have the 'tokens' property last (if present).
227      *
228      * @param properties module properties.
229      * @return Collection of ordered properties.
230      *
231      */
232     private static List<String> orderProperties(Set<String> properties) {
233 
234         final List<String> orderProperties = new LinkedList<>(properties);
235 
236         if (orderProperties.remove(TOKENS_PROPERTY)) {
237             orderProperties.add(TOKENS_PROPERTY);
238         }
239         if (orderProperties.remove(SiteUtil.JAVADOC_TOKENS)) {
240             orderProperties.add(SiteUtil.JAVADOC_TOKENS);
241         }
242         return List.copyOf(orderProperties);
243 
244     }
245 
246     /**
247      * Writes a table row with 5 columns for the given property - name, description, type,
248      * default value, since.
249      *
250      * @param sink sink to write to.
251      * @param propertyName the name of the property.
252      * @param propertyJavadoc the Javadoc of the property.
253      * @param instance the instance of the module.
254      * @param moduleJavadoc the Javadoc of the module.
255      * @throws MacroExecutionException if an error occurs during writing.
256      */
257     private static void writePropertyRow(Sink sink, String propertyName,
258                                          DetailNode propertyJavadoc, Object instance,
259                                             DetailNode moduleJavadoc)
260             throws MacroExecutionException {
261         final Field field = SiteUtil.getField(instance.getClass(), propertyName);
262 
263         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_12);
264         sink.tableRow();
265 
266         writePropertyNameCell(sink, propertyName);
267         writePropertyDescriptionCell(sink, propertyName, propertyJavadoc);
268         writePropertyTypeCell(sink, propertyName, field, instance);
269         writePropertyDefaultValueCell(sink, propertyName, field, instance);
270         writePropertySinceVersionCell(
271                 sink, propertyName, moduleJavadoc, propertyJavadoc);
272 
273         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_12);
274         sink.tableRow_();
275     }
276 
277     /**
278      * Writes a table cell with the given property name.
279      *
280      * @param sink sink to write to.
281      * @param propertyName the name of the property.
282      */
283     private static void writePropertyNameCell(Sink sink, String propertyName) {
284         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14);
285         sink.tableCell();
286         sink.text(propertyName);
287         sink.tableCell_();
288     }
289 
290     /**
291      * Writes a table cell with the property description.
292      *
293      * @param sink sink to write to.
294      * @param propertyName the name of the property.
295      * @param propertyJavadoc the Javadoc of the property containing the description.
296      * @throws MacroExecutionException if an error occurs during retrieval of the description.
297      */
298     private static void writePropertyDescriptionCell(Sink sink, String propertyName,
299                                                      DetailNode propertyJavadoc)
300             throws MacroExecutionException {
301         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14);
302         sink.tableCell();
303         final String description = SiteUtil
304                 .getPropertyDescription(propertyName, propertyJavadoc, currentModuleName);
305 
306         sink.rawText(description);
307         sink.tableCell_();
308     }
309 
310     /**
311      * Writes a table cell with the property type.
312      *
313      * @param sink sink to write to.
314      * @param propertyName the name of the property.
315      * @param field the field of the property.
316      * @param instance the instance of the module.
317      * @throws MacroExecutionException if link to the property_types.html file cannot be
318      *                                 constructed.
319      */
320     private static void writePropertyTypeCell(Sink sink, String propertyName,
321                                               Field field, Object instance)
322             throws MacroExecutionException {
323         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14);
324         sink.tableCell();
325 
326         if (SiteUtil.TOKENS.equals(propertyName)) {
327             final AbstractCheck check = (AbstractCheck) instance;
328             if (check.getRequiredTokens().length == 0
329                     && Arrays.equals(check.getAcceptableTokens(), TokenUtil.getAllTokenIds())) {
330                 sink.text("set of any supported");
331                 writeLink(sink);
332             }
333             else {
334                 final List<String> configurableTokens = SiteUtil
335                         .getDifference(check.getAcceptableTokens(),
336                                 check.getRequiredTokens())
337                         .stream()
338                         .map(TokenUtil::getTokenName)
339                         .toList();
340                 sink.text("subset of tokens");
341 
342                 writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_TOKEN_TYPES, true);
343             }
344         }
345         else if (SiteUtil.JAVADOC_TOKENS.equals(propertyName)) {
346             final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
347             final List<String> configurableTokens = SiteUtil
348                     .getDifference(check.getAcceptableJavadocTokens(),
349                             check.getRequiredJavadocTokens())
350                     .stream()
351                     .map(JavadocUtil::getTokenName)
352                     .toList();
353             sink.text("subset of javadoc tokens");
354             writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_JAVADOC_TOKEN_TYPES, true);
355         }
356         else {
357             final String type = SiteUtil.getType(field, propertyName, currentModuleName, instance);
358             if (PropertyType.TOKEN_ARRAY.getDescription().equals(type)) {
359                 processLinkForTokenTypes(sink);
360             }
361             else {
362                 final String relativePathToPropertyTypes =
363                         SiteUtil.getLinkToDocument(currentModuleName, PROPERTY_TYPES_XML);
364                 final String escapedType = type
365                         .replace("[", ".5B")
366                         .replace("]", ".5D");
367 
368                 final String url =
369                         String.format(Locale.ROOT, URL_F, relativePathToPropertyTypes, escapedType);
370 
371                 sink.link(url);
372                 sink.text(type);
373                 sink.link_();
374             }
375         }
376         sink.tableCell_();
377     }
378 
379     /**
380      * Writes a formatted link for "TokenTypes" to the given sink.
381      *
382      * @param sink The output target where the link is written.
383      * @throws MacroExecutionException If an error occurs during the link processing.
384      */
385     private static void processLinkForTokenTypes(Sink sink)
386             throws MacroExecutionException {
387         final String link =
388                 SiteUtil.getLinkToDocument(currentModuleName, SiteUtil.PATH_TO_TOKEN_TYPES);
389 
390         sink.text("subset of tokens ");
391         sink.link(link);
392         sink.text("TokenTypes");
393         sink.link_();
394     }
395 
396     /**
397      * Write a link when all types of token supported.
398      *
399      * @param sink sink to write to.
400      * @throws MacroExecutionException if link cannot be constructed.
401      */
402     private static void writeLink(Sink sink)
403             throws MacroExecutionException {
404         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_16);
405         final String link =
406                 SiteUtil.getLinkToDocument(currentModuleName, SiteUtil.PATH_TO_TOKEN_TYPES);
407         sink.link(link);
408         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_20);
409         sink.text(SiteUtil.TOKENS);
410         sink.link_();
411         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14);
412     }
413 
414     /**
415      * Write a list of tokens with links to the tokenTypesLink file.
416      *
417      * @param sink sink to write to.
418      * @param tokens the list of tokens to write.
419      * @param tokenTypesLink the link to the token types file.
420      * @param printDotAtTheEnd defines if printing period symbols is required.
421      * @throws MacroExecutionException if link to the tokenTypesLink file cannot be constructed.
422      */
423     private static void writeTokensList(Sink sink, List<String> tokens, String tokenTypesLink,
424                                         boolean printDotAtTheEnd)
425             throws MacroExecutionException {
426         for (int index = 0; index < tokens.size(); index++) {
427             final String token = tokens.get(index);
428             sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_16);
429             if (index != 0) {
430                 sink.text(SiteUtil.COMMA_SPACE);
431             }
432             writeLinkToToken(sink, tokenTypesLink, token);
433         }
434         if (tokens.isEmpty()) {
435             sink.rawText(CODE_START);
436             sink.text(EMPTY);
437             sink.rawText(CODE_END);
438         }
439         else if (printDotAtTheEnd) {
440             sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_18);
441             sink.text(SiteUtil.DOT);
442             sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14);
443         }
444         else {
445             sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14);
446         }
447     }
448 
449     /**
450      * Writes a link to the given token.
451      *
452      * @param sink sink to write to.
453      * @param document the document to link to.
454      * @param tokenName the name of the token.
455      * @throws MacroExecutionException if link to the document file cannot be constructed.
456      */
457     private static void writeLinkToToken(Sink sink, String document, String tokenName)
458             throws MacroExecutionException {
459         final String link = SiteUtil.getLinkToDocument(currentModuleName, document)
460                         + "#" + tokenName;
461         sink.link(link);
462         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_20);
463         sink.text(tokenName);
464         sink.link_();
465     }
466 
467     /**
468      * Writes a table cell with the property default value.
469      *
470      * @param sink sink to write to.
471      * @param propertyName the name of the property.
472      * @param field the field of the property.
473      * @param instance the instance of the module.
474      * @throws MacroExecutionException if an error occurs during retrieval of the default value.
475      */
476     private static void writePropertyDefaultValueCell(Sink sink, String propertyName,
477                                                       Field field, Object instance)
478             throws MacroExecutionException {
479         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14);
480         sink.tableCell();
481 
482         if (SiteUtil.TOKENS.equals(propertyName)) {
483             final AbstractCheck check = (AbstractCheck) instance;
484             if (check.getRequiredTokens().length == 0
485                     && Arrays.equals(check.getDefaultTokens(), TokenUtil.getAllTokenIds())) {
486                 sink.text(SiteUtil.TOKEN_TYPES);
487             }
488             else {
489                 final List<String> configurableTokens = SiteUtil
490                         .getDifference(check.getDefaultTokens(),
491                                 check.getRequiredTokens())
492                         .stream()
493                         .map(TokenUtil::getTokenName)
494                         .toList();
495                 writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_TOKEN_TYPES, true);
496             }
497         }
498         else if (SiteUtil.JAVADOC_TOKENS.equals(propertyName)) {
499             final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
500             final List<String> configurableTokens = SiteUtil
501                     .getDifference(check.getDefaultJavadocTokens(),
502                             check.getRequiredJavadocTokens())
503                     .stream()
504                     .map(JavadocUtil::getTokenName)
505                     .toList();
506             writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_JAVADOC_TOKEN_TYPES, true);
507         }
508         else {
509             final String defaultValue = getDefaultValue(propertyName, field, instance);
510             final String checkName = CHECK_PATTERN
511                     .matcher(instance.getClass().getSimpleName()).replaceAll("");
512 
513             final boolean isSpecialTokenProp = NON_BASE_TOKEN_PROPERTIES.stream()
514                     .anyMatch(tokenProp -> tokenProp.equals(checkName + " - " + propertyName));
515 
516             if (isSpecialTokenProp && !CURLY_BRACKET.equals(defaultValue)) {
517                 final List<String> defaultValuesList =
518                         Arrays.asList(COMMA_SPACE_PATTERN.split(defaultValue));
519                 writeTokensList(sink, defaultValuesList, SiteUtil.PATH_TO_TOKEN_TYPES, false);
520             }
521             else {
522                 sink.rawText(CODE_START);
523                 sink.text(defaultValue);
524                 sink.rawText(CODE_END);
525             }
526         }
527 
528         sink.tableCell_();
529     }
530 
531     /**
532      * Get the default value of the property.
533      *
534      * @param propertyName the name of the property.
535      * @param field the field of the property.
536      * @param instance the instance of the module.
537      * @return the default value of the property.
538      * @throws MacroExecutionException if an error occurs during retrieval of the default value.
539      */
540     private static String getDefaultValue(String propertyName, Field field, Object instance)
541             throws MacroExecutionException {
542         final String result;
543 
544         if (field != null) {
545             result = SiteUtil.getDefaultValue(
546                     propertyName, field, instance, currentModuleName);
547         }
548         else {
549             final Class<?> fieldClass = SiteUtil.getPropertyClass(propertyName, instance);
550 
551             if (fieldClass.isArray()) {
552                 result = CURLY_BRACKET;
553             }
554             else {
555                 result = "null";
556             }
557         }
558         return result;
559     }
560 
561     /**
562      * Writes a table cell with the property since version.
563      *
564      * @param sink sink to write to.
565      * @param propertyName the name of the property.
566      * @param moduleJavadoc the Javadoc of the module.
567      * @param propertyJavadoc the Javadoc of the property containing the since version.
568      * @throws MacroExecutionException if an error occurs during retrieval of the since version.
569      */
570     private static void writePropertySinceVersionCell(Sink sink, String propertyName,
571                                                       DetailNode moduleJavadoc,
572                                                       DetailNode propertyJavadoc)
573             throws MacroExecutionException {
574         sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14);
575         sink.tableCell();
576         final String sinceVersion = SiteUtil.getPropertySinceVersion(
577                 currentModuleName, moduleJavadoc, propertyName, propertyJavadoc);
578         sink.text(sinceVersion);
579         sink.tableCell_();
580     }
581 }