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