1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.PrintWriter;
25  import java.nio.charset.StandardCharsets;
26  import java.util.function.Consumer;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
31  import com.puppycrawl.tools.checkstyle.api.DetailAST;
32  import com.puppycrawl.tools.checkstyle.api.DetailNode;
33  import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
34  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
35  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
36  import picocli.CommandLine;
37  import picocli.CommandLine.Command;
38  import picocli.CommandLine.Option;
39  import picocli.CommandLine.ParameterException;
40  import picocli.CommandLine.Parameters;
41  import picocli.CommandLine.ParseResult;
42  
43  /**
44   * This class is used internally in the build process to write a property file
45   * with short descriptions (the first sentences) of TokenTypes constants.
46   * Request: 724871
47   * For IDE plugins (like the eclipse plugin) it would be useful to have
48   * a programmatic access to the first sentence of the TokenType constants,
49   * so they can use them in their configuration gui.
50   * @noinspection UseOfSystemOutOrSystemErr, unused, ClassIndependentOfModule
51   */
52  public final class JavadocPropertiesGenerator {
53  
54      /**
55       * This regexp is used to extract the first sentence from the text.
56       * The end of the sentence is determined by the symbol "period", "exclamation mark" or
57       * "question mark", followed by a space or the end of the text.
58       */
59      private static final Pattern END_OF_SENTENCE_PATTERN = Pattern.compile("(.*?[.?!])(\\s|$)");
60  
61      /** Max width of the usage help message for this command. */
62      private static final int USAGE_HELP_WIDTH = 100;
63  
64      /**
65       * Don't create instance of this class, use the {@link #main(String[])} method instead.
66       */
67      private JavadocPropertiesGenerator() {
68      }
69  
70      /**
71       * TokenTypes.properties generator entry point.
72       * @param args the command line arguments
73       * @throws CheckstyleException if parser or lexer failed or if there is an IO problem
74       **/
75      public static void main(String... args) throws CheckstyleException {
76          final CliOptions cliOptions = new CliOptions();
77          final CommandLine cmd = new CommandLine(cliOptions).setUsageHelpWidth(USAGE_HELP_WIDTH);
78          try {
79              final ParseResult parseResult = cmd.parseArgs(args);
80              if (parseResult.isUsageHelpRequested()) {
81                  cmd.usage(System.out);
82              }
83              else {
84                  writePropertiesFile(cliOptions);
85              }
86          }
87          catch (ParameterException ex) {
88              System.err.println(ex.getMessage());
89              ex.getCommandLine().usage(System.err);
90          }
91      }
92  
93      /**
94       * Creates the .properties file from a .java file.
95       * @param options the user-specified options
96       * @throws CheckstyleException if a javadoc comment can not be parsed
97       */
98      private static void writePropertiesFile(CliOptions options) throws CheckstyleException {
99          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 }