PropertiesMacro.java
///////////////////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
// Copyright (C) 2001-2024 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
///////////////////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle.site;
import java.io.File;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.maven.doxia.macro.AbstractMacro;
import org.apache.maven.doxia.macro.Macro;
import org.apache.maven.doxia.macro.MacroExecutionException;
import org.apache.maven.doxia.macro.MacroRequest;
import org.apache.maven.doxia.module.xdoc.XdocSink;
import org.apache.maven.doxia.sink.Sink;
import org.codehaus.plexus.component.annotations.Component;
import com.puppycrawl.tools.checkstyle.PropertyType;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailNode;
import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
/**
* A macro that inserts a table of properties for the given checkstyle module.
*/
@Component(role = Macro.class, hint = "properties")
public class PropertiesMacro extends AbstractMacro {
/**
* Constant value for cases when tokens set is empty.
*/
public static final String EMPTY = "empty";
/** Set of properties not inherited from the base token configuration. */
public static final Set<String> NON_BASE_TOKEN_PROPERTIES = Collections.unmodifiableSet(
Arrays.stream(new String[] {
"AtclauseOrder - target",
"DescendantToken - limitedTokens",
"IllegalType - memberModifiers",
"MagicNumber - constantWaiverParentToken",
"MultipleStringLiterals - ignoreOccurrenceContext",
}).collect(Collectors.toSet()));
/** The precompiled pattern for a comma followed by a space. */
private static final Pattern COMMA_SPACE_PATTERN = Pattern.compile(", ");
/** The precompiled pattern for a Check. */
private static final Pattern CHECK_PATTERN = Pattern.compile("Check$");
/** The string '{}'. */
private static final String CURLY_BRACKET = "{}";
/** Represents the relative path to the property types XML. */
private static final String PROPERTY_TYPES_XML = "property_types.xml";
/** Represents the format string for constructing URLs with two placeholders. */
private static final String URL_F = "%s#%s";
/** Reflects start of a code segment. */
private static final String CODE_START = "<code>";
/** Reflects end of a code segment. */
private static final String CODE_END = "</code>";
/** A newline with 10 spaces of indentation. */
private static final String INDENT_LEVEL_10 = SiteUtil.getNewlineAndIndentSpaces(10);
/** A newline with 12 spaces of indentation. */
private static final String INDENT_LEVEL_12 = SiteUtil.getNewlineAndIndentSpaces(12);
/** A newline with 14 spaces of indentation. */
private static final String INDENT_LEVEL_14 = SiteUtil.getNewlineAndIndentSpaces(14);
/** A newline with 16 spaces of indentation. */
private static final String INDENT_LEVEL_16 = SiteUtil.getNewlineAndIndentSpaces(16);
/** A newline with 18 spaces of indentation. */
private static final String INDENT_LEVEL_18 = SiteUtil.getNewlineAndIndentSpaces(18);
/** A newline with 20 spaces of indentation. */
private static final String INDENT_LEVEL_20 = SiteUtil.getNewlineAndIndentSpaces(20);
/**
* This property is used to change the existing properties for javadoc.
* Tokens always present at the end of all properties.
*/
private static final String TOKENS_PROPERTY = SiteUtil.TOKENS;
/** The name of the current module being processed. */
private static String currentModuleName = "";
/** The file of the current module being processed. */
private static File currentModuleFile = new File("");
@Override
public void execute(Sink sink, MacroRequest request) throws MacroExecutionException {
// until https://github.com/checkstyle/checkstyle/issues/13426
if (!(sink instanceof XdocSink)) {
throw new MacroExecutionException("Expected Sink to be an XdocSink.");
}
final String modulePath = (String) request.getParameter("modulePath");
configureGlobalProperties(modulePath);
writePropertiesTable((XdocSink) sink);
}
/**
* Configures the global properties for the current module.
*
* @param modulePath the path of the current module processed.
*/
private static void configureGlobalProperties(String modulePath) {
final File moduleFile = new File(modulePath);
currentModuleFile = moduleFile;
currentModuleName = CommonUtil.getFileNameWithoutExtension(moduleFile.getName());
}
/**
* Writes the properties table for the given module. Expects that the module has been processed
* with the ClassAndPropertiesSettersJavadocScraper before calling this method.
*
* @param sink the sink to write to.
* @throws MacroExecutionException if an error occurs during writing.
*/
private static void writePropertiesTable(XdocSink sink)
throws MacroExecutionException {
sink.table();
sink.setInsertNewline(false);
sink.tableRows(null, false);
sink.rawText(INDENT_LEVEL_12);
writeTableHeaderRow(sink);
writeTablePropertiesRows(sink);
sink.rawText(INDENT_LEVEL_10);
sink.tableRows_();
sink.table_();
sink.setInsertNewline(true);
}
/**
* Writes the table header row with 5 columns - name, description, type, default value, since.
*
* @param sink sink to write to.
*/
private static void writeTableHeaderRow(Sink sink) {
sink.tableRow();
writeTableHeaderCell(sink, "name");
writeTableHeaderCell(sink, "description");
writeTableHeaderCell(sink, "type");
writeTableHeaderCell(sink, "default value");
writeTableHeaderCell(sink, "since");
sink.rawText(INDENT_LEVEL_12);
sink.tableRow_();
}
/**
* Writes a table header cell with the given text.
*
* @param sink sink to write to.
* @param text the text to write.
*/
private static void writeTableHeaderCell(Sink sink, String text) {
sink.rawText(INDENT_LEVEL_14);
sink.tableHeaderCell();
sink.text(text);
sink.tableHeaderCell_();
}
/**
* Writes the rows of the table with the 5 columns - name, description, type, default value,
* since. Each row corresponds to a property of the module.
*
* @param sink sink to write to.
* @throws MacroExecutionException if an error occurs during writing.
*/
private static void writeTablePropertiesRows(Sink sink)
throws MacroExecutionException {
final Object instance = SiteUtil.getModuleInstance(currentModuleName);
final Class<?> clss = instance.getClass();
final Set<String> properties = SiteUtil.getPropertiesForDocumentation(clss, instance);
final Map<String, DetailNode> propertiesJavadocs = SiteUtil
.getPropertiesJavadocs(properties, currentModuleName, currentModuleFile);
final List<String> orderedProperties = orderProperties(properties);
for (String property : orderedProperties) {
try {
final DetailNode propertyJavadoc = propertiesJavadocs.get(property);
final DetailNode currentModuleJavadoc = propertiesJavadocs.get(currentModuleName);
writePropertyRow(sink, property, propertyJavadoc, instance, currentModuleJavadoc);
}
// -@cs[IllegalCatch] we need to get details in wrapping exception
catch (Exception exc) {
final String message = String.format(Locale.ROOT,
"Exception while handling moduleName: %s propertyName: %s",
currentModuleName, property);
throw new MacroExecutionException(message, exc);
}
}
}
/**
* Reorder properties to always have the 'tokens' property last (if present).
*
* @param properties module properties.
* @return Collection of ordered properties.
*
*/
private static List<String> orderProperties(Set<String> properties) {
final List<String> orderProperties = new LinkedList<>(properties);
if (orderProperties.remove(TOKENS_PROPERTY)) {
orderProperties.add(TOKENS_PROPERTY);
}
if (orderProperties.remove(SiteUtil.JAVADOC_TOKENS)) {
orderProperties.add(SiteUtil.JAVADOC_TOKENS);
}
return List.copyOf(orderProperties);
}
/**
* Writes a table row with 5 columns for the given property - name, description, type,
* default value, since.
*
* @param sink sink to write to.
* @param propertyName the name of the property.
* @param propertyJavadoc the Javadoc of the property.
* @param instance the instance of the module.
* @param moduleJavadoc the Javadoc of the module.
* @throws MacroExecutionException if an error occurs during writing.
*/
private static void writePropertyRow(Sink sink, String propertyName,
DetailNode propertyJavadoc, Object instance,
DetailNode moduleJavadoc)
throws MacroExecutionException {
final Field field = SiteUtil.getField(instance.getClass(), propertyName);
sink.rawText(INDENT_LEVEL_12);
sink.tableRow();
writePropertyNameCell(sink, propertyName);
writePropertyDescriptionCell(sink, propertyName, propertyJavadoc);
writePropertyTypeCell(sink, propertyName, field, instance);
writePropertyDefaultValueCell(sink, propertyName, field, instance);
writePropertySinceVersionCell(
sink, propertyName, moduleJavadoc, propertyJavadoc);
sink.rawText(INDENT_LEVEL_12);
sink.tableRow_();
}
/**
* Writes a table cell with the given property name.
*
* @param sink sink to write to.
* @param propertyName the name of the property.
*/
private static void writePropertyNameCell(Sink sink, String propertyName) {
sink.rawText(INDENT_LEVEL_14);
sink.tableCell();
sink.text(propertyName);
sink.tableCell_();
}
/**
* Writes a table cell with the property description.
*
* @param sink sink to write to.
* @param propertyName the name of the property.
* @param propertyJavadoc the Javadoc of the property containing the description.
* @throws MacroExecutionException if an error occurs during retrieval of the description.
*/
private static void writePropertyDescriptionCell(Sink sink, String propertyName,
DetailNode propertyJavadoc)
throws MacroExecutionException {
sink.rawText(INDENT_LEVEL_14);
sink.tableCell();
final String description = SiteUtil
.getPropertyDescription(propertyName, propertyJavadoc, currentModuleName);
sink.rawText(description);
sink.tableCell_();
}
/**
* Writes a table cell with the property type.
*
* @param sink sink to write to.
* @param propertyName the name of the property.
* @param field the field of the property.
* @param instance the instance of the module.
* @throws MacroExecutionException if link to the property_types.html file cannot be
* constructed.
*/
private static void writePropertyTypeCell(Sink sink, String propertyName,
Field field, Object instance)
throws MacroExecutionException {
sink.rawText(INDENT_LEVEL_14);
sink.tableCell();
if (SiteUtil.TOKENS.equals(propertyName)) {
final AbstractCheck check = (AbstractCheck) instance;
if (check.getRequiredTokens().length == 0
&& Arrays.equals(check.getAcceptableTokens(), TokenUtil.getAllTokenIds())) {
sink.text("set of any supported");
writeLink(sink);
}
else {
final List<String> configurableTokens = SiteUtil
.getDifference(check.getAcceptableTokens(),
check.getRequiredTokens())
.stream()
.map(TokenUtil::getTokenName)
.collect(Collectors.toUnmodifiableList());
sink.text("subset of tokens");
writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_TOKEN_TYPES, true);
}
}
else if (SiteUtil.JAVADOC_TOKENS.equals(propertyName)) {
final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
final List<String> configurableTokens = SiteUtil
.getDifference(check.getAcceptableJavadocTokens(),
check.getRequiredJavadocTokens())
.stream()
.map(JavadocUtil::getTokenName)
.collect(Collectors.toUnmodifiableList());
sink.text("subset of javadoc tokens");
writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_JAVADOC_TOKEN_TYPES, true);
}
else {
final String type = SiteUtil.getType(field, propertyName, currentModuleName, instance);
if (PropertyType.TOKEN_ARRAY.getDescription().equals(type)) {
processLinkForTokenTypes(sink);
}
else {
final String relativePathToPropertyTypes =
SiteUtil.getLinkToDocument(currentModuleName, PROPERTY_TYPES_XML);
final String escapedType = type
.replace("[", ".5B")
.replace("]", ".5D");
final String url =
String.format(Locale.ROOT, URL_F, relativePathToPropertyTypes, escapedType);
sink.link(url);
sink.text(type);
sink.link_();
}
}
sink.tableCell_();
}
/**
* Writes a formatted link for "TokenTypes" to the given sink.
*
* @param sink The output target where the link is written.
* @throws MacroExecutionException If an error occurs during the link processing.
*/
private static void processLinkForTokenTypes(Sink sink)
throws MacroExecutionException {
final String link =
SiteUtil.getLinkToDocument(currentModuleName, SiteUtil.PATH_TO_TOKEN_TYPES);
sink.text("subset of tokens ");
sink.link(link);
sink.text("TokenTypes");
sink.link_();
}
/**
* Write a link when all types of token supported.
*
* @param sink sink to write to.
* @throws MacroExecutionException if link cannot be constructed.
*/
private static void writeLink(Sink sink)
throws MacroExecutionException {
sink.rawText(INDENT_LEVEL_16);
final String link =
SiteUtil.getLinkToDocument(currentModuleName, SiteUtil.PATH_TO_TOKEN_TYPES);
sink.link(link);
sink.rawText(INDENT_LEVEL_20);
sink.text(SiteUtil.TOKENS);
sink.link_();
sink.rawText(INDENT_LEVEL_14);
}
/**
* Write a list of tokens with links to the tokenTypesLink file.
*
* @param sink sink to write to.
* @param tokens the list of tokens to write.
* @param tokenTypesLink the link to the token types file.
* @param printDotAtTheEnd defines if printing period symbols is required.
* @throws MacroExecutionException if link to the tokenTypesLink file cannot be constructed.
*/
private static void writeTokensList(Sink sink, List<String> tokens, String tokenTypesLink,
boolean printDotAtTheEnd)
throws MacroExecutionException {
for (int index = 0; index < tokens.size(); index++) {
final String token = tokens.get(index);
sink.rawText(INDENT_LEVEL_16);
if (index != 0) {
sink.text(SiteUtil.COMMA_SPACE);
}
writeLinkToToken(sink, tokenTypesLink, token);
}
if (tokens.isEmpty()) {
sink.rawText(CODE_START);
sink.text(EMPTY);
sink.rawText(CODE_END);
}
else if (printDotAtTheEnd) {
sink.rawText(INDENT_LEVEL_18);
sink.text(SiteUtil.DOT);
sink.rawText(INDENT_LEVEL_14);
}
else {
sink.rawText(INDENT_LEVEL_14);
}
}
/**
* Writes a link to the given token.
*
* @param sink sink to write to.
* @param document the document to link to.
* @param tokenName the name of the token.
* @throws MacroExecutionException if link to the document file cannot be constructed.
*/
private static void writeLinkToToken(Sink sink, String document, String tokenName)
throws MacroExecutionException {
final String link = SiteUtil.getLinkToDocument(currentModuleName, document)
+ "#" + tokenName;
sink.link(link);
sink.rawText(INDENT_LEVEL_20);
sink.text(tokenName);
sink.link_();
}
/**
* Writes a table cell with the property default value.
*
* @param sink sink to write to.
* @param propertyName the name of the property.
* @param field the field of the property.
* @param instance the instance of the module.
* @throws MacroExecutionException if an error occurs during retrieval of the default value.
*/
private static void writePropertyDefaultValueCell(Sink sink, String propertyName,
Field field, Object instance)
throws MacroExecutionException {
sink.rawText(INDENT_LEVEL_14);
sink.tableCell();
if (SiteUtil.TOKENS.equals(propertyName)) {
final AbstractCheck check = (AbstractCheck) instance;
if (check.getRequiredTokens().length == 0
&& Arrays.equals(check.getDefaultTokens(), TokenUtil.getAllTokenIds())) {
sink.text(SiteUtil.TOKEN_TYPES);
}
else {
final List<String> configurableTokens = SiteUtil
.getDifference(check.getDefaultTokens(),
check.getRequiredTokens())
.stream()
.map(TokenUtil::getTokenName)
.collect(Collectors.toUnmodifiableList());
writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_TOKEN_TYPES, true);
}
}
else if (SiteUtil.JAVADOC_TOKENS.equals(propertyName)) {
final AbstractJavadocCheck check = (AbstractJavadocCheck) instance;
final List<String> configurableTokens = SiteUtil
.getDifference(check.getDefaultJavadocTokens(),
check.getRequiredJavadocTokens())
.stream()
.map(JavadocUtil::getTokenName)
.collect(Collectors.toUnmodifiableList());
writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_JAVADOC_TOKEN_TYPES, true);
}
else {
final String defaultValue = getDefaultValue(propertyName, field, instance);
final String checkName = CHECK_PATTERN
.matcher(instance.getClass().getSimpleName()).replaceAll("");
final boolean isSpecialTokenProp = NON_BASE_TOKEN_PROPERTIES.stream()
.anyMatch(tokenProp -> tokenProp.equals(checkName + " - " + propertyName));
if (isSpecialTokenProp && !CURLY_BRACKET.equals(defaultValue)) {
final List<String> defaultValuesList =
Arrays.asList(COMMA_SPACE_PATTERN.split(defaultValue));
writeTokensList(sink, defaultValuesList, SiteUtil.PATH_TO_TOKEN_TYPES, false);
}
else {
sink.rawText(CODE_START);
sink.text(defaultValue);
sink.rawText(CODE_END);
}
}
sink.tableCell_();
}
/**
* Get the default value of the property.
*
* @param propertyName the name of the property.
* @param field the field of the property.
* @param instance the instance of the module.
* @return the default value of the property.
* @throws MacroExecutionException if an error occurs during retrieval of the default value.
*/
private static String getDefaultValue(String propertyName, Field field, Object instance)
throws MacroExecutionException {
final String result;
if (field != null) {
result = SiteUtil.getDefaultValue(
propertyName, field, instance, currentModuleName);
}
else {
final Class<?> fieldClass = SiteUtil.getPropertyClass(propertyName, instance);
if (fieldClass.isArray()) {
result = CURLY_BRACKET;
}
else {
result = "null";
}
}
return result;
}
/**
* Writes a table cell with the property since version.
*
* @param sink sink to write to.
* @param propertyName the name of the property.
* @param moduleJavadoc the Javadoc of the module.
* @param propertyJavadoc the Javadoc of the property containing the since version.
* @throws MacroExecutionException if an error occurs during retrieval of the since version.
*/
private static void writePropertySinceVersionCell(Sink sink, String propertyName,
DetailNode moduleJavadoc,
DetailNode propertyJavadoc)
throws MacroExecutionException {
sink.rawText(INDENT_LEVEL_14);
sink.tableCell();
final String sinceVersion = SiteUtil.getSinceVersion(
currentModuleName, moduleJavadoc, propertyName, propertyJavadoc);
sink.text(sinceVersion);
sink.tableCell_();
}
}