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.util.Arrays; 025import java.util.LinkedList; 026import java.util.List; 027import java.util.Locale; 028import java.util.Map; 029import java.util.Set; 030import java.util.regex.Pattern; 031 032import org.apache.maven.doxia.macro.AbstractMacro; 033import org.apache.maven.doxia.macro.Macro; 034import org.apache.maven.doxia.macro.MacroExecutionException; 035import org.apache.maven.doxia.macro.MacroRequest; 036import org.apache.maven.doxia.module.xdoc.XdocSink; 037import org.apache.maven.doxia.sink.Sink; 038import org.codehaus.plexus.component.annotations.Component; 039 040import com.puppycrawl.tools.checkstyle.PropertyType; 041import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 042import com.puppycrawl.tools.checkstyle.api.DetailNode; 043import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck; 044import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 045import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 046import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 047 048/** 049 * A macro that inserts a table of properties for the given checkstyle module. 050 */ 051@Component(role = Macro.class, hint = "properties") 052public class PropertiesMacro extends AbstractMacro { 053 054 /** 055 * Constant value for cases when tokens set is empty. 056 */ 057 public static final String EMPTY = "empty"; 058 059 /** The precompiled pattern for a comma followed by a space. */ 060 private static final Pattern COMMA_SPACE_PATTERN = Pattern.compile(", "); 061 062 /** The string '{}'. */ 063 private static final String CURLY_BRACKET = "{}"; 064 065 /** Represents the relative path to the property types XML. */ 066 private static final String PROPERTY_TYPES_XML = "property_types.xml"; 067 068 /** The string '#'. */ 069 private static final String HASHTAG = "#"; 070 071 /** Represents the format string for constructing URLs with two placeholders. */ 072 private static final String URL_F = "%s#%s"; 073 074 /** Reflects start of a code segment. */ 075 private static final String CODE_START = "<code>"; 076 077 /** Reflects end of a code segment. */ 078 private static final String CODE_END = "</code>"; 079 080 /** 081 * This property is used to change the existing properties for javadoc. 082 * Tokens always present at the end of all properties. 083 */ 084 private static final String TOKENS_PROPERTY = SiteUtil.TOKENS; 085 086 /** The name of the current module being processed. */ 087 private static String currentModuleName = ""; 088 089 /** The file of the current module being processed. */ 090 private static Path currentModulePath = Path.of(""); 091 092 @Override 093 public void execute(Sink sink, MacroRequest request) throws MacroExecutionException { 094 // until https://github.com/checkstyle/checkstyle/issues/13426 095 if (!(sink instanceof XdocSink)) { 096 throw new MacroExecutionException("Expected Sink to be an XdocSink."); 097 } 098 099 final String modulePath = (String) request.getParameter("modulePath"); 100 101 configureGlobalProperties(modulePath); 102 103 writePropertiesTable((XdocSink) sink); 104 } 105 106 /** 107 * Configures the global properties for the current module. 108 * 109 * @param modulePath the path of the current module processed. 110 * @throws MacroExecutionException if the module path is invalid. 111 */ 112 private static void configureGlobalProperties(String modulePath) 113 throws MacroExecutionException { 114 final Path modulePathObj = Path.of(modulePath); 115 currentModulePath = modulePathObj; 116 final Path fileNamePath = modulePathObj.getFileName(); 117 118 if (fileNamePath == null) { 119 throw new MacroExecutionException( 120 "Invalid modulePath '" + modulePath + "': No file name present."); 121 } 122 123 currentModuleName = CommonUtil.getFileNameWithoutExtension( 124 fileNamePath.toString()); 125 } 126 127 /** 128 * Writes the properties table for the given module. Expects that the module has been processed 129 * with the ClassAndPropertiesSettersJavadocScraper before calling this method. 130 * 131 * @param sink the sink to write to. 132 * @throws MacroExecutionException if an error occurs during writing. 133 */ 134 private static void writePropertiesTable(XdocSink sink) 135 throws MacroExecutionException { 136 sink.table(); 137 sink.setInsertNewline(false); 138 sink.tableRows(null, false); 139 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_12); 140 writeTableHeaderRow(sink); 141 writeTablePropertiesRows(sink); 142 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_10); 143 sink.tableRows_(); 144 sink.table_(); 145 sink.setInsertNewline(true); 146 } 147 148 /** 149 * Writes the table header row with 5 columns - name, description, type, default value, since. 150 * 151 * @param sink sink to write to. 152 */ 153 private static void writeTableHeaderRow(Sink sink) { 154 sink.tableRow(); 155 writeTableHeaderCell(sink, "name"); 156 writeTableHeaderCell(sink, "description"); 157 writeTableHeaderCell(sink, "type"); 158 writeTableHeaderCell(sink, "default value"); 159 writeTableHeaderCell(sink, "since"); 160 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_12); 161 sink.tableRow_(); 162 } 163 164 /** 165 * Writes a table header cell with the given text. 166 * 167 * @param sink sink to write to. 168 * @param text the text to write. 169 */ 170 private static void writeTableHeaderCell(Sink sink, String text) { 171 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 172 sink.tableHeaderCell(); 173 sink.text(text); 174 sink.tableHeaderCell_(); 175 } 176 177 /** 178 * Writes the rows of the table with the 5 columns - name, description, type, default value, 179 * since. Each row corresponds to a property of the module. 180 * 181 * @param sink sink to write to. 182 * @throws MacroExecutionException if an error occurs during writing. 183 */ 184 private static void writeTablePropertiesRows(Sink sink) 185 throws MacroExecutionException { 186 final Object instance = SiteUtil.getModuleInstance(currentModuleName); 187 final Class<?> clss = instance.getClass(); 188 189 final Set<String> properties = SiteUtil.getPropertiesForDocumentation(clss, instance); 190 final Map<String, DetailNode> propertiesJavadocs = SiteUtil 191 .getPropertiesJavadocs(properties, currentModuleName, currentModulePath); 192 193 final List<String> orderedProperties = orderProperties(properties); 194 195 final DetailNode currentModuleJavadoc = SiteUtil.getModuleJavadoc( 196 currentModuleName, currentModulePath); 197 198 for (String property : orderedProperties) { 199 try { 200 final DetailNode propertyJavadoc = propertiesJavadocs.get(property); 201 writePropertyRow(sink, property, propertyJavadoc, instance, currentModuleJavadoc); 202 } 203 // -@cs[IllegalCatch] we need to get details in wrapping exception 204 catch (Exception exc) { 205 final String message = String.format(Locale.ROOT, 206 "Exception while handling moduleName: %s propertyName: %s", 207 currentModuleName, property); 208 throw new MacroExecutionException(message, exc); 209 } 210 } 211 } 212 213 /** 214 * Reorder properties to always have the 'tokens' property last (if present). 215 * 216 * @param properties module properties. 217 * @return Collection of ordered properties. 218 * 219 */ 220 private static List<String> orderProperties(Set<String> properties) { 221 222 final List<String> orderProperties = new LinkedList<>(properties); 223 224 if (orderProperties.remove(TOKENS_PROPERTY)) { 225 orderProperties.add(TOKENS_PROPERTY); 226 } 227 if (orderProperties.remove(SiteUtil.JAVADOC_TOKENS)) { 228 orderProperties.add(SiteUtil.JAVADOC_TOKENS); 229 } 230 return List.copyOf(orderProperties); 231 232 } 233 234 /** 235 * Writes a table row with 5 columns for the given property - name, description, type, 236 * default value, since. 237 * 238 * @param sink sink to write to. 239 * @param propertyName the name of the property. 240 * @param propertyJavadoc the Javadoc of the property. 241 * @param instance the instance of the module. 242 * @param moduleJavadoc the Javadoc of the module. 243 * @throws MacroExecutionException if an error occurs during writing. 244 */ 245 private static void writePropertyRow(Sink sink, String propertyName, 246 DetailNode propertyJavadoc, Object instance, 247 DetailNode moduleJavadoc) 248 throws MacroExecutionException { 249 final Field field = SiteUtil.getField(instance.getClass(), propertyName); 250 251 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_12); 252 sink.tableRow(); 253 254 writePropertyNameCell(sink, propertyName); 255 writePropertyDescriptionCell(sink, propertyName, propertyJavadoc); 256 writePropertyTypeCell(sink, propertyName, field, instance); 257 writePropertyDefaultValueCell(sink, propertyName, field, instance); 258 writePropertySinceVersionCell( 259 sink, moduleJavadoc, propertyJavadoc); 260 261 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_12); 262 sink.tableRow_(); 263 } 264 265 /** 266 * Writes a table cell with the given property name. 267 * 268 * @param sink sink to write to. 269 * @param propertyName the name of the property. 270 */ 271 private static void writePropertyNameCell(Sink sink, String propertyName) { 272 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 273 sink.tableCell(); 274 sink.rawText("<a id=\"" + propertyName + "\"/>"); 275 sink.link(HASHTAG + propertyName); 276 sink.text(propertyName); 277 sink.link_(); 278 sink.tableCell_(); 279 } 280 281 /** 282 * Writes a table cell with the property description. 283 * 284 * @param sink sink to write to. 285 * @param propertyName the name of the property. 286 * @param propertyJavadoc the Javadoc of the property containing the description. 287 * @throws MacroExecutionException if an error occurs during retrieval of the description. 288 */ 289 private static void writePropertyDescriptionCell(Sink sink, String propertyName, 290 DetailNode propertyJavadoc) 291 throws MacroExecutionException { 292 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 293 sink.tableCell(); 294 final String description = SiteUtil 295 .getPropertyDescriptionForXdoc(propertyName, propertyJavadoc, currentModuleName); 296 297 sink.rawText(description); 298 sink.tableCell_(); 299 } 300 301 /** 302 * Writes a table cell with the property type. 303 * 304 * @param sink sink to write to. 305 * @param propertyName the name of the property. 306 * @param field the field of the property. 307 * @param instance the instance of the module. 308 * @throws MacroExecutionException if link to the property_types.html file cannot be 309 * constructed. 310 */ 311 private static void writePropertyTypeCell(Sink sink, String propertyName, 312 Field field, Object instance) 313 throws MacroExecutionException { 314 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 315 sink.tableCell(); 316 317 if (SiteUtil.TOKENS.equals(propertyName)) { 318 final AbstractCheck check = (AbstractCheck) instance; 319 if (check.getRequiredTokens().length == 0 320 && Arrays.equals(check.getAcceptableTokens(), TokenUtil.getAllTokenIds())) { 321 sink.text("set of any supported"); 322 writeLink(sink); 323 } 324 else { 325 final List<String> configurableTokens = SiteUtil 326 .getDifference(check.getAcceptableTokens(), 327 check.getRequiredTokens()) 328 .stream() 329 .map(TokenUtil::getTokenName) 330 .toList(); 331 sink.text("subset of tokens"); 332 333 writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_TOKEN_TYPES, true); 334 } 335 } 336 else if (SiteUtil.JAVADOC_TOKENS.equals(propertyName)) { 337 final AbstractJavadocCheck check = (AbstractJavadocCheck) instance; 338 final List<String> configurableTokens = SiteUtil 339 .getDifference(check.getAcceptableJavadocTokens(), 340 check.getRequiredJavadocTokens()) 341 .stream() 342 .map(JavadocUtil::getTokenName) 343 .toList(); 344 sink.text("subset of javadoc tokens"); 345 writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_JAVADOC_TOKEN_TYPES, true); 346 } 347 else { 348 final String type; 349 350 if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field)) { 351 type = "subset of tokens TokenTypes"; 352 } 353 else { 354 final String fullTypeName = 355 SiteUtil.getType(field, propertyName, currentModuleName, instance); 356 type = SiteUtil.simplifyTypeName(fullTypeName); 357 } 358 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(ModuleJavadocParsingUtil.INDENT_LEVEL_16); 406 final String link = 407 SiteUtil.getLinkToDocument(currentModuleName, SiteUtil.PATH_TO_TOKEN_TYPES); 408 sink.link(link); 409 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_20); 410 sink.text(SiteUtil.TOKENS); 411 sink.link_(); 412 sink.rawText(ModuleJavadocParsingUtil.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(ModuleJavadocParsingUtil.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(ModuleJavadocParsingUtil.INDENT_LEVEL_18); 442 sink.text(SiteUtil.DOT); 443 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 444 } 445 else { 446 sink.rawText(ModuleJavadocParsingUtil.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 + HASHTAG + tokenName; 462 sink.link(link); 463 sink.rawText(ModuleJavadocParsingUtil.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(ModuleJavadocParsingUtil.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 .toList(); 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 .toList(); 507 writeTokensList(sink, configurableTokens, SiteUtil.PATH_TO_JAVADOC_TOKEN_TYPES, true); 508 } 509 else { 510 final String defaultValue = getDefaultValue(propertyName, field, instance); 511 512 if (ModuleJavadocParsingUtil.isPropertySpecialTokenProp(field) 513 && !CURLY_BRACKET.equals(defaultValue)) { 514 515 final List<String> defaultValuesList = 516 Arrays.asList(COMMA_SPACE_PATTERN.split(defaultValue)); 517 writeTokensList(sink, defaultValuesList, SiteUtil.PATH_TO_TOKEN_TYPES, false); 518 } 519 else { 520 sink.rawText(CODE_START); 521 sink.text(defaultValue); 522 sink.rawText(CODE_END); 523 } 524 } 525 526 sink.tableCell_(); 527 } 528 529 /** 530 * Get the default value of the property. 531 * 532 * @param propertyName the name of the property. 533 * @param field the field of the property. 534 * @param instance the instance of the module. 535 * @return the default value of the property. 536 * @throws MacroExecutionException if an error occurs during retrieval of the default value. 537 */ 538 private static String getDefaultValue(String propertyName, Field field, Object instance) 539 throws MacroExecutionException { 540 String result; 541 542 if (field != null) { 543 result = SiteUtil.getDefaultValue( 544 propertyName, field, instance, currentModuleName); 545 } 546 else { 547 final Class<?> fieldClass = SiteUtil.getPropertyClass(propertyName, instance); 548 549 if (fieldClass.isArray()) { 550 result = CURLY_BRACKET; 551 } 552 else { 553 result = "null"; 554 } 555 } 556 557 final Class<?> fieldClass = 558 SiteUtil.getFieldClass(field, propertyName, currentModuleName, instance); 559 if (result.isEmpty() && fieldClass.isArray()) { 560 result = CURLY_BRACKET; 561 562 if (fieldClass == String[].class && SiteUtil.FILE_EXTENSIONS.equals(propertyName)) { 563 result = "all files"; 564 } 565 } 566 else if (SiteUtil.CHARSET.equals(propertyName)) { 567 result = "the charset property of the parent" 568 + " <a href=\"https://checkstyle.org/config.html#Checker\">" 569 + "Checker</a> module"; 570 } 571 572 return result; 573 } 574 575 /** 576 * Writes a table cell with the property since version. 577 * 578 * @param sink sink to write to. 579 * @param moduleJavadoc the Javadoc of the module. 580 * @param propertyJavadoc the Javadoc of the property containing the since version. 581 * @throws MacroExecutionException if an error occurs during retrieval of the since version. 582 */ 583 private static void writePropertySinceVersionCell(Sink sink, DetailNode moduleJavadoc, 584 DetailNode propertyJavadoc) 585 throws MacroExecutionException { 586 sink.rawText(ModuleJavadocParsingUtil.INDENT_LEVEL_14); 587 sink.tableCell(); 588 final String sinceVersion = SiteUtil.getPropertySinceVersion( 589 currentModuleName, moduleJavadoc, propertyJavadoc); 590 sink.text(sinceVersion); 591 sink.tableCell_(); 592 } 593}