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}