View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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   * programmatic access to the first sentence of the TokenType constants,
49   * so they can use them in their configuration gui.
50   *
51   * @noinspection UseOfSystemOutOrSystemErr, unused, ClassIndependentOfModule
52   * @noinspectionreason UseOfSystemOutOrSystemErr - used for CLI output
53   * @noinspectionreason unused - main method is "unused" in code since it is driver method
54   * @noinspectionreason ClassIndependentOfModule - architecture of package requires this
55   */
56  public final class JavadocPropertiesGenerator {
57  
58      /**
59       * This regexp is used to extract the first sentence from the text.
60       * The end of the sentence is determined by the symbol "period", "exclamation mark" or
61       * "question mark", followed by a space or the end of the text.
62       */
63      private static final Pattern END_OF_SENTENCE_PATTERN = Pattern.compile(
64          "(([^.?!]|[.?!](?!\\s|$))*+[.?!])(\\s|$)");
65  
66      /**
67       * Don't create instance of this class, use the {@link #main(String[])} method instead.
68       */
69      private JavadocPropertiesGenerator() {
70      }
71  
72      /**
73       * TokenTypes.properties generator entry point.
74       *
75       * @param args the command line arguments
76       * @throws CheckstyleException if parser or lexer failed or if there is an IO problem
77       **/
78      public static void main(String... args) throws CheckstyleException {
79          final CliOptions cliOptions = new CliOptions();
80          final CommandLine cmd = new CommandLine(cliOptions);
81          try {
82              final ParseResult parseResult = cmd.parseArgs(args);
83              if (parseResult.isUsageHelpRequested()) {
84                  cmd.usage(System.out);
85              }
86              else {
87                  writePropertiesFile(cliOptions);
88              }
89          }
90          catch (ParameterException ex) {
91              System.err.println(ex.getMessage());
92              ex.getCommandLine().usage(System.err);
93          }
94      }
95  
96      /**
97       * Creates the .properties file from a .java file.
98       *
99       * @param options the user-specified options
100      * @throws CheckstyleException if a javadoc comment can not be parsed
101      */
102     private static void writePropertiesFile(CliOptions options) throws CheckstyleException {
103         try (PrintWriter writer = new PrintWriter(options.outputFile, StandardCharsets.UTF_8)) {
104             final DetailAST top = JavaParser.parseFile(options.inputFile,
105                     JavaParser.Options.WITH_COMMENTS).getFirstChild();
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 '"
113                     + options.inputFile + "' to '" + options.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      *
121      * @param objBlock the OBJBLOCK of a class to iterate over its members
122      * @param consumer first javadoc sentence consumer
123      * @throws CheckstyleException if failed to parse a javadoc comment
124      */
125     private static void iteratePublicStaticIntFields(DetailAST objBlock, Consumer<String> consumer)
126             throws CheckstyleException {
127         for (DetailAST member = objBlock.getFirstChild(); member != null;
128                 member = member.getNextSibling()) {
129             if (isPublicStaticFinalIntField(member)) {
130                 final DetailAST modifiers = member.findFirstToken(TokenTypes.MODIFIERS);
131                 final String firstJavadocSentence = getFirstJavadocSentence(modifiers);
132                 if (firstJavadocSentence != null) {
133                     consumer.accept(getName(member) + "=" + firstJavadocSentence.trim());
134                 }
135             }
136         }
137     }
138 
139     /**
140      * Finds the class body of the first class in the DetailAST.
141      *
142      * @param top AST to find the class body
143      * @return OBJBLOCK token if found; {@code null} otherwise
144      */
145     private static DetailAST getClassBody(DetailAST top) {
146         DetailAST ast = top;
147         while (ast != null && ast.getType() != TokenTypes.CLASS_DEF) {
148             ast = ast.getNextSibling();
149         }
150         DetailAST objBlock = null;
151         if (ast != null) {
152             objBlock = ast.findFirstToken(TokenTypes.OBJBLOCK);
153         }
154         return objBlock;
155     }
156 
157     /**
158      * Checks that the DetailAST is a {@code public} {@code static} {@code final} {@code int} field.
159      *
160      * @param ast to process
161      * @return {@code true} if matches; {@code false} otherwise
162      */
163     private static boolean isPublicStaticFinalIntField(DetailAST ast) {
164         boolean result = ast.getType() == TokenTypes.VARIABLE_DEF;
165         if (result) {
166             final DetailAST type = ast.findFirstToken(TokenTypes.TYPE);
167             final DetailAST arrayDeclarator = type.getFirstChild().getNextSibling();
168             result = arrayDeclarator == null
169                     && type.getFirstChild().getType() == TokenTypes.LITERAL_INT;
170             if (result) {
171                 final DetailAST modifiers = ast.findFirstToken(TokenTypes.MODIFIERS);
172                 result = modifiers.findFirstToken(TokenTypes.LITERAL_PUBLIC) != null
173                     && modifiers.findFirstToken(TokenTypes.LITERAL_STATIC) != null
174                     && modifiers.findFirstToken(TokenTypes.FINAL) != null;
175             }
176         }
177         return result;
178     }
179 
180     /**
181      * Extracts the name of an ast.
182      *
183      * @param ast to extract the name
184      * @return the text content of the inner {@code TokenTypes.IDENT} node
185      */
186     private static String getName(DetailAST ast) {
187         return ast.findFirstToken(TokenTypes.IDENT).getText();
188     }
189 
190     /**
191      * Extracts the first sentence as HTML formatted text from the comment of an DetailAST.
192      * The end of the sentence is determined by the symbol "period", "exclamation mark" or
193      * "question mark", followed by a space or the end of the text. Inline tags @code and @literal
194      * are converted to HTML code.
195      *
196      * @param ast to extract the first sentence
197      * @return the first sentence of the inner {@code TokenTypes.BLOCK_COMMENT_BEGIN} node
198      *      or {@code null} if the first sentence is absent or malformed (does not end with period)
199      * @throws CheckstyleException if a javadoc comment can not be parsed or an unsupported inline
200      *      tag found
201      */
202     private static String getFirstJavadocSentence(DetailAST ast) throws CheckstyleException {
203         String firstSentence = null;
204         for (DetailAST child = ast.getFirstChild(); child != null && firstSentence == null;
205                 child = child.getNextSibling()) {
206             // If there is an annotation, the javadoc comment will be a child of it.
207             if (child.getType() == TokenTypes.ANNOTATION) {
208                 firstSentence = getFirstJavadocSentence(child);
209             }
210             // Otherwise, the javadoc comment will be right here.
211             else if (child.getType() == TokenTypes.BLOCK_COMMENT_BEGIN
212                     && JavadocUtil.isJavadocComment(child)) {
213                 final DetailNode tree = DetailNodeTreeStringPrinter.parseJavadocAsDetailNode(child);
214                 firstSentence = getFirstJavadocSentence(tree);
215             }
216         }
217         return firstSentence;
218     }
219 
220     /**
221      * Extracts the first sentence as HTML formatted text from a DetailNode.
222      * The end of the sentence is determined by the symbol "period", "exclamation mark" or
223      * "question mark", followed by a space or the end of the text. Inline tags @code and @literal
224      * are converted to HTML code.
225      *
226      * @param tree to extract the first sentence
227      * @return the first sentence of the node or {@code null} if the first sentence is absent or
228      *      malformed (does not end with any of the end-of-sentence markers)
229      * @throws CheckstyleException if an unsupported inline tag found
230      */
231     private static String getFirstJavadocSentence(DetailNode tree) throws CheckstyleException {
232         String firstSentence = null;
233         final StringBuilder builder = new StringBuilder(128);
234         for (DetailNode node : tree.getChildren()) {
235             if (node.getType() == JavadocTokenTypes.TEXT) {
236                 final Matcher matcher = END_OF_SENTENCE_PATTERN.matcher(node.getText());
237                 if (matcher.find()) {
238                     // Commit the sentence if an end-of-sentence marker is found.
239                     firstSentence = builder.append(matcher.group(1)).toString();
240                     break;
241                 }
242                 // Otherwise append the whole line and look for an end-of-sentence marker
243                 // on the next line.
244                 builder.append(node.getText());
245             }
246             else if (node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
247                 formatInlineCodeTag(builder, node);
248             }
249             else {
250                 formatHtmlElement(builder, node);
251             }
252         }
253         return firstSentence;
254     }
255 
256     /**
257      * Converts inline code tag into HTML form.
258      *
259      * @param builder to append
260      * @param inlineTag to format
261      * @throws CheckstyleException if the inline javadoc tag is not a literal nor a code tag
262      */
263     private static void formatInlineCodeTag(StringBuilder builder, DetailNode inlineTag)
264             throws CheckstyleException {
265         boolean wrapWithCodeTag = false;
266         for (DetailNode node : inlineTag.getChildren()) {
267             switch (node.getType()) {
268                 case JavadocTokenTypes.CODE_LITERAL:
269                     wrapWithCodeTag = true;
270                     break;
271                 // The text to append.
272                 case JavadocTokenTypes.TEXT:
273                     if (wrapWithCodeTag) {
274                         builder.append("<code>").append(node.getText()).append("</code>");
275                     }
276                     else {
277                         builder.append(node.getText());
278                     }
279                     break;
280                 // Empty content tags.
281                 case JavadocTokenTypes.LITERAL_LITERAL:
282                 case JavadocTokenTypes.JAVADOC_INLINE_TAG_START:
283                 case JavadocTokenTypes.JAVADOC_INLINE_TAG_END:
284                 case JavadocTokenTypes.WS:
285                     break;
286                 default:
287                     throw new CheckstyleException("Unsupported inline tag "
288                         + JavadocUtil.getTokenName(node.getType()));
289             }
290         }
291     }
292 
293     /**
294      * Concatenates the HTML text from AST of a JavadocTokenTypes.HTML_ELEMENT.
295      *
296      * @param builder to append
297      * @param node to format
298      */
299     private static void formatHtmlElement(StringBuilder builder, DetailNode node) {
300         switch (node.getType()) {
301             case JavadocTokenTypes.START:
302             case JavadocTokenTypes.HTML_TAG_NAME:
303             case JavadocTokenTypes.END:
304             case JavadocTokenTypes.TEXT:
305             case JavadocTokenTypes.SLASH:
306                 builder.append(node.getText());
307                 break;
308             default:
309                 for (DetailNode child : node.getChildren()) {
310                     formatHtmlElement(builder, child);
311                 }
312                 break;
313         }
314     }
315 
316     /**
317      * Helper class encapsulating the command line options and positional parameters.
318      */
319     @Command(name = "java com.puppycrawl.tools.checkstyle.JavadocPropertiesGenerator",
320             mixinStandardHelpOptions = true)
321     private static final class CliOptions {
322 
323         /**
324          * The command line option to specify the output file.
325          */
326         @Option(names = "--destfile", required = true, description = "The output file.")
327         private File outputFile;
328 
329         /**
330          * The command line positional parameter to specify the input file.
331          */
332         @Parameters(index = "0", description = "The input file.")
333         private File inputFile;
334     }
335 }