001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.site;
021
022import java.lang.reflect.Field;
023import java.nio.file.Path;
024import java.nio.file.Paths;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Locale;
030import java.util.Map;
031import java.util.Set;
032import java.util.regex.Pattern;
033import java.util.stream.Collectors;
034
035import org.apache.maven.doxia.macro.AbstractMacro;
036import org.apache.maven.doxia.macro.Macro;
037import org.apache.maven.doxia.macro.MacroExecutionException;
038import org.apache.maven.doxia.macro.MacroRequest;
039import org.apache.maven.doxia.module.xdoc.XdocSink;
040import org.apache.maven.doxia.sink.Sink;
041import org.codehaus.plexus.component.annotations.Component;
042
043import com.puppycrawl.tools.checkstyle.PropertyType;
044import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
045import com.puppycrawl.tools.checkstyle.api.DetailNode;
046import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
047import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
048import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
049import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
050
051/**
052 * A macro that inserts a table of properties for the given checkstyle module.
053 */
054@Component(role = Macro.class, hint = "properties")
055public class PropertiesMacro extends AbstractMacro {
056
057    /**
058     * Constant value for cases when tokens set is empty.
059     */
060    public static final String EMPTY = "empty";
061
062    /** Set of properties not inherited from the base token configuration. */
063    public static final Set<String> NON_BASE_TOKEN_PROPERTIES = Collections.unmodifiableSet(
064            Arrays.stream(new String[] {
065                "AtclauseOrder - target",
066                "DescendantToken - limitedTokens",
067                "IllegalType - memberModifiers",
068                "MagicNumber - constantWaiverParentToken",
069                "MultipleStringLiterals - ignoreOccurrenceContext",
070            }).collect(Collectors.toSet()));
071
072    /** The precompiled pattern for a comma followed by a space. */
073    private static final Pattern COMMA_SPACE_PATTERN = Pattern.compile(", ");
074
075    /** The precompiled pattern for a Check. */
076    private static final Pattern CHECK_PATTERN = Pattern.compile("Check$");
077
078    /** The string '{}'. */
079    private static final String CURLY_BRACKET = "{}";
080
081    /** Represents the relative path to the property types XML. */
082    private static final String PROPERTY_TYPES_XML = "property_types.xml";
083
084    /** Represents the format string for constructing URLs with two placeholders. */
085    private static final String URL_F = "%s#%s";
086
087    /** Reflects start of a code segment. */
088    private static final String CODE_START = "<code>";
089
090    /** Reflects end of a code segment. */
091    private static final String CODE_END = "</code>";
092
093    /**
094     * This property is used to change the existing properties for javadoc.
095     * Tokens always present at the end of all properties.
096     */
097    private static final String TOKENS_PROPERTY = SiteUtil.TOKENS;
098
099    /** 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}