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