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