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