001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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; 021 022import java.io.File; 023import java.io.IOException; 024import java.io.PrintWriter; 025import java.nio.charset.StandardCharsets; 026import java.util.function.Consumer; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029 030import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 031import com.puppycrawl.tools.checkstyle.api.DetailAST; 032import com.puppycrawl.tools.checkstyle.api.DetailNode; 033import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes; 034import com.puppycrawl.tools.checkstyle.api.TokenTypes; 035import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 036import com.puppycrawl.tools.checkstyle.utils.NullUtil; 037import picocli.CommandLine; 038import picocli.CommandLine.Command; 039import picocli.CommandLine.Option; 040import picocli.CommandLine.ParameterException; 041import picocli.CommandLine.Parameters; 042import picocli.CommandLine.ParseResult; 043 044/** 045 * This class is used internally in the build process to write a property file 046 * with short descriptions (the first sentences) of TokenTypes constants. 047 * Request: 724871 048 * For IDE plugins (like the eclipse plugin) it would be useful to have 049 * programmatic access to the first sentence of the TokenType constants, 050 * so they can use them in their configuration gui. 051 * 052 * @noinspection UseOfSystemOutOrSystemErr, unused, ClassIndependentOfModule 053 * @noinspectionreason UseOfSystemOutOrSystemErr - used for CLI output 054 * @noinspectionreason unused - main method is "unused" in code since it is driver method 055 * @noinspectionreason ClassIndependentOfModule - architecture of package requires this 056 */ 057public final class JavadocPropertiesGenerator { 058 059 /** 060 * This regexp is used to extract the first sentence from the text. 061 * The end of the sentence is determined by the symbol "period", "exclamation mark" or 062 * "question mark", followed by a space or the end of the text. 063 */ 064 private static final Pattern END_OF_SENTENCE_PATTERN = Pattern.compile( 065 "(([^.?!]|[.?!](?!\\s|$))*+[.?!])(\\s|$)"); 066 067 /** 068 * Don't create instance of this class, use the {@link #main(String[])} method instead. 069 */ 070 private JavadocPropertiesGenerator() { 071 } 072 073 /** 074 * TokenTypes.properties generator entry point. 075 * 076 * @param args the command line arguments 077 * @throws CheckstyleException if parser or lexer failed or if there is an IO problem 078 **/ 079 public static void main(String... args) throws CheckstyleException { 080 final CliOptions cliOptions = new CliOptions(); 081 final CommandLine cmd = new CommandLine(cliOptions); 082 try { 083 final ParseResult parseResult = cmd.parseArgs(args); 084 if (parseResult.isUsageHelpRequested()) { 085 cmd.usage(System.out); 086 } 087 else { 088 writePropertiesFile(cliOptions); 089 } 090 } 091 catch (ParameterException exc) { 092 System.err.println(exc.getMessage()); 093 exc.getCommandLine().usage(System.err); 094 } 095 } 096 097 /** 098 * Creates the .properties file from a .java file. 099 * 100 * @param options the user-specified options 101 * @throws CheckstyleException if a javadoc comment can not be parsed 102 */ 103 private static void writePropertiesFile(CliOptions options) throws CheckstyleException { 104 try (PrintWriter writer = new PrintWriter(options.outputFile, StandardCharsets.UTF_8)) { 105 final DetailAST top = JavaParser.parseFile(options.inputFile, 106 JavaParser.Options.WITH_COMMENTS).getFirstChild(); 107 final DetailAST objBlock = getClassBody(top); 108 if (objBlock != null) { 109 iteratePublicStaticIntFields(objBlock, writer::println); 110 } 111 } 112 catch (IOException exc) { 113 throw new CheckstyleException("Failed to write javadoc properties of '" 114 + options.inputFile + "' to '" + options.outputFile + "'", exc); 115 } 116 } 117 118 /** 119 * Walks over the type members and push the first javadoc sentence of every 120 * {@code public} {@code static} {@code int} field to the consumer. 121 * 122 * @param objBlock the OBJBLOCK of a class to iterate over its members 123 * @param consumer first javadoc sentence consumer 124 * @throws CheckstyleException if failed to parse a javadoc comment 125 */ 126 private static void iteratePublicStaticIntFields(DetailAST objBlock, Consumer<String> consumer) 127 throws CheckstyleException { 128 for (DetailAST member = objBlock.getFirstChild(); member != null; 129 member = member.getNextSibling()) { 130 if (isPublicStaticFinalIntField(member)) { 131 final DetailAST modifiers = member.findFirstToken(TokenTypes.MODIFIERS); 132 final String firstJavadocSentence = getFirstJavadocSentence(modifiers); 133 if (firstJavadocSentence != null) { 134 consumer.accept(getName(member) + "=" + firstJavadocSentence.trim()); 135 } 136 } 137 } 138 } 139 140 /** 141 * Finds the class body of the first class in the DetailAST. 142 * 143 * @param top AST to find the class body 144 * @return OBJBLOCK token if found; {@code null} otherwise 145 */ 146 private static DetailAST getClassBody(DetailAST top) { 147 DetailAST ast = top; 148 while (ast != null && ast.getType() != TokenTypes.CLASS_DEF) { 149 ast = ast.getNextSibling(); 150 } 151 DetailAST objBlock = null; 152 if (ast != null) { 153 objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK); 154 } 155 return objBlock; 156 } 157 158 /** 159 * Checks that the DetailAST is a {@code public} {@code static} {@code final} {@code int} field. 160 * 161 * @param ast to process 162 * @return {@code true} if matches; {@code false} otherwise 163 */ 164 private static boolean isPublicStaticFinalIntField(DetailAST ast) { 165 boolean result = ast.getType() == TokenTypes.VARIABLE_DEF; 166 if (result) { 167 final DetailAST type = ast.findFirstToken(TokenTypes.TYPE); 168 final DetailAST arrayDeclarator = type.getFirstChild().getNextSibling(); 169 result = arrayDeclarator == null 170 && type.getFirstChild().getType() == TokenTypes.LITERAL_INT; 171 if (result) { 172 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS); 173 result = modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null 174 && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null 175 && modifiers.findFirstToken(TokenTypes.FINAL) != null; 176 } 177 } 178 return result; 179 } 180 181 /** 182 * Extracts the name of an ast. 183 * 184 * @param ast to extract the name 185 * @return the text content of the inner {@code TokenTypes.IDENT} node 186 */ 187 private static String getName(DetailAST ast) { 188 return NullUtil.notNull(ast.findFirstToken(TokenTypes.IDENT)).getText(); 189 } 190 191 /** 192 * Extracts the first sentence as HTML formatted text from the comment of an DetailAST. 193 * The end of the sentence is determined by the symbol "period", "exclamation mark" or 194 * "question mark", followed by a space or the end of the text. Inline tags @code and @literal 195 * are converted to HTML code. 196 * 197 * @param ast to extract the first sentence 198 * @return the first sentence of the inner {@code TokenTypes.BLOCK_COMMENT_BEGIN} node 199 * or {@code null} if the first sentence is absent or malformed (does not end with period) 200 * @throws CheckstyleException if a javadoc comment can not be parsed or an unsupported inline 201 * tag found 202 */ 203 private static String getFirstJavadocSentence(DetailAST ast) throws CheckstyleException { 204 String firstSentence = null; 205 for (DetailAST child = ast.getFirstChild(); child != null && firstSentence == null; 206 child = child.getNextSibling()) { 207 // If there is an annotation, the javadoc comment will be a child of it. 208 if (child.getType() == TokenTypes.ANNOTATION) { 209 firstSentence = getFirstJavadocSentence(child); 210 } 211 // Otherwise, the javadoc comment will be right here. 212 else if (child.getType() == TokenTypes.BLOCK_COMMENT_BEGIN 213 && JavadocUtil.isJavadocComment(child)) { 214 final DetailNode tree = DetailNodeTreeStringPrinter.parseJavadocAsDetailNode(child); 215 firstSentence = getFirstJavadocSentence(tree); 216 } 217 } 218 return firstSentence; 219 } 220 221 /** 222 * Extracts the first sentence as HTML formatted text from a DetailNode. 223 * The end of the sentence is determined by the symbol "period", "exclamation mark" or 224 * "question mark", followed by a space or the end of the text. Inline tags @code and @literal 225 * are converted to HTML code. 226 * 227 * @param tree to extract the first sentence 228 * @return the first sentence of the node or {@code null} if the first sentence is absent or 229 * malformed (does not end with any of the end-of-sentence markers) 230 * @throws CheckstyleException if an unsupported inline tag found 231 */ 232 private static String getFirstJavadocSentence(DetailNode tree) throws CheckstyleException { 233 String firstSentence = null; 234 final StringBuilder builder = new StringBuilder(128); 235 236 for (DetailNode node = tree.getFirstChild(); node != null; 237 node = node.getNextSibling()) { 238 if (node.getType() == JavadocCommentsTokenTypes.TEXT) { 239 final Matcher matcher = END_OF_SENTENCE_PATTERN.matcher(node.getText()); 240 if (matcher.find()) { 241 // Commit the sentence if an end-of-sentence marker is found. 242 firstSentence = builder.append(matcher.group(1)).toString(); 243 break; 244 } 245 // Otherwise append the whole line and look for an end-of-sentence marker 246 // on the next line. 247 builder.append(node.getText()); 248 } 249 else if (node.getType() == JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG) { 250 formatInlineCodeTag(builder, node); 251 } 252 else { 253 formatHtmlElement(builder, node); 254 } 255 } 256 return firstSentence; 257 } 258 259 /** 260 * Converts inline code tag into HTML form. 261 * 262 * @param builder to append 263 * @param inlineTag to format 264 * @throws CheckstyleException if the inline javadoc tag is not a literal nor a code tag 265 */ 266 private static void formatInlineCodeTag(StringBuilder builder, DetailNode inlineTag) 267 throws CheckstyleException { 268 final int tagType = inlineTag.getFirstChild().getType(); 269 270 if (tagType != JavadocCommentsTokenTypes.LITERAL_INLINE_TAG 271 && tagType != JavadocCommentsTokenTypes.CODE_INLINE_TAG) { 272 throw new CheckstyleException("Unsupported inline tag " 273 + JavadocUtil.getTokenName(tagType)); 274 } 275 276 final boolean wrapWithCodeTag = tagType == JavadocCommentsTokenTypes.CODE_INLINE_TAG; 277 278 for (DetailNode node = inlineTag.getFirstChild().getFirstChild(); node != null; 279 node = node.getNextSibling()) { 280 switch (node.getType()) { 281 // The text to append. 282 case JavadocCommentsTokenTypes.TEXT -> { 283 if (wrapWithCodeTag) { 284 builder.append("<code>").append(node.getText().trim()).append("</code>"); 285 } 286 else { 287 builder.append(node.getText().trim()); 288 } 289 } 290 case JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_START, 291 JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_END, 292 JavadocCommentsTokenTypes.TAG_NAME -> { 293 // skip tag markers 294 } 295 default -> throw new CheckstyleException("Unsupported child in the inline tag " 296 + JavadocUtil.getTokenName(node.getType())); 297 } 298 } 299 } 300 301 /** 302 * Concatenates the HTML text from AST of a JavadocTokenTypes.HTML_ELEMENT. 303 * 304 * @param builder to append 305 * @param node to format 306 */ 307 private static void formatHtmlElement(StringBuilder builder, DetailNode node) { 308 switch (node.getType()) { 309 case JavadocCommentsTokenTypes.TAG_OPEN, 310 JavadocCommentsTokenTypes.TAG_CLOSE, 311 JavadocCommentsTokenTypes.TAG_SLASH, 312 JavadocCommentsTokenTypes.TAG_SLASH_CLOSE, 313 JavadocCommentsTokenTypes.TAG_NAME, 314 JavadocCommentsTokenTypes.TEXT -> 315 builder.append(node.getText()); 316 317 default -> { 318 for (DetailNode child = node.getFirstChild(); child != null; 319 child = child.getNextSibling()) { 320 formatHtmlElement(builder, child); 321 } 322 } 323 } 324 325 } 326 327 /** 328 * Helper class encapsulating the command line options and positional parameters. 329 */ 330 @Command(name = "java com.puppycrawl.tools.checkstyle.JavadocPropertiesGenerator", 331 mixinStandardHelpOptions = true) 332 private static final class CliOptions { 333 334 /** 335 * The command line option to specify the output file. 336 */ 337 @Option(names = "--destfile", required = true, description = "The output file.") 338 private File outputFile; 339 340 /** 341 * The command line positional parameter to specify the input file. 342 */ 343 @Parameters(index = "0", description = "The input file.") 344 private File inputFile; 345 } 346}