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