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