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