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