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