View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.io.File;
23  import java.lang.reflect.Field;
24  import java.util.Arrays;
25  import java.util.Collections;
26  import java.util.LinkedList;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.regex.Pattern;
32  import java.util.stream.Collectors;
33  
34  import org.apache.maven.doxia.macro.AbstractMacro;
35  import org.apache.maven.doxia.macro.Macro;
36  import org.apache.maven.doxia.macro.MacroExecutionException;
37  import org.apache.maven.doxia.macro.MacroRequest;
38  import org.apache.maven.doxia.module.xdoc.XdocSink;
39  import org.apache.maven.doxia.sink.Sink;
40  import org.codehaus.plexus.component.annotations.Component;
41  
42  import com.puppycrawl.tools.checkstyle.PropertyType;
43  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
44  import com.puppycrawl.tools.checkstyle.api.DetailNode;
45  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
46  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
47  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
48  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
49  
50  /**
51   * A macro that inserts a table of properties for the given checkstyle module.
52   */
53  @Component(role = Macro.class, hint = "properties")
54  public class PropertiesMacro extends AbstractMacro {
55  
56      /**
57       * Constant value for cases when tokens set is empty.
58       */
59      public static final String EMPTY = "empty";
60  
61      /** Set of properties not inherited from the base token configuration. */
62      public static final Set<String> NON_BASE_TOKEN_PROPERTIES = Collections.unmodifiableSet(
63              Arrays.stream(new String[] {
64                  "AtclauseOrder - target",
65                  "DescendantToken - limitedTokens",
66                  "IllegalType - memberModifiers",
67                  "MagicNumber - constantWaiverParentToken",
68                  "MultipleStringLiterals - ignoreOccurrenceContext",
69              }).collect(Collectors.toSet()));
70  
71      /** The precompiled pattern for a comma followed by a space. */
72      private static final Pattern COMMA_SPACE_PATTERN = Pattern.compile(", ");
73  
74      /** The precompiled pattern for a Check. */
75      private static final Pattern CHECK_PATTERN = Pattern.compile("Check$");
76  
77      /** The string '{}'. */
78      private static final String CURLY_BRACKET = "{}";
79  
80      /** Represents the relative path to the property types XML. */
81      private static final String PROPERTY_TYPES_XML = "property_types.xml";
82  
83      /** Represents the format string for constructing URLs with two placeholders. */
84      private static final String URL_F = "%s#%s";
85  
86      /** Reflects start of a code segment. */
87      private static final String CODE_START = "<code>";
88  
89      /** Reflects end of a code segment. */
90      private static final String CODE_END = "</code>";
91  
92      /** A newline with 10 spaces of indentation. */
93      private static final String INDENT_LEVEL_10 = SiteUtil.getNewlineAndIndentSpaces(10);
94      /** A newline with 12 spaces of indentation. */
95      private static final String INDENT_LEVEL_12 = SiteUtil.getNewlineAndIndentSpaces(12);
96      /** A newline with 14 spaces of indentation. */
97      private static final String INDENT_LEVEL_14 = SiteUtil.getNewlineAndIndentSpaces(14);
98      /** A newline with 16 spaces of indentation. */
99      private static final String INDENT_LEVEL_16 = SiteUtil.getNewlineAndIndentSpaces(16);
100     /** A newline with 18 spaces of indentation. */
101     private static final String INDENT_LEVEL_18 = SiteUtil.getNewlineAndIndentSpaces(18);
102     /** A newline with 20 spaces of indentation. */
103     private static final String INDENT_LEVEL_20 = SiteUtil.getNewlineAndIndentSpaces(20);
104 
105     /**
106      * This property is used to change the existing properties for javadoc.
107      * Tokens always present at the end of all properties.
108      */
109     private static final String TOKENS_PROPERTY = SiteUtil.TOKENS;
110 
111     /** The name of the current module being processed. */
112     private static String currentModuleName = "";
113 
114     /** The file of the current module being processed. */
115     private static File currentModuleFile = new File("");
116 
117     @Override
118     public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
119         // until https://github.com/checkstyle/checkstyle/issues/13426
120         if (!(sink instanceof XdocSink)) {
121             throw new MacroExecutionException("Expected Sink to be an XdocSink.");
122         }
123 
124         final String modulePath = (String) request.getParameter("modulePath");
125 
126         configureGlobalProperties(modulePath);
127 
128         writePropertiesTable((XdocSink) sink);
129     }
130 
131     /**
132      * Configures the global properties for the current module.
133      *
134      * @param modulePath the path of the current module processed.
135      */
136     private static void configureGlobalProperties(String modulePath) {
137         final File moduleFile = new File(modulePath);
138         currentModuleFile = moduleFile;
139         currentModuleName = CommonUtil.getFileNameWithoutExtension(moduleFile.getName());
140     }
141 
142     /**
143      * Writes the properties table for the given module. Expects that the module has been processed
144      * with the ClassAndPropertiesSettersJavadocScraper before calling this method.
145      *
146      * @param sink the sink to write to.
147      * @throws MacroExecutionException if an error occurs during writing.
148      */
149     private static void writePropertiesTable(XdocSink sink)
150             throws MacroExecutionException {
151         sink.table();
152         sink.setInsertNewline(false);
153         sink.tableRows(null, false);
154         sink.rawText(INDENT_LEVEL_12);
155         writeTableHeaderRow(sink);
156         writeTablePropertiesRows(sink);
157         sink.rawText(INDENT_LEVEL_10);
158         sink.tableRows_();
159         sink.table_();
160         sink.setInsertNewline(true);
161     }
162 
163     /**
164      * Writes the table header row with 5 columns - name, description, type, default value, since.
165      *
166      * @param sink sink to write to.
167      */
168     private static void writeTableHeaderRow(Sink sink) {
169         sink.tableRow();
170         writeTableHeaderCell(sink, "name");
171         writeTableHeaderCell(sink, "description");
172         writeTableHeaderCell(sink, "type");
173         writeTableHeaderCell(sink, "default value");
174         writeTableHeaderCell(sink, "since");
175         sink.rawText(INDENT_LEVEL_12);
176         sink.tableRow_();
177     }
178 
179     /**
180      * Writes a table header cell with the given text.
181      *
182      * @param sink sink to write to.
183      * @param text the text to write.
184      */
185     private static void writeTableHeaderCell(Sink sink, String text) {
186         sink.rawText(INDENT_LEVEL_14);
187         sink.tableHeaderCell();
188         sink.text(text);
189         sink.tableHeaderCell_();
190     }
191 
192     /**
193      * Writes the rows of the table with the 5 columns - name, description, type, default value,
194      * since. Each row corresponds to a property of the module.
195      *
196      * @param sink sink to write to.
197      * @throws MacroExecutionException if an error occurs during writing.
198      */
199     private static void writeTablePropertiesRows(Sink sink)
200             throws MacroExecutionException {
201         final Object instance = SiteUtil.getModuleInstance(currentModuleName);
202         final Class<?> clss = instance.getClass();
203 
204         final Set<String> properties = SiteUtil.getPropertiesForDocumentation(clss, instance);
205         final Map<String, DetailNode> propertiesJavadocs = SiteUtil
206                 .getPropertiesJavadocs(properties, currentModuleName, currentModuleFile);
207 
208         final List<String> orderedProperties = orderProperties(properties);
209 
210         for (String property : orderedProperties) {
211             try {
212                 final DetailNode propertyJavadoc = propertiesJavadocs.get(property);
213                 final DetailNode currentModuleJavadoc = propertiesJavadocs.get(currentModuleName);
214                 writePropertyRow(sink, property, propertyJavadoc, instance, currentModuleJavadoc);
215             }
216             // -@cs[IllegalCatch] we need to get details in wrapping exception
217             catch (Exception exc) {
218                 final String message = String.format(Locale.ROOT,
219                         "Exception while handling moduleName: %s propertyName: %s",
220                         currentModuleName, property);
221                 throw new MacroExecutionException(message, exc);
222             }
223         }
224     }
225 
226     /**
227      * Reorder properties to always have the 'tokens' property last (if present).
228      *
229      * @param properties module properties.
230      * @return Collection of ordered properties.
231      *
232      */
233     private static List<String> orderProperties(Set<String> properties) {
234 
235         final List<String> orderProperties = new LinkedList<>(properties);
236 
237         if (orderProperties.remove(TOKENS_PROPERTY)) {
238             orderProperties.add(TOKENS_PROPERTY);
239         }
240         if (orderProperties.remove(SiteUtil.JAVADOC_TOKENS)) {
241             orderProperties.add(SiteUtil.JAVADOC_TOKENS);
242         }
243         return List.copyOf(orderProperties);
244 
245     }
246 
247     /**
248      * Writes a table row with 5 columns for the given property - name, description, type,
249      * default value, since.
250      *
251      * @param sink sink to write to.
252      * @param propertyName the name of the property.
253      * @param propertyJavadoc the Javadoc of the property.
254      * @param instance the instance of the module.
255      * @param moduleJavadoc the Javadoc of the module.
256      * @throws MacroExecutionException if an error occurs during writing.
257      */
258     private static void writePropertyRow(Sink sink, String propertyName,
259                                          DetailNode propertyJavadoc, Object instance,
260                                             DetailNode moduleJavadoc)
261             throws MacroExecutionException {
262         final Field field = SiteUtil.getField(instance.getClass(), propertyName);
263 
264         sink.rawText(INDENT_LEVEL_12);
265         sink.tableRow();
266 
267         writePropertyNameCell(sink, propertyName);
268         writePropertyDescriptionCell(sink, propertyName, propertyJavadoc);
269         writePropertyTypeCell(sink, propertyName, field, instance);
270         writePropertyDefaultValueCell(sink, propertyName, field, instance);
271         writePropertySinceVersionCell(
272                 sink, propertyName, moduleJavadoc, propertyJavadoc);
273 
274         sink.rawText(INDENT_LEVEL_12);
275         sink.tableRow_();
276     }
277 
278     /**
279      * Writes a table cell with the given property name.
280      *
281      * @param sink sink to write to.
282      * @param propertyName the name of the property.
283      */
284     private static void writePropertyNameCell(Sink sink, String propertyName) {
285         sink.rawText(INDENT_LEVEL_14);
286         sink.tableCell();
287         sink.text(propertyName);
288         sink.tableCell_();
289     }
290 
291     /**
292      * Writes a table cell with the property description.
293      *
294      * @param sink sink to write to.
295      * @param propertyName the name of the property.
296      * @param propertyJavadoc the Javadoc of the property containing the description.
297      * @throws MacroExecutionException if an error occurs during retrieval of the description.
298      */
299     private static void writePropertyDescriptionCell(Sink sink, String propertyName,
300                                                      DetailNode propertyJavadoc)
301             throws MacroExecutionException {
302         sink.rawText(INDENT_LEVEL_14);
303         sink.tableCell();
304         final String description = SiteUtil
305                 .getPropertyDescription(propertyName, propertyJavadoc, currentModuleName);
306 
307         sink.rawText(description);
308         sink.tableCell_();
309     }
310 
311     /**
312      * Writes a table cell with the property type.
313      *
314      * @param sink sink to write to.
315      * @param propertyName the name of the property.
316      * @param field the field of the property.
317      * @param instance the instance of the module.
318      * @throws MacroExecutionException if link to the property_types.html file cannot be
319      *                                 constructed.
320      */
321     private static void writePropertyTypeCell(Sink sink, String propertyName,
322                                               Field field, Object instance)
323             throws MacroExecutionException {
324         sink.rawText(INDENT_LEVEL_14);
325         sink.tableCell();
326 
327         if (SiteUtil.TOKENS.equals(propertyName)) {
328             final AbstractCheck check = (AbstractCheck) instance;
329             if (check.getRequiredTokens().length == 0
330                     && Arrays.equals(check.getAcceptableTokens(), TokenUtil.getAllTokenIds())) {
331                 sink.text("set of any supported");
332                 writeLink(sink);
333             }
334             else {
335                 final List<String> configurableTokens = SiteUtil
336                         .getDifference(check.getAcceptableTokens(),
337                                 check.getRequiredTokens())
338                         .stream()
339                         .map(TokenUtil::getTokenName)
340                         .collect(Collectors.toUnmodifiableList());
341                 sink.text("subset of tokens");
342 
343                 writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_TOKEN_TYPES, true);
344             }
345         }
346         else if (SiteUtil.JAVADOC_TOKENS.equals(propertyName)) {
347             final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
348             final List<String> configurableTokens = SiteUtil
349                     .getDifference(check.getAcceptableJavadocTokens(),
350                             check.getRequiredJavadocTokens())
351                     .stream()
352                     .map(JavadocUtil::getTokenName)
353                     .collect(Collectors.toUnmodifiableList());
354             sink.text("subset of javadoc tokens");
355             writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_JAVADOC_TOKEN_TYPES, true);
356         }
357         else {
358             final String type = SiteUtil.getType(field, propertyName, currentModuleName, instance);
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(INDENT_LEVEL_16);
406         final String link =
407                 SiteUtil.getLinkToDocument(currentModuleName, SiteUtil.PATH_TO_TOKEN_TYPES);
408         sink.link(link);
409         sink.rawText(INDENT_LEVEL_20);
410         sink.text(SiteUtil.TOKENS);
411         sink.link_();
412         sink.rawText(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(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(INDENT_LEVEL_18);
442             sink.text(SiteUtil.DOT);
443             sink.rawText(INDENT_LEVEL_14);
444         }
445         else {
446             sink.rawText(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                         + "#" + tokenName;
462         sink.link(link);
463         sink.rawText(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(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                         .collect(Collectors.toUnmodifiableList());
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                     .collect(Collectors.toUnmodifiableList());
507             writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_JAVADOC_TOKEN_TYPES, true);
508         }
509         else {
510             final String defaultValue = getDefaultValue(propertyName, field, instance);
511             final String checkName = CHECK_PATTERN
512                     .matcher(instance.getClass().getSimpleName()).replaceAll("");
513 
514             final boolean isSpecialTokenProp = NON_BASE_TOKEN_PROPERTIES.stream()
515                     .anyMatch(tokenProp -> tokenProp.equals(checkName + " - " + propertyName));
516 
517             if (isSpecialTokenProp && !CURLY_BRACKET.equals(defaultValue)) {
518                 final List<String> defaultValuesList =
519                         Arrays.asList(COMMA_SPACE_PATTERN.split(defaultValue));
520                 writeTokensList(sink, defaultValuesList, SiteUtil.PATH_TO_TOKEN_TYPES, false);
521             }
522             else {
523                 sink.rawText(CODE_START);
524                 sink.text(defaultValue);
525                 sink.rawText(CODE_END);
526             }
527         }
528 
529         sink.tableCell_();
530     }
531 
532     /**
533      * Get the default value of the property.
534      *
535      * @param propertyName the name of the property.
536      * @param field the field of the property.
537      * @param instance the instance of the module.
538      * @return the default value of the property.
539      * @throws MacroExecutionException if an error occurs during retrieval of the default value.
540      */
541     private static String getDefaultValue(String propertyName, Field field, Object instance)
542             throws MacroExecutionException {
543         final String result;
544 
545         if (field != null) {
546             result = SiteUtil.getDefaultValue(
547                     propertyName, field, instance, currentModuleName);
548         }
549         else {
550             final Class<?> fieldClass = SiteUtil.getPropertyClass(propertyName, instance);
551 
552             if (fieldClass.isArray()) {
553                 result = CURLY_BRACKET;
554             }
555             else {
556                 result = "null";
557             }
558         }
559         return result;
560     }
561 
562     /**
563      * Writes a table cell with the property since version.
564      *
565      * @param sink sink to write to.
566      * @param propertyName the name of the property.
567      * @param moduleJavadoc the Javadoc of the module.
568      * @param propertyJavadoc the Javadoc of the property containing the since version.
569      * @throws MacroExecutionException if an error occurs during retrieval of the since version.
570      */
571     private static void writePropertySinceVersionCell(Sink sink, String propertyName,
572                                                       DetailNode moduleJavadoc,
573                                                       DetailNode propertyJavadoc)
574             throws MacroExecutionException {
575         sink.rawText(INDENT_LEVEL_14);
576         sink.tableCell();
577         final String sinceVersion = SiteUtil.getSinceVersion(
578                 currentModuleName, moduleJavadoc, propertyName, propertyJavadoc);
579         sink.text(sinceVersion);
580         sink.tableCell_();
581     }
582 }